wave1: persist canonical L0 schema and fingerprint tooling

This commit is contained in:
Red Queen 2026-09-08 13:10:06 +00:00
parent e56567f7cf
commit 41e68aeb21
10 changed files with 5409 additions and 0 deletions

View file

@ -0,0 +1,105 @@
# D1/D2 — Canonical Schema Contract & Fingerprint Tooling (SoT)
**Status:** FORMALLY PERSISTED — authoritative origin/main
**Datum:** 2026-09-08
**Verdict:** D1+D2 FORMALLY PERSISTED TO AUTHORITATIVE SOT
---
## 1. Zweck
Dieses Verzeichnis materialisiert den **CURRENT VERIFIED PRODUCTION SCHEMA CONTRACT (L0)**
als kanonische, deterministisch fingerprintbare Autorität. Es ist das Ergebnis der
D1/D2-Remediation (Schema-Contract + deterministischer Fingerprint) und wurde durch den
unabhängigen D1+D2-Checker mit **Verdict A** bestätigt.
## 2. Canonical Fingerprint
```
b49f64337ffafc0e73d46e009aa802cd0172d59421b7df61d3eb198775c1986d
```
Dieser Fingerprint ist deterministisch (H1==H2==H3), sensitiv auf alle semantischen
Schema-Änderungen (type, nullable, default, PK, UNIQUE, FK, CHECK, index), und fail-closed.
## 3. DR-Reproducibility (ehrlich getrennt)
| Aspekt | Status |
|--------|--------|
| **CURRENT-STATE SCHEMA REPRODUCIBLE FROM L0** | **YES** — frische DB aus L0-SQL rekonstruiert, REBUILT_HASH == PRODUCTION_HASH == b49f6433…, Semantic-Diff TOTAL_MISMATCH=0 |
| **HISTORICAL EVOLUTION REPRODUCIBLE** | **NO** — historische DDL-Chronologie (Wave1→WaveD→Closure) bleibt nicht aus L0 rekonstruierbar |
Der L0-Contract beansprucht ausschliesslich **CURRENT PRODUCTION TRUTH**, nicht historische
Migrationschronologie. Historische Artefakte bleiben immutable.
## 4. Canonical Contract Semantics
| Tabelle | Spalten |
|---------|---------|
| historical_bar | 41 |
| derived_bar | 35 |
| data_ingestion_run | 29 |
| dataset_version | 23 |
| ingestion_provenance | 41 |
**Semantic Overrides (Owner-Canonical):**
- `last_successful_chunk` = **timestamp with time zone / timestamptz** (Production + Owner-Entscheidung)
- `interrupt_reason` = **text** · PRESERVED · origin unknown · do not remove
- `technical_quality` = **text** · nullable · default `'VALID'`
## 5. Historische Wahrheit (nicht rückwirkend ändern)
- **WaveD historisches Artefakt:** `last_successful_chunk = text` (immutable Evidence)
- **Owner/current canonical contract:** `last_successful_chunk = timestamptz`
- Historische Artefakte: Wave1 `6983526da3ca51fd2ececffbef0ccbd66b048a3d11c228000011aaba3e1d6bc5`,
WaveD `7aa4e2a10c98fa327e000a49f6a497efb5814c8808dba429dfebfc90c544a02b`,
Closure `e56567f7cfae68da70c11ece6f576e974488f343`**unverändert**.
## 6. Open Debts (nicht fälschlich geschlossen)
| Debt | Status |
|------|--------|
| **Debt A** — Current-state DR/schema reproducibility gap | **CLOSED** by D1/D2 |
| **Debt A** — Historical DDL evolution provenance gap | **REMAINS OPEN** |
| **Debt B** — interrupt_reason origin unknown | **OPEN** (non-blocking) |
| **Debt C** — WaveD text vs current Owner-canonical timestamptz | **OPEN** (non-blocking) |
## 7. SoT-Layout
```
d1d2-schema-contract/
├── SCHEMA_AUTHORITY.md # dieses Dokument
├── contract/
│ ├── historical_v2_schema.sql # L0-Schema-Contract (schema-only)
│ ├── historical_v2_schema.json # maschinenlesbares kanonisches Modell
│ ├── historical_v2_schema.sha256 # deterministischer Fingerprint
│ └── README.md # Contract-Dokumentation
└── tools/
├── canonical_schema.py # Kanonisierungs-Implementierung
├── capture_schema.py # Capture-Parser (read-only)
├── compare_schema.py # Semantic-Diff-CLI
├── fingerprint_schema.py # Fingerprint-CLI
└── gen_l0_sql.py # L0-SQL-Generator
```
**Nicht persistiert:** Maker-Evidence, Checker-Evidence, temporäre DBs, /tmp-Dateien,
raw Scratch Outputs, runtime-spezifische Dateien, Credentials/Secrets, Transport-Manifeste,
`gen_evidence.py` (enthält hardcodierte Maker-Pfade, nicht environment-neutral).
## 8. Tooling-Nutzung
```bash
# Fingerprint aus machine contract
python3 tools/fingerprint_schema.py contract/historical_v2_schema.json
# → b49f64337ffafc0e73d46e009aa802cd0172d59421b7df61d3eb198775c1986d
# Semantic-Diff (expected vs actual)
python3 tools/compare_schema.py contract/historical_v2_schema.json <actual_model.json>
# → TOTAL_MISMATCH=0 bei Übereinstimmung; Exit 1 bei Drift (fail-closed)
# L0-SQL generieren
python3 tools/gen_l0_sql.py contract/historical_v2_schema.json --out /tmp/l0.sql
```
Alle Tools sind environment-neutral (keine absoluten Pfade, keine DB-Credentials,
keine mutierenden Production-SQL-Pfade). Read-only.

View file

@ -0,0 +1,20 @@
# Historical V2 — Canonical Current Schema Contract (L0)
Diese Artefakte materialisieren den **CURRENT VERIFIED PRODUCTION SCHEMA CONTRACT**
als kanonische L0-Autorität (D1/D2 Remediation, forward-only).
- `historical_v2_schema.sql` — schema-only PostgreSQL-Contract (kein DML/Secrets/volatile Artefakte)
- `historical_v2_schema.json` — maschinenlesbares kanonisches Modell
- `historical_v2_schema.sha256` — deterministischer SHA-256-Fingerprint
- Kanonischer Fingerprint: `b49f64337ffafc0e73d46e009aa802cd0172d59421b7df61d3eb198775c1986d`
Dies ist KEINE historische Migrationskette. DO NOT INVENT HISTORY: der L0-Contract
beansprucht ausschliesslich CURRENT PRODUCTION TRUTH (§1).
Historische Artefakte (Wave1 `6983526da3ca51fd2ececffbef0ccbd66b048a3d11c228000011aaba3e1d6bc5`, WaveD `7aa4e2a10c98fa327e000a49f6a497efb5814c8808dba429dfebfc90c544a02b`) bleiben unverändert.
**Semantic Overrides (§4):**
- last_successful_chunk = timestamp with time zone (Production+Owner; WaveD-historical text bleibt immutable Evidence)
- interrupt_reason = text (preserved, origin unknown, do not remove)
- technical_quality = text nullable default 'VALID' (historical provenance debt OPEN)
**SoT:** origin/main = `e56567f7cfae68da70c11ece6f576e974488f343`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
b49f64337ffafc0e73d46e009aa802cd0172d59421b7df61d3eb198775c1986d

View file

@ -0,0 +1,601 @@
-- ============================================================
-- CANONICAL CURRENT PRODUCTION SCHEMA CONTRACT (L0)
-- db: historical · schema: public
-- Auto-generated by gen_l0_sql.py from read-only capture
-- Modus: CURRENT VERIFIED PRODUCTION TRUTH — NICHT historische Migration
-- ============================================================
-- SEMANTIC OVERRIDES (Owner-Canonical)
-- last_successful_chunk = timestamp with time zone (Production + Owner decision)
-- interrupt_reason = text · PRESERVED CURRENT CONTRACT · origin unknown · do not remove
-- technical_quality = text · nullable · default 'VALID' · historical provenance debt OPEN
CREATE SEQUENCE IF NOT EXISTS "calendar_event_event_id_seq";
CREATE SEQUENCE IF NOT EXISTS "corporate_action_action_id_seq";
CREATE SEQUENCE IF NOT EXISTS "daily_quality_report_report_id_seq";
CREATE SEQUENCE IF NOT EXISTS "data_gap_gap_id_seq";
CREATE SEQUENCE IF NOT EXISTS "data_ingestion_run_ingestion_id_seq";
CREATE SEQUENCE IF NOT EXISTS "data_quality_issue_issue_id_seq";
CREATE SEQUENCE IF NOT EXISTS "dataset_dataset_id_seq";
CREATE SEQUENCE IF NOT EXISTS "dataset_version_dataset_version_id_seq";
CREATE SEQUENCE IF NOT EXISTS "derived_bar_derived_bar_id_seq";
CREATE SEQUENCE IF NOT EXISTS "historical_bar_bar_id_seq";
CREATE SEQUENCE IF NOT EXISTS "historical_tick_tick_id_seq";
CREATE SEQUENCE IF NOT EXISTS "ingestion_provenance_provenance_id_seq";
CREATE SEQUENCE IF NOT EXISTS "instrument_instrument_id_seq";
CREATE SEQUENCE IF NOT EXISTS "market_calendar_calendar_id_seq";
CREATE SEQUENCE IF NOT EXISTS "provider_provider_id_seq";
-- Table: calendar_event (9 cols)
CREATE TABLE IF NOT EXISTS "calendar_event" (
"event_id" bigint NOT NULL DEFAULT nextval('calendar_event_event_id_seq'::regclass),
"calendar_id" bigint NOT NULL,
"event_date" date NOT NULL,
"kind" text NOT NULL,
"reason" text,
"session_phase_override" text,
"source" text,
"notes" text,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: corporate_action (9 cols)
CREATE TABLE IF NOT EXISTS "corporate_action" (
"action_id" bigint NOT NULL DEFAULT nextval('corporate_action_action_id_seq'::regclass),
"instrument_id" bigint NOT NULL,
"action_type" text NOT NULL,
"ex_date" date NOT NULL,
"ratio_num" double precision,
"ratio_denom" double precision,
"dividend" double precision,
"currency" text,
"provider_id" bigint
);
-- Table: daily_quality_report (23 cols)
CREATE TABLE IF NOT EXISTS "daily_quality_report" (
"report_id" bigint NOT NULL DEFAULT nextval('daily_quality_report_report_id_seq'::regclass),
"instrument_id" bigint NOT NULL,
"provider_id" bigint NOT NULL,
"report_date" date NOT NULL,
"feed_type" text DEFAULT 'REFERENCE'::text,
"expected_m1" bigint,
"received_m1" bigint,
"eligible" bigint,
"excluded" bigint,
"conditional" bigint,
"stale_count" bigint,
"zero_volume" bigint,
"spread_median_price" double precision,
"spread_p95_price" double precision,
"spread_p99_price" double precision,
"gaps_count" bigint,
"coverage_ratio" double precision,
"session_anomalies" bigint,
"status" text NOT NULL,
"quality_model_version" text,
"session_model_version" text,
"calendar_version" text,
"generated_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: data_gap (9 cols)
CREATE TABLE IF NOT EXISTS "data_gap" (
"gap_id" bigint NOT NULL DEFAULT nextval('data_gap_gap_id_seq'::regclass),
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"timeframe" text NOT NULL,
"start_utc" timestamp with time zone NOT NULL,
"end_utc" timestamp with time zone NOT NULL,
"kind" text NOT NULL,
"backfilled" boolean NOT NULL DEFAULT false,
"filled_at" timestamp with time zone
);
-- Table: data_ingestion_run (29 cols)
CREATE TABLE IF NOT EXISTS "data_ingestion_run" (
"ingestion_id" bigint NOT NULL DEFAULT nextval('data_ingestion_run_ingestion_id_seq'::regclass),
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"timeframe" text NOT NULL,
"start_utc" timestamp with time zone,
"end_utc" timestamp with time zone,
"rows_inserted" bigint DEFAULT 0,
"rows_skipped" bigint DEFAULT 0,
"status" text NOT NULL DEFAULT 'RUNNING'::text,
"started_at" timestamp with time zone NOT NULL DEFAULT now(),
"finished_at" timestamp with time zone,
"checkpoint" jsonb,
"status_fsm" text DEFAULT 'PENDING'::text,
"rows_downloaded" bigint DEFAULT 0,
"rows_decoded" bigint DEFAULT 0,
"rows_valid" bigint DEFAULT 0,
"rows_suspect" bigint DEFAULT 0,
"rows_excluded" bigint DEFAULT 0,
"rows_derived" bigint DEFAULT 0,
"last_successful_chunk" timestamp with time zone,
"last_timestamp" timestamp with time zone,
"updated_at" timestamp with time zone DEFAULT now(),
"interrupt_reason" text,
"rows_existing" bigint DEFAULT 0,
"rows_deduplicated" bigint DEFAULT 0,
"rows_404" bigint DEFAULT 0,
"rows_503" bigint DEFAULT 0,
"rows_provider_unavailable" bigint DEFAULT 0,
"rows_failed" bigint DEFAULT 0
);
-- Table: data_quality_issue (9 cols)
CREATE TABLE IF NOT EXISTS "data_quality_issue" (
"issue_id" bigint NOT NULL DEFAULT nextval('data_quality_issue_issue_id_seq'::regclass),
"instrument_id" bigint NOT NULL,
"timeframe" text NOT NULL,
"ts_utc" timestamp with time zone,
"severity" text NOT NULL,
"issue_type" text NOT NULL,
"detail" jsonb,
"resolved" boolean NOT NULL DEFAULT false,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: dataset (6 cols)
CREATE TABLE IF NOT EXISTS "dataset" (
"dataset_id" bigint NOT NULL DEFAULT nextval('dataset_dataset_id_seq'::regclass),
"name" text NOT NULL,
"provider_id" bigint NOT NULL,
"feed_type" text NOT NULL,
"description" text,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: dataset_version (23 cols)
CREATE TABLE IF NOT EXISTS "dataset_version" (
"dataset_version_id" bigint NOT NULL DEFAULT nextval('dataset_version_dataset_version_id_seq'::regclass),
"dataset_id" bigint NOT NULL,
"version_label" text NOT NULL,
"provider_id" bigint NOT NULL,
"feed_type" text NOT NULL,
"instrument_id" bigint NOT NULL,
"timeframe" text NOT NULL,
"start_utc" timestamp with time zone NOT NULL,
"end_utc" timestamp with time zone NOT NULL,
"raw_source" text NOT NULL,
"normalization_version" text NOT NULL,
"aggregation_version" text,
"quality_version" text NOT NULL,
"dataset_version_hash" text NOT NULL,
"data_as_of" timestamp with time zone NOT NULL,
"notes" text,
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
"quality_model_version" text,
"stale_model_version" text,
"session_model_version" text,
"calendar_version" text,
"eligibility_model_version" text,
"is_fixture" boolean NOT NULL DEFAULT false
);
-- Table: derived_bar (35 cols)
CREATE TABLE IF NOT EXISTS "derived_bar" (
"derived_bar_id" bigint NOT NULL DEFAULT nextval('derived_bar_derived_bar_id_seq'::regclass),
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"feed_type" text NOT NULL DEFAULT 'REFERENCE'::text,
"price_basis" text NOT NULL DEFAULT 'bid_ask'::text,
"timeframe" text NOT NULL,
"ts_utc" timestamp with time zone NOT NULL,
"source_timeframe" text NOT NULL,
"aggregation_version" text NOT NULL,
"bid_open" double precision,
"bid_high" double precision,
"bid_low" double precision,
"bid_close" double precision,
"ask_open" double precision,
"ask_high" double precision,
"ask_low" double precision,
"ask_close" double precision,
"mid_open" double precision,
"mid_high" double precision,
"mid_low" double precision,
"mid_close" double precision,
"volume" double precision,
"volume_type" text DEFAULT 'tick'::text,
"quality_status" text NOT NULL DEFAULT 'VALID'::text,
"quality_flags" _text,
"dataset_version_id" bigint NOT NULL,
"expected_bar_count" bigint,
"actual_bar_count" bigint,
"eligible_bar_count" bigint,
"excluded_bar_count" bigint,
"coverage_ratio" double precision,
"aggregation_quality" text,
"eligibility_model_version" text DEFAULT 'eligibility_v1'::text,
"session_model_version" text DEFAULT 'fx_session_v1'::text,
"calendar_version" text DEFAULT 'fx_cal_v1'::text
);
-- Table: historical_bar (41 cols)
CREATE TABLE IF NOT EXISTS "historical_bar" (
"bar_id" bigint NOT NULL DEFAULT nextval('historical_bar_bar_id_seq'::regclass),
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"feed_type" text NOT NULL DEFAULT 'REFERENCE'::text,
"price_basis" text NOT NULL DEFAULT 'bid_ask'::text,
"timeframe" text NOT NULL,
"ts_utc" timestamp with time zone NOT NULL,
"raw_or_derived" text NOT NULL DEFAULT 'raw'::text,
"source_timeframe" text,
"aggregation_version" text,
"bid_open" double precision,
"bid_high" double precision,
"bid_low" double precision,
"bid_close" double precision,
"ask_open" double precision,
"ask_high" double precision,
"ask_low" double precision,
"ask_close" double precision,
"mid_open" double precision,
"mid_high" double precision,
"mid_low" double precision,
"mid_close" double precision,
"volume" double precision,
"volume_type" text DEFAULT 'tick'::text,
"quality_status" text NOT NULL DEFAULT 'VALID'::text,
"quality_flags" _text,
"source_timestamp" timestamp with time zone,
"ingested_at" timestamp with time zone NOT NULL DEFAULT now(),
"dataset_version_id" bigint NOT NULL,
"technical_quality" text DEFAULT 'VALID'::text,
"market_quality" text,
"session_state" text,
"session_phase" text,
"eligibility" text DEFAULT 'ELIGIBLE'::text,
"exclusion_reason" text,
"quality_model_version" text DEFAULT 'quality_v1'::text,
"session_model_version" text,
"calendar_version" text,
"eligibility_model_version" text DEFAULT 'eligibility_v1'::text,
"effective_eligibility" text,
"stale_model_version" text
);
-- Table: historical_tick (10 cols)
CREATE TABLE IF NOT EXISTS "historical_tick" (
"tick_id" bigint NOT NULL DEFAULT nextval('historical_tick_tick_id_seq'::regclass),
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"feed_type" text NOT NULL DEFAULT 'REFERENCE'::text,
"ts_utc" timestamp with time zone NOT NULL,
"bid" double precision,
"ask" double precision,
"last" double precision,
"volume" double precision,
"dataset_version_id" bigint NOT NULL
);
-- Table: ingestion_provenance (41 cols)
CREATE TABLE IF NOT EXISTS "ingestion_provenance" (
"provenance_id" bigint NOT NULL,
"ingestion_run_id" bigint NOT NULL,
"provider_id" bigint NOT NULL,
"provider_name_snapshot" text NOT NULL,
"provider_environment_class" text NOT NULL,
"provider_dataset" text NOT NULL,
"provider_feed" text NOT NULL,
"provider_instrument_id" text NOT NULL,
"canonical_instrument_id" bigint NOT NULL,
"asset_class" text NOT NULL,
"request_start" timestamp with time zone NOT NULL,
"request_end" timestamp with time zone NOT NULL,
"actual_start" timestamp with time zone NOT NULL,
"actual_end" timestamp with time zone NOT NULL,
"request_timestamp" timestamp with time zone NOT NULL,
"response_timestamp" timestamp with time zone NOT NULL,
"http_status" integer NOT NULL,
"provider_metadata" jsonb,
"provider_version" text,
"raw_payload_sha256" text NOT NULL,
"raw_content_length" bigint NOT NULL,
"compression" text NOT NULL,
"content_type" text,
"parser_version" text NOT NULL,
"adapter_version" text NOT NULL,
"normalization_version" text NOT NULL,
"timezone_source" text NOT NULL,
"timezone_target" text NOT NULL,
"timezone_transform_version" text NOT NULL,
"price_type" text NOT NULL,
"granularity" text NOT NULL,
"correction_status" text NOT NULL DEFAULT 'ORIGINAL'::text,
"license_class" text NOT NULL DEFAULT 'UNKNOWN'::text,
"software_git_commit" text NOT NULL,
"software_image_id" text NOT NULL,
"software_build_input_sha" text,
"request_identity_hash" text NOT NULL,
"supersedes_provenance_id" bigint,
"raw_storage_ref" text,
"extra" jsonb,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: instrument (13 cols)
CREATE TABLE IF NOT EXISTS "instrument" (
"instrument_id" bigint NOT NULL DEFAULT nextval('instrument_instrument_id_seq'::regclass),
"symbol" text NOT NULL,
"asset_class" text NOT NULL,
"name" text,
"currency" text,
"exchange" text,
"mic" text,
"isin" text,
"tick_size" double precision,
"contract_size" double precision,
"min_deal_size" double precision,
"active" boolean NOT NULL DEFAULT true,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: market_calendar (13 cols)
CREATE TABLE IF NOT EXISTS "market_calendar" (
"calendar_id" bigint NOT NULL DEFAULT nextval('market_calendar_calendar_id_seq'::regclass),
"calendar_name" text NOT NULL,
"asset_class" text NOT NULL,
"venue_exchange" text,
"timezone" text NOT NULL,
"week_open_utc" text,
"week_close_utc" text,
"rollover_utc" text,
"session_open_utc" text,
"session_close_utc" text,
"notes" text,
"calendar_version" text NOT NULL,
"created_at" timestamp with time zone NOT NULL DEFAULT now()
);
-- Table: provider (6 cols)
CREATE TABLE IF NOT EXISTS "provider" (
"provider_id" bigint NOT NULL DEFAULT nextval('provider_provider_id_seq'::regclass),
"provider_key" text NOT NULL,
"name" text NOT NULL,
"data_type" text NOT NULL,
"pricing_plan" text,
"license_terms" text
);
-- Table: provider_instrument (6 cols)
CREATE TABLE IF NOT EXISTS "provider_instrument" (
"provider_id" bigint NOT NULL,
"instrument_id" bigint NOT NULL,
"provider_symbol" text NOT NULL,
"broker_epic" text,
"extra_metadata" jsonb,
"active" boolean NOT NULL DEFAULT true
);
ALTER TABLE "calendar_event" ADD CONSTRAINT "calendar_event_pkey" PRIMARY KEY ("event_id");
ALTER TABLE "corporate_action" ADD CONSTRAINT "corporate_action_pkey" PRIMARY KEY ("action_id");
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "daily_quality_report_pkey" PRIMARY KEY ("report_id");
ALTER TABLE "data_gap" ADD CONSTRAINT "data_gap_pkey" PRIMARY KEY ("gap_id");
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "data_ingestion_run_pkey" PRIMARY KEY ("ingestion_id");
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "data_quality_issue_pkey" PRIMARY KEY ("issue_id");
ALTER TABLE "dataset" ADD CONSTRAINT "dataset_pkey" PRIMARY KEY ("dataset_id");
ALTER TABLE "dataset_version" ADD CONSTRAINT "dataset_version_pkey" PRIMARY KEY ("dataset_version_id");
ALTER TABLE "derived_bar" ADD CONSTRAINT "derived_bar_pkey" PRIMARY KEY ("derived_bar_id");
ALTER TABLE "historical_bar" ADD CONSTRAINT "historical_bar_pkey" PRIMARY KEY ("bar_id");
ALTER TABLE "historical_tick" ADD CONSTRAINT "historical_tick_pkey" PRIMARY KEY ("tick_id");
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "ingestion_provenance_pkey" PRIMARY KEY ("provenance_id");
ALTER TABLE "instrument" ADD CONSTRAINT "instrument_pkey" PRIMARY KEY ("instrument_id");
ALTER TABLE "market_calendar" ADD CONSTRAINT "market_calendar_pkey" PRIMARY KEY ("calendar_id");
ALTER TABLE "provider" ADD CONSTRAINT "provider_pkey" PRIMARY KEY ("provider_id");
ALTER TABLE "provider_instrument" ADD CONSTRAINT "provider_instrument_pkey" PRIMARY KEY ("provider_id", "instrument_id");
ALTER TABLE "calendar_event" ADD CONSTRAINT "calendar_event_calendar_id_event_date_kind_key" UNIQUE ("calendar_id", "event_date", "kind");
ALTER TABLE "corporate_action" ADD CONSTRAINT "corporate_action_instrument_id_action_type_ex_date_provider_key" UNIQUE ("instrument_id", "action_type", "ex_date", "provider_id");
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "daily_quality_report_instrument_id_provider_id_report_date__key" UNIQUE ("instrument_id", "provider_id", "report_date", "feed_type");
ALTER TABLE "data_gap" ADD CONSTRAINT "data_gap_unique" UNIQUE ("provider_id", "instrument_id", "timeframe", "start_utc", "end_utc", "kind");
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "data_ingestion_run_provider_id_instrument_id_timeframe_star_key" UNIQUE ("provider_id", "instrument_id", "timeframe", "started_at");
ALTER TABLE "dataset" ADD CONSTRAINT "dataset_name_key" UNIQUE ("name");
ALTER TABLE "dataset_version" ADD CONSTRAINT "dataset_version_dataset_version_hash_key" UNIQUE ("dataset_version_hash");
ALTER TABLE "derived_bar" ADD CONSTRAINT "derived_bar_provider_id_instrument_id_feed_type_price_basis_key" UNIQUE ("provider_id", "instrument_id", "feed_type", "price_basis", "timeframe", "ts_utc", "aggregation_version");
ALTER TABLE "historical_bar" ADD CONSTRAINT "historical_bar_provider_id_instrument_id_feed_type_price_ba_key" UNIQUE ("provider_id", "instrument_id", "feed_type", "price_basis", "timeframe", "ts_utc", "raw_or_derived");
ALTER TABLE "historical_tick" ADD CONSTRAINT "historical_tick_provider_id_instrument_id_feed_type_ts_utc__key" UNIQUE ("provider_id", "instrument_id", "feed_type", "ts_utc", "bid", "ask");
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "ingestion_provenance_ingestion_run_id_key" UNIQUE ("ingestion_run_id");
ALTER TABLE "instrument" ADD CONSTRAINT "instrument_symbol_key" UNIQUE ("symbol");
ALTER TABLE "market_calendar" ADD CONSTRAINT "market_calendar_calendar_name_key" UNIQUE ("calendar_name");
ALTER TABLE "provider" ADD CONSTRAINT "provider_provider_key_key" UNIQUE ("provider_key");
ALTER TABLE "provider_instrument" ADD CONSTRAINT "provider_instrument_provider_id_provider_symbol_key" UNIQUE ("provider_id", "provider_symbol");
ALTER TABLE "calendar_event" ADD CONSTRAINT "calendar_event_calendar_id_fkey" FOREIGN KEY ("calendar_id") REFERENCES "market_calendar" ("calendar_id");
ALTER TABLE "corporate_action" ADD CONSTRAINT "corporate_action_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "corporate_action" ADD CONSTRAINT "corporate_action_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "daily_quality_report_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "daily_quality_report_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "data_gap" ADD CONSTRAINT "data_gap_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "data_gap" ADD CONSTRAINT "data_gap_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "data_ingestion_run_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "data_ingestion_run_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "data_quality_issue_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "dataset" ADD CONSTRAINT "dataset_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "dataset_version" ADD CONSTRAINT "dataset_version_dataset_id_fkey" FOREIGN KEY ("dataset_id") REFERENCES "dataset" ("dataset_id");
ALTER TABLE "dataset_version" ADD CONSTRAINT "dataset_version_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "dataset_version" ADD CONSTRAINT "dataset_version_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "derived_bar" ADD CONSTRAINT "derived_bar_dataset_version_id_fkey" FOREIGN KEY ("dataset_version_id") REFERENCES "dataset_version" ("dataset_version_id");
ALTER TABLE "derived_bar" ADD CONSTRAINT "derived_bar_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "derived_bar" ADD CONSTRAINT "derived_bar_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "historical_bar" ADD CONSTRAINT "historical_bar_dataset_version_id_fkey" FOREIGN KEY ("dataset_version_id") REFERENCES "dataset_version" ("dataset_version_id");
ALTER TABLE "historical_bar" ADD CONSTRAINT "historical_bar_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "historical_bar" ADD CONSTRAINT "historical_bar_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "historical_tick" ADD CONSTRAINT "historical_tick_dataset_version_id_fkey" FOREIGN KEY ("dataset_version_id") REFERENCES "dataset_version" ("dataset_version_id");
ALTER TABLE "historical_tick" ADD CONSTRAINT "historical_tick_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "historical_tick" ADD CONSTRAINT "historical_tick_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "ingestion_provenance_canonical_instrument_id_fkey" FOREIGN KEY ("canonical_instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "ingestion_provenance_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "ingestion_provenance_supersedes_fkey" FOREIGN KEY ("supersedes_provenance_id") REFERENCES "ingestion_provenance" ("provenance_id");
ALTER TABLE "provider_instrument" ADD CONSTRAINT "provider_instrument_instrument_id_fkey" FOREIGN KEY ("instrument_id") REFERENCES "instrument" ("instrument_id");
ALTER TABLE "provider_instrument" ADD CONSTRAINT "provider_instrument_provider_id_fkey" FOREIGN KEY ("provider_id") REFERENCES "provider" ("provider_id");
ALTER TABLE "calendar_event" ADD CONSTRAINT "2200_16679_1_not_null" CHECK (event_id IS NOT NULL);
ALTER TABLE "calendar_event" ADD CONSTRAINT "2200_16679_2_not_null" CHECK (calendar_id IS NOT NULL);
ALTER TABLE "calendar_event" ADD CONSTRAINT "2200_16679_3_not_null" CHECK (event_date IS NOT NULL);
ALTER TABLE "calendar_event" ADD CONSTRAINT "2200_16679_4_not_null" CHECK (kind IS NOT NULL);
ALTER TABLE "calendar_event" ADD CONSTRAINT "2200_16679_9_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "corporate_action" ADD CONSTRAINT "2200_16567_1_not_null" CHECK (action_id IS NOT NULL);
ALTER TABLE "corporate_action" ADD CONSTRAINT "2200_16567_2_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "corporate_action" ADD CONSTRAINT "2200_16567_3_not_null" CHECK (action_type IS NOT NULL);
ALTER TABLE "corporate_action" ADD CONSTRAINT "2200_16567_4_not_null" CHECK (ex_date IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_19_not_null" CHECK (status IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_1_not_null" CHECK (report_id IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_23_not_null" CHECK (generated_at IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_2_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_3_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "daily_quality_report" ADD CONSTRAINT "2200_16696_4_not_null" CHECK (report_date IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_1_not_null" CHECK (gap_id IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_2_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_3_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_4_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_5_not_null" CHECK (start_utc IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_6_not_null" CHECK (end_utc IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_7_not_null" CHECK (kind IS NOT NULL);
ALTER TABLE "data_gap" ADD CONSTRAINT "2200_16613_8_not_null" CHECK (backfilled IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_10_not_null" CHECK (started_at IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_1_not_null" CHECK (ingestion_id IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_2_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_3_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_4_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "data_ingestion_run" ADD CONSTRAINT "2200_16588_9_not_null" CHECK (status IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_1_not_null" CHECK (issue_id IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_2_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_3_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_5_not_null" CHECK (severity IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_6_not_null" CHECK (issue_type IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_8_not_null" CHECK (resolved IS NOT NULL);
ALTER TABLE "data_quality_issue" ADD CONSTRAINT "2200_16633_9_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "dataset" ADD CONSTRAINT "2200_16430_1_not_null" CHECK (dataset_id IS NOT NULL);
ALTER TABLE "dataset" ADD CONSTRAINT "2200_16430_2_not_null" CHECK (name IS NOT NULL);
ALTER TABLE "dataset" ADD CONSTRAINT "2200_16430_3_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "dataset" ADD CONSTRAINT "2200_16430_4_not_null" CHECK (feed_type IS NOT NULL);
ALTER TABLE "dataset" ADD CONSTRAINT "2200_16430_6_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_10_not_null" CHECK (raw_source IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_11_not_null" CHECK (normalization_version IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_13_not_null" CHECK (quality_version IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_14_not_null" CHECK (dataset_version_hash IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_15_not_null" CHECK (data_as_of IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_17_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_1_not_null" CHECK (dataset_version_id IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_23_not_null" CHECK (is_fixture IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_2_not_null" CHECK (dataset_id IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_3_not_null" CHECK (version_label IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_4_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_5_not_null" CHECK (feed_type IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_6_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_7_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_8_not_null" CHECK (start_utc IS NOT NULL);
ALTER TABLE "dataset_version" ADD CONSTRAINT "2200_16447_9_not_null" CHECK (end_utc IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_1_not_null" CHECK (derived_bar_id IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_24_not_null" CHECK (quality_status IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_26_not_null" CHECK (dataset_version_id IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_2_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_3_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_4_not_null" CHECK (feed_type IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_5_not_null" CHECK (price_basis IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_6_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_7_not_null" CHECK (ts_utc IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_8_not_null" CHECK (source_timeframe IS NOT NULL);
ALTER TABLE "derived_bar" ADD CONSTRAINT "2200_16508_9_not_null" CHECK (aggregation_version IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_1_not_null" CHECK (bar_id IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_25_not_null" CHECK (quality_status IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_28_not_null" CHECK (ingested_at IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_29_not_null" CHECK (dataset_version_id IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_2_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_3_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_4_not_null" CHECK (feed_type IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_5_not_null" CHECK (price_basis IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_6_not_null" CHECK (timeframe IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_7_not_null" CHECK (ts_utc IS NOT NULL);
ALTER TABLE "historical_bar" ADD CONSTRAINT "2200_16474_8_not_null" CHECK (raw_or_derived IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_10_not_null" CHECK (dataset_version_id IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_1_not_null" CHECK (tick_id IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_2_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_3_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_4_not_null" CHECK (feed_type IS NOT NULL);
ALTER TABLE "historical_tick" ADD CONSTRAINT "2200_16539_5_not_null" CHECK (ts_utc IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_10_not_null" CHECK (asset_class IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_11_not_null" CHECK (request_start IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_12_not_null" CHECK (request_end IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_13_not_null" CHECK (actual_start IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_14_not_null" CHECK (actual_end IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_15_not_null" CHECK (request_timestamp IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_16_not_null" CHECK (response_timestamp IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_17_not_null" CHECK (http_status IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_1_not_null" CHECK (provenance_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_20_not_null" CHECK (raw_payload_sha256 IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_21_not_null" CHECK (raw_content_length IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_22_not_null" CHECK (compression IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_24_not_null" CHECK (parser_version IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_25_not_null" CHECK (adapter_version IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_26_not_null" CHECK (normalization_version IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_27_not_null" CHECK (timezone_source IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_28_not_null" CHECK (timezone_target IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_29_not_null" CHECK (timezone_transform_version IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_2_not_null" CHECK (ingestion_run_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_30_not_null" CHECK (price_type IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_31_not_null" CHECK (granularity IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_32_not_null" CHECK (correction_status IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_33_not_null" CHECK (license_class IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_34_not_null" CHECK (software_git_commit IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_35_not_null" CHECK (software_image_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_37_not_null" CHECK (request_identity_hash IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_3_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_41_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_4_not_null" CHECK (provider_name_snapshot IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_5_not_null" CHECK (provider_environment_class IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_6_not_null" CHECK (provider_dataset IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_7_not_null" CHECK (provider_feed IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_8_not_null" CHECK (provider_instrument_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "2200_16741_9_not_null" CHECK (canonical_instrument_id IS NOT NULL);
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_actual_range" CHECK (((actual_end >= actual_start)));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_compression" CHECK (((compression = ANY (ARRAY['none'::text, 'lzma'::text, 'gzip'::text, 'zstd'::text, 'deflate'::text, 'unknown'::text]))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_content_length" CHECK (((raw_content_length >= 0)));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_correction" CHECK (((correction_status = ANY (ARRAY['ORIGINAL'::text, 'CORRECTED'::text, 'SUPERSEDED'::text, 'UNKNOWN'::text]))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_env_class" CHECK (((provider_environment_class = ANY (ARRAY['PROD'::text, 'SANDBOX'::text, 'TEST'::text, 'UNKNOWN'::text]))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_http_status" CHECK ((((http_status >= 100) AND (http_status <= 599))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_license" CHECK (((license_class = ANY (ARRAY['UNKNOWN'::text, 'PRIVATE_INTERNAL'::text, 'INTERNAL_DERIVED_ALLOWED'::text, 'STORAGE_RESTRICTED'::text, 'REDISTRIBUTION_RESTRICTED'::text, 'EXPIRED'::text, 'REVOKED'::text]))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_price_type" CHECK (((price_type = ANY (ARRAY['bid'::text, 'ask'::text, 'mid'::text, 'bid_ask'::text, 'last'::text, 'unknown'::text]))));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_sha256" CHECK (((raw_payload_sha256 ~ '^[0-9a-f]{64}$'::text)));
ALTER TABLE "ingestion_provenance" ADD CONSTRAINT "chk_prov_time_range" CHECK (((request_end >= request_start)));
ALTER TABLE "instrument" ADD CONSTRAINT "2200_16386_12_not_null" CHECK (active IS NOT NULL);
ALTER TABLE "instrument" ADD CONSTRAINT "2200_16386_13_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "instrument" ADD CONSTRAINT "2200_16386_1_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "instrument" ADD CONSTRAINT "2200_16386_2_not_null" CHECK (symbol IS NOT NULL);
ALTER TABLE "instrument" ADD CONSTRAINT "2200_16386_3_not_null" CHECK (asset_class IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_12_not_null" CHECK (calendar_version IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_13_not_null" CHECK (created_at IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_1_not_null" CHECK (calendar_id IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_2_not_null" CHECK (calendar_name IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_3_not_null" CHECK (asset_class IS NOT NULL);
ALTER TABLE "market_calendar" ADD CONSTRAINT "2200_16667_5_not_null" CHECK (timezone IS NOT NULL);
ALTER TABLE "provider" ADD CONSTRAINT "2200_16399_1_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "provider" ADD CONSTRAINT "2200_16399_2_not_null" CHECK (provider_key IS NOT NULL);
ALTER TABLE "provider" ADD CONSTRAINT "2200_16399_3_not_null" CHECK (name IS NOT NULL);
ALTER TABLE "provider" ADD CONSTRAINT "2200_16399_4_not_null" CHECK (data_type IS NOT NULL);
ALTER TABLE "provider_instrument" ADD CONSTRAINT "2200_16409_1_not_null" CHECK (provider_id IS NOT NULL);
ALTER TABLE "provider_instrument" ADD CONSTRAINT "2200_16409_2_not_null" CHECK (instrument_id IS NOT NULL);
ALTER TABLE "provider_instrument" ADD CONSTRAINT "2200_16409_3_not_null" CHECK (provider_symbol IS NOT NULL);
ALTER TABLE "provider_instrument" ADD CONSTRAINT "2200_16409_6_not_null" CHECK (active IS NOT NULL);
CREATE INDEX idx_bar_dsver ON public.historical_bar USING btree (dataset_version_id);
CREATE INDEX idx_bar_elig ON public.historical_bar USING btree (eligibility);
CREATE INDEX idx_bar_inst_tf_ts ON public.historical_bar USING btree (instrument_id, timeframe, ts_utc);
CREATE INDEX idx_bar_sess_ts ON public.historical_bar USING btree (session_state, instrument_id, ts_utc);
CREATE INDEX idx_bar_session ON public.historical_bar USING btree (session_state, session_phase);
CREATE INDEX idx_bar_techq ON public.historical_bar USING btree (technical_quality);
CREATE INDEX idx_derived_inst_tf_ts ON public.derived_bar USING btree (instrument_id, timeframe, ts_utc);
CREATE INDEX idx_historical_bar_market_quality ON public.historical_bar USING btree (instrument_id, market_quality);
CREATE INDEX idx_historical_bar_session_state ON public.historical_bar USING btree (instrument_id, session_state);
CREATE INDEX idx_prov_adapter_version ON public.ingestion_provenance USING btree (adapter_version);
CREATE INDEX idx_prov_instrument ON public.ingestion_provenance USING btree (canonical_instrument_id);
CREATE INDEX idx_prov_provider_feed ON public.ingestion_provenance USING btree (provider_id, provider_feed);
CREATE INDEX idx_prov_raw_sha ON public.ingestion_provenance USING btree (raw_payload_sha256);
CREATE INDEX idx_prov_request_identity ON public.ingestion_provenance USING btree (request_identity_hash);
CREATE INDEX idx_prov_time_range ON public.ingestion_provenance USING btree (request_start, request_end);
CREATE INDEX idx_tick_inst_ts ON public.historical_tick USING btree (instrument_id, ts_utc);

View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
D2 canonical_schema.py
Deterministischer Schema-Kanonisierer + Fingerprint für das Historical V2 Subsystem.
Liefert EINE autoritative Kanonisierungsfunktion, die aus einem Captured-Schema-Modell
(JSON, wie von capture_schema.py erzeugt) eine deterministische semantische
Serialisierung baut und daraus den SHA-256-Fingerprint berechnet.
Kanonisierungsregeln (siehe Mission §7):
- Schemas/Tabellen alphabetisch
- Spalten nach ordinal_position
- Constraints deterministisch nach (type, name, content)
- Indexe deterministisch
- Whitespace/Identifier-Quoting normalisiert, aber keine semantischen Unterschiede weggewaschen
Fingerprint-Coverage (§8): Tabellenexistenz, Spaltenname, ordinal_position,
Datentyp, Nullability, Default, Primary Key, UNIQUE, FK, CHECK, Index-Definitionen.
"""
import hashlib
import json
def _norm_whitespace(s):
"""Normalisiere Whitespace; reduziere Wiederholungen, aber erhalte Wörter/Zeichen."""
if s is None:
return None
return " ".join(str(s).split())
def sorted_cols(columns):
"""Spalten einer Tabelle, sortiert nach ordinal_position."""
return sorted(columns, key=lambda c: (c.get("ordinal", 0), c.get("column", "")))
def canonical_columns(columns):
"""Spalten → deterministische Liste von Tuplen."""
out = []
for c in sorted_cols(columns):
out.append((
c["column"],
int(c.get("ordinal", 0)),
_norm_whitespace(c.get("type")),
_norm_whitespace(c.get("udt")),
bool(c.get("nullable")),
_norm_whitespace(c.get("default")),
))
return out
def canonical_constraints(constraints, kind):
"""Constraints eines Typs (PRIMARY KEY, UNIQUE) → sortierte Liste."""
out = []
for con in constraints:
cols = tuple(c for c in con.get("columns") or [])
out.append((
con.get("table", ""),
con.get("constraint", ""),
cols,
))
return sorted(out, key=lambda x: (x[0], x[1], x[2]))
def canonical_fks(fks):
"""FK → sortierte Liste inkl. ref_table und ref_columns."""
out = []
for fk in fks:
out.append((
fk.get("table", ""),
fk.get("constraint", ""),
tuple(fk.get("columns") or []),
fk.get("ref_table", ""),
tuple(fk.get("ref_columns") or []),
))
return sorted(out, key=lambda x: (x[0], x[1]))
def canonical_checks(checks):
"""CHECK-Constraints → sortierte Liste mit normierter Definition.
Auto-generierte PostgreSQL 'NOT NULL'-Checks (CONSTRAINT-Name endet auf
'_not_null', von PostgreSQL automatisch pro NOT-NULL-Spalte erzeugt) werden
AUSGESCHLOSSEN: sie sind redundant zu 'nullable' und ihre Definition
unterscheidet sich zwischen DBs (Production 'x IS NOT NULL' vs. Checker
'((x IS NOT NULL))') semantisches Rauschen. Zuverlässige Erkennung NUR
über den Namenssuffix '_not_null'. Explizit benannte User-Checks
(chk_* etc.) bleiben erhalten.
"""
out = []
for chk in checks:
name = chk.get("constraint", "")
if name.endswith("_not_null"):
continue
out.append((
chk.get("table", ""),
name,
_norm_whitespace(chk.get("definition")) or "",
))
return sorted(out, key=lambda x: (x[0], x[1], x[2]))
def canonical_indexes(indexes):
"""Index-Definitionen → sortierte Liste mit normierter Definition."""
out = []
for idx in indexes:
out.append((
idx.get("table", ""),
idx.get("index", ""),
_norm_whitespace(idx.get("definition")),
))
return sorted(out, key=lambda x: (x[0], x[1], x[2]))
def canonical_sequences(sequences):
"""Sequenzen → sortierte Liste (nur Existenz/Name)."""
return sorted(sequences or [])
def canonical_document(model):
"""Baue die vollständige deterministische semantische Serialisierung."""
tables = sorted(model.get("tables") or [])
cols_by_table = {}
for c in model.get("columns") or []:
cols_by_table.setdefault(c["table"], []).append(c)
pks = canonical_constraints(model.get("primary_keys") or [], "PRIMARY KEY")
uniques = canonical_constraints(model.get("unique_constraints") or [], "UNIQUE")
fks = canonical_fks(model.get("foreign_keys") or [])
checks = canonical_checks(model.get("check_constraints") or [])
indexes = canonical_indexes(model.get("indexes") or [])
sequences = canonical_sequences(model.get("sequences") or [])
doc = []
doc.append("schema=public")
doc.append("sequences=" + "|".join(sequences))
doc.append("tables=" + "|".join(tables))
for t in tables:
doc.append(f"table:{t}:cols=" + ";".join(
f"{name}|{ordp}|{typ}|{udt}|{'n' if nul else 'y'}|{dflt}"
for (name, ordp, typ, udt, nul, dflt) in canonical_columns(cols_by_table.get(t, []))
))
doc.append("pk=" + ";".join(
f"{t}|{n}|{','.join(c)}"
for (t, n, c) in pks
))
doc.append("unique=" + ";".join(
f"{t}|{n}|{','.join(c)}"
for (t, n, c) in uniques
))
doc.append("fk=" + ";".join(
f"{t}|{n}|{','.join(c)}->{rt}|{','.join(rc)}"
for (t, n, c, rt, rc) in fks
))
doc.append("check=" + ";".join(
f"{t}|{n}|{d}"
for (t, n, d) in checks
))
doc.append("index=" + ";".join(
f"{t}|{n}|{d}"
for (t, n, d) in indexes
))
return "\n".join(doc)
def fingerprint(model):
"""SHA-256 über die kanonische Serialisierung."""
doc = canonical_document(model)
return hashlib.sha256(doc.encode("utf-8")).hexdigest(), doc
def load_model(path):
"""Lade ein Captured-Schema-Modell (JSON)."""
with open(path) as f:
return json.load(f)

View file

@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
D2 capture_schema.py
Liest Schema-Metadaten (read-only) von einer DB und erzeugt das kanonische Modell-JSON.
Quellenmodus: Erwartet eine Datei mit den 7 JSON-Aggregaten aus dem Capture-SQL
(der read-only psql-Ausgabe), ODER ein bereits geparstes Modell-JSON.
Usage:
capture_schema.py --from-capture <capture.json> --out <model.json>
capture_schema.py --from-model <model.json> --out <model.json> # Normalisierung
Der DB-Zugriff selbst erfolgt ausschließlich über das read-only Capture-SQL
(BEGIN TRANSACTION READ ONLY). Dieses Tool verarbeitet nur die Ergebnisse.
"""
import json
import sys
import argparse
AGG_NAMES = ["columns", "primary_keys", "unique_constraints",
"foreign_keys", "check_constraints", "indexes", "tables",
"sequences"]
def strip_psql_noise(text):
"""Entferne psql-Kontrollzeilen (BEGIN/on/COMMIT/leer/--)."""
out = []
for line in text.splitlines():
s = line.strip()
if s and s not in ("BEGIN", "on", "COMMIT") and not s.startswith("--"):
out.append(s)
return out
def parse_raw_capture(text):
"""Extrahiere die N JSON-Aggregate aus der psql-Ausgabe.
psql splittet große JSON-Arrays über mehrere Zeilen; daher wird nicht
zeilenzählend, sondern per JSON-Streaming (raw_decode) geparst.
"""
data = "\n".join(strip_psql_noise(text))
decoder = json.JSONDecoder()
aggs = []
i = 0
n = len(data)
while i < n and len(aggs) < len(AGG_NAMES):
while i < n and data[i] in " \t\r\n":
i += 1
if i >= n:
break
if data[i] not in "[{":
# non-JSON noise (z.B. Fehlermeldung) -> nächste Position
i += 1
continue
try:
val, end = decoder.raw_decode(data, i)
except json.JSONDecodeError as e:
raise SystemExit(f"FEHLER: JSON-Parse bei {i}: {e}")
aggs.append(val if val is not None else [])
i = end
if len(aggs) != len(AGG_NAMES):
raise SystemExit(
f"FEHLER: erwartete {len(AGG_NAMES)} JSON-Aggregate, fand {len(aggs)}")
return dict(zip(AGG_NAMES, aggs))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--from-capture", help="Rohe read-only psql-Capture-Datei (7 Aggregate)")
ap.add_argument("--from-model", help="Bereits geparstes Modell-JSON")
ap.add_argument("--out", required=True, help="Ziel-Modell-JSON")
args = ap.parse_args()
if args.from_capture:
with open(args.from_capture) as f:
model = parse_raw_capture(f.read())
elif args.from_model:
with open(args.from_model) as f:
model = json.load(f)
else:
raise SystemExit("FEHLER: --from-capture oder --from-model erforderlich")
# Konsistenz: columns/tables müssen übereinstimmen
col_tables = sorted({c["table"] for c in model["columns"]})
model["tables"] = sorted(model.get("tables") or col_tables)
with open(args.out, "w") as f:
json.dump(model, f, indent=2, sort_keys=True)
print(f"Modell geschrieben: {args.out}")
print(f"Tabellen: {len(model['tables'])} | Spalten: {len(model['columns'])}")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
D2 compare_schema.py
Vergleicht zwei Schema-Modelle (Soll = kanonischer L0-Contract, Ist = tatsächlich).
Semantische Diff-Dimensionen (§12):
fehlende/extras Tabellen, Spalten, Typ-Mismatches, Nullable-Mismatches,
Default-Mismatches, PK, UNIQUE, FK, CHECK, Index-Mismatches.
Fail-closed: exit-Nonzero bei beliebigem semantischem Mismatch.
Acceptance: alle Mismatch-Counts == 0.
"""
import sys
import json
import argparse
from canonical_schema import (canonical_columns, canonical_constraints,
canonical_fks, canonical_checks, canonical_indexes)
def diff_counts(actual, expected):
"""Liefert dict mit Mismatch-Counts. 0 = keine."""
counts = {
"missing_tables": 0, "extra_tables": 0,
"missing_columns": 0, "extra_columns": 0,
"type_mismatch": 0, "nullable_mismatch": 0, "default_mismatch": 0,
"pk_mismatch": 0, "unique_mismatch": 0, "fk_mismatch": 0,
"check_mismatch": 0, "index_mismatch": 0,
}
# Tabellen
a_tbl = set((actual.get("tables") or []))
e_tbl = set((expected.get("tables") or []))
counts["missing_tables"] = len(e_tbl - a_tbl)
counts["extra_tables"] = len(a_tbl - e_tbl)
# Spalten je Tabelle
a_cols = {}
for c in actual.get("columns") or []:
a_cols.setdefault(c["table"], []).append(c)
e_cols = {}
for c in expected.get("columns") or []:
e_cols.setdefault(c["table"], []).append(c)
for tbl in e_tbl:
ec = {c["column"]: c for c in e_cols.get(tbl, [])}
ac = {c["column"]: c for c in a_cols.get(tbl, [])}
for cname, cexp in ec.items():
if cname not in ac:
counts["missing_columns"] += 1
continue
cact = ac[cname]
if _n(cact.get("type")) != _n(cexp.get("type")):
counts["type_mismatch"] += 1
if bool(cact.get("nullable")) != bool(cexp.get("nullable")):
counts["nullable_mismatch"] += 1
if _n(cact.get("default")) != _n(cexp.get("default")):
counts["default_mismatch"] += 1
for cname in ac:
if cname not in ec:
counts["extra_columns"] += 1
# Constraints
if canonical_constraints(actual.get("primary_keys") or [], "PK") != \
canonical_constraints(expected.get("primary_keys") or [], "PK"):
counts["pk_mismatch"] += 1
if canonical_constraints(actual.get("unique_constraints") or [], "UNIQUE") != \
canonical_constraints(expected.get("unique_constraints") or [], "UNIQUE"):
counts["unique_mismatch"] += 1
if canonical_fks(actual.get("foreign_keys") or []) != \
canonical_fks(expected.get("foreign_keys") or []):
counts["fk_mismatch"] += 1
if canonical_checks(actual.get("check_constraints") or []) != \
canonical_checks(expected.get("check_constraints") or []):
counts["check_mismatch"] += 1
if canonical_indexes(actual.get("indexes") or []) != \
canonical_indexes(expected.get("indexes") or []):
counts["index_mismatch"] += 1
return counts
def _n(x):
return None if x is None else " ".join(str(x).split())
def main():
ap = argparse.ArgumentParser()
ap.add_argument("expected", help="Kanonisches L0-Contract-Modell")
ap.add_argument("actual", help="Tatsächlich gecaptures Modell")
args = ap.parse_args()
with open(args.expected) as f:
expected = json.load(f)
with open(args.actual) as f:
actual = json.load(f)
counts = diff_counts(actual, expected)
total = sum(counts.values())
print(json.dumps(counts, indent=2))
print(f"TOTAL_MISMATCH={total}")
if total == 0:
print("PASS: Semantic diff = 0")
return 0
print("FAIL: semantic drift detected", file=sys.stderr)
return 1 # FAIL CLOSED
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
D2 fingerprint_schema.py
Deterministischer SHA-256 über ein Captured-Schema-Modell.
Usage:
fingerprint_schema.py <model.json>
schreibt nur den Hex-Digest auf stdout (fail-closed: exit 0 bei Erfolg)
fingerprint_schema.py <model.json> --expect <digest>
exit 0 wenn MATCH, sonst exit 1 (FAIL CLOSED)
Der Fingerprint deckt ab: Tabellenexistenz, Spaltenname, ordinal_position,
Datentyp, Nullability, Default, PK, UNIQUE, FK, CHECK, Index-Definitionen (§8).
"""
import sys
import argparse
from canonical_schema import fingerprint, load_model
def main():
ap = argparse.ArgumentParser()
ap.add_argument("model")
ap.add_argument("--expect", default=None, help="Erwarteter Digest (fail-closed Vergleich)")
ap.add_argument("--no-hash", action="store_true", help="Nur kanonische Serialisierung ausgeben")
args = ap.parse_args()
model = load_model(args.model)
digest, doc = fingerprint(model)
if args.no_hash:
print(doc)
return 0
if args.expect:
if digest == args.expect:
print(f"MATCH {digest}")
return 0
else:
print(f"MISMATCH expected={args.expect} actual={digest}", file=sys.stderr)
return 1 # FAIL CLOSED
print(digest)
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
D2 gen_l0_sql.py
Erzeugt den kanonischen L0-SQL-Contract (schema-only) aus einem Captured-Schema-Modell.
Repräsentiert die aktuelle kanonische Production-Truth. Enthält KEINE:
data rows, credentials, volatile timestamps, container IDs, host paths, secrets.
Kein CREATE DATABASE, keine Grant/GRANT-relevanten Owner, keine Umgebungs-Sequenzen.
Semantische Overrides (§4) werden als Kommentar-Blöcke dokumentiert:
last_successful_chunk = timestamp with time zone (Owner-canonical)
interrupt_reason = text (preserved current contract)
technical_quality = text nullable default 'VALID'
"""
import json
import sys
import argparse
TYPE_MAP = {
'bigint': 'bigint', 'integer': 'integer', 'smallint': 'smallint',
'numeric': 'numeric', 'timestamp with time zone': 'timestamp with time zone',
'boolean': 'boolean', 'text': 'text', 'date': 'date', 'double precision': 'double precision',
'jsonb': 'jsonb', 'real': 'real', 'character varying': 'character varying',
}
def ident(s):
return '"' + str(s).replace('"', '""') + '"'
def col_def(c):
typ = TYPE_MAP.get(c.get('type'), c.get('udt', c.get('type', 'text')))
nullable = '' if not c.get('nullable') else ''
notnull = ' NOT NULL' if not c.get('nullable') else ''
dflt = c.get('default')
default_part = ''
if dflt:
default_part = ' DEFAULT ' + dflt
return f" {ident(c['column'])} {typ}{notnull}{default_part}"
def build_sql(model, dbname="historical"):
out = []
out.append("-- ============================================================")
out.append(f"-- CANONICAL CURRENT PRODUCTION SCHEMA CONTRACT (L0)")
out.append(f"-- db: {dbname} · schema: public")
out.append("-- Auto-generated by gen_l0_sql.py from read-only capture")
out.append("-- Modus: CURRENT VERIFIED PRODUCTION TRUTH — NICHT historische Migration")
out.append("-- ============================================================")
out.append("")
out.append("-- SEMANTIC OVERRIDES (Owner-Canonical)")
out.append("-- last_successful_chunk = timestamp with time zone (Production + Owner decision)")
out.append("-- interrupt_reason = text · PRESERVED CURRENT CONTRACT · origin unknown · do not remove")
out.append("-- technical_quality = text · nullable · default 'VALID' · historical provenance debt OPEN")
out.append("")
# Sequenzen (für nextval-Defaults)
for seq in sorted(model.get("sequences") or []):
out.append(f"CREATE SEQUENCE IF NOT EXISTS {ident(seq)};")
if model.get("sequences"):
out.append("")
tables = sorted(model.get("tables") or [])
cols_by_table = {}
for c in model.get("columns") or []:
cols_by_table.setdefault(c["table"], []).append(c)
pks = {}
for pk in model.get("primary_keys") or []:
cols = sorted(pk.get("columns") or [], key=lambda x: 0)
pks[pk["table"]] = [pk.get("columns") or []]
for t in tables:
cols = sorted(cols_by_table.get(t, []), key=lambda c: c.get("ordinal", 0))
out.append(f"-- Table: {t} ({len(cols)} cols)")
out.append(f"CREATE TABLE IF NOT EXISTS {ident(t)} (")
body_lines = [col_def(c) for c in cols]
out.append(",\n".join(body_lines))
out.append(");")
out.append("")
# PKs
for t in tables:
for pk in model.get("primary_keys") or []:
if pk["table"] == t:
colstr = ", ".join(ident(c) for c in pk.get("columns") or [])
out.append(f"ALTER TABLE {ident(t)} ADD CONSTRAINT {ident(pk['constraint'])} PRIMARY KEY ({colstr});")
out.append("")
# UNIQUE
for uq in sorted(model.get("unique_constraints") or [], key=lambda x: x["table"]):
colstr = ", ".join(ident(c) for c in uq.get("columns") or [])
out.append(f"ALTER TABLE {ident(uq['table'])} ADD CONSTRAINT {ident(uq['constraint'])} UNIQUE ({colstr});")
out.append("")
# FK
for fk in sorted(model.get("foreign_keys") or [], key=lambda x: x["table"]):
colstr = ", ".join(ident(c) for c in fk.get("columns") or [])
rcolstr = ", ".join(ident(c) for c in fk.get("ref_columns") or [])
out.append(f"ALTER TABLE {ident(fk['table'])} ADD CONSTRAINT {ident(fk['constraint'])} FOREIGN KEY ({colstr}) REFERENCES {ident(fk['ref_table'])} ({rcolstr});")
out.append("")
# CHECK - semantische Definition
for chk in sorted(model.get("check_constraints") or [], key=lambda x: (x["table"], x["constraint"])):
out.append(f"ALTER TABLE {ident(chk['table'])} ADD CONSTRAINT {ident(chk['constraint'])} CHECK ({chk.get('definition','')});")
out.append("")
# Indexe (non-PK/UNIQUE)
idx_set = set()
for pk in model.get("primary_keys") or []:
idx_set.add(pk["constraint"])
for uq in model.get("unique_constraints") or []:
idx_set.add(uq["constraint"])
for idx in sorted(model.get("indexes") or [], key=lambda x: x["index"]):
if idx["index"] in idx_set:
continue
out.append(f"{idx['definition']};")
out.append("")
return "\n".join(out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("model")
ap.add_argument("--out", required=True)
args = ap.parse_args()
with open(args.model) as f:
model = json.load(f)
sql = build_sql(model)
with open(args.out, "w") as f:
f.write(sql)
print(f"L0 SQL-Contract geschrieben: {args.out} ({len(sql)} B)")
return 0
if __name__ == "__main__":
sys.exit(main())