Schema 1.3.0: meta.find_series_registry_mismatches()

An occasional consistency check (not for connect time) comparing
meta.series with the registered series schemas: non-series tables, views,
sequences and functions inside a series schema; registered series whose
table is missing; series tables without a timestamp column or its BRIN
index. Report only; user schemas are never examined. find_schema_problems()
fingerprints its body like every other procedure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
peter 2026-10-06 11:48:26 +01:00
parent 4c067bc4b9
commit eb0f93e032
4 changed files with 104 additions and 7 deletions

View file

@ -1,10 +1,16 @@
-- ============================================================================
-- Schema self-check: meta.find_schema_problems()
-- Database self-checks, callable by every client (Python, C++, psql)
-- ============================================================================
-- Answers one question for every client (Python, C++, psql):
-- is this database complete and as the DDL defines it?
--
-- SELECT * FROM meta.find_schema_problems(); -- zero rows = usable
-- meta.find_schema_problems() -- on connect: is the database complete and as
-- the DDL defines it? zero rows = usable
--
-- meta.find_series_registry_mismatches() -- occasional consistency check:
-- do the series schemas and meta.series agree? (defined at the end)
--
-- --------------------------------------------------------------------------
-- meta.find_schema_problems()
-- --------------------------------------------------------------------------
--
-- Returns one row per problem: (object, problem). Read-only.
--
@ -268,6 +274,7 @@ BEGIN
('meta.drop_all_series', 'fc404a746d91578860bea0d89b166bc7'),
('meta.drop_series', '6f2a919de965b87521be68252deee0e2'),
('meta.drop_series_if_exists', 'aa1b6ab64d2d3b9ca19ed9033f129524'),
('meta.find_series_registry_mismatches', '6dc9ecd2b7656b91a8b410c78a19fa2b'),
('meta.prevent_direct_drop', '1d38cbad0caba72a5104da42e3030929'),
('meta.read_series_batch', '93ef12652fd22b14d210971b537de736'),
('meta.read_series_batch_cursor', '2ed7d17f545fdfb9d6af577a051699e1'),
@ -337,3 +344,93 @@ BEGIN
END IF;
END;
$function$;
-- --------------------------------------------------------------------------
-- meta.find_series_registry_mismatches()
-- --------------------------------------------------------------------------
-- Occasional consistency check -- NOT run on connect. Compares the series
-- registry (meta.series) with what actually exists in the REGISTERED series
-- schemas (meta.series_schemas). User schemas are never examined.
--
-- SELECT * FROM meta.find_series_registry_mismatches(); -- zero rows = consistent
--
-- One (object, problem) row per finding:
-- * a table, view, materialized view, foreign table or sequence in a
-- series schema that is not a registered series (an orphaned series table,
-- or a user table in the wrong place) -- harmless to the clients;
-- * a function in a series schema -- harmless;
-- * a series registered in meta.series whose table does not exist --
-- every operation on that series fails;
-- * a series table without a "timestamp timestamp without time zone NOT
-- NULL" column, or without a BRIN index on it.
-- Read-only. It reports; deciding what to do with a finding is up to the user.
CREATE OR REPLACE FUNCTION meta.find_series_registry_mismatches()
RETURNS TABLE(object text, problem text)
LANGUAGE plpgsql
STABLE
AS $function$
BEGIN
-- Relations in series schemas that are not registered series -----------
RETURN QUERY
SELECT n.nspname || '.' || c.relname,
CASE c.relkind
WHEN 'r' THEN 'table' WHEN 'p' THEN 'table' WHEN 'v' THEN 'view'
WHEN 'm' THEN 'materialized view' WHEN 'f' THEN 'foreign table'
ELSE 'sequence' END
|| ' in a series schema but not registered in meta.series'
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN meta.series_schemas r ON r.name = n.nspname
WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f', 'S')
AND NOT EXISTS (SELECT 1 FROM meta.series m
WHERE m.db_schema = n.nspname AND m.table_name = c.relname)
ORDER BY 1;
RETURN QUERY
SELECT n.nspname || '.' || p.proname, 'function in a series schema'
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
JOIN meta.series_schemas r ON r.name = n.nspname
ORDER BY 1;
-- Registered series whose table is missing ------------------------------
RETURN QUERY
SELECT m.db_schema || '.' || m.table_name,
'registered in meta.series but the table does not exist'
FROM meta.series m
WHERE to_regclass(format('%I.%I', m.db_schema, m.table_name)) IS NULL
ORDER BY 1;
-- Series tables without a proper timestamp column or its BRIN index -----
RETURN QUERY
SELECT m.db_schema || '.' || m.table_name,
CASE WHEN a.attname IS NULL THEN 'no "timestamp" column'
ELSE '"timestamp" is ' || format_type(a.atttypid, a.atttypmod)
|| CASE WHEN a.attnotnull THEN ' NOT NULL' ELSE '' END
|| ', expected timestamp without time zone NOT NULL' END
FROM meta.series m
CROSS JOIN LATERAL (SELECT to_regclass(format('%I.%I', m.db_schema, m.table_name)) AS rel) t
LEFT JOIN pg_attribute a ON a.attrelid = t.rel AND a.attname = 'timestamp'
AND a.attnum > 0 AND NOT a.attisdropped
WHERE t.rel IS NOT NULL
AND (a.attname IS NULL
OR format_type(a.atttypid, a.atttypmod) <> 'timestamp without time zone'
OR NOT a.attnotnull)
ORDER BY 1;
RETURN QUERY
SELECT m.db_schema || '.' || m.table_name, 'no BRIN index on "timestamp"'
FROM meta.series m
CROSS JOIN LATERAL (SELECT to_regclass(format('%I.%I', m.db_schema, m.table_name)) AS rel) t
JOIN pg_attribute a ON a.attrelid = t.rel AND a.attname = 'timestamp'
AND a.attnum > 0 AND NOT a.attisdropped
WHERE NOT EXISTS (
SELECT 1 FROM pg_index i
JOIN pg_class ic ON ic.oid = i.indexrelid
JOIN pg_am am ON am.oid = ic.relam
WHERE i.indrelid = t.rel AND am.amname = 'brin' AND i.indkey[0] = a.attnum)
ORDER BY 1;
END;
$function$;

View file

@ -5,4 +5,4 @@
-- they were built for (Python: DB_SCHEMA_VERSION in tsapi_pgdb/_version.py).
INSERT INTO meta.schema_version (version, description)
VALUES ('1.2.0', 'meta.series_schemas registry; lower-case table name check');
VALUES ('1.3.0', 'meta.find_series_registry_mismatches()');