nyay-setu-backend / data /document_intelligence_schema.sql
ayush712145's picture
Deploy NyaySetu backend document intelligence
a47c573 verified
Raw
History Blame Contribute Delete
19.1 kB
-- 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
)
);