diff --git a/README.md b/README.md index 2ca361e..5d02ae8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ timeseries database. Both client libraries target the schema defined here: Neither project should keep its own copy of the DDL. When the schema changes, it changes **here**, and both projects re-provision / re-test against it. -Current schema version: **1.2.0** (see [`VERSION`](VERSION); recorded at runtime +Current schema version: **1.3.0** (see [`VERSION`](VERSION); recorded at runtime in the `meta.schema_version` table). --- diff --git a/VERSION b/VERSION index 26aaba0..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/sql/09_schema_check.sql b/sql/09_schema_check.sql index 8e6d46f..951d224 100644 --- a/sql/09_schema_check.sql +++ b/sql/09_schema_check.sql @@ -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$; diff --git a/sql/10_schema_version.sql b/sql/10_schema_version.sql index c6cd3b4..7fee6e6 100644 --- a/sql/10_schema_version.sql +++ b/sql/10_schema_version.sql @@ -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()');