@happyvertical/smrt-core 0.40.14 → 0.40.16

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.
Files changed (92) hide show
  1. package/README.md +85 -11
  2. package/dist/browser.js +2 -2
  3. package/dist/change-feed.d.ts.map +1 -1
  4. package/dist/change-feed.js +21 -4
  5. package/dist/change-feed.js.map +1 -1
  6. package/dist/class.d.ts.map +1 -1
  7. package/dist/class.js +5 -3
  8. package/dist/class.js.map +1 -1
  9. package/dist/dispatch/bus.d.ts.map +1 -1
  10. package/dist/dispatch/bus.js +11 -6
  11. package/dist/dispatch/bus.js.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/manifest/static-manifest.js +2 -2
  14. package/dist/manifest/static-manifest.js.map +1 -1
  15. package/dist/manifest/store.js +1 -1
  16. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  17. package/dist/manifest/test-manifest-stub.js +132 -2
  18. package/dist/manifest/test-manifest-stub.js.map +1 -1
  19. package/dist/manifest.json +2 -2
  20. package/dist/migrations/backfill-tracker.d.ts +1 -0
  21. package/dist/migrations/backfill-tracker.d.ts.map +1 -1
  22. package/dist/migrations/backfill-tracker.js +9 -3
  23. package/dist/migrations/backfill-tracker.js.map +1 -1
  24. package/dist/migrations/differ.d.ts +18 -0
  25. package/dist/migrations/differ.d.ts.map +1 -1
  26. package/dist/migrations/differ.js +28 -5
  27. package/dist/migrations/differ.js.map +1 -1
  28. package/dist/migrations/generator.d.ts +8 -0
  29. package/dist/migrations/generator.d.ts.map +1 -1
  30. package/dist/migrations/generator.js +5 -1
  31. package/dist/migrations/generator.js.map +1 -1
  32. package/dist/migrations/orchestrate.d.ts +10 -0
  33. package/dist/migrations/orchestrate.d.ts.map +1 -1
  34. package/dist/migrations/orchestrate.js +8 -2
  35. package/dist/migrations/orchestrate.js.map +1 -1
  36. package/dist/migrations/tracker.d.ts.map +1 -1
  37. package/dist/migrations/tracker.js +4 -2
  38. package/dist/migrations/tracker.js.map +1 -1
  39. package/dist/registry/schema-builder.d.ts +13 -6
  40. package/dist/registry/schema-builder.d.ts.map +1 -1
  41. package/dist/registry/schema-builder.js +54 -15
  42. package/dist/registry/schema-builder.js.map +1 -1
  43. package/dist/registry.d.ts +3 -2
  44. package/dist/registry.d.ts.map +1 -1
  45. package/dist/registry.js +4 -4
  46. package/dist/registry.js.map +1 -1
  47. package/dist/schema/code-generator.js +123 -0
  48. package/dist/schema/code-generator.js.map +1 -0
  49. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  50. package/dist/schema/ddl/base-strategy.js +2 -2
  51. package/dist/schema/ddl/base-strategy.js.map +1 -1
  52. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  53. package/dist/schema/ddl/duckdb-strategy.js +2 -3
  54. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  55. package/dist/schema/ddl/index.js +26 -2
  56. package/dist/schema/ddl/index.js.map +1 -1
  57. package/dist/schema/ddl/materialize-manifest.d.ts +36 -0
  58. package/dist/schema/ddl/materialize-manifest.d.ts.map +1 -0
  59. package/dist/schema/ddl/materialize-manifest.js +361 -0
  60. package/dist/schema/ddl/materialize-manifest.js.map +1 -0
  61. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  62. package/dist/schema/ddl/postgres-strategy.js +1 -1
  63. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  64. package/dist/schema/index.js +12 -0
  65. package/dist/schema/override-system.js +205 -0
  66. package/dist/schema/override-system.js.map +1 -0
  67. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  68. package/dist/schema/schema-aggregator.js +261 -0
  69. package/dist/schema/schema-aggregator.js.map +1 -0
  70. package/dist/schema/schema-manager.js +11 -1
  71. package/dist/schema/schema-manager.js.map +1 -1
  72. package/dist/schema/utils.d.ts +1 -0
  73. package/dist/schema/utils.d.ts.map +1 -1
  74. package/dist/schema/utils.js +2 -1
  75. package/dist/schema/utils.js.map +1 -1
  76. package/dist/schema.d.ts +2 -0
  77. package/dist/schema.d.ts.map +1 -0
  78. package/dist/schema.js +13 -0
  79. package/dist/smrt-knowledge.json +5 -4
  80. package/dist/system/compatibility.d.ts +25 -0
  81. package/dist/system/compatibility.d.ts.map +1 -1
  82. package/dist/system/compatibility.js +61 -1
  83. package/dist/system/compatibility.js.map +1 -1
  84. package/dist/system/index.js +2 -2
  85. package/dist/system/schema.d.ts +17 -9
  86. package/dist/system/schema.d.ts.map +1 -1
  87. package/dist/system/schema.js +25 -11
  88. package/dist/system/schema.js.map +1 -1
  89. package/dist/testing/database.d.ts.map +1 -1
  90. package/dist/testing/database.js +5 -2
  91. package/dist/testing/database.js.map +1 -1
  92. package/package.json +10 -5
@@ -1,5 +1,30 @@
1
1
  import { DatabaseInterface } from '@happyvertical/sql';
2
+ import { detectEngine } from '../schema/ddl/index.js';
2
3
  export declare function tableExists(db: DatabaseInterface, tableName: string, typeHint?: string): Promise<boolean>;
4
+ export declare function getDatabaseEngine(db: DatabaseInterface, typeHint?: string): ReturnType<typeof detectEngine>;
5
+ /**
6
+ * Upgrade framework-owned PostgreSQL timestamp columns to instant-safe storage.
7
+ *
8
+ * Legacy SMRT and system writers serialized instants as UTC wall times. The
9
+ * conversion is intentionally fail-closed unless the migration session is UTC:
10
+ * operators must first confirm that historical database-default/raw writers
11
+ * also used UTC. A deployment with non-UTC legacy writers needs an explicit,
12
+ * provenance-aware data migration rather than a guessed offset.
13
+ */
14
+ export interface PostgresSystemTimestampMigrationConfirmation {
15
+ /**
16
+ * Explicitly confirmed timezone used by every historical writer of legacy
17
+ * timezone-naive system timestamps. Only UTC is supported by this helper.
18
+ */
19
+ legacyTimezone: 'UTC';
20
+ }
21
+ /**
22
+ * Fail closed when framework-owned PostgreSQL tables still contain legacy
23
+ * timezone-naive timestamps. Call this before recording the current system
24
+ * schema version so a partial installation cannot be stamped as upgraded.
25
+ */
26
+ export declare function assertPostgresSystemTimestampsCurrent(db: DatabaseInterface, typeHint?: string): Promise<void>;
27
+ export declare function migratePostgresSystemTimestamps(db: DatabaseInterface, confirmation: PostgresSystemTimestampMigrationConfirmation, typeHint?: string): Promise<void>;
3
28
  export declare function ensureDispatchSystemTableCompatibility(db: DatabaseInterface, typeHint?: string): Promise<void>;
4
29
  export declare function ensureDispatchSubscriptionsSystemTableCompatibility(db: DatabaseInterface, typeHint?: string): Promise<void>;
5
30
  export declare function ensureJobsSystemTableCompatibility(db: DatabaseInterface, typeHint?: string): Promise<void>;
@@ -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;AA0B5D,wBAAsB,WAAW,CAC/B,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAiXD,wBAAsB,sCAAsC,CAC1D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA4Cf;AAED,wBAAsB,mDAAmD,CACvE,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,uCAAuC,CAC3D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAwCf;AAED,wBAAsB,oCAAoC,CACxD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf"}
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;;;;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;AAyUD,wBAAsB,sCAAsC,CAC1D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA4Cf;AAED,wBAAsB,mDAAmD,CACvE,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAsB,uCAAuC,CAC3D,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAwCf;AAED,wBAAsB,oCAAoC,CACxD,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -1,4 +1,5 @@
1
1
  import { detectEngine } from "../schema/ddl/index.js";
2
+ import { CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY } from "./schema.js";
2
3
  //#region src/system/compatibility.ts
3
4
  function getQueryRows(result) {
4
5
  if (Array.isArray(result)) return result;
@@ -33,6 +34,65 @@ function getDatabaseEngine(db, typeHint) {
33
34
  const dbWithConfig = db;
34
35
  return detectEngine(getDatabaseUrl(db), typeHint || dbWithConfig.type || dbWithConfig.config?.type);
35
36
  }
37
+ /**
38
+ * Fail closed when framework-owned PostgreSQL tables still contain legacy
39
+ * timezone-naive timestamps. Call this before recording the current system
40
+ * schema version so a partial installation cannot be stamped as upgraded.
41
+ */
42
+ async function assertPostgresSystemTimestampsCurrent(db, typeHint) {
43
+ if (getDatabaseEngine(db, typeHint) !== "postgres") return;
44
+ const rows = getQueryRows(await db.query(`
45
+ SELECT table_name, column_name
46
+ FROM information_schema.columns
47
+ WHERE table_schema = current_schema()
48
+ AND table_name LIKE '\\_smrt\\_%' ESCAPE '\\'
49
+ AND data_type = 'timestamp without time zone'
50
+ ORDER BY table_name, ordinal_position
51
+ `));
52
+ if (rows.length === 0) return;
53
+ const columns = rows.map((row) => `${String(row.table_name)}.${String(row.column_name)}`).join(", ");
54
+ throw new Error(`Legacy SMRT system timestamps remain (${columns}); run the explicit audited migratePostgresSystemTimestamps() migration before bootstrap`);
55
+ }
56
+ async function migratePostgresSystemTimestamps(db, confirmation, typeHint) {
57
+ if (getDatabaseEngine(db, typeHint) !== "postgres") return;
58
+ if (confirmation.legacyTimezone !== "UTC") throw new Error("Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation");
59
+ await db.query(`
60
+ DO $smrt_migrate_system_timestamps$
61
+ DECLARE
62
+ target RECORD;
63
+ BEGIN
64
+ PERFORM pg_advisory_xact_lock(
65
+ hashtext('smrt'),
66
+ hashtext('system-tables')
67
+ );
68
+ IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN
69
+ RAISE EXCEPTION 'Refusing to reinterpret legacy SMRT timestamps outside a UTC PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';
70
+ END IF;
71
+ FOR target IN
72
+ SELECT table_name, column_name
73
+ FROM information_schema.columns
74
+ WHERE table_schema = current_schema()
75
+ AND table_name LIKE '\\_smrt\\_%' ESCAPE '\\'
76
+ AND data_type = 'timestamp without time zone'
77
+ ORDER BY table_name, ordinal_position
78
+ LOOP
79
+ EXECUTE format(
80
+ 'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE ''UTC''',
81
+ target.table_name,
82
+ target.column_name,
83
+ target.column_name
84
+ );
85
+ END LOOP;
86
+ IF to_regclass('_smrt_changes') IS NOT NULL THEN
87
+ DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};
88
+ EXECUTE $smrt_change_feed_ddl$
89
+ ${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
90
+ $smrt_change_feed_ddl$;
91
+ END IF;
92
+ END;
93
+ $smrt_migrate_system_timestamps$;
94
+ `);
95
+ }
36
96
  function isDuplicateColumnError(error) {
37
97
  const message = error && typeof error === "object" ? String(error.message || error) : String(error);
38
98
  return /column .*already exists/i.test(message) || /duplicate column name/i.test(message);
@@ -231,6 +291,6 @@ async function ensureLegacySystemTableCompatibility(db, typeHint) {
231
291
  await ensureJobEventsSystemTableCompatibility(db, typeHint);
232
292
  }
233
293
  //#endregion
234
- export { ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, tableExists };
294
+ export { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, tableExists };
235
295
 
236
296
  //# sourceMappingURL=compatibility.js.map
@@ -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';\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\nfunction 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\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\nasync function addIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columnName: 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}(${columnName})`,\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\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 // 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 (id, signal_type, subscriber, handler, delivery, enabled, tenant_id, created_at, updated_at)\n SELECT id, signal_type, subscriber, handler, delivery, enabled, tenant_id, created_at, updated_at\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\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}\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\nexport async function ensureLegacySystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureDispatchSystemTableCompatibility(db, typeHint);\n await ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint);\n await ensureJobsSystemTableCompatibility(db, typeHint);\n await ensureJobEventsSystemTableCompatibility(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\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_regclass('_smrt_changes') 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\nasync function addIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columnName: 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}(${columnName})`,\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\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 // 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 (id, signal_type, subscriber, handler, delivery, enabled, tenant_id, created_at, updated_at)\n SELECT id, signal_type, subscriber, handler, delivery, enabled, tenant_id, created_at, updated_at\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\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}\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\nexport async function ensureLegacySystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureDispatchSystemTableCompatibility(db, typeHint);\n await ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint);\n await ensureJobsSystemTableCompatibility(db, typeHint);\n await ensureJobEventsSystemTableCompatibility(db, typeHint);\n}\n"],"mappings":";;;AAeA,SAAS,aAAa,QAA4C;CAChE,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO;CAGT,IAAI,UAAU,OAAO,WAAW,YAAY,UAAU,QAAQ;EAC5D,MAAM,OAAQ,OAA8B;EAC5C,IAAI,MAAM,QAAQ,IAAI,GACpB,OAAO;CAEX;CAEA,OAAO,CAAC;AACV;AAEA,eAAsB,YACpB,IACA,WACA,UACkB;CAClB,IAAI;EAEF,IADe,kBAAkB,IAAI,QACjC,MAAW,YAKb,OAAO,aAAa,MAJC,GAAG,MACtB,6GACA,SACF,CAC0B,CAAC,CAAC,SAAS;EAGvC,MAAM,GAAG,MAAM,iBAAiB,UAAU,SAAS;EACnD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,aACb,IACA,WACA,YACA,UACkB;CAClB,IAAI;EAEF,IADe,kBAAkB,IAAI,QACjC,MAAW,YAMb,OAAO,aAAa,MALC,GAAG,MACtB,mIACA,WACA,UACF,CAC0B,CAAC,CAAC,SAAS;EAIvC,OAAO,aAAa,MADC,GAAG,MAAM,qBAAqB,UAAU,EAAE,CACrC,CAAC,CAAC,MAAM,QAAQ,IAAI,SAAS,UAAU;CACnE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,IAA+B;CACrD,MAAM,eAAe;CACrB,OAAO,GAAG,OAAO,aAAa,QAAQ,OAAO;AAC/C;AAEA,SAAgB,kBACd,IACA,UACiC;CACjC,MAAM,eAAe;CACrB,OAAO,aACL,eAAe,EAAE,GACjB,YAAY,aAAa,QAAQ,aAAa,QAAQ,IACxD;AACF;;;;;;AAwBA,eAAsB,sCACpB,IACA,UACe;CACf,IAAI,kBAAkB,IAAI,QAAQ,MAAM,YAAY;CAEpD,MAAM,OAAO,aACX,MAAM,GAAG,MAAM;;;;;;;KAOd,CACH;CACA,IAAI,KAAK,WAAW,GAAG;CAEvB,MAAM,UAAU,KACb,KAAK,QAAQ,GAAG,OAAO,IAAI,UAAU,EAAE,GAAG,OAAO,IAAI,WAAW,GAAG,CAAC,CACpE,KAAK,IAAI;CACZ,MAAM,IAAI,MACR,yCAAyC,QAAQ,yFACnD;AACF;AAEA,eAAsB,gCACpB,IACA,cACA,UACe;CACf,IAAI,kBAAkB,IAAI,QAAQ,MAAM,YAAY;CACpD,IAAI,aAAa,mBAAmB,OAClC,MAAM,IAAI,MACR,yFACF;CAMF,MAAM,GAAG,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCA4BiB,qDAAqD;;EAErF,4CAA4C,KAAK,EAAE;;;;;GAKlD;AACH;AAEA,SAAS,uBAAuB,OAAyB;CACvD,MAAM,UACJ,SAAS,OAAO,UAAU,WACtB,OAAQ,MAAgC,WAAW,KAAK,IACxD,OAAO,KAAK;CAElB,OACE,2BAA2B,KAAK,OAAO,KACvC,yBAAyB,KAAK,OAAO;AAEzC;AAEA,SAAS,oBACP,OACA,uBAAO,IAAI,IAAa,GAIxB;CACA,MAAM,wBAAQ,IAAI,IAAY;CAC9B,MAAM,WAAqB,CAAC;CAE5B,IAAI,CAAC,SAAS,KAAK,IAAI,KAAK,GAC1B,OAAO;EAAE;EAAO;CAAS;CAG3B,KAAK,IAAI,KAAK;CAEd,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,cAAc;EAEpB,IAAI,OAAO,YAAY,SAAS,UAC9B,MAAM,IAAI,YAAY,IAAI;EAG5B,KAAK,MAAM,cAAc,CAAC,WAAW,QAAQ,GAC3C,IAAI,OAAO,YAAY,gBAAgB,UACrC,SAAS,KAAK,YAAY,WAAW;EAIzC,KAAK,MAAM,aAAa;GAAC;GAAS;GAAW;EAAe,GAAG;GAC7D,MAAM,SAAS,oBAAoB,YAAY,YAAY,IAAI;GAC/D,KAAK,MAAM,QAAQ,OAAO,OACxB,MAAM,IAAI,IAAI;GAEhB,SAAS,KAAK,GAAG,OAAO,QAAQ;EAClC;CACF,OACE,SAAS,KAAK,OAAO,KAAK,CAAC;CAG7B,OAAO;EAAE;EAAO;CAAS;AAC3B;AAEA,SAAS,0BAA0B,OAAgB,WAA4B;CAC7E,MAAM,EAAE,OAAO,aAAa,oBAAoB,KAAK;CACrD,MAAM,UAAU,SAAS,KAAK,IAAI,CAAC,CAAC,YAAY;CAChD,MAAM,sBAAsB,UAAU,YAAY;CAClD,MAAM,mBACJ,MAAM,IAAI,OAAO,KAAK,uBAAuB,KAAK,OAAO;CAC3D,MAAM,wBACJ,MAAM,IAAI,OAAO,KAAK,wBAAwB,KAAK,OAAO;CAE5D,IAAI,CAAC,QAAQ,SAAS,mBAAmB,GACvC,OAAO;CAGT,IAAI,oBAAoB,QAAQ,SAAS,4BAA4B,GACnE,OAAO;CAGT,IAAI,yBAAyB,6BAA6B,KAAK,OAAO,GACpE,OAAO;CAGT,OAAO;AACT;AAEA,eAAe,mBACb,IACA,WACA,YACA,YACA,UACe;CAGf,IAFe,kBAAkB,IAAI,QAEjC,MAAW,YAAY;EACzB,IAAI,MAAM,aAAa,IAAI,WAAW,YAAY,QAAQ,GACxD;EAGF,MAAM,GAAG,MACP,eAAe,UAAU,4BAA4B,WAAW,GAAG,YACrE;EACA;CACF;CAEA,IAAI,MAAM,aAAa,IAAI,WAAW,YAAY,QAAQ,GACxD;CAGF,IAAI;EACF,MAAM,GAAG,MACP,eAAe,UAAU,cAAc,WAAW,GAAG,YACvD;CACF,SAAS,OAAO;EACd,IAAI,uBAAuB,KAAK,GAC9B;EAGF,MAAM;CACR;AACF;AAEA,eAAe,YACb,IACA,WACA,UACkB;CAClB,IAAI;EAEF,IADe,kBAAkB,IAAI,QACjC,MAAW,YAKb,OAAO,aAAa,MAJC,GAAG,MACtB,2FACA,SACF,CAC0B,CAAC,CAAC,SAAS;EAOvC,OAAO,aAAa,MAJC,GAAG,MACtB,yEACA,SACF,CAC0B,CAAC,CAAC,SAAS;CACvC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,kBACb,IACA,WACA,WACA,YACA,UACe;CACf,IAAI,MAAM,YAAY,IAAI,WAAW,QAAQ,GAC3C;CAGF,IAAI;EACF,MAAM,GAAG,MACP,8BAA8B,UAAU,MAAM,UAAU,GAAG,WAAW,EACxE;CACF,SAAS,OAAO;EACd,IAAI,0BAA0B,OAAO,SAAS;OACxC,MAAM,YAAY,IAAI,WAAW,QAAQ,GAC3C;EAAA;EAIJ,MAAM;CACR;AACF;AAEA,eAAe,wBACb,IACA,WACA,WACA,SACA,UACe;CACf,IAAI,MAAM,YAAY,IAAI,WAAW,QAAQ,GAC3C;CAGF,IAAI;EACF,MAAM,GAAG,MACP,qCAAqC,UAAU,MAAM,UAAU,GAAG,QAAQ,EAC5E;CACF,SAAS,OAAO;EACd,IAAI,0BAA0B,OAAO,SAAS;OACxC,MAAM,YAAY,IAAI,WAAW,QAAQ,GAC3C;EAAA;EAIJ,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;AAkBA,eAAe,qCACb,IACA,UACe;CACf,MAAM,SAAS,kBAAkB,IAAI,QAAQ;CAG7C,MAAM,wBACJ,IACA,kDACA,gCACA,sCACA,QACF;CAGA,IAAI,WAAW,YAAY;EAGzB,IAAI;GACF,MAAM,GAAG,MACP,4HACF;EACF,QAAQ,CAGR;EACA;CACF;CAIA,IAAI,WAAW,UACb;CAGF,IAAI,kBAAiC;CACrC,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,MACtB;;;8CAIF;EACA,KAAK,MAAM,OAAO,aAAa,MAAM,GAAG;GACtC,MAAM,YAAY,OAAO,IAAI,IAAI;GAEjC,MAAM,OAAO,aAAa,MADP,GAAG,MAAM,qBAAqB,UAAU,EAAE,CAC/B,CAAC,CAC5B,KAAK,MAAM,OAAO,EAAE,IAAI,CAAC,CAAC,CAC1B,KAAK;GACR,IACE,KAAK,WAAW,KAChB,KAAK,OAAO,iBACZ,KAAK,OAAO,cACZ;IACA,kBAAkB;IAClB;GACF;EACF;CACF,QAAQ;EAGN,kBAAkB;CACpB;CAEA,IAAI,CAAC,iBACH;CAKF,MAAM,GAAG,MAAM,yBAAyB;CACxC,IAAI;EACF,MAAM,GAAG,MAAM;;;;;;;;;;;;KAYd;EACD,MAAM,GAAG,MAAM;;;;;KAKd;EACD,MAAM,GAAG,MAAM,yCAAyC;EACxD,MAAM,GAAG,MACP,qFACF;EAGA,MAAM,GAAG,MACP,sJACF;EACA,MAAM,GAAG,MACP,0GACF;EACA,MAAM,GAAG,MACP,wGACF;EACA,MAAM,GAAG,MACP,4GACF;EACA,MAAM,GAAG,MACP,oGACF;CACF,UAAU;EACR,MAAM,GAAG,MAAM,wBAAwB;CACzC;AACF;AAEA,eAAsB,uCACpB,IACA,UACe;CACf,IAAI,CAAE,MAAM,YAAY,IAAI,kBAAkB,QAAQ,GACpD;CAGF,MAAM,mBACJ,IACA,kBACA,qBACA,QACA,QACF;CACA,MAAM,mBACJ,IACA,kBACA,kBACA,QACA,QACF;CAIA,MAAM,mBAAmB,IAAI,kBAAkB,aAAa,QAAQ,QAAQ;CAC5E,MAAM,kBACJ,IACA,4BACA,kBACA,qBACA,QACF;CACA,MAAM,kBACJ,IACA,iCACA,kBACA,kBACA,QACF;CACA,MAAM,kBACJ,IACA,+BACA,kBACA,aACA,QACF;AACF;AAEA,eAAsB,oDACpB,IACA,UACe;CACf,IAAI,CAAE,MAAM,YAAY,IAAI,gCAAgC,QAAQ,GAClE;CAGF,MAAM,mBACJ,IACA,gCACA,YACA,mCACA,QACF;CAGA,MAAM,mBACJ,IACA,gCACA,aACA,QACA,QACF;CACA,MAAM,kBACJ,IACA,oCACA,gCACA,aACA,QACF;CAGA,MAAM,qCAAqC,IAAI,QAAQ;AACzD;AAEA,eAAsB,mCACpB,IACA,UACe;CACf,IAAI,CAAE,MAAM,YAAY,IAAI,cAAc,QAAQ,GAChD;CAGF,MAAM,mBAAmB,IAAI,cAAc,aAAa,QAAQ,QAAQ;CACxE,MAAM,kBACJ,IACA,2BACA,cACA,aACA,QACF;AACF;AAEA,eAAsB,wCACpB,IACA,UACe;CACf,IAAI,CAAE,MAAM,YAAY,IAAI,oBAAoB,QAAQ,GACtD;CAGF,MAAM,mBACJ,IACA,oBACA,aACA,QACA,QACF;CACA,MAAM,kBACJ,IACA,iCACA,oBACA,aACA,QACF;CACA,MAAM,kBACJ,IACA,8BACA,oBACA,UACA,QACF;CACA,MAAM,kBACJ,IACA,4BACA,oBACA,QACA,QACF;CACA,MAAM,kBACJ,IACA,kCACA,oBACA,cACA,QACF;AACF;AAEA,eAAsB,qCACpB,IACA,UACe;CACf,MAAM,uCAAuC,IAAI,QAAQ;CACzD,MAAM,oDAAoD,IAAI,QAAQ;CACtE,MAAM,mCAAmC,IAAI,QAAQ;CACrD,MAAM,wCAAwC,IAAI,QAAQ;AAC5D"}
@@ -1,2 +1,2 @@
1
- import { ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, tableExists } from "./compatibility.js";
2
- export { ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, tableExists };
1
+ import { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, tableExists } from "./compatibility.js";
2
+ export { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, tableExists };
@@ -1,9 +1,4 @@
1
- /**
2
- * SMRT System Tables Schema
3
- *
4
- * System tables use _smrt_ prefix to avoid conflicts with user tables.
5
- * All system tables are created in the same database as user data.
6
- */
1
+ import { DatabaseEngine } from '../schema/ddl/types.js';
7
2
  /**
8
3
  * Context memory storage
9
4
  * Stores remembered context (learned strategies, patterns, selectors) for reuse
@@ -72,10 +67,14 @@ export declare const CREATE_SMRT_AI_USAGE_TABLE = "\nCREATE TABLE IF NOT EXISTS
72
67
  * table-level change without a specific row.
73
68
  */
74
69
  export declare const CREATE_SMRT_CHANGES_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n";
70
+ /** PostgreSQL materialization of the portable change-feed table DDL. */
71
+ export declare const CREATE_POSTGRES_SMRT_CHANGES_TABLE: string;
75
72
  /** PostgreSQL helper used to isolate best-effort feed appends (#2026). */
76
73
  export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = "_smrt_append_change";
74
+ /** Legacy helper identity used before PostgreSQL Date values became instants. */
75
+ export declare const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp without time zone)";
77
76
  /** Exact PostgreSQL identity used for catalog lookup of the append helper. */
78
- export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp without time zone)";
77
+ export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp with time zone)";
79
78
  /**
80
79
  * PostgreSQL-only change-feed append function.
81
80
  *
@@ -86,7 +85,7 @@ export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_appe
86
85
  * semicolons, so callers must execute it whole rather than adding it to
87
86
  * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.
88
87
  */
89
- export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_append_change(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMP\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::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 NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n";
88
+ export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_append_change(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::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 NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n";
90
89
  /**
91
90
  * Serialize PostgreSQL helper replacement inside one server-side statement.
92
91
  *
@@ -124,8 +123,17 @@ export declare const CREATE_SMRT_BACKFILLS_TABLE = "\nCREATE TABLE IF NOT EXISTS
124
123
  * All system table creation statements
125
124
  */
126
125
  export declare const ALL_SYSTEM_TABLES: string[];
126
+ /**
127
+ * Materialize the portable system-table schema for a target engine.
128
+ *
129
+ * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.
130
+ * Other engines keep the established portable TIMESTAMP declarations.
131
+ */
132
+ export declare function getSystemTableDDL(engine: DatabaseEngine): string[];
133
+ /** Materialize one portable system-table statement for a target engine. */
134
+ export declare function getSystemTableDDLForEngine(ddl: string, engine: DatabaseEngine): string;
127
135
  /**
128
136
  * Current SMRT system schema version
129
137
  */
130
- export declare const SMRT_SCHEMA_VERSION = "1.8.0";
138
+ export declare const SMRT_SCHEMA_VERSION = "1.9.0";
131
139
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B,+5BA+BtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,6OAUtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,sgBAoBrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,kmCAwCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,swBA4BtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,yEAAkG,CAAC;AAE7J;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,ulCAiDvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAYxD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2CAA2C,QAcvD,CAAC;AAYF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QAe9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAY7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAAU,CAAC"}
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,+5BA+BtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,6OAUtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,sgBAoBrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,kmCAwCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,swBA4BtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,ylCAiDvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAaxD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2CAA2C,QAevD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QAgB9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAY7B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAIlE;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GACrB,MAAM,CAIR;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,UAAU,CAAC"}
@@ -1,11 +1,5 @@
1
1
  //#region src/system/schema.ts
2
2
  /**
3
- * SMRT System Tables Schema
4
- *
5
- * System tables use _smrt_ prefix to avoid conflicts with user tables.
6
- * All system tables are created in the same database as user data.
7
- */
8
- /**
9
3
  * Context memory storage
10
4
  * Stores remembered context (learned strategies, patterns, selectors) for reuse
11
5
  */
@@ -317,10 +311,14 @@ CREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq
317
311
  CREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at
318
312
  ON _smrt_changes(created_at);
319
313
  `;
314
+ /** PostgreSQL materialization of the portable change-feed table DDL. */
315
+ var CREATE_POSTGRES_SMRT_CHANGES_TABLE = CREATE_SMRT_CHANGES_TABLE.replace(/\bTIMESTAMP\b/g, "TIMESTAMPTZ");
320
316
  /** PostgreSQL helper used to isolate best-effort feed appends (#2026). */
321
317
  var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = "_smrt_append_change";
318
+ /** Legacy helper identity used before PostgreSQL Date values became instants. */
319
+ var LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;
322
320
  /** Exact PostgreSQL identity used for catalog lookup of the append helper. */
323
- var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;
321
+ var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;
324
322
  /**
325
323
  * PostgreSQL-only change-feed append function.
326
324
  *
@@ -337,7 +335,7 @@ CREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(
337
335
  p_row_id TEXT,
338
336
  p_operation TEXT,
339
337
  p_tenant_id TEXT,
340
- p_created_at TIMESTAMP
338
+ p_created_at TIMESTAMPTZ
341
339
  )
342
340
  RETURNS TABLE(
343
341
  allocated_seq BIGINT,
@@ -395,6 +393,7 @@ BEGIN
395
393
  hashtext('smrt'),
396
394
  hashtext('system-tables')
397
395
  );
396
+ DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};
398
397
  EXECUTE $smrt_change_feed_ddl$
399
398
  ${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
400
399
  $smrt_change_feed_ddl$;
@@ -415,6 +414,7 @@ BEGIN
415
414
  hashtext('smrt'),
416
415
  hashtext('system-tables')
417
416
  );
417
+ DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};
418
418
  IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN
419
419
  EXECUTE $smrt_change_feed_ddl$
420
420
  ${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
@@ -437,9 +437,10 @@ BEGIN
437
437
  hashtext('smrt'),
438
438
  hashtext('system-tables')
439
439
  );
440
- ${CREATE_SMRT_CHANGES_TABLE.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0).map((statement) => `EXECUTE $smrt_change_feed_schema_ddl$
440
+ ${CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0).map((statement) => `EXECUTE $smrt_change_feed_schema_ddl$
441
441
  ${statement}
442
442
  $smrt_change_feed_schema_ddl$;`).join("\n")}
443
+ DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};
443
444
  IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN
444
445
  EXECUTE $smrt_change_feed_ddl$
445
446
  ${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
@@ -481,10 +482,23 @@ var ALL_SYSTEM_TABLES = [
481
482
  CREATE_SMRT_CHANGES_TABLE
482
483
  ];
483
484
  /**
485
+ * Materialize the portable system-table schema for a target engine.
486
+ *
487
+ * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.
488
+ * Other engines keep the established portable TIMESTAMP declarations.
489
+ */
490
+ function getSystemTableDDL(engine) {
491
+ return ALL_SYSTEM_TABLES.map((ddl) => getSystemTableDDLForEngine(ddl, engine));
492
+ }
493
+ /** Materialize one portable system-table statement for a target engine. */
494
+ function getSystemTableDDLForEngine(ddl, engine) {
495
+ return engine === "postgres" ? ddl.replace(/\bTIMESTAMP\b/g, "TIMESTAMPTZ") : ddl;
496
+ }
497
+ /**
484
498
  * Current SMRT system schema version
485
499
  */
486
- var SMRT_SCHEMA_VERSION = "1.8.0";
500
+ var SMRT_SCHEMA_VERSION = "1.9.0";
487
501
  //#endregion
488
- export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_REGISTRY_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, CREATE_SMRT_SIGNALS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, SMRT_SCHEMA_VERSION };
502
+ export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_SMRT_CHANGES_TABLE, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_REGISTRY_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, CREATE_SMRT_SIGNALS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, SMRT_SCHEMA_VERSION, getSystemTableDDL, getSystemTableDDLForEngine };
489
503
 
490
504
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Runtime object registry persistence\n * Stores metadata about registered SMRT objects for introspection\n */\nexport const CREATE_SMRT_REGISTRY_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_registry (\n class_name TEXT PRIMARY KEY,\n schema_version TEXT,\n fields TEXT,\n relationships TEXT,\n config TEXT,\n manifest TEXT,\n last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n`;\n\n/**\n * Signal history/audit log\n * Optional persistence of signals for debugging and auditing\n */\nexport const CREATE_SMRT_SIGNALS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_signals (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source_class TEXT,\n source_id TEXT,\n target_class TEXT,\n target_id TEXT,\n payload TEXT,\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_source\n ON _smrt_signals(source_class, source_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_type\n ON _smrt_signals(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_timestamp\n ON _smrt_signals(timestamp);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\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-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMP\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::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 NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helper only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL = CREATE_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_REGISTRY_TABLE,\n CREATE_SMRT_SIGNALS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Current SMRT system schema version\n */\nexport const SMRT_SCHEMA_VERSION = '1.8.0';\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqC1C,IAAa,+BAA+B;;;;;;;;;;;;;AAc5C,IAAa,sCAAsC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnD,IAAa,6BAA6B;;;;;;;;;;;;;;;AAgB1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;AA0BzC,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC5C,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8C1C,IAAa,2CAA2C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCxD,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoD1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;AAqBzC,IAAa,4CAA4C;;AAGzD,IAAa,gDAAgD,GAAG,0CAA0C;;;;;;;;;;;AAY1G,IAAa,8CAA8C;6BAC9B,0CAA0C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDvE,IAAa,+CAA+C;;;;;;;;EAQ1D,4CAA4C,KAAK,EAAE;;;;;;;;;;;;AAarD,IAAa,8CAA8C;;;;;;;wBAOnC,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;AAwBrD,IAAa,qCAAqC;;;;;;;EAjBV,0BAA0B,MAAM,GAAG,CAAC,CACzE,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,QAAQ,cAAc,UAAU,SAAS,CAAC,CAAC,CAC3C,KACE,cAAc;EACjB,UAAU;+BAEV,CAAC,CACA,KAAK,IAgBN,EAAgC;wBACV,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;;AAerD,IAAa,8BAA8B;;;;;;;;;;;AAY3C,IAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;AAKA,IAAa,sBAAsB"}
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Runtime object registry persistence\n * Stores metadata about registered SMRT objects for introspection\n */\nexport const CREATE_SMRT_REGISTRY_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_registry (\n class_name TEXT PRIMARY KEY,\n schema_version TEXT,\n fields TEXT,\n relationships TEXT,\n config TEXT,\n manifest TEXT,\n last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n`;\n\n/**\n * Signal history/audit log\n * Optional persistence of signals for debugging and auditing\n */\nexport const CREATE_SMRT_SIGNALS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_signals (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source_class TEXT,\n source_id TEXT,\n target_class TEXT,\n target_id TEXT,\n payload TEXT,\n timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_source\n ON _smrt_signals(source_class, source_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_type\n ON _smrt_signals(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_signals_timestamp\n ON _smrt_signals(timestamp);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\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-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ');\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::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 NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\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$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helper only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n IF to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NULL THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_REGISTRY_TABLE,\n CREATE_SMRT_SIGNALS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.\n * Other engines keep the established portable TIMESTAMP declarations.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n return engine === 'postgres'\n ? ddl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ')\n : ddl;\n}\n\n/**\n * Current SMRT system schema version\n */\nexport const SMRT_SCHEMA_VERSION = '1.9.0';\n"],"mappings":";;;;;AAaA,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqC1C,IAAa,+BAA+B;;;;;;;;;;;;;AAc5C,IAAa,sCAAsC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnD,IAAa,6BAA6B;;;;;;;;;;;;;;;AAgB1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;AA0BzC,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiC5C,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8C1C,IAAa,2CAA2C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCxD,IAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoD1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;AAqBzC,IAAa,qCACX,0BAA0B,QAAQ,kBAAkB,aAAa;;AAGnE,IAAa,4CAA4C;;AAGzD,IAAa,uDAAuD,GAAG,0CAA0C;;AAGjH,IAAa,gDAAgD,GAAG,0CAA0C;;;;;;;;;;;AAY1G,IAAa,8CAA8C;6BAC9B,0CAA0C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDvE,IAAa,+CAA+C;;;;;;;4BAOhC,qDAAqD;;EAE/E,4CAA4C,KAAK,EAAE;;;;;;;;;;;;AAarD,IAAa,8CAA8C;;;;;;;4BAO/B,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;AAyBrD,IAAa,qCAAqC;;;;;;;EAjBhD,mCAAmC,MAAM,GAAG,CAAC,CAC1C,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,QAAQ,cAAc,UAAU,SAAS,CAAC,CAAC,CAC3C,KACE,cAAc;EACnB,UAAU;+BAER,CAAC,CACA,KAAK,IAgBR,EAAgC;4BACN,qDAAqD;wBACzD,8CAA8C;;EAEpE,4CAA4C,KAAK,EAAE;;;;;;;;;;;;;;AAerD,IAAa,8BAA8B;;;;;;;;;;;AAY3C,IAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAQA,SAAgB,kBAAkB,QAAkC;CAClE,OAAO,kBAAkB,KAAK,QAC5B,2BAA2B,KAAK,MAAM,CACxC;AACF;;AAGA,SAAgB,2BACd,KACA,QACQ;CACR,OAAO,WAAW,aACd,IAAI,QAAQ,kBAAkB,aAAa,IAC3C;AACN;;;;AAKA,IAAa,sBAAsB"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiJ5D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAEpC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAuCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAwH5B"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAkJ5D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAEpC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAuCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAwH5B"}
@@ -1,4 +1,5 @@
1
- import { ALL_SYSTEM_TABLES } from "../system/schema.js";
1
+ import { detectEngine } from "../schema/ddl/index.js";
2
+ import { getSystemTableDDL } from "../system/schema.js";
2
3
  import { ensurePostgresChangeFeedAppendFunction } from "../change-feed.js";
3
4
  import { ensureLegacySystemTableCompatibility } from "../system/compatibility.js";
4
5
  import { SchemaGenerator } from "../schema/generator.js";
@@ -166,8 +167,10 @@ async function getTestDatabase(options = {}) {
166
167
  */
167
168
  async function initializeSystemTables(db) {
168
169
  await ensureLegacySystemTableCompatibility(db);
170
+ const configuredDb = db;
171
+ const engine = detectEngine(db.url || configuredDb.config?.url || "", configuredDb.type || configuredDb.config?.type);
169
172
  const allStatements = [];
170
- for (const multiStatementSQL of ALL_SYSTEM_TABLES) {
173
+ for (const multiStatementSQL of getSystemTableDDL(engine)) {
171
174
  const statements = multiStatementSQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
172
175
  allStatements.push(...statements);
173
176
  }
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport { ensurePostgresChangeFeedAppendFunction } from '../change-feed.js';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { ALL_SYSTEM_TABLES } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n * - 'duckdb': Native DuckDB database\n */\n type?: 'sqlite' | 'json' | 'duckdb';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // Native DuckDB adapter\n * const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' ||\n typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : type === 'duckdb'\n ? 'duckdb'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // PostgreSQL keeps the best-effort append boundary in a function that must\n // be executed whole rather than included in semicolon-split portable DDL.\n await ensurePostgresChangeFeedAppendFunction(db);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport { ensurePostgresChangeFeedAppendFunction } from '../change-feed.js';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { getSystemTableDDL } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n * - 'duckdb': Native DuckDB database\n */\n type?: 'sqlite' | 'json' | 'duckdb';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // Native DuckDB adapter\n * const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' ||\n typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : type === 'duckdb'\n ? 'duckdb'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n const configuredDb = db as DatabaseInterface & {\n config?: { type?: string; url?: string };\n type?: string;\n };\n const engine = detectEngine(\n db.url || configuredDb.config?.url || '',\n configuredDb.type || configuredDb.config?.type,\n );\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of getSystemTableDDL(engine)) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // PostgreSQL keeps the best-effort append boundary in a function that must\n // be executed whole rather than included in semicolon-split portable DDL.\n await ensurePostgresChangeFeedAppendFunction(db);\n}\n"],"mappings":";;;;;;;;;AAqCA,SAAS,2BAA2B,WAAmB,aAAqB;CAC1E,MAAM,aAAa,eAAe,SAAS,SAAS;CAEpD,IAAI,YAAY,SAAS,GAAG,GAC1B,OAAO,eAAe,SAAS,WAAW;CAG5C,IAAI,YAAY,aAAa;EAC3B,MAAM,kBAAkB,eAAe,kBACrC,WAAW,aACX,WACF;EACA,IAAI,iBACF,OAAO;CAEX;CAEA,OAAO,eAAe,SAAS,WAAW;AAC5C;AAEA,SAAS,yBACP,WACA,aACQ;CACR,MAAM,UAAU,2BAA2B,WAAW,WAAW;CACjE,OAAO,SAAS,iBAAiB,SAAS,QAAQ;AACpD;AAEA,SAAS,WAAW,WAA4B;CAC9C,MAAM,cAAc,eAAe,WAAW,SAAS;CACvD,IAAI,CAAC,aACH,OAAO;CAGT,MAAM,aAAa,eAAe,SAAS,SAAS;CACpD,MAAM,UAAU,2BAA2B,WAAW,WAAW;CAEjE,IAAI,cAAc,SAChB,OAAO,eAAe;CAGxB,OAAO,gBAAgB;AACzB;AAMA,IAAM,+BAA6D;CACjE,YAAY,cAAc,eAAe,SAAS,SAAS;CAC3D,qBAAqB,aAAa,cAChC,eAAe,kBAAkB,aAAa,SAAS;CACzD,sBAAsB,cACpB,eAAe,oBAAoB,SAAS;AAChD;AAEA,SAAS,iCACP,WACA,YACQ;CACR,MAAM,gBAAgB,+BACpB,WACA,YACA,4BACF;CACA,IAAI,eAAe;EACjB,MAAM,mBAAmB,2BACvB,eACA,YACA,4BACF;EACA,MAAM,iBACJ,kBAAkB,iBAClB,kBAAkB,QAClB;EACF,MAAM,UAAU,eAAe,WAAW,cAAc;EACxD,OAAO,UACH,yBAAyB,gBAAgB,OAAO,IAChD;CACN;CAEA,MAAM,YACJ,WAAW,QAAQ,aACnB,WAAW,OAAO,aAClB,eAAe,aAAa,SAAS;CACvC,IAAI,CAAC,WACH,OAAO;CAGT,MAAM,oBAAoB,WAAW;CAErC,KAAK,MAAM,aAAa,eAAe,cAAc,CAAC,CAAC,OAAO,GAAG;EAC/D,IACE,cAAc,cACd,yBACE,UAAU,iBAAiB,UAAU,MACrC,WACA,4BACF,GAEA;EAKF,KADE,UAAU,QAAQ,aAAa,UAAU,OAAO,eACvB,WACzB;EAGF,IACE,qBACA,UAAU,eACV,UAAU,gBAAgB,mBAE1B;EAGF,MAAM,sBAAsB,UAAU,iBAAiB,UAAU;EACjE,MAAM,UAAU,eAAe,WAAW,mBAAmB;EAC7D,OAAO,UACH,yBAAyB,qBAAqB,OAAO,IACrD;CACN;CAEA,OAAO;AACT;AAwCA,SAAS,gCAAgC,WAA2B;CAClE,MAAM,aAAa,eAAe,SAAS,SAAS;CACpD,IAAI,CAAC,YACH,OAAO;CAGT,IACE,CAAC,yBACC,WACA,YACA,4BACF,GACA;EACA,MAAM,UAAU,eAAe,WAAW,SAAS;EACnD,OAAO,UAAU,yBAAyB,WAAW,OAAO,IAAI;CAClE;CAEA,OAAO,iCAAiC,WAAW,UAAU;AAC/D;AAEA,SAAS,iCAAiC,YAAgC;CACxE,MAAM,WAAqB,CAAC;CAC5B,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,kBAAkB,gCAAgC,SAAS;EACjE,IAAI,KAAK,IAAI,eAAe,GAC1B;EAGF,KAAK,IAAI,eAAe;EACxB,SAAS,KAAK,eAAe;CAC/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,eAAsB,gBACpB,UAA+B,CAAC,GACJ;CAC5B,MAAM,EACJ,OAAO,UACP,MAAM,YACN,IAAI,YACJ,SACA,sBAAsB,SACpB;CAGJ,MAAM,KACJ,cACC,MAAM,YAAY;EACjB;EACA;EACA,mCAAmC;CACrC,CAAkC;CAGpC,IAAI,qBACF,MAAM,uBAAuB,EAAE;CAIjC,MAAM,aAAa,iCACjB,WAAW,eAAe,uBAAuB,CACnD;CAGA,IAAI,WAAW,WAAW,GACxB,OAAO;CAIT,MAAM,kBAAkB,IAAI,gBAAgB;CAC5C,MAAM,YACJ,SAAS,UACT,OAAQ,GAAiC,gBAAgB,aACrD,SACA,SAAS,WACP,WACA;CAGR,MAAM,gCAAgB,IAAI,IAAY;CAEtC,KAAK,MAAM,aAAa,YAAY;EAGlC,MAAM,aAAa,eAAe,SAAS,SAAS;EAKpD,IAAI,WAAW,SAAS,GACtB;EAGF,MAAM,YAAY,eAAe,aAAa,SAAS;EACvD,IAAI,CAAC,aAAa,cAAc,IAAI,SAAS,GAC3C;EAGF,MAAM,SAAS,MAAM,eAAe,aAAa,SAAS;EAC1D,MAAM,WAAW,eAAe,iBAAiB,SAAS;EAC1D,MAAM,sBAAsB;GAC1B,iBAAiB,eAAe,mBAAmB,SAAS;GAC5D,QAAQ,YAAY,OAAO;GAC3B,UAAU;EACZ;EAGA,MAAM,SACJ,aAAa,QACT,MAAM,gBAAgB,8BACpB,WACA,WACA,QACA,mBACF,IACA,gBAAgB,2BACd,WACA,WACA,QACA,mBACF;EAGN,MAAM,MAAM,gBAAgB,YAAY,QAAQ,SAAS;EAEzD,IAAI;GACF,MAAM,GAAG,MAAM,GAAG;GAClB,cAAc,IAAI,SAAS;GAM3B,MAAM,mBAFJ,MAAM,OAAO,0BAAA,CACb,eAAe,SACO,CAAA,CAAY,gBAAgB,MAAM;GAC1D,KAAK,MAAM,YAAY,iBACrB,IAAI;IACF,MAAM,GAAG,MAAM,QAAQ;GACzB,SAAS,YAAY;IAGnB,QAAQ,KACN,sDAAsD,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU,EAAE,SAAS,SAAS,EAChJ;GACF;EAEJ,SAAS,OAAO;GAEd,MAAM,IAAI,MACR,6CAA6C,UAAU,eAAe,UAAU,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAC1I,EAAE,OAAO,MAAM,CACjB;EACF;CACF;CAEA,OAAO;AACT;;;;;;;;;AAUA,eAAe,uBAAuB,IAAsC;CAC1E,MAAM,qCAAqC,EAAE;CAE7C,MAAM,eAAe;CAIrB,MAAM,SAAS,aACb,GAAG,OAAO,aAAa,QAAQ,OAAO,IACtC,aAAa,QAAQ,aAAa,QAAQ,IAC5C;CAGA,MAAM,gBAA0B,CAAC;CACjC,KAAK,MAAM,qBAAqB,kBAAkB,MAAM,GAAG;EACzD,MAAM,aAAa,kBAChB,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CACpB,QAAQ,MAAM,EAAE,SAAS,CAAC;EAC7B,cAAc,KAAK,GAAG,UAAU;CAClC;CAIA,KAAK,MAAM,aAAa,eACtB,MAAM,GAAG,MAAM,SAAS;CAK1B,MAAM,uCAAuC,EAAE;AACjD"}