From eed06559c09cee3de857b772342f64fc5623abed Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 2 Sep 2026 17:27:26 +0100 Subject: [PATCH] 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 --- .gitignore | 6 + README.md | 104 +++ VERSION | 1 + db_config.json.example | 7 + run_setup.py | 211 +++++ sql/00_extensions.sql | 4 + sql/01_schemas.sql | 6 + sql/02_sequences.sql | 16 + sql/03_tables.sql | 145 ++++ sql/04_indexes.sql | 46 ++ sql/05_procedures.sql | 426 ++++++++++ sql/06_triggers.sql | 37 + sql/07_reference_data.sql | 769 ++++++++++++++++++ sql/08_script_log.sql | 44 + sql/migrate/migrate_drop_series_if_exists.sql | 48 ++ sql/migrate/migrate_instrument_metadata.sql | 99 +++ sql/migrate/migrate_metadata_identity.sql | 111 +++ 17 files changed, 2080 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 VERSION create mode 100644 db_config.json.example create mode 100644 run_setup.py create mode 100644 sql/00_extensions.sql create mode 100644 sql/01_schemas.sql create mode 100644 sql/02_sequences.sql create mode 100644 sql/03_tables.sql create mode 100644 sql/04_indexes.sql create mode 100644 sql/05_procedures.sql create mode 100644 sql/06_triggers.sql create mode 100644 sql/07_reference_data.sql create mode 100644 sql/08_script_log.sql create mode 100644 sql/migrate/migrate_drop_series_if_exists.sql create mode 100644 sql/migrate/migrate_instrument_metadata.sql create mode 100644 sql/migrate/migrate_metadata_identity.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..561f087 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Local connection settings (never commit credentials) +db_config.json + +# Python +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..110af2e --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# TSAPI_SERIESDB_SCHEMA — canonical timeseries DB schema + +**Single source of truth** for the PostgreSQL schema used by the TSAPI +timeseries database. Both client libraries target the schema defined here: + +- **Python** — `TSAPI_SERIESDB_PG_PY` +- **C++** — `TSAPI_POSTGRES_CPP` + +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.0.0** (see [`VERSION`](VERSION); recorded at runtime +in the `meta.schema_version` table). + +--- + +## Layout + +``` +TSAPI_SERIESDB_SCHEMA/ +├── VERSION # canonical schema version string +├── run_setup.py # provisioner — runs the numbered files in order +├── db_config.json.example # copy to db_config.json and edit (gitignored) +├── README.md +└── sql/ + ├── 00_extensions.sql # pg_trgm + ├── 01_schemas.sql # meta / series / log schemas + ├── 02_sequences.sql # id sequences + ├── 03_tables.sql # meta.* and log.* tables + ├── 04_indexes.sql # indexes + registry constraints + ├── 05_procedures.sql # meta.* / log.* stored procedures + ├── 06_triggers.sql # prevent_series_drops event trigger + ├── 07_reference_data.sql # timezones + currencies reference data + ├── 08_script_log.sql # auxiliary (not part of a fresh install) + └── migrate/ # one-off migration scripts (run manually) +``` + +`run_setup.py` executes `00`–`07` in order. `08_script_log.sql` and +`sql/migrate/*.sql` are auxiliary and are **not** run by a fresh install. + +--- + +## Provisioning a database + +```bash +# 1. configure +cp db_config.json.example db_config.json # then edit host/user/password +# (or pass connection flags on the command line instead) + +# 2. create the schema +python run_setup.py # uses db_config.json / defaults +python run_setup.py --host 192.168.1.7 --user claude --password claudepwd +python run_setup.py --drop-existing # drop meta+series+log first +python run_setup.py --clean-slate # drop ALL user objects first +``` + +Requires `psycopg2` and a PostgreSQL user with CREATE privileges on the target +database. + +--- + +## What gets created + +- **Schemas:** `meta` (registry + reference data), `series` (timeseries data + tables live here), `log` (event/audit trail). +- **Registry:** `meta.series` — one row per series table, PK `table_id`, + `UNIQUE (db_schema, table_name)`. +- **Reference tables:** `meta.currencies`, `meta.timezones`, `meta.exchanges`, + `meta.instruments`, `meta.sessions`, `meta.holidays`, `meta.security_types`, + `meta.schema_version`. +- **Procedures:** `meta.drop_series` / `_if_exists` / `drop_all_series`, + `meta.update_series_last_timestamp`, `meta.refresh_series_timestamps`, + `meta.read_series_batch[_cursor|_ohlc]`, `log.notify_event_log`, + `log.flush_pending_to_event_log`. +- **Event trigger:** `prevent_series_drops` — blocks direct `DROP TABLE` on a + registered series (drops must go through `meta.drop_series*`). + +Series data tables have **no `UNIQUE` constraint on `timestamp`** — the index is +BRIN. Concurrent writers must serialise per-series with the advisory-lock +protocol (`pg_advisory_lock(hashtext('tsapi_pgdb.write:{schema}.{table}'))`); see +the C++ project's `planning/2026-09-02_cpp_locking_interop_plan.md`. + +--- + +## Versioning contract + +Bump [`VERSION`](VERSION) whenever `sql/` changes in a way clients must know +about, and have each client read `meta.schema_version` on connect and warn/fail +on mismatch. This turns silent schema drift into a loud error. + +--- + +## Provenance + +The `sql/` files were seeded (byte-for-byte, 2026-09-02) from the then-current +authoritative copy in `TSAPI_SERIESDB_PG_PY/tsapi_pgdb/sql/`. From this point on, +**this** directory is authoritative and the Python project should consume it +rather than keep its own copy — see the migration note in the C++ project's +`planning/` folder. + +> **Known drift caught during setup:** on 2026-09-02 the remote instance +> `192.168.1.7` had `meta.sessions.exchange_id` as nullable, while the canonical +> DDL (`03_tables.sql`) defines it `NOT NULL`. Re-provision drifted databases +> from these files. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/db_config.json.example b/db_config.json.example new file mode 100644 index 0000000..fbac013 --- /dev/null +++ b/db_config.json.example @@ -0,0 +1,7 @@ +{ + "host": "localhost", + "port": 5432, + "database": "timeseries_db__dev", + "user": "tsapi", + "password": "tsapipwd" +} diff --git a/run_setup.py b/run_setup.py new file mode 100644 index 0000000..cd18fec --- /dev/null +++ b/run_setup.py @@ -0,0 +1,211 @@ +""" +Provision the TSAPI series database schema from the canonical SQL files. + +This is the SINGLE SOURCE OF TRUTH provisioner for the TSAPI timeseries +PostgreSQL schema. Both the Python library (TSAPI_SERIESDB_PG_PY) and the C++ +library (TSAPI_POSTGRES_CPP) target the schema it creates. Do not fork the SQL +into either project -- edit it here. + +It executes the numbered DDL files in ./sql/ in order: + 00_extensions, 01_schemas, 02_sequences, 03_tables, 04_indexes, + 05_procedures, 06_triggers, 07_reference_data + +Connection settings come from (highest precedence first): + 1. command-line flags (--host --port --dbname --user --password) + 2. db_config.json next to this script + 3. built-in defaults (localhost:5432 / timeseries_db__dev / tsapi) + +Usage: + python run_setup.py # use db_config.json / defaults + python run_setup.py --host 192.168.1.7 --user claude --password claudepwd + python run_setup.py --drop-existing # drop meta+series schemas first + python run_setup.py --clean-slate # drop ALL user objects first +""" + +import sys +import json +import argparse +from pathlib import Path +from typing import Dict + +import psycopg2 + +SQL_DIR = Path(__file__).parent / "sql" +CONFIG_FILE = Path(__file__).parent / "db_config.json" + +# Executed in order. 08_script_log.sql and sql/migrate/*.sql are auxiliary and +# are intentionally NOT part of a fresh install. +SQL_FILES = [ + "00_extensions.sql", + "01_schemas.sql", + "02_sequences.sql", + "03_tables.sql", + "04_indexes.sql", + "05_procedures.sql", + "06_triggers.sql", + "07_reference_data.sql", +] + +DEFAULTS = { + "host": "localhost", + "port": 5432, + "database": "timeseries_db__dev", + "user": "tsapi", + "password": "tsapipwd", +} + + +def load_config(args) -> Dict: + config = dict(DEFAULTS) + if CONFIG_FILE.exists(): + try: + config.update(json.loads(CONFIG_FILE.read_text())) + except json.JSONDecodeError as e: + print(f"[FAIL] Invalid JSON in {CONFIG_FILE}: {e}") + sys.exit(1) + # CLI overrides + for key, val in ( + ("host", args.host), ("port", args.port), ("database", args.dbname), + ("user", args.user), ("password", args.password), + ): + if val is not None: + config[key] = val + return config + + +def execute_sql_file(conn, sql_file: Path): + try: + sql = sql_file.read_text(encoding="utf-8") + with conn.cursor() as cur: + cur.execute(sql) + conn.commit() + return True, f"[OK] Executed {sql_file.name}" + except Exception as e: + conn.rollback() + return False, f"[FAIL] {sql_file.name}: {e}" + + +def verify_setup(conn) -> None: + print("\n" + "=" * 70) + print("Verifying setup") + print("=" * 70) + with conn.cursor() as cur: + cur.execute( + "SELECT schema_name FROM information_schema.schemata " + "WHERE schema_name IN ('meta','series','log') ORDER BY 1" + ) + print("[OK] Schemas:", ", ".join(r[0] for r in cur.fetchall())) + + cur.execute("SELECT count(*) FROM information_schema.tables WHERE table_schema='meta'") + print("[OK] meta tables:", cur.fetchone()[0]) + + cur.execute("SELECT count(*) FROM information_schema.routines WHERE routine_schema IN ('meta','log')") + print("[OK] procedures:", cur.fetchone()[0]) + + cur.execute("SELECT count(*) FROM pg_event_trigger WHERE evtname='prevent_series_drops'") + print("[OK] event triggers:", cur.fetchone()[0]) + + cur.execute("SELECT count(*) FROM meta.timezones") + tz = cur.fetchone()[0] + cur.execute("SELECT count(*) FROM meta.currencies") + cu = cur.fetchone()[0] + print(f"[OK] reference data: {tz} timezones, {cu} currencies") + + cur.execute("SELECT version, description FROM meta.schema_version ORDER BY installed_at DESC LIMIT 1") + row = cur.fetchone() + if row: + print(f"[OK] schema_version: {row[0]} ({row[1]})") + + +def clean_slate(conn) -> None: + print("Performing clean slate...") + with conn.cursor() as cur: + cur.execute( + "SELECT schema_name FROM information_schema.schemata " + "WHERE schema_name NOT IN ('pg_catalog','information_schema','pg_toast','public') " + "AND schema_name NOT LIKE 'pg_temp_%' AND schema_name NOT LIKE 'pg_toast_temp_%'" + ) + for (schema,) in cur.fetchall(): + print(f" drop schema {schema}") + cur.execute(f"DROP SCHEMA IF EXISTS {schema} CASCADE") + cur.execute("SELECT evtname FROM pg_event_trigger") + for (trig,) in cur.fetchall(): + cur.execute(f"DROP EVENT TRIGGER IF EXISTS {trig}") + cur.execute("CREATE SCHEMA IF NOT EXISTS public") + conn.commit() + print("[OK] clean slate done\n") + + +def main() -> int: + p = argparse.ArgumentParser(description="Provision the TSAPI series DB schema") + p.add_argument("--host") + p.add_argument("--port", type=int) + p.add_argument("--dbname") + p.add_argument("--user") + p.add_argument("--password") + p.add_argument("--drop-existing", action="store_true", + help="Drop meta and series schemas before creating (destructive)") + p.add_argument("--clean-slate", action="store_true", + help="Drop ALL user objects before creating (very destructive)") + p.add_argument("--yes", action="store_true", help="Skip confirmation prompts") + args = p.parse_args() + + cfg = load_config(args) + + print("=" * 70) + print("TSAPI series DB schema setup") + print("=" * 70) + print(f"Target : {cfg['host']}:{cfg['port']}/{cfg['database']} (user {cfg['user']})") + print(f"SQL dir: {SQL_DIR}") + + if (args.clean_slate or args.drop_existing) and not args.yes: + what = "ALL user objects" if args.clean_slate else "meta and series schemas" + if input(f"This will DROP {what}. Continue? (yes/no): ").lower() != "yes": + print("Aborted.") + return 1 + + try: + conn = psycopg2.connect( + host=cfg["host"], port=cfg["port"], dbname=cfg["database"], + user=cfg["user"], password=cfg["password"], + ) + except Exception as e: + print(f"[FAIL] connect: {e}") + return 1 + + try: + if args.clean_slate: + clean_slate(conn) + elif args.drop_existing: + with conn.cursor() as cur: + cur.execute("DROP SCHEMA IF EXISTS meta CASCADE") + cur.execute("DROP SCHEMA IF EXISTS series CASCADE") + cur.execute("DROP SCHEMA IF EXISTS log CASCADE") + cur.execute("DROP EVENT TRIGGER IF EXISTS prevent_series_drops") + conn.commit() + print("[OK] existing schemas dropped\n") + + failed = [] + for name in SQL_FILES: + f = SQL_DIR / name + if not f.exists(): + print(f"[FAIL] missing {name}") + failed.append(name) + continue + ok, msg = execute_sql_file(conn, f) + print(msg) + if not ok: + failed.append(name) + + if failed: + print(f"\n[FAIL] {len(failed)} file(s) failed: {', '.join(failed)}") + return 1 + verify_setup(conn) + print("\n[SUCCESS] schema setup complete") + return 0 + finally: + conn.close() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/sql/00_extensions.sql b/sql/00_extensions.sql new file mode 100644 index 0000000..30d94b2 --- /dev/null +++ b/sql/00_extensions.sql @@ -0,0 +1,4 @@ +-- PostgreSQL extensions +-- Generated: 2025-10-15T12:24:56.026074 + +CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA public; diff --git a/sql/01_schemas.sql b/sql/01_schemas.sql new file mode 100644 index 0000000..12071b5 --- /dev/null +++ b/sql/01_schemas.sql @@ -0,0 +1,6 @@ +-- Database schemas +-- Generated: 2025-10-15T12:24:56.026074 + +CREATE SCHEMA IF NOT EXISTS meta; +CREATE SCHEMA IF NOT EXISTS series; +CREATE SCHEMA IF NOT EXISTS log; diff --git a/sql/02_sequences.sql b/sql/02_sequences.sql new file mode 100644 index 0000000..ae22f65 --- /dev/null +++ b/sql/02_sequences.sql @@ -0,0 +1,16 @@ +-- Sequences +-- Generated: 2025-10-15T12:24:56.037289 + +CREATE SEQUENCE meta._table_meta__table_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO CYCLE +; + +CREATE SEQUENCE log.event_log_id_seq + AS bigint + START WITH 1 + INCREMENT BY 1 + NO CYCLE +; diff --git a/sql/03_tables.sql b/sql/03_tables.sql new file mode 100644 index 0000000..f6e5a2b --- /dev/null +++ b/sql/03_tables.sql @@ -0,0 +1,145 @@ +-- Meta schema tables +-- Generated: 2025-10-15T12:24:56.295509 + +CREATE UNLOGGED TABLE log.pending_changes ( + id BIGSERIAL PRIMARY KEY, + "timestamp" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + series_name TEXT NOT NULL, + db_schema TEXT NOT NULL, + op_type TEXT NOT NULL, + db_user TEXT DEFAULT CURRENT_USER, + details JSONB +); + +CREATE TABLE log.event_log ( + id BIGINT NOT NULL DEFAULT nextval('log.event_log_id_seq'), + "timestamp" TIMESTAMPTZ, + series_name TEXT NOT NULL, + db_schema TEXT NOT NULL, + event_type TEXT NOT NULL, + details JSONB, + source TEXT, + db_user TEXT, + PRIMARY KEY (id) +); + +CREATE TABLE meta.currencies ( + code character varying(3) NOT NULL, + name character varying(100) NOT NULL, + symbol character varying(10), + decimal_places integer DEFAULT 2, + notes text +); + +CREATE TABLE meta.exchanges ( + id character varying(12) NOT NULL, + name character varying(50), + timezone character varying(35), + currency character varying(3), + default_sec_type character varying(18), + default_session character varying(40), + default_holiday_calendar character varying(40), + default_tick_size double precision, + default_tick_value double precision, + default_extended_hours boolean DEFAULT false, + country character varying(3) +); + +CREATE TABLE meta.holidays ( + calendar_name character varying(50) NOT NULL, + date date NOT NULL, + description character varying(255), + early_close boolean NOT NULL DEFAULT false, + PRIMARY KEY (calendar_name, date) +); + +CREATE TABLE meta.instruments ( + symbol character varying(12) NOT NULL, + exchange character varying(12) NOT NULL, + name character varying(50) NOT NULL, + type character varying(10), + tick_size double precision, + tick_value double precision, + expiry character varying(20), + strike double precision, + currency character varying(3), + session character varying(40), + holiday_calendar character varying(50), + sec_id character varying(50), + sec_id_type character varying(20), + description character varying(255), + country character varying(3), + extended_hours boolean, + sector character varying(50), + industry character varying(100), + category character varying(50), + subcategory character varying(50), + asset_class character varying(30), + geography character varying(30), + underlying character varying(30) +); + +CREATE TABLE meta.security_types ( + code character varying(10) NOT NULL, + name character varying(50) NOT NULL, + description text, + notes text +); + +CREATE TABLE meta.schema_version ( + version character varying(20) NOT NULL, + installed_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + description text +); + +CREATE TABLE meta.series ( + table_id integer DEFAULT nextval('meta._table_meta__table_id_seq'::regclass) NOT NULL, + db_schema character varying(70) NOT NULL, + table_name character varying(70) NOT NULL, + type character varying(30), + description character varying(255), + column_schema text DEFAULT ''::text, + symbol character varying(30), + root_symbol character varying(30), + exchange character varying(30), + frequency character varying(30), + session text, + sec_type character varying(30), + sec_id character varying(30), + sec_id_type character varying(30), + create_timestamp timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + num_records bigint, + first_timestamp timestamp without time zone, + last_timestamp timestamp without time zone, + bar_type character varying(30), + feed character varying(30), + currency character varying(30), + expiry character varying(30), + roll_type character varying(30), + feed_type character varying(30), + user_attr text, + user_attr_2 text, + script text, + notes text, + timezone character varying(50), + category character varying(30), + subcategory character varying(30), + json JSONB, + needs_reindex boolean NOT NULL DEFAULT false +); + +CREATE TABLE meta.sessions ( + name character varying(50) NOT NULL, + definition text NOT NULL, + "json" text, + exchange_id character varying(12) NOT NULL +); + +CREATE TABLE meta.timezones ( + iana_name character varying(50) NOT NULL, + display_name character varying(100) NOT NULL, + description text, + region character varying(50), + utc_offset_hours real, + notes text +); diff --git a/sql/04_indexes.sql b/sql/04_indexes.sql new file mode 100644 index 0000000..20c50c2 --- /dev/null +++ b/sql/04_indexes.sql @@ -0,0 +1,46 @@ +-- 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); + +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)); diff --git a/sql/05_procedures.sql b/sql/05_procedures.sql new file mode 100644 index 0000000..b4c2b4c --- /dev/null +++ b/sql/05_procedures.sql @@ -0,0 +1,426 @@ +-- Stored procedures and functions +-- Generated: 2025-10-15T12:24:56.582392 + +-- Log schema functions + +-- Function: log.notify_event_log +-- Writes a breadcrumb to the staging table (UNLOGGED, fast). +-- Self-drains every 20,000th insert to prevent unbounded growth. +CREATE OR REPLACE FUNCTION log.notify_event_log( + p_series_name TEXT, + p_db_schema TEXT, + p_op_type TEXT, + p_details JSONB DEFAULT NULL +) RETURNS void +LANGUAGE plpgsql +AS $function$ +BEGIN + INSERT INTO log.pending_changes (series_name, db_schema, op_type, details) + VALUES (p_series_name, p_db_schema, p_op_type, p_details); + + -- Self-drain every 20,000th insert + IF (currval('log.pending_changes_id_seq') % 20000 = 0) THEN + PERFORM log.flush_pending_to_event_log(); + END IF; +EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'notify_event_log failed: %', SQLERRM; +END; +$function$; + + +-- Function: log.flush_pending_to_event_log +-- Drains the staging table into the permanent event_log. +-- Safe for concurrent use: snapshots max(id) before processing. +-- After moving events, verifies that meta.series timestamps match actual +-- data for any series that had data-modifying events. If a mismatch is +-- found, writes a TIMESTAMP_MISMATCH event to log.event_log for visibility. +-- (Timestamps are kept accurate by the application on every data operation, +-- so this step is a safety net, not a correction mechanism.) +CREATE OR REPLACE FUNCTION log.flush_pending_to_event_log() +RETURNS INT +LANGUAGE plpgsql +AS $function$ +DECLARE + batch_id BIGINT; + v_count INT; + _rec RECORD; + _actual_first TIMESTAMP; + _actual_last TIMESTAMP; + _meta_first TIMESTAMP; + _meta_last TIMESTAMP; +BEGIN + -- Serialise flushes. Snapshotting MAX(id) does not make this safe on its + -- own: two callers can both read the same batch_id, both copy those rows + -- to log.event_log, and only then delete them -- duplicating permanent + -- audit events. The lock is transaction-scoped, so it is released when + -- this function's transaction ends, however it ends. + PERFORM pg_advisory_xact_lock(hashtext('tsapi_pgdb.flush_event_log')); + + SELECT COALESCE(MAX(id), 0) INTO batch_id FROM log.pending_changes; + IF batch_id = 0 THEN RETURN 0; END IF; + + -- 1. Move rows from staging to permanent event_log + INSERT INTO log.event_log ("timestamp", series_name, db_schema, event_type, details, source, db_user) + SELECT "timestamp", series_name, db_schema, op_type, details, 'app', db_user + FROM log.pending_changes + WHERE id <= batch_id + ORDER BY id; + + GET DIAGNOSTICS v_count = ROW_COUNT; + + -- 2. Verify meta.series timestamps for series with data-modifying events + FOR _rec IN + SELECT DISTINCT series_name, db_schema + FROM log.pending_changes + WHERE id <= batch_id + AND op_type IN ( + 'CSV_IMPORT', 'JSON_APPEND', 'DATAFRAME_IMPORT', + 'DELETE_FROM', 'DELETE_AFTER', 'DELETE_RANGE', 'DELETE_UNTIL', + 'DELETE_WEEKENDS', 'DELETE_TIME_RANGE', 'SERIES_TRUNCATED' + ) + LOOP + BEGIN + EXECUTE format( + 'SELECT MIN(timestamp), MAX(timestamp) FROM %I.%I', + _rec.db_schema, _rec.series_name + ) INTO _actual_first, _actual_last; + + SELECT first_timestamp, last_timestamp INTO _meta_first, _meta_last + FROM meta.series + WHERE table_name = _rec.series_name AND db_schema = _rec.db_schema; + + IF _meta_first IS DISTINCT FROM _actual_first + OR _meta_last IS DISTINCT FROM _actual_last THEN + INSERT INTO log.event_log + ("timestamp", series_name, db_schema, event_type, details, source, db_user) + VALUES ( + CURRENT_TIMESTAMP, + _rec.series_name, + _rec.db_schema, + 'TIMESTAMP_MISMATCH', + jsonb_build_object( + 'meta_first', _meta_first, + 'meta_last', _meta_last, + 'actual_first', _actual_first, + 'actual_last', _actual_last + ), + 'flush_verify', + CURRENT_USER + ); + END IF; + EXCEPTION WHEN OTHERS THEN + NULL; -- table may have been dropped in same batch + END; + END LOOP; + + -- 3. Clear staging + DELETE FROM log.pending_changes WHERE id <= batch_id; + + RETURN v_count; +END; +$function$; + + +-- Meta schema functions + +-- Function: meta.update_series_last_timestamp +-- Lightweight timestamp update for append operations. +-- Uses GREATEST to conditionally update last_timestamp (no table scan). +-- Sets first_timestamp on first insert via COALESCE. +-- p_first_timestamp is optional: when NULL, uses p_last_timestamp for first_timestamp. +-- For single inserts, only p_last_timestamp is needed. +-- For batch inserts, pass min timestamp as p_first_timestamp. +-- Both Python and C++ clients call this after appending data. +CREATE OR REPLACE FUNCTION meta.update_series_last_timestamp( + p_table_name TEXT, + p_db_schema TEXT DEFAULT 'series', + p_last_timestamp TIMESTAMP DEFAULT NULL, + p_first_timestamp TIMESTAMP DEFAULT NULL +) RETURNS void +LANGUAGE plpgsql +AS $function$ +BEGIN + UPDATE meta.series + SET first_timestamp = COALESCE(first_timestamp, p_first_timestamp, p_last_timestamp), + last_timestamp = GREATEST(last_timestamp, p_last_timestamp) + WHERE table_name = p_table_name AND db_schema = p_db_schema; +END; +$function$; + + +-- Function: meta.refresh_series_timestamps +-- Full timestamp refresh using MIN/MAX scan. +-- Used after deletes, truncates, or any operation that may change first_timestamp. +-- BRIN indexes make MIN/MAX fast even on large tables. +-- Both Python and C++ clients call this after data deletion. +CREATE OR REPLACE FUNCTION meta.refresh_series_timestamps( + p_table_name TEXT, + p_db_schema TEXT DEFAULT 'series' +) RETURNS void +LANGUAGE plpgsql +AS $function$ +DECLARE + _first_ts TIMESTAMP; + _last_ts TIMESTAMP; +BEGIN + EXECUTE format( + 'SELECT MIN(timestamp), MAX(timestamp) FROM %I.%I', + p_db_schema, p_table_name + ) INTO _first_ts, _last_ts; + + UPDATE meta.series + SET first_timestamp = _first_ts, last_timestamp = _last_ts + WHERE table_name = p_table_name AND db_schema = p_db_schema; +END; +$function$; + + +-- Function: meta.drop_all_series +CREATE OR REPLACE FUNCTION meta.drop_all_series() + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE + _rec record; +BEGIN + -- Set the context to allow drops + PERFORM set_config('application.dropping_series', 'true', true); + + -- Loop through all series in series table + FOR _rec IN SELECT "table_name", "db_schema" FROM meta.series + LOOP + -- Drop each table + EXECUTE format('DROP TABLE IF EXISTS %I.%I', _rec."db_schema", _rec."table_name"); + END LOOP; + + -- Clear the series table + DELETE FROM meta.series; + + -- Reset the context + PERFORM set_config('application.dropping_series', 'false', true); +END; +$function$; + + +-- Function: meta.drop_series +CREATE OR REPLACE FUNCTION meta.drop_series(_table_name text, _schema_name text DEFAULT 'series'::text, _raise_if_missing boolean DEFAULT true) + RETURNS void + LANGUAGE plpgsql +AS $function$ +BEGIN + -- Check if the series exists in series table + IF NOT EXISTS ( + SELECT 1 FROM meta.series + WHERE "table_name" = _table_name + AND "db_schema" = _schema_name + ) THEN + IF _raise_if_missing THEN + RAISE EXCEPTION 'Series %.% does not exist', _schema_name, _table_name; + END IF; + RETURN; + END IF; + + -- Set the context to allow drops + 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; + + -- Reset the context + PERFORM set_config('application.dropping_series', 'false', true); +END; +$function$; + + +-- Function: meta.drop_series_if_exists +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$; + + +-- Function: meta.prevent_direct_drop +CREATE OR REPLACE FUNCTION meta.prevent_direct_drop() + RETURNS event_trigger + LANGUAGE plpgsql +AS $function$ +DECLARE + obj record; +BEGIN + -- Get the current context + IF current_setting('application.dropping_series', true) = 'true' THEN + -- Allow the drop if it's coming from our procedures + RETURN; + END IF; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + WHERE object_type = 'table' + LOOP + IF EXISTS ( + SELECT 1 FROM meta.series + WHERE table_name = obj.object_name + AND db_schema = obj.schema_name + ) THEN + RAISE EXCEPTION 'Table %.% is registered in series and can only be dropped using TimeseriesDB.drop_series()', + obj.schema_name, obj.object_name; + END IF; + END LOOP; +END; +$function$; + + +-- Function: meta.read_series_batch +CREATE OR REPLACE FUNCTION meta.read_series_batch(p_schema_name text, p_table_name text, p_start_ts timestamp without time zone DEFAULT NULL::timestamp without time zone, p_batch_size integer DEFAULT 10000) + RETURNS TABLE(result_json jsonb) + LANGUAGE plpgsql +AS $function$ +DECLARE + v_query TEXT; + v_qualified_name TEXT; +BEGIN + -- Build qualified table name (with proper quoting) + v_qualified_name := format('%I.%I', p_schema_name, p_table_name); + + -- Build query with proper WHERE clause + IF p_start_ts IS NULL THEN + -- Read from beginning + v_query := format(' + SELECT to_jsonb(t.*) AS result_json + FROM ( + SELECT * + FROM %s + ORDER BY timestamp + LIMIT %s + ) t + ', v_qualified_name, p_batch_size); + ELSE + -- Read from specified timestamp (inclusive) + v_query := format(' + SELECT to_jsonb(t.*) AS result_json + FROM ( + SELECT * + FROM %s + WHERE timestamp >= %L + ORDER BY timestamp + LIMIT %s + ) t + ', v_qualified_name, p_start_ts, p_batch_size); + END IF; + + -- Execute and return + RETURN QUERY EXECUTE v_query; +END; +$function$; + + +-- Function: meta.read_series_batch_cursor +CREATE OR REPLACE FUNCTION meta.read_series_batch_cursor(p_schema_name text, p_table_name text, p_start_ts timestamp without time zone DEFAULT NULL::timestamp without time zone, p_batch_size integer DEFAULT 10000, p_cursor_name text DEFAULT 'series_cursor'::text) + RETURNS refcursor + LANGUAGE plpgsql +AS $function$ +DECLARE + v_query TEXT; + v_qualified_name TEXT; + v_cursor refcursor; +BEGIN + -- Build qualified table name (with proper quoting) + v_qualified_name := format('%I.%I', p_schema_name, p_table_name); + + -- Open cursor with name + v_cursor := p_cursor_name; + + -- Build query with proper WHERE clause + IF p_start_ts IS NULL THEN + -- Read from beginning + v_query := format(' + SELECT * + FROM %s + ORDER BY timestamp + LIMIT %s + ', v_qualified_name, p_batch_size); + ELSE + -- Read from specified timestamp (inclusive) + v_query := format(' + SELECT * + FROM %s + WHERE timestamp >= %L + ORDER BY timestamp + LIMIT %s + ', v_qualified_name, p_start_ts, p_batch_size); + END IF; + + -- Open cursor + OPEN v_cursor FOR EXECUTE v_query; + + RETURN v_cursor; +END; +$function$; + + +-- Function: meta.read_series_batch_ohlc +CREATE OR REPLACE FUNCTION meta.read_series_batch_ohlc(p_schema_name text, p_table_name text, p_start_ts timestamp without time zone DEFAULT NULL::timestamp without time zone, p_batch_size integer DEFAULT 10000) + RETURNS TABLE(ts timestamp without time zone, open double precision, high double precision, low double precision, close double precision, volume bigint) + LANGUAGE plpgsql +AS $function$ +DECLARE + v_query TEXT; + v_qualified_name TEXT; +BEGIN + -- Build qualified table name (with proper quoting) + v_qualified_name := format('%I.%I', p_schema_name, p_table_name); + + -- Build query with proper WHERE clause + IF p_start_ts IS NULL THEN + -- Read from beginning + v_query := format(' + SELECT timestamp AS ts, open, high, low, close, volume + FROM %s + ORDER BY timestamp + LIMIT %s + ', v_qualified_name, p_batch_size); + ELSE + -- Read from specified timestamp (inclusive) + v_query := format(' + SELECT timestamp AS ts, open, high, low, close, volume + FROM %s + WHERE timestamp >= %L + ORDER BY timestamp + LIMIT %s + ', v_qualified_name, p_start_ts, p_batch_size); + END IF; + + -- Execute and return + RETURN QUERY EXECUTE v_query; +END; +$function$; + diff --git a/sql/06_triggers.sql b/sql/06_triggers.sql new file mode 100644 index 0000000..9f2ebee --- /dev/null +++ b/sql/06_triggers.sql @@ -0,0 +1,37 @@ +-- Triggers and event triggers +-- Generated: 2025-10-15T12:24:56.582392 + +-- Event triggers +-- Event trigger: prevent_series_drops +CREATE OR REPLACE FUNCTION meta.prevent_direct_drop() + RETURNS event_trigger + LANGUAGE plpgsql +AS $function$ +DECLARE + obj record; +BEGIN + -- Get the current context + IF current_setting('application.dropping_series', true) = 'true' THEN + -- Allow the drop if it's coming from our procedures + RETURN; + END IF; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + WHERE object_type = 'table' + LOOP + IF EXISTS ( + SELECT 1 FROM meta.series + WHERE table_name = obj.object_name + AND db_schema = obj.schema_name + ) THEN + RAISE EXCEPTION 'Table %.% is registered in series and can only be dropped using TimeseriesDB.drop_series()', + obj.schema_name, obj.object_name; + END IF; + END LOOP; +END; +$function$; + + +CREATE EVENT TRIGGER prevent_series_drops + ON sql_drop + EXECUTE FUNCTION meta.prevent_direct_drop(); diff --git a/sql/07_reference_data.sql b/sql/07_reference_data.sql new file mode 100644 index 0000000..23c1fdd --- /dev/null +++ b/sql/07_reference_data.sql @@ -0,0 +1,769 @@ +-- Reference data (timezones and currencies) +-- Generated: 2025-10-15T12:24:56.582392 + +-- Timezones +INSERT INTO meta.timezones (iana_name, display_name, description, region, utc_offset_hours, notes) +VALUES + ('Africa/Abidjan', 'Africa/Abidjan', 'IANA timezone: Africa/Abidjan', 'Africa', 0.0, NULL), + ('Africa/Accra', 'Africa/Accra', 'IANA timezone: Africa/Accra', 'Africa', 0.0, NULL), + ('Africa/Addis_Ababa', 'Africa/Addis Ababa', 'IANA timezone: Africa/Addis_Ababa', 'Africa', 3.0, NULL), + ('Africa/Algiers', 'Africa/Algiers', 'IANA timezone: Africa/Algiers', 'Africa', 1.0, NULL), + ('Africa/Asmara', 'Africa/Asmara', 'IANA timezone: Africa/Asmara', 'Africa', 3.0, NULL), + ('Africa/Asmera', 'Africa/Asmera', 'IANA timezone: Africa/Asmera', 'Africa', 3.0, NULL), + ('Africa/Bamako', 'Africa/Bamako', 'IANA timezone: Africa/Bamako', 'Africa', 0.0, NULL), + ('Africa/Bangui', 'Africa/Bangui', 'IANA timezone: Africa/Bangui', 'Africa', 1.0, NULL), + ('Africa/Banjul', 'Africa/Banjul', 'IANA timezone: Africa/Banjul', 'Africa', 0.0, NULL), + ('Africa/Bissau', 'Africa/Bissau', 'IANA timezone: Africa/Bissau', 'Africa', 0.0, NULL), + ('Africa/Blantyre', 'Africa/Blantyre', 'IANA timezone: Africa/Blantyre', 'Africa', 2.0, NULL), + ('Africa/Brazzaville', 'Africa/Brazzaville', 'IANA timezone: Africa/Brazzaville', 'Africa', 1.0, NULL), + ('Africa/Bujumbura', 'Africa/Bujumbura', 'IANA timezone: Africa/Bujumbura', 'Africa', 2.0, NULL), + ('Africa/Cairo', 'Africa/Cairo', 'IANA timezone: Africa/Cairo', 'Africa', 3.0, NULL), + ('Africa/Casablanca', 'Africa/Casablanca', 'IANA timezone: Africa/Casablanca', 'Africa', 1.0, NULL), + ('Africa/Ceuta', 'Africa/Ceuta', 'IANA timezone: Africa/Ceuta', 'Africa', 2.0, NULL), + ('Africa/Conakry', 'Africa/Conakry', 'IANA timezone: Africa/Conakry', 'Africa', 0.0, NULL), + ('Africa/Dakar', 'Africa/Dakar', 'IANA timezone: Africa/Dakar', 'Africa', 0.0, NULL), + ('Africa/Dar_es_Salaam', 'Africa/Dar es Salaam', 'IANA timezone: Africa/Dar_es_Salaam', 'Africa', 3.0, NULL), + ('Africa/Djibouti', 'Africa/Djibouti', 'IANA timezone: Africa/Djibouti', 'Africa', 3.0, NULL), + ('Africa/Douala', 'Africa/Douala', 'IANA timezone: Africa/Douala', 'Africa', 1.0, NULL), + ('Africa/El_Aaiun', 'Africa/El Aaiun', 'IANA timezone: Africa/El_Aaiun', 'Africa', 1.0, NULL), + ('Africa/Freetown', 'Africa/Freetown', 'IANA timezone: Africa/Freetown', 'Africa', 0.0, NULL), + ('Africa/Gaborone', 'Africa/Gaborone', 'IANA timezone: Africa/Gaborone', 'Africa', 2.0, NULL), + ('Africa/Harare', 'Africa/Harare', 'IANA timezone: Africa/Harare', 'Africa', 2.0, NULL), + ('Africa/Johannesburg', 'Africa/Johannesburg', 'IANA timezone: Africa/Johannesburg', 'Africa', 2.0, NULL), + ('Africa/Juba', 'Africa/Juba', 'IANA timezone: Africa/Juba', 'Africa', 2.0, NULL), + ('Africa/Kampala', 'Africa/Kampala', 'IANA timezone: Africa/Kampala', 'Africa', 3.0, NULL), + ('Africa/Khartoum', 'Africa/Khartoum', 'IANA timezone: Africa/Khartoum', 'Africa', 2.0, NULL), + ('Africa/Kigali', 'Africa/Kigali', 'IANA timezone: Africa/Kigali', 'Africa', 2.0, NULL), + ('Africa/Kinshasa', 'Africa/Kinshasa', 'IANA timezone: Africa/Kinshasa', 'Africa', 1.0, NULL), + ('Africa/Lagos', 'Africa/Lagos', 'IANA timezone: Africa/Lagos', 'Africa', 1.0, NULL), + ('Africa/Libreville', 'Africa/Libreville', 'IANA timezone: Africa/Libreville', 'Africa', 1.0, NULL), + ('Africa/Lome', 'Africa/Lome', 'IANA timezone: Africa/Lome', 'Africa', 0.0, NULL), + ('Africa/Luanda', 'Africa/Luanda', 'IANA timezone: Africa/Luanda', 'Africa', 1.0, NULL), + ('Africa/Lubumbashi', 'Africa/Lubumbashi', 'IANA timezone: Africa/Lubumbashi', 'Africa', 2.0, NULL), + ('Africa/Lusaka', 'Africa/Lusaka', 'IANA timezone: Africa/Lusaka', 'Africa', 2.0, NULL), + ('Africa/Malabo', 'Africa/Malabo', 'IANA timezone: Africa/Malabo', 'Africa', 1.0, NULL), + ('Africa/Maputo', 'Africa/Maputo', 'IANA timezone: Africa/Maputo', 'Africa', 2.0, NULL), + ('Africa/Maseru', 'Africa/Maseru', 'IANA timezone: Africa/Maseru', 'Africa', 2.0, NULL), + ('Africa/Mbabane', 'Africa/Mbabane', 'IANA timezone: Africa/Mbabane', 'Africa', 2.0, NULL), + ('Africa/Mogadishu', 'Africa/Mogadishu', 'IANA timezone: Africa/Mogadishu', 'Africa', 3.0, NULL), + ('Africa/Monrovia', 'Africa/Monrovia', 'IANA timezone: Africa/Monrovia', 'Africa', 0.0, NULL), + ('Africa/Nairobi', 'Africa/Nairobi', 'IANA timezone: Africa/Nairobi', 'Africa', 3.0, NULL), + ('Africa/Ndjamena', 'Africa/Ndjamena', 'IANA timezone: Africa/Ndjamena', 'Africa', 1.0, NULL), + ('Africa/Niamey', 'Africa/Niamey', 'IANA timezone: Africa/Niamey', 'Africa', 1.0, NULL), + ('Africa/Nouakchott', 'Africa/Nouakchott', 'IANA timezone: Africa/Nouakchott', 'Africa', 0.0, NULL), + ('Africa/Ouagadougou', 'Africa/Ouagadougou', 'IANA timezone: Africa/Ouagadougou', 'Africa', 0.0, NULL), + ('Africa/Porto-Novo', 'Africa/Porto-Novo', 'IANA timezone: Africa/Porto-Novo', 'Africa', 1.0, NULL), + ('Africa/Sao_Tome', 'Africa/Sao Tome', 'IANA timezone: Africa/Sao_Tome', 'Africa', 0.0, NULL), + ('Africa/Timbuktu', 'Africa/Timbuktu', 'IANA timezone: Africa/Timbuktu', 'Africa', 0.0, NULL), + ('Africa/Tripoli', 'Africa/Tripoli', 'IANA timezone: Africa/Tripoli', 'Africa', 2.0, NULL), + ('Africa/Tunis', 'Africa/Tunis', 'IANA timezone: Africa/Tunis', 'Africa', 1.0, NULL), + ('Africa/Windhoek', 'Africa/Windhoek', 'IANA timezone: Africa/Windhoek', 'Africa', 2.0, NULL), + ('America/Adak', 'America/Adak', 'IANA timezone: America/Adak', 'America', -9.0, NULL), + ('America/Anchorage', 'America/Anchorage', 'IANA timezone: America/Anchorage', 'America', -8.0, NULL), + ('America/Anguilla', 'America/Anguilla', 'IANA timezone: America/Anguilla', 'America', -4.0, NULL), + ('America/Antigua', 'America/Antigua', 'IANA timezone: America/Antigua', 'America', -4.0, NULL), + ('America/Araguaina', 'America/Araguaina', 'IANA timezone: America/Araguaina', 'America', -3.0, NULL), + ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos Aires', 'IANA timezone: America/Argentina/Buenos_Aires', 'America', -3.0, NULL), + ('America/Argentina/Catamarca', 'America/Argentina/Catamarca', 'IANA timezone: America/Argentina/Catamarca', 'America', -3.0, NULL), + ('America/Argentina/ComodRivadavia', 'America/Argentina/ComodRivadavia', 'IANA timezone: America/Argentina/ComodRivadavia', 'America', -3.0, NULL), + ('America/Argentina/Cordoba', 'America/Argentina/Cordoba', 'IANA timezone: America/Argentina/Cordoba', 'America', -3.0, NULL), + ('America/Argentina/Jujuy', 'America/Argentina/Jujuy', 'IANA timezone: America/Argentina/Jujuy', 'America', -3.0, NULL), + ('America/Argentina/La_Rioja', 'America/Argentina/La Rioja', 'IANA timezone: America/Argentina/La_Rioja', 'America', -3.0, NULL), + ('America/Argentina/Mendoza', 'America/Argentina/Mendoza', 'IANA timezone: America/Argentina/Mendoza', 'America', -3.0, NULL), + ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio Gallegos', 'IANA timezone: America/Argentina/Rio_Gallegos', 'America', -3.0, NULL), + ('America/Argentina/Salta', 'America/Argentina/Salta', 'IANA timezone: America/Argentina/Salta', 'America', -3.0, NULL), + ('America/Argentina/San_Juan', 'America/Argentina/San Juan', 'IANA timezone: America/Argentina/San_Juan', 'America', -3.0, NULL), + ('America/Argentina/San_Luis', 'America/Argentina/San Luis', 'IANA timezone: America/Argentina/San_Luis', 'America', -3.0, NULL), + ('America/Argentina/Tucuman', 'America/Argentina/Tucuman', 'IANA timezone: America/Argentina/Tucuman', 'America', -3.0, NULL), + ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia', 'IANA timezone: America/Argentina/Ushuaia', 'America', -3.0, NULL), + ('America/Aruba', 'America/Aruba', 'IANA timezone: America/Aruba', 'America', -4.0, NULL), + ('America/Asuncion', 'America/Asuncion', 'IANA timezone: America/Asuncion', 'America', -3.0, NULL), + ('America/Atikokan', 'America/Atikokan', 'IANA timezone: America/Atikokan', 'America', -5.0, NULL), + ('America/Atka', 'America/Atka', 'IANA timezone: America/Atka', 'America', -9.0, NULL), + ('America/Bahia', 'America/Bahia', 'IANA timezone: America/Bahia', 'America', -3.0, NULL), + ('America/Bahia_Banderas', 'America/Bahia Banderas', 'IANA timezone: America/Bahia_Banderas', 'America', -6.0, NULL), + ('America/Barbados', 'America/Barbados', 'IANA timezone: America/Barbados', 'America', -4.0, NULL), + ('America/Belem', 'America/Belem', 'IANA timezone: America/Belem', 'America', -3.0, NULL), + ('America/Belize', 'America/Belize', 'IANA timezone: America/Belize', 'America', -6.0, NULL), + ('America/Blanc-Sablon', 'America/Blanc-Sablon', 'IANA timezone: America/Blanc-Sablon', 'America', -4.0, NULL), + ('America/Boa_Vista', 'America/Boa Vista', 'IANA timezone: America/Boa_Vista', 'America', -4.0, NULL), + ('America/Bogota', 'America/Bogota', 'IANA timezone: America/Bogota', 'America', -5.0, NULL), + ('America/Boise', 'America/Boise', 'IANA timezone: America/Boise', 'America', -6.0, NULL), + ('America/Buenos_Aires', 'America/Buenos Aires', 'IANA timezone: America/Buenos_Aires', 'America', -3.0, NULL), + ('America/Cambridge_Bay', 'America/Cambridge Bay', 'IANA timezone: America/Cambridge_Bay', 'America', -6.0, NULL), + ('America/Campo_Grande', 'America/Campo Grande', 'IANA timezone: America/Campo_Grande', 'America', -4.0, NULL), + ('America/Cancun', 'America/Cancun', 'IANA timezone: America/Cancun', 'America', -5.0, NULL), + ('America/Caracas', 'America/Caracas', 'IANA timezone: America/Caracas', 'America', -4.0, NULL), + ('America/Catamarca', 'America/Catamarca', 'IANA timezone: America/Catamarca', 'America', -3.0, NULL), + ('America/Cayenne', 'America/Cayenne', 'IANA timezone: America/Cayenne', 'America', -3.0, NULL), + ('America/Cayman', 'America/Cayman', 'IANA timezone: America/Cayman', 'America', -5.0, NULL), + ('America/Chicago', 'America/Chicago', 'IANA timezone: America/Chicago', 'America', -5.0, NULL), + ('America/Chihuahua', 'America/Chihuahua', 'IANA timezone: America/Chihuahua', 'America', -6.0, NULL), + ('America/Ciudad_Juarez', 'America/Ciudad Juarez', 'IANA timezone: America/Ciudad_Juarez', 'America', -6.0, NULL), + ('America/Coral_Harbour', 'America/Coral Harbour', 'IANA timezone: America/Coral_Harbour', 'America', -5.0, NULL), + ('America/Cordoba', 'America/Cordoba', 'IANA timezone: America/Cordoba', 'America', -3.0, NULL), + ('America/Costa_Rica', 'America/Costa Rica', 'IANA timezone: America/Costa_Rica', 'America', -6.0, NULL), + ('America/Coyhaique', 'America/Coyhaique', 'IANA timezone: America/Coyhaique', 'America', -3.0, NULL), + ('America/Creston', 'America/Creston', 'IANA timezone: America/Creston', 'America', -7.0, NULL), + ('America/Cuiaba', 'America/Cuiaba', 'IANA timezone: America/Cuiaba', 'America', -4.0, NULL), + ('America/Curacao', 'America/Curacao', 'IANA timezone: America/Curacao', 'America', -4.0, NULL), + ('America/Danmarkshavn', 'America/Danmarkshavn', 'IANA timezone: America/Danmarkshavn', 'America', 0.0, NULL), + ('America/Dawson', 'America/Dawson', 'IANA timezone: America/Dawson', 'America', -7.0, NULL), + ('America/Dawson_Creek', 'America/Dawson Creek', 'IANA timezone: America/Dawson_Creek', 'America', -7.0, NULL), + ('America/Denver', 'America/Denver', 'IANA timezone: America/Denver', 'America', -6.0, NULL), + ('America/Detroit', 'America/Detroit', 'IANA timezone: America/Detroit', 'America', -4.0, NULL), + ('America/Dominica', 'America/Dominica', 'IANA timezone: America/Dominica', 'America', -4.0, NULL), + ('America/Edmonton', 'America/Edmonton', 'IANA timezone: America/Edmonton', 'America', -6.0, NULL), + ('America/Eirunepe', 'America/Eirunepe', 'IANA timezone: America/Eirunepe', 'America', -5.0, NULL), + ('America/El_Salvador', 'America/El Salvador', 'IANA timezone: America/El_Salvador', 'America', -6.0, NULL), + ('America/Ensenada', 'America/Ensenada', 'IANA timezone: America/Ensenada', 'America', -7.0, NULL), + ('America/Fortaleza', 'America/Fortaleza', 'IANA timezone: America/Fortaleza', 'America', -3.0, NULL), + ('America/Fort_Nelson', 'America/Fort Nelson', 'IANA timezone: America/Fort_Nelson', 'America', -7.0, NULL), + ('America/Fort_Wayne', 'America/Fort Wayne', 'IANA timezone: America/Fort_Wayne', 'America', -4.0, NULL), + ('America/Glace_Bay', 'America/Glace Bay', 'IANA timezone: America/Glace_Bay', 'America', -3.0, NULL), + ('America/Godthab', 'America/Godthab', 'IANA timezone: America/Godthab', 'America', -1.0, NULL), + ('America/Goose_Bay', 'America/Goose Bay', 'IANA timezone: America/Goose_Bay', 'America', -3.0, NULL), + ('America/Grand_Turk', 'America/Grand Turk', 'IANA timezone: America/Grand_Turk', 'America', -4.0, NULL), + ('America/Grenada', 'America/Grenada', 'IANA timezone: America/Grenada', 'America', -4.0, NULL), + ('America/Guadeloupe', 'America/Guadeloupe', 'IANA timezone: America/Guadeloupe', 'America', -4.0, NULL), + ('America/Guatemala', 'America/Guatemala', 'IANA timezone: America/Guatemala', 'America', -6.0, NULL), + ('America/Guayaquil', 'America/Guayaquil', 'IANA timezone: America/Guayaquil', 'America', -5.0, NULL), + ('America/Guyana', 'America/Guyana', 'IANA timezone: America/Guyana', 'America', -4.0, NULL), + ('America/Halifax', 'America/Halifax', 'IANA timezone: America/Halifax', 'America', -3.0, NULL), + ('America/Havana', 'America/Havana', 'IANA timezone: America/Havana', 'America', -4.0, NULL), + ('America/Hermosillo', 'America/Hermosillo', 'IANA timezone: America/Hermosillo', 'America', -7.0, NULL), + ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis', 'IANA timezone: America/Indiana/Indianapolis', 'America', -4.0, NULL), + ('America/Indiana/Knox', 'America/Indiana/Knox', 'IANA timezone: America/Indiana/Knox', 'America', -5.0, NULL), + ('America/Indiana/Marengo', 'America/Indiana/Marengo', 'IANA timezone: America/Indiana/Marengo', 'America', -4.0, NULL), + ('America/Indiana/Petersburg', 'America/Indiana/Petersburg', 'IANA timezone: America/Indiana/Petersburg', 'America', -4.0, NULL), + ('America/Indianapolis', 'America/Indianapolis', 'IANA timezone: America/Indianapolis', 'America', -4.0, NULL), + ('America/Indiana/Tell_City', 'America/Indiana/Tell City', 'IANA timezone: America/Indiana/Tell_City', 'America', -5.0, NULL), + ('America/Indiana/Vevay', 'America/Indiana/Vevay', 'IANA timezone: America/Indiana/Vevay', 'America', -4.0, NULL), + ('America/Indiana/Vincennes', 'America/Indiana/Vincennes', 'IANA timezone: America/Indiana/Vincennes', 'America', -4.0, NULL), + ('America/Indiana/Winamac', 'America/Indiana/Winamac', 'IANA timezone: America/Indiana/Winamac', 'America', -4.0, NULL), + ('America/Inuvik', 'America/Inuvik', 'IANA timezone: America/Inuvik', 'America', -6.0, NULL), + ('America/Iqaluit', 'America/Iqaluit', 'IANA timezone: America/Iqaluit', 'America', -4.0, NULL), + ('America/Jamaica', 'America/Jamaica', 'IANA timezone: America/Jamaica', 'America', -5.0, NULL), + ('America/Jujuy', 'America/Jujuy', 'IANA timezone: America/Jujuy', 'America', -3.0, NULL), + ('America/Juneau', 'America/Juneau', 'IANA timezone: America/Juneau', 'America', -8.0, NULL), + ('America/Kentucky/Louisville', 'America/Kentucky/Louisville', 'IANA timezone: America/Kentucky/Louisville', 'America', -4.0, NULL), + ('America/Kentucky/Monticello', 'America/Kentucky/Monticello', 'IANA timezone: America/Kentucky/Monticello', 'America', -4.0, NULL), + ('America/Knox_IN', 'America/Knox IN', 'IANA timezone: America/Knox_IN', 'America', -5.0, NULL), + ('America/Kralendijk', 'America/Kralendijk', 'IANA timezone: America/Kralendijk', 'America', -4.0, NULL), + ('America/La_Paz', 'America/La Paz', 'IANA timezone: America/La_Paz', 'America', -4.0, NULL), + ('America/Lima', 'America/Lima', 'IANA timezone: America/Lima', 'America', -5.0, NULL), + ('America/Los_Angeles', 'America/Los Angeles', 'IANA timezone: America/Los_Angeles', 'America', -7.0, NULL), + ('America/Louisville', 'America/Louisville', 'IANA timezone: America/Louisville', 'America', -4.0, NULL), + ('America/Lower_Princes', 'America/Lower Princes', 'IANA timezone: America/Lower_Princes', 'America', -4.0, NULL), + ('America/Maceio', 'America/Maceio', 'IANA timezone: America/Maceio', 'America', -3.0, NULL), + ('America/Managua', 'America/Managua', 'IANA timezone: America/Managua', 'America', -6.0, NULL), + ('America/Manaus', 'America/Manaus', 'IANA timezone: America/Manaus', 'America', -4.0, NULL), + ('America/Marigot', 'America/Marigot', 'IANA timezone: America/Marigot', 'America', -4.0, NULL), + ('America/Martinique', 'America/Martinique', 'IANA timezone: America/Martinique', 'America', -4.0, NULL), + ('America/Matamoros', 'America/Matamoros', 'IANA timezone: America/Matamoros', 'America', -5.0, NULL), + ('America/Mazatlan', 'America/Mazatlan', 'IANA timezone: America/Mazatlan', 'America', -7.0, NULL), + ('America/Mendoza', 'America/Mendoza', 'IANA timezone: America/Mendoza', 'America', -3.0, NULL), + ('America/Menominee', 'America/Menominee', 'IANA timezone: America/Menominee', 'America', -5.0, NULL), + ('America/Merida', 'America/Merida', 'IANA timezone: America/Merida', 'America', -6.0, NULL), + ('America/Metlakatla', 'America/Metlakatla', 'IANA timezone: America/Metlakatla', 'America', -8.0, NULL), + ('America/Mexico_City', 'America/Mexico City', 'IANA timezone: America/Mexico_City', 'America', -6.0, NULL), + ('America/Miquelon', 'America/Miquelon', 'IANA timezone: America/Miquelon', 'America', -2.0, NULL), + ('America/Moncton', 'America/Moncton', 'IANA timezone: America/Moncton', 'America', -3.0, NULL), + ('America/Monterrey', 'America/Monterrey', 'IANA timezone: America/Monterrey', 'America', -6.0, NULL), + ('America/Montevideo', 'America/Montevideo', 'IANA timezone: America/Montevideo', 'America', -3.0, NULL), + ('America/Montreal', 'America/Montreal', 'IANA timezone: America/Montreal', 'America', -4.0, NULL), + ('America/Montserrat', 'America/Montserrat', 'IANA timezone: America/Montserrat', 'America', -4.0, NULL), + ('America/Nassau', 'America/Nassau', 'IANA timezone: America/Nassau', 'America', -4.0, NULL), + ('America/New_York', 'America/New York', 'IANA timezone: America/New_York', 'America', -4.0, NULL), + ('America/Nipigon', 'America/Nipigon', 'IANA timezone: America/Nipigon', 'America', -4.0, NULL), + ('America/Nome', 'America/Nome', 'IANA timezone: America/Nome', 'America', -8.0, NULL), + ('America/Noronha', 'America/Noronha', 'IANA timezone: America/Noronha', 'America', -2.0, NULL), + ('America/North_Dakota/Beulah', 'America/North Dakota/Beulah', 'IANA timezone: America/North_Dakota/Beulah', 'America', -5.0, NULL), + ('America/North_Dakota/Center', 'America/North Dakota/Center', 'IANA timezone: America/North_Dakota/Center', 'America', -5.0, NULL), + ('America/North_Dakota/New_Salem', 'America/North Dakota/New Salem', 'IANA timezone: America/North_Dakota/New_Salem', 'America', -5.0, NULL), + ('America/Nuuk', 'America/Nuuk', 'IANA timezone: America/Nuuk', 'America', -1.0, NULL), + ('America/Ojinaga', 'America/Ojinaga', 'IANA timezone: America/Ojinaga', 'America', -5.0, NULL), + ('America/Panama', 'America/Panama', 'IANA timezone: America/Panama', 'America', -5.0, NULL), + ('America/Pangnirtung', 'America/Pangnirtung', 'IANA timezone: America/Pangnirtung', 'America', -4.0, NULL), + ('America/Paramaribo', 'America/Paramaribo', 'IANA timezone: America/Paramaribo', 'America', -3.0, NULL), + ('America/Phoenix', 'America/Phoenix', 'IANA timezone: America/Phoenix', 'America', -7.0, NULL), + ('America/Port-au-Prince', 'America/Port-au-Prince', 'IANA timezone: America/Port-au-Prince', 'America', -4.0, NULL), + ('America/Porto_Acre', 'America/Porto Acre', 'IANA timezone: America/Porto_Acre', 'America', -5.0, NULL), + ('America/Port_of_Spain', 'America/Port of Spain', 'IANA timezone: America/Port_of_Spain', 'America', -4.0, NULL), + ('America/Porto_Velho', 'America/Porto Velho', 'IANA timezone: America/Porto_Velho', 'America', -4.0, NULL), + ('America/Puerto_Rico', 'America/Puerto Rico', 'IANA timezone: America/Puerto_Rico', 'America', -4.0, NULL), + ('America/Punta_Arenas', 'America/Punta Arenas', 'IANA timezone: America/Punta_Arenas', 'America', -3.0, NULL), + ('America/Rainy_River', 'America/Rainy River', 'IANA timezone: America/Rainy_River', 'America', -5.0, NULL), + ('America/Rankin_Inlet', 'America/Rankin Inlet', 'IANA timezone: America/Rankin_Inlet', 'America', -5.0, NULL), + ('America/Recife', 'America/Recife', 'IANA timezone: America/Recife', 'America', -3.0, NULL), + ('America/Regina', 'America/Regina', 'IANA timezone: America/Regina', 'America', -6.0, NULL), + ('America/Resolute', 'America/Resolute', 'IANA timezone: America/Resolute', 'America', -5.0, NULL), + ('America/Rio_Branco', 'America/Rio Branco', 'IANA timezone: America/Rio_Branco', 'America', -5.0, NULL), + ('America/Rosario', 'America/Rosario', 'IANA timezone: America/Rosario', 'America', -3.0, NULL), + ('America/Santa_Isabel', 'America/Santa Isabel', 'IANA timezone: America/Santa_Isabel', 'America', -7.0, NULL), + ('America/Santarem', 'America/Santarem', 'IANA timezone: America/Santarem', 'America', -3.0, NULL), + ('America/Santiago', 'America/Santiago', 'IANA timezone: America/Santiago', 'America', -3.0, NULL), + ('America/Santo_Domingo', 'America/Santo Domingo', 'IANA timezone: America/Santo_Domingo', 'America', -4.0, NULL), + ('America/Sao_Paulo', 'America/Sao Paulo', 'IANA timezone: America/Sao_Paulo', 'America', -3.0, NULL), + ('America/Scoresbysund', 'America/Scoresbysund', 'IANA timezone: America/Scoresbysund', 'America', -1.0, NULL), + ('America/Shiprock', 'America/Shiprock', 'IANA timezone: America/Shiprock', 'America', -6.0, NULL), + ('America/Sitka', 'America/Sitka', 'IANA timezone: America/Sitka', 'America', -8.0, NULL), + ('America/St_Barthelemy', 'America/St Barthelemy', 'IANA timezone: America/St_Barthelemy', 'America', -4.0, NULL), + ('America/St_Johns', 'America/St Johns', 'IANA timezone: America/St_Johns', 'America', -2.5, NULL), + ('America/St_Kitts', 'America/St Kitts', 'IANA timezone: America/St_Kitts', 'America', -4.0, NULL), + ('America/St_Lucia', 'America/St Lucia', 'IANA timezone: America/St_Lucia', 'America', -4.0, NULL), + ('America/St_Thomas', 'America/St Thomas', 'IANA timezone: America/St_Thomas', 'America', -4.0, NULL), + ('America/St_Vincent', 'America/St Vincent', 'IANA timezone: America/St_Vincent', 'America', -4.0, NULL), + ('America/Swift_Current', 'America/Swift Current', 'IANA timezone: America/Swift_Current', 'America', -6.0, NULL), + ('America/Tegucigalpa', 'America/Tegucigalpa', 'IANA timezone: America/Tegucigalpa', 'America', -6.0, NULL), + ('America/Thule', 'America/Thule', 'IANA timezone: America/Thule', 'America', -3.0, NULL), + ('America/Thunder_Bay', 'America/Thunder Bay', 'IANA timezone: America/Thunder_Bay', 'America', -4.0, NULL), + ('America/Tijuana', 'America/Tijuana', 'IANA timezone: America/Tijuana', 'America', -7.0, NULL), + ('America/Toronto', 'America/Toronto', 'IANA timezone: America/Toronto', 'America', -4.0, NULL), + ('America/Tortola', 'America/Tortola', 'IANA timezone: America/Tortola', 'America', -4.0, NULL), + ('America/Vancouver', 'America/Vancouver', 'IANA timezone: America/Vancouver', 'America', -7.0, NULL), + ('America/Virgin', 'America/Virgin', 'IANA timezone: America/Virgin', 'America', -4.0, NULL), + ('America/Whitehorse', 'America/Whitehorse', 'IANA timezone: America/Whitehorse', 'America', -7.0, NULL), + ('America/Winnipeg', 'America/Winnipeg', 'IANA timezone: America/Winnipeg', 'America', -5.0, NULL), + ('America/Yakutat', 'America/Yakutat', 'IANA timezone: America/Yakutat', 'America', -8.0, NULL), + ('America/Yellowknife', 'America/Yellowknife', 'IANA timezone: America/Yellowknife', 'America', -6.0, NULL), + ('Antarctica/Casey', 'Antarctica/Casey', 'IANA timezone: Antarctica/Casey', 'Antarctica', 8.0, NULL), + ('Antarctica/Davis', 'Antarctica/Davis', 'IANA timezone: Antarctica/Davis', 'Antarctica', 7.0, NULL), + ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville', 'IANA timezone: Antarctica/DumontDUrville', 'Antarctica', 10.0, NULL), + ('Antarctica/Macquarie', 'Antarctica/Macquarie', 'IANA timezone: Antarctica/Macquarie', 'Antarctica', 11.0, NULL), + ('Antarctica/Mawson', 'Antarctica/Mawson', 'IANA timezone: Antarctica/Mawson', 'Antarctica', 5.0, NULL), + ('Antarctica/McMurdo', 'Antarctica/McMurdo', 'IANA timezone: Antarctica/McMurdo', 'Antarctica', 13.0, NULL), + ('Antarctica/Palmer', 'Antarctica/Palmer', 'IANA timezone: Antarctica/Palmer', 'Antarctica', -3.0, NULL), + ('Antarctica/Rothera', 'Antarctica/Rothera', 'IANA timezone: Antarctica/Rothera', 'Antarctica', -3.0, NULL), + ('Antarctica/South_Pole', 'Antarctica/South Pole', 'IANA timezone: Antarctica/South_Pole', 'Antarctica', 13.0, NULL), + ('Antarctica/Syowa', 'Antarctica/Syowa', 'IANA timezone: Antarctica/Syowa', 'Antarctica', 3.0, NULL), + ('Antarctica/Troll', 'Antarctica/Troll', 'IANA timezone: Antarctica/Troll', 'Antarctica', 2.0, NULL), + ('Antarctica/Vostok', 'Antarctica/Vostok', 'IANA timezone: Antarctica/Vostok', 'Antarctica', 5.0, NULL), + ('Arctic/Longyearbyen', 'Arctic/Longyearbyen', 'IANA timezone: Arctic/Longyearbyen', 'Arctic', 2.0, NULL), + ('Asia/Aden', 'Asia/Aden', 'IANA timezone: Asia/Aden', 'Asia', 3.0, NULL), + ('Asia/Almaty', 'Asia/Almaty', 'IANA timezone: Asia/Almaty', 'Asia', 5.0, NULL), + ('Asia/Amman', 'Asia/Amman', 'IANA timezone: Asia/Amman', 'Asia', 3.0, NULL), + ('Asia/Anadyr', 'Asia/Anadyr', 'IANA timezone: Asia/Anadyr', 'Asia', 12.0, NULL), + ('Asia/Aqtau', 'Asia/Aqtau', 'IANA timezone: Asia/Aqtau', 'Asia', 5.0, NULL), + ('Asia/Aqtobe', 'Asia/Aqtobe', 'IANA timezone: Asia/Aqtobe', 'Asia', 5.0, NULL), + ('Asia/Ashgabat', 'Asia/Ashgabat', 'IANA timezone: Asia/Ashgabat', 'Asia', 5.0, NULL), + ('Asia/Ashkhabad', 'Asia/Ashkhabad', 'IANA timezone: Asia/Ashkhabad', 'Asia', 5.0, NULL), + ('Asia/Atyrau', 'Asia/Atyrau', 'IANA timezone: Asia/Atyrau', 'Asia', 5.0, NULL), + ('Asia/Baghdad', 'Asia/Baghdad', 'IANA timezone: Asia/Baghdad', 'Asia', 3.0, NULL), + ('Asia/Bahrain', 'Asia/Bahrain', 'IANA timezone: Asia/Bahrain', 'Asia', 3.0, NULL), + ('Asia/Baku', 'Asia/Baku', 'IANA timezone: Asia/Baku', 'Asia', 4.0, NULL), + ('Asia/Bangkok', 'Asia/Bangkok', 'IANA timezone: Asia/Bangkok', 'Asia', 7.0, NULL), + ('Asia/Barnaul', 'Asia/Barnaul', 'IANA timezone: Asia/Barnaul', 'Asia', 7.0, NULL), + ('Asia/Beirut', 'Asia/Beirut', 'IANA timezone: Asia/Beirut', 'Asia', 3.0, NULL), + ('Asia/Bishkek', 'Asia/Bishkek', 'IANA timezone: Asia/Bishkek', 'Asia', 6.0, NULL), + ('Asia/Brunei', 'Asia/Brunei', 'IANA timezone: Asia/Brunei', 'Asia', 8.0, NULL), + ('Asia/Calcutta', 'Asia/Calcutta', 'IANA timezone: Asia/Calcutta', 'Asia', 5.5, NULL), + ('Asia/Chita', 'Asia/Chita', 'IANA timezone: Asia/Chita', 'Asia', 9.0, NULL), + ('Asia/Choibalsan', 'Asia/Choibalsan', 'IANA timezone: Asia/Choibalsan', 'Asia', 8.0, NULL), + ('Asia/Chongqing', 'Asia/Chongqing', 'IANA timezone: Asia/Chongqing', 'Asia', 8.0, NULL), + ('Asia/Chungking', 'Asia/Chungking', 'IANA timezone: Asia/Chungking', 'Asia', 8.0, NULL), + ('Asia/Colombo', 'Asia/Colombo', 'IANA timezone: Asia/Colombo', 'Asia', 5.5, NULL), + ('Asia/Dacca', 'Asia/Dacca', 'IANA timezone: Asia/Dacca', 'Asia', 6.0, NULL), + ('Asia/Damascus', 'Asia/Damascus', 'IANA timezone: Asia/Damascus', 'Asia', 3.0, NULL), + ('Asia/Dhaka', 'Asia/Dhaka', 'IANA timezone: Asia/Dhaka', 'Asia', 6.0, NULL), + ('Asia/Dili', 'Asia/Dili', 'IANA timezone: Asia/Dili', 'Asia', 9.0, NULL), + ('Asia/Dubai', 'Asia/Dubai', 'IANA timezone: Asia/Dubai', 'Asia', 4.0, NULL), + ('Asia/Dushanbe', 'Asia/Dushanbe', 'IANA timezone: Asia/Dushanbe', 'Asia', 5.0, NULL), + ('Asia/Famagusta', 'Asia/Famagusta', 'IANA timezone: Asia/Famagusta', 'Asia', 3.0, NULL), + ('Asia/Gaza', 'Asia/Gaza', 'IANA timezone: Asia/Gaza', 'Asia', 3.0, NULL), + ('Asia/Harbin', 'Asia/Harbin', 'IANA timezone: Asia/Harbin', 'Asia', 8.0, NULL), + ('Asia/Hebron', 'Asia/Hebron', 'IANA timezone: Asia/Hebron', 'Asia', 3.0, NULL), + ('Asia/Ho_Chi_Minh', 'Asia/Ho Chi Minh', 'IANA timezone: Asia/Ho_Chi_Minh', 'Asia', 7.0, NULL), + ('Asia/Hong_Kong', 'Asia/Hong Kong', 'IANA timezone: Asia/Hong_Kong', 'Asia', 8.0, NULL), + ('Asia/Hovd', 'Asia/Hovd', 'IANA timezone: Asia/Hovd', 'Asia', 7.0, NULL), + ('Asia/Irkutsk', 'Asia/Irkutsk', 'IANA timezone: Asia/Irkutsk', 'Asia', 8.0, NULL), + ('Asia/Istanbul', 'Asia/Istanbul', 'IANA timezone: Asia/Istanbul', 'Asia', 3.0, NULL), + ('Asia/Jakarta', 'Asia/Jakarta', 'IANA timezone: Asia/Jakarta', 'Asia', 7.0, NULL), + ('Asia/Jayapura', 'Asia/Jayapura', 'IANA timezone: Asia/Jayapura', 'Asia', 9.0, NULL), + ('Asia/Jerusalem', 'Asia/Jerusalem', 'IANA timezone: Asia/Jerusalem', 'Asia', 3.0, NULL), + ('Asia/Kabul', 'Asia/Kabul', 'IANA timezone: Asia/Kabul', 'Asia', 4.5, NULL), + ('Asia/Kamchatka', 'Asia/Kamchatka', 'IANA timezone: Asia/Kamchatka', 'Asia', 12.0, NULL), + ('Asia/Karachi', 'Asia/Karachi', 'IANA timezone: Asia/Karachi', 'Asia', 5.0, NULL), + ('Asia/Kashgar', 'Asia/Kashgar', 'IANA timezone: Asia/Kashgar', 'Asia', 6.0, NULL), + ('Asia/Kathmandu', 'Asia/Kathmandu', 'IANA timezone: Asia/Kathmandu', 'Asia', 5.75, NULL), + ('Asia/Katmandu', 'Asia/Katmandu', 'IANA timezone: Asia/Katmandu', 'Asia', 5.75, NULL), + ('Asia/Khandyga', 'Asia/Khandyga', 'IANA timezone: Asia/Khandyga', 'Asia', 9.0, NULL), + ('Asia/Kolkata', 'Asia/Kolkata', 'IANA timezone: Asia/Kolkata', 'Asia', 5.5, NULL), + ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk', 'IANA timezone: Asia/Krasnoyarsk', 'Asia', 7.0, NULL), + ('Asia/Kuala_Lumpur', 'Asia/Kuala Lumpur', 'IANA timezone: Asia/Kuala_Lumpur', 'Asia', 8.0, NULL), + ('Asia/Kuching', 'Asia/Kuching', 'IANA timezone: Asia/Kuching', 'Asia', 8.0, NULL), + ('Asia/Kuwait', 'Asia/Kuwait', 'IANA timezone: Asia/Kuwait', 'Asia', 3.0, NULL), + ('Asia/Macao', 'Asia/Macao', 'IANA timezone: Asia/Macao', 'Asia', 8.0, NULL), + ('Asia/Macau', 'Asia/Macau', 'IANA timezone: Asia/Macau', 'Asia', 8.0, NULL), + ('Asia/Magadan', 'Asia/Magadan', 'IANA timezone: Asia/Magadan', 'Asia', 11.0, NULL), + ('Asia/Makassar', 'Asia/Makassar', 'IANA timezone: Asia/Makassar', 'Asia', 8.0, NULL), + ('Asia/Manila', 'Asia/Manila', 'IANA timezone: Asia/Manila', 'Asia', 8.0, NULL), + ('Asia/Muscat', 'Asia/Muscat', 'IANA timezone: Asia/Muscat', 'Asia', 4.0, NULL), + ('Asia/Nicosia', 'Asia/Nicosia', 'IANA timezone: Asia/Nicosia', 'Asia', 3.0, NULL), + ('Asia/Novokuznetsk', 'Asia/Novokuznetsk', 'IANA timezone: Asia/Novokuznetsk', 'Asia', 7.0, NULL), + ('Asia/Novosibirsk', 'Asia/Novosibirsk', 'IANA timezone: Asia/Novosibirsk', 'Asia', 7.0, NULL), + ('Asia/Omsk', 'Asia/Omsk', 'IANA timezone: Asia/Omsk', 'Asia', 6.0, NULL), + ('Asia/Oral', 'Asia/Oral', 'IANA timezone: Asia/Oral', 'Asia', 5.0, NULL), + ('Asia/Phnom_Penh', 'Asia/Phnom Penh', 'IANA timezone: Asia/Phnom_Penh', 'Asia', 7.0, NULL), + ('Asia/Pontianak', 'Asia/Pontianak', 'IANA timezone: Asia/Pontianak', 'Asia', 7.0, NULL), + ('Asia/Pyongyang', 'Asia/Pyongyang', 'IANA timezone: Asia/Pyongyang', 'Asia', 9.0, NULL), + ('Asia/Qatar', 'Asia/Qatar', 'IANA timezone: Asia/Qatar', 'Asia', 3.0, NULL), + ('Asia/Qostanay', 'Asia/Qostanay', 'IANA timezone: Asia/Qostanay', 'Asia', 5.0, NULL), + ('Asia/Qyzylorda', 'Asia/Qyzylorda', 'IANA timezone: Asia/Qyzylorda', 'Asia', 5.0, NULL), + ('Asia/Rangoon', 'Asia/Rangoon', 'IANA timezone: Asia/Rangoon', 'Asia', 6.5, NULL), + ('Asia/Riyadh', 'Asia/Riyadh', 'IANA timezone: Asia/Riyadh', 'Asia', 3.0, NULL), + ('Asia/Saigon', 'Asia/Saigon', 'IANA timezone: Asia/Saigon', 'Asia', 7.0, NULL), + ('Asia/Sakhalin', 'Asia/Sakhalin', 'IANA timezone: Asia/Sakhalin', 'Asia', 11.0, NULL), + ('Asia/Samarkand', 'Asia/Samarkand', 'IANA timezone: Asia/Samarkand', 'Asia', 5.0, NULL), + ('Asia/Seoul', 'Asia/Seoul', 'IANA timezone: Asia/Seoul', 'Asia', 9.0, NULL), + ('Asia/Shanghai', 'Asia/Shanghai', 'IANA timezone: Asia/Shanghai', 'Asia', 8.0, NULL), + ('Asia/Singapore', 'Asia/Singapore', 'IANA timezone: Asia/Singapore', 'Asia', 8.0, NULL), + ('Asia/Srednekolymsk', 'Asia/Srednekolymsk', 'IANA timezone: Asia/Srednekolymsk', 'Asia', 11.0, NULL), + ('Asia/Taipei', 'Asia/Taipei', 'IANA timezone: Asia/Taipei', 'Asia', 8.0, NULL), + ('Asia/Tashkent', 'Asia/Tashkent', 'IANA timezone: Asia/Tashkent', 'Asia', 5.0, NULL), + ('Asia/Tbilisi', 'Asia/Tbilisi', 'IANA timezone: Asia/Tbilisi', 'Asia', 4.0, NULL), + ('Asia/Tehran', 'Asia/Tehran', 'IANA timezone: Asia/Tehran', 'Asia', 3.5, NULL), + ('Asia/Tel_Aviv', 'Asia/Tel Aviv', 'IANA timezone: Asia/Tel_Aviv', 'Asia', 3.0, NULL), + ('Asia/Thimbu', 'Asia/Thimbu', 'IANA timezone: Asia/Thimbu', 'Asia', 6.0, NULL), + ('Asia/Thimphu', 'Asia/Thimphu', 'IANA timezone: Asia/Thimphu', 'Asia', 6.0, NULL), + ('Asia/Tokyo', 'Asia/Tokyo', 'IANA timezone: Asia/Tokyo', 'Asia', 9.0, NULL), + ('Asia/Tomsk', 'Asia/Tomsk', 'IANA timezone: Asia/Tomsk', 'Asia', 7.0, NULL), + ('Asia/Ujung_Pandang', 'Asia/Ujung Pandang', 'IANA timezone: Asia/Ujung_Pandang', 'Asia', 8.0, NULL), + ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar', 'IANA timezone: Asia/Ulaanbaatar', 'Asia', 8.0, NULL), + ('Asia/Ulan_Bator', 'Asia/Ulan Bator', 'IANA timezone: Asia/Ulan_Bator', 'Asia', 8.0, NULL), + ('Asia/Urumqi', 'Asia/Urumqi', 'IANA timezone: Asia/Urumqi', 'Asia', 6.0, NULL), + ('Asia/Ust-Nera', 'Asia/Ust-Nera', 'IANA timezone: Asia/Ust-Nera', 'Asia', 10.0, NULL), + ('Asia/Vientiane', 'Asia/Vientiane', 'IANA timezone: Asia/Vientiane', 'Asia', 7.0, NULL), + ('Asia/Vladivostok', 'Asia/Vladivostok', 'IANA timezone: Asia/Vladivostok', 'Asia', 10.0, NULL), + ('Asia/Yakutsk', 'Asia/Yakutsk', 'IANA timezone: Asia/Yakutsk', 'Asia', 9.0, NULL), + ('Asia/Yangon', 'Asia/Yangon', 'IANA timezone: Asia/Yangon', 'Asia', 6.5, NULL), + ('Asia/Yekaterinburg', 'Asia/Yekaterinburg', 'IANA timezone: Asia/Yekaterinburg', 'Asia', 5.0, NULL), + ('Asia/Yerevan', 'Asia/Yerevan', 'IANA timezone: Asia/Yerevan', 'Asia', 4.0, NULL), + ('Atlantic/Azores', 'Atlantic/Azores', 'IANA timezone: Atlantic/Azores', 'Atlantic', 0.0, NULL), + ('Atlantic/Bermuda', 'Atlantic/Bermuda', 'IANA timezone: Atlantic/Bermuda', 'Atlantic', -3.0, NULL), + ('Atlantic/Canary', 'Atlantic/Canary', 'IANA timezone: Atlantic/Canary', 'Atlantic', 1.0, NULL), + ('Atlantic/Cape_Verde', 'Atlantic/Cape Verde', 'IANA timezone: Atlantic/Cape_Verde', 'Atlantic', -1.0, NULL), + ('Atlantic/Faeroe', 'Atlantic/Faeroe', 'IANA timezone: Atlantic/Faeroe', 'Atlantic', 1.0, NULL), + ('Atlantic/Faroe', 'Atlantic/Faroe', 'IANA timezone: Atlantic/Faroe', 'Atlantic', 1.0, NULL), + ('Atlantic/Jan_Mayen', 'Atlantic/Jan Mayen', 'IANA timezone: Atlantic/Jan_Mayen', 'Atlantic', 2.0, NULL), + ('Atlantic/Madeira', 'Atlantic/Madeira', 'IANA timezone: Atlantic/Madeira', 'Atlantic', 1.0, NULL), + ('Atlantic/Reykjavik', 'Atlantic/Reykjavik', 'IANA timezone: Atlantic/Reykjavik', 'Atlantic', 0.0, NULL), + ('Atlantic/South_Georgia', 'Atlantic/South Georgia', 'IANA timezone: Atlantic/South_Georgia', 'Atlantic', -2.0, NULL), + ('Atlantic/Stanley', 'Atlantic/Stanley', 'IANA timezone: Atlantic/Stanley', 'Atlantic', -3.0, NULL), + ('Atlantic/St_Helena', 'Atlantic/St Helena', 'IANA timezone: Atlantic/St_Helena', 'Atlantic', 0.0, NULL), + ('Australia/ACT', 'Australia/ACT', 'IANA timezone: Australia/ACT', 'Australia', 11.0, NULL), + ('Australia/Adelaide', 'Australia/Adelaide', 'IANA timezone: Australia/Adelaide', 'Australia', 10.5, NULL), + ('Australia/Brisbane', 'Australia/Brisbane', 'IANA timezone: Australia/Brisbane', 'Australia', 10.0, NULL), + ('Australia/Broken_Hill', 'Australia/Broken Hill', 'IANA timezone: Australia/Broken_Hill', 'Australia', 10.5, NULL), + ('Australia/Canberra', 'Australia/Canberra', 'IANA timezone: Australia/Canberra', 'Australia', 11.0, NULL), + ('Australia/Currie', 'Australia/Currie', 'IANA timezone: Australia/Currie', 'Australia', 11.0, NULL), + ('Australia/Darwin', 'Australia/Darwin', 'IANA timezone: Australia/Darwin', 'Australia', 9.5, NULL), + ('Australia/Eucla', 'Australia/Eucla', 'IANA timezone: Australia/Eucla', 'Australia', 8.75, NULL), + ('Australia/Hobart', 'Australia/Hobart', 'IANA timezone: Australia/Hobart', 'Australia', 11.0, NULL), + ('Australia/LHI', 'Australia/LHI', 'IANA timezone: Australia/LHI', 'Australia', 11.0, NULL), + ('Australia/Lindeman', 'Australia/Lindeman', 'IANA timezone: Australia/Lindeman', 'Australia', 10.0, NULL), + ('Australia/Lord_Howe', 'Australia/Lord Howe', 'IANA timezone: Australia/Lord_Howe', 'Australia', 11.0, NULL), + ('Australia/Melbourne', 'Australia/Melbourne', 'IANA timezone: Australia/Melbourne', 'Australia', 11.0, NULL), + ('Australia/North', 'Australia/North', 'IANA timezone: Australia/North', 'Australia', 9.5, NULL), + ('Australia/NSW', 'Australia/NSW', 'IANA timezone: Australia/NSW', 'Australia', 11.0, NULL), + ('Australia/Perth', 'Australia/Perth', 'IANA timezone: Australia/Perth', 'Australia', 8.0, NULL), + ('Australia/Queensland', 'Australia/Queensland', 'IANA timezone: Australia/Queensland', 'Australia', 10.0, NULL), + ('Australia/South', 'Australia/South', 'IANA timezone: Australia/South', 'Australia', 10.5, NULL), + ('Australia/Sydney', 'Australia/Sydney', 'IANA timezone: Australia/Sydney', 'Australia', 11.0, NULL), + ('Australia/Tasmania', 'Australia/Tasmania', 'IANA timezone: Australia/Tasmania', 'Australia', 11.0, NULL), + ('Australia/Victoria', 'Australia/Victoria', 'IANA timezone: Australia/Victoria', 'Australia', 11.0, NULL), + ('Australia/West', 'Australia/West', 'IANA timezone: Australia/West', 'Australia', 8.0, NULL), + ('Australia/Yancowinna', 'Australia/Yancowinna', 'IANA timezone: Australia/Yancowinna', 'Australia', 10.5, NULL), + ('Brazil/Acre', 'Brazil/Acre', 'IANA timezone: Brazil/Acre', 'Brazil', -5.0, NULL), + ('Brazil/DeNoronha', 'Brazil/DeNoronha', 'IANA timezone: Brazil/DeNoronha', 'Brazil', -2.0, NULL), + ('Brazil/East', 'Brazil/East', 'IANA timezone: Brazil/East', 'Brazil', -3.0, NULL), + ('Brazil/West', 'Brazil/West', 'IANA timezone: Brazil/West', 'Brazil', -4.0, NULL), + ('Canada/Atlantic', 'Canada/Atlantic', 'IANA timezone: Canada/Atlantic', 'Canada', -3.0, NULL), + ('Canada/Central', 'Canada/Central', 'IANA timezone: Canada/Central', 'Canada', -5.0, NULL), + ('Canada/Eastern', 'Canada/Eastern', 'IANA timezone: Canada/Eastern', 'Canada', -4.0, NULL), + ('Canada/Mountain', 'Canada/Mountain', 'IANA timezone: Canada/Mountain', 'Canada', -6.0, NULL), + ('Canada/Newfoundland', 'Canada/Newfoundland', 'IANA timezone: Canada/Newfoundland', 'Canada', -2.5, NULL), + ('Canada/Pacific', 'Canada/Pacific', 'IANA timezone: Canada/Pacific', 'Canada', -7.0, NULL), + ('Canada/Saskatchewan', 'Canada/Saskatchewan', 'IANA timezone: Canada/Saskatchewan', 'Canada', -6.0, NULL), + ('Canada/Yukon', 'Canada/Yukon', 'IANA timezone: Canada/Yukon', 'Canada', -7.0, NULL), + ('CET', 'CET', 'IANA timezone: CET', 'Other', 2.0, NULL), + ('Chile/Continental', 'Chile/Continental', 'IANA timezone: Chile/Continental', 'Chile', -3.0, NULL), + ('Chile/EasterIsland', 'Chile/EasterIsland', 'IANA timezone: Chile/EasterIsland', 'Chile', -5.0, NULL), + ('CST6CDT', 'CST6CDT', 'IANA timezone: CST6CDT', 'Other', -5.0, NULL), + ('Cuba', 'Cuba', 'IANA timezone: Cuba', 'Other', -4.0, NULL), + ('EET', 'EET', 'IANA timezone: EET', 'Other', 3.0, NULL), + ('Egypt', 'Egypt', 'IANA timezone: Egypt', 'Other', 3.0, NULL), + ('Eire', 'Eire', 'IANA timezone: Eire', 'Other', 1.0, NULL), + ('EST', 'EST', 'IANA timezone: EST', 'Other', -5.0, NULL), + ('EST5EDT', 'EST5EDT', 'IANA timezone: EST5EDT', 'Other', -4.0, NULL), + ('Europe/Amsterdam', 'Europe/Amsterdam', 'IANA timezone: Europe/Amsterdam', 'Europe', 2.0, NULL), + ('Europe/Andorra', 'Europe/Andorra', 'IANA timezone: Europe/Andorra', 'Europe', 2.0, NULL), + ('Europe/Astrakhan', 'Europe/Astrakhan', 'IANA timezone: Europe/Astrakhan', 'Europe', 4.0, NULL), + ('Europe/Athens', 'Europe/Athens', 'IANA timezone: Europe/Athens', 'Europe', 3.0, NULL), + ('Europe/Belfast', 'Europe/Belfast', 'IANA timezone: Europe/Belfast', 'Europe', 1.0, NULL), + ('Europe/Belgrade', 'Europe/Belgrade', 'IANA timezone: Europe/Belgrade', 'Europe', 2.0, NULL), + ('Europe/Berlin', 'Europe/Berlin', 'IANA timezone: Europe/Berlin', 'Europe', 2.0, NULL), + ('Europe/Bratislava', 'Europe/Bratislava', 'IANA timezone: Europe/Bratislava', 'Europe', 2.0, NULL), + ('Europe/Brussels', 'Europe/Brussels', 'IANA timezone: Europe/Brussels', 'Europe', 2.0, NULL), + ('Europe/Bucharest', 'Europe/Bucharest', 'IANA timezone: Europe/Bucharest', 'Europe', 3.0, NULL), + ('Europe/Budapest', 'Europe/Budapest', 'IANA timezone: Europe/Budapest', 'Europe', 2.0, NULL), + ('Europe/Busingen', 'Europe/Busingen', 'IANA timezone: Europe/Busingen', 'Europe', 2.0, NULL), + ('Europe/Chisinau', 'Europe/Chisinau', 'IANA timezone: Europe/Chisinau', 'Europe', 3.0, NULL), + ('Europe/Copenhagen', 'Europe/Copenhagen', 'IANA timezone: Europe/Copenhagen', 'Europe', 2.0, NULL), + ('Europe/Dublin', 'Europe/Dublin', 'IANA timezone: Europe/Dublin', 'Europe', 1.0, NULL), + ('Europe/Gibraltar', 'Europe/Gibraltar', 'IANA timezone: Europe/Gibraltar', 'Europe', 2.0, NULL), + ('Europe/Guernsey', 'Europe/Guernsey', 'IANA timezone: Europe/Guernsey', 'Europe', 1.0, NULL), + ('Europe/Helsinki', 'Europe/Helsinki', 'IANA timezone: Europe/Helsinki', 'Europe', 3.0, NULL), + ('Europe/Isle_of_Man', 'Europe/Isle of Man', 'IANA timezone: Europe/Isle_of_Man', 'Europe', 1.0, NULL), + ('Europe/Istanbul', 'Europe/Istanbul', 'IANA timezone: Europe/Istanbul', 'Europe', 3.0, NULL), + ('Europe/Jersey', 'Europe/Jersey', 'IANA timezone: Europe/Jersey', 'Europe', 1.0, NULL), + ('Europe/Kaliningrad', 'Europe/Kaliningrad', 'IANA timezone: Europe/Kaliningrad', 'Europe', 2.0, NULL), + ('Europe/Kiev', 'Europe/Kiev', 'IANA timezone: Europe/Kiev', 'Europe', 3.0, NULL), + ('Europe/Kirov', 'Europe/Kirov', 'IANA timezone: Europe/Kirov', 'Europe', 3.0, NULL), + ('Europe/Kyiv', 'Europe/Kyiv', 'IANA timezone: Europe/Kyiv', 'Europe', 3.0, NULL), + ('Europe/Lisbon', 'Europe/Lisbon', 'IANA timezone: Europe/Lisbon', 'Europe', 1.0, NULL), + ('Europe/Ljubljana', 'Europe/Ljubljana', 'IANA timezone: Europe/Ljubljana', 'Europe', 2.0, NULL), + ('Europe/London', 'Europe/London', 'IANA timezone: Europe/London', 'Europe', 1.0, NULL), + ('Europe/Luxembourg', 'Europe/Luxembourg', 'IANA timezone: Europe/Luxembourg', 'Europe', 2.0, NULL), + ('Europe/Madrid', 'Europe/Madrid', 'IANA timezone: Europe/Madrid', 'Europe', 2.0, NULL), + ('Europe/Malta', 'Europe/Malta', 'IANA timezone: Europe/Malta', 'Europe', 2.0, NULL), + ('Europe/Mariehamn', 'Europe/Mariehamn', 'IANA timezone: Europe/Mariehamn', 'Europe', 3.0, NULL), + ('Europe/Minsk', 'Europe/Minsk', 'IANA timezone: Europe/Minsk', 'Europe', 3.0, NULL), + ('Europe/Monaco', 'Europe/Monaco', 'IANA timezone: Europe/Monaco', 'Europe', 2.0, NULL), + ('Europe/Moscow', 'Europe/Moscow', 'IANA timezone: Europe/Moscow', 'Europe', 3.0, NULL), + ('Europe/Nicosia', 'Europe/Nicosia', 'IANA timezone: Europe/Nicosia', 'Europe', 3.0, NULL), + ('Europe/Oslo', 'Europe/Oslo', 'IANA timezone: Europe/Oslo', 'Europe', 2.0, NULL), + ('Europe/Paris', 'Europe/Paris', 'IANA timezone: Europe/Paris', 'Europe', 2.0, NULL), + ('Europe/Podgorica', 'Europe/Podgorica', 'IANA timezone: Europe/Podgorica', 'Europe', 2.0, NULL), + ('Europe/Prague', 'Europe/Prague', 'IANA timezone: Europe/Prague', 'Europe', 2.0, NULL), + ('Europe/Riga', 'Europe/Riga', 'IANA timezone: Europe/Riga', 'Europe', 3.0, NULL), + ('Europe/Rome', 'Europe/Rome', 'IANA timezone: Europe/Rome', 'Europe', 2.0, NULL), + ('Europe/Samara', 'Europe/Samara', 'IANA timezone: Europe/Samara', 'Europe', 4.0, NULL), + ('Europe/San_Marino', 'Europe/San Marino', 'IANA timezone: Europe/San_Marino', 'Europe', 2.0, NULL), + ('Europe/Sarajevo', 'Europe/Sarajevo', 'IANA timezone: Europe/Sarajevo', 'Europe', 2.0, NULL), + ('Europe/Saratov', 'Europe/Saratov', 'IANA timezone: Europe/Saratov', 'Europe', 4.0, NULL), + ('Europe/Simferopol', 'Europe/Simferopol', 'IANA timezone: Europe/Simferopol', 'Europe', 3.0, NULL), + ('Europe/Skopje', 'Europe/Skopje', 'IANA timezone: Europe/Skopje', 'Europe', 2.0, NULL), + ('Europe/Sofia', 'Europe/Sofia', 'IANA timezone: Europe/Sofia', 'Europe', 3.0, NULL), + ('Europe/Stockholm', 'Europe/Stockholm', 'IANA timezone: Europe/Stockholm', 'Europe', 2.0, NULL), + ('Europe/Tallinn', 'Europe/Tallinn', 'IANA timezone: Europe/Tallinn', 'Europe', 3.0, NULL), + ('Europe/Tirane', 'Europe/Tirane', 'IANA timezone: Europe/Tirane', 'Europe', 2.0, NULL), + ('Europe/Tiraspol', 'Europe/Tiraspol', 'IANA timezone: Europe/Tiraspol', 'Europe', 3.0, NULL), + ('Europe/Ulyanovsk', 'Europe/Ulyanovsk', 'IANA timezone: Europe/Ulyanovsk', 'Europe', 4.0, NULL), + ('Europe/Uzhgorod', 'Europe/Uzhgorod', 'IANA timezone: Europe/Uzhgorod', 'Europe', 3.0, NULL), + ('Europe/Vaduz', 'Europe/Vaduz', 'IANA timezone: Europe/Vaduz', 'Europe', 2.0, NULL), + ('Europe/Vatican', 'Europe/Vatican', 'IANA timezone: Europe/Vatican', 'Europe', 2.0, NULL), + ('Europe/Vienna', 'Europe/Vienna', 'IANA timezone: Europe/Vienna', 'Europe', 2.0, NULL), + ('Europe/Vilnius', 'Europe/Vilnius', 'IANA timezone: Europe/Vilnius', 'Europe', 3.0, NULL), + ('Europe/Volgograd', 'Europe/Volgograd', 'IANA timezone: Europe/Volgograd', 'Europe', 3.0, NULL), + ('Europe/Warsaw', 'Europe/Warsaw', 'IANA timezone: Europe/Warsaw', 'Europe', 2.0, NULL), + ('Europe/Zagreb', 'Europe/Zagreb', 'IANA timezone: Europe/Zagreb', 'Europe', 2.0, NULL), + ('Europe/Zaporozhye', 'Europe/Zaporozhye', 'IANA timezone: Europe/Zaporozhye', 'Europe', 3.0, NULL), + ('Europe/Zurich', 'Europe/Zurich', 'IANA timezone: Europe/Zurich', 'Europe', 2.0, NULL), + ('Factory', 'Factory', 'IANA timezone: Factory', 'Other', 0.0, NULL), + ('GB', 'GB', 'IANA timezone: GB', 'Other', 1.0, NULL), + ('GB-Eire', 'GB-Eire', 'IANA timezone: GB-Eire', 'Other', 1.0, NULL), + ('GMT', 'GMT', 'IANA timezone: GMT', 'Other', 0.0, NULL), + ('GMT+0', 'GMT+0', 'IANA timezone: GMT+0', 'Other', 0.0, NULL), + ('GMT-0', 'GMT-0', 'IANA timezone: GMT-0', 'Other', 0.0, NULL), + ('GMT0', 'GMT0', 'IANA timezone: GMT0', 'Other', 0.0, NULL), + ('Greenwich', 'Greenwich', 'IANA timezone: Greenwich', 'Other', 0.0, NULL), + ('Hongkong', 'Hongkong', 'IANA timezone: Hongkong', 'Other', 8.0, NULL), + ('HST', 'HST', 'IANA timezone: HST', 'Other', -10.0, NULL), + ('Iceland', 'Iceland', 'IANA timezone: Iceland', 'Other', 0.0, NULL), + ('Indian/Antananarivo', 'Indian/Antananarivo', 'IANA timezone: Indian/Antananarivo', 'Indian', 3.0, NULL), + ('Indian/Chagos', 'Indian/Chagos', 'IANA timezone: Indian/Chagos', 'Indian', 6.0, NULL), + ('Indian/Christmas', 'Indian/Christmas', 'IANA timezone: Indian/Christmas', 'Indian', 7.0, NULL), + ('Indian/Cocos', 'Indian/Cocos', 'IANA timezone: Indian/Cocos', 'Indian', 6.5, NULL), + ('Indian/Comoro', 'Indian/Comoro', 'IANA timezone: Indian/Comoro', 'Indian', 3.0, NULL), + ('Indian/Kerguelen', 'Indian/Kerguelen', 'IANA timezone: Indian/Kerguelen', 'Indian', 5.0, NULL), + ('Indian/Mahe', 'Indian/Mahe', 'IANA timezone: Indian/Mahe', 'Indian', 4.0, NULL), + ('Indian/Maldives', 'Indian/Maldives', 'IANA timezone: Indian/Maldives', 'Indian', 5.0, NULL), + ('Indian/Mauritius', 'Indian/Mauritius', 'IANA timezone: Indian/Mauritius', 'Indian', 4.0, NULL), + ('Indian/Mayotte', 'Indian/Mayotte', 'IANA timezone: Indian/Mayotte', 'Indian', 3.0, NULL), + ('Indian/Reunion', 'Indian/Reunion', 'IANA timezone: Indian/Reunion', 'Indian', 4.0, NULL), + ('Iran', 'Iran', 'IANA timezone: Iran', 'Other', 3.5, NULL), + ('Israel', 'Israel', 'IANA timezone: Israel', 'Other', 3.0, NULL), + ('Jamaica', 'Jamaica', 'IANA timezone: Jamaica', 'Other', -5.0, NULL), + ('Japan', 'Japan', 'IANA timezone: Japan', 'Other', 9.0, NULL), + ('Kwajalein', 'Kwajalein', 'IANA timezone: Kwajalein', 'Other', 12.0, NULL), + ('Libya', 'Libya', 'IANA timezone: Libya', 'Other', 2.0, NULL), + ('MET', 'MET', 'IANA timezone: MET', 'Other', 2.0, NULL), + ('Mexico/BajaNorte', 'Mexico/BajaNorte', 'IANA timezone: Mexico/BajaNorte', 'Mexico', -7.0, NULL), + ('Mexico/BajaSur', 'Mexico/BajaSur', 'IANA timezone: Mexico/BajaSur', 'Mexico', -7.0, NULL), + ('Mexico/General', 'Mexico/General', 'IANA timezone: Mexico/General', 'Mexico', -6.0, NULL), + ('MST', 'MST', 'IANA timezone: MST', 'Other', -7.0, NULL), + ('MST7MDT', 'MST7MDT', 'IANA timezone: MST7MDT', 'Other', -6.0, NULL), + ('Navajo', 'Navajo', 'IANA timezone: Navajo', 'Other', -6.0, NULL), + ('NZ', 'NZ', 'IANA timezone: NZ', 'Other', 13.0, NULL), + ('NZ-CHAT', 'NZ-CHAT', 'IANA timezone: NZ-CHAT', 'Other', 13.75, NULL), + ('Pacific/Apia', 'Pacific/Apia', 'IANA timezone: Pacific/Apia', 'Pacific', 13.0, NULL), + ('Pacific/Auckland', 'Pacific/Auckland', 'IANA timezone: Pacific/Auckland', 'Pacific', 13.0, NULL), + ('Pacific/Bougainville', 'Pacific/Bougainville', 'IANA timezone: Pacific/Bougainville', 'Pacific', 11.0, NULL), + ('Pacific/Chatham', 'Pacific/Chatham', 'IANA timezone: Pacific/Chatham', 'Pacific', 13.75, NULL), + ('Pacific/Chuuk', 'Pacific/Chuuk', 'IANA timezone: Pacific/Chuuk', 'Pacific', 10.0, NULL), + ('Pacific/Easter', 'Pacific/Easter', 'IANA timezone: Pacific/Easter', 'Pacific', -5.0, NULL), + ('Pacific/Efate', 'Pacific/Efate', 'IANA timezone: Pacific/Efate', 'Pacific', 11.0, NULL), + ('Pacific/Enderbury', 'Pacific/Enderbury', 'IANA timezone: Pacific/Enderbury', 'Pacific', 13.0, NULL), + ('Pacific/Fakaofo', 'Pacific/Fakaofo', 'IANA timezone: Pacific/Fakaofo', 'Pacific', 13.0, NULL), + ('Pacific/Fiji', 'Pacific/Fiji', 'IANA timezone: Pacific/Fiji', 'Pacific', 12.0, NULL), + ('Pacific/Funafuti', 'Pacific/Funafuti', 'IANA timezone: Pacific/Funafuti', 'Pacific', 12.0, NULL), + ('Pacific/Galapagos', 'Pacific/Galapagos', 'IANA timezone: Pacific/Galapagos', 'Pacific', -6.0, NULL), + ('Pacific/Gambier', 'Pacific/Gambier', 'IANA timezone: Pacific/Gambier', 'Pacific', -9.0, NULL), + ('Pacific/Guadalcanal', 'Pacific/Guadalcanal', 'IANA timezone: Pacific/Guadalcanal', 'Pacific', 11.0, NULL), + ('Pacific/Guam', 'Pacific/Guam', 'IANA timezone: Pacific/Guam', 'Pacific', 10.0, NULL), + ('Pacific/Honolulu', 'Pacific/Honolulu', 'IANA timezone: Pacific/Honolulu', 'Pacific', -10.0, NULL), + ('Pacific/Johnston', 'Pacific/Johnston', 'IANA timezone: Pacific/Johnston', 'Pacific', -10.0, NULL), + ('Pacific/Kanton', 'Pacific/Kanton', 'IANA timezone: Pacific/Kanton', 'Pacific', 13.0, NULL), + ('Pacific/Kiritimati', 'Pacific/Kiritimati', 'IANA timezone: Pacific/Kiritimati', 'Pacific', 14.0, NULL), + ('Pacific/Kosrae', 'Pacific/Kosrae', 'IANA timezone: Pacific/Kosrae', 'Pacific', 11.0, NULL), + ('Pacific/Kwajalein', 'Pacific/Kwajalein', 'IANA timezone: Pacific/Kwajalein', 'Pacific', 12.0, NULL), + ('Pacific/Majuro', 'Pacific/Majuro', 'IANA timezone: Pacific/Majuro', 'Pacific', 12.0, NULL), + ('Pacific/Marquesas', 'Pacific/Marquesas', 'IANA timezone: Pacific/Marquesas', 'Pacific', -9.5, NULL), + ('Pacific/Midway', 'Pacific/Midway', 'IANA timezone: Pacific/Midway', 'Pacific', -11.0, NULL), + ('Pacific/Nauru', 'Pacific/Nauru', 'IANA timezone: Pacific/Nauru', 'Pacific', 12.0, NULL), + ('Pacific/Niue', 'Pacific/Niue', 'IANA timezone: Pacific/Niue', 'Pacific', -11.0, NULL), + ('Pacific/Norfolk', 'Pacific/Norfolk', 'IANA timezone: Pacific/Norfolk', 'Pacific', 12.0, NULL), + ('Pacific/Noumea', 'Pacific/Noumea', 'IANA timezone: Pacific/Noumea', 'Pacific', 11.0, NULL), + ('Pacific/Pago_Pago', 'Pacific/Pago Pago', 'IANA timezone: Pacific/Pago_Pago', 'Pacific', -11.0, NULL), + ('Pacific/Palau', 'Pacific/Palau', 'IANA timezone: Pacific/Palau', 'Pacific', 9.0, NULL), + ('Pacific/Pitcairn', 'Pacific/Pitcairn', 'IANA timezone: Pacific/Pitcairn', 'Pacific', -8.0, NULL), + ('Pacific/Pohnpei', 'Pacific/Pohnpei', 'IANA timezone: Pacific/Pohnpei', 'Pacific', 11.0, NULL), + ('Pacific/Ponape', 'Pacific/Ponape', 'IANA timezone: Pacific/Ponape', 'Pacific', 11.0, NULL), + ('Pacific/Port_Moresby', 'Pacific/Port Moresby', 'IANA timezone: Pacific/Port_Moresby', 'Pacific', 10.0, NULL), + ('Pacific/Rarotonga', 'Pacific/Rarotonga', 'IANA timezone: Pacific/Rarotonga', 'Pacific', -10.0, NULL), + ('Pacific/Saipan', 'Pacific/Saipan', 'IANA timezone: Pacific/Saipan', 'Pacific', 10.0, NULL), + ('Pacific/Samoa', 'Pacific/Samoa', 'IANA timezone: Pacific/Samoa', 'Pacific', -11.0, NULL), + ('Pacific/Tahiti', 'Pacific/Tahiti', 'IANA timezone: Pacific/Tahiti', 'Pacific', -10.0, NULL), + ('Pacific/Tarawa', 'Pacific/Tarawa', 'IANA timezone: Pacific/Tarawa', 'Pacific', 12.0, NULL), + ('Pacific/Tongatapu', 'Pacific/Tongatapu', 'IANA timezone: Pacific/Tongatapu', 'Pacific', 13.0, NULL), + ('Pacific/Truk', 'Pacific/Truk', 'IANA timezone: Pacific/Truk', 'Pacific', 10.0, NULL), + ('Pacific/Wake', 'Pacific/Wake', 'IANA timezone: Pacific/Wake', 'Pacific', 12.0, NULL), + ('Pacific/Wallis', 'Pacific/Wallis', 'IANA timezone: Pacific/Wallis', 'Pacific', 12.0, NULL), + ('Pacific/Yap', 'Pacific/Yap', 'IANA timezone: Pacific/Yap', 'Pacific', 10.0, NULL), + ('Poland', 'Poland', 'IANA timezone: Poland', 'Other', 2.0, NULL), + ('Portugal', 'Portugal', 'IANA timezone: Portugal', 'Other', 1.0, NULL), + ('PRC', 'PRC', 'IANA timezone: PRC', 'Other', 8.0, NULL), + ('PST8PDT', 'PST8PDT', 'IANA timezone: PST8PDT', 'Other', -7.0, NULL), + ('ROC', 'ROC', 'IANA timezone: ROC', 'Other', 8.0, NULL), + ('ROK', 'ROK', 'IANA timezone: ROK', 'Other', 9.0, NULL), + ('Singapore', 'Singapore', 'IANA timezone: Singapore', 'Other', 8.0, NULL), + ('Turkey', 'Turkey', 'IANA timezone: Turkey', 'Other', 3.0, NULL), + ('UCT', 'UCT', 'IANA timezone: UCT', 'Other', 0.0, NULL), + ('Universal', 'Universal', 'IANA timezone: Universal', 'Other', 0.0, NULL), + ('US/Alaska', 'US/Alaska', 'IANA timezone: US/Alaska', 'US', -8.0, NULL), + ('US/Aleutian', 'US/Aleutian', 'IANA timezone: US/Aleutian', 'US', -9.0, NULL), + ('US/Arizona', 'US/Arizona', 'IANA timezone: US/Arizona', 'US', -7.0, NULL), + ('US/Central', 'US/Central', 'IANA timezone: US/Central', 'US', -5.0, NULL), + ('US/Eastern', 'US/Eastern', 'IANA timezone: US/Eastern', 'US', -4.0, NULL), + ('US/East-Indiana', 'US/East-Indiana', 'IANA timezone: US/East-Indiana', 'US', -4.0, NULL), + ('US/Hawaii', 'US/Hawaii', 'IANA timezone: US/Hawaii', 'US', -10.0, NULL), + ('US/Indiana-Starke', 'US/Indiana-Starke', 'IANA timezone: US/Indiana-Starke', 'US', -5.0, NULL), + ('US/Michigan', 'US/Michigan', 'IANA timezone: US/Michigan', 'US', -4.0, NULL), + ('US/Mountain', 'US/Mountain', 'IANA timezone: US/Mountain', 'US', -6.0, NULL), + ('US/Pacific', 'US/Pacific', 'IANA timezone: US/Pacific', 'US', -7.0, NULL), + ('US/Samoa', 'US/Samoa', 'IANA timezone: US/Samoa', 'US', -11.0, NULL), + ('UTC', 'UTC', 'IANA timezone: UTC', 'Other', 0.0, NULL), + ('WET', 'WET', 'IANA timezone: WET', 'Other', 1.0, NULL), + ('W-SU', 'W-SU', 'IANA timezone: W-SU', 'Other', 3.0, NULL), + ('Zulu', 'Zulu', 'IANA timezone: Zulu', 'Other', 0.0, NULL); + +-- Currencies +INSERT INTO meta.currencies (code, name, symbol, decimal_places, notes) +VALUES + ('AED', 'UAE Dirham', 'د.إ', 2, NULL), + ('AFN', 'Afghani', 'AFN', 2, NULL), + ('ALL', 'Lek', 'ALL', 2, NULL), + ('AMD', 'Armenian Dram', 'AMD', 2, NULL), + ('AOA', 'Kwanza', 'AOA', 2, NULL), + ('ARS', 'Argentine Peso', '$', 2, NULL), + ('AUD', 'Australian Dollar', 'A$', 2, NULL), + ('AWG', 'Aruban Florin', 'AWG', 2, NULL), + ('AZN', 'Azerbaijan Manat', 'AZN', 2, NULL), + ('BAM', 'Convertible Mark', 'BAM', 2, NULL), + ('BBD', 'Barbados Dollar', 'BBD', 2, NULL), + ('BDT', 'Taka', 'BDT', 2, NULL), + ('BGN', 'Bulgarian Lev', 'BGN', 2, NULL), + ('BHD', 'Bahraini Dinar', 'BHD', 3, NULL), + ('BIF', 'Burundi Franc', 'BIF', 0, NULL), + ('BMD', 'Bermudian Dollar', 'BMD', 2, NULL), + ('BND', 'Brunei Dollar', 'BND', 2, NULL), + ('BOB', 'Boliviano', 'BOB', 2, NULL), + ('BOV', 'Mvdol', 'BOV', 2, NULL), + ('BRL', 'Brazilian Real', 'R$', 2, NULL), + ('BSD', 'Bahamian Dollar', 'BSD', 2, NULL), + ('BTN', 'Ngultrum', 'BTN', 2, NULL), + ('BWP', 'Pula', 'BWP', 2, NULL), + ('BYN', 'Belarusian Ruble', 'BYN', 2, NULL), + ('BZD', 'Belize Dollar', 'BZD', 2, NULL), + ('CAD', 'Canadian Dollar', 'C$', 2, NULL), + ('CDF', 'Congolese Franc', 'CDF', 2, NULL), + ('CHE', 'WIR Euro', 'CHE', 2, NULL), + ('CHF', 'Swiss Franc', 'CHF', 2, NULL), + ('CHW', 'WIR Franc', 'CHW', 2, NULL), + ('CLF', 'Unidad de Fomento', 'CLF', 4, NULL), + ('CLP', 'Chilean Peso', '$', 0, NULL), + ('CNY', 'Yuan Renminbi', '¥', 2, NULL), + ('COP', 'Colombian Peso', 'COP', 2, NULL), + ('COU', 'Unidad de Valor Real', 'COU', 2, NULL), + ('CRC', 'Costa Rican Colon', 'CRC', 2, NULL), + ('CUP', 'Cuban Peso', 'CUP', 2, NULL), + ('CVE', 'Cabo Verde Escudo', 'CVE', 2, NULL), + ('CZK', 'Czech Koruna', 'Kč', 2, NULL), + ('DJF', 'Djibouti Franc', 'DJF', 0, NULL), + ('DKK', 'Danish Krone', 'kr', 2, NULL), + ('DOP', 'Dominican Peso', 'DOP', 2, NULL), + ('DZD', 'Algerian Dinar', 'DZD', 2, NULL), + ('EGP', 'Egyptian Pound', 'EGP', 2, NULL), + ('ERN', 'Nakfa', 'ERN', 2, NULL), + ('ETB', 'Ethiopian Birr', 'ETB', 2, NULL), + ('EUR', 'Euro', '€', 2, NULL), + ('FJD', 'Fiji Dollar', 'FJD', 2, NULL), + ('FKP', 'Falkland Islands Pound', 'FKP', 2, NULL), + ('GBP', 'Pound Sterling', '£', 2, NULL), + ('GEL', 'Lari', 'GEL', 2, NULL), + ('GHS', 'Ghana Cedi', 'GHS', 2, NULL), + ('GIP', 'Gibraltar Pound', 'GIP', 2, NULL), + ('GMD', 'Dalasi', 'GMD', 2, NULL), + ('GNF', 'Guinean Franc', 'GNF', 0, NULL), + ('GTQ', 'Quetzal', 'GTQ', 2, NULL), + ('GYD', 'Guyana Dollar', 'GYD', 2, NULL), + ('HKD', 'Hong Kong Dollar', 'HK$', 2, NULL), + ('HNL', 'Lempira', 'HNL', 2, NULL), + ('HTG', 'Gourde', 'HTG', 2, NULL), + ('HUF', 'Forint', 'Ft', 2, NULL), + ('IDR', 'Rupiah', 'Rp', 2, NULL), + ('ILS', 'New Israeli Sheqel', '₪', 2, NULL), + ('INR', 'Indian Rupee', '₹', 2, NULL), + ('IQD', 'Iraqi Dinar', 'IQD', 3, NULL), + ('IRR', 'Iranian Rial', 'IRR', 2, NULL), + ('ISK', 'Iceland Krona', 'ISK', 0, NULL), + ('JMD', 'Jamaican Dollar', 'JMD', 2, NULL), + ('JOD', 'Jordanian Dinar', 'JOD', 3, NULL), + ('JPY', 'Yen', '¥', 0, NULL), + ('KES', 'Kenyan Shilling', 'KES', 2, NULL), + ('KGS', 'Som', 'KGS', 2, NULL), + ('KHR', 'Riel', 'KHR', 2, NULL), + ('KMF', 'Comorian Franc', 'KMF', 0, NULL), + ('KPW', 'North Korean Won', 'KPW', 2, NULL), + ('KRW', 'Won', '₩', 0, NULL), + ('KWD', 'Kuwaiti Dinar', 'KWD', 3, NULL), + ('KYD', 'Cayman Islands Dollar', 'KYD', 2, NULL), + ('KZT', 'Tenge', 'KZT', 2, NULL), + ('LAK', 'Lao Kip', 'LAK', 2, NULL), + ('LBP', 'Lebanese Pound', 'LBP', 2, NULL), + ('LKR', 'Sri Lanka Rupee', 'LKR', 2, NULL), + ('LRD', 'Liberian Dollar', 'LRD', 2, NULL), + ('LSL', 'Loti', 'LSL', 2, NULL), + ('LYD', 'Libyan Dinar', 'LYD', 3, NULL), + ('MAD', 'Moroccan Dirham', 'MAD', 2, NULL), + ('MDL', 'Moldovan Leu', 'MDL', 2, NULL), + ('MGA', 'Malagasy Ariary', 'MGA', 2, NULL), + ('MKD', 'Denar', 'MKD', 2, NULL), + ('MMK', 'Kyat', 'MMK', 2, NULL), + ('MNT', 'Tugrik', 'MNT', 2, NULL), + ('MOP', 'Pataca', 'MOP', 2, NULL), + ('MRU', 'Ouguiya', 'MRU', 2, NULL), + ('MUR', 'Mauritius Rupee', 'MUR', 2, NULL), + ('MVR', 'Rufiyaa', 'MVR', 2, NULL), + ('MWK', 'Malawi Kwacha', 'MWK', 2, NULL), + ('MXN', 'Mexican Peso', 'Mex$', 2, NULL), + ('MXV', 'Mexican Unidad de Inversion (UDI)', 'MXV', 2, NULL), + ('MYR', 'Malaysian Ringgit', 'RM', 2, NULL), + ('MZN', 'Mozambique Metical', 'MZN', 2, NULL), + ('NAD', 'Namibia Dollar', 'NAD', 2, NULL), + ('NGN', 'Naira', 'NGN', 2, NULL), + ('NIO', 'Cordoba Oro', 'NIO', 2, NULL), + ('NOK', 'Norwegian Krone', 'kr', 2, NULL), + ('NPR', 'Nepalese Rupee', 'NPR', 2, NULL), + ('NZD', 'New Zealand Dollar', 'NZ$', 2, NULL), + ('OMR', 'Rial Omani', 'OMR', 3, NULL), + ('PAB', 'Balboa', 'PAB', 2, NULL), + ('PEN', 'Sol', 'PEN', 2, NULL), + ('PGK', 'Kina', 'PGK', 2, NULL), + ('PHP', 'Philippine Peso', '₱', 2, NULL), + ('PKR', 'Pakistan Rupee', 'PKR', 2, NULL), + ('PLN', 'Zloty', 'zł', 2, NULL), + ('PYG', 'Guarani', 'PYG', 0, NULL), + ('QAR', 'Qatari Rial', 'QAR', 2, NULL), + ('RON', 'Romanian Leu', 'RON', 2, NULL), + ('RSD', 'Serbian Dinar', 'RSD', 2, NULL), + ('RUB', 'Russian Ruble', '₽', 2, NULL), + ('RWF', 'Rwanda Franc', 'RWF', 0, NULL), + ('SAR', 'Saudi Riyal', '﷼', 2, NULL), + ('SBD', 'Solomon Islands Dollar', 'SBD', 2, NULL), + ('SCR', 'Seychelles Rupee', 'SCR', 2, NULL), + ('SDG', 'Sudanese Pound', 'SDG', 2, NULL), + ('SEK', 'Swedish Krona', 'kr', 2, NULL), + ('SGD', 'Singapore Dollar', 'S$', 2, NULL), + ('SHP', 'Saint Helena Pound', 'SHP', 2, NULL), + ('SLE', 'Leone', 'SLE', 2, NULL), + ('SOS', 'Somali Shilling', 'SOS', 2, NULL), + ('SRD', 'Surinam Dollar', 'SRD', 2, NULL), + ('SSP', 'South Sudanese Pound', 'SSP', 2, NULL), + ('STN', 'Dobra', 'STN', 2, NULL), + ('SVC', 'El Salvador Colon', 'SVC', 2, NULL), + ('SYP', 'Syrian Pound', 'SYP', 2, NULL), + ('SZL', 'Lilangeni', 'SZL', 2, NULL), + ('THB', 'Baht', '฿', 2, NULL), + ('TJS', 'Somoni', 'TJS', 2, NULL), + ('TMT', 'Turkmenistan New Manat', 'TMT', 2, NULL), + ('TND', 'Tunisian Dinar', 'TND', 3, NULL), + ('TOP', 'Pa’anga', 'TOP', 2, NULL), + ('TRY', 'Turkish Lira', '₺', 2, NULL), + ('TTD', 'Trinidad and Tobago Dollar', 'TTD', 2, NULL), + ('TWD', 'New Taiwan Dollar', 'NT$', 2, NULL), + ('TZS', 'Tanzanian Shilling', 'TZS', 2, NULL), + ('UAH', 'Hryvnia', 'UAH', 2, NULL), + ('UGX', 'Uganda Shilling', 'UGX', 0, NULL), + ('USD', 'US Dollar', '$', 2, NULL), + ('USN', 'US Dollar (Next day)', 'USN', 2, NULL), + ('UYI', 'Uruguay Peso en Unidades Indexadas (UI)', 'UYI', 0, NULL), + ('UYU', 'Peso Uruguayo', 'UYU', 2, NULL), + ('UYW', 'Unidad Previsional', 'UYW', 4, NULL), + ('UZS', 'Uzbekistan Sum', 'UZS', 2, NULL), + ('VED', 'Bolívar Soberano', 'VED', 2, NULL), + ('VES', 'Bolívar Soberano', 'VES', 2, NULL), + ('VND', 'Dong', '₫', 0, NULL), + ('VUV', 'Vatu', 'VUV', 0, NULL), + ('WST', 'Tala', 'WST', 2, NULL), + ('XAD', 'Arab Accounting Dinar', 'XAD', 2, NULL), + ('XAF', 'CFA Franc BEAC', 'XAF', 0, NULL), + ('XAG', 'Silver', 'XAG', 2, NULL), + ('XAU', 'Gold', 'XAU', 2, NULL), + ('XBA', 'Bond Markets Unit European Composite Unit (EURCO)', 'XBA', 2, NULL), + ('XBB', 'Bond Markets Unit European Monetary Unit (E.M.U.-6)', 'XBB', 2, NULL), + ('XBC', 'Bond Markets Unit European Unit of Account 9 (E.U.A.-9)', 'XBC', 2, NULL), + ('XBD', 'Bond Markets Unit European Unit of Account 17 (E.U.A.-17)', 'XBD', 2, NULL), + ('XCD', 'East Caribbean Dollar', 'XCD', 2, NULL), + ('XCG', 'Caribbean Guilder', 'XCG', 2, NULL), + ('XDR', 'SDR (Special Drawing Right)', 'XDR', 2, NULL), + ('XOF', 'CFA Franc BCEAO', 'XOF', 0, NULL), + ('XPD', 'Palladium', 'XPD', 2, NULL), + ('XPF', 'CFP Franc', 'XPF', 0, NULL), + ('XPT', 'Platinum', 'XPT', 2, NULL), + ('XSU', 'Sucre', 'XSU', 2, NULL), + ('XTS', 'Codes specifically reserved for testing purposes', 'XTS', 2, NULL), + ('XUA', 'ADB Unit of Account', 'XUA', 2, NULL), + ('XXX', 'The codes assigned for transactions where no currency is involved', 'XXX', 2, NULL), + ('YER', 'Yemeni Rial', 'YER', 2, NULL), + ('ZAR', 'Rand', 'R', 2, NULL), + ('ZMW', 'Zambian Kwacha', 'ZMW', 2, NULL), + ('ZWG', 'Zimbabwe Gold', 'ZWG', 2, NULL); + +-- Security Types (Interactive Brokers conventions) +INSERT INTO meta.security_types (code, name, description, notes) +VALUES + ('ADR', 'American Depositary Receipt', 'Negotiable certificate representing shares in a foreign company', NULL), + ('BOND', 'Bond', 'Government and corporate bonds', NULL), + ('CASH', 'Forex', 'Foreign exchange currency pairs', NULL), + ('CFD', 'CFD', 'Contracts for difference', NULL), + ('CRYPTO', 'Cryptocurrency', 'Digital currencies and tokens', NULL), + ('ETF', 'Exchange Traded Fund', 'Exchange-traded fund that tracks an index, commodity, or basket of assets', NULL), + ('FOP', 'Futures Option', 'Options on futures contracts', NULL), + ('FUND', 'Mutual Fund', 'Open and closed-end mutual funds', NULL), + ('FUT', 'Future', 'Exchange-traded futures contracts', NULL), + ('IOPT', 'Structured Product', 'Dutch warrants and structured products', NULL), + ('OPT', 'Option', 'Equity and index options', NULL), + ('STK', 'Stock', 'Common and preferred equities', NULL), + ('WAR', 'Warrant', 'Equity warrants', NULL); diff --git a/sql/08_script_log.sql b/sql/08_script_log.sql new file mode 100644 index 0000000..3685f24 --- /dev/null +++ b/sql/08_script_log.sql @@ -0,0 +1,44 @@ +-- ============================================================================ +-- Script Logging Table +-- ============================================================================ +-- Persistent log for import scripts and library operations. +-- Provides queryable event log without cluttering CLI output. +-- +-- Usage: +-- session = db.log_session("import_ohlc.py") +-- session.info("Starting import for AAPL") +-- session.warning("Missing data for 2024-01-15") +-- session.error("Failed to parse file", details={"file": "bad.csv"}) +-- ============================================================================ + +-- Create script_log table in log schema +CREATE TABLE IF NOT EXISTS log.script_log ( + id SERIAL PRIMARY KEY, + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + level VARCHAR(10) NOT NULL DEFAULT 'INFO' CHECK (level IN ('DEBUG', 'INFO', 'WARNING', 'ERROR')), + message TEXT NOT NULL, + script_name VARCHAR(100), + series_name VARCHAR(255), + session_id UUID, + details JSONB +); + +-- Indexes for common query patterns +CREATE INDEX IF NOT EXISTS idx_script_log_timestamp ON log.script_log(timestamp DESC); +CREATE INDEX IF NOT EXISTS idx_script_log_session ON log.script_log(session_id) WHERE session_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_script_log_series ON log.script_log(series_name) WHERE series_name IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_script_log_level ON log.script_log(level); + +-- View for recent logs (last 1000) +CREATE OR REPLACE VIEW log.script_log_recent AS +SELECT * FROM log.script_log +ORDER BY timestamp DESC +LIMIT 1000; + +-- Table and column comments +COMMENT ON TABLE log.script_log IS 'Persistent log for import scripts and library operations'; +COMMENT ON COLUMN log.script_log.level IS 'Log level: DEBUG, INFO, WARNING, ERROR'; +COMMENT ON COLUMN log.script_log.session_id IS 'Groups logs from a single script run (UUID)'; +COMMENT ON COLUMN log.script_log.script_name IS 'Name of the script or operation (e.g., import_ohlc.py)'; +COMMENT ON COLUMN log.script_log.series_name IS 'Optional: link to specific series'; +COMMENT ON COLUMN log.script_log.details IS 'Optional structured data stored as JSONB'; diff --git a/sql/migrate/migrate_drop_series_if_exists.sql b/sql/migrate/migrate_drop_series_if_exists.sql new file mode 100644 index 0000000..dd39e0b --- /dev/null +++ b/sql/migrate/migrate_drop_series_if_exists.sql @@ -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$; diff --git a/sql/migrate/migrate_instrument_metadata.sql b/sql/migrate/migrate_instrument_metadata.sql new file mode 100644 index 0000000..f80ef19 --- /dev/null +++ b/sql/migrate/migrate_instrument_metadata.sql @@ -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; diff --git a/sql/migrate/migrate_metadata_identity.sql b/sql/migrate/migrate_metadata_identity.sql new file mode 100644 index 0000000..938063e --- /dev/null +++ b/sql/migrate/migrate_metadata_identity.sql @@ -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 $$;