CREATE TABLE IF NOT EXISTS source_connections ( id VARCHAR(36) PRIMARY KEY, source_type VARCHAR(20) NOT NULL CHECK (source_type IN ('pp', 'alist', 'dingtalk')), name VARCHAR(255) NOT NULL, base_url TEXT NOT NULL DEFAULT '', secret_ref VARCHAR(255) NOT NULL DEFAULT '', enabled BOOLEAN NOT NULL DEFAULT FALSE, schedule JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(schedule) = 'object'), last_health JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(last_health) = 'object'), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (name) ); CREATE TABLE IF NOT EXISTS source_items ( id VARCHAR(36) PRIMARY KEY, source_connection_id VARCHAR(36) NOT NULL REFERENCES source_connections(id) ON DELETE RESTRICT, stable_external_id TEXT NOT NULL, canonical_uri_hash CHAR(64) NOT NULL CHECK (canonical_uri_hash ~ '^[0-9a-f]{64}$'), parent_external_id TEXT NOT NULL DEFAULT '', normalized_path TEXT NOT NULL DEFAULT '', title TEXT NOT NULL, item_type VARCHAR(20) NOT NULL CHECK (item_type IN ('document', 'file', 'link', 'video')), size_bytes BIGINT CHECK (size_bytes IS NULL OR size_bytes >= 0), mime TEXT NOT NULL DEFAULT '', revision TEXT NOT NULL DEFAULT '', updated_at_source TIMESTAMPTZ, visibility VARCHAR(20) NOT NULL DEFAULT 'internal' CHECK (visibility IN ('internal', 'external')), visibility_origin VARCHAR(30) NOT NULL DEFAULT 'manual' CHECK (visibility_origin IN ('pp_public_tag', 'configured_scope', 'manual')), last_seen_scan_id VARCHAR(36) NOT NULL DEFAULT '', source_change VARCHAR(20) NOT NULL DEFAULT 'unchanged' CHECK (source_change IN ('unchanged', 'added', 'updated', 'deleted', 'unavailable')), absence_streak INTEGER NOT NULL DEFAULT 0 CHECK (absence_streak >= 0), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (source_connection_id, stable_external_id) ); CREATE TABLE IF NOT EXISTS content_objects ( id VARCHAR(36) PRIMARY KEY, sha256 CHAR(64) NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'), size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0), mime TEXT NOT NULL DEFAULT '', validated_format VARCHAR(50) NOT NULL DEFAULT '', local_spool_ref TEXT NOT NULL DEFAULT '', content_state VARCHAR(20) NOT NULL DEFAULT 'unknown' CHECK (content_state IN ('unknown', 'downloading', 'verified', 'invalid', 'over_limit', 'failed')), first_discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (sha256) ); CREATE TABLE IF NOT EXISTS source_item_versions ( id VARCHAR(36) PRIMARY KEY, source_item_id VARCHAR(36) NOT NULL REFERENCES source_items(id) ON DELETE RESTRICT, revision TEXT NOT NULL, content_object_id VARCHAR(36) REFERENCES content_objects(id) ON DELETE RESTRICT, content_state VARCHAR(20) NOT NULL DEFAULT 'unknown' CHECK (content_state IN ('unknown', 'downloading', 'verified', 'invalid', 'over_limit', 'failed')), download_error TEXT NOT NULL DEFAULT '', raw_record_summary JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(raw_record_summary) = 'object'), source_updated_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (source_item_id, revision) ); CREATE TABLE IF NOT EXISTS test_publications ( id VARCHAR(36) PRIMARY KEY, source_item_version_id VARCHAR(36) NOT NULL REFERENCES source_item_versions(id) ON DELETE RESTRICT, test_kb_id VARCHAR(255) NOT NULL, knowledge_id VARCHAR(255) NOT NULL DEFAULT '', ingest_state VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (ingest_state IN ('pending', 'uploading', 'uploaded', 'failed')), processing_state VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (processing_state IN ('pending', 'processing', 'finalizing', 'completed', 'failed', 'stalled')), retrieval_state VARCHAR(20) NOT NULL DEFAULT 'unpublished' CHECK (retrieval_state IN ('unpublished', 'enabled', 'disabling', 'disabled', 'enabling', 'failed')), parser_generation VARCHAR(255) NOT NULL DEFAULT '', index_generation VARCHAR(255) NOT NULL DEFAULT '', last_error TEXT NOT NULL DEFAULT '', retry_count INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (source_item_version_id, test_kb_id) ); CREATE INDEX IF NOT EXISTS idx_source_items_change ON source_items (source_connection_id, source_change, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_source_items_visibility ON source_items (visibility, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_source_item_versions_content ON source_item_versions (content_object_id) WHERE content_object_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_test_publications_state ON test_publications (processing_state, retrieval_state, updated_at DESC);