For the coverage-aware holiday-calendar API in TSAPI core (db_base holiday_calendar__get / __set_coverage / __set_metadata / __upsert). - meta.holiday_calendars (name, description, covered_from, covered_to, source, updated_at): a calendar exists when it has a row here, not when it has holidays (retires the 9999-12-31 sentinel row). covered_from/to: the range over which the closure list is complete; both set or both NULL, from <= to (holiday_calendars_coverage_check). - Calendar names are citext in every table (registry, holidays.calendar_name, exchanges.default_holiday_calendar, instruments.holiday_calendar): stored as given, unique and matched case-insensitively -- also in the foreign keys -- as the native TSAPI backend does with COLLATE NOCASE. New extension citext (00_extensions.sql). Name check: non-empty, trimmed, <= 50 characters. - FKs: holidays -> registry ON DELETE CASCADE; exchanges/instruments -> registry ON DELETE RESTRICT (a calendar in use cannot be dropped); ON UPDATE CASCADE everywhere. - meta.series.num_records dropped. Only some write paths maintained it (never C++), it sometimes held an estimate, and it was NULL for a series never written to. Clients ask the table: count(*), or SELECT EXISTS (SELECT 1 FROM <table>) for "is it empty?". - find_schema_problems() covers the new table, columns, constraints and extension, and runs with search_path = pg_catalog so format_type() reports 'public.citext' whatever the caller's search_path. Verified on a fresh database (PG 17): run_setup.py installs it and find_schema_problems() returns zero rows, also for a caller without public on the search_path; constraint behaviour checked by hand (case-insensitive duplicate refused, FK matches any spelling, RESTRICT and CASCADE, coverage and name checks). Re-provision databases (no migration). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
461 lines
23 KiB
PL/PgSQL
461 lines
23 KiB
PL/PgSQL
-- ============================================================================
|
|
-- Database self-checks, callable by every client (Python, C++, psql)
|
|
-- ============================================================================
|
|
--
|
|
-- 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), in a stable order
|
|
-- (section by section, each sorted). Read-only.
|
|
--
|
|
-- The expectations below mirror 00..08. When any of those files changes,
|
|
-- update this file in the same commit. The release tests (a freshly
|
|
-- provisioned database must return zero rows) fail until you do.
|
|
--
|
|
-- Function bodies are compared by fingerprint: md5 of the body with runs of
|
|
-- whitespace collapsed, so line endings and re-indentation do not count as a
|
|
-- change. To refresh the fingerprints after editing a function:
|
|
--
|
|
-- SELECT n.nspname || '.' || p.proname,
|
|
-- md5(btrim(regexp_replace(p.prosrc, '\s+', ' ', 'g')))
|
|
-- FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
-- WHERE n.nspname IN ('meta', 'log') ORDER BY 1;
|
|
--
|
|
-- The schema VERSION string is not compared here: each client compares
|
|
-- meta.schema_version with the version it was built for. This function only
|
|
-- reports a missing version row.
|
|
--
|
|
-- The function runs with search_path = pg_catalog, so format_type() spells
|
|
-- extension types schema-qualified ('public.citext') whatever the caller's
|
|
-- search_path is.
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION meta.find_schema_problems()
|
|
RETURNS TABLE(object text, problem text)
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SET search_path = pg_catalog, pg_temp
|
|
AS $function$
|
|
BEGIN
|
|
-- Schemas and extension -------------------------------------------------
|
|
RETURN QUERY
|
|
SELECT 'schema ' || s, 'missing'
|
|
FROM unnest(ARRAY['meta', 'series', 'log']) AS s
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = s)
|
|
ORDER BY 1;
|
|
|
|
RETURN QUERY
|
|
SELECT 'extension ' || e, 'missing'
|
|
FROM unnest(ARRAY['citext', 'pg_trgm']) AS e
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = e)
|
|
ORDER BY 1;
|
|
|
|
RETURN QUERY
|
|
SELECT 'sequence ' || s, 'missing'
|
|
FROM unnest(ARRAY[
|
|
'meta._table_meta__table_id_seq',
|
|
'log.event_log_id_seq',
|
|
'log.pending_changes_id_seq',
|
|
'log.script_log_id_seq'
|
|
]) AS s
|
|
WHERE to_regclass(s) IS NULL
|
|
ORDER BY 1;
|
|
|
|
-- Tables and columns ----------------------------------------------------
|
|
RETURN QUERY
|
|
WITH e(tbl, col, typ, not_null) AS (VALUES
|
|
('log.pending_changes', 'id', 'bigint', true),
|
|
('log.pending_changes', 'timestamp', 'timestamp with time zone', false),
|
|
('log.pending_changes', 'series_name', 'text', true),
|
|
('log.pending_changes', 'db_schema', 'text', true),
|
|
('log.pending_changes', 'op_type', 'text', true),
|
|
('log.pending_changes', 'db_user', 'text', false),
|
|
('log.pending_changes', 'details', 'jsonb', false),
|
|
('log.event_log', 'id', 'bigint', true),
|
|
('log.event_log', 'timestamp', 'timestamp with time zone', false),
|
|
('log.event_log', 'series_name', 'text', true),
|
|
('log.event_log', 'db_schema', 'text', true),
|
|
('log.event_log', 'event_type', 'text', true),
|
|
('log.event_log', 'details', 'jsonb', false),
|
|
('log.event_log', 'source', 'text', false),
|
|
('log.event_log', 'db_user', 'text', false),
|
|
('meta.currencies', 'code', 'character varying(3)', true),
|
|
('meta.currencies', 'name', 'character varying(100)', true),
|
|
('meta.currencies', 'symbol', 'character varying(10)', false),
|
|
('meta.currencies', 'decimal_places', 'integer', false),
|
|
('meta.currencies', 'notes', 'text', false),
|
|
('meta.exchanges', 'id', 'character varying(12)', true),
|
|
('meta.exchanges', 'name', 'character varying(50)', false),
|
|
('meta.exchanges', 'timezone', 'character varying(35)', false),
|
|
('meta.exchanges', 'currency', 'character varying(3)', false),
|
|
('meta.exchanges', 'default_sec_type', 'character varying(18)', false),
|
|
('meta.exchanges', 'default_session', 'character varying(40)', false),
|
|
('meta.exchanges', 'default_holiday_calendar', 'public.citext', false),
|
|
('meta.exchanges', 'default_tick_size', 'double precision', false),
|
|
('meta.exchanges', 'default_tick_value', 'double precision', false),
|
|
('meta.exchanges', 'default_extended_hours', 'boolean', false),
|
|
('meta.exchanges', 'country', 'character varying(3)', false),
|
|
('meta.holiday_calendars', 'name', 'public.citext', true),
|
|
('meta.holiday_calendars', 'description', 'text', false),
|
|
('meta.holiday_calendars', 'covered_from', 'date', false),
|
|
('meta.holiday_calendars', 'covered_to', 'date', false),
|
|
('meta.holiday_calendars', 'source', 'text', false),
|
|
('meta.holiday_calendars', 'updated_at', 'timestamp with time zone', true),
|
|
('meta.holidays', 'calendar_name', 'public.citext', true),
|
|
('meta.holidays', 'date', 'date', true),
|
|
('meta.holidays', 'description', 'character varying(255)', false),
|
|
('meta.holidays', 'early_close', 'boolean', true),
|
|
('meta.instruments', 'symbol', 'character varying(12)', true),
|
|
('meta.instruments', 'exchange', 'character varying(12)', true),
|
|
('meta.instruments', 'name', 'character varying(50)', true),
|
|
('meta.instruments', 'type', 'character varying(10)', false),
|
|
('meta.instruments', 'tick_size', 'double precision', false),
|
|
('meta.instruments', 'tick_value', 'double precision', false),
|
|
('meta.instruments', 'expiry', 'character varying(20)', false),
|
|
('meta.instruments', 'strike', 'double precision', false),
|
|
('meta.instruments', 'currency', 'character varying(3)', false),
|
|
('meta.instruments', 'session', 'character varying(40)', false),
|
|
('meta.instruments', 'holiday_calendar', 'public.citext', false),
|
|
('meta.instruments', 'sec_id', 'character varying(50)', false),
|
|
('meta.instruments', 'sec_id_type', 'character varying(20)', false),
|
|
('meta.instruments', 'description', 'character varying(255)', false),
|
|
('meta.instruments', 'country', 'character varying(3)', false),
|
|
('meta.instruments', 'extended_hours', 'boolean', false),
|
|
('meta.instruments', 'sector', 'character varying(50)', false),
|
|
('meta.instruments', 'industry', 'character varying(100)', false),
|
|
('meta.instruments', 'category', 'character varying(50)', false),
|
|
('meta.instruments', 'subcategory', 'character varying(50)', false),
|
|
('meta.instruments', 'asset_class', 'character varying(30)', false),
|
|
('meta.instruments', 'geography', 'character varying(30)', false),
|
|
('meta.instruments', 'underlying', 'character varying(30)', false),
|
|
('meta.series_schemas', 'name', 'text', true),
|
|
('meta.series_schemas', 'description', 'text', false),
|
|
('meta.series_schemas', 'created_at', 'timestamp with time zone', true),
|
|
('meta.series_schemas', 'created_by', 'text', true),
|
|
('meta.security_types', 'code', 'character varying(10)', true),
|
|
('meta.security_types', 'name', 'character varying(50)', true),
|
|
('meta.security_types', 'description', 'text', false),
|
|
('meta.security_types', 'notes', 'text', false),
|
|
('meta.schema_version', 'version', 'character varying(20)', true),
|
|
('meta.schema_version', 'installed_at', 'timestamp with time zone', true),
|
|
('meta.schema_version', 'description', 'text', false),
|
|
('meta.series', 'table_id', 'integer', true),
|
|
('meta.series', 'db_schema', 'character varying(70)', true),
|
|
('meta.series', 'table_name', 'character varying(70)', true),
|
|
('meta.series', 'type', 'character varying(30)', false),
|
|
('meta.series', 'description', 'character varying(255)', false),
|
|
('meta.series', 'column_schema', 'text', false),
|
|
('meta.series', 'symbol', 'character varying(30)', false),
|
|
('meta.series', 'root_symbol', 'character varying(30)', false),
|
|
('meta.series', 'exchange', 'character varying(30)', false),
|
|
('meta.series', 'frequency', 'character varying(30)', false),
|
|
('meta.series', 'session', 'text', false),
|
|
('meta.series', 'sec_type', 'character varying(30)', false),
|
|
('meta.series', 'sec_id', 'character varying(30)', false),
|
|
('meta.series', 'sec_id_type', 'character varying(30)', false),
|
|
('meta.series', 'create_timestamp', 'timestamp without time zone', false),
|
|
('meta.series', 'first_timestamp', 'timestamp without time zone', false),
|
|
('meta.series', 'last_timestamp', 'timestamp without time zone', false),
|
|
('meta.series', 'bar_type', 'character varying(30)', false),
|
|
('meta.series', 'feed', 'character varying(30)', false),
|
|
('meta.series', 'currency', 'character varying(30)', false),
|
|
('meta.series', 'expiry', 'character varying(30)', false),
|
|
('meta.series', 'roll_type', 'character varying(30)', false),
|
|
('meta.series', 'feed_type', 'character varying(30)', false),
|
|
('meta.series', 'user_attr', 'text', false),
|
|
('meta.series', 'user_attr_2', 'text', false),
|
|
('meta.series', 'script', 'text', false),
|
|
('meta.series', 'notes', 'text', false),
|
|
('meta.series', 'timezone', 'character varying(50)', false),
|
|
('meta.series', 'category', 'character varying(30)', false),
|
|
('meta.series', 'subcategory', 'character varying(30)', false),
|
|
('meta.series', 'json', 'jsonb', false),
|
|
('meta.series', 'needs_reindex', 'boolean', true),
|
|
('meta.sessions', 'name', 'character varying(50)', true),
|
|
('meta.sessions', 'definition', 'text', true),
|
|
('meta.sessions', 'exchange_id', 'character varying(12)', true),
|
|
('meta.timezones', 'iana_name', 'character varying(50)', true),
|
|
('meta.timezones', 'display_name', 'character varying(100)', true),
|
|
('meta.timezones', 'description', 'text', false),
|
|
('meta.timezones', 'region', 'character varying(50)', false),
|
|
('meta.timezones', 'utc_offset_hours', 'real', false),
|
|
('meta.timezones', 'notes', 'text', false),
|
|
('log.script_log', 'id', 'integer', true),
|
|
('log.script_log', 'timestamp', 'timestamp with time zone', true),
|
|
('log.script_log', 'level', 'character varying(10)', true),
|
|
('log.script_log', 'message', 'text', true),
|
|
('log.script_log', 'script_name', 'character varying(100)', false),
|
|
('log.script_log', 'series_name', 'character varying(255)', false),
|
|
('log.script_log', 'session_id', 'uuid', false),
|
|
('log.script_log', 'details', 'jsonb', false)
|
|
),
|
|
a AS (
|
|
SELECT e.*, x.attname IS NOT NULL AS present,
|
|
format_type(x.atttypid, x.atttypmod) AS got_typ, x.attnotnull AS got_not_null
|
|
FROM e
|
|
LEFT JOIN pg_attribute x ON x.attrelid = to_regclass(e.tbl) AND x.attname = e.col
|
|
AND x.attnum > 0 AND NOT x.attisdropped
|
|
WHERE to_regclass(e.tbl) IS NOT NULL
|
|
),
|
|
found(sort_key, object, problem) AS (
|
|
SELECT 1, 'table ' || tbl, 'missing'
|
|
FROM (SELECT DISTINCT tbl FROM e WHERE to_regclass(tbl) IS NULL) m
|
|
UNION ALL
|
|
SELECT 2, 'table ' || tbl, 'missing columns: ' || string_agg(col, ', ')
|
|
FROM a WHERE NOT present GROUP BY tbl
|
|
UNION ALL
|
|
SELECT 3, 'table ' || tbl, col || ' is ' || got_typ || ', expected ' || typ
|
|
FROM a WHERE present AND got_typ <> typ
|
|
UNION ALL
|
|
SELECT 4, 'table ' || tbl,
|
|
col || ' is ' || CASE WHEN got_not_null THEN 'NOT NULL' ELSE 'nullable' END
|
|
|| ', expected ' || CASE WHEN not_null THEN 'NOT NULL' ELSE 'nullable' END
|
|
FROM a WHERE present AND got_not_null <> not_null
|
|
)
|
|
SELECT f.object, f.problem FROM found f ORDER BY f.object, f.sort_key, f.problem;
|
|
|
|
-- Constraints and indexes (only on tables that exist) -------------------
|
|
RETURN QUERY
|
|
SELECT 'constraint ' || c.tbl || '.' || c.name, 'missing'
|
|
FROM (VALUES
|
|
('log.pending_changes', 'pending_changes_pkey'),
|
|
('log.event_log', 'event_log_pkey'),
|
|
('meta.holidays', 'holidays_pkey'),
|
|
('meta.holidays', 'holidays_calendar_fkey'),
|
|
('meta.holiday_calendars', 'holiday_calendars_pkey'),
|
|
('meta.holiday_calendars', 'holiday_calendars_name_check'),
|
|
('meta.holiday_calendars', 'holiday_calendars_coverage_check'),
|
|
('log.script_log', 'script_log_pkey'),
|
|
('meta.instruments', 'instruments_pkey'),
|
|
('meta.instruments', 'instruments_holiday_calendar_fkey'),
|
|
('meta.exchanges', 'exchanges_pkey'),
|
|
('meta.exchanges', 'exchanges_holiday_calendar_fkey'),
|
|
('meta.sessions', 'sessions_pkey'),
|
|
('meta.series', 'series_pkey'),
|
|
('meta.series', 'series_schema_table_key'),
|
|
('meta.series', 'series_db_schema_fkey'),
|
|
('meta.series', 'series_table_name_check'),
|
|
('meta.series_schemas', 'series_schemas_pkey'),
|
|
('meta.series_schemas', 'series_schemas_name_check'),
|
|
('meta.currencies', 'currencies_pkey'),
|
|
('meta.security_types', 'security_types_pkey'),
|
|
('meta.timezones', 'timezones_pkey')
|
|
) AS c(tbl, name)
|
|
WHERE to_regclass(c.tbl) IS NOT NULL
|
|
AND NOT EXISTS (SELECT 1 FROM pg_constraint k
|
|
WHERE k.conrelid = to_regclass(c.tbl) AND k.conname = c.name)
|
|
ORDER BY 1;
|
|
|
|
RETURN QUERY
|
|
SELECT 'index ' || i.idx, 'missing'
|
|
FROM (VALUES
|
|
('log.idx_event_log_series_timestamp', 'log.event_log'),
|
|
('log.idx_event_log_timestamp', 'log.event_log'),
|
|
('log.idx_event_log_event_type', 'log.event_log'),
|
|
('meta.idx_series_create_timestamp_desc', 'meta.series'),
|
|
('meta.idx_series_db_schema', 'meta.series'),
|
|
('meta.idx_series_exchange_pattern', 'meta.series'),
|
|
('meta.idx_series_frequency_pattern', 'meta.series'),
|
|
('meta.idx_series_symbol_exchange', 'meta.series'),
|
|
('meta.idx_series_symbol_pattern', 'meta.series'),
|
|
('meta.idx_series_symbol_trgm', 'meta.series'),
|
|
('meta.idx_series_table_name_trgm', 'meta.series'),
|
|
('meta.idx_series_type_pattern', 'meta.series'),
|
|
('log.idx_script_log_timestamp', 'log.script_log'),
|
|
('log.idx_script_log_session', 'log.script_log'),
|
|
('log.idx_script_log_series', 'log.script_log'),
|
|
('log.idx_script_log_level', 'log.script_log')
|
|
) AS i(idx, tbl)
|
|
WHERE to_regclass(i.tbl) IS NOT NULL AND to_regclass(i.idx) IS NULL
|
|
ORDER BY 1;
|
|
|
|
RETURN QUERY
|
|
SELECT 'view ' || v, 'missing'
|
|
FROM unnest(ARRAY['log.script_log_recent']) AS v
|
|
WHERE to_regclass(v) IS NULL
|
|
ORDER BY 1;
|
|
|
|
-- Functions (compared by whitespace-insensitive body fingerprint) -------
|
|
RETURN QUERY
|
|
SELECT 'function ' || f.name,
|
|
CASE WHEN NOT EXISTS (SELECT 1 FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname || '.' || p.proname = f.name)
|
|
THEN 'missing'
|
|
ELSE 'differs from the DDL (stale or edited)' END
|
|
FROM (VALUES
|
|
('log.flush_pending_to_event_log', 'be02d914fc5e73475eb1ce7ce7a78f29'),
|
|
('log.notify_event_log', '7e5898f1d1d128ef2e44c1d4ae8a7312'),
|
|
('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'),
|
|
('meta.read_series_batch_ohlc', '4bdef4d3573caab023ddd12db38b978c'),
|
|
('meta.refresh_series_timestamps', '1cdfd496752142cbde78d9646532d1aa'),
|
|
('meta.update_series_last_timestamp', 'f27394672bc5026456c4625ac3091a62')
|
|
) AS f(name, fingerprint)
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname || '.' || p.proname = f.name
|
|
AND md5(btrim(regexp_replace(p.prosrc, '\s+', ' ', 'g'))) = f.fingerprint)
|
|
ORDER BY 1;
|
|
|
|
-- Drop-protection event trigger -----------------------------------------
|
|
RETURN QUERY
|
|
SELECT 'event trigger prevent_series_drops', 'missing'
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_event_trigger WHERE evtname = 'prevent_series_drops');
|
|
|
|
RETURN QUERY
|
|
SELECT 'event trigger prevent_series_drops', p
|
|
FROM pg_event_trigger t,
|
|
LATERAL (VALUES
|
|
(CASE WHEN t.evtevent <> 'sql_drop'
|
|
THEN 'fires on ' || t.evtevent || ', expected sql_drop' END),
|
|
(CASE WHEN t.evtenabled = 'D' THEN 'disabled' END),
|
|
(CASE WHEN t.evtfoid::regproc::text <> 'meta.prevent_direct_drop'
|
|
THEN 'calls ' || t.evtfoid::regproc::text || ', expected meta.prevent_direct_drop' END)
|
|
) AS x(p)
|
|
WHERE t.evtname = 'prevent_series_drops' AND p IS NOT NULL
|
|
ORDER BY 2;
|
|
|
|
-- Reference data and version row ----------------------------------------
|
|
RETURN QUERY
|
|
SELECT 'reference data ' || r.tbl,
|
|
r.actual || ' rows, expected at least ' || r.expected
|
|
FROM (
|
|
SELECT e.tbl, e.expected,
|
|
(xpath('/row/n/text()', query_to_xml(
|
|
format('SELECT count(*) AS n FROM %s', e.tbl), false, true, '')))[1]::text::int
|
|
AS actual
|
|
FROM (VALUES
|
|
('meta.timezones', 563),
|
|
('meta.currencies', 179),
|
|
('meta.security_types', 13),
|
|
('meta.series_schemas', 1)
|
|
) AS e(tbl, expected)
|
|
WHERE to_regclass(e.tbl) IS NOT NULL
|
|
) AS r
|
|
WHERE r.actual < r.expected
|
|
ORDER BY 1;
|
|
|
|
-- Series schema registry ------------------------------------------------
|
|
-- Only REGISTERED schemas are examined; user schemas are never looked at.
|
|
IF to_regclass('meta.series_schemas') IS NOT NULL THEN
|
|
RETURN QUERY
|
|
SELECT 'series schema series', 'not registered in meta.series_schemas'
|
|
WHERE NOT EXISTS (SELECT 1 FROM meta.series_schemas WHERE name = 'series');
|
|
|
|
RETURN QUERY
|
|
SELECT 'series schema ' || r.name, 'registered but the schema does not exist'
|
|
FROM meta.series_schemas r
|
|
WHERE NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = r.name)
|
|
ORDER BY r.name;
|
|
END IF;
|
|
|
|
IF to_regclass('meta.schema_version') IS NOT NULL THEN
|
|
RETURN QUERY
|
|
SELECT 'schema version', 'not recorded in meta.schema_version'
|
|
WHERE NOT EXISTS (SELECT 1 FROM meta.schema_version);
|
|
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$;
|