diff --git a/CHANGELOG.md b/CHANGELOG.md index ec89c80..7102cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ This file records what changes **in the product** – process and session state ### Changed - AI service logs use the web/worker format: `time` (ISO 8601, `Z`) instead of `ts`, lower-case pino level labels (`warn`, not `WARNING`), `logger` only on library records. +- The request list reads each page from an index in its sort order instead of sorting all of the + company's requests (#61). ### Fixed - AI verifier: a unit quoted together with the neighbouring table cell (e.g. `60 | Stk.`) is now diff --git a/docs/technical/data-model.md b/docs/technical/data-model.md index 013ef01..77d0679 100644 --- a/docs/technical/data-model.md +++ b/docs/technical/data-model.md @@ -39,7 +39,8 @@ query sees zero rows and every write fails. | `rejection_reason` | text | free-text reason of a rejection, max 1000 chars (refused above, not cut – review module); also in the `request.rejected` audit event | confidential | Unique `(id, company_id)` so child tables can pin the company with composite foreign keys (FK checks -bypass RLS). Purpose: one quote request per row. Retention: open question for the customer (ADR-0001 open points). +bypass RLS). Index `(company_id, created_at desc, id desc)` serves the paged request list (#48, #61) and +every lookup by company. Purpose: one quote request per row. Retention: open question for the customer (ADR-0001 open points). ### `app.extraction_runs`, `app.extraction_segments`, `app.extracted_fields` – processing results (#7) diff --git a/src/db/migrations/0017_request_list_index.sql b/src/db/migrations/0017_request_list_index.sql new file mode 100644 index 0000000..284239c --- /dev/null +++ b/src/db/migrations/0017_request_list_index.sql @@ -0,0 +1,2 @@ +DROP INDEX "app"."requests_company_id_idx";--> statement-breakpoint +CREATE INDEX "requests_company_created_idx" ON "app"."requests" USING btree ("company_id","created_at" DESC NULLS FIRST,"id" DESC NULLS FIRST); \ No newline at end of file diff --git a/src/db/migrations/meta/0017_snapshot.json b/src/db/migrations/meta/0017_snapshot.json new file mode 100644 index 0000000..61401a8 --- /dev/null +++ b/src/db/migrations/meta/0017_snapshot.json @@ -0,0 +1,2006 @@ +{ + "id": "a3cdd9c7-27dd-40ea-8548-769ec4bd713f", + "prevId": "9fbd27f3-9dad-4f60-90d9-fbad651b7d69", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.account": { + "name": "account", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.invitation": { + "name": "invitation", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.member": { + "name": "member", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.organization": { + "name": "organization", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.rate_limit": { + "name": "rate_limit", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.session": { + "name": "session", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user": { + "name": "user", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification": { + "name": "verification", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "app.audit_events": { + "name": "audit_events", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_entity_idx": { + "name": "audit_events_entity_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_company_id_organization_id_fk": { + "name": "audit_events_company_id_organization_id_fk", + "tableFrom": "audit_events", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "audit_events_tenant_isolation": { + "name": "audit_events_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "app.documents": { + "name": "documents", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_id_idx": { + "name": "documents_company_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_request_id_idx": { + "name": "documents_request_id_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_organization_id_fk": { + "name": "documents_company_id_organization_id_fk", + "tableFrom": "documents", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "documents_request_same_company_fk": { + "name": "documents_request_same_company_fk", + "tableFrom": "documents", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "request_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "documents_tenant_isolation": { + "name": "documents_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": { + "documents_kind_check": { + "name": "documents_kind_check", + "value": "kind in ('eml', 'msg', 'pdf', 'xlsx', 'docx')" + } + }, + "isRLSEnabled": true + }, + "app.extracted_fields": { + "name": "extracted_fields", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "field_key": { + "name": "field_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_status": { + "name": "model_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "segment_id": { + "name": "segment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "item_index": { + "name": "item_index", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "extracted_fields_request_idx": { + "name": "extracted_fields_request_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extracted_fields_run_same_company_fk": { + "name": "extracted_fields_run_same_company_fk", + "tableFrom": "extracted_fields", + "tableTo": "extraction_runs", + "schemaTo": "app", + "columnsFrom": [ + "run_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extracted_fields_request_same_company_fk": { + "name": "extracted_fields_request_same_company_fk", + "tableFrom": "extracted_fields", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "request_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "extracted_fields_evidence_segment_fk": { + "name": "extracted_fields_evidence_segment_fk", + "tableFrom": "extracted_fields", + "tableTo": "extraction_segments", + "schemaTo": "app", + "columnsFrom": [ + "run_id", + "document_id", + "segment_id" + ], + "columnsTo": [ + "run_id", + "document_id", + "segment_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "extracted_fields_run_field_item_unique": { + "name": "extracted_fields_run_field_item_unique", + "nullsNotDistinct": true, + "columns": [ + "run_id", + "field_key", + "item_index" + ] + } + }, + "policies": { + "extracted_fields_tenant_isolation": { + "name": "extracted_fields_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": { + "extracted_fields_item_index_check": { + "name": "extracted_fields_item_index_check", + "value": "item_index is null or item_index >= 0" + }, + "extracted_fields_status_check": { + "name": "extracted_fields_status_check", + "value": "status in ('found', 'uncertain', 'missing', 'unverified')" + }, + "extracted_fields_found_has_evidence": { + "name": "extracted_fields_found_has_evidence", + "value": "status <> 'found' or (quote is not null and segment_id is not null)" + } + }, + "isRLSEnabled": true + }, + "app.extraction_runs": { + "name": "extraction_runs", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "documents": { + "name": "documents", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "extraction_runs_request_idx": { + "name": "extraction_runs_request_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "extraction_runs_company_id_organization_id_fk": { + "name": "extraction_runs_company_id_organization_id_fk", + "tableFrom": "extraction_runs", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "extraction_runs_request_same_company_fk": { + "name": "extraction_runs_request_same_company_fk", + "tableFrom": "extraction_runs", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "request_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "extraction_runs_job_id_unique": { + "name": "extraction_runs_job_id_unique", + "nullsNotDistinct": false, + "columns": [ + "job_id" + ] + }, + "extraction_runs_id_company_unique": { + "name": "extraction_runs_id_company_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "company_id" + ] + } + }, + "policies": { + "extraction_runs_tenant_isolation": { + "name": "extraction_runs_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "app.extraction_segments": { + "name": "extraction_segments", + "schema": "app", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "segment_id": { + "name": "segment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locator": { + "name": "locator", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "extraction_segments_run_same_company_fk": { + "name": "extraction_segments_run_same_company_fk", + "tableFrom": "extraction_segments", + "tableTo": "extraction_runs", + "schemaTo": "app", + "columnsFrom": [ + "run_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "extraction_segments_pk": { + "name": "extraction_segments_pk", + "columns": [ + "run_id", + "document_id", + "segment_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": { + "extraction_segments_tenant_isolation": { + "name": "extraction_segments_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "app.field_corrections": { + "name": "field_corrections", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "field_key": { + "name": "field_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_index": { + "name": "item_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "corrected_by": { + "name": "corrected_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "field_corrections_request_idx": { + "name": "field_corrections_request_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "field_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "field_corrections_request_same_company_fk": { + "name": "field_corrections_request_same_company_fk", + "tableFrom": "field_corrections", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "request_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": { + "field_corrections_tenant_isolation": { + "name": "field_corrections_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": { + "field_corrections_item_index_check": { + "name": "field_corrections_item_index_check", + "value": "item_index is null or item_index >= 0" + } + }, + "isRLSEnabled": true + }, + "app.request_exports": { + "name": "request_exports", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "erp_reference": { + "name": "erp_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "request_exports_company_id_organization_id_fk": { + "name": "request_exports_company_id_organization_id_fk", + "tableFrom": "request_exports", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "request_exports_request_same_company_fk": { + "name": "request_exports_request_same_company_fk", + "tableFrom": "request_exports", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "request_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "request_exports_request_id_unique": { + "name": "request_exports_request_id_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id" + ] + } + }, + "policies": { + "request_exports_tenant_isolation": { + "name": "request_exports_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": { + "request_exports_status_check": { + "name": "request_exports_status_check", + "value": "status in ('pending', 'succeeded')" + }, + "request_exports_succeeded_has_reference": { + "name": "request_exports_succeeded_has_reference", + "value": "status <> 'succeeded' or (erp_reference is not null and exported_at is not null)" + } + }, + "isRLSEnabled": true + }, + "app.requests": { + "name": "requests", + "schema": "app", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'NEW'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'upload'" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "possible_duplicate": { + "name": "possible_duplicate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "duplicate_of_id": { + "name": "duplicate_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error_stage": { + "name": "error_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duplicate_decision": { + "name": "duplicate_decision", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "requests_company_created_idx": { + "name": "requests_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "first" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "requests_company_message_id_idx": { + "name": "requests_company_message_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "requests_company_fingerprint_idx": { + "name": "requests_company_fingerprint_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "requests_company_id_organization_id_fk": { + "name": "requests_company_id_organization_id_fk", + "tableFrom": "requests", + "tableTo": "organization", + "schemaTo": "auth", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "requests_duplicate_same_company_fk": { + "name": "requests_duplicate_same_company_fk", + "tableFrom": "requests", + "tableTo": "requests", + "schemaTo": "app", + "columnsFrom": [ + "duplicate_of_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "requests_id_company_unique": { + "name": "requests_id_company_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "company_id" + ] + } + }, + "policies": { + "requests_tenant_isolation": { + "name": "requests_tenant_isolation", + "as": "PERMISSIVE", + "for": "ALL", + "to": [ + "public" + ], + "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid", + "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid" + } + }, + "checkConstraints": { + "requests_status_check": { + "name": "requests_status_check", + "value": "status in ('NEW', 'PROCESSING', 'REVIEW', 'APPROVED', 'EXPORTED', 'REJECTED', 'ERROR')" + }, + "requests_duplicate_decision_check": { + "name": "requests_duplicate_decision_check", + "value": "duplicate_decision is null or duplicate_decision in ('distinct', 'duplicate')" + } + }, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": { + "auth": "auth", + "app": "app" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index d4d1acd..dd2d753 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1790248059090, "tag": "0016_correction_item_index_check", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1790256184345, + "tag": "0017_request_list_index", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/app.ts b/src/db/schema/app.ts index 96a0c43..85e6304 100644 --- a/src/db/schema/app.ts +++ b/src/db/schema/app.ts @@ -55,7 +55,9 @@ export const requests = appSchema duplicateDecision: text("duplicate_decision", { enum: ["distinct", "duplicate"] }), }, (table) => [ - index("requests_company_id_idx").on(table.companyId), + // Request list keyset paging (#48, #61): one company, newest first, ties by id. Its leading + // column also serves every lookup by company_id alone. + index("requests_company_created_idx").on(table.companyId, table.createdAt.desc().nullsFirst(), table.id.desc().nullsFirst()), index("requests_company_message_id_idx").on(table.companyId, table.messageId), index("requests_company_fingerprint_idx").on(table.companyId, table.fingerprint), // FK checks bypass RLS: child rows pin company_id through composite keys. diff --git a/tests/integration/indexes.test.ts b/tests/integration/indexes.test.ts new file mode 100644 index 0000000..198326c --- /dev/null +++ b/tests/integration/indexes.test.ts @@ -0,0 +1,56 @@ +import pg from "pg"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +// #61: the request list pages by (created_at desc, id desc) within one company (#48). The index must +// match that order, so a page is read from the index instead of sorting all of the company's requests. +describe("indexes: request list keyset paging", () => { + let owner: pg.Client; + + beforeAll(async () => { + owner = new pg.Client({ connectionString: process.env.MIGRATION_DATABASE_URL }); + await owner.connect(); + }); + + afterAll(async () => { + await owner.end(); + }); + + it("has an index on (company_id, created_at desc, id desc)", async () => { + const { rows } = await owner.query<{ indexdef: string }>( + "select indexdef from pg_indexes where schemaname = 'app' and tablename = 'requests' and indexname = 'requests_company_created_idx'", + ); + expect(rows.map((row) => row.indexdef)).toEqual([ + "CREATE INDEX requests_company_created_idx ON app.requests USING btree (company_id, created_at DESC, id DESC)", + ]); + }); + + it("lets the planner read a page of one company from that index without a sort", async () => { + await owner.query("begin"); + try { + // The planner's choice depends on the test table's size and statistics. Discouraging sequential + // scans, bitmap scans and sorts asks a stable question instead: CAN the index serve the query in + // its order? If it cannot, the plan still contains a Sort node (the switches only raise costs). + await owner.query("set local enable_seqscan = off"); + await owner.query("set local enable_bitmapscan = off"); + await owner.query("set local enable_sort = off"); + const explain = async (query: string) => + (await owner.query<{ "QUERY PLAN": string }>(`explain ${query}`)).rows.map((row) => row["QUERY PLAN"]).join("\n"); + const company = "'00000000-0000-0000-0000-000000000001'"; + const first = await explain(`select * from app.requests where company_id = ${company} order by created_at desc, id desc limit 51`); + expect(first).toContain("requests_company_created_idx"); + expect(first).not.toMatch(/\bSort\b/); + // A following page seeks with the row comparison of #48: it must be part of the index condition + // (a range scan from the cursor), not a filter over all of the company's rows. + const next = await explain( + `select * from app.requests where company_id = ${company} + and (created_at, id) < ('2026-09-24T12:00:00Z'::timestamptz, '00000000-0000-0000-0000-000000000009'::uuid) + order by created_at desc, id desc limit 51`, + ); + expect(next).toContain("requests_company_created_idx"); + expect(next).not.toMatch(/\bSort\b/); + expect(next).toMatch(/Index Cond: .*ROW\(created_at, id\) < ROW/); + } finally { + await owner.query("rollback"); + } + }); +});