tsapi-seriesdb-shared-sql/sql/04_indexes.sql
peter 85679b5e78 Schema 1.5.0: holiday calendar registry, citext names; drop num_records
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>
2026-10-07 17:56:04 +01:00

85 lines
5.2 KiB
SQL

-- Indexes and constraints
-- Generated: 2025-10-15T12:24:56.570321
-- Primary key constraints for metadata tables
ALTER TABLE meta.instruments ADD CONSTRAINT instruments_pkey PRIMARY KEY (symbol, exchange);
ALTER TABLE meta.exchanges ADD CONSTRAINT exchanges_pkey PRIMARY KEY (id);
ALTER TABLE meta.sessions ADD CONSTRAINT sessions_pkey PRIMARY KEY (name);
-- Identity constraints for the series registry and reference tables.
--
-- These are small registries, not timeseries data: a btree key here costs
-- almost nothing. This is unrelated to the deliberate decision NOT to put a
-- unique constraint on series timestamps, where BRIN is used precisely
-- because a btree would be prohibitive at millions of rows per series.
--
-- Without these, duplicate registry rows were possible and the add_* methods
-- advertised "already exists" errors that no constraint could raise.
ALTER TABLE meta.series ADD CONSTRAINT series_pkey PRIMARY KEY (table_id);
ALTER TABLE meta.series ADD CONSTRAINT series_schema_table_key UNIQUE (db_schema, table_name);
ALTER TABLE meta.currencies ADD CONSTRAINT currencies_pkey PRIMARY KEY (code);
ALTER TABLE meta.security_types ADD CONSTRAINT security_types_pkey PRIMARY KEY (code);
ALTER TABLE meta.timezones ADD CONSTRAINT timezones_pkey PRIMARY KEY (iana_name);
-- Series schema registry. Names follow the table-name rule shared by both
-- clients: lower case, [a-z_][a-z0-9_]*, at most 63 bytes, never quoted.
ALTER TABLE meta.series_schemas ADD CONSTRAINT series_schemas_pkey PRIMARY KEY (name);
ALTER TABLE meta.series_schemas ADD CONSTRAINT series_schemas_name_check
CHECK (name ~ '^[a-z_][a-z0-9_]*$' AND octet_length(name) <= 63);
-- A series may only live in a registered schema -- enforced here, so no
-- client can bypass it. RESTRICT: a schema cannot be unregistered while it
-- still holds series.
ALTER TABLE meta.series ADD CONSTRAINT series_db_schema_fkey
FOREIGN KEY (db_schema) REFERENCES meta.series_schemas (name)
ON UPDATE RESTRICT ON DELETE RESTRICT;
-- Table names follow the same rule (Core contract 6 in both clients).
ALTER TABLE meta.series ADD CONSTRAINT series_table_name_check
CHECK (table_name ~ '^[a-z_][a-z0-9_]*$' AND octet_length(table_name) <= 63);
-- Holiday calendar registry (1.5.0). citext keys: unique and referenced
-- case-insensitively, stored as given.
ALTER TABLE meta.holiday_calendars ADD CONSTRAINT holiday_calendars_pkey PRIMARY KEY (name);
ALTER TABLE meta.holiday_calendars ADD CONSTRAINT holiday_calendars_name_check
CHECK (name <> '' AND name::text = btrim(name::text) AND char_length(name::text) <= 50);
ALTER TABLE meta.holiday_calendars ADD CONSTRAINT holiday_calendars_coverage_check
CHECK ((covered_from IS NULL) = (covered_to IS NULL)
AND (covered_from IS NULL OR covered_from <= covered_to));
-- Holidays belong to their calendar: dropping the calendar drops them.
ALTER TABLE meta.holidays ADD CONSTRAINT holidays_calendar_fkey
FOREIGN KEY (calendar_name) REFERENCES meta.holiday_calendars (name)
ON UPDATE CASCADE ON DELETE CASCADE;
-- A calendar still named by an exchange or instrument cannot be dropped.
ALTER TABLE meta.exchanges ADD CONSTRAINT exchanges_holiday_calendar_fkey
FOREIGN KEY (default_holiday_calendar) REFERENCES meta.holiday_calendars (name)
ON UPDATE CASCADE ON DELETE RESTRICT;
ALTER TABLE meta.instruments ADD CONSTRAINT instruments_holiday_calendar_fkey
FOREIGN KEY (holiday_calendar) REFERENCES meta.holiday_calendars (name)
ON UPDATE CASCADE ON DELETE RESTRICT;
CREATE INDEX idx_event_log_series_timestamp ON log.event_log USING btree (series_name, "timestamp" DESC);
CREATE INDEX idx_event_log_timestamp ON log.event_log USING btree ("timestamp" DESC);
CREATE INDEX idx_event_log_event_type ON log.event_log USING btree (event_type);
CREATE INDEX idx_series_create_timestamp_desc ON meta.series USING btree (create_timestamp DESC NULLS LAST);
CREATE INDEX idx_series_db_schema ON meta.series USING btree (db_schema) WHERE ((db_schema IS NOT NULL) AND ((db_schema)::text <> ''::text));
CREATE INDEX idx_series_exchange_pattern ON meta.series USING btree (lower((exchange)::text) text_pattern_ops) WHERE ((exchange IS NOT NULL) AND ((exchange)::text <> ''::text));
CREATE INDEX idx_series_frequency_pattern ON meta.series USING btree (lower((frequency)::text) text_pattern_ops) WHERE ((frequency IS NOT NULL) AND ((frequency)::text <> ''::text));
CREATE INDEX idx_series_symbol_exchange ON meta.series USING btree (lower((symbol)::text), lower((exchange)::text)) WHERE ((symbol IS NOT NULL) AND ((symbol)::text <> ''::text) AND (exchange IS NOT NULL) AND ((exchange)::text <> ''::text));
CREATE INDEX idx_series_symbol_pattern ON meta.series USING btree (lower((symbol)::text) text_pattern_ops) WHERE ((symbol IS NOT NULL) AND ((symbol)::text <> ''::text));
CREATE INDEX idx_series_symbol_trgm ON meta.series USING gin (symbol public.gin_trgm_ops) WHERE ((symbol IS NOT NULL) AND ((symbol)::text <> ''::text));
CREATE INDEX idx_series_table_name_trgm ON meta.series USING gin (table_name public.gin_trgm_ops) WHERE ((table_name IS NOT NULL) AND ((table_name)::text <> ''::text));
CREATE INDEX idx_series_type_pattern ON meta.series USING btree (lower((type)::text) text_pattern_ops) WHERE ((type IS NOT NULL) AND ((type)::text <> ''::text));