@happyvertical/smrt-core 0.40.70 → 0.41.0

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 (157) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/change-feed.md +1 -1
  3. package/agents/schema-paths.md +487 -29
  4. package/dist/browser.js +5 -4
  5. package/dist/cascade.d.ts +120 -0
  6. package/dist/cascade.d.ts.map +1 -0
  7. package/dist/cascade.js +430 -0
  8. package/dist/cascade.js.map +1 -0
  9. package/dist/change-feed.d.ts +34 -2
  10. package/dist/change-feed.d.ts.map +1 -1
  11. package/dist/change-feed.js +52 -11
  12. package/dist/change-feed.js.map +1 -1
  13. package/dist/class.d.ts +36 -3
  14. package/dist/class.d.ts.map +1 -1
  15. package/dist/class.js +59 -5
  16. package/dist/class.js.map +1 -1
  17. package/dist/collection-cache.js +0 -0
  18. package/dist/collection-cache.js.map +1 -1
  19. package/dist/collection.d.ts +29 -0
  20. package/dist/collection.d.ts.map +1 -1
  21. package/dist/collection.js +66 -14
  22. package/dist/collection.js.map +1 -1
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js.map +1 -1
  26. package/dist/decorators/index.d.ts +63 -5
  27. package/dist/decorators/index.d.ts.map +1 -1
  28. package/dist/decorators/index.js +37 -5
  29. package/dist/decorators/index.js.map +1 -1
  30. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  31. package/dist/dispatch/collections/Dispatches.js +19 -4
  32. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  33. package/dist/dispatch/types.d.ts +5 -0
  34. package/dist/dispatch/types.d.ts.map +1 -1
  35. package/dist/embedded-write-queue.d.ts +46 -0
  36. package/dist/embedded-write-queue.d.ts.map +1 -0
  37. package/dist/embedded-write-queue.js +66 -0
  38. package/dist/embedded-write-queue.js.map +1 -0
  39. package/dist/embeddings/storage.d.ts +7 -0
  40. package/dist/embeddings/storage.d.ts.map +1 -1
  41. package/dist/embeddings/storage.js +29 -12
  42. package/dist/embeddings/storage.js.map +1 -1
  43. package/dist/errors.d.ts +1 -1
  44. package/dist/hierarchical.js +1 -1
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -6
  48. package/dist/interceptors.d.ts +21 -0
  49. package/dist/interceptors.d.ts.map +1 -1
  50. package/dist/interceptors.js +27 -1
  51. package/dist/interceptors.js.map +1 -1
  52. package/dist/manifest/generator.d.ts.map +1 -1
  53. package/dist/manifest/generator.js +4 -7
  54. package/dist/manifest/generator.js.map +1 -1
  55. package/dist/manifest/static-manifest.js +10 -10
  56. package/dist/manifest/static-manifest.js.map +1 -1
  57. package/dist/manifest/store.js +1 -1
  58. package/dist/manifest/store.js.map +1 -1
  59. package/dist/manifest.json +19 -19
  60. package/dist/migrations/differ.d.ts +9 -1
  61. package/dist/migrations/differ.d.ts.map +1 -1
  62. package/dist/migrations/differ.js +10 -5
  63. package/dist/migrations/differ.js.map +1 -1
  64. package/dist/migrations/index.d.ts +1 -0
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +2 -1
  67. package/dist/migrations/minor-units.d.ts +162 -0
  68. package/dist/migrations/minor-units.d.ts.map +1 -0
  69. package/dist/migrations/minor-units.js +381 -0
  70. package/dist/migrations/minor-units.js.map +1 -0
  71. package/dist/migrations.js +2 -1
  72. package/dist/object.d.ts +98 -5
  73. package/dist/object.d.ts.map +1 -1
  74. package/dist/object.js +138 -30
  75. package/dist/object.js.map +1 -1
  76. package/dist/query-bounds.d.ts +5 -1
  77. package/dist/query-bounds.d.ts.map +1 -1
  78. package/dist/query-bounds.js +5 -1
  79. package/dist/query-bounds.js.map +1 -1
  80. package/dist/registry/class-registration.d.ts.map +1 -1
  81. package/dist/registry/class-registration.js +3 -1
  82. package/dist/registry/class-registration.js.map +1 -1
  83. package/dist/registry/manifest-field-merge.d.ts +12 -0
  84. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  85. package/dist/registry/manifest-field-merge.js +14 -2
  86. package/dist/registry/manifest-field-merge.js.map +1 -1
  87. package/dist/registry/schema-builder.d.ts +22 -1
  88. package/dist/registry/schema-builder.d.ts.map +1 -1
  89. package/dist/registry/schema-builder.js +205 -165
  90. package/dist/registry/schema-builder.js.map +1 -1
  91. package/dist/registry/types.d.ts +3 -2
  92. package/dist/registry/types.d.ts.map +1 -1
  93. package/dist/registry.d.ts +41 -46
  94. package/dist/registry.d.ts.map +1 -1
  95. package/dist/registry.js +61 -83
  96. package/dist/registry.js.map +1 -1
  97. package/dist/scanner/manifest-generator.d.ts +45 -0
  98. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  99. package/dist/scanner/manifest-generator.js +92 -28
  100. package/dist/scanner/manifest-generator.js.map +1 -1
  101. package/dist/schema/conflict-target.d.ts +104 -0
  102. package/dist/schema/conflict-target.d.ts.map +1 -0
  103. package/dist/schema/conflict-target.js +129 -0
  104. package/dist/schema/conflict-target.js.map +1 -0
  105. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  106. package/dist/schema/ddl/duckdb-strategy.js +2 -1
  107. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  108. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  109. package/dist/schema/ddl/postgres-strategy.js +12 -1
  110. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  111. package/dist/schema/generator.d.ts +252 -16
  112. package/dist/schema/generator.d.ts.map +1 -1
  113. package/dist/schema/generator.js +411 -106
  114. package/dist/schema/generator.js.map +1 -1
  115. package/dist/schema/index-utils.d.ts +120 -0
  116. package/dist/schema/index-utils.d.ts.map +1 -1
  117. package/dist/schema/index-utils.js +242 -1
  118. package/dist/schema/index-utils.js.map +1 -1
  119. package/dist/schema/index.d.ts +1 -1
  120. package/dist/schema/index.d.ts.map +1 -1
  121. package/dist/schema/index.js +2 -2
  122. package/dist/schema/live-parity.d.ts.map +1 -1
  123. package/dist/schema/live-parity.js +4 -2
  124. package/dist/schema/live-parity.js.map +1 -1
  125. package/dist/schema/system-table-shapes.d.ts +13 -0
  126. package/dist/schema/system-table-shapes.d.ts.map +1 -1
  127. package/dist/schema/system-table-shapes.js +14 -1
  128. package/dist/schema/system-table-shapes.js.map +1 -1
  129. package/dist/schema/utils.d.ts +1 -1
  130. package/dist/schema/utils.d.ts.map +1 -1
  131. package/dist/schema/utils.js +3 -3
  132. package/dist/schema/utils.js.map +1 -1
  133. package/dist/schema.js +2 -2
  134. package/dist/smrt-knowledge.json +9 -9
  135. package/dist/system/compatibility.d.ts +42 -0
  136. package/dist/system/compatibility.d.ts.map +1 -1
  137. package/dist/system/compatibility.js +182 -9
  138. package/dist/system/compatibility.js.map +1 -1
  139. package/dist/system/index.d.ts +1 -0
  140. package/dist/system/index.d.ts.map +1 -1
  141. package/dist/system/index.js +3 -2
  142. package/dist/system/retention.d.ts +237 -0
  143. package/dist/system/retention.d.ts.map +1 -0
  144. package/dist/system/retention.js +497 -0
  145. package/dist/system/retention.js.map +1 -0
  146. package/dist/system/schema.d.ts +100 -15
  147. package/dist/system/schema.d.ts.map +1 -1
  148. package/dist/system/schema.js +81 -45
  149. package/dist/system/schema.js.map +1 -1
  150. package/dist/system/types.d.ts +0 -2
  151. package/dist/system/types.d.ts.map +1 -1
  152. package/dist/testing/database.d.ts.map +1 -1
  153. package/dist/testing/database.js.map +1 -1
  154. package/dist/vite-plugin/index.d.ts.map +1 -1
  155. package/dist/vite-plugin/index.js +4 -9
  156. package/dist/vite-plugin/index.js.map +1 -1
  157. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"compatibility.js","names":[],"sources":["../../src/system/compatibility.ts"],"sourcesContent":["import type { DatabaseInterface } from '@happyvertical/sql';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport {\n CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION,\n LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY,\n} from './schema.js';\n\ntype DatabaseWithConfig = DatabaseInterface & {\n config?: {\n type?: string;\n url?: string;\n };\n type?: string;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) {\n return result as Record<string, unknown>[];\n }\n\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) {\n return rows as Record<string, unknown>[];\n }\n }\n\n return [];\n}\n\nexport async function tableExists(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1 LIMIT 1',\n tableName,\n );\n return getQueryRows(result).length > 0;\n }\n\n await db.query(`SELECT 1 FROM ${tableName} LIMIT 1`);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function columnExists(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2 LIMIT 1',\n tableName,\n columnName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(`PRAGMA table_info(${tableName})`);\n return getQueryRows(result).some((row) => row.name === columnName);\n } catch {\n return false;\n }\n}\n\nfunction getDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseWithConfig;\n return db.url || dbWithConfig.config?.url || '';\n}\n\nexport function getDatabaseEngine(\n db: DatabaseInterface,\n typeHint?: string,\n): ReturnType<typeof detectEngine> {\n const dbWithConfig = db as DatabaseWithConfig;\n return detectEngine(\n getDatabaseUrl(db),\n typeHint || dbWithConfig.type || dbWithConfig.config?.type,\n );\n}\n\n/**\n * Upgrade framework-owned PostgreSQL timestamp columns to instant-safe storage.\n *\n * Legacy SMRT and system writers serialized instants as UTC wall times. The\n * conversion is intentionally fail-closed unless the migration session is UTC:\n * operators must first confirm that historical database-default/raw writers\n * also used UTC. A deployment with non-UTC legacy writers needs an explicit,\n * provenance-aware data migration rather than a guessed offset.\n */\nexport interface PostgresSystemTimestampMigrationConfirmation {\n /**\n * Explicitly confirmed timezone used by every historical writer of legacy\n * timezone-naive system timestamps. Only UTC is supported by this helper.\n */\n legacyTimezone: 'UTC';\n}\n\nexport interface PostgresSystemTimestampMigrationPlan {\n kind: 'column' | 'change-feed-function';\n tableName: string;\n columnName?: string;\n sql: string;\n}\n\nfunction quotePostgresIdentifier(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Read-only plan for the framework-owned timestamp conversion. This is kept\n * separate from the atomic mutator so CLI dry-runs can report every ALTER\n * without bootstrapping or changing framework tables.\n */\nexport async function planPostgresSystemTimestampMigrations(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<PostgresSystemTimestampMigrationPlan[]> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return [];\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n const sessionRows = getQueryRows(\n await db.query(`\n SELECT\n current_setting('TimeZone') AS timezone,\n to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n IS NOT NULL AS legacy_change_feed_function_exists\n `),\n );\n const session = sessionRows[0] ?? {};\n const timezone = String(session.timezone ?? '').toUpperCase();\n if (!['UTC', 'ETC/UTC', 'GMT'].includes(timezone)) {\n throw new Error(\n `Refusing to preview legacy SMRT timestamp migration outside a UTC PostgreSQL session (current TimeZone: ${String(session.timezone ?? 'unknown')})`,\n );\n }\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n\n const plans: PostgresSystemTimestampMigrationPlan[] = rows.map((row) => {\n const tableName = String(row.table_name);\n const columnName = String(row.column_name);\n const table = quotePostgresIdentifier(tableName);\n const column = quotePostgresIdentifier(columnName);\n return {\n kind: 'column',\n tableName,\n columnName,\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE TIMESTAMPTZ USING ${column} AT TIME ZONE 'UTC'`,\n };\n });\n\n if (\n session.legacy_change_feed_function_exists === true ||\n session.legacy_change_feed_function_exists === 't' ||\n session.legacy_change_feed_function_exists === 'true' ||\n session.legacy_change_feed_function_exists === 1\n ) {\n plans.push({\n kind: 'change-feed-function',\n tableName: '_smrt_changes',\n sql: `DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}`,\n });\n }\n\n return plans;\n}\n\n/**\n * Fail closed when framework-owned PostgreSQL tables still contain legacy\n * timezone-naive timestamps. Call this before recording the current system\n * schema version so a partial installation cannot be stamped as upgraded.\n */\nexport async function assertPostgresSystemTimestampsCurrent(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n if (rows.length === 0) return;\n\n const columns = rows\n .map((row) => `${String(row.table_name)}.${String(row.column_name)}`)\n .join(', ');\n throw new Error(\n `Legacy SMRT system timestamps remain (${columns}); run the explicit audited migratePostgresSystemTimestamps() migration before bootstrap`,\n );\n}\n\nexport async function migratePostgresSystemTimestamps(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n // One server-side statement makes the catalog change atomic and holds the\n // same transaction-scoped lock used by system bootstrap. Any failing column\n // rolls back every preceding ALTER in the block.\n await db.query(`\n DO $smrt_migrate_system_timestamps$\n DECLARE\n target RECORD;\n BEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN\n RAISE EXCEPTION 'Refusing to reinterpret legacy SMRT timestamps outside a UTC PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n END IF;\n FOR target IN\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n LOOP\n EXECUTE format(\n 'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE ''UTC''',\n target.table_name,\n target.column_name,\n target.column_name\n );\n END LOOP;\n IF to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NOT NULL THEN\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\n END;\n $smrt_migrate_system_timestamps$;\n `);\n}\n\nfunction isDuplicateColumnError(error: unknown): boolean {\n const message =\n error && typeof error === 'object'\n ? String((error as { message?: unknown }).message || error)\n : String(error);\n\n return (\n /column .*already exists/i.test(message) ||\n /duplicate column name/i.test(message)\n );\n}\n\nfunction collectErrorDetails(\n error: unknown,\n seen = new Set<unknown>(),\n): {\n codes: Set<string>;\n messages: string[];\n} {\n const codes = new Set<string>();\n const messages: string[] = [];\n\n if (!error || seen.has(error)) {\n return { codes, messages };\n }\n\n seen.add(error);\n\n if (typeof error === 'object') {\n const errorRecord = error as Record<string, unknown>;\n\n if (typeof errorRecord.code === 'string') {\n codes.add(errorRecord.code);\n }\n\n for (const messageKey of ['message', 'detail']) {\n if (typeof errorRecord[messageKey] === 'string') {\n messages.push(errorRecord[messageKey]);\n }\n }\n\n for (const nestedKey of ['cause', 'context', 'originalError']) {\n const nested = collectErrorDetails(errorRecord[nestedKey], seen);\n for (const code of nested.codes) {\n codes.add(code);\n }\n messages.push(...nested.messages);\n }\n } else {\n messages.push(String(error));\n }\n\n return { codes, messages };\n}\n\nfunction isDuplicateIndexRaceError(error: unknown, indexName: string): boolean {\n const { codes, messages } = collectErrorDetails(error);\n const message = messages.join('\\n').toLowerCase();\n const normalizedIndexName = indexName.toLowerCase();\n const hasDuplicateCode =\n codes.has('23505') || /\\bcode\\s*=\\s*23505\\b/.test(message);\n const hasRelationExistsCode =\n codes.has('42P07') || /\\bcode\\s*=\\s*42p07\\b/i.test(message);\n\n if (!message.includes(normalizedIndexName)) {\n return false;\n }\n\n if (hasDuplicateCode && message.includes('pg_class_relname_nsp_index')) {\n return true;\n }\n\n if (hasRelationExistsCode && /relation .*already exists/i.test(message)) {\n return true;\n }\n\n return false;\n}\n\nasync function addColumnIfMissing(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n definition: string,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n if (engine === 'postgres') {\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${definition}`,\n );\n return;\n }\n\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`,\n );\n } catch (error) {\n if (isDuplicateColumnError(error)) {\n return;\n }\n\n throw error;\n }\n}\n\nasync function indexExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND indexname = $1 LIMIT 1',\n indexName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(\n `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1`,\n indexName,\n );\n return getQueryRows(result).length > 0;\n } catch {\n return false;\n }\n}\n\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 // MCP task support was added after `_smrt_jobs` had already shipped. Keep\n // the task projection in the job row so a task can never outlive (or lose\n // track of) the worker job that executes it. JSON is portable here because\n // SMRT's adapters serialize JSON fields to text where necessary.\n await addColumnIfMissing(db, '_smrt_jobs', 'task_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_owner_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_result', 'TEXT', typeHint);\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_requests',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_responses',\n 'TEXT',\n typeHint,\n );\n await addUniqueIndexIfMissing(\n db,\n 'idx_smrt_jobs_task_id',\n '_smrt_jobs',\n 'task_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\nexport interface PostgresSystemTimestampMigrationPlan {\n kind: 'column' | 'change-feed-function';\n tableName: string;\n columnName?: string;\n sql: string;\n}\n\nfunction quotePostgresIdentifier(identifier: string): string {\n return `\"${identifier.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Read-only plan for the framework-owned timestamp conversion. This is kept\n * separate from the atomic mutator so CLI dry-runs can report every ALTER\n * without bootstrapping or changing framework tables.\n */\nexport async function planPostgresSystemTimestampMigrations(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<PostgresSystemTimestampMigrationPlan[]> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return [];\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n const sessionRows = getQueryRows(\n await db.query(`\n SELECT\n current_setting('TimeZone') AS timezone,\n to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n IS NOT NULL AS legacy_change_feed_function_exists\n `),\n );\n const session = sessionRows[0] ?? {};\n const timezone = String(session.timezone ?? '').toUpperCase();\n if (!['UTC', 'ETC/UTC', 'GMT'].includes(timezone)) {\n throw new Error(\n `Refusing to preview legacy SMRT timestamp migration outside a UTC PostgreSQL session (current TimeZone: ${String(session.timezone ?? 'unknown')})`,\n );\n }\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n\n const plans: PostgresSystemTimestampMigrationPlan[] = rows.map((row) => {\n const tableName = String(row.table_name);\n const columnName = String(row.column_name);\n const table = quotePostgresIdentifier(tableName);\n const column = quotePostgresIdentifier(columnName);\n return {\n kind: 'column',\n tableName,\n columnName,\n sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE TIMESTAMPTZ USING ${column} AT TIME ZONE 'UTC'`,\n };\n });\n\n if (\n session.legacy_change_feed_function_exists === true ||\n session.legacy_change_feed_function_exists === 't' ||\n session.legacy_change_feed_function_exists === 'true' ||\n session.legacy_change_feed_function_exists === 1\n ) {\n plans.push({\n kind: 'change-feed-function',\n tableName: '_smrt_changes',\n sql: `DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}`,\n });\n }\n\n return plans;\n}\n\n/**\n * Fail closed when framework-owned PostgreSQL tables still contain legacy\n * timezone-naive timestamps. Call this before recording the current system\n * schema version so a partial installation cannot be stamped as upgraded.\n */\nexport async function assertPostgresSystemTimestampsCurrent(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n\n const rows = getQueryRows(\n await db.query(`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n `),\n );\n if (rows.length === 0) return;\n\n const columns = rows\n .map((row) => `${String(row.table_name)}.${String(row.column_name)}`)\n .join(', ');\n throw new Error(\n `Legacy SMRT system timestamps remain (${columns}); run the explicit audited migratePostgresSystemTimestamps() migration before bootstrap`,\n );\n}\n\nexport async function migratePostgresSystemTimestamps(\n db: DatabaseInterface,\n confirmation: PostgresSystemTimestampMigrationConfirmation,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') return;\n if (confirmation.legacyTimezone !== 'UTC') {\n throw new Error(\n 'Legacy SMRT system timestamp migration requires an explicit UTC provenance confirmation',\n );\n }\n\n // One server-side statement makes the catalog change atomic and holds the\n // same transaction-scoped lock used by system bootstrap. Any failing column\n // rolls back every preceding ALTER in the block.\n await db.query(`\n DO $smrt_migrate_system_timestamps$\n DECLARE\n target RECORD;\n BEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n IF upper(current_setting('TimeZone')) NOT IN ('UTC', 'ETC/UTC', 'GMT') THEN\n RAISE EXCEPTION 'Refusing to reinterpret legacy SMRT timestamps outside a UTC PostgreSQL session; confirm historical writers used UTC and SET TIME ZONE UTC';\n END IF;\n FOR target IN\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = current_schema()\n AND table_name LIKE '\\\\_smrt\\\\_%' ESCAPE '\\\\'\n AND data_type = 'timestamp without time zone'\n ORDER BY table_name, ordinal_position\n LOOP\n EXECUTE format(\n 'ALTER TABLE %I ALTER COLUMN %I TYPE TIMESTAMPTZ USING %I AT TIME ZONE ''UTC''',\n target.table_name,\n target.column_name,\n target.column_name\n );\n END LOOP;\n IF to_regprocedure('${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}') IS NOT NULL THEN\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\n END;\n $smrt_migrate_system_timestamps$;\n `);\n}\n\nfunction isDuplicateColumnError(error: unknown): boolean {\n const message =\n error && typeof error === 'object'\n ? String((error as { message?: unknown }).message || error)\n : String(error);\n\n return (\n /column .*already exists/i.test(message) ||\n /duplicate column name/i.test(message)\n );\n}\n\nfunction collectErrorDetails(\n error: unknown,\n seen = new Set<unknown>(),\n): {\n codes: Set<string>;\n messages: string[];\n} {\n const codes = new Set<string>();\n const messages: string[] = [];\n\n if (!error || seen.has(error)) {\n return { codes, messages };\n }\n\n seen.add(error);\n\n if (typeof error === 'object') {\n const errorRecord = error as Record<string, unknown>;\n\n if (typeof errorRecord.code === 'string') {\n codes.add(errorRecord.code);\n }\n\n for (const messageKey of ['message', 'detail']) {\n if (typeof errorRecord[messageKey] === 'string') {\n messages.push(errorRecord[messageKey]);\n }\n }\n\n for (const nestedKey of ['cause', 'context', 'originalError']) {\n const nested = collectErrorDetails(errorRecord[nestedKey], seen);\n for (const code of nested.codes) {\n codes.add(code);\n }\n messages.push(...nested.messages);\n }\n } else {\n messages.push(String(error));\n }\n\n return { codes, messages };\n}\n\nfunction isDuplicateIndexRaceError(error: unknown, indexName: string): boolean {\n const { codes, messages } = collectErrorDetails(error);\n const message = messages.join('\\n').toLowerCase();\n const normalizedIndexName = indexName.toLowerCase();\n const hasDuplicateCode =\n codes.has('23505') || /\\bcode\\s*=\\s*23505\\b/.test(message);\n const hasRelationExistsCode =\n codes.has('42P07') || /\\bcode\\s*=\\s*42p07\\b/i.test(message);\n\n if (!message.includes(normalizedIndexName)) {\n return false;\n }\n\n if (hasDuplicateCode && message.includes('pg_class_relname_nsp_index')) {\n return true;\n }\n\n if (hasRelationExistsCode && /relation .*already exists/i.test(message)) {\n return true;\n }\n\n return false;\n}\n\nasync function addColumnIfMissing(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n definition: string,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n if (engine === 'postgres') {\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS ${columnName} ${definition}`,\n );\n return;\n }\n\n if (await columnExists(db, tableName, columnName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`,\n );\n } catch (error) {\n if (isDuplicateColumnError(error)) {\n return;\n }\n\n throw error;\n }\n}\n\nasync function indexExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<boolean> {\n try {\n const engine = getDatabaseEngine(db, typeHint);\n if (engine === 'postgres') {\n const result = await db.query(\n 'SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND indexname = $1 LIMIT 1',\n indexName,\n );\n return getQueryRows(result).length > 0;\n }\n\n const result = await db.query(\n `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1`,\n indexName,\n );\n return getQueryRows(result).length > 0;\n } catch {\n return false;\n }\n}\n\n/** @param columns - One column, or a comma-separated list for a composite index. */\nasync function addIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Read a PostgreSQL `text[]` result as a JS array.\n *\n * `attname` is `name`-typed, and drivers that have no parser registered for\n * the resulting array OID hand back the raw literal (`{a,b}`) instead. Reading\n * that as \"no columns\" would silently make every index look like it covers\n * nothing — and, for #2376's reconciliation, would recreate an index that\n * already exists. The query casts to `text` and this parses either shape.\n */\nfunction parsePostgresTextArray(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map(String);\n }\n if (typeof value !== 'string') {\n return [];\n }\n const literal = value.trim();\n if (!literal.startsWith('{') || !literal.endsWith('}')) {\n return [];\n }\n const body = literal.slice(1, -1).trim();\n if (body.length === 0) {\n return [];\n }\n return body\n .split(',')\n .map((entry) => entry.trim().replace(/^\"|\"$/g, ''))\n .filter((entry) => entry.length > 0);\n}\n\n/** One live index as reported by the engine's catalog. */\ninterface LiveIndex {\n name: string;\n unique: boolean;\n columns: string[];\n /** True when the index carries a WHERE predicate (partial index). */\n partial: boolean;\n}\n\n/**\n * Read the live indexes of a table, including the implicit ones a UNIQUE\n * column constraint creates (`sqlite_autoindex_*`, PostgreSQL's `*_key`).\n *\n * PostgreSQL and SQLite only. Returns `null` on DuckDB and the JSON adapter,\n * whose catalogs report constraint-backed uniqueness somewhere other than the\n * index list (`duckdb_constraints()` rather than `duckdb_indexes()`) — reading\n * one without the other would report \"no unique index\" for a column that has\n * one. Callers must treat `null` as \"unknown\" and keep their pre-existing\n * behaviour rather than guess: leaving a redundant index in place is a cost,\n * dropping the only one that enforces an upsert conflict target is a bug.\n */\nasync function listTableIndexes(\n db: DatabaseInterface,\n tableName: string,\n typeHint?: string,\n): Promise<LiveIndex[] | null> {\n const engine = getDatabaseEngine(db, typeHint);\n\n try {\n if (engine === 'postgres') {\n const result = await db.query(\n `SELECT\n i.relname AS name,\n ix.indisunique AS is_unique,\n (ix.indpred IS NOT NULL) AS is_partial,\n ARRAY(\n SELECT a.attname::text\n FROM unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a\n ON a.attrelid = t.oid AND a.attnum = k.attnum\n ORDER BY k.ord\n ) AS columns\n FROM pg_class t\n JOIN pg_index ix ON ix.indrelid = t.oid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = current_schema() AND t.relname = $1`,\n tableName,\n );\n return getQueryRows(result).map((row) => ({\n name: String(row.name),\n unique: row.is_unique === true || row.is_unique === 't',\n partial: row.is_partial === true || row.is_partial === 't',\n columns: parsePostgresTextArray(row.columns),\n }));\n }\n\n if (engine !== 'sqlite') {\n return null;\n }\n\n const listed = getQueryRows(\n await db.query(`PRAGMA index_list(${tableName})`),\n );\n const indexes: LiveIndex[] = [];\n for (const row of listed) {\n const name = String(row.name);\n const info = getQueryRows(await db.query(`PRAGMA index_info(${name})`));\n indexes.push({\n name,\n unique: row.unique === 1 || row.unique === true || row.unique === '1',\n partial:\n row.partial === 1 || row.partial === true || row.partial === '1',\n columns: info.map((column) => String(column.name)),\n });\n }\n return indexes;\n } catch {\n return null;\n }\n}\n\n/** Drop an index, tolerating engines/permissions that refuse the statement. */\nasync function dropIndexIfExists(\n db: DatabaseInterface,\n indexName: string,\n typeHint?: string,\n): Promise<void> {\n if (!(await indexExists(db, indexName, typeHint))) {\n return;\n }\n\n try {\n await db.query(`DROP INDEX IF EXISTS ${indexName}`);\n } catch {\n // Best-effort: a redundant index is a cost, not a correctness problem.\n }\n}\n\nasync function addUniqueIndexIfMissing(\n db: DatabaseInterface,\n indexName: string,\n tableName: string,\n columns: string,\n typeHint?: string,\n): Promise<void> {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n\n try {\n await db.query(\n `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columns})`,\n );\n } catch (error) {\n if (isDuplicateIndexRaceError(error, indexName)) {\n if (await indexExists(db, indexName, typeHint)) {\n return;\n }\n }\n\n throw error;\n }\n}\n\n/**\n * Migrate the legacy `UNIQUE(signal_type, subscriber)` subscription identity to\n * the tenant-scoped `UNIQUE(tenant_id, signal_type, subscriber)` (S5 #1398).\n *\n * The old identity let any tenant overwrite/delete/enable another tenant's\n * subscription because the natural key ignored the tenant. This:\n * 1. Creates the tenant-scoped unique index used by upsert conflict handling.\n * 2. Drops the legacy 2-column unique constraint:\n * - **SQLite**: inline `UNIQUE(...)` constraints can't be dropped in place,\n * so the table is rebuilt without it (data preserved). The legacy\n * constraint surfaces as an auto-created `sqlite_autoindex_*` index.\n * - **Postgres**: the inline constraint is dropped by its conventional\n * name if present.\n *\n * Additive and idempotent: re-running is a no-op once migrated.\n */\nasync function migrateDispatchSubscriptionsIdentity(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const engine = getDatabaseEngine(db, typeHint);\n\n // 1. Tenant-scoped unique index (used by upsert's ON CONFLICT target).\n await addUniqueIndexIfMissing(\n db,\n 'uq_smrt_dispatch_subs_tenant_signal_subscriber',\n '_smrt_dispatch_subscriptions',\n 'tenant_id, signal_type, subscriber',\n typeHint,\n );\n\n // 2. Drop the legacy (signal_type, subscriber) uniqueness.\n if (engine === 'postgres') {\n // Inline table-level UNIQUE constraints get a conventional name. Drop it if\n // present; ignore if it was never created under that name.\n try {\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions DROP CONSTRAINT IF EXISTS _smrt_dispatch_subscriptions_signal_type_subscriber_key',\n );\n } catch {\n // Best-effort: a differently-named legacy constraint is left in place; the\n // tenant-scoped index above still enforces correct per-tenant identity.\n }\n return;\n }\n\n // SQLite: detect a legacy auto-index over exactly (signal_type, subscriber)\n // and, if found, rebuild the table without the inline UNIQUE.\n if (engine !== 'sqlite') {\n return;\n }\n\n let legacyAutoIndex: string | null = null;\n try {\n const result = await db.query(\n `SELECT name FROM sqlite_master\n WHERE type = 'index'\n AND tbl_name = '_smrt_dispatch_subscriptions'\n AND name LIKE 'sqlite_autoindex_%'`,\n );\n for (const row of getQueryRows(result)) {\n const indexName = String(row.name);\n const info = await db.query(`PRAGMA index_info(${indexName})`);\n const cols = getQueryRows(info)\n .map((r) => String(r.name))\n .sort();\n if (\n cols.length === 2 &&\n cols[0] === 'signal_type' &&\n cols[1] === 'subscriber'\n ) {\n legacyAutoIndex = indexName;\n break;\n }\n }\n } catch {\n // Unable to introspect → assume no legacy constraint (fresh tables created\n // from the new schema have none).\n legacyAutoIndex = null;\n }\n\n if (!legacyAutoIndex) {\n return;\n }\n\n // Copy only the columns the legacy table actually has. The rebuild used to\n // name all nine unconditionally, so a database predating any one of them\n // failed the whole bootstrap with a bare `no such column` (issue #2376).\n // Columns absent from the source take the new table's default.\n const legacyColumns = new Set(\n getQueryRows(\n await db.query('PRAGMA table_info(_smrt_dispatch_subscriptions)'),\n ).map((row) => String(row.name)),\n );\n const copiedColumns = [\n 'id',\n 'signal_type',\n 'subscriber',\n 'handler',\n 'delivery',\n 'enabled',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ].filter((column) => legacyColumns.has(column));\n const copiedColumnList = copiedColumns.join(', ');\n\n // Rebuild the table without the inline UNIQUE(signal_type, subscriber).\n // System table is small; this runs once per legacy database.\n await db.query('PRAGMA foreign_keys=OFF');\n try {\n await db.query(`\n CREATE TABLE _smrt_dispatch_subscriptions_new (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )\n `);\n await db.query(`\n INSERT INTO _smrt_dispatch_subscriptions_new\n (${copiedColumnList})\n SELECT ${copiedColumnList}\n FROM _smrt_dispatch_subscriptions\n `);\n await db.query('DROP TABLE _smrt_dispatch_subscriptions');\n await db.query(\n 'ALTER TABLE _smrt_dispatch_subscriptions_new RENAME TO _smrt_dispatch_subscriptions',\n );\n // Recreate the secondary indexes (the rebuild dropped them) plus the\n // tenant-scoped unique index.\n await db.query(\n 'CREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber ON _smrt_dispatch_subscriptions(subscriber)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id ON _smrt_dispatch_subscriptions(tenant_id)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type ON _smrt_dispatch_subscriptions(signal_type)',\n );\n await db.query(\n 'CREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled ON _smrt_dispatch_subscriptions(enabled)',\n );\n } finally {\n await db.query('PRAGMA foreign_keys=ON');\n }\n}\n\nexport async function ensureDispatchSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'target_subscriber',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_dispatch',\n 'correlation_id',\n 'TEXT',\n typeHint,\n );\n // Tenant isolation (S5 #1398): dispatches are scoped to the emitting\n // tenant context so a subscriber in tenant A cannot snoop/claim tenant B's\n // dispatches. Nullable so non-tenant (global) dispatches keep tenant_id NULL.\n await addColumnIfMissing(db, '_smrt_dispatch', 'tenant_id', 'TEXT', typeHint);\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_target',\n '_smrt_dispatch',\n 'target_subscriber',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_correlation',\n '_smrt_dispatch',\n 'correlation_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_tenant_id',\n '_smrt_dispatch',\n 'tenant_id',\n typeHint,\n );\n}\n\nexport async function ensureDispatchSubscriptionsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_dispatch_subscriptions', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'delivery',\n \"TEXT NOT NULL DEFAULT 'compete'\",\n typeHint,\n );\n // Tenant isolation (S5 #1398): records the tenant context a subscription was\n // registered in. Nullable so global/non-tenant subscriptions keep it NULL.\n await addColumnIfMissing(\n db,\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n 'TEXT',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_dispatch_subs_tenant_id',\n '_smrt_dispatch_subscriptions',\n 'tenant_id',\n typeHint,\n );\n // Migrate the subscription natural key to (tenant_id, signal_type, subscriber)\n // so tenants can't clobber each other's subscriptions (S5 #1398).\n await migrateDispatchSubscriptionsIdentity(db, typeHint);\n}\n\n/** Compat-owned unique index over `_smrt_jobs(task_id)`. */\nconst JOBS_TASK_ID_COMPAT_INDEX = 'idx_smrt_jobs_task_id';\n\n/**\n * Reconcile the two owners of `_smrt_jobs(task_id)` uniqueness (issue #2376).\n *\n * `SmrtJob.taskId` is declared `unique: true`, so a table created from the\n * jobs manifest already carries a constraint-backed unique index. Legacy\n * databases got the column from `addColumnIfMissing()` instead, and\n * `ALTER TABLE ADD COLUMN ... UNIQUE` is rejected by SQLite and DuckDB — so\n * those databases need this compat index and only this compat index.\n *\n * Creating it unconditionally left every manifest-created table with two\n * unique indexes on one column. Create it only when nothing else already\n * enforces the uniqueness, and drop it when something does.\n *\n * Known limitation: this reconciliation is PostgreSQL/SQLite-only. DuckDB and\n * the JSON adapter give {@link listTableIndexes} no usable answer, so those\n * installs keep the redundant index — deliberately, because the alternative is\n * guessing and possibly leaving the upsert conflict target unenforced.\n */\nasync function reconcileJobsTaskIdUniqueness(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n const indexes = await listTableIndexes(db, '_smrt_jobs', typeHint);\n\n if (indexes === null) {\n // Engine catalog not readable here — keep the historical behaviour rather\n // than risk leaving the upsert conflict target unenforced.\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n return;\n }\n\n // A partial unique index does not enforce uniqueness over the whole table,\n // so it can never stand in for the compat index.\n const otherUniqueOnTaskId = indexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n index.name !== JOBS_TASK_ID_COMPAT_INDEX &&\n index.columns.length === 1 &&\n index.columns[0] === 'task_id',\n );\n\n if (otherUniqueOnTaskId) {\n await dropIndexIfExists(db, JOBS_TASK_ID_COMPAT_INDEX, typeHint);\n return;\n }\n\n await addUniqueIndexIfMissing(\n db,\n JOBS_TASK_ID_COMPAT_INDEX,\n '_smrt_jobs',\n 'task_id',\n typeHint,\n );\n}\n\nexport async function ensureJobsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_jobs', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(db, '_smrt_jobs', 'tenant_id', 'TEXT', typeHint);\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_tenant_id',\n '_smrt_jobs',\n 'tenant_id',\n typeHint,\n );\n // MCP task support was added after `_smrt_jobs` had already shipped. Keep\n // the task projection in the job row so a task can never outlive (or lose\n // track of) the worker job that executes it. JSON is portable here because\n // SMRT's adapters serialize JSON fields to text where necessary.\n await addColumnIfMissing(db, '_smrt_jobs', 'task_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_owner_id', 'TEXT', typeHint);\n await addColumnIfMissing(db, '_smrt_jobs', 'task_result', 'TEXT', typeHint);\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_requests',\n 'TEXT',\n typeHint,\n );\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'task_input_responses',\n 'TEXT',\n typeHint,\n );\n await reconcileJobsTaskIdUniqueness(db, typeHint);\n // Retention predicate of SmrtJobCollection.cleanup() (#2375): terminal jobs\n // aged out by `(status, completed_at)`. `_smrt_jobs` comes from a decorated\n // class rather than the hand-written system DDL, so this compatibility path\n // — which the jobs collection runs on every initialize — is where the index\n // reaches both existing and freshly migrated databases. `completed_at` is\n // guarded the same as every other column this pass touches: a table created\n // before the field existed (or a minimal test fixture) must not fail the\n // whole sweep with \"no such column\".\n await addColumnIfMissing(\n db,\n '_smrt_jobs',\n 'completed_at',\n 'TIMESTAMP',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_jobs_status_completed_at',\n '_smrt_jobs',\n 'status, completed_at',\n typeHint,\n );\n}\n\nexport async function ensureJobEventsSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (!(await tableExists(db, '_smrt_job_events', typeHint))) {\n return;\n }\n\n await addColumnIfMissing(\n db,\n '_smrt_job_events',\n 'tenant_id',\n 'TEXT',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_tenant_id',\n '_smrt_job_events',\n 'tenant_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_job_id',\n '_smrt_job_events',\n 'job_id',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_type',\n '_smrt_job_events',\n 'type',\n typeHint,\n );\n await addIndexIfMissing(\n db,\n 'idx_smrt_job_events_created_at',\n '_smrt_job_events',\n 'created_at',\n typeHint,\n );\n}\n\n/**\n * Compatibility pass for the tables `db:migrate` creates from the jobs\n * manifest rather than `bootstrapSystemTables()` (issue #2376).\n *\n * These are dual-owned, and on a *fresh* install the manifest wins the race:\n * bootstrap runs first, finds no `_smrt_jobs`, returns early, stamps the\n * system schema version — and every later boot takes the version fast path, so\n * the columns and indexes this pass owns never appeared on that database at\n * all. Splitting the pass out lets the caller re-check it after the tables\n * show up, independently of the system schema version.\n *\n * @returns `settled: true` once every deferred table exists and has been\n * upgraded — the point at which a caller may stop re-checking. `false` means\n * the tables are not there yet, not that anything failed.\n */\nexport async function ensureDeferredSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<{ settled: boolean }> {\n // Sequential, never `Promise.all`: a caller can hand us one shared\n // single-statement connection (the OIDC provisioning coordinator does), and\n // two probes in flight at once overlap on it.\n const jobsExists = await tableExists(db, '_smrt_jobs', typeHint);\n const jobEventsExists = await tableExists(db, '_smrt_job_events', typeHint);\n\n if (jobsExists) {\n await ensureJobsSystemTableCompatibility(db, typeHint);\n }\n if (jobEventsExists) {\n await ensureJobEventsSystemTableCompatibility(db, typeHint);\n }\n\n return { settled: jobsExists && jobEventsExists };\n}\n\n/**\n * The compatibility pass that must precede replaying system DDL.\n *\n * Only the tables `bootstrapSystemTables()` itself creates belong here: an\n * older install can be missing a column that the DDL's `CREATE INDEX` clauses\n * reference, so those tables have to be reshaped first, inside the same\n * transaction as the replay.\n *\n * The deferred tables are deliberately NOT part of this pass. They are owned by\n * the jobs manifest, not by the system DDL, so nothing in the replay depends on\n * them — and reshaping a table the framework does not own must not be able to\n * abort the bootstrap transaction and roll back system-table creation with it\n * (issue #2376). {@link ensureDeferredSystemTableCompatibility} runs outside\n * the lock instead.\n */\nexport async function ensureBootstrapSystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureDispatchSystemTableCompatibility(db, typeHint);\n await ensureDispatchSubscriptionsSystemTableCompatibility(db, typeHint);\n}\n\n/**\n * Run every compatibility pass — the bootstrap tables and the deferred,\n * manifest-created ones.\n *\n * This is the convenience entry point for callers that own the whole database\n * lifecycle in one step (test-database setup). Framework bootstrap deliberately\n * does not use it: see {@link ensureBootstrapSystemTableCompatibility}.\n */\nexport async function ensureLegacySystemTableCompatibility(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n await ensureDeferredSystemTableCompatibility(db, typeHint);\n}\n"],"mappings":"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"}
@@ -5,5 +5,6 @@
5
5
  * All tables use _smrt_ prefix and share the application's database.
6
6
  */
7
7
  export * from './compatibility.js';
8
+ export * from './retention.js';
8
9
  export * from './types.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/system/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/system/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -1,2 +1,3 @@
1
- import { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./compatibility.js";
2
- export { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists };
1
+ import { assertPostgresSystemTimestampsCurrent, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./compatibility.js";
2
+ import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./retention.js";
3
+ export { DEFAULT_RETENTION_POLICY, assertPostgresSystemTimestampsCurrent, clearRetentionTasks, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, getRetentionTasks, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, tableExists, unregisterRetentionTask };
@@ -0,0 +1,237 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ /** Retention for the append-only change feed (`_smrt_changes`). */
3
+ export interface ChangeFeedRetentionPolicy {
4
+ /** Run this task (default `true`). */
5
+ enabled?: boolean;
6
+ /** Drop entries older than this many days (default 30). */
7
+ maxAgeDays?: number;
8
+ /** Additionally keep at most this many newest entries (default: unbounded). */
9
+ maxRows?: number;
10
+ }
11
+ /** Retention for AI usage telemetry (`_smrt_ai_usage`). */
12
+ export interface AiUsageRetentionPolicy {
13
+ /** Run this task (default `true`). */
14
+ enabled?: boolean;
15
+ /** Drop records older than this many days (default 90). */
16
+ maxAgeDays?: number;
17
+ /** Additionally keep at most this many newest records (default: unbounded). */
18
+ maxRows?: number;
19
+ }
20
+ /** Retention for remembered context entries (`_smrt_contexts`). */
21
+ export interface ContextsRetentionPolicy {
22
+ /** Run this task (default `true`). */
23
+ enabled?: boolean;
24
+ }
25
+ /** Retention for the dispatch queue (`_smrt_dispatch`). */
26
+ export interface DispatchRetentionPolicy {
27
+ /** Run this task (default `true`). */
28
+ enabled?: boolean;
29
+ /** Drop completed dispatches processed more than this many days ago (default 30). */
30
+ completedOlderThanDays?: number;
31
+ /** Drop failed dispatches last touched more than this many days ago (default 90). */
32
+ failedOlderThanDays?: number;
33
+ }
34
+ /**
35
+ * Retention policy for a sweep.
36
+ *
37
+ * Every table is pruned by default; set a table's entry to `false` (or
38
+ * `{ enabled: false }`) to opt that table out, or `enabled: false` at the top
39
+ * level to opt the whole sweep out.
40
+ */
41
+ export interface RetentionPolicy {
42
+ /** Run the sweep at all (default `true`). */
43
+ enabled?: boolean;
44
+ /** Report what would be pruned without deleting anything (default `false`). */
45
+ dryRun?: boolean;
46
+ /** `_smrt_changes` retention, or `false` to skip. */
47
+ changes?: ChangeFeedRetentionPolicy | false;
48
+ /** `_smrt_ai_usage` retention, or `false` to skip. */
49
+ aiUsage?: AiUsageRetentionPolicy | false;
50
+ /** `_smrt_contexts` expiry enforcement, or `false` to skip. */
51
+ contexts?: ContextsRetentionPolicy | false;
52
+ /** `_smrt_dispatch` retention, or `false` to skip. */
53
+ dispatch?: DispatchRetentionPolicy | false;
54
+ /**
55
+ * Enable/disable individual tasks contributed through
56
+ * {@link registerRetentionTask}, keyed by task name. Unlisted tasks run.
57
+ */
58
+ tasks?: Record<string, boolean>;
59
+ }
60
+ /**
61
+ * Documented retention defaults.
62
+ *
63
+ * Chosen to be generous relative to every in-tree consumer: change-feed
64
+ * cursors resync automatically past the window, AI usage outlives a monthly
65
+ * billing cycle by two months, and dispatch keeps a full quarter of failures
66
+ * for forensics. Applications that need longer history raise these; those that
67
+ * need none set the table to `false`.
68
+ */
69
+ export declare const DEFAULT_RETENTION_POLICY: {
70
+ changes: ChangeFeedRetentionPolicy;
71
+ aiUsage: AiUsageRetentionPolicy;
72
+ contexts: ContextsRetentionPolicy;
73
+ dispatch: DispatchRetentionPolicy;
74
+ };
75
+ /** Per-run inputs handed to every retention task. */
76
+ export interface RetentionTaskContext {
77
+ /** Count what would be deleted instead of deleting it. */
78
+ dryRun: boolean;
79
+ /** Sweep start time — every task derives its cutoffs from this one clock. */
80
+ now: Date;
81
+ }
82
+ /** What one task did (or why it did nothing). */
83
+ export interface RetentionTaskResult {
84
+ /** Task name, unique within a sweep. */
85
+ task: string;
86
+ /** Rows deleted, or — under `dryRun` — rows that would be deleted. */
87
+ pruned: number;
88
+ /**
89
+ * Why the task did no work: `disabled` (policy opted it out) or
90
+ * `unavailable` (the table does not exist in this database).
91
+ */
92
+ skipped?: 'disabled' | 'unavailable';
93
+ /** Message of the error this task failed with, if any. */
94
+ error?: string;
95
+ /** Optional per-bucket breakdown (e.g. completed vs failed dispatches). */
96
+ details?: Record<string, number>;
97
+ }
98
+ /** What a task returns; a bare number is shorthand for `{ pruned }`. */
99
+ export type RetentionTaskOutcome = number | {
100
+ pruned: number;
101
+ details?: Record<string, number>;
102
+ };
103
+ /**
104
+ * A retention task contributed by a package that owns its own tables.
105
+ *
106
+ * Tasks must honour `context.dryRun` — a task that deletes during a dry run
107
+ * makes the whole preview a lie.
108
+ */
109
+ export interface RetentionTask {
110
+ /** Unique name; also the {@link RetentionPolicy.tasks} opt-out key. */
111
+ name: string;
112
+ /** One-line description surfaced by `smrt db:prune --json`. */
113
+ description?: string;
114
+ /** Perform (or, under `dryRun`, count) the deletion. */
115
+ run(db: DatabaseInterface, context: RetentionTaskContext): Promise<RetentionTaskOutcome>;
116
+ }
117
+ /**
118
+ * Register a retention task so {@link runRetentionSweep} includes it.
119
+ *
120
+ * Registering a task does not schedule anything — a task only runs when
121
+ * something calls {@link runRetentionSweep}, and the policy can still turn it
122
+ * off by name. Re-registering the same name replaces the previous task, which
123
+ * keeps module re-evaluation (HMR, repeated test imports) idempotent.
124
+ *
125
+ * @throws If `task.name` is empty, or is one of the four built-in table
126
+ * names (`changes`, `ai-usage`, `contexts`, `dispatch`) — see
127
+ * {@link RESERVED_TASK_NAMES}.
128
+ */
129
+ export declare function registerRetentionTask(task: RetentionTask): void;
130
+ /** Remove a registered retention task. Returns `true` if one was removed. */
131
+ export declare function unregisterRetentionTask(name: string): boolean;
132
+ /** Registered retention tasks, ordered by name for deterministic sweeps. */
133
+ export declare function getRetentionTasks(): RetentionTask[];
134
+ /** Drop every registered retention task (test helper). */
135
+ export declare function clearRetentionTasks(): void;
136
+ /** Bounds for {@link pruneAiUsage}. At least one bound is required. */
137
+ export interface AiUsageRetention {
138
+ /** Delete records older than this many milliseconds. */
139
+ maxAgeMs?: number;
140
+ /**
141
+ * Keep at most this many newest records (by `created_at`).
142
+ *
143
+ * A ceiling, not a floor: combined with `maxAgeMs`, the two bounds union
144
+ * — a row past the age cutoff is deleted regardless of `maxRows`, so
145
+ * `maxRows` never resurrects rows the age bound already excludes. If every
146
+ * row is already older than `maxAgeMs`, a finite `maxRows` keeps none of
147
+ * them; it does not guarantee `maxRows` survivors independent of age. (A
148
+ * `maxRows` that overrode age this way would defeat the reason `maxAgeMs`
149
+ * usually exists in the first place — a compliance/privacy deletion
150
+ * deadline that a row-count floor should not be able to postpone.)
151
+ *
152
+ * Rows sharing the cutoff timestamp all survive, so the retained count can
153
+ * exceed `maxRows` by the size of one timestamp tie.
154
+ */
155
+ maxRows?: number;
156
+ /**
157
+ * Restrict the prune to one tenant: a string matches that tenant, `null`
158
+ * matches the global (tenant-less) records. Omit to prune every tenant.
159
+ */
160
+ tenantId?: string | null;
161
+ /** Count matching records without deleting them. */
162
+ dryRun?: boolean;
163
+ /** Clock for the age cutoff (default: now). */
164
+ now?: Date;
165
+ }
166
+ /**
167
+ * Prune AI usage telemetry to bound `_smrt_ai_usage` growth.
168
+ *
169
+ * `_smrt_ai_usage` gains one row per AI call and persistence is on by default,
170
+ * so a busy deployment writes millions of rows a year with nothing to remove
171
+ * them. Both bounds may be combined; each is applied independently and the
172
+ * returned count is the total deleted.
173
+ *
174
+ * Predicates are covered by `idx_smrt_ai_usage_tenant_created`
175
+ * (`tenant_id, created_at`) — the same index the subscriptions billing meter
176
+ * needs for its `tenant_id + created_at` range scan.
177
+ *
178
+ * @param db - Database holding the system tables.
179
+ * @param retention - At least one of `maxAgeMs` / `maxRows`.
180
+ * @returns Number of records deleted (or, under `dryRun`, matched).
181
+ */
182
+ export declare function pruneAiUsage(db: DatabaseInterface, retention: AiUsageRetention): Promise<{
183
+ pruned: number;
184
+ }>;
185
+ /**
186
+ * Delete context entries whose `expires_at` has passed.
187
+ *
188
+ * `remember({ expiresAt })` has always stored the expiry and never acted on
189
+ * it. Enforcement is deliberately prune-side only: `recall()`/`recallAll()`
190
+ * keep their documented "expiry is not applied at read time" contract, so this
191
+ * bounds storage without changing read semantics for existing callers.
192
+ * (`LearningMemory` filters expired rows itself.)
193
+ *
194
+ * Rows with a `NULL` `expires_at` never expire and are never touched.
195
+ * The predicate is covered by `idx_smrt_contexts_expires_at`.
196
+ *
197
+ * @param db - Database holding the system tables.
198
+ * @param options.now - Clock for the expiry comparison (default: now).
199
+ * @param options.dryRun - Count matching rows without deleting them.
200
+ * @returns Number of expired entries deleted (or, under `dryRun`, matched).
201
+ */
202
+ export declare function pruneExpiredContexts(db: DatabaseInterface, options?: {
203
+ now?: Date;
204
+ dryRun?: boolean;
205
+ }): Promise<{
206
+ pruned: number;
207
+ }>;
208
+ /** Aggregate outcome of one {@link runRetentionSweep} call. */
209
+ export interface RetentionSweepResult {
210
+ /** Whether this run only counted rows. */
211
+ dryRun: boolean;
212
+ /** Sweep start time, ISO-8601. */
213
+ startedAt: string;
214
+ /** Wall-clock duration of the sweep. */
215
+ durationMs: number;
216
+ /** Total rows pruned (or matched) across all tasks. */
217
+ pruned: number;
218
+ /** Per-task outcomes, in execution order. */
219
+ tasks: RetentionTaskResult[];
220
+ /** Whether any task failed — the sweep never throws for a task failure. */
221
+ failed: boolean;
222
+ }
223
+ /**
224
+ * Apply a {@link RetentionPolicy} across every framework-owned table.
225
+ *
226
+ * Built-in tasks run first, in a fixed order (`changes`, `ai-usage`,
227
+ * `contexts`, `dispatch`), followed by tasks registered via
228
+ * {@link registerRetentionTask} in name order. Tables that do not exist in
229
+ * this database are reported as `skipped: 'unavailable'` rather than failing —
230
+ * a sweep must be safe to run against a partially bootstrapped database.
231
+ *
232
+ * @param db - Database holding the system tables.
233
+ * @param policy - Overrides on {@link DEFAULT_RETENTION_POLICY}.
234
+ * @returns Per-task and aggregate counts; never throws for a task failure.
235
+ */
236
+ export declare function runRetentionSweep(db: DatabaseInterface, policy?: RetentionPolicy): Promise<RetentionSweepResult>;
237
+ //# sourceMappingURL=retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/system/retention.ts"],"names":[],"mappings":"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"}