-- NyaySetu Track C document-intelligence schema. -- Apply after backend/app/db/models/schema.sql. Tenant and case scopes are -- retained for every source, job, evidence item, and report. CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS source_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id TEXT NOT NULL DEFAULT 'default', case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, original_name TEXT NOT NULL, created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (tenant_id, case_id) REFERENCES cases(tenant_id, id) ON DELETE CASCADE, FOREIGN KEY (tenant_id, created_by) REFERENCES users(tenant_id, id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS document_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), document_id UUID NOT NULL REFERENCES source_documents(id) ON DELETE CASCADE, sha256 CHAR(64) NOT NULL CHECK (sha256 ~ '^[a-f0-9]{64}$'), media_type TEXT NOT NULL, document_format TEXT NOT NULL, size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0), object_uri TEXT NOT NULL, original_object_key TEXT NOT NULL, ir_object_key TEXT, parse_status TEXT NOT NULL CHECK ( parse_status IN ( 'pending', 'ready', 'partial', 'ocr_required', 'password_protected', 'corrupted', 'limit_exceeded', 'unsupported', 'failed' ) ), parser_name TEXT NOT NULL, parser_version TEXT NOT NULL, language_hint TEXT, warnings JSONB NOT NULL DEFAULT '[]'::jsonb, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (document_id, sha256) ); CREATE TABLE IF NOT EXISTS document_pages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), version_id UUID NOT NULL REFERENCES document_versions(id) ON DELETE CASCADE, page_number INTEGER NOT NULL CHECK (page_number > 0), width DOUBLE PRECISION, height DOUBLE PRECISION, rotation INTEGER NOT NULL DEFAULT 0, confidence DOUBLE PRECISION NOT NULL DEFAULT 0 CHECK ( confidence BETWEEN 0 AND 1 ), extraction_method TEXT NOT NULL DEFAULT 'none' CHECK ( extraction_method IN ('digital', 'ocr', 'mixed', 'none') ), warnings JSONB NOT NULL DEFAULT '[]'::jsonb, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, UNIQUE (version_id, page_number) ); CREATE TABLE IF NOT EXISTS document_blocks ( id TEXT PRIMARY KEY, page_id UUID NOT NULL REFERENCES document_pages(id) ON DELETE CASCADE, sequence INTEGER NOT NULL CHECK (sequence >= 0), block_kind TEXT NOT NULL, text_content TEXT NOT NULL, confidence DOUBLE PRECISION NOT NULL CHECK (confidence BETWEEN 0 AND 1), bbox JSONB, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, search_vector TSVECTOR GENERATED ALWAYS AS ( to_tsvector('simple', coalesce(text_content, '')) ) STORED, UNIQUE (page_id, sequence) ); CREATE TABLE IF NOT EXISTS document_tables ( id TEXT PRIMARY KEY, page_id UUID NOT NULL REFERENCES document_pages(id) ON DELETE CASCADE, block_id TEXT NOT NULL UNIQUE REFERENCES document_blocks(id) ON DELETE CASCADE, bbox JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS document_table_rows ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), table_id TEXT NOT NULL REFERENCES document_tables(id) ON DELETE CASCADE, row_index INTEGER NOT NULL CHECK (row_index >= 0), UNIQUE (table_id, row_index) ); CREATE TABLE IF NOT EXISTS document_table_cells ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), row_id UUID NOT NULL REFERENCES document_table_rows(id) ON DELETE CASCADE, column_index INTEGER NOT NULL CHECK (column_index >= 0), text_content TEXT NOT NULL DEFAULT '', row_span INTEGER NOT NULL DEFAULT 1 CHECK (row_span > 0), column_span INTEGER NOT NULL DEFAULT 1 CHECK (column_span > 0), bbox JSONB, UNIQUE (row_id, column_index) ); CREATE TABLE IF NOT EXISTS analysis_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, tenant_id VARCHAR(128) NOT NULL DEFAULT 'default', requested_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK ( status IN ( 'queued', 'extracting', 'researching', 'synthesizing', 'verifying', 'completed', 'needs_review', 'failed', 'cancelled' ) ), workflow_version TEXT NOT NULL, idempotency_key CHAR(64) NOT NULL UNIQUE CHECK ( idempotency_key ~ '^[a-f0-9]{64}$' ), document_version_ids JSONB NOT NULL CHECK ( jsonb_typeof(document_version_ids) = 'array' ), document_hashes JSONB NOT NULL CHECK ( jsonb_typeof(document_hashes) = 'array' ), enable_external_research BOOLEAN NOT NULL DEFAULT FALSE, attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0), max_attempts INTEGER NOT NULL DEFAULT 3 CHECK (max_attempts > 0), available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), leased_until TIMESTAMPTZ, worker_id TEXT, cancel_requested_at TIMESTAMPTZ, run_id TEXT, report_id TEXT, error JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (tenant_id, case_id) REFERENCES cases(tenant_id, id) ON DELETE CASCADE, FOREIGN KEY (tenant_id, requested_by_user_id) REFERENCES users(tenant_id, id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS analysis_job_events ( id BIGSERIAL PRIMARY KEY, job_id UUID NOT NULL REFERENCES analysis_jobs(id) ON DELETE CASCADE, sequence INTEGER NOT NULL CHECK (sequence >= 0), event_type TEXT NOT NULL, status TEXT NOT NULL CHECK ( status IN ( 'queued', 'extracting', 'researching', 'synthesizing', 'verifying', 'completed', 'needs_review', 'failed', 'cancelled' ) ), agent TEXT, message TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (job_id, sequence) ); CREATE TABLE IF NOT EXISTS analysis_artifacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), job_id UUID NOT NULL REFERENCES analysis_jobs(id) ON DELETE CASCADE, case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, artifact_type TEXT NOT NULL CHECK ( artifact_type IN ('run', 'report', 'evidence', 'integrity', 'research') ), artifact_id TEXT NOT NULL, reference_uri TEXT NOT NULL, sha256 CHAR(64) NOT NULL CHECK (sha256 ~ '^[a-f0-9]{64}$'), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (job_id, artifact_type, artifact_id) ); CREATE TABLE IF NOT EXISTS evidence_atoms ( id TEXT PRIMARY KEY, case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, evidence_kind TEXT NOT NULL, label TEXT NOT NULL, value TEXT NOT NULL, normalized_value TEXT NOT NULL, confidence DOUBLE PRECISION NOT NULL CHECK (confidence BETWEEN 0 AND 1), review_state TEXT NOT NULL CHECK ( review_state IN ('verified', 'needs_review', 'rejected') ), source_spans JSONB NOT NULL CHECK (jsonb_array_length(source_spans) > 0), extractor_name TEXT NOT NULL, extractor_version TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS document_chunks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, version_id UUID NOT NULL REFERENCES document_versions(id) ON DELETE CASCADE, first_block_id TEXT NOT NULL REFERENCES document_blocks(id) ON DELETE CASCADE, last_block_id TEXT NOT NULL REFERENCES document_blocks(id) ON DELETE CASCADE, chunk_text TEXT NOT NULL, token_count INTEGER NOT NULL CHECK (token_count > 0), embedding_model TEXT, embedding vector(768), search_vector TSVECTOR GENERATED ALWAYS AS ( to_tsvector('simple', coalesce(chunk_text, '')) ) STORED, UNIQUE (version_id, first_block_id, last_block_id) ); CREATE TABLE IF NOT EXISTS evidence_relationships ( id TEXT PRIMARY KEY, case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, source_evidence_id TEXT NOT NULL REFERENCES evidence_atoms(id) ON DELETE CASCADE, target_evidence_id TEXT NOT NULL REFERENCES evidence_atoms(id) ON DELETE CASCADE, relationship_type TEXT NOT NULL, description TEXT NOT NULL, confidence DOUBLE PRECISION NOT NULL CHECK (confidence BETWEEN 0 AND 1), supporting_evidence_ids JSONB NOT NULL, review_state TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS timeline_events ( id TEXT PRIMARY KEY, case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, date_text TEXT NOT NULL, normalized_date DATE, description TEXT NOT NULL, evidence_ids JSONB NOT NULL CHECK (jsonb_array_length(evidence_ids) > 0), confidence DOUBLE PRECISION NOT NULL CHECK (confidence BETWEEN 0 AND 1), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS document_analysis_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, status TEXT NOT NULL, workflow_version TEXT NOT NULL, document_version_ids JSONB NOT NULL, error JSONB, started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS document_agent_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), run_id UUID NOT NULL REFERENCES document_analysis_runs(id) ON DELETE CASCADE, sequence INTEGER NOT NULL, agent_name TEXT NOT NULL, status TEXT NOT NULL, round_number INTEGER NOT NULL CHECK (round_number BETWEEN 1 AND 2), summary TEXT NOT NULL, input_ids JSONB NOT NULL DEFAULT '[]'::jsonb, output_ids JSONB NOT NULL DEFAULT '[]'::jsonb, started_at TIMESTAMPTZ NOT NULL, completed_at TIMESTAMPTZ, UNIQUE (run_id, sequence) ); CREATE TABLE IF NOT EXISTS legal_analysis_reports ( id TEXT PRIMARY KEY, case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, run_id UUID NOT NULL REFERENCES document_analysis_runs(id) ON DELETE CASCADE, version INTEGER NOT NULL CHECK (version > 0), title TEXT NOT NULL, status TEXT NOT NULL, workflow_version TEXT NOT NULL, source_document_versions JSONB NOT NULL, caveats JSONB NOT NULL DEFAULT '[]'::jsonb, report_json JSONB NOT NULL, html_object_uri TEXT, pdf_object_uri TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (case_id, version) ); CREATE TABLE IF NOT EXISTS report_claims ( id TEXT PRIMARY KEY, report_id TEXT NOT NULL REFERENCES legal_analysis_reports(id) ON DELETE CASCADE, section_id TEXT NOT NULL, claim_kind TEXT NOT NULL CHECK ( claim_kind IN ('fact', 'law', 'inference', 'recommendation') ), statement TEXT NOT NULL, evidence_ids JSONB NOT NULL, confidence DOUBLE PRECISION NOT NULL CHECK (confidence BETWEEN 0 AND 1), caveat TEXT ); CREATE TABLE IF NOT EXISTS report_citations ( id TEXT PRIMARY KEY, report_id TEXT NOT NULL REFERENCES legal_analysis_reports(id) ON DELETE CASCADE, claim_id TEXT REFERENCES report_claims(id) ON DELETE CASCADE, evidence_id TEXT NOT NULL REFERENCES evidence_atoms(id) ON DELETE RESTRICT, source_span JSONB NOT NULL, display_label TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS review_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), case_id UUID NOT NULL REFERENCES cases(id) ON DELETE CASCADE, source_type TEXT NOT NULL, source_id TEXT NOT NULL, severity TEXT NOT NULL CHECK (severity IN ('warning', 'blocking')), reason TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open' CHECK ( status IN ('open', 'resolved', 'dismissed') ), assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, resolution_note TEXT, resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS document_audit_events ( id BIGSERIAL PRIMARY KEY, tenant_id VARCHAR(128) NOT NULL, actor_user_id UUID NOT NULL, case_id UUID REFERENCES cases(id) ON DELETE SET NULL, event_type VARCHAR(80) NOT NULL, outcome VARCHAR(16) NOT NULL CHECK ( outcome IN ('allowed', 'denied', 'failed') ), resource_type VARCHAR(80), resource_id TEXT, request_id VARCHAR(128), client_ip TEXT, user_agent VARCHAR(500), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (tenant_id, actor_user_id) REFERENCES users(tenant_id, id) ON DELETE RESTRICT ); CREATE TABLE IF NOT EXISTS document_rate_limit_windows ( tenant_id VARCHAR(128) NOT NULL, user_id UUID NOT NULL, bucket VARCHAR(80) NOT NULL, window_started_at TIMESTAMPTZ NOT NULL, request_count INTEGER NOT NULL DEFAULT 0 CHECK (request_count >= 0), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (tenant_id, user_id, bucket, window_started_at), FOREIGN KEY (tenant_id, user_id) REFERENCES users(tenant_id, id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS document_quota_usage ( tenant_id VARCHAR(128) NOT NULL, user_id UUID NOT NULL, period_started_at TIMESTAMPTZ NOT NULL, bytes_used BIGINT NOT NULL DEFAULT 0 CHECK (bytes_used >= 0), file_count INTEGER NOT NULL DEFAULT 0 CHECK (file_count >= 0), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (tenant_id, user_id, period_started_at), FOREIGN KEY (tenant_id, user_id) REFERENCES users(tenant_id, id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS document_upload_reservations ( id UUID PRIMARY KEY, tenant_id VARCHAR(128) NOT NULL, user_id UUID NOT NULL, case_id UUID NOT NULL, period_started_at TIMESTAMPTZ NOT NULL, size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0), status VARCHAR(16) NOT NULL CHECK ( status IN ('reserved', 'committed', 'released') ), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (tenant_id, user_id, period_started_at) REFERENCES document_quota_usage(tenant_id, user_id, period_started_at) ON DELETE CASCADE, FOREIGN KEY (tenant_id, case_id) REFERENCES cases(tenant_id, id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS outbox_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), aggregate_type TEXT NOT NULL, aggregate_id TEXT NOT NULL, event_type TEXT NOT NULL, payload JSONB NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), published_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_source_documents_tenant_case ON source_documents(tenant_id, case_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_document_versions_sha ON document_versions(sha256); CREATE INDEX IF NOT EXISTS idx_document_blocks_search ON document_blocks USING GIN(search_vector); CREATE INDEX IF NOT EXISTS idx_document_tables_page ON document_tables(page_id); CREATE INDEX IF NOT EXISTS idx_document_table_rows_table ON document_table_rows(table_id, row_index); CREATE INDEX IF NOT EXISTS idx_document_chunks_case ON document_chunks(case_id, version_id); CREATE INDEX IF NOT EXISTS idx_document_chunks_search ON document_chunks USING GIN(search_vector); CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding ON document_chunks USING hnsw (embedding vector_cosine_ops); CREATE INDEX IF NOT EXISTS idx_analysis_jobs_claim ON analysis_jobs(status, available_at, leased_until); CREATE INDEX IF NOT EXISTS idx_analysis_job_events_stream ON analysis_job_events(job_id, sequence); CREATE INDEX IF NOT EXISTS idx_analysis_artifacts_case ON analysis_artifacts(case_id, artifact_type, created_at DESC); CREATE INDEX IF NOT EXISTS idx_evidence_case_kind ON evidence_atoms(case_id, evidence_kind, review_state); CREATE INDEX IF NOT EXISTS idx_relationships_case ON evidence_relationships(case_id, relationship_type); CREATE INDEX IF NOT EXISTS idx_timeline_case_date ON timeline_events(case_id, normalized_date); CREATE INDEX IF NOT EXISTS idx_review_queue ON review_items(case_id, status, severity, created_at); CREATE INDEX IF NOT EXISTS idx_document_audit_case_time ON document_audit_events(tenant_id, case_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_document_audit_actor_time ON document_audit_events(actor_user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_document_rate_limit_cleanup ON document_rate_limit_windows(window_started_at); CREATE INDEX IF NOT EXISTS idx_document_upload_reservations_case ON document_upload_reservations(tenant_id, case_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_outbox_pending ON outbox_events(available_at) WHERE published_at IS NULL; -- Service connections are expected to bypass RLS. User-scoped connections must -- set app.current_user_id and can only see cases owned by that user. ALTER TABLE source_documents ENABLE ROW LEVEL SECURITY; ALTER TABLE analysis_jobs ENABLE ROW LEVEL SECURITY; ALTER TABLE analysis_job_events ENABLE ROW LEVEL SECURITY; ALTER TABLE analysis_artifacts ENABLE ROW LEVEL SECURITY; ALTER TABLE document_versions ENABLE ROW LEVEL SECURITY; ALTER TABLE evidence_atoms ENABLE ROW LEVEL SECURITY; ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY; ALTER TABLE evidence_relationships ENABLE ROW LEVEL SECURITY; ALTER TABLE timeline_events ENABLE ROW LEVEL SECURITY; ALTER TABLE document_analysis_runs ENABLE ROW LEVEL SECURITY; ALTER TABLE legal_analysis_reports ENABLE ROW LEVEL SECURITY; ALTER TABLE review_items ENABLE ROW LEVEL SECURITY; ALTER TABLE document_audit_events ENABLE ROW LEVEL SECURITY; ALTER TABLE document_rate_limit_windows ENABLE ROW LEVEL SECURITY; ALTER TABLE document_quota_usage ENABLE ROW LEVEL SECURITY; ALTER TABLE document_upload_reservations ENABLE ROW LEVEL SECURITY; CREATE POLICY source_documents_case_owner ON source_documents USING ( EXISTS ( SELECT 1 FROM cases WHERE cases.id = source_documents.case_id AND cases.tenant_id = source_documents.tenant_id AND cases.tenant_id = nullif(current_setting('app.current_tenant_id', true), '') AND cases.user_id = nullif(current_setting('app.current_user_id', true), '')::uuid ) );