@happyvertical/smrt-core 0.57.45 → 0.57.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -3
- package/agents/browser-entry.md +16 -0
- package/agents/build-knowledge.md +58 -14
- package/agents/change-feed.md +20 -0
- package/agents/generators.md +69 -3
- package/agents/schema-paths.md +82 -0
- package/dist/_virtual/_rolldown/runtime.js +22 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts +46 -0
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +96 -7
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +7 -4
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +3 -3
- package/dist/class.js.map +1 -1
- package/dist/consumer-plugin/index.d.ts +13 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +69 -13
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/consumer-plugin.js +2 -2
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +3 -3
- package/dist/database.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +1 -4
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/index.d.ts +1 -1
- package/dist/dispatch/index.d.ts.map +1 -1
- package/dist/dispatch/types.d.ts +22 -3
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +2 -2
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/custom-action.d.ts +11 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +23 -4
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/lazy-config.d.ts +120 -3
- package/dist/lazy-config.d.ts.map +1 -1
- package/dist/lazy-config.js +81 -10
- package/dist/lazy-config.js.map +1 -1
- package/dist/manifest/generator.d.ts +8 -2
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +6 -5
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/index.d.ts +1 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +2 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/local-manifest-reader.d.ts +13 -2
- package/dist/manifest/local-manifest-reader.d.ts.map +1 -1
- package/dist/manifest/local-manifest-reader.js +38 -6
- package/dist/manifest/local-manifest-reader.js.map +1 -1
- package/dist/manifest/manifest-loader.d.ts +9 -8
- package/dist/manifest/manifest-loader.d.ts.map +1 -1
- package/dist/manifest/manifest-loader.js +67 -27
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/object-declarations.d.ts +116 -0
- package/dist/manifest/object-declarations.d.ts.map +1 -0
- package/dist/manifest/object-declarations.js +161 -0
- package/dist/manifest/object-declarations.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +5 -3
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.d.ts.map +1 -1
- package/dist/manifest/store.js +18 -2
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-artifacts.d.ts +57 -0
- package/dist/manifest/test-artifacts.d.ts.map +1 -0
- package/dist/manifest/test-artifacts.js +111 -0
- package/dist/manifest/test-artifacts.js.map +1 -0
- package/dist/manifest.js +2 -1
- package/dist/manifest.json +5 -3
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +11 -3
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -1
- package/dist/migrations/qualified-name-aliases.d.ts.map +1 -1
- package/dist/migrations/qualified-name-aliases.js +1 -1
- package/dist/migrations/qualified-name-aliases.js.map +1 -1
- package/dist/migrations.js +3 -1
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +2 -2
- package/dist/object.js.map +1 -1
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +4 -3
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +30 -3
- package/dist/postgres-timeouts.d.ts.map +1 -1
- package/dist/postgres-timeouts.js +39 -2
- package/dist/postgres-timeouts.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +14 -11
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +24 -2
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/collection-exposure.d.ts +27 -0
- package/dist/registry/collection-exposure.d.ts.map +1 -1
- package/dist/registry/collection-exposure.js +74 -1
- package/dist/registry/collection-exposure.js.map +1 -1
- package/dist/registry/collection-resolution.d.ts.map +1 -1
- package/dist/registry/collection-resolution.js +1 -0
- package/dist/registry/collection-resolution.js.map +1 -1
- package/dist/registry/framework-base-classes.d.ts.map +1 -1
- package/dist/registry/framework-base-classes.js +5 -0
- package/dist/registry/framework-base-classes.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +8 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +17 -4
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +12 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +2 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +30 -10
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +10 -8
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +29 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/generator.d.ts +32 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +59 -0
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +4 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/scripts/browser-unbundled-dev.mjs +90 -0
- package/dist/scripts/generate-test-manifest.js +9 -49
- package/dist/smrt-knowledge.json +16 -16
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +12 -4
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/schema.d.ts +70 -8
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +231 -24
- package/dist/system/schema.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +14 -2
- package/dist/testing/database.js.map +1 -1
- package/dist/tools/tool-generator.d.ts.map +1 -1
- package/dist/tools/tool-generator.js +2 -2
- package/dist/tools/tool-generator.js.map +1 -1
- package/dist/utils/import-workspace-module.d.ts.map +1 -1
- package/dist/utils/import-workspace-module.js +23 -0
- package/dist/utils/import-workspace-module.js.map +1 -1
- package/dist/utils/naming.js +5 -3
- package/dist/utils/naming.js.map +1 -1
- package/dist/vendor/pluralize/pluralize.js +421 -0
- package/dist/vendor/pluralize/pluralize.js.map +1 -0
- package/dist/vite-plugin/api-client-entries.d.ts +5 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +11 -6
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/consumer-import.d.ts +28 -0
- package/dist/vite-plugin/consumer-import.d.ts.map +1 -0
- package/dist/vite-plugin/consumer-import.js +42 -0
- package/dist/vite-plugin/consumer-import.js.map +1 -0
- package/dist/vite-plugin/dev-plane-route.d.ts +6 -1
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
- package/dist/vite-plugin/dev-plane-route.js +8 -9
- package/dist/vite-plugin/dev-plane-route.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +12 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/resources-route.d.ts.map +1 -1
- package/dist/vite-plugin/resources-route.js +3 -8
- package/dist/vite-plugin/resources-route.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +7 -11
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts +16 -0
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +48 -6
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../../src/system/compatibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA6BtD,wBAAsB,WAAW,CAC/B,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AA+BD,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,UAAU,CAAC,OAAO,YAAY,CAAC,CAMjC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4CAA4C;IAC3D;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,oCAAoC;IACnD,IAAI,EAAE,QAAQ,GAAG,sBAAsB,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACb;AAMD;;;;GAIG;AACH,wBAAsB,qCAAqC,CACzD,EAAE,EAAE,iBAAiB,EACrB,YAAY,EAAE,4CAA4C,EAC1D,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,oCAAoC,EAAE,CAAC,CA8DjD;AAED;;;;GAIG;AACH,wBAAsB,qCAAqC,CACzD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,iBAAiB,EACrB,YAAY,EAAE,4CAA4C,EAC1D,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Cf;
|
|
1
|
+
{"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../../src/system/compatibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA6BtD,wBAAsB,WAAW,CAC/B,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AA+BD,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,UAAU,CAAC,OAAO,YAAY,CAAC,CAMjC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,4CAA4C;IAC3D;;;OAGG;IACH,cAAc,EAAE,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,oCAAoC;IACnD,IAAI,EAAE,QAAQ,GAAG,sBAAsB,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;CACb;AAMD;;;;GAIG;AACH,wBAAsB,qCAAqC,CACzD,EAAE,EAAE,iBAAiB,EACrB,YAAY,EAAE,4CAA4C,EAC1D,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,oCAAoC,EAAE,CAAC,CA8DjD;AAED;;;;GAIG;AACH,wBAAsB,qCAAqC,CACzD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAsB,+BAA+B,CACnD,EAAE,EAAE,iBAAiB,EACrB,YAAY,EAAE,4CAA4C,EAC1D,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Cf;AA0eD,wBAAsB,sCAAsC,CAC1D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,mDAAmD,CACvE,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAmED,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Df;AAED,wBAAsB,uCAAuC,CAC3D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAwCf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sCAAsC,CAC1D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAe/B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,uCAAuC,CAC3D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;GAOG;AACH,wBAAsB,oCAAoC,CACxD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -184,6 +184,14 @@ function isDuplicateIndexRaceError(error, indexName) {
|
|
|
184
184
|
if (hasRelationExistsCode && /relation .*already exists/i.test(message)) return true;
|
|
185
185
|
return false;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* A tenant id column added to an existing system table: native uuid on
|
|
189
|
+
* PostgreSQL, like fresh system DDL and every application `tenant_id`
|
|
190
|
+
* (#3297); TEXT elsewhere.
|
|
191
|
+
*/
|
|
192
|
+
function tenantIdColumnType(db, typeHint) {
|
|
193
|
+
return getDatabaseEngine(db, typeHint) === "postgres" ? "UUID" : "TEXT";
|
|
194
|
+
}
|
|
187
195
|
async function addColumnIfMissing(db, tableName, columnName, definition, typeHint) {
|
|
188
196
|
if (getDatabaseEngine(db, typeHint) === "postgres") {
|
|
189
197
|
if (await columnExists(db, tableName, columnName, typeHint)) return;
|
|
@@ -400,7 +408,7 @@ async function ensureDispatchSystemTableCompatibility(db, typeHint) {
|
|
|
400
408
|
if (!await tableExists(db, "_smrt_dispatch", typeHint)) return;
|
|
401
409
|
await addColumnIfMissing(db, "_smrt_dispatch", "target_subscriber", "TEXT", typeHint);
|
|
402
410
|
await addColumnIfMissing(db, "_smrt_dispatch", "correlation_id", "TEXT", typeHint);
|
|
403
|
-
await addColumnIfMissing(db, "_smrt_dispatch", "tenant_id",
|
|
411
|
+
await addColumnIfMissing(db, "_smrt_dispatch", "tenant_id", tenantIdColumnType(db, typeHint), typeHint);
|
|
404
412
|
await addIndexIfMissing(db, "idx_smrt_dispatch_target", "_smrt_dispatch", "target_subscriber", typeHint);
|
|
405
413
|
await addIndexIfMissing(db, "idx_smrt_dispatch_correlation", "_smrt_dispatch", "correlation_id", typeHint);
|
|
406
414
|
await addIndexIfMissing(db, "idx_smrt_dispatch_tenant_id", "_smrt_dispatch", "tenant_id", typeHint);
|
|
@@ -408,7 +416,7 @@ async function ensureDispatchSystemTableCompatibility(db, typeHint) {
|
|
|
408
416
|
async function ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint) {
|
|
409
417
|
if (!await tableExists(db, "_smrt_dispatch_subscriptions", typeHint)) return;
|
|
410
418
|
await addColumnIfMissing(db, "_smrt_dispatch_subscriptions", "delivery", "TEXT NOT NULL DEFAULT 'compete'", typeHint);
|
|
411
|
-
await addColumnIfMissing(db, "_smrt_dispatch_subscriptions", "tenant_id",
|
|
419
|
+
await addColumnIfMissing(db, "_smrt_dispatch_subscriptions", "tenant_id", tenantIdColumnType(db, typeHint), typeHint);
|
|
412
420
|
await addIndexIfMissing(db, "idx_smrt_dispatch_subs_tenant_id", "_smrt_dispatch_subscriptions", "tenant_id", typeHint);
|
|
413
421
|
await migrateDispatchSubscriptionsIdentity(db, typeHint);
|
|
414
422
|
}
|
|
@@ -446,7 +454,7 @@ async function reconcileJobsTaskIdUniqueness(db, typeHint) {
|
|
|
446
454
|
}
|
|
447
455
|
async function ensureJobsSystemTableCompatibility(db, typeHint) {
|
|
448
456
|
if (!await tableExists(db, "_smrt_jobs", typeHint)) return;
|
|
449
|
-
await addColumnIfMissing(db, "_smrt_jobs", "tenant_id",
|
|
457
|
+
await addColumnIfMissing(db, "_smrt_jobs", "tenant_id", tenantIdColumnType(db, typeHint), typeHint);
|
|
450
458
|
await addIndexIfMissing(db, "idx_smrt_jobs_tenant_id", "_smrt_jobs", "tenant_id", typeHint);
|
|
451
459
|
await addColumnIfMissing(db, "_smrt_jobs", "task_id", "TEXT", typeHint);
|
|
452
460
|
await addColumnIfMissing(db, "_smrt_jobs", "task_owner_id", "TEXT", typeHint);
|
|
@@ -459,7 +467,7 @@ async function ensureJobsSystemTableCompatibility(db, typeHint) {
|
|
|
459
467
|
}
|
|
460
468
|
async function ensureJobEventsSystemTableCompatibility(db, typeHint) {
|
|
461
469
|
if (!await tableExists(db, "_smrt_job_events", typeHint)) return;
|
|
462
|
-
await addColumnIfMissing(db, "_smrt_job_events", "tenant_id",
|
|
470
|
+
await addColumnIfMissing(db, "_smrt_job_events", "tenant_id", tenantIdColumnType(db, typeHint), typeHint);
|
|
463
471
|
await addIndexIfMissing(db, "idx_smrt_job_events_tenant_id", "_smrt_job_events", "tenant_id", typeHint);
|
|
464
472
|
await addIndexIfMissing(db, "idx_smrt_job_events_job_id", "_smrt_job_events", "job_id", typeHint);
|
|
465
473
|
await addIndexIfMissing(db, "idx_smrt_job_events_type", "_smrt_job_events", "type", typeHint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.js","names":[],"sources":["../../src/system/compatibility.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport {\n CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION,\n LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY,\n} from './schema.js';\n\ntype DatabaseWithConfig = DatabaseInterface & {\n config?: {\n type?: string;\n url?: string;\n };\n type?: string;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n\n return [];\n}\n\nexport async function tableExists(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1 LIMIT 1',\n tableName,\n );\n return getQueryRows(result).length > 0;\n }\n\n await db.query(`SELECT 1 FROM ${tableName} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function columnExists(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',\n tableName,\n columnName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(`PRAGMA table_info(${tableName})`);\n return getQueryRows(result).some((row) => row.name === columnName);\n } catch {\n return false;\n }\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseWithConfig;\n return db.url || dbWithConfig.config?.url || '';\n}\n\nexport function getDatabaseEngine(\n db: DatabaseInterface,\n typeHint?: string,\n): ReturnType<typeof detectEngine> {\n const dbWithConfig = db as DatabaseWithConfig;\n return detectEngine(\n getDatabaseUrl(db),\n typeHint || dbWithConfig.type || dbWithConfig.config?.type,\n );\n}\n\n/**\n * Upgrade framework-owned PostgreSQL timestamp columns to instant-safe storage.\n *\n * Legacy SMRT and system writers serialized instants as UTC wall times. The\n * conversion is intentionally fail-closed unless the migration session is UTC:\n * operators must first confirm that historical database-default/raw writers\n * also used UTC. A deployment with non-UTC legacy writers needs an explicit,\n * provenance-aware data migration rather than a guessed offset.\n */\nexport interface PostgresSystemTimestampMigrationConfirmation {\n /**\n * Explicitly confirmed timezone used by every historical writer of legacy\n * timezone-naive system timestamps. Only UTC is supported by this helper.\n */\n legacyTimezone: 'UTC';\n}\n\nexport interface PostgresSystemTimestampMigrationPlan {\n kind: 'column' | 'change-feed-function';\n tableName: string;\n columnName?: string;\n sql: string;\n}\n\nfunction quotePostgresIdentifier(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Read-only plan for the framework-owned timestamp conversion. This is kept\n * separate from the atomic mutator so CLI dry-runs can report every ALTER\n * without bootstrapping or changing framework tables.\n */\nexport async function planPostgresSystemTimestampMigrations(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<PostgresSystemTimestampMigrationPlan[]> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return [];\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n const sessionRows = getQueryRows(\n await db.query(`\n SELECT\n current_setting('TimeZone') AS timezone,\n to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n IS NOT NULL AS legacy_change_feed_function_exists\n `),\n );\n const session = sessionRows[0] ?? {};\n const timezone = String(session.timezone ?? '').toUpperCase();\n if (!['UTC', 'ETC/UTC', 'GMT'].includes(timezone)) {\n throw new Error(\n `Refusing to preview legacy SMRT timestamp migration outside a UTC PostgreSQL session (current TimeZone: ${String(session.timezone ?? 'unknown')})`,\n );\n }\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n\n const plans: PostgresSystemTimestampMigrationPlan[] = rows.map((row) => {\n const tableName = String(row.table_name);\n const columnName = String(row.column_name);\n const table = quotePostgresIdentifier(tableName);\n const column = quotePostgresIdentifier(columnName);\n return {\n kind: 'column',\n tableName,\n columnName,\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE TIMESTAMPTZ USING ${column} AT TIME ZONE 'UTC'`,\n };\n });\n\n if (\n session.legacy_change_feed_function_exists === true ||\n session.legacy_change_feed_function_exists === 't' ||\n session.legacy_change_feed_function_exists === 'true' ||\n session.legacy_change_feed_function_exists === 1\n ) {\n plans.push({\n kind: 'change-feed-function',\n tableName: '_smrt_changes',\n sql: `DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}`,\n });\n }\n\n return plans;\n}\n\n/**\n * Fail closed when framework-owned PostgreSQL tables still contain legacy\n * timezone-naive timestamps. Call this before recording the current system\n * schema version so a partial installation cannot be stamped as upgraded.\n */\nexport async function assertPostgresSystemTimestampsCurrent(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n if (rows.length === 0) return;\n\n const columns = rows\n .map((row) => `${String(row.table_name)}.${String(row.column_name)}`)\n .join(', ');\n throw new Error(\n `Legacy SMRT system timestamps remain (${columns}); run the explicit audited migratePostgresSystemTimestamps() migration before bootstrap`,\n );\n}\n\nexport async function migratePostgresSystemTimestamps(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n // One server-side statement makes the catalog change atomic and holds the\n // same transaction-scoped lock used by system bootstrap. Any failing column\n // rolls back every preceding ALTER in the block.\n await db.query(`\n DO $smrt_migrate_system_timestamps$\n DECLARE\n target RECORD;\n BEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN\n RAISE EXCEPTION 'Refusing to reinterpret legacy SMRT timestamps outside a UTC PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n END IF;\n FOR target IN\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n LOOP\n EXECUTE format(\n 'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE ''UTC''',\n target.table_name,\n target.column_name,\n target.column_name\n );\n END LOOP;\n IF to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NOT NULL THEN\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\n END;\n $smrt_migrate_system_timestamps$;\n `);\n}\n\nfunction isDuplicateColumnError(error: unknown): boolean {\n const message =\n error && typeof error === 'object'\n ? String((error as { message?: unknown }).message || error)\n : String(error);\n\n return (\n /column .*already exists/i.test(message) ||\n /duplicate column name/i.test(message)\n );\n}\n\nfunction collectErrorDetails(\n error: unknown,\n seen = new Set<unknown>(),\n): {\n codes: Set<string>;\n messages: string[];\n} {\n const codes = new Set<string>();\n const messages: string[] = [];\n\n if (!error || seen.has(error)) {\n return { codes, messages };\n }\n\n seen.add(error);\n\n if (typeof error === 'object') {\n const errorRecord = error as Record<string, unknown>;\n\n if (typeof errorRecord.code === 'string') {\n codes.add(errorRecord.code);\n }\n\n for (const messageKey of ['message', 'detail']) {\n if (typeof errorRecord[messageKey] === 'string') {\n messages.push(errorRecord[messageKey]);\n }\n }\n\n for (const nestedKey of ['cause', 'context', 'originalError']) {\n const nested = collectErrorDetails(errorRecord[nestedKey], seen);\n for (const code of nested.codes) {\n codes.add(code);\n }\n messages.push(...nested.messages);\n }\n } else {\n messages.push(String(error));\n }\n\n return { codes, messages };\n}\n\nfunction isDuplicateIndexRaceError(error: unknown, indexName: string): boolean {\n const { codes, messages } = collectErrorDetails(error);\n const message = messages.join('\\n').toLowerCase();\n const normalizedIndexName = indexName.toLowerCase();\n const hasDuplicateCode =\n codes.has('23505') || /\\bcode\\s*=\\s*23505\\b/.test(message);\n const hasRelationExistsCode =\n codes.has('42P07') || /\\bcode\\s*=\\s*42p07\\b/i.test(message);\n\n if (!message.includes(normalizedIndexName)) {\n return false;\n }\n\n if (hasDuplicateCode && message.includes('pg_class_relname_nsp_index')) {\n return true;\n }\n\n if (hasRelationExistsCode && /relation .*already exists/i.test(message)) {\n return true;\n }\n\n return false;\n}\n\nasync function addColumnIfMissing(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n definition: string,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n if (engine === 'postgres') {\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${definition}`,\n );\n return;\n }\n\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`,\n );\n } catch (error) {\n if (isDuplicateColumnError(error)) {\n return;\n }\n\n throw error;\n }\n}\n\nasync function indexExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND indexname = $1 LIMIT 1',\n indexName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(\n `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1`,\n indexName,\n );\n return getQueryRows(result).length > 0;\n } catch {\n return false;\n }\n}\n\n/** @param columns - One column, or a comma-separated list for a composite index. */\nasync function addIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Read a PostgreSQL `text[]` result as a JS array.\n *\n * `attname` is `name`-typed, and drivers that have no parser registered for\n * the resulting array OID hand back the raw literal (`{a,b}`) instead. Reading\n * that as \"no columns\" would silently make every index look like it covers\n * nothing — and, for #2376's reconciliation, would recreate an index that\n * already exists. The query casts to `text` and this parses either shape.\n */\nfunction parsePostgresTextArray(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map(String);\n }\n if (typeof value !== 'string') {\n return [];\n }\n const literal = value.trim();\n if (!literal.startsWith('{') || !literal.endsWith('}')) {\n return [];\n }\n const body = literal.slice(1, -1).trim();\n if (body.length === 0) {\n return [];\n }\n return body\n .split(',')\n .map((entry) => entry.trim().replace(/^\"|\"$/g, ''))\n .filter((entry) => entry.length > 0);\n}\n\n/** One live index as reported by the engine's catalog. */\ninterface LiveIndex {\n name: string;\n unique: boolean;\n columns: string[];\n /** True when the index carries a WHERE predicate (partial index). */\n partial: boolean;\n}\n\n/**\n * Read the live indexes of a table, including the implicit ones a UNIQUE\n * column constraint creates (`sqlite_autoindex_*`, PostgreSQL's `*_key`).\n *\n * PostgreSQL and SQLite only. Returns `null` on DuckDB and the JSON adapter,\n * whose catalogs report constraint-backed uniqueness somewhere other than the\n * index list (`duckdb_constraints()` rather than `duckdb_indexes()`) — reading\n * one without the other would report \"no unique index\" for a column that has\n * one. Callers must treat `null` as \"unknown\" and keep their pre-existing\n * behaviour rather than guess: leaving a redundant index in place is a cost,\n * dropping the only one that enforces an upsert conflict target is a bug.\n */\nasync function listTableIndexes(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<LiveIndex[] | null> {\n const engine = getDatabaseEngine(db, typeHint);\n\n try {\n if (engine === 'postgres') {\n const result = await db.query(\n `SELECT\n i.relname AS name,\n ix.indisunique AS is_unique,\n (ix.indpred IS NOT NULL) AS is_partial,\n ARRAY(\n SELECT a.attname::text\n FROM unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a\n ON a.attrelid = t.oid AND a.attnum = k.attnum\n ORDER BY k.ord\n ) AS columns\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = current_schema() AND t.relname = $1`,\n tableName,\n );\n return getQueryRows(result).map((row) => ({\n name: String(row.name),\n unique: row.is_unique === true || row.is_unique === 't',\n partial: row.is_partial === true || row.is_partial === 't',\n columns: parsePostgresTextArray(row.columns),\n }));\n }\n\n if (engine !== 'sqlite') {\n return null;\n }\n\n const listed = getQueryRows(\n await db.query(`PRAGMA index_list(${tableName})`),\n );\n const indexes: LiveIndex[] = [];\n for (const row of listed) {\n const name = String(row.name);\n const info = getQueryRows(await db.query(`PRAGMA index_info(${name})`));\n indexes.push({\n name,\n unique: row.unique === 1 || row.unique === true || row.unique === '1',\n partial:\n row.partial === 1 || row.partial === true || row.partial === '1',\n columns: info.map((column) => String(column.name)),\n });\n }\n return indexes;\n } catch {\n return null;\n }\n}\n\n/** Drop an index, tolerating engines/permissions that refuse the statement. */\nasync function dropIndexIfExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<void> {\n if (!(await indexExists(db, indexName, typeHint))) {\n return;\n }\n\n try {\n await db.query(`DROP INDEX IF EXISTS ${indexName}`);\n } catch {\n // Best-effort: a redundant index is a cost, not a correctness problem.\n }\n}\n\nasync function addUniqueIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Migrate the legacy `UNIQUE(signal_type, subscriber)` subscription identity to\n * the tenant-scoped `UNIQUE(tenant_id, signal_type, subscriber)` (S5 #1398).\n *\n * The old identity let any tenant overwrite/delete/enable another tenant's\n * subscription because the natural key ignored the tenant. This:\n * 1. Creates the tenant-scoped unique index used by upsert conflict handling.\n * 2. Drops the legacy 2-column unique constraint:\n * - **SQLite**: inline `UNIQUE(...)` constraints can't be dropped in place,\n * so the table is rebuilt without it (data preserved). The legacy\n * constraint surfaces as an auto-created `sqlite_autoindex_*` index.\n * - **Postgres**: the inline constraint is dropped by its conventional\n * name if present.\n *\n * Additive and idempotent: re-running is a no-op once migrated.\n */\nasync function migrateDispatchSubscriptionsIdentity(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n // 1. Tenant-scoped unique index (used by upsert's ON CONFLICT target).\n await addUniqueIndexIfMissing(\n db,\n 'uq_smrt_dispatch_subs_tenant_signal_subscriber',\n '_smrt_dispatch_subscriptions',\n 'tenant_id, signal_type, subscriber',\n typeHint,\n );\n\n // 2. Drop the legacy (signal_type, subscriber) uniqueness.\n if (engine === 'postgres') {\n // Inline table-level UNIQUE constraints get a conventional name. Drop it if\n // present; ignore if it was never created under that name.\n try {\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions DROP CONSTRAINT IF EXISTS _smrt_dispatch_subscriptions_signal_type_subscriber_key',\n );\n } catch {\n // Best-effort: a differently-named legacy constraint is left in place; the\n // tenant-scoped index above still enforces correct per-tenant identity.\n }\n return;\n }\n\n // SQLite: detect a legacy auto-index over exactly (signal_type, subscriber)\n // and, if found, rebuild the table without the inline UNIQUE.\n if (engine !== 'sqlite') {\n return;\n }\n\n let legacyAutoIndex: string | null = null;\n try {\n const result = await db.query(\n `SELECT name FROM sqlite_master\n WHERE type = 'index'\n AND tbl_name = '_smrt_dispatch_subscriptions'\n AND name LIKE 'sqlite_autoindex_%'`,\n );\n for (const row of getQueryRows(result)) {\n const indexName = String(row.name);\n const info = await db.query(`PRAGMA index_info(${indexName})`);\n const cols = getQueryRows(info)\n .map((r) => String(r.name))\n .sort();\n if (\n cols.length === 2 &&\n cols[0] === 'signal_type' &&\n cols[1] === 'subscriber'\n ) {\n legacyAutoIndex = indexName;\n break;\n }\n }\n } catch {\n // Unable to introspect → assume no legacy constraint (fresh tables created\n // from the new schema have none).\n legacyAutoIndex = null;\n }\n\n if (!legacyAutoIndex) {\n return;\n }\n\n // Copy only the columns the legacy table actually has. The rebuild used to\n // name all nine unconditionally, so a database predating any one of them\n // failed the whole bootstrap with a bare `no such column` (issue #2376).\n // Columns absent from the source take the new table's default.\n const legacyColumns = new Set(\n getQueryRows(\n await db.query('PRAGMA table_info(_smrt_dispatch_subscriptions)'),\n ).map((row) => String(row.name)),\n );\n const copiedColumns = [\n 'id',\n 'signal_type',\n 'subscriber',\n 'handler',\n 'delivery',\n 'enabled',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ].filter((column) => legacyColumns.has(column));\n const copiedColumnList = copiedColumns.join(', ');\n\n // Rebuild the table without the inline UNIQUE(signal_type, subscriber).\n // System table is small; this runs once per legacy database.\n await db.query('PRAGMA foreign_keys=OFF');\n try {\n await db.query(`\n CREATE TABLE _smrt_dispatch_subscriptions_new (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )\n `);\n await db.query(`\n INSERT INTO _smrt_dispatch_subscriptions_new\n (${copiedColumnList})\n SELECT ${copiedColumnList}\n FROM _smrt_dispatch_subscriptions\n `);\n await db.query('DROP TABLE _smrt_dispatch_subscriptions');\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions_new RENAME TO _smrt_dispatch_subscriptions',\n );\n // Recreate the secondary indexes (the rebuild dropped them) plus the\n // tenant-scoped unique index.\n await db.query(\n 'CREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber ON _smrt_dispatch_subscriptions(subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id ON _smrt_dispatch_subscriptions(tenant_id)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type ON _smrt_dispatch_subscriptions(signal_type)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled ON _smrt_dispatch_subscriptions(enabled)',\n );\n } finally {\n await db.query('PRAGMA foreign_keys=ON');\n }\n}\n\nexport async function ensureDispatchSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'target_subscriber',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'correlation_id',\n 'TEXT',\n typeHint,\n );\n // Tenant isolation (S5 #1398): dispatches are scoped to the emitting\n // tenant context so a subscriber in tenant A cannot snoop/claim tenant B's\n // dispatches. Nullable so non-tenant (global) dispatches keep tenant_id NULL.\n await addColumnIfMissing(db, '_smrt_dispatch', 'tenant_id', 'TEXT', typeHint);\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_target',\n '_smrt_dispatch',\n 'target_subscriber',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_correlation',\n '_smrt_dispatch',\n 'correlation_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_tenant_id',\n '_smrt_dispatch',\n 'tenant_id',\n typeHint,\n );\n}\n\nexport async function ensureDispatchSubscriptionsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch_subscriptions', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'delivery',\n \"TEXT NOT NULL DEFAULT 'compete'\",\n typeHint,\n );\n // Tenant isolation (S5 #1398): records the tenant context a subscription was\n // registered in. Nullable so global/non-tenant subscriptions keep it NULL.\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n 'TEXT',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_subs_tenant_id',\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n typeHint,\n );\n // Migrate the subscription natural key to (tenant_id, signal_type, subscriber)\n // so tenants can't clobber each other's subscriptions (S5 #1398).\n await migrateDispatchSubscriptionsIdentity(db, typeHint);\n}\n\n/** Compat-owned unique index over `_smrt_jobs(task_id)`. */\nconst JOBS_TASK_ID_COMPAT_INDEX = 'idx_smrt_jobs_task_id';\n\n/**\n * Reconcile the two owners of `_smrt_jobs(task_id)` uniqueness (issue #2376).\n *\n * `SmrtJob.taskId` is declared `unique: true`, so a table created from the\n * jobs manifest already carries a constraint-backed unique index. Legacy\n * databases got the column from `addColumnIfMissing()` instead, and\n * `ALTER TABLE ADD COLUMN ... UNIQUE` is rejected by SQLite and DuckDB — so\n * those databases need this compat index and only this compat index.\n *\n * Creating it unconditionally left every manifest-created table with two\n * unique indexes on one column. Create it only when nothing else already\n * enforces the uniqueness, and drop it when something does.\n *\n * Known limitation: this reconciliation is PostgreSQL/SQLite-only. DuckDB and\n * the JSON adapter give {@link listTableIndexes} no usable answer, so those\n * installs keep the redundant index — deliberately, because the alternative is\n * guessing and possibly leaving the upsert conflict target unenforced.\n */\nasync function reconcileJobsTaskIdUniqueness(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const indexes = await listTableIndexes(db, '_smrt_jobs', typeHint);\n\n if (indexes === null) {\n // Engine catalog not readable here — keep the historical behaviour rather\n // than risk leaving the upsert conflict target unenforced.\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n return;\n }\n\n // A partial unique index does not enforce uniqueness over the whole table,\n // so it can never stand in for the compat index.\n const otherUniqueOnTaskId = indexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n index.name !== JOBS_TASK_ID_COMPAT_INDEX &&\n index.columns.length === 1 &&\n index.columns[0] === 'task_id',\n );\n\n if (otherUniqueOnTaskId) {\n await dropIndexIfExists(db, JOBS_TASK_ID_COMPAT_INDEX, typeHint);\n return;\n }\n\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n}\n\nexport async function ensureJobsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_jobs', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(db, '_smrt_jobs', 'tenant_id', 'TEXT', typeHint);\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_tenant_id',\n '_smrt_jobs',\n 'tenant_id',\n typeHint,\n );\n // MCP task support was added after `_smrt_jobs` had already shipped. Keep\n // the task projection in the job row so a task can never outlive (or lose\n // track of) the worker job that executes it. JSON is portable here because\n // SMRT's adapters serialize JSON fields to text where necessary.\n await addColumnIfMissing(db, '_smrt_jobs', 'task_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_owner_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_result', 'TEXT', typeHint);\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_requests',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_responses',\n 'TEXT',\n typeHint,\n );\n await reconcileJobsTaskIdUniqueness(db, typeHint);\n // Retention predicate of SmrtJobCollection.cleanup() (#2375): terminal jobs\n // aged out by `(status, completed_at)`. `_smrt_jobs` comes from a decorated\n // class rather than the hand-written system DDL, so this compatibility path\n // — which the jobs collection runs on every initialize — is where the index\n // reaches both existing and freshly migrated databases. `completed_at` is\n // guarded the same as every other column this pass touches: a table created\n // before the field existed (or a minimal test fixture) must not fail the\n // whole sweep with \"no such column\".\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'completed_at',\n 'TIMESTAMP',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_status_completed_at',\n '_smrt_jobs',\n 'status, completed_at',\n typeHint,\n );\n}\n\nexport async function ensureJobEventsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_job_events', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_job_events',\n 'tenant_id',\n 'TEXT',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_tenant_id',\n '_smrt_job_events',\n 'tenant_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_job_id',\n '_smrt_job_events',\n 'job_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_type',\n '_smrt_job_events',\n 'type',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_created_at',\n '_smrt_job_events',\n 'created_at',\n typeHint,\n );\n}\n\n/**\n * Compatibility pass for the tables `db:migrate` creates from the jobs\n * manifest rather than `bootstrapSystemTables()` (issue #2376).\n *\n * These are dual-owned, and on a *fresh* install the manifest wins the race:\n * bootstrap runs first, finds no `_smrt_jobs`, returns early, stamps the\n * system schema version — and every later boot takes the version fast path, so\n * the columns and indexes this pass owns never appeared on that database at\n * all. Splitting the pass out lets the caller re-check it after the tables\n * show up, independently of the system schema version.\n *\n * @returns `settled: true` once every deferred table exists and has been\n * upgraded — the point at which a caller may stop re-checking. `false` means\n * the tables are not there yet, not that anything failed.\n */\nexport async function ensureDeferredSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<{ settled: boolean }> {\n // Sequential, never `Promise.all`: a caller can hand us one shared\n // single-statement connection (the OIDC provisioning coordinator does), and\n // two probes in flight at once overlap on it.\n const jobsExists = await tableExists(db, '_smrt_jobs', typeHint);\n const jobEventsExists = await tableExists(db, '_smrt_job_events', typeHint);\n\n if (jobsExists) {\n await ensureJobsSystemTableCompatibility(db, typeHint);\n }\n if (jobEventsExists) {\n await ensureJobEventsSystemTableCompatibility(db, typeHint);\n }\n\n return { settled: jobsExists && jobEventsExists };\n}\n\n/**\n * The compatibility pass that must precede replaying system DDL.\n *\n * Only the tables `bootstrapSystemTables()` itself creates belong here: an\n * older install can be missing a column that the DDL's `CREATE INDEX` clauses\n * reference, so those tables have to be reshaped first, inside the same\n * transaction as the replay.\n *\n * The deferred tables are deliberately NOT part of this pass. They are owned by\n * the jobs manifest, not by the system DDL, so nothing in the replay depends on\n * them — and reshaping a table the framework does not own must not be able to\n * abort the bootstrap transaction and roll back system-table creation with it\n * (issue #2376). {@link ensureDeferredSystemTableCompatibility} runs outside\n * the lock instead.\n */\nexport async function ensureBootstrapSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureDispatchSystemTableCompatibility(db, typeHint);\n await ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint);\n}\n\n/**\n * Run every compatibility pass — the bootstrap tables and the deferred,\n * manifest-created ones.\n *\n * This is the convenience entry point for callers that own the whole database\n * lifecycle in one step (test-database setup). Framework bootstrap deliberately\n * does not use it: see {@link ensureBootstrapSystemTableCompatibility}.\n */\nexport async function ensureLegacySystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n await ensureDeferredSystemTableCompatibility(db, typeHint);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compatibility.js","names":[],"sources":["../../src/system/compatibility.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport {\n CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION,\n LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY,\n} from './schema.js';\n\ntype DatabaseWithConfig = DatabaseInterface & {\n config?: {\n type?: string;\n url?: string;\n };\n type?: string;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n\n return [];\n}\n\nexport async function tableExists(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1 LIMIT 1',\n tableName,\n );\n return getQueryRows(result).length > 0;\n }\n\n await db.query(`SELECT 1 FROM ${tableName} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function columnExists(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',\n tableName,\n columnName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(`PRAGMA table_info(${tableName})`);\n return getQueryRows(result).some((row) => row.name === columnName);\n } catch {\n return false;\n }\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseWithConfig;\n return db.url || dbWithConfig.config?.url || '';\n}\n\nexport function getDatabaseEngine(\n db: DatabaseInterface,\n typeHint?: string,\n): ReturnType<typeof detectEngine> {\n const dbWithConfig = db as DatabaseWithConfig;\n return detectEngine(\n getDatabaseUrl(db),\n typeHint || dbWithConfig.type || dbWithConfig.config?.type,\n );\n}\n\n/**\n * Upgrade framework-owned PostgreSQL timestamp columns to instant-safe storage.\n *\n * Legacy SMRT and system writers serialized instants as UTC wall times. The\n * conversion is intentionally fail-closed unless the migration session is UTC:\n * operators must first confirm that historical database-default/raw writers\n * also used UTC. A deployment with non-UTC legacy writers needs an explicit,\n * provenance-aware data migration rather than a guessed offset.\n */\nexport interface PostgresSystemTimestampMigrationConfirmation {\n /**\n * Explicitly confirmed timezone used by every historical writer of legacy\n * timezone-naive system timestamps. Only UTC is supported by this helper.\n */\n legacyTimezone: 'UTC';\n}\n\nexport interface PostgresSystemTimestampMigrationPlan {\n kind: 'column' | 'change-feed-function';\n tableName: string;\n columnName?: string;\n sql: string;\n}\n\nfunction quotePostgresIdentifier(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Read-only plan for the framework-owned timestamp conversion. This is kept\n * separate from the atomic mutator so CLI dry-runs can report every ALTER\n * without bootstrapping or changing framework tables.\n */\nexport async function planPostgresSystemTimestampMigrations(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<PostgresSystemTimestampMigrationPlan[]> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return [];\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n const sessionRows = getQueryRows(\n await db.query(`\n SELECT\n current_setting('TimeZone') AS timezone,\n to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n IS NOT NULL AS legacy_change_feed_function_exists\n `),\n );\n const session = sessionRows[0] ?? {};\n const timezone = String(session.timezone ?? '').toUpperCase();\n if (!['UTC', 'ETC/UTC', 'GMT'].includes(timezone)) {\n throw new Error(\n `Refusing to preview legacy SMRT timestamp migration outside a UTC PostgreSQL session (current TimeZone: ${String(session.timezone ?? 'unknown')})`,\n );\n }\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n\n const plans: PostgresSystemTimestampMigrationPlan[] = rows.map((row) => {\n const tableName = String(row.table_name);\n const columnName = String(row.column_name);\n const table = quotePostgresIdentifier(tableName);\n const column = quotePostgresIdentifier(columnName);\n return {\n kind: 'column',\n tableName,\n columnName,\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE TIMESTAMPTZ USING ${column} AT TIME ZONE 'UTC'`,\n };\n });\n\n if (\n session.legacy_change_feed_function_exists === true ||\n session.legacy_change_feed_function_exists === 't' ||\n session.legacy_change_feed_function_exists === 'true' ||\n session.legacy_change_feed_function_exists === 1\n ) {\n plans.push({\n kind: 'change-feed-function',\n tableName: '_smrt_changes',\n sql: `DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}`,\n });\n }\n\n return plans;\n}\n\n/**\n * Fail closed when framework-owned PostgreSQL tables still contain legacy\n * timezone-naive timestamps. Call this before recording the current system\n * schema version so a partial installation cannot be stamped as upgraded.\n */\nexport async function assertPostgresSystemTimestampsCurrent(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n if (rows.length === 0) return;\n\n const columns = rows\n .map((row) => `${String(row.table_name)}.${String(row.column_name)}`)\n .join(', ');\n throw new Error(\n `Legacy SMRT system timestamps remain (${columns}); run the explicit audited migratePostgresSystemTimestamps() migration before bootstrap`,\n );\n}\n\nexport async function migratePostgresSystemTimestamps(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n // One server-side statement makes the catalog change atomic and holds the\n // same transaction-scoped lock used by system bootstrap. Any failing column\n // rolls back every preceding ALTER in the block.\n await db.query(`\n DO $smrt_migrate_system_timestamps$\n DECLARE\n target RECORD;\n BEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN\n RAISE EXCEPTION 'Refusing to reinterpret legacy SMRT timestamps outside a UTC PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n END IF;\n FOR target IN\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n LOOP\n EXECUTE format(\n 'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE ''UTC''',\n target.table_name,\n target.column_name,\n target.column_name\n );\n END LOOP;\n IF to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NOT NULL THEN\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\n END;\n $smrt_migrate_system_timestamps$;\n `);\n}\n\nfunction isDuplicateColumnError(error: unknown): boolean {\n const message =\n error && typeof error === 'object'\n ? String((error as { message?: unknown }).message || error)\n : String(error);\n\n return (\n /column .*already exists/i.test(message) ||\n /duplicate column name/i.test(message)\n );\n}\n\nfunction collectErrorDetails(\n error: unknown,\n seen = new Set<unknown>(),\n): {\n codes: Set<string>;\n messages: string[];\n} {\n const codes = new Set<string>();\n const messages: string[] = [];\n\n if (!error || seen.has(error)) {\n return { codes, messages };\n }\n\n seen.add(error);\n\n if (typeof error === 'object') {\n const errorRecord = error as Record<string, unknown>;\n\n if (typeof errorRecord.code === 'string') {\n codes.add(errorRecord.code);\n }\n\n for (const messageKey of ['message', 'detail']) {\n if (typeof errorRecord[messageKey] === 'string') {\n messages.push(errorRecord[messageKey]);\n }\n }\n\n for (const nestedKey of ['cause', 'context', 'originalError']) {\n const nested = collectErrorDetails(errorRecord[nestedKey], seen);\n for (const code of nested.codes) {\n codes.add(code);\n }\n messages.push(...nested.messages);\n }\n } else {\n messages.push(String(error));\n }\n\n return { codes, messages };\n}\n\nfunction isDuplicateIndexRaceError(error: unknown, indexName: string): boolean {\n const { codes, messages } = collectErrorDetails(error);\n const message = messages.join('\\n').toLowerCase();\n const normalizedIndexName = indexName.toLowerCase();\n const hasDuplicateCode =\n codes.has('23505') || /\\bcode\\s*=\\s*23505\\b/.test(message);\n const hasRelationExistsCode =\n codes.has('42P07') || /\\bcode\\s*=\\s*42p07\\b/i.test(message);\n\n if (!message.includes(normalizedIndexName)) {\n return false;\n }\n\n if (hasDuplicateCode && message.includes('pg_class_relname_nsp_index')) {\n return true;\n }\n\n if (hasRelationExistsCode && /relation .*already exists/i.test(message)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * A tenant id column added to an existing system table: native uuid on\n * PostgreSQL, like fresh system DDL and every application `tenant_id`\n * (#3297); TEXT elsewhere.\n */\nfunction tenantIdColumnType(db: DatabaseInterface, typeHint?: string): string {\n return getDatabaseEngine(db, typeHint) === 'postgres' ? 'UUID' : 'TEXT';\n}\n\nasync function addColumnIfMissing(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n definition: string,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n if (engine === 'postgres') {\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${definition}`,\n );\n return;\n }\n\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`,\n );\n } catch (error) {\n if (isDuplicateColumnError(error)) {\n return;\n }\n\n throw error;\n }\n}\n\nasync function indexExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND indexname = $1 LIMIT 1',\n indexName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(\n `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1`,\n indexName,\n );\n return getQueryRows(result).length > 0;\n } catch {\n return false;\n }\n}\n\n/** @param columns - One column, or a comma-separated list for a composite index. */\nasync function addIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Read a PostgreSQL `text[]` result as a JS array.\n *\n * `attname` is `name`-typed, and drivers that have no parser registered for\n * the resulting array OID hand back the raw literal (`{a,b}`) instead. Reading\n * that as \"no columns\" would silently make every index look like it covers\n * nothing — and, for #2376's reconciliation, would recreate an index that\n * already exists. The query casts to `text` and this parses either shape.\n */\nfunction parsePostgresTextArray(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map(String);\n }\n if (typeof value !== 'string') {\n return [];\n }\n const literal = value.trim();\n if (!literal.startsWith('{') || !literal.endsWith('}')) {\n return [];\n }\n const body = literal.slice(1, -1).trim();\n if (body.length === 0) {\n return [];\n }\n return body\n .split(',')\n .map((entry) => entry.trim().replace(/^\"|\"$/g, ''))\n .filter((entry) => entry.length > 0);\n}\n\n/** One live index as reported by the engine's catalog. */\ninterface LiveIndex {\n name: string;\n unique: boolean;\n columns: string[];\n /** True when the index carries a WHERE predicate (partial index). */\n partial: boolean;\n}\n\n/**\n * Read the live indexes of a table, including the implicit ones a UNIQUE\n * column constraint creates (`sqlite_autoindex_*`, PostgreSQL's `*_key`).\n *\n * PostgreSQL and SQLite only. Returns `null` on DuckDB and the JSON adapter,\n * whose catalogs report constraint-backed uniqueness somewhere other than the\n * index list (`duckdb_constraints()` rather than `duckdb_indexes()`) — reading\n * one without the other would report \"no unique index\" for a column that has\n * one. Callers must treat `null` as \"unknown\" and keep their pre-existing\n * behaviour rather than guess: leaving a redundant index in place is a cost,\n * dropping the only one that enforces an upsert conflict target is a bug.\n */\nasync function listTableIndexes(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<LiveIndex[] | null> {\n const engine = getDatabaseEngine(db, typeHint);\n\n try {\n if (engine === 'postgres') {\n const result = await db.query(\n `SELECT\n i.relname AS name,\n ix.indisunique AS is_unique,\n (ix.indpred IS NOT NULL) AS is_partial,\n ARRAY(\n SELECT a.attname::text\n FROM unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a\n ON a.attrelid = t.oid AND a.attnum = k.attnum\n ORDER BY k.ord\n ) AS columns\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = current_schema() AND t.relname = $1`,\n tableName,\n );\n return getQueryRows(result).map((row) => ({\n name: String(row.name),\n unique: row.is_unique === true || row.is_unique === 't',\n partial: row.is_partial === true || row.is_partial === 't',\n columns: parsePostgresTextArray(row.columns),\n }));\n }\n\n if (engine !== 'sqlite') {\n return null;\n }\n\n const listed = getQueryRows(\n await db.query(`PRAGMA index_list(${tableName})`),\n );\n const indexes: LiveIndex[] = [];\n for (const row of listed) {\n const name = String(row.name);\n const info = getQueryRows(await db.query(`PRAGMA index_info(${name})`));\n indexes.push({\n name,\n unique: row.unique === 1 || row.unique === true || row.unique === '1',\n partial:\n row.partial === 1 || row.partial === true || row.partial === '1',\n columns: info.map((column) => String(column.name)),\n });\n }\n return indexes;\n } catch {\n return null;\n }\n}\n\n/** Drop an index, tolerating engines/permissions that refuse the statement. */\nasync function dropIndexIfExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<void> {\n if (!(await indexExists(db, indexName, typeHint))) {\n return;\n }\n\n try {\n await db.query(`DROP INDEX IF EXISTS ${indexName}`);\n } catch {\n // Best-effort: a redundant index is a cost, not a correctness problem.\n }\n}\n\nasync function addUniqueIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Migrate the legacy `UNIQUE(signal_type, subscriber)` subscription identity to\n * the tenant-scoped `UNIQUE(tenant_id, signal_type, subscriber)` (S5 #1398).\n *\n * The old identity let any tenant overwrite/delete/enable another tenant's\n * subscription because the natural key ignored the tenant. This:\n * 1. Creates the tenant-scoped unique index used by upsert conflict handling.\n * 2. Drops the legacy 2-column unique constraint:\n * - **SQLite**: inline `UNIQUE(...)` constraints can't be dropped in place,\n * so the table is rebuilt without it (data preserved). The legacy\n * constraint surfaces as an auto-created `sqlite_autoindex_*` index.\n * - **Postgres**: the inline constraint is dropped by its conventional\n * name if present.\n *\n * Additive and idempotent: re-running is a no-op once migrated.\n */\nasync function migrateDispatchSubscriptionsIdentity(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n // 1. Tenant-scoped unique index (used by upsert's ON CONFLICT target).\n await addUniqueIndexIfMissing(\n db,\n 'uq_smrt_dispatch_subs_tenant_signal_subscriber',\n '_smrt_dispatch_subscriptions',\n 'tenant_id, signal_type, subscriber',\n typeHint,\n );\n\n // 2. Drop the legacy (signal_type, subscriber) uniqueness.\n if (engine === 'postgres') {\n // Inline table-level UNIQUE constraints get a conventional name. Drop it if\n // present; ignore if it was never created under that name.\n try {\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions DROP CONSTRAINT IF EXISTS _smrt_dispatch_subscriptions_signal_type_subscriber_key',\n );\n } catch {\n // Best-effort: a differently-named legacy constraint is left in place; the\n // tenant-scoped index above still enforces correct per-tenant identity.\n }\n return;\n }\n\n // SQLite: detect a legacy auto-index over exactly (signal_type, subscriber)\n // and, if found, rebuild the table without the inline UNIQUE.\n if (engine !== 'sqlite') {\n return;\n }\n\n let legacyAutoIndex: string | null = null;\n try {\n const result = await db.query(\n `SELECT name FROM sqlite_master\n WHERE type = 'index'\n AND tbl_name = '_smrt_dispatch_subscriptions'\n AND name LIKE 'sqlite_autoindex_%'`,\n );\n for (const row of getQueryRows(result)) {\n const indexName = String(row.name);\n const info = await db.query(`PRAGMA index_info(${indexName})`);\n const cols = getQueryRows(info)\n .map((r) => String(r.name))\n .sort();\n if (\n cols.length === 2 &&\n cols[0] === 'signal_type' &&\n cols[1] === 'subscriber'\n ) {\n legacyAutoIndex = indexName;\n break;\n }\n }\n } catch {\n // Unable to introspect → assume no legacy constraint (fresh tables created\n // from the new schema have none).\n legacyAutoIndex = null;\n }\n\n if (!legacyAutoIndex) {\n return;\n }\n\n // Copy only the columns the legacy table actually has. The rebuild used to\n // name all nine unconditionally, so a database predating any one of them\n // failed the whole bootstrap with a bare `no such column` (issue #2376).\n // Columns absent from the source take the new table's default.\n const legacyColumns = new Set(\n getQueryRows(\n await db.query('PRAGMA table_info(_smrt_dispatch_subscriptions)'),\n ).map((row) => String(row.name)),\n );\n const copiedColumns = [\n 'id',\n 'signal_type',\n 'subscriber',\n 'handler',\n 'delivery',\n 'enabled',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ].filter((column) => legacyColumns.has(column));\n const copiedColumnList = copiedColumns.join(', ');\n\n // Rebuild the table without the inline UNIQUE(signal_type, subscriber).\n // System table is small; this runs once per legacy database.\n await db.query('PRAGMA foreign_keys=OFF');\n try {\n await db.query(`\n CREATE TABLE _smrt_dispatch_subscriptions_new (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )\n `);\n await db.query(`\n INSERT INTO _smrt_dispatch_subscriptions_new\n (${copiedColumnList})\n SELECT ${copiedColumnList}\n FROM _smrt_dispatch_subscriptions\n `);\n await db.query('DROP TABLE _smrt_dispatch_subscriptions');\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions_new RENAME TO _smrt_dispatch_subscriptions',\n );\n // Recreate the secondary indexes (the rebuild dropped them) plus the\n // tenant-scoped unique index.\n await db.query(\n 'CREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber ON _smrt_dispatch_subscriptions(subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id ON _smrt_dispatch_subscriptions(tenant_id)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type ON _smrt_dispatch_subscriptions(signal_type)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled ON _smrt_dispatch_subscriptions(enabled)',\n );\n } finally {\n await db.query('PRAGMA foreign_keys=ON');\n }\n}\n\nexport async function ensureDispatchSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'target_subscriber',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'correlation_id',\n 'TEXT',\n typeHint,\n );\n // Tenant isolation (S5 #1398): dispatches are scoped to the emitting\n // tenant context so a subscriber in tenant A cannot snoop/claim tenant B's\n // dispatches. Nullable so non-tenant (global) dispatches keep tenant_id NULL.\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'tenant_id',\n tenantIdColumnType(db, typeHint),\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_target',\n '_smrt_dispatch',\n 'target_subscriber',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_correlation',\n '_smrt_dispatch',\n 'correlation_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_tenant_id',\n '_smrt_dispatch',\n 'tenant_id',\n typeHint,\n );\n}\n\nexport async function ensureDispatchSubscriptionsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch_subscriptions', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'delivery',\n \"TEXT NOT NULL DEFAULT 'compete'\",\n typeHint,\n );\n // Tenant isolation (S5 #1398): records the tenant context a subscription was\n // registered in. Nullable so global/non-tenant subscriptions keep it NULL.\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n tenantIdColumnType(db, typeHint),\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_subs_tenant_id',\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n typeHint,\n );\n // Migrate the subscription natural key to (tenant_id, signal_type, subscriber)\n // so tenants can't clobber each other's subscriptions (S5 #1398).\n await migrateDispatchSubscriptionsIdentity(db, typeHint);\n}\n\n/** Compat-owned unique index over `_smrt_jobs(task_id)`. */\nconst JOBS_TASK_ID_COMPAT_INDEX = 'idx_smrt_jobs_task_id';\n\n/**\n * Reconcile the two owners of `_smrt_jobs(task_id)` uniqueness (issue #2376).\n *\n * `SmrtJob.taskId` is declared `unique: true`, so a table created from the\n * jobs manifest already carries a constraint-backed unique index. Legacy\n * databases got the column from `addColumnIfMissing()` instead, and\n * `ALTER TABLE ADD COLUMN ... UNIQUE` is rejected by SQLite and DuckDB — so\n * those databases need this compat index and only this compat index.\n *\n * Creating it unconditionally left every manifest-created table with two\n * unique indexes on one column. Create it only when nothing else already\n * enforces the uniqueness, and drop it when something does.\n *\n * Known limitation: this reconciliation is PostgreSQL/SQLite-only. DuckDB and\n * the JSON adapter give {@link listTableIndexes} no usable answer, so those\n * installs keep the redundant index — deliberately, because the alternative is\n * guessing and possibly leaving the upsert conflict target unenforced.\n */\nasync function reconcileJobsTaskIdUniqueness(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const indexes = await listTableIndexes(db, '_smrt_jobs', typeHint);\n\n if (indexes === null) {\n // Engine catalog not readable here — keep the historical behaviour rather\n // than risk leaving the upsert conflict target unenforced.\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n return;\n }\n\n // A partial unique index does not enforce uniqueness over the whole table,\n // so it can never stand in for the compat index.\n const otherUniqueOnTaskId = indexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n index.name !== JOBS_TASK_ID_COMPAT_INDEX &&\n index.columns.length === 1 &&\n index.columns[0] === 'task_id',\n );\n\n if (otherUniqueOnTaskId) {\n await dropIndexIfExists(db, JOBS_TASK_ID_COMPAT_INDEX, typeHint);\n return;\n }\n\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n}\n\nexport async function ensureJobsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_jobs', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'tenant_id',\n tenantIdColumnType(db, typeHint),\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_tenant_id',\n '_smrt_jobs',\n 'tenant_id',\n typeHint,\n );\n // MCP task support was added after `_smrt_jobs` had already shipped. Keep\n // the task projection in the job row so a task can never outlive (or lose\n // track of) the worker job that executes it. JSON is portable here because\n // SMRT's adapters serialize JSON fields to text where necessary.\n await addColumnIfMissing(db, '_smrt_jobs', 'task_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_owner_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_result', 'TEXT', typeHint);\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_requests',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_responses',\n 'TEXT',\n typeHint,\n );\n await reconcileJobsTaskIdUniqueness(db, typeHint);\n // Retention predicate of SmrtJobCollection.cleanup() (#2375): terminal jobs\n // aged out by `(status, completed_at)`. `_smrt_jobs` comes from a decorated\n // class rather than the hand-written system DDL, so this compatibility path\n // — which the jobs collection runs on every initialize — is where the index\n // reaches both existing and freshly migrated databases. `completed_at` is\n // guarded the same as every other column this pass touches: a table created\n // before the field existed (or a minimal test fixture) must not fail the\n // whole sweep with \"no such column\".\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'completed_at',\n 'TIMESTAMP',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_status_completed_at',\n '_smrt_jobs',\n 'status, completed_at',\n typeHint,\n );\n}\n\nexport async function ensureJobEventsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_job_events', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_job_events',\n 'tenant_id',\n tenantIdColumnType(db, typeHint),\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_tenant_id',\n '_smrt_job_events',\n 'tenant_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_job_id',\n '_smrt_job_events',\n 'job_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_type',\n '_smrt_job_events',\n 'type',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_created_at',\n '_smrt_job_events',\n 'created_at',\n typeHint,\n );\n}\n\n/**\n * Compatibility pass for the tables `db:migrate` creates from the jobs\n * manifest rather than `bootstrapSystemTables()` (issue #2376).\n *\n * These are dual-owned, and on a *fresh* install the manifest wins the race:\n * bootstrap runs first, finds no `_smrt_jobs`, returns early, stamps the\n * system schema version — and every later boot takes the version fast path, so\n * the columns and indexes this pass owns never appeared on that database at\n * all. Splitting the pass out lets the caller re-check it after the tables\n * show up, independently of the system schema version.\n *\n * @returns `settled: true` once every deferred table exists and has been\n * upgraded — the point at which a caller may stop re-checking. `false` means\n * the tables are not there yet, not that anything failed.\n */\nexport async function ensureDeferredSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<{ settled: boolean }> {\n // Sequential, never `Promise.all`: a caller can hand us one shared\n // single-statement connection (the OIDC provisioning coordinator does), and\n // two probes in flight at once overlap on it.\n const jobsExists = await tableExists(db, '_smrt_jobs', typeHint);\n const jobEventsExists = await tableExists(db, '_smrt_job_events', typeHint);\n\n if (jobsExists) {\n await ensureJobsSystemTableCompatibility(db, typeHint);\n }\n if (jobEventsExists) {\n await ensureJobEventsSystemTableCompatibility(db, typeHint);\n }\n\n return { settled: jobsExists && jobEventsExists };\n}\n\n/**\n * The compatibility pass that must precede replaying system DDL.\n *\n * Only the tables `bootstrapSystemTables()` itself creates belong here: an\n * older install can be missing a column that the DDL's `CREATE INDEX` clauses\n * reference, so those tables have to be reshaped first, inside the same\n * transaction as the replay.\n *\n * The deferred tables are deliberately NOT part of this pass. They are owned by\n * the jobs manifest, not by the system DDL, so nothing in the replay depends on\n * them — and reshaping a table the framework does not own must not be able to\n * abort the bootstrap transaction and roll back system-table creation with it\n * (issue #2376). {@link ensureDeferredSystemTableCompatibility} runs outside\n * the lock instead.\n */\nexport async function ensureBootstrapSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureDispatchSystemTableCompatibility(db, typeHint);\n await ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint);\n}\n\n/**\n * Run every compatibility pass — the bootstrap tables and the deferred,\n * manifest-created ones.\n *\n * This is the convenience entry point for callers that own the whole database\n * lifecycle in one step (test-database setup). Framework bootstrap deliberately\n * does not use it: see {@link ensureBootstrapSystemTableCompatibility}.\n */\nexport async function ensureLegacySystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n await ensureDeferredSystemTableCompatibility(db, typeHint);\n}\n"],"mappings":"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"}
|
package/dist/system/schema.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export declare const CREATE_SMRT_CHANGES_TABLE = "\nCREATE TABLE IF NOT EXISTS _
|
|
|
86
86
|
* (SQLite serializes writers outright), and keeping the portable DDL untouched
|
|
87
87
|
* keeps the embedded path byte-identical.
|
|
88
88
|
*/
|
|
89
|
-
export declare const CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_changes_pending (\n pending_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id
|
|
89
|
+
export declare const CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_changes_pending (\n pending_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id UUID,\n created_at TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_pending_table\n ON _smrt_changes_pending(table_name);\n";
|
|
90
90
|
/** Name of the PostgreSQL staging table for deferred change-feed appends. */
|
|
91
91
|
export declare const POSTGRES_CHANGE_FEED_PENDING_TABLE = "_smrt_changes_pending";
|
|
92
92
|
/** PostgreSQL materialization of the portable change-feed table DDL. */
|
|
@@ -110,11 +110,34 @@ export declare const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = "_smr
|
|
|
110
110
|
* replaced. Bump it whenever either helper's body changes in a way an existing
|
|
111
111
|
* database must pick up.
|
|
112
112
|
*/
|
|
113
|
-
export declare const POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:
|
|
113
|
+
export declare const POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:v5";
|
|
114
|
+
/**
|
|
115
|
+
* The previous body marker, still stamped into every v5 body (#3297).
|
|
116
|
+
*
|
|
117
|
+
* v5 only changes how a tenant id is coerced: it is converted to whatever type
|
|
118
|
+
* the live `tenant_id` column has (TEXT before `db:migrate-uuid`, native uuid
|
|
119
|
+
* after), so a v5 body is a drop-in for v4 callers. Older SMRT processes probe
|
|
120
|
+
* for the v4 marker; carrying it stops them from replacing a v5 body with a
|
|
121
|
+
* v4 one (which inserts text into a converted uuid column) during a
|
|
122
|
+
* mixed-version rollout.
|
|
123
|
+
*/
|
|
124
|
+
export declare const POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER = "smrt-change-feed-helpers:v4";
|
|
114
125
|
/** PostgreSQL helper that sequences staged change-feed appends (#2649). */
|
|
115
126
|
export declare const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = "_smrt_drain_changes";
|
|
116
|
-
/**
|
|
117
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Exact PostgreSQL identity of the drain helper (#2982): the batch limit plus
|
|
129
|
+
* the live set of sensitive tables, which the drain purges from staging and
|
|
130
|
+
* never sequences.
|
|
131
|
+
*/
|
|
132
|
+
export declare const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = "_smrt_drain_changes(integer,text[])";
|
|
133
|
+
/**
|
|
134
|
+
* Identity of the pre-#2982 single-argument drain, kept as a compatibility
|
|
135
|
+
* entry point so replicas of an older SMRT that still call
|
|
136
|
+
* `_smrt_drain_changes()` keep working during a mixed-version rollout. It
|
|
137
|
+
* delegates to the two-argument drain with no exclusions — exactly the old
|
|
138
|
+
* behavior — because it has no way to know the live sensitive set.
|
|
139
|
+
*/
|
|
140
|
+
export declare const POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY = "_smrt_drain_changes(integer)";
|
|
118
141
|
/** How many staged entries one drain call sequences. */
|
|
119
142
|
export declare const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;
|
|
120
143
|
/**
|
|
@@ -140,8 +163,21 @@ export declare const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;
|
|
|
140
163
|
* Failures are returned as data, like the append helper, so a drain problem
|
|
141
164
|
* never aborts the caller's transaction. A failed drain leaves the staged rows
|
|
142
165
|
* in place for the next attempt.
|
|
166
|
+
*
|
|
167
|
+
* Sensitive tables (#2982): `p_excluded_tables` is the caller's live
|
|
168
|
+
* sensitive set. Under the drain lock, staged rows for those tables are
|
|
169
|
+
* deleted before any sequence is allocated, and the sequencing batch skips
|
|
170
|
+
* them too, so an entry staged by an older replica (or left over from before
|
|
171
|
+
* an upgrade) never reaches `_smrt_changes`. Neither parameter has a default:
|
|
172
|
+
* a call with no arguments resolves only to the compatibility overload.
|
|
173
|
+
*/
|
|
174
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION: string;
|
|
175
|
+
/**
|
|
176
|
+
* Compatibility single-argument drain (#2982): see
|
|
177
|
+
* {@link POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}. Current SMRT
|
|
178
|
+
* never calls it.
|
|
143
179
|
*/
|
|
144
|
-
export declare const
|
|
180
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_drain_changes(\n p_limit INTEGER DEFAULT 1000\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain_compat$\n-- smrt-change-feed-helpers:v5\nDECLARE\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- Its own boundary, like the drain it delegates to: a resolution or\n -- privilege error on the inner call must come back as data, never abort an\n -- older replica's caller transaction (#2026).\n BEGIN\n RETURN QUERY\n SELECT * FROM _smrt_drain_changes(p_limit, ARRAY[]::TEXT[]);\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain_compat$;\n";
|
|
145
181
|
/**
|
|
146
182
|
* PostgreSQL-only change-feed append function.
|
|
147
183
|
*
|
|
@@ -152,14 +188,14 @@ export declare const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = "\nCREATE OR R
|
|
|
152
188
|
* semicolons, so callers must execute it whole rather than adding it to
|
|
153
189
|
* {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.
|
|
154
190
|
*/
|
|
155
|
-
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION
|
|
191
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION: string;
|
|
156
192
|
/**
|
|
157
193
|
* PostgreSQL batch append helper. It makes the staged/direct decision before
|
|
158
194
|
* its own writes, then appends every element through that one path. This is
|
|
159
195
|
* necessary because a direct first insert assigns the transaction id that
|
|
160
196
|
* later entries must not mistake for a caller-owned write transaction.
|
|
161
197
|
*/
|
|
162
|
-
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION
|
|
198
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION: string;
|
|
163
199
|
/**
|
|
164
200
|
* Serialize PostgreSQL helper replacement inside one server-side statement.
|
|
165
201
|
*
|
|
@@ -273,10 +309,36 @@ export declare const DEFERRED_COMPATIBILITY_TABLES: readonly string[];
|
|
|
273
309
|
* table is not reversible and a deployment may have repurposed the name.
|
|
274
310
|
*/
|
|
275
311
|
export declare const RETIRED_SYSTEM_TABLES: readonly string[];
|
|
312
|
+
/**
|
|
313
|
+
* System-table columns that hold a real tenant id (#3297): a tenant UUID from
|
|
314
|
+
* the tenant context, or NULL for global rows. On PostgreSQL they materialize
|
|
315
|
+
* as native `uuid`, like every application `tenant_id`; other engines keep
|
|
316
|
+
* TEXT, where `UUID` renders as TEXT anyway. Existing PostgreSQL TEXT columns
|
|
317
|
+
* convert through `smrt db:migrate-uuid` in a maintenance window.
|
|
318
|
+
*
|
|
319
|
+
* Deliberately NOT here (they stay TEXT):
|
|
320
|
+
* - text identity encodings that are not tenant ids: `_smrt_contexts.owner_id`
|
|
321
|
+
* (`__collection__` / `__collection__:<tenantId>` scope keys, object/agent
|
|
322
|
+
* row ids), `_smrt_run_once_claims.actor` (an arbitration namespace),
|
|
323
|
+
* `_smrt_dispatch.processed_by` (a subscriber name) and
|
|
324
|
+
* `_smrt_schema_migrations.applied_by` (a hostname);
|
|
325
|
+
* - `_smrt_run_once_claims.tenant_id`: a NOT NULL claim scope folded into the
|
|
326
|
+
* claim key; a caller without a tenant must pass a scope string, so it is a
|
|
327
|
+
* scope key, not a tenant FK;
|
|
328
|
+
* - `_smrt_ai_usage.tenant_id`: model usage writes tenant UUIDs, but the
|
|
329
|
+
* published external-agent usage API accepts any non-empty text tenant id,
|
|
330
|
+
* so converting it is a contract change tracked separately.
|
|
331
|
+
*/
|
|
332
|
+
export declare const SYSTEM_TABLE_TENANT_UUID_COLUMNS: ReadonlyArray<{
|
|
333
|
+
table: string;
|
|
334
|
+
column: string;
|
|
335
|
+
}>;
|
|
276
336
|
/**
|
|
277
337
|
* Materialize the portable system-table schema for a target engine.
|
|
278
338
|
*
|
|
279
|
-
* PostgreSQL must use TIMESTAMPTZ because every system Date is an instant
|
|
339
|
+
* PostgreSQL must use TIMESTAMPTZ because every system Date is an instant,
|
|
340
|
+
* and native UUID for the tenant columns in
|
|
341
|
+
* {@link SYSTEM_TABLE_TENANT_UUID_COLUMNS} (#3297).
|
|
280
342
|
* PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching
|
|
281
343
|
* application-table DDL; SQLite keeps its variable-width INTEGER.
|
|
282
344
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,0CAA0C,uWAYtD,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,0BAA0B,CAAC;AAE1E,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"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"}
|