-- v1.1.2: Documents — schemaless JSONB store with basic query semantics. -- -- Identity tuple `(app_id, collection, id)`. `id` is a server-generated -- UUID; scripts never supply it on create. `app_id` is first in the -- primary key so the implicit index is always per-app — cross-app reads -- are impossible even under a buggy query. -- -- `data` is JSONB so scripts can store nested structures without a -- separate serialization step. The GIN-on-`jsonb_path_ops` index -- accelerates the v1.1.2 query DSL's equality and containment operators -- (`docs::find` with `$eq` / `$in`); range/comparison operators rely on -- the per-collection seq scan within the small `app_id` partition. -- -- `created_at` / `updated_at` are server-managed: created on insert, -- bumped on every successful update. The returned doc envelope surfaces -- both fields to scripts for read-only access (no script-side override). CREATE TABLE docs ( app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE, collection TEXT NOT NULL, id UUID NOT NULL, data JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (app_id, collection, id) ); -- The dispatcher/find hot path: "all docs in app X / collection Y." -- The PK already covers (app_id, collection) as a prefix but spelling -- out the explicit index makes intent clear for the planner. Mirrors -- 0007_kv.sql's idx_kv_entries_app_collection. CREATE INDEX idx_docs_app_collection ON docs (app_id, collection); -- GIN on JSONB with the `jsonb_path_ops` opclass: smaller index than -- the default `jsonb_ops`, supports `@>` (containment) which is what -- equality filters compile to under the GIN-friendly path. Range -- operators ($gt/$gte/$lt/$lte/$ne) fall back to per-collection scans; -- those are still bounded by the (app_id, collection) selectivity. CREATE INDEX idx_docs_data_gin ON docs USING GIN (data jsonb_path_ops);