diff --git a/.prettierignore b/.prettierignore index aefbc2e0b..2c40481a2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -16,3 +16,4 @@ scratch/ # Generated by `supabase gen types`; keep the generator's formatting so # regeneration stays churn-free. src/lib/supabase/database.types.ts +supabase/drift-manifest.json diff --git a/docs/database-drift-detection.md b/docs/database-drift-detection.md new file mode 100644 index 000000000..956cd5567 --- /dev/null +++ b/docs/database-drift-detection.md @@ -0,0 +1,134 @@ +# Database drift detection (`npm run check:drift`) + +Last updated: 2026-07-07 + +This repo's worst operational incidents were live-vs-repo schema drift: hybrid +retrieval RPCs silently broken on live for an unknown period, and migrations +recorded as applied whose objects were absent. `search_schema_health()` guards +a curated subset (signatures, 22 required indexes, execution smoke). +`check:drift` generalizes that into a full-inventory comparison of **every** +application-owned object against `supabase/schema.sql`. + +## How it works + +Three committed artifacts: + +| Artifact | Role | +| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `supabase/migrations/20260706200000_schema_drift_snapshot.sql` | `public.schema_drift_snapshot()` — service-role-only RPC returning the normalized live inventory (also declared in `supabase/schema.sql`; a test enforces byte parity). | +| `supabase/drift-manifest.json` | The expected state: the same snapshot captured from a **from-scratch replay of `supabase/schema.sql`** into a disposable `supabase/postgres` Docker container (`npm run drift:manifest`). Embeds the sha256 of the schema.sql it came from. | +| `supabase/drift-allowlist.json` | Known, documented divergence (each entry has a `reason`). Reported as warnings; anything not listed fails the check. | + +`npm run check:drift` (needs live service-role env) verifies the project ref, +fails fast if the manifest is stale, calls the RPC, diffs, applies the +allowlist, and exits 1 on unallowlisted divergence. The offline half runs in +`tests/drift-detection.test.ts` under `verify:cheap`: manifest freshness +(sha256), migration↔schema.sql parity for the snapshot function, allowlist +hygiene, and unit tests of the comparison engine. + +Inventory coverage: functions (comment/whitespace-stripped `pg_get_functiondef` +md5 + sorted ACLs), indexes (normalized `pg_get_indexdef`), RLS policies +(permissive/roles/cmd/qual/with_check), table shapes (columns sorted by name, +RLS flags, reloptions, ACLs), constraints, triggers, views, extensions, and +storage bucket rows + storage.objects policies. + +### Noise sources handled by design + +- **Whitespace/comments in function bodies** — `prosrc` is stored verbatim, so + migration text vs schema.sql text differ trivially; both are stripped before + hashing (the same trick `20260701140631` used to validate byte-equivalence). +- **Rendering search_path** — the snapshot pins `search_path = ''` so + `pg_get_expr`/`pg_get_indexdef`/policy quals render fully qualified and + identically on live and replay. +- **Column ordinal drift** — live tables grew via `ALTER TABLE ADD COLUMN`; + columns compare sorted by name, not `attnum`. +- **ACL append order** — aclitem arrays are sorted. +- **Duplicate migration-history versions** — history is _not_ compared at all; + the check compares actual object state (history presence proved unreliable: + see `20260703030000` below). +- **Platform-provisioned extensions** (pg_net, pgsodium, pgmq, …) — extra live + extensions are informational; missing schema.sql-declared ones fail. +- **Legacy index names** — `alias` allowlist entries assert the live database + carries the _identical_ name-stripped index definition under a legacy name + (the machine-checked version of `search_schema_health()`'s `index_aliases`). + +### Workflow + +- Change `supabase/schema.sql` → run `npm run drift:manifest` (Docker) in the + same PR. The freshness test fails otherwise. This also continuously proves + schema.sql replays from scratch — which it did **not** before 2026-07-07 + (`document_index_units` was declared after its first validating reference). +- Live drifts (check:drift red) → either codify live state (migration + + schema.sql + manifest regen) or fix live **through an approved migration**. + Never raw SQL against live; that is how this incident class started. +- New known-divergence → allowlist entry with a reason and a backlog line here. +- After the pending-migration backlog lands, delete the matching allowlist + entries; check:drift reports stale entries so they cannot silently linger. + +## 2026-07-07 baseline audit (three-way: live vs schema.sql vs migration chain) + +Both repo lineages were replayed into scratch containers and compared with the +live inventory. 166 divergent keys, fully classified: + +- **Reconciled in this PR (schema.sql/migrations only, live untouched):** + replay-order fix; `20260707000000_codify_live_observed_drift.sql` codifying + 15 live-only columns (`document_images` ×7, `document_index_quality` ×6, + `ingestion_job_stages` ×2 — worker-written, branch DBs broke without them), + 3 `content_not_blank` NOT VALID checks, autovacuum reloptions on 5 RAG + tables, `content_hash` nullability alignment, 4 live-only functions + (`set_owner_id_from_auth_uid` + rag_queries/misses triggers, + `purge_expired_rag_queries`, `correct_clinical_query_terms`, + `invoke_ingestion_worker`) and 2 ACL tightenings; schema.sql function/policy + text realigned to the migration-chain truth for `analyze_rag_tables`, + `claim_indexing_v3_agent_jobs`, `is_committed_artifact_generation(uuid,jsonb)`, + `match_document_memory_cards_hybrid`, and 6 owner-read policies (operand + order only). +- **Allowlisted (124 entries)** — see `supabase/drift-allowlist.json`; backlog + below. + +## Reconciliation backlog + +Ordered; each item removes allowlist entries when it lands. Items touching the +live project need explicit operator approval. + +1. **Apply the pending migrations** (`supabase db push` after review): + `20260705210000` (owner-sentinel rewrite — 8 live function bodies stale), + `20260706010000` (search_schema_health M13 guard), `20260706130000` + (embedding-fields-text sentinel), `20260706200000` (drift snapshot RPC — + required before check:drift can run at all), `20260707000000` (codification + wave; no-op on live by construction). +2. **`20260703030000` is recorded as applied but its effects are absent on + live** (storage_cleanup_jobs still has the legacy index names and the + non-partial status index). Recorded-but-ineffective is the exact original + incident class, recurring. Re-apply its idempotent statements under a new + version with approval; do not trust history presence. +3. **Codify the remaining live-only functions**: `get_visual_evidence_cards`, + `repair_enrichment_quality_batch`, `run_all_visual_eval_cases`, + `run_visual_eval_case` (same pattern as `20260707000000`). +4. **Authenticated-grant posture decision**: live revoked the authenticated + Data API grants on 17 tables (fail-closed hardening; the owner-read RLS + policies are currently dead on live) while schema.sql still declares them. + Either codify the revokes (schema.sql + tests + migration) or restore the + grants live. +5. **PUBLIC-execute revokes**: 4 security-invoker functions retain default + PUBLIC execute on live (`detect_legacy_ivfflat_indexes`, + `document_summary_text`, `search_document_chunks`, + `set_document_embedding_field_content_hash`). +6. **`document_label_metadata` direction**: schema.sql is AHEAD (hidden-label + filtering added without a migration). Ship the migration or revert. +7. **Index estate**: rename 10 legacy-named live indexes to schema.sql names; + decide the 24 schema.sql-declared indexes absent on live (recreate vs + remove — includes `documents_search_idx`, `document_chunks_anchor_idx`, + `documents_owner_content_hash_unique_idx`); drop ~45 live-only duplicate + indexes after `pg_stat_user_indexes` scan verification; reshape 3 + (`import_batches_status_created_idx`, `ingestion_jobs_document_status_idx`, + `ingestion_jobs_status_next_run_idx`). +8. **Constraints**: add `ingestion_job_stages_job_id_fkey` to live; align the + `rag_visual_eval_*` document FK definitions. +9. **`invoke_ingestion_worker`** hardcodes the project URL — migrate to the + GUC pattern (`20260702160000` precedent). +10. **Migration-chain fidelity** (affects Supabase Preview/branches, not + live): 13 keys where the chain diverges from schema.sql — buckets are only + created by schema.sql, `documents`/`ingestion_jobs` updated_at trigger + variants, post-legacy-drop embedding-fields index set, + `document_chunks_content_trgm_idx` shape, `rag_visual_eval_*` shapes. diff --git a/docs/disaster-recovery-runbook.md b/docs/disaster-recovery-runbook.md new file mode 100644 index 000000000..05943ef56 --- /dev/null +++ b/docs/disaster-recovery-runbook.md @@ -0,0 +1,126 @@ +# Disaster recovery runbook — Clinical KB Database + +Last rehearsed: 2026-07-07 (schema restore rehearsal against a local Supabase +Postgres container; the live project `sjrfecxgysukkwxsowpy` was read-only +throughout). + +## What a recovery is made of + +A full recovery of this system has four independent layers. **Only the first +is covered by the repo**; the rest are Supabase-platform or operator state: + +| Layer | Source of truth | RPO | RTO (measured/estimated) | +| --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Schema (tables, RPCs, indexes, RLS, buckets config) | `supabase/schema.sql` in git | 0 (git) | **~19 s** into a local container (measured, repeatedly); minutes on a hosted project | +| Data (documents, 69k+ chunks, embeddings, jobs, logs) | Supabase managed backups | Daily backups → up to 24 h; with PITR enabled → ~2 min granularity. **Check the dashboard (Database → Backups) for the actual plan setting.** | Platform restore; not directly measurable without executing one. For the ~8.6 GB database expect tens of minutes to hours. Practice target: restore to a **new** project, never in place. | +| Storage objects (`clinical-documents`, `clinical-images` files) | Supabase Storage (S3) — separate from DB backups | Platform-managed | Bucket **rows** are recreated by schema.sql; the **files** are not in a DB restore. A DB-only restore leaves `documents.storage_path` pointing at objects that must still exist in Storage. | +| Config & secrets | Nowhere in the repo (deliberate) | n/a | Manual re-entry, see the checklist below | + +## Schema restore procedure (rehearsed) + +Works on any machine with Docker; identical mechanics on a fresh hosted +project via psql. This is exactly what `npm run drift:manifest` automates +(container start → scaffold → replay → snapshot → destroy), so **schema +restorability is re-proven every time the drift manifest is regenerated**. + +```sh +# 1. Scratch Supabase Postgres matching live (17.6.1.127) +docker run -d --name kb-restore -e POSTGRES_PASSWORD=postgres -p 56543:5432 supabase/postgres:17.6.1.127 +docker exec kb-restore pg_isready -U postgres # wait until ready + +# 2. Storage scaffold (bare image ships an empty storage schema; the hosted +# platform provisions the real one). Run as supabase_admin. +docker cp scripts/sql/drift-replay-scaffold.sql kb-restore:/tmp/scaffold.sql +docker exec kb-restore psql -U supabase_admin -d postgres -v ON_ERROR_STOP=1 -f /tmp/scaffold.sql + +# 3. Replay the canonical schema as postgres (matches how live is administered) +docker cp supabase/schema.sql kb-restore:/tmp/schema.sql +docker exec kb-restore psql -U postgres -d postgres -v ON_ERROR_STOP=1 -f /tmp/schema.sql + +# 4. Verify +docker exec kb-restore psql -U postgres -d postgres -tAc "select (public.search_schema_health())->>'ok'" # must print: true +``` + +On a hosted target, prefer the migration chain (`supabase db push` / +`supabase db reset --linked` semantics); measured chain replay: **48 s** for +102 migrations into the same container. Note the chain has known fidelity gaps +vs schema.sql (buckets, two triggers, a handful of indexes — item 10 in +[database-drift-detection.md](database-drift-detection.md#reconciliation-backlog)). + +## Retrieval verification on the restored copy (rehearsed 2026-07-07) + +1. `search_schema_health()` → `ok: true, missing: []` on the restored copy. +2. `schema_drift_snapshot()` → full inventory captured; this became + `supabase/drift-manifest.json`. +3. Seeded a synthetic document + chunk + embedding field + index unit + memory + card (1536-dim unit vectors) and invoked the full retrieval surface: + `match_document_chunks_hybrid`, `match_document_embedding_fields_hybrid`, + `match_document_index_units_hybrid`, `match_document_memory_cards_hybrid`, + `match_document_chunks_text`, `match_documents_for_query`. All returned the + seeded rows (similarity 1.000 on the identity vector, hybrid score 0.760) — + the lexical, vector, and hybrid scoring paths all execute correctly on a + from-scratch restore. +4. **Golden retrieval eval (`npm run eval:retrieval:quality`) cannot run + against a schema-only restore** — it needs the real ~2,065-document corpus + plus `OPENAI_API_KEY` and service-role env. After a _data_ restore, point + `.env.local` at the restored project (update `NEXT_PUBLIC_SUPABASE_URL`, + `SUPABASE_PROJECT_REF`, keys), run `npm run check:supabase-project`, then + `npm run eval:retrieval:quality` and require the 23/23 pass before serving + traffic. This is the acceptance test for a real recovery. + +## What did NOT survive the schema restore (verified) + +Everything below must come from a data restore or manual re-entry — plan for +it before you need it: + +- **All data** — documents, chunks, embeddings (re-embedding the corpus from + scratch costs real OpenAI spend and hours of worker time; the backup is the + only cheap path), jobs, caches, logs, registry records. +- **Storage object files** — schema.sql recreates the two private bucket rows + (`clinical-documents`, `clinical-images`) but no files. +- **Auth users** — `auth.users` is platform state; owner-scoped rows restored + from backup reference user ids that must exist again (same project restore + preserves them; a new project does not). +- **12 platform-provisioned extensions** (pg_net, pgsodium, pgmq, pg_cron, + pg_graphql, vault, …) — present on hosted projects, absent in the bare + image; schema.sql only declares vector/pg_trgm/uuid-ossp. +- **pg_cron schedules** — the invoked functions (`invoke_ingestion_worker`, + `invoke_indexing_v3_agent`) are codified, but the `cron.schedule(...)` rows + themselves are live-only. After restore, re-create the cron jobs. +- **Vault secrets** — `cron_ingestion_jwt` (and any siblings) must be re-added + before the cron→edge-function chain works. +- **Custom GUCs** — `20260702160000` reads the agent URL from a database GUC; + re-set it (`alter database ... set ...`) on the restored project. +- **Edge functions** — deploy `indexing-v3-agent` (and the ingestion worker + function) separately via the CLI. +- **Dashboard config** — auth providers (magic link, Google/Microsoft SSO + redirect URLs), connection-pool caps (the documented 10-connection auth cap + is dashboard-only), API keys (publishable + service role are per-project; + every consumer needs the new values), `E2E_USER_*` test accounts. +- **Role settings** — e.g. `alter role authenticator set +idle_in_transaction_session_timeout` (in the migration chain, so a chain + replay restores it; a schema.sql-only replay does not). + +## Rehearsal findings that changed the repo + +- `supabase/schema.sql` did **not** replay from scratch before 2026-07-07 + (`document_index_units` was created ~900 lines after its first validating + reference — the strict-gate view). Fixed by reordering; now continuously + re-proven by `npm run drift:manifest` + the manifest freshness test. +- The bare `supabase/postgres` image lacks the storage schema objects; the + committed scaffold `scripts/sql/drift-replay-scaffold.sql` fills the gap. +- The first migration (`20260527000000`) dies at its storage-policy section + without that scaffold — the chain is **not** self-sufficient on a bare + Postgres either. +- Worker-written columns existed only on live (see + `20260707000000_codify_live_observed_drift.sql`): a branch/preview database + restored from the repo would have broken ingestion writes. Codified. + +## Standing cadence + +- Every `drift:manifest` regeneration = a schema-restore rehearsal (free). +- Run `npm run check:drift` after any live apply and on the operational + cadence alongside `check:indexing`. +- Re-rehearse the **data** layer (platform restore to a scratch project + + golden eval) before any risky bulk operation (re-index, mass migration), and + record the measured restore time here. diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 053ed64a4..29da82d10 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -105,6 +105,13 @@ All approved render-surface modules are extracted. `ClinicalDashboard.tsx` went - **Edge function follow-up:** deploy `indexing-v3-agent` after merge so JSONB status RPC parsing is live. - **Operator-only:** publishable key rotation (`docs/archive/operator-decisions-2026-07-04.md`). +## Full-inventory drift detection & DR rehearsal (2026-07-07) + +- `npm run check:drift` generalizes the `search_schema_health()` single-hash approach into a full-inventory comparison of every live function (normalized `pg_get_functiondef` hash + ACLs), index, RLS policy, table shape, constraint, trigger, view, and storage bucket against `supabase/schema.sql`. Expected state is `supabase/drift-manifest.json` (generated by `npm run drift:manifest`, which replays schema.sql from scratch into a Docker container — so schema.sql replayability is re-proven on every regeneration); known divergence lives in `supabase/drift-allowlist.json` with per-entry reasons. Offline halves (manifest freshness, migration↔schema.sql snapshot parity, allowlist hygiene, engine unit tests) run in `verify:cheap` via `tests/drift-detection.test.ts`. See [docs/database-drift-detection.md](database-drift-detection.md). +- The 2026-07-07 three-way audit (live vs schema.sql replay vs migration-chain replay) found 166 divergent keys: worker-written columns existing only on live (codified by `20260707000000`), schema.sql not replayable from scratch (fixed), pending migrations explaining 11 function-body drifts, `20260703030000` **recorded as applied but ineffective on live**, live-revoked authenticated grants, and a large legacy index estate. Full classified backlog in the drift doc. +- DR rehearsal completed: schema restore ≈ 19 s to a local container, `search_schema_health()` ok and all four hybrid RPCs proven on the restored copy with seeded vectors; measured RPO/RTO and the did-not-survive list are in [docs/disaster-recovery-runbook.md](disaster-recovery-runbook.md). Expand/contract policy for retrieval tables added to [docs/supabase-migration-reconciliation.md](supabase-migration-reconciliation.md). +- Migration `20260706200000_schema_drift_snapshot.sql` (the snapshot RPC) and `20260707000000_codify_live_observed_drift.sql` are **prepared but NOT applied to live** — apply with the other pending migrations on the next approved `supabase db push`; `check:drift` cannot run against live until the RPC lands. + ## PR merge gate: tiered CI + required checks (2026-07-02) - CI is now two parallel PR jobs instead of one serial 6-7 minute job: `verify` (runtime alignment, edge typecheck, CI-safe production readiness, lint, typecheck, unit tests with coverage gate, build — ~3 min) and `ui-smoke` (Chromium Playwright smoke against its own dev server — ~4.5 min). Wall-clock PR feedback drops to the slower of the two, and a flaky smoke rerun no longer repeats lint/typecheck/tests/build. diff --git a/docs/supabase-migration-reconciliation.md b/docs/supabase-migration-reconciliation.md index e6c51c7fa..5d3b675c5 100644 --- a/docs/supabase-migration-reconciliation.md +++ b/docs/supabase-migration-reconciliation.md @@ -1,6 +1,6 @@ # Supabase Migration Reconciliation -Last reviewed: 2026-07-05 +Last reviewed: 2026-07-07 Target project: Clinical KB Database (`sjrfecxgysukkwxsowpy`) @@ -11,6 +11,62 @@ Target project: Clinical KB Database (`sjrfecxgysukkwxsowpy`) - Use `supabase migration repair --linked --status applied ` only when live database evidence proves the migration effect already exists. - Leave other local-only migrations unrepaired until their effects are verified or deliberately applied. - Run `npx supabase migration list --linked` at apply/reconcile time; do not rely on a frozen “aligned through” snapshot in this doc alone. +- **History presence is not effect presence.** `20260703030000` is recorded as applied on live while its index changes are absent. After every apply, verify object state with `npm run check:drift` (and `search_schema_health()`), not the history table. +- Any PR that changes `supabase/schema.sql` regenerates `supabase/drift-manifest.json` in the same PR (`npm run drift:manifest`, Docker required); `tests/drift-detection.test.ts` fails otherwise. This doubles as a from-scratch replay proof of schema.sql. + +## Expand/contract policy for retrieval tables + +Applies to anything touching `documents`, `document_chunks`, +`document_embedding_fields`, `document_index_units`, `document_memory_cards`, +`document_table_facts`, embedding columns, or the RPCs that read them. Written +after the 2026-07-07 DR rehearsal +([disaster-recovery-runbook.md](disaster-recovery-runbook.md)), which showed +(a) live carried worker-written columns that existed in no repo lineage, so a +restored/branch database silently broke ingestion, and (b) a "recorded as +applied" migration whose effects never landed. + +**Expand phase (additive, ships first):** + +- New columns are `add column if not exists`, nullable or defaulted — the RAG + tables have 69k–215k rows; a table rewrite (non-constant default, type + change) needs an explicit lock/duration plan in the migration header. +- New/changed RPC behaviour ships as a side-by-side version + (`match_document_memory_cards_hybrid_v2` precedent) with the old RPC left + callable until the app is fully cut over; grants replicated explicitly + (`revoke ... from public, anon, authenticated; grant ... to service_role`). +- New constraints on populated tables use `NOT VALID` now + `VALIDATE +CONSTRAINT` in a later migration (the live `*_content_not_blank` checks are + the precedent). +- Index replacements create the new index first (on live, prefer + `CONCURRENTLY` run manually outside the transaction — CLI migrations are + transactional); the old index is NOT dropped in the same migration. +- Embedding columns: `vector(N)` is coupled to `EMBEDDING_DIMENSIONS` and the + worker's startup check — a dimension change is a re-index project with the + reindex-eval gate, never a plain migration. +- The same PR updates `supabase/schema.sql`, regenerates the drift manifest, + and (for anything behaviour-adjacent) passes `npm run +eval:retrieval:quality` per the standing merge gate. + +**Verify phase (between expand and contract):** + +- Apply to live only through the linked workflow with explicit approval, then + immediately run `npm run check:drift` — the applied objects must match the + manifest (this is what catches recorded-but-ineffective applies). +- Run `search_schema_health()` / `npm run check:indexing` and the golden + retrieval eval against live before relying on the new path. +- Dual-read/dual-write windows (old + new column/RPC) stay until the eval and + telemetry confirm the new path. + +**Contract phase (destructive, ships last and separately):** + +- Drops (columns, old RPC versions, superseded indexes) go in their own + migration, at least one release after expand, never bundled with it. +- Before contracting: a fresh backup/PITR point exists, `pg_stat_user_indexes` + scan evidence for index drops (the `20260702014803` discipline), and + check:drift green so the pre-contract state is fully accounted for. +- Rollback plan is written in the migration header (what to recreate, from + where) — after contract, rollback means restore-from-backup for data-bearing + drops, so say so explicitly. ## Verified Applied (through June 2026) @@ -45,6 +101,17 @@ The repo also includes additional July 2026 migrations beyond the June checkpoin Live-only drift, duplicate migration-version churn, and outstanding follow-up debts are tracked in the **Retrieval RPC drift & indexing hygiene** section of [`docs/process-hardening.md`](process-hardening.md). Treat that section as the operational supplement to this reconciliation doc. +**2026-07-07 full-inventory audit:** the standing drift check +([database-drift-detection.md](database-drift-detection.md)) measured live +against both repo lineages. Pending on live as of the audit: `20260705210000` +(owner-sentinel — 8 function bodies), `20260706010000` (M13 guard), +`20260706130000`, plus the new `20260706200000` (drift snapshot RPC) and +`20260707000000` (codification wave, no-op on live). `20260703030000` is +recorded in live history but its effects are absent — repair by re-applying +its statements under a new version, with approval. The complete reconciliation +backlog (index estate, grant posture, remaining live-only functions) lives in +the drift doc. + Before applying pending migrations to live: 1. Run `npx supabase migration list --linked` and confirm local vs remote alignment. diff --git a/package.json b/package.json index 8dd106ce1..03dff5c84 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,9 @@ "workflow:deps": "node ../.local-dev/workflow-deps.mjs", "workflow:clean-state": "node ../.local-dev/workflow-clean-state.mjs", "workflow:export": "node ../.local-dev/workflow-export.mjs", - "workflow:handoff": "node ../.local-dev/workflow-handoff.mjs" + "workflow:handoff": "node ../.local-dev/workflow-handoff.mjs", + "check:drift": "tsx scripts/check-drift.ts", + "drift:manifest": "tsx scripts/generate-drift-manifest.ts" }, "dependencies": { "@next/env": "16.2.10", diff --git a/scripts/check-drift.ts b/scripts/check-drift.ts new file mode 100644 index 000000000..965dc064b --- /dev/null +++ b/scripts/check-drift.ts @@ -0,0 +1,312 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { loadEnvConfig } from "@next/env"; + +loadEnvConfig(process.cwd()); + +/** + * check:drift — compares the live database's full schema inventory against the + * committed expectation derived from supabase/schema.sql. + * + * Expected side: supabase/drift-manifest.json, generated by replaying + * supabase/schema.sql into a scratch Supabase Postgres container + * (`npm run drift:manifest`). The manifest embeds the sha256 of the schema.sql + * it was generated from, so a stale manifest fails fast here (and offline in + * tests/drift-detection.test.ts) instead of producing phantom drift. + * + * Live side: public.schema_drift_snapshot() (migration + * 20260706200000_schema_drift_snapshot.sql), a service-role-only RPC returning + * the same normalized inventory the manifest holds. + * + * Known, documented divergence is carried in supabase/drift-allowlist.json — + * every entry needs a reason and is reported as a warning, never silently + * dropped. Anything not allowlisted exits 1. See docs/database-drift-detection.md. + */ + +type SnapshotObject = Record; +type Snapshot = Record; + +type AllowlistEntry = { + category: string; + kind: "missing_live" | "unexpected_live" | "mismatch" | "alias"; + key: string; + live_key?: string; + reason: string; +}; + +type Finding = { + category: string; + kind: "missing_live" | "unexpected_live" | "mismatch"; + key: string; + detail?: string; +}; + +const read = (relative: string) => readFileSync(new URL(`../${relative}`, import.meta.url), "utf8"); + +export function normalizedSchemaSha256(schemaSqlText: string) { + return createHash("sha256").update(schemaSqlText.replace(/\r\n/g, "\n")).digest("hex"); +} + +// category -> unique key extractor. Must stay aligned with the RPC's shape. +const categoryKeys: Record string> = { + extensions: (o) => String(o.name), + tables: (o) => String(o.name), + views: (o) => String(o.name), + functions: (o) => String(o.signature), + indexes: (o) => String(o.name), + policies: (o) => `${o.schema}.${o.table}.${o.name}`, + constraints: (o) => `${o.table}.${o.name}`, + triggers: (o) => `${o.table}.${o.name}`, + storage_buckets: (o) => String(o.id), +}; + +// Fields that identify content per category (compared as canonical JSON). +const comparedFields: Record = { + extensions: ["schema"], + tables: ["rls_enabled", "rls_forced", "reloptions", "acl", "columns"], + views: ["def_hash"], + functions: ["def_hash", "acl"], + indexes: ["table", "def_hash"], + policies: ["permissive", "roles", "cmd", "qual", "with_check"], + constraints: ["def"], + triggers: ["def"], + storage_buckets: ["public", "file_size_limit", "allowed_mime_types"], +}; + +function canonical(value: unknown): string { + return JSON.stringify(value, (_key, v) => + v && typeof v === "object" && !Array.isArray(v) + ? Object.fromEntries( + Object.keys(v as Record) + .sort() + .map((k) => [k, (v as Record)[k]]), + ) + : v, + ); +} + +function indexObjects(category: string, snapshot: Snapshot): Map { + const list = snapshot[category]; + const keyOf = categoryKeys[category]; + const map = new Map(); + if (!Array.isArray(list)) return map; + for (const raw of list) { + if (raw && typeof raw === "object") { + const o = raw as SnapshotObject; + map.set(keyOf(o), o); + } + } + return map; +} + +function contentOf(category: string, o: SnapshotObject): string { + return canonical(Object.fromEntries(comparedFields[category].map((f) => [f, o[f] ?? null]))); +} + +function fieldDiff(category: string, expected: SnapshotObject, actual: SnapshotObject): string { + const parts: string[] = []; + for (const field of comparedFields[category]) { + const e = canonical(expected[field] ?? null); + const a = canonical(actual[field] ?? null); + if (e !== a) { + const clip = (s: string) => (s.length > 240 ? `${s.slice(0, 240)}…` : s); + parts.push(`${field}: manifest=${clip(e)} live=${clip(a)}`); + } + } + return parts.join("; "); +} + +export type DriftComparison = { + findings: Finding[]; + allowed: { entry: AllowlistEntry; finding: Finding }[]; + staleEntries: AllowlistEntry[]; + infos: string[]; +}; + +export function compareDriftSnapshots( + expected: Snapshot, + live: Snapshot, + allowlist: AllowlistEntry[], +): DriftComparison { + const findings: Finding[] = []; + const infos: string[] = []; + + for (const category of Object.keys(categoryKeys)) { + const expectedMap = indexObjects(category, expected); + const liveMap = indexObjects(category, live); + + for (const [key, expectedObject] of expectedMap) { + const liveObject = liveMap.get(key); + if (!liveObject) { + findings.push({ category, kind: "missing_live", key, detail: contentOf(category, expectedObject) }); + continue; + } + if (contentOf(category, expectedObject) !== contentOf(category, liveObject)) { + findings.push({ category, kind: "mismatch", key, detail: fieldDiff(category, expectedObject, liveObject) }); + } + } + + for (const key of liveMap.keys()) { + if (expectedMap.has(key)) continue; + // Hosted platform provisions extensions (pg_net, pgsodium, …) that a + // schema.sql replay never creates; extra live extensions are informational. + if (category === "extensions") { + infos.push(`extra live extension (platform-provisioned): ${key}`); + continue; + } + findings.push({ category, kind: "unexpected_live", key }); + } + } + + // Apply the allowlist. Alias entries assert the live database carries the + // same index under a legacy name; they consume both the missing_live finding + // for the manifest name and the unexpected_live finding for the legacy name. + const allowed: { entry: AllowlistEntry; finding: Finding }[] = []; + const remaining: Finding[] = []; + const usedEntries = new Set(); + + const liveIndexes = indexObjects("indexes", live); + const expectedIndexes = indexObjects("indexes", expected); + + const matchesAlias = (entry: AllowlistEntry, finding: Finding): boolean => { + if (entry.category !== "indexes" || finding.category !== "indexes") return false; + if (finding.kind === "missing_live" && finding.key === entry.key) { + const manifestIdx = expectedIndexes.get(entry.key); + const liveIdx = entry.live_key ? liveIndexes.get(entry.live_key) : undefined; + if (!manifestIdx || !liveIdx) return false; + const stripName = (o: SnapshotObject) => + String(o.def ?? "") + .split(String(o.name)) + .join("@NAME@") + .replace(/\s+/g, ""); + return manifestIdx.table === liveIdx.table && stripName(manifestIdx) === stripName(liveIdx); + } + return finding.kind === "unexpected_live" && finding.key === entry.live_key; + }; + + for (const finding of findings) { + const entry = allowlist.find( + (candidate) => + (candidate.kind === "alias" && matchesAlias(candidate, finding)) || + (candidate.kind === finding.kind && candidate.category === finding.category && candidate.key === finding.key), + ); + if (entry) { + usedEntries.add(entry); + allowed.push({ entry, finding }); + } else { + remaining.push(finding); + } + } + + return { + findings: remaining, + allowed, + staleEntries: allowlist.filter((entry) => !usedEntries.has(entry)), + infos, + }; +} + +async function main() { + const [{ requireServerEnv }, { createAdminClient }, { checkSupabaseProjectConfig, formatSupabaseProjectCheck }] = + await Promise.all([import("@/lib/env"), import("@/lib/supabase/admin"), import("@/lib/supabase/project")]); + + requireServerEnv(); + const projectCheck = checkSupabaseProjectConfig( + { + NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, + SUPABASE_PROJECT_REF: process.env.SUPABASE_PROJECT_REF, + SUPABASE_PROJECT_NAME: process.env.SUPABASE_PROJECT_NAME, + }, + { requireMetadata: false }, + ); + if (projectCheck.status === "missing" || projectCheck.status === "mismatch") { + throw new Error(formatSupabaseProjectCheck(projectCheck)); + } + + const manifestFile = JSON.parse(read("supabase/drift-manifest.json")) as { + schema_sha256: string; + generated_at: string; + postgres_image: string; + snapshot: Snapshot; + }; + const allowlistFile = JSON.parse(read("supabase/drift-allowlist.json")) as { entries: AllowlistEntry[] }; + const allowlist = allowlistFile.entries ?? []; + + const schemaSha = normalizedSchemaSha256(read("supabase/schema.sql")); + if (schemaSha !== manifestFile.schema_sha256) { + throw new Error( + `supabase/drift-manifest.json is stale: it was generated from a different supabase/schema.sql ` + + `(manifest ${manifestFile.schema_sha256.slice(0, 12)}…, current ${schemaSha.slice(0, 12)}…). ` + + `Regenerate with: npm run drift:manifest`, + ); + } + + const supabase = createAdminClient(); + const { data: liveSnapshot, error } = await supabase.rpc("schema_drift_snapshot" as never); + if (error) { + const message = String(error.message ?? error); + if (/could not find the function|schema cache|PGRST202/i.test(message)) { + throw new Error( + `schema_drift_snapshot() is not available on the live project. Apply migration ` + + `20260706200000_schema_drift_snapshot.sql through the normal linked migration workflow first. (${message})`, + ); + } + throw new Error(`schema_drift_snapshot RPC failed: ${message}`); + } + if (!liveSnapshot || typeof liveSnapshot !== "object") { + throw new Error("schema_drift_snapshot returned an unexpected payload"); + } + const live = liveSnapshot as Snapshot; + const expected = manifestFile.snapshot; + + const { findings: remaining, allowed, staleEntries, infos } = compareDriftSnapshots(expected, live, allowlist); + + console.log(`Drift manifest: generated ${manifestFile.generated_at} from schema.sql ${schemaSha.slice(0, 12)}…`); + console.log( + `Compared ${Object.keys(categoryKeys) + .map((category) => `${indexObjects(category, expected).size} ${category}`) + .join(", ")} against live.`, + ); + for (const info of infos) console.log(` info: ${info}`); + if (allowed.length > 0) { + console.log(`\nAllowlisted divergence (${allowed.length}) — known and documented, not failures:`); + for (const { entry, finding } of allowed) { + console.log(` ~ [${finding.category}] ${finding.kind} ${finding.key} — ${entry.reason}`); + } + } + if (staleEntries.length > 0) { + console.log(`\nStale allowlist entries (${staleEntries.length}) — no longer matching, remove them:`); + for (const entry of staleEntries) { + console.log(` ? [${entry.category}] ${entry.kind} ${entry.key}`); + } + } + if (remaining.length > 0) { + console.error(`\nUNEXPECTED DRIFT (${remaining.length}):`); + for (const finding of remaining) { + console.error( + ` ! [${finding.category}] ${finding.kind} ${finding.key}${finding.detail ? ` :: ${finding.detail}` : ""}`, + ); + } + console.error( + `\nLive schema diverges from supabase/schema.sql. Either codify the live state ` + + `(committed migration + schema.sql + regenerate the manifest) or fix live through ` + + `an approved migration. Raw SQL against live is how this class of incident started; ` + + `do not \"fix\" drift that way. See docs/database-drift-detection.md.`, + ); + process.exitCode = 1; + return; + } + + console.log(`\nNo unexpected schema drift between live and supabase/schema.sql.`); + if (staleEntries.length > 0) { + console.log("(Clean up the stale allowlist entries above in a follow-up commit.)"); + } +} + +const invokedDirectly = process.argv[1] && /check-drift\.(ts|mts|js)$/.test(process.argv[1]); +if (invokedDirectly) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/scripts/generate-drift-manifest.ts b/scripts/generate-drift-manifest.ts new file mode 100644 index 000000000..65f107fc5 --- /dev/null +++ b/scripts/generate-drift-manifest.ts @@ -0,0 +1,130 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +/** + * drift:manifest — regenerates supabase/drift-manifest.json. + * + * Replays supabase/schema.sql from scratch into a disposable Supabase Postgres + * Docker container (the same image family the hosted platform runs), then + * captures public.schema_drift_snapshot() from that pristine database. The + * result is the expected-state side of `npm run check:drift`. + * + * Requires Docker. Never touches the live project. Run this whenever + * supabase/schema.sql changes — the manifest embeds schema.sql's sha256 and + * both check:drift and tests/supabase-schema.test.ts fail while it is stale. + * + * Flags: + * --keep leave the container running (for inspection / DR rehearsal) + * --port host port for the scratch Postgres (default 56599) + * --image override the Postgres image tag + */ + +const IMAGE_DEFAULT = "supabase/postgres:17.6.1.127"; +const CONTAINER = "clinical-kb-drift-manifest"; + +const repoUrl = (relative: string) => fileURLToPath(new URL(`../${relative}`, import.meta.url)); + +function arg(flag: string): string | undefined { + const index = process.argv.indexOf(flag); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +function docker(args: string[], input?: string): string { + return execFileSync("docker", args, { + encoding: "utf8", + input, + maxBuffer: 64 * 1024 * 1024, + stdio: ["pipe", "pipe", "pipe"], + }); +} + +async function main() { + const image = arg("--image") ?? IMAGE_DEFAULT; + const port = arg("--port") ?? "56599"; + const keep = process.argv.includes("--keep"); + + try { + docker(["info", "--format", "{{.ServerVersion}}"]); + } catch { + throw new Error( + "Docker is required for drift:manifest (it replays schema.sql into a scratch container). Start Docker and retry.", + ); + } + + const schemaSql = readFileSync(repoUrl("supabase/schema.sql"), "utf8"); + const scaffoldSql = readFileSync(repoUrl("scripts/sql/drift-replay-scaffold.sql"), "utf8"); + const { normalizedSchemaSha256 } = await import("./check-drift"); + + console.log(`Starting scratch container ${CONTAINER} (${image}) on port ${port}…`); + try { + docker(["rm", "-f", CONTAINER]); + } catch { + // not running — fine + } + const startedAt = Date.now(); + docker(["run", "-d", "--name", CONTAINER, "-e", "POSTGRES_PASSWORD=postgres", "-p", `${port}:5432`, image]); + + try { + // Wait for Postgres readiness (image init runs its own restarts; give it time). + let ready = false; + for (let attempt = 0; attempt < 60; attempt += 1) { + try { + docker(["exec", CONTAINER, "pg_isready", "-U", "postgres", "-q"]); + ready = true; + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + if (!ready) throw new Error("scratch Postgres did not become ready in time"); + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const psql = (user: string, sql: string) => + docker( + ["exec", "-i", CONTAINER, "psql", "-U", user, "-d", "postgres", "-v", "ON_ERROR_STOP=1", "-q", "-f", "-"], + sql, + ); + + console.log("Applying storage scaffold (supabase_admin)…"); + psql("supabase_admin", scaffoldSql); + console.log("Replaying supabase/schema.sql from scratch (postgres)…"); + psql("postgres", schemaSql); + const replaySeconds = ((Date.now() - startedAt) / 1000).toFixed(0); + console.log(`Replay complete in ${replaySeconds}s (container start included).`); + + const raw = docker( + ["exec", "-i", CONTAINER, "psql", "-U", "postgres", "-d", "postgres", "-tA", "-v", "ON_ERROR_STOP=1", "-f", "-"], + "select public.schema_drift_snapshot()::text;", + ).trim(); + const snapshot = JSON.parse(raw) as Record; + delete snapshot.captured_at; + + const manifest = { + generated_at: new Date().toISOString(), + generator: "scripts/generate-drift-manifest.ts", + postgres_image: image, + schema_sha256: normalizedSchemaSha256(schemaSql), + replay_seconds: Number(replaySeconds), + snapshot, + }; + writeFileSync(repoUrl("supabase/drift-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + console.log("Wrote supabase/drift-manifest.json"); + console.log("Next: run `npm run check:drift` against live (needs service-role env)."); + } finally { + if (keep) { + console.log(`Container ${CONTAINER} kept running on port ${port} (--keep).`); + } else { + try { + docker(["rm", "-f", CONTAINER]); + } catch { + console.warn(`Could not remove container ${CONTAINER}; remove it manually.`); + } + } + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}); diff --git a/scripts/sql/drift-replay-scaffold.sql b/scripts/sql/drift-replay-scaffold.sql new file mode 100644 index 000000000..efbd8bad2 --- /dev/null +++ b/scripts/sql/drift-replay-scaffold.sql @@ -0,0 +1,72 @@ +-- Minimal storage-schema scaffold so supabase/schema.sql can replay on a bare +-- supabase/postgres Docker image (used by `npm run drift:manifest` and the +-- disaster-recovery rehearsal in docs/disaster-recovery-runbook.md). +-- +-- The hosted platform provisions storage.buckets / storage.objects / +-- storage.foldername() through the Storage service's own migrations; the bare +-- image ships the empty `storage` schema only. schema.sql needs the buckets +-- table (bucket inserts), the objects table (owner-read policies), and +-- foldername() (referenced by those policy predicates). Column shapes below +-- mirror the hosted service closely enough for DDL replay; they are NOT a +-- substitute for the real Storage service. +-- +-- Idempotent; safe to re-run. Never run against the live project (it already +-- has the real storage schema, and every statement here is create-if-missing, +-- but there is no reason for it to touch live). +-- +-- Run as `supabase_admin` (the image's superuser): the bare image ships the +-- `storage` schema owned by supabase_admin, and `postgres` cannot create in +-- it. Ownership of the scaffold tables is handed to `postgres` afterwards so +-- schema.sql (applied as `postgres`, matching how live is administered) can +-- insert bucket rows and create the storage.objects policies. + +create schema if not exists storage; + +create table if not exists storage.buckets ( + id text primary key, + name text not null unique, + owner uuid, + public boolean default false, + avif_autodetection boolean default false, + file_size_limit bigint, + allowed_mime_types text[], + created_at timestamptz default now(), + updated_at timestamptz default now() +); + +create table if not exists storage.objects ( + id uuid primary key default gen_random_uuid(), + bucket_id text references storage.buckets(id), + name text, + owner uuid, + owner_id text, + version text, + metadata jsonb, + path_tokens text[] generated always as (string_to_array(name, '/')) stored, + created_at timestamptz default now(), + updated_at timestamptz default now(), + last_accessed_at timestamptz default now() +); + +alter table storage.buckets enable row level security; +alter table storage.objects enable row level security; + +create or replace function storage.foldername(name text) +returns text[] +language plpgsql +immutable +as $$ +declare + _parts text[]; +begin + select string_to_array(name, '/') into _parts; + return _parts[1 : array_length(_parts, 1) - 1]; +end; +$$; + +alter table storage.buckets owner to postgres; +alter table storage.objects owner to postgres; +alter function storage.foldername(text) owner to postgres; + +grant usage on schema storage to postgres, anon, authenticated, service_role; +grant create on schema storage to postgres; diff --git a/supabase/drift-allowlist.json b/supabase/drift-allowlist.json new file mode 100644 index 000000000..6c888b048 --- /dev/null +++ b/supabase/drift-allowlist.json @@ -0,0 +1,883 @@ +{ + "_comment": "Known live-vs-schema.sql divergence measured 2026-07-07 (see docs/database-drift-detection.md). Every entry must carry a reason; check:drift reports matches as warnings and fails on anything NOT listed here. Remove entries as the reconciliation backlog lands.", + "entries": [ + { + "category": "constraints", + "key": "ingestion_job_stages.ingestion_job_stages_job_id_fkey", + "kind": "missing_live", + "reason": "FK declared in schema.sql and migrations but absent on live; add via approved migration (NOT VALID + VALIDATE pattern).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "constraints", + "key": "rag_visual_eval_cases.rag_visual_eval_cases_document_id_fkey", + "kind": "mismatch", + "reason": "Live FK definition differs from the codified repo definition (rag_visual_eval_* document FKs); align via approved migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "constraints", + "key": "rag_visual_eval_runs.rag_visual_eval_runs_document_id_fkey", + "kind": "mismatch", + "reason": "Live FK definition differs from the codified repo definition (rag_visual_eval_* document FKs); align via approved migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.detect_legacy_ivfflat_indexes()", + "kind": "mismatch", + "reason": "Live retains default PUBLIC execute (security-invoker fn, RLS still applies); schema.sql declares service-role-only. Revoke on live via an approved hardening migration. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.document_label_metadata(uuid)", + "kind": "mismatch", + "reason": "schema.sql is AHEAD of live: hidden-label filtering (review_status/hidden metadata) was added to schema.sql without a migration. Ship a migration or revert schema.sql; see backlog.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.document_summary_text(uuid)", + "kind": "mismatch", + "reason": "Live retains default PUBLIC execute (security-invoker fn, RLS still applies); schema.sql declares service-role-only. Revoke on live via an approved hardening migration. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.get_related_document_metadata(uuid[],uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.get_visual_evidence_cards(uuid,integer)", + "kind": "unexpected_live", + "reason": "Live-only function (raw-SQL era) not yet codified into schema.sql/migrations; codification wave follow-up (same pattern as 20260705230000 and 20260707000000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_document_chunks(extensions.vector,integer,double precision,uuid,uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_document_chunks_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_document_chunks_text(text,integer,uuid[],uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_document_embedding_fields_text(text,integer,double precision,uuid[],uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260706130000_fix_embedding_fields_text_owner_sentinel not yet applied to live. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_document_table_facts_text(text,integer,uuid[],uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.match_documents_for_query(text,integer,uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.repair_enrichment_quality_batch(integer)", + "kind": "unexpected_live", + "reason": "Live-only function (raw-SQL era) not yet codified into schema.sql/migrations; codification wave follow-up (same pattern as 20260705230000 and 20260707000000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.repair_strict_enrichment_gate_batch(integer)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.retrieval_owner_matches(uuid,uuid)", + "kind": "mismatch", + "reason": "Pending migration 20260705210000_retrieval_owner_filter_sentinel (owner-sentinel rewrite) not yet applied to live; resolves on the next approved supabase db push. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.run_all_visual_eval_cases(integer)", + "kind": "unexpected_live", + "reason": "Live-only function (raw-SQL era) not yet codified into schema.sql/migrations; codification wave follow-up (same pattern as 20260705230000 and 20260707000000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.run_visual_eval_case(uuid,integer)", + "kind": "unexpected_live", + "reason": "Live-only function (raw-SQL era) not yet codified into schema.sql/migrations; codification wave follow-up (same pattern as 20260705230000 and 20260707000000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.search_document_chunks(uuid,text,integer,uuid)", + "kind": "mismatch", + "reason": "Live retains default PUBLIC execute (security-invoker fn, RLS still applies); schema.sql declares service-role-only. Revoke on live via an approved hardening migration. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.search_schema_health()", + "kind": "mismatch", + "reason": "Pending migration 20260706010000_search_schema_health_m13_guard not yet applied to live. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "functions", + "key": "public.set_document_embedding_field_content_hash()", + "kind": "mismatch", + "reason": "Live retains default PUBLIC execute (security-invoker fn, RLS still applies); schema.sql declares service-role-only. Revoke on live via an approved hardening migration. Remove after apply.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "api_rate_limits_bucket_updated_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "audit_logs_action_created_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "audit_logs_owner_created_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "audit_logs_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "clinical_registry_record_sources_document_idx", + "kind": "alias", + "live_key": "clinical_registry_record_sources_document_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "clinical_registry_record_sources_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "differential_records_owner_slug_kind_uidx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_chunks_anchor_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_chunks_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_chunks_meta_rag_indexing_version_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_chunks_search_idx", + "kind": "alias", + "live_key": "document_chunks_search_tsv_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_embedding_fields_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_embedding_fields_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_created_at_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_duplicate_group_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_hash_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_priority_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_searchable_idx1", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_structured_profile_gin_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_images_visual_intelligence_version_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_quality_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_quality_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_quality_owner_score_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_heading_path_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_owner_document_created_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_source_chunk_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_index_units_source_image_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_labels_document_idx", + "kind": "alias", + "live_key": "document_labels_document_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_labels_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_labels_owner_label_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_memory_cards_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_memory_cards_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_memory_cards_owner_document_created_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_memory_cards_owner_idx", + "kind": "alias", + "live_key": "document_memory_cards_owner_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_memory_cards_section_idx", + "kind": "alias", + "live_key": "document_memory_cards_section_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_sections_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_sections_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_sections_owner_idx", + "kind": "alias", + "live_key": "document_sections_owner_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_summaries_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_summaries_owner_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_table_facts_chunk_idx", + "kind": "alias", + "live_key": "document_table_facts_source_chunk_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_table_facts_document_generation_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_table_facts_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_table_facts_owner_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "document_table_facts_title_row_param_trgm_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "documents_import_batch_idx", + "kind": "alias", + "live_key": "documents_import_batch_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "documents_indexed_updated_at_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "documents_owner_content_hash_unique_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "documents_search_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "documents_title_search_idx", + "kind": "alias", + "live_key": "documents_title_search_tsv_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "image_caption_cache_owner_hash_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "image_caption_cache_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "import_batches_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "import_batches_owner_status_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "import_batches_status_created_idx", + "kind": "mismatch", + "reason": "Live carries a pre-hardening index shape under the schema.sql name; reconcile via approved migration (same class as 20260703030000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_job_stages_job_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_job_stages_job_stage_started_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_jobs_document_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_jobs_document_status_idx", + "kind": "mismatch", + "reason": "Live carries a pre-hardening index shape under the schema.sql name; reconcile via approved migration (same class as 20260703030000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_jobs_status_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_jobs_status_next_run_idx", + "kind": "mismatch", + "reason": "Live carries a pre-hardening index shape under the schema.sql name; reconcile via approved migration (same class as 20260703030000).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "ingestion_jobs_updated_at_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_aliases_alias_trgm_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_aliases_type_enabled_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_queries_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_queries_owner_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_queries_source_chunk_ids_gin_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_aliases_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_expected_chunk_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_expected_document_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_normalized_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_owner_created_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_query_misses_owner_review_status_created_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_response_cache_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "rag_retrieval_logs_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "storage_cleanup_jobs_document_idx", + "kind": "alias", + "live_key": "storage_cleanup_jobs_document_id_idx", + "reason": "Same index shape lives under a legacy migration-era name on live (search_schema_health index_aliases class). Rename on live via approved migration to retire this alias.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "storage_cleanup_jobs_owner_id_idx", + "kind": "unexpected_live", + "reason": "Live-only legacy/duplicate index with no schema.sql equivalent; drop candidates for a cleanup migration (verify pg_stat_user_indexes scans first).", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "storage_cleanup_jobs_owner_status_idx", + "kind": "missing_live", + "reason": "Declared in schema.sql (and migration chain) but absent on live - likely raw-SQL cleanup or never applied. Decide per index: recreate via migration or remove from schema.sql.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "indexes", + "key": "storage_cleanup_jobs_status_created_idx", + "kind": "mismatch", + "reason": "Migration 20260703030000_reconcile_storage_cleanup_jobs_indexes is RECORDED as applied in live history but its effects are absent (partial index shape missing). Needs migration repair + re-apply with approval.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_chunks", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_embedding_fields", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_images", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_index_quality", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_index_units", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_labels", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_pages", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_summaries", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "document_table_facts", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "documents", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "import_batches", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "ingestion_jobs", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "rag_aliases", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "rag_queries", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "rag_query_misses", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "rag_retrieval_logs", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + }, + { + "category": "tables", + "key": "storage_cleanup_jobs", + "kind": "mismatch", + "reason": "Live revoked the authenticated-role Data API grants that schema.sql (and the owner-read RLS policies) still declare; app access is service-role only. Open posture decision: codify the revokes or restore the grants via migration.", + "ref": "docs/database-drift-detection.md#reconciliation-backlog" + } + ] +} diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json new file mode 100644 index 000000000..710da2a31 --- /dev/null +++ b/supabase/drift-manifest.json @@ -0,0 +1,6776 @@ +{ + "generated_at": "2026-07-06T18:30:39.629Z", + "generator": "scripts/generate-drift-manifest.ts", + "postgres_image": "supabase/postgres:17.6.1.127", + "schema_sha256": "9c29e76446f57874b3ea4f474713dcdc2d4bf029213bc96dbebbb60a5057a84c", + "replay_seconds": 15, + "snapshot": { + "views": [ + { + "name": "document_strict_gate_status", + "def_hash": "9f21fc372b45ddfdd32ceb65007d1ba3" + } + ], + "tables": [ + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "api_rate_limit_subjects", + "columns": [ + { + "name": "bucket", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "request_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "subject_key", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "window_start", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "api_rate_limits", + "columns": [ + { + "name": "bucket", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "request_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "window_start", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "audit_logs", + "columns": [ + { + "name": "action", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "resource_id", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "resource_type", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "clinical_registry_record_sources", + "columns": [ + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "note", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "record_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "clinical_registry_records", + "columns": [ + { + "name": "best_use", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "catalog_payload", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "catalogue_label", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "catchments", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "contacts", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "cost", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "criteria", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "eligibility", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "kind", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "last_reviewed_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "location", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "navigator_query", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "primary_contact", + "type": "jsonb", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "referral", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "referral_info", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "review_due_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "route", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "slug", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_status", + "type": "text", + "default": "'unknown'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "status_chips", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "subtitle", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "summary_cards", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "tags", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "title", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "validation_status", + "type": "text", + "default": "'unverified'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "verification", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "differential_records", + "columns": [ + { + "name": "clinical_hinge", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "kind", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "last_reviewed_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "payload", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "review_due_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "slug", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_status", + "type": "text", + "default": "'unknown'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "subtitle", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "tags", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "title", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "validation_status", + "type": "text", + "default": "'unverified'::text", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_chunks", + "columns": [ + { + "name": "anchor_id", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "chunk_index", + "type": "integer", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "content", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "content_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "embedding", + "type": "extensions.vector(1536)", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "heading_level", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "page_number", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "parent_heading", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "retrieval_synopsis", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, ((((COALESCE(section_heading, ''::text) || ' '::text) || COALESCE(retrieval_synopsis, ''::text)) || ' '::text) || content))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "section_heading", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "section_path", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "token_estimate", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_analyze_threshold=500", + "autovacuum_vacuum_scale_factor=0.05", + "autovacuum_vacuum_threshold=1000" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_embedding_fields", + "columns": [ + { + "name": "content", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "content_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "embedding", + "type": "extensions.vector(1536)", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "field_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, content)", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "source_chunk_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_vacuum_scale_factor=0.05" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_images", + "columns": [ + { + "name": "bbox", + "type": "jsonb", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "caption", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "caption_confidence", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "clinical_priority_score", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "clinical_relevance_score", + "type": "real", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "crop_completeness", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "height", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "image_quality_score", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "image_type", + "type": "text", + "default": "'unclear'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "labels", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "mime_type", + "type": "text", + "default": "'image/png'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "ocr_text_density", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_number", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "perceptual_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "searchable", + "type": "boolean", + "default": "true", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "skip_reason", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_kind", + "type": "text", + "default": "'embedded'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "storage_path", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "structured_extraction_confidence", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "visual_duplicate_group", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "width", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_analyze_threshold=500", + "autovacuum_vacuum_scale_factor=0.05", + "autovacuum_vacuum_threshold=1000" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_index_quality", + "columns": [ + { + "name": "anchor_coverage", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "extraction_quality", + "type": "text", + "default": "'unknown'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "issues", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metrics", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "model_fallback_rate", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "noisy_unit_rate", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "quality_score", + "type": "real", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "retrievable_visual_hit", + "type": "boolean", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_span_coverage", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "typed_unit_coverage", + "type": "real", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_index_units", + "columns": [ + { + "name": "content", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "embedding", + "type": "extensions.vector(1536)", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "extraction_mode", + "type": "text", + "default": "'deterministic'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "heading_path", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "normalized_terms", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_end", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_start", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "quality_score", + "type": "real", + "default": "0.7", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, ((((COALESCE(unit_type, ''::text) || ' '::text) || COALESCE(title, ''::text)) || ' '::text) || COALESCE(content, ''::text)))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "source_chunk_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_image_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_span", + "type": "jsonb", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "title", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "unit_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=arwd/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_labels", + "columns": [ + { + "name": "confidence", + "type": "real", + "default": "0.5", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "label", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "label_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source", + "type": "text", + "default": "'generated'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_analyze_threshold=100", + "autovacuum_vacuum_scale_factor=0.05", + "autovacuum_vacuum_threshold=200" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_memory_cards", + "columns": [ + { + "name": "card_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "confidence", + "type": "real", + "default": "0.5", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "content", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "embedding", + "type": "extensions.vector(1536)", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "normalized_terms", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_number", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, ((COALESCE(title, ''::text) || ' '::text) || COALESCE(content, ''::text)))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "section_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_image_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "title", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_pages", + "columns": [ + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "ocr_used", + "type": "boolean", + "default": "false", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "page_number", + "type": "integer", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "text", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_analyze_threshold=500", + "autovacuum_vacuum_scale_factor=0.05", + "autovacuum_vacuum_threshold=1000" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_sections", + "columns": [ + { + "name": "chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "extraction_quality", + "type": "text", + "default": "'unknown'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "heading", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "heading_path", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_end", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_start", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "section_index", + "type": "integer", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "summary", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "tags", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_summaries", + "columns": [ + { + "name": "clinical_specifics", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "generated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "model", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_image_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "summary", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "document_table_facts", + "columns": [ + { + "name": "action", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "clinical_parameter", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_generation_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "normalized_terms", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_number", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "row_label", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, ((((((((COALESCE(table_title, ''::text) || ' '::text) || COALESCE(row_label, ''::text)) || ' '::text) || COALESCE(clinical_parameter, ''::text)) || ' '::text) || COALESCE(threshold_value, ''::text)) || ' '::text) || COALESCE(action, ''::text)))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "source_chunk_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "source_image_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "table_title", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "threshold_value", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": [ + "autovacuum_analyze_scale_factor=0.02", + "autovacuum_analyze_threshold=500", + "autovacuum_vacuum_scale_factor=0.05", + "autovacuum_vacuum_threshold=1000" + ], + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "documents", + "columns": [ + { + "name": "chunk_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "content_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "description", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "error_message", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "file_name", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "file_size", + "type": "bigint", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "file_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "import_batch_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "page_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, ((COALESCE(title, ''::text) || ' '::text) || COALESCE(file_name, ''::text)))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "source_path", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": "'queued'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "storage_path", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "title", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "title_search_tsv", + "type": "tsvector", + "default": "to_tsvector('english'::regconfig, regexp_replace(regexp_replace(((COALESCE(title, ''::text) || ' '::text) || COALESCE(file_name, ''::text)), '([[:lower:]])([[:upper:]])'::text, '\\1 \\2'::text, 'g'::text), '[^[:alnum:]]+'::text, ' '::text, 'g'::text))", + "identity": "", + "not_null": false, + "generated": "s" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "image_caption_cache", + "columns": [ + { + "name": "caption", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_hash", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "mime_type", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "model", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "import_batches", + "columns": [ + { + "name": "completed_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "failed_files", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "include_glob", + "type": "text", + "default": "'**/*.pdf'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "name", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "queued_files", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "skipped_files", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_root", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": "'queued'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "total_bytes", + "type": "bigint", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "total_files", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "indexing_v3_agent_jobs", + "columns": [ + { + "name": "attempt_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "enrichment_status", + "type": "text", + "default": "'pending'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "extensions.gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "last_error", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "locked_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "locked_by", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "max_attempts", + "type": "integer", + "default": "3", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "next_run_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": "'pending'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "version", + "type": "text", + "default": "'visual-core-v3'::text", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "ingestion_job_stages", + "columns": [ + { + "name": "artifact_counts", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "error_class", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "error_message", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "finished_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "job_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "retry_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "stage_name", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "stage_status", + "type": "text", + "default": "'started'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "started_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "ingestion_jobs", + "columns": [ + { + "name": "attempt_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "batch_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "completed_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "error_message", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "locked_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "locked_by", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "max_attempts", + "type": "integer", + "default": "3", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "next_run_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "progress", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "stage", + "type": "text", + "default": "'queued'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "started_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": "'pending'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "medication_records", + "columns": [ + { + "name": "accent", + "type": "text", + "default": "'#0f766e'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "category", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "class", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "last_reviewed_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "name", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "quick", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "review_due_at", + "type": "date", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "schedule", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "sections", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "slug", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_status", + "type": "text", + "default": "'unknown'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "stats", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "subclass", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "tag", + "type": "text", + "default": "''::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "validation_status", + "type": "text", + "default": "'unverified'::text", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_aliases", + "columns": [ + { + "name": "alias", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "alias_type", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "canonical", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "enabled", + "type": "boolean", + "default": "true", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "weight", + "type": "real", + "default": "1.0", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_queries", + "columns": [ + { + "name": "answer", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "model", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "source_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_query_misses", + "columns": [ + { + "name": "candidate_aliases", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "candidate_labels", + "type": "jsonb", + "default": "'[]'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "cited_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "clicked_chunk_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "clicked_document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "expected_chunk_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "expected_document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "expected_file", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "miss_reason", + "type": "text", + "default": "'weak_search'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "normalized_query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "promoted_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "promoted_eval_case", + "type": "boolean", + "default": "false", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "query_class", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "retrieval_strategy", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "review_notes", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "review_status", + "type": "text", + "default": "'new'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "reviewed_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "route", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "top_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "top_files", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "top_score", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_response_cache", + "columns": [ + { + "name": "cache_kind", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "dependency_version", + "type": "text", + "default": "'rag-cache-v1'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "expires_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "indexing_version", + "type": "text", + "default": "'rag-deep-memory-v1'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "normalized_query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "payload", + "type": "jsonb", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "scope_key", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_retrieval_logs", + "columns": [ + { + "name": "candidate_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "embedding_cache_hit", + "type": "boolean", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "embedding_field_count", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "embedding_latency_ms", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "index_unit_count", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "is_miss", + "type": "boolean", + "default": "false", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "mean_hybrid_score", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "memory_card_count", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "miss_reason", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "normalized_query", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "query_class", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "rerank_latency_ms", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "retrieval_strategy", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "rpc_latency_ms", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "selected_chunk_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "selected_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "selected_document_ids", + "type": "uuid[]", + "default": "'{}'::uuid[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "text_candidate_count", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "top_hybrid_score", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "top_rrf_score", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "top_similarity", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "top_text_rank", + "type": "double precision", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "total_latency_ms", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "vector_candidate_count", + "type": "integer", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_visual_eval_cases", + "columns": [ + { + "name": "active", + "type": "boolean", + "default": "true", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "case_name", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "expected_image_type", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "expected_terms", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "expected_unit_types", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "query", + "type": "text", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "rag_visual_eval_runs", + "columns": [ + { + "name": "case_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "hit_payload", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "matched_count", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "passed", + "type": "boolean", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "run_metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "top_hit", + "type": "boolean", + "default": null, + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + }, + { + "acl": [ + "authenticated=r/postgres", + "postgres=arwdDxtm/postgres", + "service_role=arwdDxtm/postgres" + ], + "name": "storage_cleanup_jobs", + "columns": [ + { + "name": "attempts", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "completed_at", + "type": "timestamp with time zone", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "created_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_bucket", + "type": "text", + "default": "'clinical-documents'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "document_paths", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "document_title", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "id", + "type": "uuid", + "default": "gen_random_uuid()", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_bucket", + "type": "text", + "default": "'clinical-images'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "image_paths", + "type": "text[]", + "default": "'{}'::text[]", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "last_error", + "type": "text", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "metadata", + "type": "jsonb", + "default": "'{}'::jsonb", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "owner_id", + "type": "uuid", + "default": null, + "identity": "", + "not_null": false, + "generated": "" + }, + { + "name": "status", + "type": "text", + "default": "'pending'::text", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "storage_removed", + "type": "integer", + "default": "0", + "identity": "", + "not_null": true, + "generated": "" + }, + { + "name": "updated_at", + "type": "timestamp with time zone", + "default": "now()", + "identity": "", + "not_null": true, + "generated": "" + } + ], + "reloptions": null, + "rls_forced": false, + "rls_enabled": true + } + ], + "indexes": [ + { + "def": "CREATE INDEX api_rate_limit_subjects_bucket_updated_idx ON public.api_rate_limit_subjects USING btree (bucket, updated_at DESC)", + "name": "api_rate_limit_subjects_bucket_updated_idx", + "table": "api_rate_limit_subjects", + "def_hash": "f491eb4e5ed92c6c03bbc53ec80e72e8" + }, + { + "def": "CREATE UNIQUE INDEX api_rate_limit_subjects_pkey ON public.api_rate_limit_subjects USING btree (subject_key, bucket)", + "name": "api_rate_limit_subjects_pkey", + "table": "api_rate_limit_subjects", + "def_hash": "221b5377cbb2ed21375f2a113623d771" + }, + { + "def": "CREATE INDEX api_rate_limits_bucket_updated_idx ON public.api_rate_limits USING btree (bucket, updated_at DESC)", + "name": "api_rate_limits_bucket_updated_idx", + "table": "api_rate_limits", + "def_hash": "7fa48ed19dcb7b6ee164622c855773ad" + }, + { + "def": "CREATE UNIQUE INDEX api_rate_limits_pkey ON public.api_rate_limits USING btree (owner_id, bucket)", + "name": "api_rate_limits_pkey", + "table": "api_rate_limits", + "def_hash": "08320ed2788aaedf048adaaff21b84e7" + }, + { + "def": "CREATE INDEX audit_logs_action_created_idx ON public.audit_logs USING btree (action, created_at DESC)", + "name": "audit_logs_action_created_idx", + "table": "audit_logs", + "def_hash": "aad107f749435845db7dc21c50dd0de5" + }, + { + "def": "CREATE INDEX audit_logs_owner_created_idx ON public.audit_logs USING btree (owner_id, created_at DESC)", + "name": "audit_logs_owner_created_idx", + "table": "audit_logs", + "def_hash": "26c4becd95952ebdd2896baa2cd8af5d" + }, + { + "def": "CREATE UNIQUE INDEX audit_logs_pkey ON public.audit_logs USING btree (id)", + "name": "audit_logs_pkey", + "table": "audit_logs", + "def_hash": "c46e5a1e2717dce747858c846533afb2" + }, + { + "def": "CREATE INDEX clinical_registry_record_sources_document_idx ON public.clinical_registry_record_sources USING btree (document_id)", + "name": "clinical_registry_record_sources_document_idx", + "table": "clinical_registry_record_sources", + "def_hash": "c29c8a1dac5dec480b1b0634f90de59e" + }, + { + "def": "CREATE UNIQUE INDEX clinical_registry_record_sources_pkey ON public.clinical_registry_record_sources USING btree (id)", + "name": "clinical_registry_record_sources_pkey", + "table": "clinical_registry_record_sources", + "def_hash": "d1d7f8fa169356f4f24fce8845b4794b" + }, + { + "def": "CREATE UNIQUE INDEX clinical_registry_record_sources_record_id_document_id_key ON public.clinical_registry_record_sources USING btree (record_id, document_id)", + "name": "clinical_registry_record_sources_record_id_document_id_key", + "table": "clinical_registry_record_sources", + "def_hash": "aff03b1509bdec54ffc6611caf47b647" + }, + { + "def": "CREATE INDEX clinical_registry_record_sources_record_idx ON public.clinical_registry_record_sources USING btree (record_id)", + "name": "clinical_registry_record_sources_record_idx", + "table": "clinical_registry_record_sources", + "def_hash": "ef53891aac4bf949c894e35187d71517" + }, + { + "def": "CREATE UNIQUE INDEX clinical_registry_records_owner_id_kind_slug_key ON public.clinical_registry_records USING btree (owner_id, kind, slug)", + "name": "clinical_registry_records_owner_id_kind_slug_key", + "table": "clinical_registry_records", + "def_hash": "5873ba4cd38d5b586e65be39fbfb711b" + }, + { + "def": "CREATE INDEX clinical_registry_records_owner_kind_idx ON public.clinical_registry_records USING btree (owner_id, kind, title)", + "name": "clinical_registry_records_owner_kind_idx", + "table": "clinical_registry_records", + "def_hash": "74628550dc0681873e6fda5e02a613a1" + }, + { + "def": "CREATE UNIQUE INDEX clinical_registry_records_pkey ON public.clinical_registry_records USING btree (id)", + "name": "clinical_registry_records_pkey", + "table": "clinical_registry_records", + "def_hash": "26fa0d61d1d9125e8c2d9c8c62d461f0" + }, + { + "def": "CREATE UNIQUE INDEX differential_records_owner_id_kind_slug_key ON public.differential_records USING btree (owner_id, kind, slug)", + "name": "differential_records_owner_id_kind_slug_key", + "table": "differential_records", + "def_hash": "024cd1459a2d48df16e527d547a8561c" + }, + { + "def": "CREATE INDEX differential_records_owner_kind_title_idx ON public.differential_records USING btree (owner_id, kind, title)", + "name": "differential_records_owner_kind_title_idx", + "table": "differential_records", + "def_hash": "101b09c5ffc2b9cbb9a5a89fefa4ddf8" + }, + { + "def": "CREATE INDEX differential_records_owner_status_idx ON public.differential_records USING btree (owner_id, status)", + "name": "differential_records_owner_status_idx", + "table": "differential_records", + "def_hash": "d9fa9617991e5970c4fed3de3dfdec4e" + }, + { + "def": "CREATE UNIQUE INDEX differential_records_pkey ON public.differential_records USING btree (id)", + "name": "differential_records_pkey", + "table": "differential_records", + "def_hash": "4b955d1f51f3978acf64eb58d2eb4847" + }, + { + "def": "CREATE INDEX document_chunks_anchor_idx ON public.document_chunks USING btree (document_id, anchor_id) WHERE (anchor_id IS NOT NULL)", + "name": "document_chunks_anchor_idx", + "table": "document_chunks", + "def_hash": "fa0ae4163a0b93cec394d0bc2e5c18c0" + }, + { + "def": "CREATE INDEX document_chunks_content_hash_idx ON public.document_chunks USING btree (document_id, content_hash)", + "name": "document_chunks_content_hash_idx", + "table": "document_chunks", + "def_hash": "13195be1032c9dca09220a12dae0f1cd" + }, + { + "def": "CREATE INDEX document_chunks_content_trgm_idx ON public.document_chunks USING gin (lower(((COALESCE(section_heading, ''::text) || ' '::text) || COALESCE(content, ''::text))) extensions.gin_trgm_ops)", + "name": "document_chunks_content_trgm_idx", + "table": "document_chunks", + "def_hash": "8499c3d35fc205ab0f7237031eee7926" + }, + { + "def": "CREATE UNIQUE INDEX document_chunks_document_generation_chunk_idx ON public.document_chunks USING btree (document_id, index_generation_id, chunk_index) WHERE (index_generation_id IS NOT NULL)", + "name": "document_chunks_document_generation_chunk_idx", + "table": "document_chunks", + "def_hash": "6116ee95152dd5c594d9f818373a1304" + }, + { + "def": "CREATE INDEX document_chunks_document_idx ON public.document_chunks USING btree (document_id, chunk_index)", + "name": "document_chunks_document_idx", + "table": "document_chunks", + "def_hash": "14066ade8829223e86a9be65aa44bbda" + }, + { + "def": "CREATE INDEX document_chunks_embedding_hnsw_idx ON public.document_chunks USING hnsw (embedding extensions.vector_cosine_ops) WITH (m='24', ef_construction='128')", + "name": "document_chunks_embedding_hnsw_idx", + "table": "document_chunks", + "def_hash": "3a9d9a81ecbb13cd76a0dc360f078e9d" + }, + { + "def": "CREATE INDEX document_chunks_generation_idx ON public.document_chunks USING btree (document_id, index_generation_id)", + "name": "document_chunks_generation_idx", + "table": "document_chunks", + "def_hash": "f7eff752fa5a51527c43c69672bfa498" + }, + { + "def": "CREATE UNIQUE INDEX document_chunks_pkey ON public.document_chunks USING btree (id)", + "name": "document_chunks_pkey", + "table": "document_chunks", + "def_hash": "553b713d2276b9b8004de2904f7e840e" + }, + { + "def": "CREATE INDEX document_chunks_search_idx ON public.document_chunks USING gin (search_tsv)", + "name": "document_chunks_search_idx", + "table": "document_chunks", + "def_hash": "89b1e9c636e8e3228a1bf4bcb416a931" + }, + { + "def": "CREATE UNIQUE INDEX document_embedding_fields_dedup_idx ON public.document_embedding_fields USING btree (document_id, source_chunk_id, field_type, content_hash)", + "name": "document_embedding_fields_dedup_idx", + "table": "document_embedding_fields", + "def_hash": "ecd71e75ddb61a863adffd5a1771778a" + }, + { + "def": "CREATE INDEX document_embedding_fields_document_idx ON public.document_embedding_fields USING btree (document_id, field_type)", + "name": "document_embedding_fields_document_idx", + "table": "document_embedding_fields", + "def_hash": "ebf10641640efe3a8ebc9f17e661eff6" + }, + { + "def": "CREATE INDEX document_embedding_fields_embedding_hnsw_idx ON public.document_embedding_fields USING hnsw (embedding extensions.vector_cosine_ops) WITH (m='24', ef_construction='128')", + "name": "document_embedding_fields_embedding_hnsw_idx", + "table": "document_embedding_fields", + "def_hash": "0a6cdb240e0efc9a89b7dc69dc99e741" + }, + { + "def": "CREATE INDEX document_embedding_fields_meta_rag_indexing_version_idx ON public.document_embedding_fields USING btree (((metadata ->> 'rag_indexing_version'::text)))", + "name": "document_embedding_fields_meta_rag_indexing_version_idx", + "table": "document_embedding_fields", + "def_hash": "5fb02e475d7061b2436b9179d097ab18" + }, + { + "def": "CREATE INDEX document_embedding_fields_owner_chunk_idx ON public.document_embedding_fields USING btree (owner_id, source_chunk_id) WHERE (source_chunk_id IS NOT NULL)", + "name": "document_embedding_fields_owner_chunk_idx", + "table": "document_embedding_fields", + "def_hash": "b14ba6222b4e8d98d6b4f1b077053517" + }, + { + "def": "CREATE INDEX document_embedding_fields_owner_document_created_idx ON public.document_embedding_fields USING btree (owner_id, document_id, created_at DESC)", + "name": "document_embedding_fields_owner_document_created_idx", + "table": "document_embedding_fields", + "def_hash": "7bb98cc6be1af5cb34eed50d8625264c" + }, + { + "def": "CREATE INDEX document_embedding_fields_owner_id_idx ON public.document_embedding_fields USING btree (owner_id)", + "name": "document_embedding_fields_owner_id_idx", + "table": "document_embedding_fields", + "def_hash": "ad61100a6b49f06e2ffde2c04eb68d9a" + }, + { + "def": "CREATE UNIQUE INDEX document_embedding_fields_pkey ON public.document_embedding_fields USING btree (id)", + "name": "document_embedding_fields_pkey", + "table": "document_embedding_fields", + "def_hash": "705d33fdf9c9ee6e2919bf41a5ea73b5" + }, + { + "def": "CREATE INDEX document_embedding_fields_search_tsv_chunk_gin_idx ON public.document_embedding_fields USING gin (search_tsv) WHERE (source_chunk_id IS NOT NULL)", + "name": "document_embedding_fields_search_tsv_chunk_gin_idx", + "table": "document_embedding_fields", + "def_hash": "56b2a3ce845ae13bfeb0551906cf9581" + }, + { + "def": "CREATE INDEX document_embedding_fields_source_chunk_id_idx ON public.document_embedding_fields USING btree (source_chunk_id)", + "name": "document_embedding_fields_source_chunk_id_idx", + "table": "document_embedding_fields", + "def_hash": "7224a35b50c37a7394210ca930e4e99f" + }, + { + "def": "CREATE INDEX document_images_document_idx ON public.document_images USING btree (document_id, page_number)", + "name": "document_images_document_idx", + "table": "document_images", + "def_hash": "941d83214261706d52f69051f9e5b356" + }, + { + "def": "CREATE INDEX document_images_hash_idx ON public.document_images USING btree (document_id, image_hash) WHERE (image_hash IS NOT NULL)", + "name": "document_images_hash_idx", + "table": "document_images", + "def_hash": "ff6e67c25d1526171a59251371910c2d" + }, + { + "def": "CREATE UNIQUE INDEX document_images_pkey ON public.document_images USING btree (id)", + "name": "document_images_pkey", + "table": "document_images", + "def_hash": "cc6e5155f2d486e97a4f8622f6796929" + }, + { + "def": "CREATE INDEX document_images_searchable_idx ON public.document_images USING btree (document_id, searchable, image_type, page_number)", + "name": "document_images_searchable_idx", + "table": "document_images", + "def_hash": "e135802078cd6122e02c5f462c5ed4cb" + }, + { + "def": "CREATE INDEX document_images_structured_profile_gin_idx ON public.document_images USING gin (((metadata -> 'structured_visual_profile'::text)))", + "name": "document_images_structured_profile_gin_idx", + "table": "document_images", + "def_hash": "bc60d8d940ac4489c6328de2dd566a87" + }, + { + "def": "CREATE INDEX document_images_visual_family_idx ON public.document_images USING btree (((metadata ->> 'visual_family_id'::text))) WHERE (metadata ? 'visual_family_id'::text)", + "name": "document_images_visual_family_idx", + "table": "document_images", + "def_hash": "bdd30d6fe65c8ef21439bfb591cd1807" + }, + { + "def": "CREATE INDEX document_images_visual_intelligence_version_idx ON public.document_images USING btree (((metadata ->> 'visual_intelligence_version'::text))) WHERE (metadata ? 'visual_intelligence_version'::text)", + "name": "document_images_visual_intelligence_version_idx", + "table": "document_images", + "def_hash": "064a542af2b97dc915fcc29e9a76e6c3" + }, + { + "def": "CREATE INDEX document_index_quality_owner_score_idx ON public.document_index_quality USING btree (owner_id, quality_score, updated_at DESC)", + "name": "document_index_quality_owner_score_idx", + "table": "document_index_quality", + "def_hash": "56c102e84b93346b576cb2f0118d943a" + }, + { + "def": "CREATE UNIQUE INDEX document_index_quality_pkey ON public.document_index_quality USING btree (document_id)", + "name": "document_index_quality_pkey", + "table": "document_index_quality", + "def_hash": "0dbac2b24f340ffbbdb7f668419581d1" + }, + { + "def": "CREATE INDEX document_index_units_chunk_idx ON public.document_index_units USING btree (source_chunk_id) WHERE (source_chunk_id IS NOT NULL)", + "name": "document_index_units_chunk_idx", + "table": "document_index_units", + "def_hash": "23177769dc083936f9f6b5d42be88228" + }, + { + "def": "CREATE INDEX document_index_units_document_idx ON public.document_index_units USING btree (document_id, unit_type, page_start)", + "name": "document_index_units_document_idx", + "table": "document_index_units", + "def_hash": "976f99088d6eafd9be7bb49eb9aa301b" + }, + { + "def": "CREATE INDEX document_index_units_heading_path_idx ON public.document_index_units USING gin (heading_path)", + "name": "document_index_units_heading_path_idx", + "table": "document_index_units", + "def_hash": "d0cf5b9b5b4408a3dc8d097a11baf8af" + }, + { + "def": "CREATE INDEX document_index_units_image_idx ON public.document_index_units USING btree (source_image_id) WHERE (source_image_id IS NOT NULL)", + "name": "document_index_units_image_idx", + "table": "document_index_units", + "def_hash": "0f08af96bb917dfbaee4901e1e5e69b3" + }, + { + "def": "CREATE INDEX document_index_units_owner_chunk_type_idx ON public.document_index_units USING btree (owner_id, source_chunk_id, unit_type) WHERE (source_chunk_id IS NOT NULL)", + "name": "document_index_units_owner_chunk_type_idx", + "table": "document_index_units", + "def_hash": "2acbcf265eed582aeada506d20f1a2c9" + }, + { + "def": "CREATE INDEX document_index_units_owner_type_idx ON public.document_index_units USING btree (owner_id, unit_type, quality_score DESC)", + "name": "document_index_units_owner_type_idx", + "table": "document_index_units", + "def_hash": "4cdc73e0751cb72c2d85b56cea452da2" + }, + { + "def": "CREATE UNIQUE INDEX document_index_units_pkey ON public.document_index_units USING btree (id)", + "name": "document_index_units_pkey", + "table": "document_index_units", + "def_hash": "4c6a54693a5fb959b1b58355ba5351f6" + }, + { + "def": "CREATE INDEX document_index_units_search_idx ON public.document_index_units USING gin (search_tsv)", + "name": "document_index_units_search_idx", + "table": "document_index_units", + "def_hash": "2daf014bbe1ddcaabbf04fd4e59aa1dd" + }, + { + "def": "CREATE INDEX document_index_units_terms_idx ON public.document_index_units USING gin (normalized_terms)", + "name": "document_index_units_terms_idx", + "table": "document_index_units", + "def_hash": "6b46846921b58c36f7acee4cd14e070b" + }, + { + "def": "CREATE UNIQUE INDEX document_labels_document_id_label_type_label_source_key ON public.document_labels USING btree (document_id, label_type, label, source)", + "name": "document_labels_document_id_label_type_label_source_key", + "table": "document_labels", + "def_hash": "47911d4600fb8d3525260c096c441851" + }, + { + "def": "CREATE INDEX document_labels_document_idx ON public.document_labels USING btree (document_id)", + "name": "document_labels_document_idx", + "table": "document_labels", + "def_hash": "8e2e1a20127690fc37d52b25d7279b84" + }, + { + "def": "CREATE INDEX document_labels_label_trgm_idx ON public.document_labels USING gin (lower(label) extensions.gin_trgm_ops)", + "name": "document_labels_label_trgm_idx", + "table": "document_labels", + "def_hash": "3eacf182331d8ecd11758ffb1398e2c9" + }, + { + "def": "CREATE INDEX document_labels_owner_label_idx ON public.document_labels USING btree (owner_id, label_type, label)", + "name": "document_labels_owner_label_idx", + "table": "document_labels", + "def_hash": "9679c4053024ed4fd6928f8b4399ce49" + }, + { + "def": "CREATE UNIQUE INDEX document_labels_pkey ON public.document_labels USING btree (id)", + "name": "document_labels_pkey", + "table": "document_labels", + "def_hash": "be7f08f94762e5e0f938a81897438627" + }, + { + "def": "CREATE INDEX document_memory_cards_document_idx ON public.document_memory_cards USING btree (document_id, card_type, confidence DESC)", + "name": "document_memory_cards_document_idx", + "table": "document_memory_cards", + "def_hash": "f8e3582f7ad3500ab36e90674f2b2a98" + }, + { + "def": "CREATE INDEX document_memory_cards_embedding_hnsw_idx ON public.document_memory_cards USING hnsw (embedding extensions.vector_cosine_ops) WITH (m='24', ef_construction='128')", + "name": "document_memory_cards_embedding_hnsw_idx", + "table": "document_memory_cards", + "def_hash": "0a6948e68c1aae1d77c582936677e584" + }, + { + "def": "CREATE INDEX document_memory_cards_owner_idx ON public.document_memory_cards USING btree (owner_id)", + "name": "document_memory_cards_owner_idx", + "table": "document_memory_cards", + "def_hash": "fc5b738d14a2a090de2b50e0ccad2bd7" + }, + { + "def": "CREATE UNIQUE INDEX document_memory_cards_pkey ON public.document_memory_cards USING btree (id)", + "name": "document_memory_cards_pkey", + "table": "document_memory_cards", + "def_hash": "e0de0793827f87bd1bb61deb52974a05" + }, + { + "def": "CREATE INDEX document_memory_cards_search_idx ON public.document_memory_cards USING gin (search_tsv)", + "name": "document_memory_cards_search_idx", + "table": "document_memory_cards", + "def_hash": "602a71024fb4de99ef9812e4cc4bfbb8" + }, + { + "def": "CREATE INDEX document_memory_cards_section_idx ON public.document_memory_cards USING btree (section_id)", + "name": "document_memory_cards_section_idx", + "table": "document_memory_cards", + "def_hash": "a9823a094fa75f73873a28b83611c715" + }, + { + "def": "CREATE UNIQUE INDEX document_pages_document_id_page_number_key ON public.document_pages USING btree (document_id, page_number)", + "name": "document_pages_document_id_page_number_key", + "table": "document_pages", + "def_hash": "c90188cffdaedb69077ee5a4a78391bd" + }, + { + "def": "CREATE INDEX document_pages_document_idx ON public.document_pages USING btree (document_id, page_number)", + "name": "document_pages_document_idx", + "table": "document_pages", + "def_hash": "df247583a4c786241b9ba10919079743" + }, + { + "def": "CREATE UNIQUE INDEX document_pages_pkey ON public.document_pages USING btree (id)", + "name": "document_pages_pkey", + "table": "document_pages", + "def_hash": "001064d4f85791f720b56615ca14500f" + }, + { + "def": "CREATE UNIQUE INDEX document_sections_document_id_section_index_key ON public.document_sections USING btree (document_id, section_index)", + "name": "document_sections_document_id_section_index_key", + "table": "document_sections", + "def_hash": "a252b6bf70b1aca1064dba5c1119db7e" + }, + { + "def": "CREATE INDEX document_sections_document_idx ON public.document_sections USING btree (document_id, section_index)", + "name": "document_sections_document_idx", + "table": "document_sections", + "def_hash": "3c347411100782cc0952fdc3272c5e53" + }, + { + "def": "CREATE INDEX document_sections_owner_idx ON public.document_sections USING btree (owner_id)", + "name": "document_sections_owner_idx", + "table": "document_sections", + "def_hash": "4470729ff7cd7d2d7938797c8711330d" + }, + { + "def": "CREATE UNIQUE INDEX document_sections_pkey ON public.document_sections USING btree (id)", + "name": "document_sections_pkey", + "table": "document_sections", + "def_hash": "62d2a40c615f8108365c1afb74c20c79" + }, + { + "def": "CREATE UNIQUE INDEX document_summaries_document_id_key ON public.document_summaries USING btree (document_id)", + "name": "document_summaries_document_id_key", + "table": "document_summaries", + "def_hash": "b2c18ea1e491a079cd63f903d5927e83" + }, + { + "def": "CREATE INDEX document_summaries_owner_idx ON public.document_summaries USING btree (owner_id, generated_at DESC)", + "name": "document_summaries_owner_idx", + "table": "document_summaries", + "def_hash": "3e0a68ce251082f197b0b5b40ecc7134" + }, + { + "def": "CREATE UNIQUE INDEX document_summaries_pkey ON public.document_summaries USING btree (id)", + "name": "document_summaries_pkey", + "table": "document_summaries", + "def_hash": "7d04ba9df73358e5ddf279f0c77e745a" + }, + { + "def": "CREATE INDEX document_summaries_summary_trgm_idx ON public.document_summaries USING gin (lower(summary) extensions.gin_trgm_ops)", + "name": "document_summaries_summary_trgm_idx", + "table": "document_summaries", + "def_hash": "762d08bf158b5b665adf9c28750f2b9e" + }, + { + "def": "CREATE INDEX document_table_facts_chunk_idx ON public.document_table_facts USING btree (source_chunk_id)", + "name": "document_table_facts_chunk_idx", + "table": "document_table_facts", + "def_hash": "d9190255ba83d0bed31aafdede595e13" + }, + { + "def": "CREATE INDEX document_table_facts_document_idx ON public.document_table_facts USING btree (document_id, page_number)", + "name": "document_table_facts_document_idx", + "table": "document_table_facts", + "def_hash": "6b37b2211fb43f76ee4168691cba9390" + }, + { + "def": "CREATE INDEX document_table_facts_owner_document_page_idx ON public.document_table_facts USING btree (owner_id, document_id, page_number)", + "name": "document_table_facts_owner_document_page_idx", + "table": "document_table_facts", + "def_hash": "41f89a792a3a64f3a6109797e6b40d3b" + }, + { + "def": "CREATE UNIQUE INDEX document_table_facts_pkey ON public.document_table_facts USING btree (id)", + "name": "document_table_facts_pkey", + "table": "document_table_facts", + "def_hash": "fee27723dddf229414eec98a0a089798" + }, + { + "def": "CREATE INDEX document_table_facts_search_idx ON public.document_table_facts USING gin (search_tsv)", + "name": "document_table_facts_search_idx", + "table": "document_table_facts", + "def_hash": "6782d21fd03cceefbf932d34f9e17055" + }, + { + "def": "CREATE INDEX document_table_facts_source_image_idx ON public.document_table_facts USING btree (source_image_id) WHERE (source_image_id IS NOT NULL)", + "name": "document_table_facts_source_image_idx", + "table": "document_table_facts", + "def_hash": "4b079b113437859bf55481c4b72d6c64" + }, + { + "def": "CREATE INDEX document_table_facts_terms_idx ON public.document_table_facts USING gin (normalized_terms)", + "name": "document_table_facts_terms_idx", + "table": "document_table_facts", + "def_hash": "42127721dc292103a0118c33e1ba1034" + }, + { + "def": "CREATE INDEX documents_import_batch_idx ON public.documents USING btree (import_batch_id)", + "name": "documents_import_batch_idx", + "table": "documents", + "def_hash": "4a6662ef10dadbb00f4a0f2339e76ba2" + }, + { + "def": "CREATE INDEX documents_indexed_owner_title_idx ON public.documents USING btree (owner_id, title, file_name) WHERE (status = 'indexed'::text)", + "name": "documents_indexed_owner_title_idx", + "table": "documents", + "def_hash": "ec13cdd0bda40b9017955b13e9953c58" + }, + { + "def": "CREATE INDEX documents_indexing_v3_agent_claim_idx ON public.documents USING btree (status, ((metadata ->> 'enrichment_status'::text)), ((metadata ->> 'indexing_v3_agent_status'::text)), updated_at) WHERE (status = 'indexed'::text)", + "name": "documents_indexing_v3_agent_claim_idx", + "table": "documents", + "def_hash": "b3abff568c5d1e40e11e9be33afd95b5" + }, + { + "def": "CREATE UNIQUE INDEX documents_owner_content_hash_unique_idx ON public.documents USING btree (owner_id, content_hash) WHERE (content_hash IS NOT NULL)", + "name": "documents_owner_content_hash_unique_idx", + "table": "documents", + "def_hash": "6665e1d7d3bd81dc44e1a39292f35795" + }, + { + "def": "CREATE INDEX documents_owner_id_covering_idx ON public.documents USING btree (owner_id, id)", + "name": "documents_owner_id_covering_idx", + "table": "documents", + "def_hash": "937cafe05081646a043bf934476ab765" + }, + { + "def": "CREATE INDEX documents_owner_status_idx ON public.documents USING btree (owner_id, status, created_at DESC)", + "name": "documents_owner_status_idx", + "table": "documents", + "def_hash": "d516bc49b5f81bb31779e01d3d52c11b" + }, + { + "def": "CREATE UNIQUE INDEX documents_pkey ON public.documents USING btree (id)", + "name": "documents_pkey", + "table": "documents", + "def_hash": "fc27ae2194373897e2f316e1cac47fb0" + }, + { + "def": "CREATE INDEX documents_search_idx ON public.documents USING gin (search_tsv)", + "name": "documents_search_idx", + "table": "documents", + "def_hash": "56536f1f9d165504f5295f8015529a7a" + }, + { + "def": "CREATE INDEX documents_status_idx ON public.documents USING btree (status)", + "name": "documents_status_idx", + "table": "documents", + "def_hash": "7dd1181f3cb8eedf8bd8d9dd779a77ae" + }, + { + "def": "CREATE INDEX documents_title_search_idx ON public.documents USING gin (title_search_tsv)", + "name": "documents_title_search_idx", + "table": "documents", + "def_hash": "bd1f1fb12db61dbeaba23e5a29ad3f41" + }, + { + "def": "CREATE INDEX documents_title_trgm_idx ON public.documents USING gin (lower(((COALESCE(title, ''::text) || ' '::text) || COALESCE(file_name, ''::text))) extensions.gin_trgm_ops)", + "name": "documents_title_trgm_idx", + "table": "documents", + "def_hash": "27f29c956a531b8638e9d1b6a3ae0f5f" + }, + { + "def": "CREATE INDEX image_caption_cache_owner_hash_idx ON public.image_caption_cache USING btree (owner_id, image_hash, model)", + "name": "image_caption_cache_owner_hash_idx", + "table": "image_caption_cache", + "def_hash": "2f2c90dfac660eb2a30bd869f686e665" + }, + { + "def": "CREATE UNIQUE INDEX image_caption_cache_owner_id_image_hash_model_key ON public.image_caption_cache USING btree (owner_id, image_hash, model)", + "name": "image_caption_cache_owner_id_image_hash_model_key", + "table": "image_caption_cache", + "def_hash": "5cac039926e94b8e4891b3085e7f8f6e" + }, + { + "def": "CREATE UNIQUE INDEX image_caption_cache_pkey ON public.image_caption_cache USING btree (id)", + "name": "image_caption_cache_pkey", + "table": "image_caption_cache", + "def_hash": "7c01c4b34e63a2ef7adf4d309c9e2a20" + }, + { + "def": "CREATE INDEX import_batches_owner_status_idx ON public.import_batches USING btree (owner_id, status, created_at DESC)", + "name": "import_batches_owner_status_idx", + "table": "import_batches", + "def_hash": "b7f97a3906cb3764d34546ea332e8fa0" + }, + { + "def": "CREATE UNIQUE INDEX import_batches_pkey ON public.import_batches USING btree (id)", + "name": "import_batches_pkey", + "table": "import_batches", + "def_hash": "2f1309abe57d462bdc48f0ce22f730da" + }, + { + "def": "CREATE INDEX import_batches_status_created_idx ON public.import_batches USING btree (status, created_at DESC) WHERE (status = ANY (ARRAY['queued'::text, 'processing'::text]))", + "name": "import_batches_status_created_idx", + "table": "import_batches", + "def_hash": "4ec180a167729027301de12255e241a5" + }, + { + "def": "CREATE INDEX indexing_v3_agent_jobs_claim_idx ON public.indexing_v3_agent_jobs USING btree (status, enrichment_status, next_run_at, id) WHERE (status <> ALL (ARRAY['completed'::text, 'needs_enrichment_artifacts'::text]))", + "name": "indexing_v3_agent_jobs_claim_idx", + "table": "indexing_v3_agent_jobs", + "def_hash": "219fdfd973e97366c02f17ad8c2bb535" + }, + { + "def": "CREATE UNIQUE INDEX indexing_v3_agent_jobs_document_id_idx ON public.indexing_v3_agent_jobs USING btree (document_id)", + "name": "indexing_v3_agent_jobs_document_id_idx", + "table": "indexing_v3_agent_jobs", + "def_hash": "05c68de8272e8044b5a85fe094168520" + }, + { + "def": "CREATE INDEX indexing_v3_agent_jobs_locked_at_idx ON public.indexing_v3_agent_jobs USING btree (locked_at) WHERE (status = 'processing'::text)", + "name": "indexing_v3_agent_jobs_locked_at_idx", + "table": "indexing_v3_agent_jobs", + "def_hash": "c2b20b3bc77be8e5e5e9232fb4020c53" + }, + { + "def": "CREATE UNIQUE INDEX indexing_v3_agent_jobs_pkey ON public.indexing_v3_agent_jobs USING btree (id)", + "name": "indexing_v3_agent_jobs_pkey", + "table": "indexing_v3_agent_jobs", + "def_hash": "5992fd30161e00aefe35a78f83c4606d" + }, + { + "def": "CREATE INDEX ingestion_job_stages_document_started_idx ON public.ingestion_job_stages USING btree (document_id, started_at DESC)", + "name": "ingestion_job_stages_document_started_idx", + "table": "ingestion_job_stages", + "def_hash": "424bd9e873cff104a27de3143a5038aa" + }, + { + "def": "CREATE INDEX ingestion_job_stages_job_stage_started_idx ON public.ingestion_job_stages USING btree (job_id, stage_name, started_at DESC)", + "name": "ingestion_job_stages_job_stage_started_idx", + "table": "ingestion_job_stages", + "def_hash": "e5c9d22411f0019fced7e55a4e96dfb7" + }, + { + "def": "CREATE UNIQUE INDEX ingestion_job_stages_pkey ON public.ingestion_job_stages USING btree (id)", + "name": "ingestion_job_stages_pkey", + "table": "ingestion_job_stages", + "def_hash": "b9d832809f61a3ab40c3d3892fdf8a06" + }, + { + "def": "CREATE INDEX ingestion_jobs_batch_idx ON public.ingestion_jobs USING btree (batch_id, status)", + "name": "ingestion_jobs_batch_idx", + "table": "ingestion_jobs", + "def_hash": "c0c15d138628cceb2dd8e9d22f4241a8" + }, + { + "def": "CREATE INDEX ingestion_jobs_document_idx ON public.ingestion_jobs USING btree (document_id)", + "name": "ingestion_jobs_document_idx", + "table": "ingestion_jobs", + "def_hash": "71b6a732fcff818b5c08776a41c59a27" + }, + { + "def": "CREATE INDEX ingestion_jobs_document_status_idx ON public.ingestion_jobs USING btree (document_id, status, created_at)", + "name": "ingestion_jobs_document_status_idx", + "table": "ingestion_jobs", + "def_hash": "a59d61034f2cb9337c13283574b698d9" + }, + { + "def": "CREATE UNIQUE INDEX ingestion_jobs_pkey ON public.ingestion_jobs USING btree (id)", + "name": "ingestion_jobs_pkey", + "table": "ingestion_jobs", + "def_hash": "c698e7813ec7c23f9176072848ddfaa4" + }, + { + "def": "CREATE INDEX ingestion_jobs_status_next_run_idx ON public.ingestion_jobs USING btree (status, next_run_at, created_at) WHERE (status = ANY (ARRAY['pending'::text, 'processing'::text, 'failed'::text]))", + "name": "ingestion_jobs_status_next_run_idx", + "table": "ingestion_jobs", + "def_hash": "5b76e9007454fd9118ca073fb122f205" + }, + { + "def": "CREATE INDEX medication_records_owner_category_idx ON public.medication_records USING btree (owner_id, category)", + "name": "medication_records_owner_category_idx", + "table": "medication_records", + "def_hash": "a2397ec35f6e960a437da673b6ab0c4c" + }, + { + "def": "CREATE UNIQUE INDEX medication_records_owner_id_slug_key ON public.medication_records USING btree (owner_id, slug)", + "name": "medication_records_owner_id_slug_key", + "table": "medication_records", + "def_hash": "583d0cdb7fb794a50994c61ba5733575" + }, + { + "def": "CREATE INDEX medication_records_owner_name_idx ON public.medication_records USING btree (owner_id, name)", + "name": "medication_records_owner_name_idx", + "table": "medication_records", + "def_hash": "102bd86663cca144ab5f5f65d4e5dbad" + }, + { + "def": "CREATE INDEX medication_records_owner_schedule_idx ON public.medication_records USING btree (owner_id, schedule)", + "name": "medication_records_owner_schedule_idx", + "table": "medication_records", + "def_hash": "6289fa704649f49b81a1a4ddcaadb5da" + }, + { + "def": "CREATE UNIQUE INDEX medication_records_pkey ON public.medication_records USING btree (id)", + "name": "medication_records_pkey", + "table": "medication_records", + "def_hash": "834158198dd0ccb889275cfe43b7ca09" + }, + { + "def": "CREATE INDEX rag_aliases_alias_trgm_idx ON public.rag_aliases USING gin (lower(alias) extensions.gin_trgm_ops)", + "name": "rag_aliases_alias_trgm_idx", + "table": "rag_aliases", + "def_hash": "de4c954f6260a846824662bdc55ea171" + }, + { + "def": "CREATE INDEX rag_aliases_owner_enabled_idx ON public.rag_aliases USING btree (owner_id, enabled)", + "name": "rag_aliases_owner_enabled_idx", + "table": "rag_aliases", + "def_hash": "6185aa062f46ec93e759afbbf23bd537" + }, + { + "def": "CREATE UNIQUE INDEX rag_aliases_pkey ON public.rag_aliases USING btree (id)", + "name": "rag_aliases_pkey", + "table": "rag_aliases", + "def_hash": "35493f328b43459ef69952195743ad48" + }, + { + "def": "CREATE INDEX rag_aliases_type_enabled_idx ON public.rag_aliases USING btree (alias_type, enabled)", + "name": "rag_aliases_type_enabled_idx", + "table": "rag_aliases", + "def_hash": "7cc5a5a8c70475e51a21636802aa00fd" + }, + { + "def": "CREATE INDEX rag_queries_owner_idx ON public.rag_queries USING btree (owner_id, created_at DESC)", + "name": "rag_queries_owner_idx", + "table": "rag_queries", + "def_hash": "4f59cb5f71bfb4941bfa752248a41671" + }, + { + "def": "CREATE UNIQUE INDEX rag_queries_pkey ON public.rag_queries USING btree (id)", + "name": "rag_queries_pkey", + "table": "rag_queries", + "def_hash": "d05242aafc6802f90b57881081d21751" + }, + { + "def": "CREATE INDEX rag_queries_source_chunk_ids_gin_idx ON public.rag_queries USING gin (source_chunk_ids)", + "name": "rag_queries_source_chunk_ids_gin_idx", + "table": "rag_queries", + "def_hash": "bdb4f291551d7a920df1a311941e3abb" + }, + { + "def": "CREATE INDEX rag_query_misses_aliases_idx ON public.rag_query_misses USING gin (candidate_aliases)", + "name": "rag_query_misses_aliases_idx", + "table": "rag_query_misses", + "def_hash": "7c4309538ddcc2c8464eaece0bd8b843" + }, + { + "def": "CREATE INDEX rag_query_misses_normalized_idx ON public.rag_query_misses USING btree (normalized_query, created_at DESC)", + "name": "rag_query_misses_normalized_idx", + "table": "rag_query_misses", + "def_hash": "95691ef1b9e9050ca5d080b0aabb4d14" + }, + { + "def": "CREATE INDEX rag_query_misses_owner_created_idx ON public.rag_query_misses USING btree (owner_id, created_at DESC)", + "name": "rag_query_misses_owner_created_idx", + "table": "rag_query_misses", + "def_hash": "e032e4e4da4f4f2d9422456a6e1c5bc0" + }, + { + "def": "CREATE INDEX rag_query_misses_owner_review_status_created_idx ON public.rag_query_misses USING btree (owner_id, review_status, created_at DESC)", + "name": "rag_query_misses_owner_review_status_created_idx", + "table": "rag_query_misses", + "def_hash": "dbd0a3622d35bf159fce232deaba7c3e" + }, + { + "def": "CREATE UNIQUE INDEX rag_query_misses_pkey ON public.rag_query_misses USING btree (id)", + "name": "rag_query_misses_pkey", + "table": "rag_query_misses", + "def_hash": "bd9276b39a97541ae59e868a653688b1" + }, + { + "def": "CREATE INDEX rag_response_cache_expiry_idx ON public.rag_response_cache USING btree (expires_at)", + "name": "rag_response_cache_expiry_idx", + "table": "rag_response_cache", + "def_hash": "af941f7ea89e501d90e09e8d838168c0" + }, + { + "def": "CREATE UNIQUE INDEX rag_response_cache_key_idx ON public.rag_response_cache USING btree (COALESCE(owner_id, '00000000-0000-0000-0000-000000000000'::uuid), cache_kind, scope_key, normalized_query, indexing_version, dependency_version)", + "name": "rag_response_cache_key_idx", + "table": "rag_response_cache", + "def_hash": "3bdfc874700c8efd2d564eaa1cbf9a76" + }, + { + "def": "CREATE INDEX rag_response_cache_owner_kind_idx ON public.rag_response_cache USING btree (owner_id, cache_kind, updated_at DESC)", + "name": "rag_response_cache_owner_kind_idx", + "table": "rag_response_cache", + "def_hash": "903e1e073e5f700541d355083882611c" + }, + { + "def": "CREATE UNIQUE INDEX rag_response_cache_pkey ON public.rag_response_cache USING btree (id)", + "name": "rag_response_cache_pkey", + "table": "rag_response_cache", + "def_hash": "4e3b5ddcea7a70e4ad9a32c1953448e6" + }, + { + "def": "CREATE INDEX rag_retrieval_logs_miss_idx ON public.rag_retrieval_logs USING btree (is_miss, created_at DESC) WHERE (is_miss = true)", + "name": "rag_retrieval_logs_miss_idx", + "table": "rag_retrieval_logs", + "def_hash": "c12f85fde2e26a15b104a6f4632747c4" + }, + { + "def": "CREATE INDEX rag_retrieval_logs_owner_created_idx ON public.rag_retrieval_logs USING btree (owner_id, created_at DESC)", + "name": "rag_retrieval_logs_owner_created_idx", + "table": "rag_retrieval_logs", + "def_hash": "8505a0b9980a57e701259db684d5b40b" + }, + { + "def": "CREATE UNIQUE INDEX rag_retrieval_logs_pkey ON public.rag_retrieval_logs USING btree (id)", + "name": "rag_retrieval_logs_pkey", + "table": "rag_retrieval_logs", + "def_hash": "2394384241d796e555b946b043d9c17e" + }, + { + "def": "CREATE INDEX rag_retrieval_logs_strategy_idx ON public.rag_retrieval_logs USING btree (retrieval_strategy, created_at DESC)", + "name": "rag_retrieval_logs_strategy_idx", + "table": "rag_retrieval_logs", + "def_hash": "a0feef2fe90b93f14797484a140a7139" + }, + { + "def": "CREATE INDEX rag_visual_eval_cases_doc_idx ON public.rag_visual_eval_cases USING btree (document_id, active)", + "name": "rag_visual_eval_cases_doc_idx", + "table": "rag_visual_eval_cases", + "def_hash": "65293ca211bddfe03e56a381c3037279" + }, + { + "def": "CREATE INDEX rag_visual_eval_cases_owner_id_idx ON public.rag_visual_eval_cases USING btree (owner_id)", + "name": "rag_visual_eval_cases_owner_id_idx", + "table": "rag_visual_eval_cases", + "def_hash": "00d0940db664fd4f5dc68687cf8e2c68" + }, + { + "def": "CREATE UNIQUE INDEX rag_visual_eval_cases_pkey ON public.rag_visual_eval_cases USING btree (id)", + "name": "rag_visual_eval_cases_pkey", + "table": "rag_visual_eval_cases", + "def_hash": "3b355999eb8cf1871408805cf5f8160d" + }, + { + "def": "CREATE INDEX rag_visual_eval_runs_case_id_idx ON public.rag_visual_eval_runs USING btree (case_id)", + "name": "rag_visual_eval_runs_case_id_idx", + "table": "rag_visual_eval_runs", + "def_hash": "5d2cb8ef0473a83d260b3a2283c2b58f" + }, + { + "def": "CREATE INDEX rag_visual_eval_runs_document_id_idx ON public.rag_visual_eval_runs USING btree (document_id)", + "name": "rag_visual_eval_runs_document_id_idx", + "table": "rag_visual_eval_runs", + "def_hash": "b67cb6fedb710f4fb518825a7f5547d1" + }, + { + "def": "CREATE UNIQUE INDEX rag_visual_eval_runs_pkey ON public.rag_visual_eval_runs USING btree (id)", + "name": "rag_visual_eval_runs_pkey", + "table": "rag_visual_eval_runs", + "def_hash": "6d913e54f17c817a9863443d02b8935c" + }, + { + "def": "CREATE INDEX storage_cleanup_jobs_document_idx ON public.storage_cleanup_jobs USING btree (document_id)", + "name": "storage_cleanup_jobs_document_idx", + "table": "storage_cleanup_jobs", + "def_hash": "789f84338afb4a29cca4f9cb621c9b10" + }, + { + "def": "CREATE INDEX storage_cleanup_jobs_owner_status_idx ON public.storage_cleanup_jobs USING btree (owner_id, status, created_at DESC)", + "name": "storage_cleanup_jobs_owner_status_idx", + "table": "storage_cleanup_jobs", + "def_hash": "ccdcfbc105cce2c25a8bc8cbc1d93b01" + }, + { + "def": "CREATE UNIQUE INDEX storage_cleanup_jobs_pkey ON public.storage_cleanup_jobs USING btree (id)", + "name": "storage_cleanup_jobs_pkey", + "table": "storage_cleanup_jobs", + "def_hash": "77fff7a3fb73bfb90616664eafa2682e" + }, + { + "def": "CREATE INDEX storage_cleanup_jobs_status_created_idx ON public.storage_cleanup_jobs USING btree (status, created_at) WHERE (status = ANY (ARRAY['pending'::text, 'failed'::text]))", + "name": "storage_cleanup_jobs_status_created_idx", + "table": "storage_cleanup_jobs", + "def_hash": "2cbfd92ff37d036d37344943f57dd95b" + } + ], + "policies": [ + { + "cmd": "ALL", + "name": "api rate limit subjects service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "api_rate_limit_subjects", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "api rate limits service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "api_rate_limits", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "audit logs service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "audit_logs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "registry record sources service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "clinical_registry_record_sources", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "registry records service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "clinical_registry_records", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "differential records service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "differential_records", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "SELECT", + "name": "chunks owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_chunks.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_chunks", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "embedding fields owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_embedding_fields.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_embedding_fields", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "images owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_images.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_images", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "index quality owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "document_index_quality", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "document index units owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_index_units.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_index_units", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "DELETE", + "name": "labels owner manual delete", + "qual": "((( SELECT auth.uid() AS uid) = owner_id) AND (source = 'manual'::text))", + "roles": [ + "authenticated" + ], + "table": "document_labels", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "INSERT", + "name": "labels owner manual insert", + "qual": null, + "roles": [ + "authenticated" + ], + "table": "document_labels", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "((( SELECT auth.uid() AS uid) = owner_id) AND (source = 'manual'::text))" + }, + { + "cmd": "UPDATE", + "name": "labels owner manual update", + "qual": "((( SELECT auth.uid() AS uid) = owner_id) AND (source = 'manual'::text))", + "roles": [ + "authenticated" + ], + "table": "document_labels", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "((( SELECT auth.uid() AS uid) = owner_id) AND (source = 'manual'::text))" + }, + { + "cmd": "SELECT", + "name": "labels owner read", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "document_labels", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "document memory cards owner all", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "document_memory_cards", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "(( SELECT auth.uid() AS uid) = owner_id)" + }, + { + "cmd": "SELECT", + "name": "pages owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_pages.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_pages", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "document sections owner all", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "document_sections", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "(( SELECT auth.uid() AS uid) = owner_id)" + }, + { + "cmd": "SELECT", + "name": "summaries owner read", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "document_summaries", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "table facts owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = document_table_facts.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "document_table_facts", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "documents owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "documents", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "image caption cache owner all", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "image_caption_cache", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "(( SELECT auth.uid() AS uid) = owner_id)" + }, + { + "cmd": "SELECT", + "name": "import batches owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "import_batches", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "indexing v3 agent jobs service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "indexing_v3_agent_jobs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "ingestion job stages service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "ingestion_job_stages", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "SELECT", + "name": "jobs owner read", + "qual": "(EXISTS ( SELECT 1\n FROM public.documents d\n WHERE ((d.id = ingestion_jobs.document_id) AND (d.owner_id = ( SELECT auth.uid() AS uid)))))", + "roles": [ + "authenticated" + ], + "table": "ingestion_jobs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "medication records service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "medication_records", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "SELECT", + "name": "rag aliases owner read", + "qual": "((owner_id IS NULL) OR (owner_id = ( SELECT auth.uid() AS uid)))", + "roles": [ + "authenticated" + ], + "table": "rag_aliases", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "rag owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "rag_queries", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "rag misses owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "rag_query_misses", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "rag response cache service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "rag_response_cache", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "SELECT", + "name": "rag retrieval logs owner read", + "qual": "(owner_id = ( SELECT auth.uid() AS uid))", + "roles": [ + "authenticated" + ], + "table": "rag_retrieval_logs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "ALL", + "name": "rag visual eval cases service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "rag_visual_eval_cases", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "ALL", + "name": "rag visual eval runs service role all", + "qual": "true", + "roles": [ + "service_role" + ], + "table": "rag_visual_eval_runs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": "true" + }, + { + "cmd": "SELECT", + "name": "storage cleanup owner read", + "qual": "(( SELECT auth.uid() AS uid) = owner_id)", + "roles": [ + "authenticated" + ], + "table": "storage_cleanup_jobs", + "schema": "public", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "document storage owner read", + "qual": "((bucket_id = 'clinical-documents'::text) AND ((storage.foldername(name))[1] = (( SELECT auth.uid() AS uid))::text))", + "roles": [ + "authenticated" + ], + "table": "objects", + "schema": "storage", + "permissive": "PERMISSIVE", + "with_check": null + }, + { + "cmd": "SELECT", + "name": "image storage owner read", + "qual": "((bucket_id = 'clinical-images'::text) AND ((storage.foldername(name))[1] = (( SELECT auth.uid() AS uid))::text))", + "roles": [ + "authenticated" + ], + "table": "objects", + "schema": "storage", + "permissive": "PERMISSIVE", + "with_check": null + } + ], + "triggers": [ + { + "def": "CREATE TRIGGER clinical_registry_records_updated_at BEFORE UPDATE ON public.clinical_registry_records FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "clinical_registry_records_updated_at", + "table": "clinical_registry_records" + }, + { + "def": "CREATE TRIGGER differential_records_updated_at BEFORE UPDATE ON public.differential_records FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "differential_records_updated_at", + "table": "differential_records" + }, + { + "def": "CREATE TRIGGER set_document_embedding_field_content_hash BEFORE INSERT OR UPDATE ON public.document_embedding_fields FOR EACH ROW EXECUTE FUNCTION public.set_document_embedding_field_content_hash()", + "name": "set_document_embedding_field_content_hash", + "table": "document_embedding_fields" + }, + { + "def": "CREATE TRIGGER document_index_units_updated_at BEFORE UPDATE ON public.document_index_units FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "document_index_units_updated_at", + "table": "document_index_units" + }, + { + "def": "CREATE TRIGGER document_memory_cards_updated_at BEFORE UPDATE ON public.document_memory_cards FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "document_memory_cards_updated_at", + "table": "document_memory_cards" + }, + { + "def": "CREATE TRIGGER document_sections_updated_at BEFORE UPDATE ON public.document_sections FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "document_sections_updated_at", + "table": "document_sections" + }, + { + "def": "CREATE TRIGGER documents_updated_at BEFORE UPDATE ON public.documents FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "documents_updated_at", + "table": "documents" + }, + { + "def": "CREATE TRIGGER image_caption_cache_updated_at BEFORE UPDATE ON public.image_caption_cache FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "image_caption_cache_updated_at", + "table": "image_caption_cache" + }, + { + "def": "CREATE TRIGGER import_batches_updated_at BEFORE UPDATE ON public.import_batches FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "import_batches_updated_at", + "table": "import_batches" + }, + { + "def": "CREATE TRIGGER ingestion_jobs_updated_at BEFORE UPDATE ON public.ingestion_jobs FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "ingestion_jobs_updated_at", + "table": "ingestion_jobs" + }, + { + "def": "CREATE TRIGGER medication_records_updated_at BEFORE UPDATE ON public.medication_records FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "medication_records_updated_at", + "table": "medication_records" + }, + { + "def": "CREATE TRIGGER rag_aliases_updated_at BEFORE UPDATE ON public.rag_aliases FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "rag_aliases_updated_at", + "table": "rag_aliases" + }, + { + "def": "CREATE TRIGGER trg_set_owner_id_rag_queries BEFORE INSERT ON public.rag_queries FOR EACH ROW EXECUTE FUNCTION public.set_owner_id_from_auth_uid()", + "name": "trg_set_owner_id_rag_queries", + "table": "rag_queries" + }, + { + "def": "CREATE TRIGGER trg_set_owner_id_rag_query_misses BEFORE INSERT ON public.rag_query_misses FOR EACH ROW EXECUTE FUNCTION public.set_owner_id_from_auth_uid()", + "name": "trg_set_owner_id_rag_query_misses", + "table": "rag_query_misses" + }, + { + "def": "CREATE TRIGGER rag_response_cache_updated_at BEFORE UPDATE ON public.rag_response_cache FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "rag_response_cache_updated_at", + "table": "rag_response_cache" + }, + { + "def": "CREATE TRIGGER storage_cleanup_jobs_updated_at BEFORE UPDATE ON public.storage_cleanup_jobs FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", + "name": "storage_cleanup_jobs_updated_at", + "table": "storage_cleanup_jobs" + } + ], + "functions": [ + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "fc2b64914e236c259c192c7527a32af7", + "signature": "public.analyze_rag_tables()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "e518193f2ffad7f87ae9bcd18164e099", + "signature": "public.chunk_image_metadata(uuid[])" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "fc509a871eb928b94d7a333b345904a0", + "signature": "public.claim_indexing_v3_agent_jobs(text,integer,integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "d15147f6f3763c5642b6c4af63d50c67", + "signature": "public.claim_ingestion_jobs(text,integer,integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "7ae76db3475c957f6af5199d59fa05ff", + "signature": "public.cleanup_abandoned_document_index_generations(uuid,integer,boolean)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "b9b0836c354240f45f61ccfe40392317", + "signature": "public.commit_document_index_generation(uuid,uuid,text,integer,integer,integer,jsonb,jsonb,jsonb)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "de9b9a4cc8ee9398b23488ca5a4749b9", + "signature": "public.complete_ingestion_job(uuid,uuid,uuid,text)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "48a83471e1ce7a272cb81cfe6137b3ca", + "signature": "public.complete_strict_enrichment_job(uuid,uuid,text,text,text)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "c30484095942ebbcbfd087215dd7ec0a", + "signature": "public.consume_api_rate_limit(uuid,text,integer,integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "e263f3819b05177abcf2eedd3f468991", + "signature": "public.consume_api_subject_rate_limit(text,text,integer,integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "8f1094c4e0c40dfd99a87a455037a62e", + "signature": "public.correct_clinical_query_terms(text,real)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "37f32cc93a66d9ce45bdcecf263ace70", + "signature": "public.detect_legacy_ivfflat_indexes()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "143cc43d9a00f140f3da8f9c054db74a", + "signature": "public.document_label_metadata(uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "4f3e6b143eba09bf1bed09b6ec9f71b8", + "signature": "public.document_summary_text(uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "c22bbe7ec1655a2c623b43b3e74cb23b", + "signature": "public.explain_retrieval_rpc(text,text,integer,uuid,uuid[],boolean)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "bdc7c13da9993cd550a10202b2c725e5", + "signature": "public.fail_or_retry_ingestion_job(uuid,uuid,uuid,boolean,text,text,text,timestamp with time zone)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "40a59e4d265fa58bd90acd0aa6621ac6", + "signature": "public.get_related_document_metadata(uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "3ffdb174b4cef3905a3ed45e9b0dab6b", + "signature": "public.invoke_indexing_v3_agent(integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "5028a7406b7014ea8e274d29c8bbf6f9", + "signature": "public.invoke_ingestion_worker(integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "7607823e2b292756cf7295e2136dea6e", + "signature": "public.is_committed_artifact_generation(jsonb,jsonb)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "755e8ba3a7f52efcef604074d0832dde", + "signature": "public.is_committed_artifact_generation(uuid,jsonb)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "d4913081cb28b11030c854deaa55c816", + "signature": "public.is_committed_document_generation(uuid,jsonb)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "6b939af904532b02ecea756f345a641b", + "signature": "public.match_document_chunks_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "7df00b3e8e430df3a8a598fa053c7260", + "signature": "public.match_document_chunks_text(text,integer,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "be4f3ade7a4462b0a0af39f90824e0cc", + "signature": "public.match_document_chunks(extensions.vector,integer,double precision,uuid,uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "9f88829888c327466397c78e9bd71764", + "signature": "public.match_document_embedding_fields_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "48117cf83cf9221a3630e31af0c701ec", + "signature": "public.match_document_embedding_fields_text(text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "2519ccc7472de09e21370a95eae52b9a", + "signature": "public.match_document_index_units_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "2f8d5dae0fd7eb3c75b83fec12fea470", + "signature": "public.match_document_lookup_chunks_text(text,uuid[],integer,uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "0534ad140950e83128b3434caa5ffd32", + "signature": "public.match_document_memory_cards_hybrid_v2(extensions.vector,text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "630b74038e6c603cd6b03ecca3fcc602", + "signature": "public.match_document_memory_cards_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "66c996b85f233ccc73edff634f71d4d0", + "signature": "public.match_document_table_facts_text(text,integer,uuid[],uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "660de201d7d7d4d016a8f8a01e125503", + "signature": "public.match_documents_for_query(text,integer,uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "3f7a8939ee41c93b0c0126a6b1b65124", + "signature": "public.purge_expired_rag_queries(integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "ded3c523452e1483097d15a7ac8a4639", + "signature": "public.refresh_import_batch_status(uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "bac3b2ee419e9ba7c019808f0f83b3ad", + "signature": "public.repair_strict_enrichment_gate_batch(integer)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "243f3960a32db0192d1cce2ebd050004", + "signature": "public.reset_document_index(uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "2dff50fc9bc17ee3c6d71c9dcf9cee5e", + "signature": "public.retrieval_owner_matches(uuid,uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "903e3420eb672a0fb4f5e608c0b4726e", + "signature": "public.schema_drift_snapshot()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "e546ed66f9b45a3d4905cc911fcbed62", + "signature": "public.search_document_chunks(uuid,text,integer,uuid)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "f4f5f536026c4dd27d506a8e40b8c6d7", + "signature": "public.search_schema_health()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "937a3e85da84f673d32b7a8f30acc654", + "signature": "public.set_document_embedding_field_content_hash()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "fefc17c166d7a65a9816e73a14341c1a", + "signature": "public.set_owner_id_from_auth_uid()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "ed100a39682201102314634bc4ad2246", + "signature": "public.set_updated_at()" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "3b5c6084651ca0f8fd8e069106e8f7dd", + "signature": "public.stamp_document_deep_memory_version(uuid,text)" + }, + { + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "c5fafb6bc7cc303cb1199895b777d6a6", + "signature": "public.update_indexing_v3_agent_job_status(uuid,text,text,timestamp with time zone)" + } + ], + "extensions": [ + { + "name": "pg_stat_statements", + "schema": "extensions" + }, + { + "name": "pg_trgm", + "schema": "extensions" + }, + { + "name": "pgcrypto", + "schema": "extensions" + }, + { + "name": "supabase_vault", + "schema": "vault" + }, + { + "name": "uuid-ossp", + "schema": "extensions" + }, + { + "name": "vector", + "schema": "extensions" + } + ], + "constraints": [ + { + "def": "CHECK ((btrim(bucket) <> ''::text))", + "name": "api_rate_limit_subjects_bucket_nonempty", + "table": "api_rate_limit_subjects" + }, + { + "def": "PRIMARY KEY (subject_key, bucket)", + "name": "api_rate_limit_subjects_pkey", + "table": "api_rate_limit_subjects" + }, + { + "def": "CHECK ((request_count >= 0))", + "name": "api_rate_limit_subjects_request_count_check", + "table": "api_rate_limit_subjects" + }, + { + "def": "CHECK ((btrim(subject_key) <> ''::text))", + "name": "api_rate_limit_subjects_subject_key_nonempty", + "table": "api_rate_limit_subjects" + }, + { + "def": "CHECK ((btrim(bucket) <> ''::text))", + "name": "api_rate_limits_bucket_nonempty", + "table": "api_rate_limits" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "api_rate_limits_owner_id_fkey", + "table": "api_rate_limits" + }, + { + "def": "PRIMARY KEY (owner_id, bucket)", + "name": "api_rate_limits_pkey", + "table": "api_rate_limits" + }, + { + "def": "CHECK ((request_count >= 0))", + "name": "api_rate_limits_request_count_check", + "table": "api_rate_limits" + }, + { + "def": "CHECK ((btrim(action) <> ''::text))", + "name": "audit_logs_action_nonempty", + "table": "audit_logs" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "audit_logs_owner_id_fkey", + "table": "audit_logs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "audit_logs_pkey", + "table": "audit_logs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "clinical_registry_record_sources_document_id_fkey", + "table": "clinical_registry_record_sources" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "clinical_registry_record_sources_owner_id_fkey", + "table": "clinical_registry_record_sources" + }, + { + "def": "PRIMARY KEY (id)", + "name": "clinical_registry_record_sources_pkey", + "table": "clinical_registry_record_sources" + }, + { + "def": "UNIQUE (record_id, document_id)", + "name": "clinical_registry_record_sources_record_id_document_id_key", + "table": "clinical_registry_record_sources" + }, + { + "def": "FOREIGN KEY (record_id) REFERENCES public.clinical_registry_records(id) ON DELETE CASCADE", + "name": "clinical_registry_record_sources_record_id_fkey", + "table": "clinical_registry_record_sources" + }, + { + "def": "CHECK ((kind = ANY (ARRAY['service'::text, 'form'::text])))", + "name": "clinical_registry_records_kind_check", + "table": "clinical_registry_records" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "clinical_registry_records_owner_id_fkey", + "table": "clinical_registry_records" + }, + { + "def": "UNIQUE (owner_id, kind, slug)", + "name": "clinical_registry_records_owner_id_kind_slug_key", + "table": "clinical_registry_records" + }, + { + "def": "PRIMARY KEY (id)", + "name": "clinical_registry_records_pkey", + "table": "clinical_registry_records" + }, + { + "def": "CHECK ((btrim(slug) <> ''::text))", + "name": "clinical_registry_records_slug_check", + "table": "clinical_registry_records" + }, + { + "def": "CHECK ((source_status = ANY (ARRAY['current'::text, 'review_due'::text, 'outdated'::text, 'unknown'::text])))", + "name": "clinical_registry_records_source_status_check", + "table": "clinical_registry_records" + }, + { + "def": "CHECK ((btrim(title) <> ''::text))", + "name": "clinical_registry_records_title_check", + "table": "clinical_registry_records" + }, + { + "def": "CHECK ((validation_status = ANY (ARRAY['unverified'::text, 'locally_reviewed'::text, 'approved'::text])))", + "name": "clinical_registry_records_validation_status_check", + "table": "clinical_registry_records" + }, + { + "def": "CHECK ((kind = ANY (ARRAY['presentation'::text, 'diagnosis'::text])))", + "name": "differential_records_kind_check", + "table": "differential_records" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "differential_records_owner_id_fkey", + "table": "differential_records" + }, + { + "def": "UNIQUE (owner_id, kind, slug)", + "name": "differential_records_owner_id_kind_slug_key", + "table": "differential_records" + }, + { + "def": "PRIMARY KEY (id)", + "name": "differential_records_pkey", + "table": "differential_records" + }, + { + "def": "CHECK ((btrim(slug) <> ''::text))", + "name": "differential_records_slug_check", + "table": "differential_records" + }, + { + "def": "CHECK ((source_status = ANY (ARRAY['current'::text, 'review_due'::text, 'outdated'::text, 'unknown'::text])))", + "name": "differential_records_source_status_check", + "table": "differential_records" + }, + { + "def": "CHECK ((status = ANY (ARRAY['emergent'::text, 'urgent'::text, 'routine'::text])))", + "name": "differential_records_status_check", + "table": "differential_records" + }, + { + "def": "CHECK ((btrim(title) <> ''::text))", + "name": "differential_records_title_check", + "table": "differential_records" + }, + { + "def": "CHECK ((validation_status = ANY (ARRAY['unverified'::text, 'locally_reviewed'::text, 'approved'::text])))", + "name": "differential_records_validation_status_check", + "table": "differential_records" + }, + { + "def": "CHECK ((length(btrim(content)) > 0)) NOT VALID", + "name": "document_chunks_content_not_blank", + "table": "document_chunks" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_chunks_document_id_fkey", + "table": "document_chunks" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_chunks_pkey", + "table": "document_chunks" + }, + { + "def": "CHECK ((length(btrim(content)) > 0)) NOT VALID", + "name": "document_embedding_fields_content_not_blank", + "table": "document_embedding_fields" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_embedding_fields_document_id_fkey", + "table": "document_embedding_fields" + }, + { + "def": "CHECK ((field_type = ANY (ARRAY['document_title'::text, 'document_summary'::text, 'section_context'::text, 'memory_card'::text, 'chunk_high_yield'::text, 'table_row'::text, 'image_caption'::text, 'clinical_action'::text, 'threshold_fact'::text])))", + "name": "document_embedding_fields_field_type_check", + "table": "document_embedding_fields" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_embedding_fields_owner_id_fkey", + "table": "document_embedding_fields" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_embedding_fields_pkey", + "table": "document_embedding_fields" + }, + { + "def": "FOREIGN KEY (source_chunk_id) REFERENCES public.document_chunks(id) ON DELETE CASCADE", + "name": "document_embedding_fields_source_chunk_id_fkey", + "table": "document_embedding_fields" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_images_document_id_fkey", + "table": "document_images" + }, + { + "def": "CHECK ((image_type = ANY (ARRAY['clinical_table'::text, 'flowchart_algorithm'::text, 'form_checklist'::text, 'risk_matrix'::text, 'medication_chart'::text, 'graph'::text, 'screenshot_ui'::text, 'photo'::text, 'logo_decorative'::text, 'unclear'::text])))", + "name": "document_images_image_type_check", + "table": "document_images" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_images_pkey", + "table": "document_images" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_index_quality_document_id_fkey", + "table": "document_index_quality" + }, + { + "def": "CHECK ((extraction_quality = ANY (ARRAY['good'::text, 'partial'::text, 'poor'::text, 'unknown'::text])))", + "name": "document_index_quality_extraction_quality_check", + "table": "document_index_quality" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_index_quality_owner_id_fkey", + "table": "document_index_quality" + }, + { + "def": "PRIMARY KEY (document_id)", + "name": "document_index_quality_pkey", + "table": "document_index_quality" + }, + { + "def": "CHECK (((quality_score >= (0)::double precision) AND (quality_score <= (1)::double precision)))", + "name": "document_index_quality_quality_score_check", + "table": "document_index_quality" + }, + { + "def": "CHECK ((length(btrim(content)) > 0)) NOT VALID", + "name": "document_index_units_content_not_blank", + "table": "document_index_units" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_index_units_document_id_fkey", + "table": "document_index_units" + }, + { + "def": "CHECK ((extraction_mode = ANY (ARRAY['deterministic'::text, 'model_heavy'::text, 'hybrid'::text])))", + "name": "document_index_units_extraction_mode_check", + "table": "document_index_units" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_index_units_owner_id_fkey", + "table": "document_index_units" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_index_units_pkey", + "table": "document_index_units" + }, + { + "def": "CHECK (((quality_score >= (0)::double precision) AND (quality_score <= (1)::double precision)))", + "name": "document_index_units_quality_score_check", + "table": "document_index_units" + }, + { + "def": "FOREIGN KEY (source_chunk_id) REFERENCES public.document_chunks(id) ON DELETE CASCADE", + "name": "document_index_units_source_chunk_id_fkey", + "table": "document_index_units" + }, + { + "def": "FOREIGN KEY (source_image_id) REFERENCES public.document_images(id) ON DELETE SET NULL", + "name": "document_index_units_source_image_id_fkey", + "table": "document_index_units" + }, + { + "def": "CHECK ((unit_type = ANY (ARRAY['document_profile'::text, 'section_summary'::text, 'page_text'::text, 'chunk_evidence'::text, 'table_fact'::text, 'askable_question'::text, 'clinical_fact'::text, 'threshold'::text, 'workflow_step'::text, 'medication_monitoring'::text, 'alias'::text, 'vocabulary_term'::text, 'visual_summary'::text, 'flowchart_step'::text, 'diagram_decision'::text, 'risk_matrix_cell'::text, 'medication_chart_row'::text, 'chart_finding'::text, 'visual_askable_question'::text, 'table_threshold'::text])))", + "name": "document_index_units_unit_type_check", + "table": "document_index_units" + }, + { + "def": "CHECK (((confidence >= (0)::double precision) AND (confidence <= (1)::double precision)))", + "name": "document_labels_confidence_check", + "table": "document_labels" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_labels_document_id_fkey", + "table": "document_labels" + }, + { + "def": "UNIQUE (document_id, label_type, label, source)", + "name": "document_labels_document_id_label_type_label_source_key", + "table": "document_labels" + }, + { + "def": "CHECK ((label_type = ANY (ARRAY['site'::text, 'topic'::text, 'document_type'::text, 'medication'::text, 'risk'::text, 'setting'::text, 'workflow'::text, 'population'::text, 'service'::text, 'clinical_action'::text, 'care_phase'::text, 'document_intent'::text, 'content_feature'::text, 'custom'::text])))", + "name": "document_labels_label_type_check", + "table": "document_labels" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_labels_owner_id_fkey", + "table": "document_labels" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_labels_pkey", + "table": "document_labels" + }, + { + "def": "CHECK ((source = ANY (ARRAY['generated'::text, 'manual'::text])))", + "name": "document_labels_source_check", + "table": "document_labels" + }, + { + "def": "CHECK ((card_type = ANY (ARRAY['section_summary'::text, 'askable_question'::text, 'table_row'::text, 'threshold'::text, 'medication'::text, 'risk'::text, 'workflow'::text, 'definition'::text, 'citation_anchor'::text])))", + "name": "document_memory_cards_card_type_check", + "table": "document_memory_cards" + }, + { + "def": "CHECK (((confidence >= (0)::double precision) AND (confidence <= (1)::double precision)))", + "name": "document_memory_cards_confidence_check", + "table": "document_memory_cards" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_memory_cards_document_id_fkey", + "table": "document_memory_cards" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_memory_cards_owner_id_fkey", + "table": "document_memory_cards" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_memory_cards_pkey", + "table": "document_memory_cards" + }, + { + "def": "FOREIGN KEY (section_id) REFERENCES public.document_sections(id) ON DELETE SET NULL", + "name": "document_memory_cards_section_id_fkey", + "table": "document_memory_cards" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_pages_document_id_fkey", + "table": "document_pages" + }, + { + "def": "UNIQUE (document_id, page_number)", + "name": "document_pages_document_id_page_number_key", + "table": "document_pages" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_pages_pkey", + "table": "document_pages" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_sections_document_id_fkey", + "table": "document_sections" + }, + { + "def": "UNIQUE (document_id, section_index)", + "name": "document_sections_document_id_section_index_key", + "table": "document_sections" + }, + { + "def": "CHECK ((extraction_quality = ANY (ARRAY['good'::text, 'partial'::text, 'poor'::text, 'unknown'::text])))", + "name": "document_sections_extraction_quality_check", + "table": "document_sections" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_sections_owner_id_fkey", + "table": "document_sections" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_sections_pkey", + "table": "document_sections" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_summaries_document_id_fkey", + "table": "document_summaries" + }, + { + "def": "UNIQUE (document_id)", + "name": "document_summaries_document_id_key", + "table": "document_summaries" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_summaries_owner_id_fkey", + "table": "document_summaries" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_summaries_pkey", + "table": "document_summaries" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "document_table_facts_document_id_fkey", + "table": "document_table_facts" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "document_table_facts_owner_id_fkey", + "table": "document_table_facts" + }, + { + "def": "PRIMARY KEY (id)", + "name": "document_table_facts_pkey", + "table": "document_table_facts" + }, + { + "def": "FOREIGN KEY (source_chunk_id) REFERENCES public.document_chunks(id) ON DELETE CASCADE", + "name": "document_table_facts_source_chunk_id_fkey", + "table": "document_table_facts" + }, + { + "def": "FOREIGN KEY (source_image_id) REFERENCES public.document_images(id) ON DELETE SET NULL", + "name": "document_table_facts_source_image_id_fkey", + "table": "document_table_facts" + }, + { + "def": "FOREIGN KEY (import_batch_id) REFERENCES public.import_batches(id) ON DELETE SET NULL", + "name": "documents_import_batch_id_fkey", + "table": "documents" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "documents_owner_id_fkey", + "table": "documents" + }, + { + "def": "PRIMARY KEY (id)", + "name": "documents_pkey", + "table": "documents" + }, + { + "def": "CHECK ((status = ANY (ARRAY['queued'::text, 'processing'::text, 'indexed'::text, 'failed'::text])))", + "name": "documents_status_check", + "table": "documents" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "image_caption_cache_owner_id_fkey", + "table": "image_caption_cache" + }, + { + "def": "UNIQUE (owner_id, image_hash, model)", + "name": "image_caption_cache_owner_id_image_hash_model_key", + "table": "image_caption_cache" + }, + { + "def": "PRIMARY KEY (id)", + "name": "image_caption_cache_pkey", + "table": "image_caption_cache" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "import_batches_owner_id_fkey", + "table": "import_batches" + }, + { + "def": "PRIMARY KEY (id)", + "name": "import_batches_pkey", + "table": "import_batches" + }, + { + "def": "CHECK ((status = ANY (ARRAY['queued'::text, 'processing'::text, 'completed'::text, 'completed_with_errors'::text, 'failed'::text])))", + "name": "import_batches_status_check", + "table": "import_batches" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "indexing_v3_agent_jobs_document_id_fkey", + "table": "indexing_v3_agent_jobs" + }, + { + "def": "CHECK ((enrichment_status = ANY (ARRAY['pending'::text, 'processing'::text, 'completed'::text, 'failed'::text, 'needs_enrichment_artifacts'::text])))", + "name": "indexing_v3_agent_jobs_enrichment_status_check", + "table": "indexing_v3_agent_jobs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "indexing_v3_agent_jobs_pkey", + "table": "indexing_v3_agent_jobs" + }, + { + "def": "CHECK ((status = ANY (ARRAY['pending'::text, 'processing'::text, 'completed'::text, 'failed'::text, 'needs_enrichment_artifacts'::text])))", + "name": "indexing_v3_agent_jobs_status_check", + "table": "indexing_v3_agent_jobs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "ingestion_job_stages_document_id_fkey", + "table": "ingestion_job_stages" + }, + { + "def": "FOREIGN KEY (job_id) REFERENCES public.ingestion_jobs(id) ON DELETE CASCADE", + "name": "ingestion_job_stages_job_id_fkey", + "table": "ingestion_job_stages" + }, + { + "def": "PRIMARY KEY (id)", + "name": "ingestion_job_stages_pkey", + "table": "ingestion_job_stages" + }, + { + "def": "CHECK ((stage_status = ANY (ARRAY['started'::text, 'completed'::text, 'failed'::text])))", + "name": "ingestion_job_stages_stage_status_check", + "table": "ingestion_job_stages" + }, + { + "def": "FOREIGN KEY (batch_id) REFERENCES public.import_batches(id) ON DELETE SET NULL", + "name": "ingestion_jobs_batch_id_fkey", + "table": "ingestion_jobs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE", + "name": "ingestion_jobs_document_id_fkey", + "table": "ingestion_jobs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "ingestion_jobs_pkey", + "table": "ingestion_jobs" + }, + { + "def": "CHECK (((progress >= 0) AND (progress <= 100)))", + "name": "ingestion_jobs_progress_check", + "table": "ingestion_jobs" + }, + { + "def": "CHECK ((status = ANY (ARRAY['pending'::text, 'processing'::text, 'completed'::text, 'failed'::text])))", + "name": "ingestion_jobs_status_check", + "table": "ingestion_jobs" + }, + { + "def": "CHECK ((btrim(name) <> ''::text))", + "name": "medication_records_name_check", + "table": "medication_records" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "medication_records_owner_id_fkey", + "table": "medication_records" + }, + { + "def": "UNIQUE (owner_id, slug)", + "name": "medication_records_owner_id_slug_key", + "table": "medication_records" + }, + { + "def": "PRIMARY KEY (id)", + "name": "medication_records_pkey", + "table": "medication_records" + }, + { + "def": "CHECK ((btrim(slug) <> ''::text))", + "name": "medication_records_slug_check", + "table": "medication_records" + }, + { + "def": "CHECK ((source_status = ANY (ARRAY['current'::text, 'review_due'::text, 'outdated'::text, 'unknown'::text])))", + "name": "medication_records_source_status_check", + "table": "medication_records" + }, + { + "def": "CHECK ((validation_status = ANY (ARRAY['unverified'::text, 'locally_reviewed'::text, 'approved'::text])))", + "name": "medication_records_validation_status_check", + "table": "medication_records" + }, + { + "def": "CHECK ((btrim(alias) <> ''::text))", + "name": "rag_aliases_alias_nonempty", + "table": "rag_aliases" + }, + { + "def": "CHECK ((alias_type = ANY (ARRAY['medication'::text, 'document_title'::text, 'acronym'::text, 'service'::text, 'workflow'::text, 'typo'::text, 'clinical_term'::text, 'custom'::text])))", + "name": "rag_aliases_alias_type_check", + "table": "rag_aliases" + }, + { + "def": "CHECK ((btrim(canonical) <> ''::text))", + "name": "rag_aliases_canonical_nonempty", + "table": "rag_aliases" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "rag_aliases_owner_id_fkey", + "table": "rag_aliases" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_aliases_pkey", + "table": "rag_aliases" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "rag_queries_owner_id_fkey", + "table": "rag_queries" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_queries_pkey", + "table": "rag_queries" + }, + { + "def": "FOREIGN KEY (expected_chunk_id) REFERENCES public.document_chunks(id) ON DELETE SET NULL", + "name": "rag_query_misses_expected_chunk_id_fkey", + "table": "rag_query_misses" + }, + { + "def": "FOREIGN KEY (expected_document_id) REFERENCES public.documents(id) ON DELETE SET NULL", + "name": "rag_query_misses_expected_document_id_fkey", + "table": "rag_query_misses" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "rag_query_misses_owner_id_fkey", + "table": "rag_query_misses" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_query_misses_pkey", + "table": "rag_query_misses" + }, + { + "def": "CHECK ((review_status = ANY (ARRAY['new'::text, 'fixed'::text, 'not_in_corpus'::text, 'ambiguous'::text, 'ignored'::text])))", + "name": "rag_query_misses_review_status_check", + "table": "rag_query_misses" + }, + { + "def": "CHECK ((cache_kind = ANY (ARRAY['search'::text, 'answer'::text])))", + "name": "rag_response_cache_cache_kind_check", + "table": "rag_response_cache" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE CASCADE", + "name": "rag_response_cache_owner_id_fkey", + "table": "rag_response_cache" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_response_cache_pkey", + "table": "rag_response_cache" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "rag_retrieval_logs_owner_id_fkey", + "table": "rag_retrieval_logs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_retrieval_logs_pkey", + "table": "rag_retrieval_logs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL", + "name": "rag_visual_eval_cases_document_id_fkey", + "table": "rag_visual_eval_cases" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "rag_visual_eval_cases_owner_id_fkey", + "table": "rag_visual_eval_cases" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_visual_eval_cases_pkey", + "table": "rag_visual_eval_cases" + }, + { + "def": "FOREIGN KEY (case_id) REFERENCES public.rag_visual_eval_cases(id) ON DELETE CASCADE", + "name": "rag_visual_eval_runs_case_id_fkey", + "table": "rag_visual_eval_runs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL", + "name": "rag_visual_eval_runs_document_id_fkey", + "table": "rag_visual_eval_runs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "rag_visual_eval_runs_pkey", + "table": "rag_visual_eval_runs" + }, + { + "def": "FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL", + "name": "storage_cleanup_jobs_document_id_fkey", + "table": "storage_cleanup_jobs" + }, + { + "def": "FOREIGN KEY (owner_id) REFERENCES auth.users(id) ON DELETE SET NULL", + "name": "storage_cleanup_jobs_owner_id_fkey", + "table": "storage_cleanup_jobs" + }, + { + "def": "PRIMARY KEY (id)", + "name": "storage_cleanup_jobs_pkey", + "table": "storage_cleanup_jobs" + }, + { + "def": "CHECK ((status = ANY (ARRAY['pending'::text, 'completed'::text, 'failed'::text])))", + "name": "storage_cleanup_jobs_status_check", + "table": "storage_cleanup_jobs" + } + ], + "storage_buckets": [ + { + "id": "clinical-documents", + "public": false, + "file_size_limit": 157286400, + "allowed_mime_types": [ + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/plain" + ] + }, + { + "id": "clinical-images", + "public": false, + "file_size_limit": 52428800, + "allowed_mime_types": [ + "image/png", + "image/jpeg", + "image/webp" + ] + } + ], + "snapshot_version": 1 + } +} diff --git a/supabase/migrations/20260706200000_schema_drift_snapshot.sql b/supabase/migrations/20260706200000_schema_drift_snapshot.sql new file mode 100644 index 000000000..635bbea36 --- /dev/null +++ b/supabase/migrations/20260706200000_schema_drift_snapshot.sql @@ -0,0 +1,162 @@ +-- schema_drift_snapshot(): full-inventory schema drift detection. +-- +-- Generalizes the search_schema_health() single-function-hash approach into a +-- complete, normalized inventory of every application-owned schema object: +-- functions (comment/whitespace-stripped pg_get_functiondef md5 + sorted ACLs), +-- indexes (normalized pg_get_indexdef), RLS policies, table shapes (columns +-- sorted by NAME, not ordinal), constraints, triggers, views, extensions, and +-- storage buckets. `npm run check:drift` compares this snapshot against +-- supabase/drift-manifest.json, which is generated by replaying +-- supabase/schema.sql into a scratch Supabase Postgres container +-- (`npm run drift:manifest`). See docs/database-drift-detection.md. +-- +-- Normalization choices (known noise sources, documented in +-- docs/process-hardening.md "Retrieval RPC drift & indexing hygiene"): +-- * search_path is pinned to '' so pg_get_expr/pg_get_indexdef/pg_get_viewdef +-- and policy quals render fully qualified, identical on live and replay. +-- * Function bodies are stored verbatim in prosrc, so comments and whitespace +-- in migration files vs schema.sql would differ; both are stripped before +-- hashing (the same trick 20260701140631 used to validate byte-equivalence). +-- * Columns are sorted by name because live tables grew via ALTER TABLE ADD +-- COLUMN while schema.sql declares the final shape; ordinal position is +-- history, not shape. +-- * ACL arrays are sorted because aclitem order is append-order. +-- * Migration-history versions are deliberately NOT compared: live history has +-- known duplicate-version churn; this check compares actual object state. +-- +-- Deliberately out of scope (documented blind spots): +-- * Role settings (e.g. `alter role authenticator set ...`) and dashboard-only +-- platform config (connection caps, auth settings). +-- * Non-public schemas other than storage bucket rows/policies (auth, storage +-- DDL, cron, realtime are platform-managed). +-- * pg_cron job rows (platform/extension state, absent on fresh replays). + +create or replace function public.schema_drift_snapshot() +returns jsonb +language plpgsql +stable +security definer +set search_path to '' +as $$ +declare + snapshot jsonb; + buckets jsonb := '[]'::jsonb; +begin + select jsonb_build_object( + 'snapshot_version', 1, + 'captured_at', now(), + 'extensions', coalesce(( + select jsonb_agg(jsonb_build_object('name', e.extname, 'schema', n.nspname) order by e.extname) + from pg_extension e + join pg_namespace n on n.oid = e.extnamespace + where e.extname <> 'plpgsql' + ), '[]'::jsonb), + 'tables', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', c.relname, + 'rls_enabled', c.relrowsecurity, + 'rls_forced', c.relforcerowsecurity, + 'reloptions', (select array_agg(o.opt order by o.opt) from unnest(c.reloptions) o(opt)), + 'acl', (select array_agg(a.item::text order by a.item::text) from unnest(coalesce(c.relacl, acldefault('r', c.relowner))) a(item)), + 'columns', ( + select jsonb_agg(jsonb_build_object( + 'name', att.attname, + 'type', format_type(att.atttypid, att.atttypmod), + 'not_null', att.attnotnull, + 'identity', att.attidentity, + 'generated', att.attgenerated, + 'default', pg_get_expr(ad.adbin, ad.adrelid) + ) order by att.attname) + from pg_attribute att + left join pg_attrdef ad on ad.adrelid = att.attrelid and ad.adnum = att.attnum + where att.attrelid = c.oid and att.attnum > 0 and not att.attisdropped + ) + ) order by c.relname) + from pg_class c + where c.relnamespace = 'public'::regnamespace and c.relkind = 'r' + ), '[]'::jsonb), + 'views', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', c.relname, + 'def_hash', md5(regexp_replace(pg_get_viewdef(c.oid), '\s+', '', 'g')) + ) order by c.relname) + from pg_class c + where c.relnamespace = 'public'::regnamespace and c.relkind in ('v', 'm') + ), '[]'::jsonb), + 'functions', coalesce(( + select jsonb_agg(jsonb_build_object( + 'signature', p.oid::regprocedure::text, + 'def_hash', md5(regexp_replace(regexp_replace(regexp_replace(pg_get_functiondef(p.oid), '/\*.*?\*/', '', 'gs'), '--[^\n]*', '', 'g'), '\s+', '', 'g')), + 'acl', (select array_agg(a.item::text order by a.item::text) from unnest(coalesce(p.proacl, acldefault('f', p.proowner))) a(item)) + ) order by p.oid::regprocedure::text) + from pg_proc p + where p.pronamespace = 'public'::regnamespace + and p.prokind = 'f' + and not exists ( + select 1 from pg_depend dep + where dep.classid = 'pg_proc'::regclass and dep.objid = p.oid and dep.deptype = 'e' + ) + ), '[]'::jsonb), + 'indexes', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', ci.relname, + 'table', ct.relname, + 'def', pg_get_indexdef(ci.oid), + 'def_hash', md5(regexp_replace(pg_get_indexdef(ci.oid), '\s+', '', 'g')) + ) order by ci.relname) + from pg_index i + join pg_class ci on ci.oid = i.indexrelid + join pg_class ct on ct.oid = i.indrelid + where ct.relnamespace = 'public'::regnamespace + and ci.relnamespace = 'public'::regnamespace + ), '[]'::jsonb), + 'policies', coalesce(( + select jsonb_agg(jsonb_build_object( + 'schema', pol.schemaname, + 'table', pol.tablename, + 'name', pol.policyname, + 'permissive', pol.permissive, + 'roles', (select array_agg(r.role::text order by r.role::text) from unnest(pol.roles) r(role)), + 'cmd', pol.cmd, + 'qual', pol.qual, + 'with_check', pol.with_check + ) order by pol.schemaname, pol.tablename, pol.policyname) + from pg_policies pol + where pol.schemaname in ('public', 'storage') + ), '[]'::jsonb), + 'constraints', coalesce(( + select jsonb_agg(jsonb_build_object( + 'table', ct.relname, + 'name', con.conname, + 'def', pg_get_constraintdef(con.oid) + ) order by ct.relname, con.conname) + from pg_constraint con + join pg_class ct on ct.oid = con.conrelid + where con.connamespace = 'public'::regnamespace and ct.relkind = 'r' + ), '[]'::jsonb), + 'triggers', coalesce(( + select jsonb_agg(jsonb_build_object( + 'table', ct.relname, + 'name', t.tgname, + 'def', pg_get_triggerdef(t.oid) + ) order by ct.relname, t.tgname) + from pg_trigger t + join pg_class ct on ct.oid = t.tgrelid + where ct.relnamespace = 'public'::regnamespace and not t.tgisinternal + ), '[]'::jsonb) + ) into snapshot; + + if to_regclass('storage.buckets') is not null then + execute 'select coalesce(jsonb_agg(jsonb_build_object(' + || '''id'', b.id, ''public'', b.public, ''file_size_limit'', b.file_size_limit, ' + || '''allowed_mime_types'', b.allowed_mime_types) order by b.id), ''[]''::jsonb) ' + || 'from storage.buckets b' + into buckets; + end if; + + return snapshot || jsonb_build_object('storage_buckets', buckets); +end; +$$; + +revoke execute on function public.schema_drift_snapshot() from public, anon, authenticated; +grant execute on function public.schema_drift_snapshot() to service_role; diff --git a/supabase/migrations/20260707000000_codify_live_observed_drift.sql b/supabase/migrations/20260707000000_codify_live_observed_drift.sql new file mode 100644 index 000000000..6dbe1ae07 --- /dev/null +++ b/supabase/migrations/20260707000000_codify_live_observed_drift.sql @@ -0,0 +1,240 @@ +-- Codify live-observed drift (2026-07-07 full-inventory drift audit). +-- +-- The schema_drift_snapshot() three-way comparison (live vs schema.sql replay +-- vs migration-chain replay; see docs/database-drift-detection.md) found these +-- objects present on the LIVE project but in neither repo lineage — the same +-- raw-SQL-era class as the 20260705230000 reconciliation wave. Every statement +-- below is byte-aligned with live and idempotent: applying to live is a no-op; +-- applying to branch/preview/fresh databases converges them with live. +-- +-- Worker/app code writes several of these columns (e.g. +-- document_index_quality.noisy_unit_rate, ingestion_job_stages.error_class), +-- so a branch database without them breaks ingestion. + +set search_path = public, extensions, pg_temp; + +-- document_images: visual-intelligence-era columns (live-only until now) +alter table public.document_images add column if not exists caption_confidence real; +alter table public.document_images add column if not exists clinical_priority_score real; +alter table public.document_images add column if not exists crop_completeness real; +alter table public.document_images add column if not exists image_quality_score real; +alter table public.document_images add column if not exists ocr_text_density real; +alter table public.document_images add column if not exists structured_extraction_confidence real; +alter table public.document_images add column if not exists visual_duplicate_group text; + +-- document_index_quality: OCR/coverage metric columns (live-only until now) +alter table public.document_index_quality add column if not exists anchor_coverage real; +alter table public.document_index_quality add column if not exists model_fallback_rate real; +alter table public.document_index_quality add column if not exists noisy_unit_rate real; +alter table public.document_index_quality add column if not exists retrievable_visual_hit boolean; +alter table public.document_index_quality add column if not exists source_span_coverage real; +alter table public.document_index_quality add column if not exists typed_unit_coverage real; + +-- ingestion_job_stages: failure-classification columns (live-only until now) +alter table public.ingestion_job_stages add column if not exists error_class text; +alter table public.ingestion_job_stages add column if not exists retry_count integer not null default 0; + +-- content_hash nullability: align both directions with live. +alter table public.document_chunks alter column content_hash drop not null; +update public.document_embedding_fields set content_hash = md5(coalesce(content, '')) where content_hash is null; +alter table public.document_embedding_fields alter column content_hash set not null; + +-- indexing_v3_agent_jobs.id default is extensions-qualified on live. +alter table public.indexing_v3_agent_jobs alter column id set default extensions.gen_random_uuid(); + +-- Autovacuum tuning present on live for the high-churn RAG tables. +alter table public.document_chunks set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_pages set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_images set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_table_facts set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_labels set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 200, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 100 +); + +-- Content-quality CHECK constraints, present on live as NOT VALID. +do $guard$ +begin + if not exists (select 1 from pg_constraint where conname = 'document_chunks_content_not_blank') then + alter table public.document_chunks + add constraint document_chunks_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; + if not exists (select 1 from pg_constraint where conname = 'document_embedding_fields_content_not_blank') then + alter table public.document_embedding_fields + add constraint document_embedding_fields_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; + if not exists (select 1 from pg_constraint where conname = 'document_index_units_content_not_blank') then + alter table public.document_index_units + add constraint document_index_units_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; +end +$guard$; + +-- Live-only functions and triggers (captured verbatim from live +-- pg_get_functiondef; also declared in supabase/schema.sql). + +CREATE OR REPLACE FUNCTION public.set_owner_id_from_auth_uid() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'auth' +AS $function$ +begin + if new.owner_id is null then + new.owner_id := auth.uid(); + end if; + return new; +end; +$function$; + +revoke execute on function public.set_owner_id_from_auth_uid() from public, anon, authenticated; +grant execute on function public.set_owner_id_from_auth_uid() to service_role; + +drop trigger if exists trg_set_owner_id_rag_queries on public.rag_queries; +create trigger trg_set_owner_id_rag_queries +before insert on public.rag_queries +for each row execute function public.set_owner_id_from_auth_uid(); + +drop trigger if exists trg_set_owner_id_rag_query_misses on public.rag_query_misses; +create trigger trg_set_owner_id_rag_query_misses +before insert on public.rag_query_misses +for each row execute function public.set_owner_id_from_auth_uid(); + +CREATE OR REPLACE FUNCTION public.purge_expired_rag_queries(p_retention_days integer DEFAULT 30) + RETURNS integer + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'pg_catalog', 'pg_temp' +AS $function$ +declare + v_deleted integer; +begin + if p_retention_days < 1 then + raise exception 'retention days must be positive'; + end if; + delete from public.rag_queries where created_at < now() - make_interval(days => p_retention_days); + get diagnostics v_deleted = row_count; + return v_deleted; +end; +$function$; + +revoke execute on function public.purge_expired_rag_queries(integer) from public, anon, authenticated; +grant execute on function public.purge_expired_rag_queries(integer) to service_role; + +CREATE OR REPLACE FUNCTION public.correct_clinical_query_terms(input_query text, min_sim real DEFAULT 0.45) + RETURNS text + LANGUAGE plpgsql + STABLE SECURITY DEFINER + SET search_path TO 'public', 'extensions', 'pg_temp' +AS $function$ +declare + vocab text[]; + tokens text[]; + tok text; + best text; + best_sim real; + corrected text[] := array[]::text[]; + changed boolean := false; +begin + if input_query is null or length(trim(input_query)) = 0 then + return input_query; + end if; + + -- Build the known-term vocabulary once per call. + select array_agg(distinct term) into vocab + from ( + select lower(alias) as term from public.rag_aliases where enabled and length(alias) between 4 and 40 + union + select lower(canonical) from public.rag_aliases where enabled and length(canonical) between 4 and 40 + union + select w from public.documents d, lateral unnest(regexp_split_to_array(lower(d.title), '[^a-z]+')) as w + where d.status = 'indexed' and length(w) between 4 and 40 + ) t; + + tokens := regexp_split_to_array(lower(input_query), '\s+'); + foreach tok in array tokens loop + if length(tok) < 4 or tok = any(vocab) then + corrected := corrected || tok; + continue; + end if; + best := null; + best_sim := 0; + select v, similarity(v, tok) into best, best_sim + from unnest(vocab) as v + order by similarity(v, tok) desc + limit 1; + if best is not null and best_sim >= min_sim and best <> tok and length(best) >= length(tok) then + corrected := corrected || best; + changed := true; + else + corrected := corrected || tok; + end if; + end loop; + + if not changed then + return input_query; + end if; + return array_to_string(corrected, ' '); +end; +$function$; + +revoke execute on function public.correct_clinical_query_terms(text, real) from public, anon, authenticated; +grant execute on function public.correct_clinical_query_terms(text, real) to service_role; + +-- NOTE: unlike invoke_indexing_v3_agent (URL moved to a GUC by 20260702160000), +-- the live definition still hardcodes the project URL. Codified as-is; migrate +-- to the GUC pattern in a follow-up if this RPC stays. +CREATE OR REPLACE FUNCTION public.invoke_ingestion_worker(p_limit integer DEFAULT 25) + RETURNS bigint + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'extensions', 'vault', 'pg_temp' +AS $function$ +declare + v_request_id bigint; + v_jwt text; + v_limit integer := greatest(1, least(coalesce("p_limit", 25), 200)); +begin + select "decrypted_secret" into v_jwt + from "vault"."decrypted_secrets" + where "name" = 'cron_ingestion_jwt' + limit 1; + + if v_jwt is null or length(trim(v_jwt)) = 0 then + raise exception 'Missing Vault secret: cron_ingestion_jwt'; + end if; + + select "net"."http_post"( + url := 'https://sjrfecxgysukkwxsowpy.supabase.co/functions/v1/ingestion-worker?limit=' || v_limit::text, + headers := jsonb_build_object( + 'Content-Type','application/json', + 'Authorization','Bearer ' || v_jwt + ), + body := jsonb_build_object('source','pg_cron','worker','ingestion-worker','ts', now()), + timeout_milliseconds := 60000 + ) + into v_request_id; + + return v_request_id; +end; +$function$; + +revoke execute on function public.invoke_ingestion_worker(integer) from public, anon, authenticated; +grant execute on function public.invoke_ingestion_worker(integer) to service_role; + +-- ACL tightening already true on live: service-role-only execute. +revoke execute on function public.match_document_index_units_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid) from public, anon, authenticated; +revoke execute on function public.reset_document_index(uuid) from public, anon, authenticated; +grant execute on function public.reset_document_index(uuid) to service_role; diff --git a/supabase/schema.sql b/supabase/schema.sql index e01272b39..de8fbb62b 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -138,6 +138,13 @@ create table if not exists public.document_images ( perceptual_hash text, labels text[] not null default '{}', index_generation_id uuid, + caption_confidence real, + clinical_priority_score real, + crop_completeness real, + image_quality_score real, + ocr_text_density real, + structured_extraction_confidence real, + visual_duplicate_group text, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); @@ -271,7 +278,7 @@ create table if not exists public.document_chunks ( anchor_id text, content text not null, retrieval_synopsis text, - content_hash text not null, + content_hash text, index_generation_id uuid, token_estimate integer not null default 0, image_ids uuid[] not null default '{}', @@ -330,7 +337,7 @@ create table if not exists public.document_embedding_fields ( ) ), content text not null, - content_hash text, + content_hash text not null, embedding extensions.vector(1536) not null, index_generation_id uuid, metadata jsonb not null default '{}'::jsonb, @@ -347,11 +354,103 @@ create table if not exists public.document_index_quality ( quality_score real not null default 0 check (quality_score >= 0 and quality_score <= 1), extraction_quality text not null default 'unknown' check (extraction_quality in ('good', 'partial', 'poor', 'unknown')), + anchor_coverage real, + model_fallback_rate real, + noisy_unit_rate real, + retrievable_visual_hit boolean, + source_span_coverage real, + typed_unit_coverage real, metrics jsonb not null default '{}'::jsonb, issues text[] not null default '{}', updated_at timestamptz not null default now() ); +-- NOTE: this table is declared here, before document_strict_gate_status and the +-- late grant/RLS sections that reference it, so a from-scratch replay of this +-- file succeeds (DR rehearsal 2026-07-06; see docs/disaster-recovery-runbook.md). +-- Its updated_at trigger stays below with the other triggers (set_updated_at is +-- defined later in this file). +create table if not exists public.document_index_units ( + id uuid primary key default gen_random_uuid(), + owner_id uuid references auth.users(id) on delete set null, + document_id uuid not null references public.documents(id) on delete cascade, + unit_type text not null check (unit_type in ('document_profile', 'section_summary', 'page_text', 'chunk_evidence', 'table_fact', 'askable_question', 'clinical_fact', 'threshold', 'workflow_step', 'medication_monitoring', 'alias', 'vocabulary_term', 'visual_summary', 'flowchart_step', 'diagram_decision', 'risk_matrix_cell', 'medication_chart_row', 'chart_finding', 'visual_askable_question', 'table_threshold')), + source_chunk_id uuid references public.document_chunks(id) on delete cascade, + source_image_id uuid references public.document_images(id) on delete set null, + page_start integer, + page_end integer, + heading_path text[] not null default '{}', + title text not null, + content text not null, + normalized_terms text[] not null default '{}', + source_span jsonb, + quality_score real not null default 0.7 check (quality_score >= 0 and quality_score <= 1), + extraction_mode text not null default 'deterministic' check (extraction_mode in ('deterministic', 'model_heavy', 'hybrid')), + index_generation_id uuid, + embedding extensions.vector(1536) not null, + metadata jsonb not null default '{}'::jsonb, + search_tsv tsvector generated always as (to_tsvector('english', coalesce(unit_type, '') || ' ' || coalesce(title, '') || ' ' || coalesce(content, ''))) stored, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists document_index_units_document_idx on public.document_index_units(document_id, unit_type, page_start); +create index if not exists document_index_units_owner_type_idx on public.document_index_units(owner_id, unit_type, quality_score desc); +create index if not exists document_index_units_owner_chunk_type_idx + on public.document_index_units(owner_id, source_chunk_id, unit_type) + where source_chunk_id is not null; +create index if not exists document_index_units_chunk_idx on public.document_index_units(source_chunk_id) where source_chunk_id is not null; +create index if not exists document_index_units_image_idx on public.document_index_units(source_image_id) where source_image_id is not null; +create index if not exists document_index_units_terms_idx on public.document_index_units using gin(normalized_terms); +create index if not exists document_index_units_heading_path_idx on public.document_index_units using gin(heading_path); +create index if not exists document_index_units_search_idx on public.document_index_units using gin(search_tsv); +-- Intentionally no HNSW index on document_index_units.embedding: the hybrid RPC is +-- text-candidate-gated so the vector path never used it (0 lifetime scans; dropped +-- live 2026-07-02 by the drop_legacy_vector_indexes migration). Re-add only if the +-- RPC is rewritten to take a vector-first candidate path. + +-- Autovacuum tuning observed live on the high-churn RAG tables (advisor-era). +-- Values must match live exactly; `npm run check:drift` compares reloptions. +alter table public.document_chunks set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_pages set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_images set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_table_facts set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 1000, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 500 +); +alter table public.document_labels set ( + autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_threshold = 200, + autovacuum_analyze_scale_factor = 0.02, autovacuum_analyze_threshold = 100 +); + +-- Content-quality guards observed live (added there as NOT VALID; codified +-- byte-identically -- pg_get_constraintdef includes the NOT VALID marker). +do $guard$ +begin + if not exists (select 1 from pg_constraint where conname = 'document_chunks_content_not_blank') then + alter table public.document_chunks + add constraint document_chunks_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; + if not exists (select 1 from pg_constraint where conname = 'document_embedding_fields_content_not_blank') then + alter table public.document_embedding_fields + add constraint document_embedding_fields_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; + if not exists (select 1 from pg_constraint where conname = 'document_index_units_content_not_blank') then + alter table public.document_index_units + add constraint document_index_units_content_not_blank check (length(btrim(content)) > 0) not valid; + end if; +end +$guard$; + create table if not exists public.ingestion_jobs ( id uuid primary key default gen_random_uuid(), document_id uuid not null references public.documents(id) on delete cascade, @@ -379,6 +478,8 @@ create table if not exists public.ingestion_job_stages ( stage_name text not null, stage_status text not null default 'started' check (stage_status in ('started', 'completed', 'failed')), + error_class text, + retry_count integer not null default 0, metadata jsonb not null default '{}'::jsonb, artifact_counts jsonb not null default '{}'::jsonb, error_message text, @@ -1064,70 +1165,55 @@ create or replace function public.claim_indexing_v3_agent_jobs( p_stale_after_minutes integer default 45 ) returns table ( - id uuid, - document_id uuid, - batch_id uuid, - status text, - stage text, - progress integer, - error_message text, - attempt_count integer, - max_attempts integer, - locked_at timestamptz, - locked_by text, - documents jsonb + id uuid, document_id uuid, batch_id uuid, status text, stage text, progress integer, + error_message text, attempt_count integer, max_attempts integer, locked_at timestamptz, + locked_by text, documents jsonb ) language plpgsql set search_path = public, extensions, pg_temp as $$ --- Dual-write compatibility note: --- This RPC claims via the jobs table (SKIP LOCKED) and also patches --- documents.metadata so the edge function continues to read correct --- state. Once the edge function writes completions to this table, --- the documents.metadata patch below should be removed. begin + insert into public.indexing_v3_agent_jobs ( + document_id, status, enrichment_status, next_run_at, version, metadata, created_at, updated_at + ) + select d.id, 'pending', coalesce(d.metadata->>'enrichment_status', 'pending'), + case when coalesce(d.metadata->>'indexing_v3_agent_next_run_at', '') ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' + then (d.metadata->>'indexing_v3_agent_next_run_at')::timestamptz else null end, + coalesce(nullif(d.metadata->>'indexing_v3_agent_version', ''), 'visual-core-v3'), + '{}'::jsonb, coalesce(d.created_at, now()), now() + from public.documents d + where d.status = 'indexed' + and d.metadata ? 'indexing_v3_agent_status' + and coalesce(d.metadata->>'indexing_v3_agent_status', 'pending') + not in ('completed', 'needs_enrichment_artifacts') + on conflict (document_id) do nothing; + return query with eligible_jobs as ( select j.id, j.document_id, j.attempt_count, j.max_attempts from public.indexing_v3_agent_jobs j - -- must join documents to confirm document.status = 'indexed' - -- and to gate on enrichment_status (also stored in the job row) + join public.documents d on d.id = j.document_id and d.status = 'indexed' where j.status not in ('completed', 'needs_enrichment_artifacts') and j.enrichment_status in ('pending', 'failed', 'processing') and j.attempt_count < j.max_attempts and coalesce(j.next_run_at, now()) <= now() - and ( - j.status <> 'processing' - or j.locked_at is null - or j.locked_at < now() - make_interval(mins => p_stale_after_minutes) - ) + and (j.status <> 'processing' or j.locked_at is null + or j.locked_at < now() - make_interval(mins => p_stale_after_minutes)) order by coalesce(j.next_run_at, j.updated_at), j.id limit greatest(p_claim_limit, 1) for update of j skip locked ), claimed_jobs as ( update public.indexing_v3_agent_jobs j - set - status = 'processing', - enrichment_status = 'processing', - locked_by = p_worker_id, - locked_at = now(), - attempt_count = e.attempt_count + 1, - last_error = null, - next_run_at = null, - updated_at = now() - from eligible_jobs e - where j.id = e.id - returning j.* + set status = 'processing', enrichment_status = 'processing', locked_by = p_worker_id, + locked_at = now(), attempt_count = e.attempt_count + 1, last_error = null, + next_run_at = null, updated_at = now() + from eligible_jobs e where j.id = e.id returning j.* ), - -- Patch documents.metadata for backward compatibility with edge function patched_documents as ( update public.documents d - set - metadata = jsonb_strip_nulls( - (coalesce(d.metadata, '{}'::jsonb) - - 'indexing_v3_agent_next_run_at' - - 'indexing_v3_agent_last_error') + set metadata = jsonb_strip_nulls( + (coalesce(d.metadata, '{}'::jsonb) - 'indexing_v3_agent_next_run_at' - 'indexing_v3_agent_last_error') || jsonb_build_object( 'indexing_v3_agent_status', 'processing', 'indexing_v3_agent_version', cj.version, @@ -1138,27 +1224,16 @@ begin 'indexing_v3_agent_updated_at', now(), 'enrichment_status', 'processing' ) - ), - updated_at = now() + ), updated_at = now() from claimed_jobs cj - where d.id = cj.document_id - and d.status = 'indexed' -- safety: only touch documents still eligible + where d.id = cj.document_id and d.status = 'indexed' returning d.*, cj.id as job_id, cj.attempt_count as job_attempt_count, cj.max_attempts as job_max_attempts, cj.locked_at as job_locked_at ) - select - pd.job_id as id, - pd.id as document_id, - pd.import_batch_id as batch_id, - 'processing'::text as status, - 'v3 enrichment claimed'::text as stage, - 95::integer as progress, - null::text as error_message, - pd.job_attempt_count, - pd.job_max_attempts, - pd.job_locked_at as locked_at, - p_worker_id as locked_by, - to_jsonb(pd.*) - 'job_id' - 'job_attempt_count' - 'job_max_attempts' - 'job_locked_at' as documents + select pd.job_id, pd.id, pd.import_batch_id, 'processing'::text, 'v3 enrichment claimed'::text, + 95::integer, null::text, pd.job_attempt_count, pd.job_max_attempts, pd.job_locked_at, + p_worker_id, + to_jsonb(pd.*) - 'job_id' - 'job_attempt_count' - 'job_max_attempts' - 'job_locked_at' from patched_documents pd; end; $$; @@ -2323,10 +2398,10 @@ language plpgsql stable set search_path = public, extensions, pg_temp as $$ -begin - perform set_config('hnsw.ef_search', '100', true); - return query - select * +BEGIN + PERFORM set_config('hnsw.ef_search', '100', true); + RETURN QUERY +select * from public.match_document_memory_cards_hybrid_v2( query_embedding, query_text, @@ -2335,7 +2410,7 @@ begin document_filters, owner_filter ); -end +END $$; create or replace function public.detect_legacy_ivfflat_indexes() @@ -2550,6 +2625,298 @@ $$; revoke execute on function public.search_schema_health() from public, anon, authenticated; grant execute on function public.search_schema_health() to service_role; +-- ============================================================================ +-- Live-only objects codified 2026-07-07 (drift reconciliation). +-- These functions/triggers existed on the live project (raw-SQL era) but in +-- neither this file nor any migration. Captured verbatim from live +-- pg_get_functiondef; migration 20260707000000_codify_live_observed_drift.sql +-- carries the same definitions so branch databases converge. +-- ============================================================================ + +CREATE OR REPLACE FUNCTION public.set_owner_id_from_auth_uid() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'auth' +AS $function$ +begin + if new.owner_id is null then + new.owner_id := auth.uid(); + end if; + return new; +end; +$function$; + +revoke execute on function public.set_owner_id_from_auth_uid() from public, anon, authenticated; +grant execute on function public.set_owner_id_from_auth_uid() to service_role; + +drop trigger if exists trg_set_owner_id_rag_queries on public.rag_queries; +create trigger trg_set_owner_id_rag_queries +before insert on public.rag_queries +for each row execute function public.set_owner_id_from_auth_uid(); + +drop trigger if exists trg_set_owner_id_rag_query_misses on public.rag_query_misses; +create trigger trg_set_owner_id_rag_query_misses +before insert on public.rag_query_misses +for each row execute function public.set_owner_id_from_auth_uid(); + +CREATE OR REPLACE FUNCTION public.purge_expired_rag_queries(p_retention_days integer DEFAULT 30) + RETURNS integer + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'pg_catalog', 'pg_temp' +AS $function$ +declare + v_deleted integer; +begin + if p_retention_days < 1 then + raise exception 'retention days must be positive'; + end if; + delete from public.rag_queries where created_at < now() - make_interval(days => p_retention_days); + get diagnostics v_deleted = row_count; + return v_deleted; +end; +$function$; + +revoke execute on function public.purge_expired_rag_queries(integer) from public, anon, authenticated; +grant execute on function public.purge_expired_rag_queries(integer) to service_role; + +CREATE OR REPLACE FUNCTION public.correct_clinical_query_terms(input_query text, min_sim real DEFAULT 0.45) + RETURNS text + LANGUAGE plpgsql + STABLE SECURITY DEFINER + SET search_path TO 'public', 'extensions', 'pg_temp' +AS $function$ +declare + vocab text[]; + tokens text[]; + tok text; + best text; + best_sim real; + corrected text[] := array[]::text[]; + changed boolean := false; +begin + if input_query is null or length(trim(input_query)) = 0 then + return input_query; + end if; + + -- Build the known-term vocabulary once per call. + select array_agg(distinct term) into vocab + from ( + select lower(alias) as term from public.rag_aliases where enabled and length(alias) between 4 and 40 + union + select lower(canonical) from public.rag_aliases where enabled and length(canonical) between 4 and 40 + union + select w from public.documents d, lateral unnest(regexp_split_to_array(lower(d.title), '[^a-z]+')) as w + where d.status = 'indexed' and length(w) between 4 and 40 + ) t; + + tokens := regexp_split_to_array(lower(input_query), '\s+'); + foreach tok in array tokens loop + if length(tok) < 4 or tok = any(vocab) then + corrected := corrected || tok; + continue; + end if; + best := null; + best_sim := 0; + select v, similarity(v, tok) into best, best_sim + from unnest(vocab) as v + order by similarity(v, tok) desc + limit 1; + if best is not null and best_sim >= min_sim and best <> tok and length(best) >= length(tok) then + corrected := corrected || best; + changed := true; + else + corrected := corrected || tok; + end if; + end loop; + + if not changed then + return input_query; + end if; + return array_to_string(corrected, ' '); +end; +$function$; + +revoke execute on function public.correct_clinical_query_terms(text, real) from public, anon, authenticated; +grant execute on function public.correct_clinical_query_terms(text, real) to service_role; + +-- NOTE: unlike invoke_indexing_v3_agent (URL moved to a GUC by 20260702160000), +-- the live definition still hardcodes the project URL. Codified as-is; migrate +-- to the GUC pattern in a follow-up if this RPC stays. +CREATE OR REPLACE FUNCTION public.invoke_ingestion_worker(p_limit integer DEFAULT 25) + RETURNS bigint + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'extensions', 'vault', 'pg_temp' +AS $function$ +declare + v_request_id bigint; + v_jwt text; + v_limit integer := greatest(1, least(coalesce("p_limit", 25), 200)); +begin + select "decrypted_secret" into v_jwt + from "vault"."decrypted_secrets" + where "name" = 'cron_ingestion_jwt' + limit 1; + + if v_jwt is null or length(trim(v_jwt)) = 0 then + raise exception 'Missing Vault secret: cron_ingestion_jwt'; + end if; + + select "net"."http_post"( + url := 'https://sjrfecxgysukkwxsowpy.supabase.co/functions/v1/ingestion-worker?limit=' || v_limit::text, + headers := jsonb_build_object( + 'Content-Type','application/json', + 'Authorization','Bearer ' || v_jwt + ), + body := jsonb_build_object('source','pg_cron','worker','ingestion-worker','ts', now()), + timeout_milliseconds := 60000 + ) + into v_request_id; + + return v_request_id; +end; +$function$; + +revoke execute on function public.invoke_ingestion_worker(integer) from public, anon, authenticated; +grant execute on function public.invoke_ingestion_worker(integer) to service_role; + +-- Full-inventory drift snapshot backing `npm run check:drift`. The expected +-- state lives in supabase/drift-manifest.json (generated from a scratch replay +-- of this file via `npm run drift:manifest`). Keep this definition byte-identical +-- to supabase/migrations/20260706200000_schema_drift_snapshot.sql; a unit test +-- in tests/supabase-schema.test.ts enforces it. See docs/database-drift-detection.md. +create or replace function public.schema_drift_snapshot() +returns jsonb +language plpgsql +stable +security definer +set search_path to '' +as $$ +declare + snapshot jsonb; + buckets jsonb := '[]'::jsonb; +begin + select jsonb_build_object( + 'snapshot_version', 1, + 'captured_at', now(), + 'extensions', coalesce(( + select jsonb_agg(jsonb_build_object('name', e.extname, 'schema', n.nspname) order by e.extname) + from pg_extension e + join pg_namespace n on n.oid = e.extnamespace + where e.extname <> 'plpgsql' + ), '[]'::jsonb), + 'tables', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', c.relname, + 'rls_enabled', c.relrowsecurity, + 'rls_forced', c.relforcerowsecurity, + 'reloptions', (select array_agg(o.opt order by o.opt) from unnest(c.reloptions) o(opt)), + 'acl', (select array_agg(a.item::text order by a.item::text) from unnest(coalesce(c.relacl, acldefault('r', c.relowner))) a(item)), + 'columns', ( + select jsonb_agg(jsonb_build_object( + 'name', att.attname, + 'type', format_type(att.atttypid, att.atttypmod), + 'not_null', att.attnotnull, + 'identity', att.attidentity, + 'generated', att.attgenerated, + 'default', pg_get_expr(ad.adbin, ad.adrelid) + ) order by att.attname) + from pg_attribute att + left join pg_attrdef ad on ad.adrelid = att.attrelid and ad.adnum = att.attnum + where att.attrelid = c.oid and att.attnum > 0 and not att.attisdropped + ) + ) order by c.relname) + from pg_class c + where c.relnamespace = 'public'::regnamespace and c.relkind = 'r' + ), '[]'::jsonb), + 'views', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', c.relname, + 'def_hash', md5(regexp_replace(pg_get_viewdef(c.oid), '\s+', '', 'g')) + ) order by c.relname) + from pg_class c + where c.relnamespace = 'public'::regnamespace and c.relkind in ('v', 'm') + ), '[]'::jsonb), + 'functions', coalesce(( + select jsonb_agg(jsonb_build_object( + 'signature', p.oid::regprocedure::text, + 'def_hash', md5(regexp_replace(regexp_replace(regexp_replace(pg_get_functiondef(p.oid), '/\*.*?\*/', '', 'gs'), '--[^\n]*', '', 'g'), '\s+', '', 'g')), + 'acl', (select array_agg(a.item::text order by a.item::text) from unnest(coalesce(p.proacl, acldefault('f', p.proowner))) a(item)) + ) order by p.oid::regprocedure::text) + from pg_proc p + where p.pronamespace = 'public'::regnamespace + and p.prokind = 'f' + and not exists ( + select 1 from pg_depend dep + where dep.classid = 'pg_proc'::regclass and dep.objid = p.oid and dep.deptype = 'e' + ) + ), '[]'::jsonb), + 'indexes', coalesce(( + select jsonb_agg(jsonb_build_object( + 'name', ci.relname, + 'table', ct.relname, + 'def', pg_get_indexdef(ci.oid), + 'def_hash', md5(regexp_replace(pg_get_indexdef(ci.oid), '\s+', '', 'g')) + ) order by ci.relname) + from pg_index i + join pg_class ci on ci.oid = i.indexrelid + join pg_class ct on ct.oid = i.indrelid + where ct.relnamespace = 'public'::regnamespace + and ci.relnamespace = 'public'::regnamespace + ), '[]'::jsonb), + 'policies', coalesce(( + select jsonb_agg(jsonb_build_object( + 'schema', pol.schemaname, + 'table', pol.tablename, + 'name', pol.policyname, + 'permissive', pol.permissive, + 'roles', (select array_agg(r.role::text order by r.role::text) from unnest(pol.roles) r(role)), + 'cmd', pol.cmd, + 'qual', pol.qual, + 'with_check', pol.with_check + ) order by pol.schemaname, pol.tablename, pol.policyname) + from pg_policies pol + where pol.schemaname in ('public', 'storage') + ), '[]'::jsonb), + 'constraints', coalesce(( + select jsonb_agg(jsonb_build_object( + 'table', ct.relname, + 'name', con.conname, + 'def', pg_get_constraintdef(con.oid) + ) order by ct.relname, con.conname) + from pg_constraint con + join pg_class ct on ct.oid = con.conrelid + where con.connamespace = 'public'::regnamespace and ct.relkind = 'r' + ), '[]'::jsonb), + 'triggers', coalesce(( + select jsonb_agg(jsonb_build_object( + 'table', ct.relname, + 'name', t.tgname, + 'def', pg_get_triggerdef(t.oid) + ) order by ct.relname, t.tgname) + from pg_trigger t + join pg_class ct on ct.oid = t.tgrelid + where ct.relnamespace = 'public'::regnamespace and not t.tgisinternal + ), '[]'::jsonb) + ) into snapshot; + + if to_regclass('storage.buckets') is not null then + execute 'select coalesce(jsonb_agg(jsonb_build_object(' + || '''id'', b.id, ''public'', b.public, ''file_size_limit'', b.file_size_limit, ' + || '''allowed_mime_types'', b.allowed_mime_types) order by b.id), ''[]''::jsonb) ' + || 'from storage.buckets b' + into buckets; + end if; + + return snapshot || jsonb_build_object('storage_buckets', buckets); +end; +$$; + +revoke execute on function public.schema_drift_snapshot() from public, anon, authenticated; +grant execute on function public.schema_drift_snapshot() to service_role; + create or replace function public.explain_retrieval_rpc( p_rpc text, p_query_text text, @@ -3873,18 +4240,22 @@ create policy "images owner read" on public.document_images ); create policy "labels owner read" on public.document_labels - for select to authenticated using (owner_id = (select auth.uid())); + for select to authenticated + using ((select auth.uid()) = owner_id); create policy "labels owner manual insert" on public.document_labels - for insert to authenticated with check (owner_id = (select auth.uid()) and source = 'manual'); + for insert to authenticated + with check ((select auth.uid()) = owner_id and source = 'manual'); create policy "labels owner manual update" on public.document_labels for update to authenticated - using (owner_id = (select auth.uid()) and source = 'manual') - with check (owner_id = (select auth.uid()) and source = 'manual'); + using ((select auth.uid()) = owner_id and source = 'manual') + with check ((select auth.uid()) = owner_id and source = 'manual'); create policy "labels owner manual delete" on public.document_labels - for delete to authenticated using (owner_id = (select auth.uid()) and source = 'manual'); + for delete to authenticated + using ((select auth.uid()) = owner_id and source = 'manual'); create policy "summaries owner read" on public.document_summaries - for select to authenticated using (owner_id = (select auth.uid())); + for select to authenticated + using ((select auth.uid()) = owner_id); create policy "document sections owner all" on public.document_sections for all to authenticated @@ -3962,7 +4333,8 @@ create policy "audit logs service role all" on public.audit_logs with check (true); create policy "storage cleanup owner read" on public.storage_cleanup_jobs - for select to authenticated using (owner_id = (select auth.uid())); +for select to authenticated +using ((select auth.uid()) = owner_id); create policy "document storage owner read" on storage.objects for select to authenticated @@ -3972,45 +4344,6 @@ create policy "image storage owner read" on storage.objects for select to authenticated using (bucket_id = 'clinical-images' and (storage.foldername(name))[1] = (select auth.uid())::text); -create table if not exists public.document_index_units ( - id uuid primary key default gen_random_uuid(), - owner_id uuid references auth.users(id) on delete set null, - document_id uuid not null references public.documents(id) on delete cascade, - unit_type text not null check (unit_type in ('document_profile', 'section_summary', 'page_text', 'chunk_evidence', 'table_fact', 'askable_question', 'clinical_fact', 'threshold', 'workflow_step', 'medication_monitoring', 'alias', 'vocabulary_term', 'visual_summary', 'flowchart_step', 'diagram_decision', 'risk_matrix_cell', 'medication_chart_row', 'chart_finding', 'visual_askable_question', 'table_threshold')), - source_chunk_id uuid references public.document_chunks(id) on delete cascade, - source_image_id uuid references public.document_images(id) on delete set null, - page_start integer, - page_end integer, - heading_path text[] not null default '{}', - title text not null, - content text not null, - normalized_terms text[] not null default '{}', - source_span jsonb, - quality_score real not null default 0.7 check (quality_score >= 0 and quality_score <= 1), - extraction_mode text not null default 'deterministic' check (extraction_mode in ('deterministic', 'model_heavy', 'hybrid')), - index_generation_id uuid, - embedding extensions.vector(1536) not null, - metadata jsonb not null default '{}'::jsonb, - search_tsv tsvector generated always as (to_tsvector('english', coalesce(unit_type, '') || ' ' || coalesce(title, '') || ' ' || coalesce(content, ''))) stored, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists document_index_units_document_idx on public.document_index_units(document_id, unit_type, page_start); -create index if not exists document_index_units_owner_type_idx on public.document_index_units(owner_id, unit_type, quality_score desc); -create index if not exists document_index_units_owner_chunk_type_idx - on public.document_index_units(owner_id, source_chunk_id, unit_type) - where source_chunk_id is not null; -create index if not exists document_index_units_chunk_idx on public.document_index_units(source_chunk_id) where source_chunk_id is not null; -create index if not exists document_index_units_image_idx on public.document_index_units(source_image_id) where source_image_id is not null; -create index if not exists document_index_units_terms_idx on public.document_index_units using gin(normalized_terms); -create index if not exists document_index_units_heading_path_idx on public.document_index_units using gin(heading_path); -create index if not exists document_index_units_search_idx on public.document_index_units using gin(search_tsv); --- Intentionally no HNSW index on document_index_units.embedding: the hybrid RPC is --- text-candidate-gated so the vector path never used it (0 lifetime scans; dropped --- live 2026-07-02 by the drop_legacy_vector_indexes migration). Re-add only if the --- RPC is rewritten to take a vector-first candidate path. - drop trigger if exists document_index_units_updated_at on public.document_index_units; create trigger document_index_units_updated_at before update on public.document_index_units @@ -4111,18 +4444,39 @@ begin end; $$; +revoke execute on function public.reset_document_index(uuid) from public, anon, authenticated; +grant execute on function public.reset_document_index(uuid) to service_role; + create or replace function public.analyze_rag_tables() returns void language plpgsql set search_path = public, extensions, pg_temp as $$ +declare + table_names constant text[] := array[ + 'documents', + 'document_chunks', + 'document_pages', + 'document_images', + 'document_sections', + 'document_memory_cards', + 'document_table_facts', + 'document_embedding_fields', + 'document_index_quality', + 'document_index_units', + 'rag_queries', + 'rag_query_misses', + 'rag_retrieval_logs', + 'rag_aliases', + 'ingestion_jobs' + ]; + table_name text; begin - analyze public.document_chunks; - analyze public.document_memory_cards; - analyze public.document_index_units; - analyze public.document_embedding_fields; - analyze public.document_table_facts; - analyze public.documents; + foreach table_name in array table_names loop + if to_regclass(format('public.%I', table_name)) is not null then + execute format('analyze public.%I', table_name); + end if; + end loop; end; $$; @@ -4132,6 +4486,7 @@ grant execute on function public.analyze_rag_tables() to service_role; alter table public.document_index_units enable row level security; grant select, insert, update, delete on table public.document_index_units to service_role; grant select on table public.document_index_units to authenticated; +revoke execute on function public.match_document_index_units_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid) from public, anon, authenticated; grant execute on function public.match_document_index_units_hybrid(extensions.vector, text, integer, double precision, uuid[], uuid) to service_role; revoke execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated; grant execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) to service_role; @@ -4143,14 +4498,18 @@ revoke execute on function public.is_committed_artifact_generation(jsonb, jsonb) grant execute on function public.is_committed_artifact_generation(jsonb, jsonb) to service_role; -- Typed overload: NULL keeps legacy artifacts visible; otherwise compare to committed id -create or replace function public.is_committed_artifact_generation(p_artifact_gen_id uuid, p_document_metadata jsonb) +create or replace function public.is_committed_artifact_generation( + artifact_generation_id uuid, + document_metadata jsonb +) returns boolean language sql stable set search_path = public, extensions, pg_temp as $$ - select p_artifact_gen_id is null - or p_artifact_gen_id::text = nullif(coalesce(p_document_metadata, '{}'::jsonb)->>'index_generation_id', ''); + select artifact_generation_id is null + or artifact_generation_id::text = + nullif(coalesce(document_metadata, '{}'::jsonb)->>'index_generation_id', ''); $$; revoke execute on function public.is_committed_artifact_generation(uuid, jsonb) from public, anon, authenticated; @@ -4168,7 +4527,7 @@ create policy "document index units owner read" on public.document_index_units -- ------------------------------------------------------------------------- create table if not exists public.indexing_v3_agent_jobs ( - id uuid primary key default gen_random_uuid(), + id uuid primary key default extensions.gen_random_uuid(), document_id uuid not null references public.documents(id) on delete cascade, -- v3 agent processing status (mirrors metadata->>'indexing_v3_agent_status') status text not null default 'pending' diff --git a/tests/drift-detection.test.ts b/tests/drift-detection.test.ts new file mode 100644 index 000000000..891712399 --- /dev/null +++ b/tests/drift-detection.test.ts @@ -0,0 +1,206 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { compareDriftSnapshots, normalizedSchemaSha256 } from "../scripts/check-drift"; + +const root = join(__dirname, ".."); +const read = (relative: string) => readFileSync(join(root, relative), "utf8"); + +describe("drift manifest freshness (offline half of check:drift)", () => { + it("supabase/drift-manifest.json was generated from the current supabase/schema.sql", () => { + const manifest = JSON.parse(read("supabase/drift-manifest.json")); + expect( + manifest.schema_sha256, + "supabase/schema.sql changed without regenerating the drift manifest. Run: npm run drift:manifest (requires Docker).", + ).toBe(normalizedSchemaSha256(read("supabase/schema.sql"))); + }); + + it("manifest snapshot carries every inventory category", () => { + const { snapshot } = JSON.parse(read("supabase/drift-manifest.json")); + for (const category of [ + "extensions", + "tables", + "views", + "functions", + "indexes", + "policies", + "constraints", + "triggers", + "storage_buckets", + ]) { + expect(Array.isArray(snapshot[category]), `manifest.snapshot.${category} missing`).toBe(true); + expect((snapshot[category] as unknown[]).length, `manifest.snapshot.${category} empty`).toBeGreaterThan(0); + } + // Full-inventory sanity: the app schema is large; a tiny snapshot means the + // replay silently failed rather than schema.sql shrinking by 10x. + expect((snapshot.functions as unknown[]).length).toBeGreaterThan(40); + expect((snapshot.indexes as unknown[]).length).toBeGreaterThan(100); + expect((snapshot.tables as unknown[]).length).toBeGreaterThan(30); + }); +}); + +describe("schema_drift_snapshot definition parity (migration vs schema.sql)", () => { + const extract = (text: string) => { + const start = text.indexOf("create or replace function public.schema_drift_snapshot()"); + expect(start, "schema_drift_snapshot definition not found").toBeGreaterThanOrEqual(0); + const end = text.indexOf("grant execute on function public.schema_drift_snapshot() to service_role;", start); + expect(end, "schema_drift_snapshot grants not found").toBeGreaterThan(start); + return text.slice(start, end); + }; + + it("migration 20260706200000 and schema.sql carry byte-identical definitions", () => { + const fromMigration = extract(read("supabase/migrations/20260706200000_schema_drift_snapshot.sql")); + const fromSchema = extract(read("supabase/schema.sql")); + expect(fromSchema).toBe(fromMigration); + }); +}); + +describe("drift allowlist hygiene", () => { + const allowlist = JSON.parse(read("supabase/drift-allowlist.json")) as { + entries: { category: string; kind: string; key: string; live_key?: string; reason: string }[]; + }; + + it("every entry is well-formed with a real reason", () => { + expect(allowlist.entries.length).toBeGreaterThan(0); + for (const entry of allowlist.entries) { + expect([ + "extensions", + "tables", + "views", + "functions", + "indexes", + "policies", + "constraints", + "triggers", + "storage_buckets", + ]).toContain(entry.category); + expect(["missing_live", "unexpected_live", "mismatch", "alias"]).toContain(entry.kind); + expect(entry.key.length).toBeGreaterThan(0); + expect(entry.reason.length, `allowlist entry ${entry.category}/${entry.key} needs a reason`).toBeGreaterThan(20); + expect(entry.reason).not.toContain("UNCLASSIFIED"); + if (entry.kind === "alias") { + expect(entry.live_key, `alias entry ${entry.key} needs live_key`).toBeTruthy(); + } + } + }); + + it("has no duplicate entries", () => { + const seen = new Set(); + for (const entry of allowlist.entries) { + const id = `${entry.category}|${entry.kind}|${entry.key}|${entry.live_key ?? ""}`; + expect(seen.has(id), `duplicate allowlist entry: ${id}`).toBe(false); + seen.add(id); + } + }); +}); + +describe("compareDriftSnapshots", () => { + const base = { + extensions: [{ name: "vector", schema: "extensions" }], + tables: [ + { + name: "documents", + rls_enabled: true, + rls_forced: false, + reloptions: null, + acl: ["postgres=arwdDxtm/postgres"], + columns: [{ name: "id", type: "uuid", not_null: true, identity: "", generated: "", default: null }], + }, + ], + views: [], + functions: [{ signature: "public.fn()", def_hash: "aaa", acl: ["postgres=X/postgres"] }], + indexes: [ + { + name: "documents_pkey", + table: "documents", + def: "CREATE UNIQUE INDEX documents_pkey ON public.documents USING btree (id)", + def_hash: "h1", + }, + ], + policies: [], + constraints: [], + triggers: [], + storage_buckets: [], + }; + const clone = () => JSON.parse(JSON.stringify(base)); + + it("reports nothing when snapshots match", () => { + const r = compareDriftSnapshots(clone(), clone(), []); + expect(r.findings).toEqual([]); + expect(r.staleEntries).toEqual([]); + }); + + it("detects mismatches, missing and unexpected objects", () => { + const live = clone(); + live.functions[0].def_hash = "bbb"; + live.indexes = []; + live.tables.push({ ...base.tables[0], name: "rogue_table" }); + const r = compareDriftSnapshots(clone(), live, []); + const kinds = r.findings.map( + (f: { category: string; kind: string; key: string }) => `${f.category}:${f.kind}:${f.key}`, + ); + expect(kinds).toContain("functions:mismatch:public.fn()"); + expect(kinds).toContain("indexes:missing_live:documents_pkey"); + expect(kinds).toContain("tables:unexpected_live:rogue_table"); + expect(r.findings).toHaveLength(3); + }); + + it("treats extra live extensions as informational, missing ones as findings", () => { + const live = clone(); + live.extensions = [{ name: "pg_net", schema: "extensions" }]; + const r = compareDriftSnapshots(clone(), live, []); + expect(r.infos.some((i: string) => i.includes("pg_net"))).toBe(true); + expect(r.findings.map((f: { key: string }) => f.key)).toContain("vector"); + }); + + it("allowlist consumes matching findings and reports stale entries", () => { + const live = clone(); + live.functions[0].def_hash = "bbb"; + const entries = [ + { category: "functions", kind: "mismatch", key: "public.fn()", reason: "pending migration" }, + { category: "tables", kind: "mismatch", key: "documents", reason: "no longer true" }, + ] as never[]; + const r = compareDriftSnapshots(clone(), live, entries); + expect(r.findings).toEqual([]); + expect(r.allowed).toHaveLength(1); + expect(r.staleEntries).toHaveLength(1); + }); + + it("alias entries require an identical name-stripped definition on the same table", () => { + const expected = clone(); + const live = clone(); + live.indexes = [ + { + name: "documents_pkey_legacy", + table: "documents", + def: "CREATE UNIQUE INDEX documents_pkey_legacy ON public.documents USING btree (id)", + def_hash: "h2", + }, + ]; + const alias = [ + { + category: "indexes", + kind: "alias", + key: "documents_pkey", + live_key: "documents_pkey_legacy", + reason: "legacy live name x", + }, + ] as never[]; + const r = compareDriftSnapshots(expected, live, alias); + expect(r.findings).toEqual([]); + expect(r.allowed).toHaveLength(2); // consumes missing_live + unexpected_live + + // A legacy index with a DIFFERENT shape must NOT satisfy the alias. + const badLive = clone(); + badLive.indexes = [ + { + name: "documents_pkey_legacy", + table: "documents", + def: "CREATE INDEX documents_pkey_legacy ON public.documents USING btree (created_at)", + def_hash: "h3", + }, + ]; + const r2 = compareDriftSnapshots(expected, badLive, alias); + expect(r2.findings.map((f: { kind: string }) => f.kind)).toContain("missing_live"); + }); +});