import { backfillManagedReaderSourceConnections } from "./reader-source-scope.mjs"; export async function migrate(pool) { await pool.query("create extension if not exists timescaledb"); await pool.query("create extension if not exists postgis"); await pool.query(` create table if not exists external_data_plane_products ( id text primary key, version text not null, ontology_revision text not null, delivery_mode text not null, semantic_types jsonb not null, fields jsonb not null, field_contracts jsonb not null default '{}'::jsonb, history_policy jsonb not null, active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), check (jsonb_typeof(semantic_types) = 'array' and jsonb_array_length(semantic_types) > 0), check (jsonb_typeof(fields) = 'array' and jsonb_array_length(fields) > 0), constraint external_data_plane_products_field_contracts_object_ck check (jsonb_typeof(field_contracts) = 'object') ) `); await pool.query("alter table external_data_plane_products add column if not exists field_contracts jsonb not null default '{}'::jsonb"); await pool.query(` do $$ begin if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_products'::regclass and conname = 'external_data_plane_products_field_contracts_object_ck' ) then alter table external_data_plane_products add constraint external_data_plane_products_field_contracts_object_ck check (jsonb_typeof(field_contracts) = 'object'); end if; end $$ `); await pool.query(` create table if not exists external_data_plane_batches ( id uuid primary key, tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, contract_version text not null, ontology_revision text not null, run_id text not null, sequence integer not null, idempotency_key text not null, received_at timestamptz not null, fact_count integer not null default 0, inserted_fact_count integer not null default 0, created_at timestamptz not null default now(), unique (tenant_id, connection_id, provider_id, data_product_id, idempotency_key) ) `); await pool.query("create index if not exists external_data_plane_batches_scope_idx on external_data_plane_batches (tenant_id, connection_id, data_product_id, received_at desc)"); await pool.query("alter table external_data_plane_batches add column if not exists current_updated_count integer not null default 0"); await pool.query("alter table external_data_plane_batches add column if not exists current_removed_count integer not null default 0"); await pool.query("alter table external_data_plane_batches add column if not exists history_inserted_count integer not null default 0"); await pool.query("alter table external_data_plane_batches add column if not exists patch_operation_count integer not null default 0"); await pool.query("alter table external_data_plane_batches add column if not exists delivery_cursor bigint"); await pool.query("alter table external_data_plane_batches add column if not exists request_fingerprint text"); await pool.query(` create table if not exists external_data_plane_raw_envelopes ( id uuid primary key, batch_id uuid not null unique references external_data_plane_batches(id) on delete cascade, payload_hash text not null, content_type text not null, payload jsonb, payload_ref text, payload_bytes integer, received_at timestamptz not null, expires_at timestamptz, check (payload is not null or payload_ref is not null) ) `); await pool.query("create index if not exists external_data_plane_raw_expiry_idx on external_data_plane_raw_envelopes (expires_at)"); await pool.query(` create table if not exists external_data_plane_writer_bindings ( id uuid primary key, token_hash text not null unique, binding_key text, request_hash text, generation integer not null default 1, tenant_id text not null, connection_id text not null, provider_id text not null, allowed_data_product_ids jsonb not null, expires_at timestamptz, active boolean not null default true, created_at timestamptz not null default now(), rotated_at timestamptz, revoked_at timestamptz, check (generation > 0), check ( case when jsonb_typeof(allowed_data_product_ids) = 'array' then jsonb_array_length(allowed_data_product_ids) > 0 else false end ) ) `); await pool.query("alter table external_data_plane_writer_bindings add column if not exists binding_key text"); await pool.query("alter table external_data_plane_writer_bindings add column if not exists request_hash text"); await pool.query("alter table external_data_plane_writer_bindings add column if not exists generation integer not null default 1"); await pool.query("alter table external_data_plane_writer_bindings alter column expires_at drop not null"); await pool.query(` do $$ begin if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_writer_bindings'::regclass and conname = 'external_data_plane_writer_bindings_generation_positive_ck' ) then alter table external_data_plane_writer_bindings add constraint external_data_plane_writer_bindings_generation_positive_ck check (generation > 0); end if; if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_writer_bindings'::regclass and conname = 'external_data_plane_writer_bindings_managed_metadata_ck' ) then alter table external_data_plane_writer_bindings add constraint external_data_plane_writer_bindings_managed_metadata_ck check ( (binding_key is null and request_hash is null) or (binding_key is not null and request_hash ~ '^[a-f0-9]{64}$') ); end if; if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_writer_bindings'::regclass and conname = 'external_data_plane_writer_bindings_lifecycle_ck' ) then alter table external_data_plane_writer_bindings add constraint external_data_plane_writer_bindings_lifecycle_ck check ( (binding_key is null and expires_at is not null) or (binding_key is not null and expires_at is null) ) not valid; end if; end $$ `); await pool.query("create unique index if not exists external_data_plane_writer_bindings_managed_key_idx on external_data_plane_writer_bindings (binding_key, generation) where binding_key is not null"); await pool.query("create index if not exists external_data_plane_writer_bindings_active_idx on external_data_plane_writer_bindings (active, expires_at)"); await pool.query(` create table if not exists external_data_plane_reader_bindings ( id uuid primary key, token_hash text not null unique, binding_key text, request_hash text, generation integer not null default 1, tenant_id text not null, connection_id text not null, provider_id text not null, allowed_data_product_ids jsonb not null, expires_at timestamptz, active boolean not null default true, created_at timestamptz not null default now(), rotated_at timestamptz, revoked_at timestamptz, check (generation > 0), check ( case when jsonb_typeof(allowed_data_product_ids) = 'array' then jsonb_array_length(allowed_data_product_ids) > 0 else false end ) ) `); await pool.query("alter table external_data_plane_reader_bindings add column if not exists binding_key text"); await pool.query("alter table external_data_plane_reader_bindings add column if not exists request_hash text"); await pool.query("alter table external_data_plane_reader_bindings add column if not exists generation integer not null default 1"); await pool.query("alter table external_data_plane_reader_bindings add column if not exists source_connection_id text"); await pool.query("alter table external_data_plane_reader_bindings alter column expires_at drop not null"); await pool.query(` do $$ begin if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_reader_bindings'::regclass and conname = 'external_data_plane_reader_bindings_generation_positive_ck' ) then alter table external_data_plane_reader_bindings add constraint external_data_plane_reader_bindings_generation_positive_ck check (generation > 0); end if; if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_reader_bindings'::regclass and conname = 'external_data_plane_reader_bindings_managed_metadata_ck' ) then alter table external_data_plane_reader_bindings add constraint external_data_plane_reader_bindings_managed_metadata_ck check ( (binding_key is null and request_hash is null) or (binding_key is not null and request_hash ~ '^[a-f0-9]{64}$') ); end if; if not exists ( select 1 from pg_constraint where conrelid = 'external_data_plane_reader_bindings'::regclass and conname = 'external_data_plane_reader_bindings_lifecycle_ck' ) then alter table external_data_plane_reader_bindings add constraint external_data_plane_reader_bindings_lifecycle_ck check ( (binding_key is null and expires_at is not null) or (binding_key is not null and expires_at is null) ); end if; end $$ `); await pool.query("create unique index if not exists external_data_plane_reader_bindings_managed_key_idx on external_data_plane_reader_bindings (binding_key, generation) where binding_key is not null"); await pool.query("create index if not exists external_data_plane_reader_bindings_active_idx on external_data_plane_reader_bindings (active, expires_at)"); await pool.query("create index if not exists external_data_plane_reader_bindings_source_idx on external_data_plane_reader_bindings (tenant_id, source_connection_id, provider_id, active)"); await pool.query(` update external_data_plane_reader_bindings set source_connection_id = connection_id where binding_key is null and source_connection_id is null `); await backfillManagedReaderSourceConnections(pool); await pool.query(` create table if not exists external_data_plane_facts ( id uuid not null, batch_id uuid not null references external_data_plane_batches(id) on delete cascade, tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, source_id text not null, semantic_type text not null, observed_at timestamptz not null, received_at timestamptz not null, attributes jsonb not null default '{}'::jsonb, geometry geography(Geometry, 4326), fingerprint text not null, primary key (id, observed_at), unique (tenant_id, connection_id, provider_id, data_product_id, source_id, semantic_type, observed_at, fingerprint) ) `); await pool.query("select create_hypertable('external_data_plane_facts', 'observed_at', if_not_exists => true, migrate_data => true)"); await pool.query("create index if not exists external_data_plane_facts_source_time_idx on external_data_plane_facts (tenant_id, connection_id, data_product_id, source_id, observed_at desc)"); await pool.query("create index if not exists external_data_plane_facts_geometry_idx on external_data_plane_facts using gist (geometry)"); await pool.query(` create table if not exists external_data_plane_current ( tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, source_id text not null, semantic_type text not null, observed_at timestamptz not null, received_at timestamptz not null, attributes jsonb not null default '{}'::jsonb, geometry geography(Geometry, 4326), fingerprint text not null, updated_at timestamptz not null default now(), primary key (tenant_id, connection_id, provider_id, data_product_id, source_id, semantic_type) ) `); await pool.query(` do $$ begin if exists ( select 1 from pg_attribute where attrelid = 'external_data_plane_current'::regclass and attname = 'geometry' and not attisdropped and format_type(atttypid, atttypmod) = 'geography(Point,4326)' ) then alter table external_data_plane_current alter column geometry type geography(Geometry, 4326) using geometry::geometry::geography; end if; end $$ `); await pool.query("create index if not exists external_data_plane_current_scope_idx on external_data_plane_current (tenant_id, connection_id, data_product_id, observed_at desc)"); await pool.query("create index if not exists external_data_plane_current_geometry_idx on external_data_plane_current using gist (geometry)"); await pool.query(` create table if not exists external_data_plane_delivery_state ( tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, current_cursor bigint not null default 0, current_generation_at timestamptz, current_generation_id text, updated_at timestamptz not null default now(), primary key (tenant_id, connection_id, provider_id, data_product_id) ) `); await pool.query("alter table external_data_plane_delivery_state add column if not exists current_generation_at timestamptz"); await pool.query("alter table external_data_plane_delivery_state add column if not exists current_generation_id text"); await pool.query(` create table if not exists external_data_plane_patch_outbox ( tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, cursor bigint not null, previous_cursor bigint not null, batch_id uuid not null references external_data_plane_batches(id) on delete cascade, operations jsonb not null, emitted_at timestamptz not null default now(), primary key (tenant_id, connection_id, provider_id, data_product_id, cursor), check (jsonb_typeof(operations) = 'array' and jsonb_array_length(operations) > 0) ) `); await pool.query("create index if not exists external_data_plane_patch_outbox_retention_idx on external_data_plane_patch_outbox (emitted_at)"); await pool.query(` create table if not exists external_data_plane_history ( tenant_id text not null, connection_id text not null, provider_id text not null, data_product_id text not null, source_id text not null, semantic_type text not null, bucket_start timestamptz not null, observed_at timestamptz not null, received_at timestamptz not null, attributes jsonb not null default '{}'::jsonb, geometry geography(Geometry, 4326), fingerprint text not null, updated_at timestamptz not null default now(), primary key ( tenant_id, connection_id, provider_id, data_product_id, source_id, semantic_type, bucket_start ) ) `); await pool.query("select create_hypertable('external_data_plane_history', 'bucket_start', if_not_exists => true, migrate_data => true)"); await pool.query("create index if not exists external_data_plane_history_source_time_idx on external_data_plane_history (tenant_id, connection_id, data_product_id, source_id, bucket_start desc)"); await pool.query("create index if not exists external_data_plane_history_geometry_idx on external_data_plane_history using gist (geometry)"); }