tsapi-seriesdb-shared-sql/sql/migrate/migrate_metadata_identity.sql
peter eed06559c0 Canonical TSAPI series DB schema v1.0.0
Single source of truth for the timeseries PostgreSQL schema shared by the
Python (TSAPI_SERIESDB_PG_PY) and C++ (TSAPI_POSTGRES_CPP) libraries. SQL
seeded byte-for-byte from TSAPI_SERIESDB_PG_PY/tsapi_pgdb/sql on 2026-09-02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 17:27:26 +01:00

111 lines
5.2 KiB
SQL

-- Migration: add identity constraints to the series registry and reference tables
--
-- meta.series had no primary key and no unique constraint, so two rows could
-- describe the same series -- and copy_series() actively created that, by
-- copying the source's table_id and db_schema. meta.currencies,
-- meta.security_types and meta.timezones had no key on their natural
-- identifier either, while add_currency()/add_security_type()/add_timezone()
-- all catch IntegrityError and promise an "already exists" error that no
-- constraint could raise.
--
-- These are small registries, not timeseries data. A btree key here costs
-- almost nothing, and this is unrelated to the deliberate decision NOT to
-- constrain series timestamps, where BRIN is used precisely because a btree
-- would be prohibitive at millions of rows per series.
--
-- New databases get these from 04_indexes.sql. Run this against any database
-- initialised before the fix.
--
-- Safe to run more than once: each constraint is added only if absent.
--
-- IMPORTANT: this ABORTS if duplicate rows already exist, printing what
-- collides. It does not guess which row to keep -- that is a data decision.
-- Resolve the duplicates, then run it again.
DO $$
DECLARE
dup_count integer;
dup_list text;
BEGIN
----------------------------------------------------------------------
-- Pre-flight: refuse to run if any natural key already collides.
----------------------------------------------------------------------
SELECT count(*), string_agg(format('%s.%s', db_schema, table_name), ', ')
INTO dup_count, dup_list
FROM (SELECT db_schema, table_name FROM meta.series
GROUP BY db_schema, table_name HAVING count(*) > 1) d;
IF dup_count > 0 THEN
RAISE EXCEPTION
'meta.series has % duplicated (db_schema, table_name): %. '
'Remove the extra rows, then re-run this migration.',
dup_count, dup_list;
END IF;
SELECT count(*), string_agg(table_id::text, ', ')
INTO dup_count, dup_list
FROM (SELECT table_id FROM meta.series
GROUP BY table_id HAVING count(*) > 1) d;
IF dup_count > 0 THEN
RAISE EXCEPTION
'meta.series has % duplicated table_id: %. Most likely created by '
'copy_series() before it was fixed. Reassign with '
'UPDATE meta.series SET table_id = nextval(''meta._table_meta__table_id_seq'') '
'WHERE ... , then re-run this migration.',
dup_count, dup_list;
END IF;
SELECT count(*), string_agg(code, ', ')
INTO dup_count, dup_list
FROM (SELECT code FROM meta.currencies GROUP BY code HAVING count(*) > 1) d;
IF dup_count > 0 THEN
RAISE EXCEPTION 'meta.currencies has % duplicated code: %', dup_count, dup_list;
END IF;
SELECT count(*), string_agg(code, ', ')
INTO dup_count, dup_list
FROM (SELECT code FROM meta.security_types GROUP BY code HAVING count(*) > 1) d;
IF dup_count > 0 THEN
RAISE EXCEPTION 'meta.security_types has % duplicated code: %', dup_count, dup_list;
END IF;
SELECT count(*), string_agg(iana_name, ', ')
INTO dup_count, dup_list
FROM (SELECT iana_name FROM meta.timezones GROUP BY iana_name HAVING count(*) > 1) d;
IF dup_count > 0 THEN
RAISE EXCEPTION 'meta.timezones has % duplicated iana_name: %', dup_count, dup_list;
END IF;
----------------------------------------------------------------------
-- Add the constraints, skipping any that already exist.
----------------------------------------------------------------------
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'series_pkey'
AND conrelid = 'meta.series'::regclass) THEN
ALTER TABLE meta.series ADD CONSTRAINT series_pkey PRIMARY KEY (table_id);
RAISE NOTICE 'added meta.series primary key on (table_id)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'series_schema_table_key'
AND conrelid = 'meta.series'::regclass) THEN
ALTER TABLE meta.series ADD CONSTRAINT series_schema_table_key
UNIQUE (db_schema, table_name);
RAISE NOTICE 'added meta.series unique constraint on (db_schema, table_name)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'currencies_pkey'
AND conrelid = 'meta.currencies'::regclass) THEN
ALTER TABLE meta.currencies ADD CONSTRAINT currencies_pkey PRIMARY KEY (code);
RAISE NOTICE 'added meta.currencies primary key on (code)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'security_types_pkey'
AND conrelid = 'meta.security_types'::regclass) THEN
ALTER TABLE meta.security_types ADD CONSTRAINT security_types_pkey PRIMARY KEY (code);
RAISE NOTICE 'added meta.security_types primary key on (code)';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'timezones_pkey'
AND conrelid = 'meta.timezones'::regclass) THEN
ALTER TABLE meta.timezones ADD CONSTRAINT timezones_pkey PRIMARY KEY (iana_name);
RAISE NOTICE 'added meta.timezones primary key on (iana_name)';
END IF;
END $$;