-- TIME Phase T1 foundation for shared time-entry structures in the NEW run"time". -- No source import logic yet; this migration only creates the shared TIME schema and base state tables. CREATE SCHEMA IF NOT EXISTS TIME; SET search_path TO TIME, DOC, public; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typname = 'time_source_system' AND n.nspname = 'time' ) THEN CREATE TYPE "time".time_source_system AS ENUM ('LEITSTAND', 'TOGGL_TRACK'); END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typname = 'time_sync_run_status' AND n.nspname = 'time' ) THEN CREATE TYPE "time".time_sync_run_status AS ENUM ('RUNNING', 'COMPLETED', 'FAILED'); END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typname = 'time_sync_run_type' AND n.nspname = 'time' ) THEN CREATE TYPE "time".time_sync_run_type AS ENUM ('INITIAL', 'INCREMENTAL', 'RECONCILE', 'MANUAL'); END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_enum e JOIN pg_type t ON t.oid = e.enumtypid JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'doc' AND t.typname = 'doc_document_type' AND e.enumlabel = 'TIME_ENTRY' ) THEN ALTER TYPE doc.doc_document_type ADD VALUE 'TIME_ENTRY'; END IF; END $$; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_enum e JOIN pg_type t ON t.oid = e.enumtypid JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'doc' AND t.typname = 'doc_document_family' AND e.enumlabel = 'TIME' ) THEN ALTER TYPE doc.doc_document_family ADD VALUE 'TIME'; END IF; END $$; CREATE TABLE IF NOT EXISTS "time".time_entry ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), document_id UUID NOT NULL UNIQUE REFERENCES doc.doc_document(id) ON DELETE CASCADE, source_system "time".time_source_system NOT NULL, external_id VARCHAR(255) NOT NULL, person_external_id VARCHAR(255), person_display_name VARCHAR(255), entry_start TIMESTAMP WITH TIME ZONE, entry_end TIMESTAMP WITH TIME ZONE, duration_seconds BIGINT, description_short VARCHAR(1000), description_long TEXT, billable BOOLEAN, source_created_at TIMESTAMP WITH TIME ZONE, source_updated_at TIMESTAMP WITH TIME ZONE, source_deleted_at TIMESTAMP WITH TIME ZONE, raw_status VARCHAR(120), search_anchor_label VARCHAR(500), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_time_entry_source UNIQUE (source_system, external_id) ); CREATE TABLE IF NOT EXISTS "time".time_entry_source_link ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), time_entry_id UUID NOT NULL REFERENCES "time".time_entry(id) ON DELETE CASCADE, source_system "time".time_source_system NOT NULL, source_entity_type VARCHAR(120) NOT NULL, source_external_id VARCHAR(255) NOT NULL, linked_role VARCHAR(120), parent_source_external_id VARCHAR(255), source_updated_at TIMESTAMP WITH TIME ZONE, source_deleted_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_time_entry_source_link UNIQUE (time_entry_id, source_system, source_entity_type, source_external_id) ); CREATE TABLE IF NOT EXISTS "time".time_sync_run ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_system "time".time_source_system NOT NULL, run_type "time".time_sync_run_type NOT NULL, scope_key VARCHAR(255) NOT NULL, status "time".time_sync_run_status NOT NULL, started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, finished_at TIMESTAMP WITH TIME ZONE, watermark_from VARCHAR(255), watermark_to VARCHAR(255), rows_read INTEGER NOT NULL DEFAULT 0, rows_created INTEGER NOT NULL DEFAULT 0, rows_updated INTEGER NOT NULL DEFAULT 0, rows_deactivated INTEGER NOT NULL DEFAULT 0, rows_failed INTEGER NOT NULL DEFAULT 0, error_message TEXT, initiated_by VARCHAR(120), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS "time".time_sync_state ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_system "time".time_source_system NOT NULL, scope_key VARCHAR(255) NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, last_successful_sync_at TIMESTAMP WITH TIME ZONE, last_attempted_sync_at TIMESTAMP WITH TIME ZONE, last_seen_watermark VARCHAR(255), last_seen_external_id VARCHAR(255), last_run_id UUID REFERENCES "time".time_sync_run(id) ON DELETE SET NULL, notes TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_time_sync_state_scope UNIQUE (source_system, scope_key) ); CREATE INDEX IF NOT EXISTS idx_time_entry_source_system ON "time".time_entry(source_system); CREATE INDEX IF NOT EXISTS idx_time_entry_external_id ON "time".time_entry(external_id); CREATE INDEX IF NOT EXISTS idx_time_entry_start ON "time".time_entry(entry_start DESC); CREATE INDEX IF NOT EXISTS idx_time_entry_end ON "time".time_entry(entry_end DESC); CREATE INDEX IF NOT EXISTS idx_time_entry_source_updated ON "time".time_entry(source_updated_at DESC); CREATE INDEX IF NOT EXISTS idx_time_entry_link_entry ON "time".time_entry_source_link(time_entry_id); CREATE INDEX IF NOT EXISTS idx_time_entry_link_source_lookup ON "time".time_entry_source_link(source_system, source_entity_type, source_external_id); CREATE INDEX IF NOT EXISTS idx_time_entry_link_role ON "time".time_entry_source_link(linked_role); CREATE INDEX IF NOT EXISTS idx_time_sync_run_source_system ON "time".time_sync_run(source_system); CREATE INDEX IF NOT EXISTS idx_time_sync_run_scope_key ON "time".time_sync_run(scope_key); CREATE INDEX IF NOT EXISTS idx_time_sync_run_status ON "time".time_sync_run(status); CREATE INDEX IF NOT EXISTS idx_time_sync_run_started_at ON "time".time_sync_run(started_at DESC); CREATE INDEX IF NOT EXISTS idx_time_sync_state_source_system ON "time".time_sync_state(source_system); CREATE INDEX IF NOT EXISTS idx_time_sync_state_scope_key ON "time".time_sync_state(scope_key);