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>
This commit is contained in:
peter 2026-09-02 17:27:26 +01:00
commit eed06559c0
17 changed files with 2080 additions and 0 deletions

View file

@ -0,0 +1,48 @@
-- Migration: repair meta.drop_series_if_exists()
--
-- The function dropped a registered series without setting
-- application.dropping_series, so the prevent_direct_drop event trigger
-- rejected its own DROP:
--
-- Table series.X is registered in series and can only be dropped
-- using TimeseriesDB.drop_series()
--
-- Unlike meta.drop_series() and meta.drop_all_series(), which set the flag
-- correctly, this one never did -- so it always failed on exactly the case it
-- exists to handle. The Python API does not call it, which is why the defect
-- went unnoticed.
--
-- New databases pick this up from 05_procedures.sql. Run this against any
-- database initialised before the fix.
--
-- Safe to run more than once (CREATE OR REPLACE).
CREATE OR REPLACE FUNCTION meta.drop_series_if_exists(_table_name text, _schema_name text DEFAULT 'series'::text)
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
-- Check if the series exists in series table
IF EXISTS (
SELECT 1 FROM meta.series
WHERE "table_name" = _table_name
AND "db_schema" = _schema_name
) THEN
-- Signal the prevent_direct_drop event trigger that this DROP is
-- coming from a sanctioned procedure. Without it the trigger rejects
-- the statement below, because the table is registered in meta.series.
-- Local to the transaction, so it cannot leak to later statements.
PERFORM set_config('application.dropping_series', 'true', true);
-- Drop the table
EXECUTE format('DROP TABLE IF EXISTS %I.%I', _schema_name, _table_name);
-- Remove from series table
DELETE FROM meta.series
WHERE "table_name" = _table_name
AND "db_schema" = _schema_name;
PERFORM set_config('application.dropping_series', 'false', true);
END IF;
END;
$function$;

View file

@ -0,0 +1,99 @@
-- Migration: Add Instrument Metadata Columns
-- Date: 2026-02-11
-- Description: Adds classification and trading specification columns to meta.exchanges and meta.instruments
-- ============================================================================
-- 1. Add columns to meta.exchanges
-- ============================================================================
-- Trading specifications
ALTER TABLE meta.exchanges ADD COLUMN IF NOT EXISTS default_tick_size DOUBLE PRECISION;
ALTER TABLE meta.exchanges ADD COLUMN IF NOT EXISTS default_tick_value DOUBLE PRECISION;
ALTER TABLE meta.exchanges ADD COLUMN IF NOT EXISTS default_extended_hours BOOLEAN DEFAULT false;
-- Geographic info
ALTER TABLE meta.exchanges ADD COLUMN IF NOT EXISTS country VARCHAR(3);
COMMENT ON COLUMN meta.exchanges.default_tick_size IS 'Default minimum price increment (e.g., 0.01 for stocks)';
COMMENT ON COLUMN meta.exchanges.default_tick_value IS 'Default value per tick in currency (0.01 for stocks = 1 cent per share)';
COMMENT ON COLUMN meta.exchanges.default_extended_hours IS 'Default for whether instruments support pre/post market trading';
COMMENT ON COLUMN meta.exchanges.country IS 'ISO 3166-1 alpha-3 country code (USA, GBR, JPN)';
-- ============================================================================
-- 2. Add columns to meta.instruments
-- ============================================================================
-- Basic info
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS description VARCHAR(255);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS country VARCHAR(3);
-- Trading specifications
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS extended_hours BOOLEAN;
-- Company/Fund classification
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS sector VARCHAR(50);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS industry VARCHAR(100);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS category VARCHAR(50);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS subcategory VARCHAR(50);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS asset_class VARCHAR(30);
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS geography VARCHAR(30);
-- ETF-specific
ALTER TABLE meta.instruments ADD COLUMN IF NOT EXISTS underlying VARCHAR(30);
COMMENT ON COLUMN meta.instruments.description IS 'Full name/description of the instrument';
COMMENT ON COLUMN meta.instruments.country IS 'ISO 3166-1 alpha-3 country code (overrides exchange default)';
COMMENT ON COLUMN meta.instruments.extended_hours IS 'Override exchange default for pre/post market trading';
COMMENT ON COLUMN meta.instruments.sector IS 'GICS sector (e.g., Technology, Financials)';
COMMENT ON COLUMN meta.instruments.industry IS 'GICS industry (e.g., Software-Application)';
COMMENT ON COLUMN meta.instruments.category IS 'Custom categorization (e.g., Sector ETF, Broad Market)';
COMMENT ON COLUMN meta.instruments.subcategory IS 'Custom sub-categorization (e.g., S&P 500, Technology)';
COMMENT ON COLUMN meta.instruments.asset_class IS 'Asset class (Equity, Fixed Income, Commodity, Currency)';
COMMENT ON COLUMN meta.instruments.geography IS 'Geographic focus (US, International, Emerging)';
COMMENT ON COLUMN meta.instruments.underlying IS 'Underlying symbol for ETFs/derivatives (e.g., SPX, NDX)';
-- ============================================================================
-- 3. Update existing exchange data
-- ============================================================================
-- Update NYSE if it exists
UPDATE meta.exchanges
SET
default_tick_size = 0.01,
default_tick_value = 0.01,
default_extended_hours = true,
country = 'USA'
WHERE id = 'NYSE';
-- Update CME if it exists
UPDATE meta.exchanges
SET
country = 'USA',
default_extended_hours = true
WHERE id = 'CME';
-- Add NASDAQ if it doesn't exist
INSERT INTO meta.exchanges (
id, name, timezone, currency, default_sec_type,
default_tick_size, default_tick_value, default_extended_hours, country
)
SELECT 'NASDAQ', 'NASDAQ Stock Market', 'America/New_York', 'USD', 'STK',
0.01, 0.01, true, 'USA'
WHERE NOT EXISTS (SELECT 1 FROM meta.exchanges WHERE id = 'NASDAQ');
-- ============================================================================
-- Migration Complete
-- ============================================================================
-- Verify the changes
SELECT 'meta.exchanges columns:' as info;
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'meta' AND table_name = 'exchanges'
ORDER BY ordinal_position;
SELECT 'meta.instruments columns:' as info;
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'meta' AND table_name = 'instruments'
ORDER BY ordinal_position;

View file

@ -0,0 +1,111 @@
-- 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 $$;