File size: 19,147 Bytes
a47c573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
-- 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
        )
    );