diff --git a/topology_api/migrations/001_extensions.sql b/topology_api/migrations/001_extensions.sql index 20e062c..ddb0dfa 100644 --- a/topology_api/migrations/001_extensions.sql +++ b/topology_api/migrations/001_extensions.sql @@ -1,8 +1,14 @@ -- topology_api/migrations/001_extensions.sql -- Create schema and required PostgreSQL extensions +-- Extensions are installed in both public and topology schemas +-- so types like citext resolve without schema prefix CREATE SCHEMA IF NOT EXISTS topology; +CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; +CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public; +CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public; + CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA topology; CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA topology; CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA topology; diff --git a/topology_api/migrations/002_tables.sql b/topology_api/migrations/002_tables.sql new file mode 100644 index 0000000..08420d9 --- /dev/null +++ b/topology_api/migrations/002_tables.sql @@ -0,0 +1,149 @@ +-- topology_api/migrations/002_tables.sql +-- 8 core tables with temporal constraints + +-- 1. bases +CREATE TABLE topology.bases ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code text NOT NULL, + name text NOT NULL, + description text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_bases_code UNIQUE (code) +); + +-- 2. missions +CREATE TABLE topology.missions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + base_id uuid NOT NULL REFERENCES topology.bases(id), + code citext NOT NULL, + title text NOT NULL, + occurrence_status text NOT NULL DEFAULT 'occurred' + CHECK (occurrence_status IN ('occurred', 'planned')), + description text, + source_ref text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_missions_base_code UNIQUE (base_id, code) +); + +-- 3. mission_events +CREATE TABLE topology.mission_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + mission_id uuid NOT NULL REFERENCES topology.missions(id), + effective_at timestamptz NOT NULL, + event_order integer NOT NULL DEFAULT 0, + event_type text NOT NULL, + title text NOT NULL, + details jsonb, + source_ref text, + recorded_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_mission_event_time UNIQUE (mission_id, effective_at, event_order) +); + +-- 4. components +CREATE TABLE topology.components ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + base_id uuid NOT NULL REFERENCES topology.bases(id), + component_key text NOT NULL, + component_type text NOT NULL + CHECK (component_type IN ('module', 'node', 'vehicle', 'equipment')), + introduced_by_event_id uuid NOT NULL REFERENCES topology.mission_events(id), + retired_by_event_id uuid REFERENCES topology.mission_events(id), + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_components_base_key UNIQUE (base_id, component_key) +); + +-- 5. component_versions +CREATE TABLE topology.component_versions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + component_id uuid NOT NULL REFERENCES topology.components(id), + display_name text NOT NULL, + summary text, + description text, + length_m numeric(10,3), + width_m numeric(10,3), + height_m numeric(10,3), + diameter_m numeric(10,3), + mass_t numeric(12,3), + detail_url text, + properties jsonb, + valid_from timestamptz NOT NULL, + valid_to timestamptz, + started_by_event_id uuid NOT NULL REFERENCES topology.mission_events(id), + ended_by_event_id uuid REFERENCES topology.mission_events(id), + valid_period tstzrange GENERATED ALWAYS AS ( + tstzrange(valid_from, valid_to, '[)') + ) STORED, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_version_valid_range CHECK (valid_from < valid_to OR valid_to IS NULL) +); + +-- 6. component_states +CREATE TABLE topology.component_states ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + component_id uuid NOT NULL REFERENCES topology.components(id), + state_kind text NOT NULL, + state_value text NOT NULL, + details jsonb, + valid_from timestamptz NOT NULL, + valid_to timestamptz, + started_by_event_id uuid NOT NULL REFERENCES topology.mission_events(id), + ended_by_event_id uuid REFERENCES topology.mission_events(id), + valid_period tstzrange GENERATED ALWAYS AS ( + tstzrange(valid_from, valid_to, '[)') + ) STORED, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_state_valid_range CHECK (valid_from < valid_to OR valid_to IS NULL) +); + +-- 7. ports +CREATE TABLE topology.ports ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + component_id uuid NOT NULL REFERENCES topology.components(id), + port_key text NOT NULL, + display_name text NOT NULL, + direction text, + interface_type text NOT NULL DEFAULT 'structural', + properties jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT uq_ports_component_key UNIQUE (component_id, port_key) +); + +-- 8. topology_connections +CREATE TABLE topology.topology_connections ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + base_id uuid NOT NULL REFERENCES topology.bases(id), + port_a_id uuid NOT NULL REFERENCES topology.ports(id), + port_b_id uuid NOT NULL REFERENCES topology.ports(id), + connection_type text NOT NULL + CHECK (connection_type IN ( + 'structural_mount', 'pressurized_passage', 'docking', + 'power', 'fuel', 'cooling' + )), + properties jsonb, + valid_from timestamptz NOT NULL, + valid_to timestamptz, + established_by_event_id uuid NOT NULL REFERENCES topology.mission_events(id), + ended_by_event_id uuid REFERENCES topology.mission_events(id), + valid_period tstzrange GENERATED ALWAYS AS ( + tstzrange(valid_from, valid_to, '[)') + ) STORED, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_connection_valid_range CHECK (valid_from < valid_to OR valid_to IS NULL), + CONSTRAINT ck_different_ports CHECK (port_a_id <> port_b_id) +); + +-- Temporal exclusion constraints +ALTER TABLE topology.component_versions + ADD CONSTRAINT ex_version_no_overlap + EXCLUDE USING gist (component_id WITH =, valid_period WITH &&); + +ALTER TABLE topology.component_states + ADD CONSTRAINT ex_state_no_overlap + EXCLUDE USING gist (component_id WITH =, state_kind WITH =, valid_period WITH &&); diff --git a/topology_api/migrations/003_indexes.sql b/topology_api/migrations/003_indexes.sql new file mode 100644 index 0000000..c12a381 --- /dev/null +++ b/topology_api/migrations/003_indexes.sql @@ -0,0 +1,38 @@ +-- topology_api/migrations/003_indexes.sql +-- Performance indexes for snapshot queries + +-- missions lookup +CREATE INDEX IF NOT EXISTS idx_missions_base_status + ON topology.missions(base_id, occurrence_status); + +-- events time ordering +CREATE INDEX IF NOT EXISTS idx_events_mission_time + ON topology.mission_events(mission_id, effective_at DESC, event_order DESC); +CREATE INDEX IF NOT EXISTS idx_events_effective_time + ON topology.mission_events(effective_at DESC); + +-- components lookup +CREATE INDEX IF NOT EXISTS idx_components_base + ON topology.components(base_id); + +-- versions time range +CREATE INDEX IF NOT EXISTS idx_versions_component_time + ON topology.component_versions(component_id, valid_from DESC); +CREATE INDEX IF NOT EXISTS idx_versions_current + ON topology.component_versions(component_id) WHERE valid_to IS NULL; + +-- states time range +CREATE INDEX IF NOT EXISTS idx_states_component_kind_time + ON topology.component_states(component_id, state_kind, valid_from DESC); +CREATE INDEX IF NOT EXISTS idx_states_current + ON topology.component_states(component_id, state_kind) WHERE valid_to IS NULL; + +-- connections time range +CREATE INDEX IF NOT EXISTS idx_connections_base_time + ON topology.topology_connections(base_id, valid_from DESC); +CREATE INDEX IF NOT EXISTS idx_connections_current + ON topology.topology_connections(base_id) WHERE valid_to IS NULL; + +-- ports +CREATE INDEX IF NOT EXISTS idx_ports_component + ON topology.ports(component_id);