@happyvertical/smrt-core 0.57.46 → 0.57.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/AGENTS.md +11 -3
  2. package/agents/browser-entry.md +16 -0
  3. package/agents/build-knowledge.md +58 -14
  4. package/agents/change-feed.md +20 -0
  5. package/agents/generators.md +69 -3
  6. package/agents/schema-paths.md +82 -0
  7. package/dist/_virtual/_rolldown/runtime.js +22 -1
  8. package/dist/browser.d.ts +2 -2
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +2 -2
  11. package/dist/cascade.d.ts +46 -0
  12. package/dist/cascade.d.ts.map +1 -1
  13. package/dist/cascade.js +96 -7
  14. package/dist/cascade.js.map +1 -1
  15. package/dist/change-feed.d.ts.map +1 -1
  16. package/dist/change-feed.js +7 -4
  17. package/dist/change-feed.js.map +1 -1
  18. package/dist/class.d.ts.map +1 -1
  19. package/dist/class.js +3 -3
  20. package/dist/class.js.map +1 -1
  21. package/dist/consumer-plugin/index.d.ts +13 -0
  22. package/dist/consumer-plugin/index.d.ts.map +1 -1
  23. package/dist/consumer-plugin/index.js +69 -13
  24. package/dist/consumer-plugin/index.js.map +1 -1
  25. package/dist/consumer-plugin.js +2 -2
  26. package/dist/database.d.ts.map +1 -1
  27. package/dist/database.js +3 -3
  28. package/dist/database.js.map +1 -1
  29. package/dist/dispatch/bus.d.ts.map +1 -1
  30. package/dist/dispatch/bus.js +1 -4
  31. package/dist/dispatch/bus.js.map +1 -1
  32. package/dist/dispatch/index.d.ts +1 -1
  33. package/dist/dispatch/index.d.ts.map +1 -1
  34. package/dist/dispatch/types.d.ts +22 -3
  35. package/dist/dispatch/types.d.ts.map +1 -1
  36. package/dist/generators/changes-route.d.ts.map +1 -1
  37. package/dist/generators/changes-route.js +2 -2
  38. package/dist/generators/changes-route.js.map +1 -1
  39. package/dist/generators/custom-action.d.ts +11 -0
  40. package/dist/generators/custom-action.d.ts.map +1 -1
  41. package/dist/generators/custom-action.js +23 -4
  42. package/dist/generators/custom-action.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -2
  46. package/dist/lazy-config.d.ts +120 -3
  47. package/dist/lazy-config.d.ts.map +1 -1
  48. package/dist/lazy-config.js +81 -10
  49. package/dist/lazy-config.js.map +1 -1
  50. package/dist/manifest/generator.d.ts +8 -2
  51. package/dist/manifest/generator.d.ts.map +1 -1
  52. package/dist/manifest/generator.js +6 -5
  53. package/dist/manifest/generator.js.map +1 -1
  54. package/dist/manifest/index.d.ts +1 -0
  55. package/dist/manifest/index.d.ts.map +1 -1
  56. package/dist/manifest/index.js +2 -1
  57. package/dist/manifest/index.js.map +1 -1
  58. package/dist/manifest/local-manifest-reader.d.ts +13 -2
  59. package/dist/manifest/local-manifest-reader.d.ts.map +1 -1
  60. package/dist/manifest/local-manifest-reader.js +38 -6
  61. package/dist/manifest/local-manifest-reader.js.map +1 -1
  62. package/dist/manifest/manifest-loader.d.ts +9 -8
  63. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  64. package/dist/manifest/manifest-loader.js +67 -27
  65. package/dist/manifest/manifest-loader.js.map +1 -1
  66. package/dist/manifest/object-declarations.d.ts +116 -0
  67. package/dist/manifest/object-declarations.d.ts.map +1 -0
  68. package/dist/manifest/object-declarations.js +161 -0
  69. package/dist/manifest/object-declarations.js.map +1 -0
  70. package/dist/manifest/static-manifest.d.ts.map +1 -1
  71. package/dist/manifest/static-manifest.js +5 -3
  72. package/dist/manifest/static-manifest.js.map +1 -1
  73. package/dist/manifest/store.d.ts.map +1 -1
  74. package/dist/manifest/store.js +18 -2
  75. package/dist/manifest/store.js.map +1 -1
  76. package/dist/manifest/test-artifacts.d.ts +57 -0
  77. package/dist/manifest/test-artifacts.d.ts.map +1 -0
  78. package/dist/manifest/test-artifacts.js +111 -0
  79. package/dist/manifest/test-artifacts.js.map +1 -0
  80. package/dist/manifest.js +2 -1
  81. package/dist/manifest.json +5 -3
  82. package/dist/migrations/differ.d.ts.map +1 -1
  83. package/dist/migrations/differ.js +11 -3
  84. package/dist/migrations/differ.js.map +1 -1
  85. package/dist/migrations/index.d.ts +2 -0
  86. package/dist/migrations/index.d.ts.map +1 -1
  87. package/dist/migrations/index.js +3 -1
  88. package/dist/migrations/qualified-name-aliases.d.ts.map +1 -1
  89. package/dist/migrations/qualified-name-aliases.js +1 -1
  90. package/dist/migrations/qualified-name-aliases.js.map +1 -1
  91. package/dist/migrations.js +3 -1
  92. package/dist/object.d.ts.map +1 -1
  93. package/dist/object.js +2 -2
  94. package/dist/object.js.map +1 -1
  95. package/dist/postgres-permissions.d.ts.map +1 -1
  96. package/dist/postgres-permissions.js +4 -3
  97. package/dist/postgres-permissions.js.map +1 -1
  98. package/dist/postgres-timeouts.d.ts +30 -3
  99. package/dist/postgres-timeouts.d.ts.map +1 -1
  100. package/dist/postgres-timeouts.js +39 -2
  101. package/dist/postgres-timeouts.js.map +1 -1
  102. package/dist/prebuild/index.d.ts.map +1 -1
  103. package/dist/prebuild/index.js +14 -11
  104. package/dist/prebuild/index.js.map +1 -1
  105. package/dist/registry/class-registration.d.ts.map +1 -1
  106. package/dist/registry/class-registration.js +24 -2
  107. package/dist/registry/class-registration.js.map +1 -1
  108. package/dist/registry/collection-exposure.d.ts +27 -0
  109. package/dist/registry/collection-exposure.d.ts.map +1 -1
  110. package/dist/registry/collection-exposure.js +74 -1
  111. package/dist/registry/collection-exposure.js.map +1 -1
  112. package/dist/registry/collection-resolution.d.ts.map +1 -1
  113. package/dist/registry/collection-resolution.js +1 -0
  114. package/dist/registry/collection-resolution.js.map +1 -1
  115. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  116. package/dist/registry/framework-base-classes.js +5 -0
  117. package/dist/registry/framework-base-classes.js.map +1 -1
  118. package/dist/registry/schema-builder.d.ts +8 -1
  119. package/dist/registry/schema-builder.d.ts.map +1 -1
  120. package/dist/registry/schema-builder.js +17 -4
  121. package/dist/registry/schema-builder.js.map +1 -1
  122. package/dist/registry/types.d.ts +12 -0
  123. package/dist/registry/types.d.ts.map +1 -1
  124. package/dist/registry.d.ts +2 -1
  125. package/dist/registry.d.ts.map +1 -1
  126. package/dist/registry.js +30 -10
  127. package/dist/registry.js.map +1 -1
  128. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  129. package/dist/scanner/manifest-generator.js +10 -8
  130. package/dist/scanner/manifest-generator.js.map +1 -1
  131. package/dist/scanner/types.d.ts +29 -0
  132. package/dist/scanner/types.d.ts.map +1 -1
  133. package/dist/scanner/types.js.map +1 -1
  134. package/dist/schema/generator.d.ts +32 -0
  135. package/dist/schema/generator.d.ts.map +1 -1
  136. package/dist/schema/generator.js +59 -0
  137. package/dist/schema/generator.js.map +1 -1
  138. package/dist/schema/live-parity.d.ts.map +1 -1
  139. package/dist/schema/live-parity.js +4 -2
  140. package/dist/schema/live-parity.js.map +1 -1
  141. package/dist/scripts/browser-unbundled-dev.mjs +90 -0
  142. package/dist/scripts/generate-test-manifest.js +9 -49
  143. package/dist/smrt-knowledge.json +16 -16
  144. package/dist/system/compatibility.d.ts.map +1 -1
  145. package/dist/system/compatibility.js +12 -4
  146. package/dist/system/compatibility.js.map +1 -1
  147. package/dist/system/schema.d.ts +70 -8
  148. package/dist/system/schema.d.ts.map +1 -1
  149. package/dist/system/schema.js +231 -24
  150. package/dist/system/schema.js.map +1 -1
  151. package/dist/testing/database.d.ts.map +1 -1
  152. package/dist/testing/database.js +14 -2
  153. package/dist/testing/database.js.map +1 -1
  154. package/dist/tools/tool-generator.d.ts.map +1 -1
  155. package/dist/tools/tool-generator.js +2 -2
  156. package/dist/tools/tool-generator.js.map +1 -1
  157. package/dist/utils/import-workspace-module.d.ts.map +1 -1
  158. package/dist/utils/import-workspace-module.js +23 -0
  159. package/dist/utils/import-workspace-module.js.map +1 -1
  160. package/dist/utils/naming.js +5 -3
  161. package/dist/utils/naming.js.map +1 -1
  162. package/dist/vendor/pluralize/pluralize.js +421 -0
  163. package/dist/vendor/pluralize/pluralize.js.map +1 -0
  164. package/dist/vite-plugin/api-client-entries.d.ts +5 -1
  165. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  166. package/dist/vite-plugin/api-client-entries.js +11 -6
  167. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  168. package/dist/vite-plugin/consumer-import.d.ts +28 -0
  169. package/dist/vite-plugin/consumer-import.d.ts.map +1 -0
  170. package/dist/vite-plugin/consumer-import.js +42 -0
  171. package/dist/vite-plugin/consumer-import.js.map +1 -0
  172. package/dist/vite-plugin/dev-plane-route.d.ts +6 -1
  173. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -1
  174. package/dist/vite-plugin/dev-plane-route.js +8 -9
  175. package/dist/vite-plugin/dev-plane-route.js.map +1 -1
  176. package/dist/vite-plugin/index.d.ts.map +1 -1
  177. package/dist/vite-plugin/index.js +12 -9
  178. package/dist/vite-plugin/index.js.map +1 -1
  179. package/dist/vite-plugin/resources-route.d.ts.map +1 -1
  180. package/dist/vite-plugin/resources-route.js +3 -8
  181. package/dist/vite-plugin/resources-route.js.map +1 -1
  182. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  183. package/dist/vite-plugin/sveltekit-generator.js +7 -11
  184. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  185. package/dist/vite-plugin/sync-apply-route.d.ts +16 -0
  186. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  187. package/dist/vite-plugin/sync-apply-route.js +48 -6
  188. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  189. package/package.json +12 -12
package/dist/cascade.d.ts CHANGED
@@ -180,4 +180,50 @@ export declare function runCascadeDelete(db: DatabaseInterface, registry: Cascad
180
180
  /** An unsaved object has none; the cascade is then a no-op. */
181
181
  id: string | null | undefined;
182
182
  }, deleteSelf: (db: DatabaseInterface) => Promise<void>): Promise<CascadeResult>;
183
+ /**
184
+ * Message of the statement that aborts a caller-owned native DuckDB
185
+ * transaction after a cascade failed part-way through it (#2824).
186
+ */
187
+ export declare const CASCADE_ABORTED_CALLER_TRANSACTION = "smrt: delete cascade failed inside a caller-owned DuckDB transaction; the transaction is aborted";
188
+ /**
189
+ * Own the cascade's atomic scope (#2824).
190
+ *
191
+ * The cascade and the target's own `DELETE` run in a transaction opened on
192
+ * `db`. When `db` is already a transaction handle on an engine without
193
+ * `SAVEPOINT`, the SDK refuses the nested `transaction()` before running the
194
+ * callback or touching the enclosing transaction (`NestedTransactionError`).
195
+ * The cascade then joins that enclosing transaction directly instead of
196
+ * failing, which is the only atomic scope there is:
197
+ *
198
+ * - framework-owned handles (`withTransaction()`, the bulk junction path) —
199
+ * unchanged since #2818;
200
+ * - caller-owned native DuckDB transactions (`db.transaction(tx => …)` or
201
+ * `beginTransaction()` handed to `withDatabase()`). Commit and rollback stay
202
+ * the caller's: a rollback discards the cascade with everything else.
203
+ *
204
+ * Joining without a savepoint means a cascade that fails after issuing some
205
+ * of its writes cannot be undone on its own. On native DuckDB the framework
206
+ * therefore aborts the joined transaction before rethrowing when the cascade
207
+ * already issued a write (a refusal before the first write, such as the
208
+ * depth-0 RESTRICT / NO ACTION preflight, leaves it untouched). DuckDB's
209
+ * `delete()` reports a constant affected count and owned-row cleanup always
210
+ * runs, so in practice any failure after that preflight aborts — DuckDB then
211
+ * refuses every later statement in it and its `COMMIT` persists nothing — so a
212
+ * caller that swallows the error can never commit a half-applied cascade. The
213
+ * callback runs at most once: the nested refusal happens before it starts, and
214
+ * nothing is replayed after it has.
215
+ *
216
+ * Other no-savepoint adapters (the JSON adapter) keep the detectable
217
+ * `NestedTransactionError` refusal for caller-owned transactions by default:
218
+ * they offer no statement that poisons the enclosing transaction, so joining
219
+ * could commit a partial cascade. The compatible bulk junction path (#2818)
220
+ * passes `reuseUnsupportedNested: true` to keep its established join-any
221
+ * behavior, and still gets the DuckDB abort-on-failure guard.
222
+ *
223
+ * @param db - Database the cascade is bound to (a root or a transaction handle)
224
+ * @param run - Cascade plus the target's own delete, on the transaction-bound db
225
+ * @param reuseUnsupportedNested - Join any enclosing transaction that refuses
226
+ * nesting; defaults to framework-owned handles and native DuckDB
227
+ */
228
+ export declare function runInOwningCascadeTransaction<T>(db: DatabaseInterface, run: (bound: DatabaseInterface) => Promise<T>, reuseUnsupportedNested?: boolean): Promise<T>;
183
229
  //# sourceMappingURL=cascade.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAY5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAKpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAM1D;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAsB9C,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,MAAM,EAAE,cAAc,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,mEAAmE;AACnE,MAAM,WAAW,2BAA2B;IAC1C,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,mEAAmE;IACnE,WAAW,EAAE,2BAA2B,EAAE,CAAC;IAC3C,yEAAyE;IACzE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+EAA+E;IAC/E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,OAAO,cAAc,EACnB,oBAAoB,GACpB,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,uBAAuB,GACvB,UAAU,GACV,YAAY,GACZ,gBAAgB,CACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAE5E;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,EAAE,MAAM,GAChB,WAAW,CAkJb;AAyZD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B;;;;OAIG;IACH,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9D,OAAO,CAAC,aAAa,CAAC,CAwBxB;AASD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE;IACN,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC/B,EACD,UAAU,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GACnD,OAAO,CAAC,aAAa,CAAC,CAyDxB"}
1
+ {"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"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"}
package/dist/cascade.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { classifyDatabaseError } from "./db-errors.js";
2
2
  import { buildWhere } from "./host.js";
3
+ import { isKnownNativeDuckDbHandle } from "./native-duckdb.js";
3
4
  import { isEmbeddedDatabase, isFrameworkTransactionHandle, isPostgresDatabase, withEmbeddedWriteTransaction } from "./embedded-write-queue.js";
4
5
  import { ConfigurationError, DatabaseError } from "./errors.js";
5
6
  import { normalizeForeignKeyAction, resolveForeignKeyDeleteAction } from "./schema/foreign-key-policy.js";
@@ -221,6 +222,23 @@ function idPredicate(column, ids) {
221
222
  return ids.length === 1 ? { [column]: ids[0] } : { [`${column} in`]: ids };
222
223
  }
223
224
  /**
225
+ * Count of cascade write statements issued per transaction-bound handle
226
+ * (#2824). A joined no-savepoint transaction is aborted on failure only when
227
+ * the cascade actually changed something in it; a refusal raised before any
228
+ * write — the RESTRICT / NO ACTION preflight — leaves the caller's transaction
229
+ * usable, exactly as a refusal on a savepoint-capable engine would.
230
+ */
231
+ var cascadeWrites = /* @__PURE__ */ new WeakMap();
232
+ /**
233
+ * Record a cascade write on `db`. A result that reports `affected: 0` changed
234
+ * nothing; anything else (including adapters that report no count) counts.
235
+ */
236
+ function recordCascadeWrite(db, result) {
237
+ const affected = result?.affected;
238
+ if (typeof affected !== "number" || affected > 0) cascadeWrites.set(db, (cascadeWrites.get(db) ?? 0) + 1);
239
+ return result;
240
+ }
241
+ /**
224
242
  * Run a cascade statement against a *referencing* table, tolerating the
225
243
  * table (or an expected column) not existing in this database.
226
244
  *
@@ -300,7 +318,7 @@ async function selectIds(db, tableName, column, ids) {
300
318
  return found;
301
319
  }
302
320
  async function deleteByIds(db, tableName, ids) {
303
- for (const batch of chunkArray(ids, 900)) await tolerateMissingTable(() => db.delete(tableName, idPredicate("id", batch)), void 0, {
321
+ for (const batch of chunkArray(ids, 900)) await tolerateMissingTable(() => db.delete(tableName, idPredicate("id", batch)).then((result) => recordCascadeWrite(db, result)), void 0, {
304
322
  table: tableName,
305
323
  action: "CASCADE delete"
306
324
  });
@@ -337,7 +355,7 @@ async function deleteSystemRows(db, ids, classNames) {
337
355
  ]]) for (const batch of chunkArray(ownerIds, 900)) await tolerateMissingSystemTable(() => db.delete(table, {
338
356
  ...idPredicate(idColumn, batch),
339
357
  ...idPredicate(classColumn, classNames)
340
- }), void 0, {
358
+ }).then((result) => recordCascadeWrite(db, result)), void 0, {
341
359
  table,
342
360
  action: "framework-owned row cleanup"
343
361
  });
@@ -373,7 +391,7 @@ async function resolveReferences(ctx, className, tableName, ids, depth) {
373
391
  }
374
392
  for (const reference of plan.references) {
375
393
  if (reference.action === "SET NULL") {
376
- for (const batch of chunkArray(pending, 900)) await tolerateMissingTable(() => ctx.db.update(reference.tableName, idPredicate(reference.column, batch), { [reference.column]: null }), void 0, {
394
+ for (const batch of chunkArray(pending, 900)) await tolerateMissingTable(() => ctx.db.update(reference.tableName, idPredicate(reference.column, batch), { [reference.column]: null }).then((result) => recordCascadeWrite(ctx.db, result)), void 0, {
377
395
  table: reference.tableName,
378
396
  action: "SET NULL"
379
397
  });
@@ -396,7 +414,7 @@ async function resolveReferences(ctx, className, tableName, ids, depth) {
396
414
  const where = { ...idPredicate("meta_id", batch) };
397
415
  if (plan.metaTypes.length === 1) where.meta_type = plan.metaTypes[0];
398
416
  else where["meta_type in"] = plan.metaTypes;
399
- if (((await tolerateMissingTable(() => ctx.db.delete(association.tableName, where), void 0, {
417
+ if (((await tolerateMissingTable(() => ctx.db.delete(association.tableName, where).then((result) => recordCascadeWrite(ctx.db, result)), void 0, {
400
418
  table: association.tableName,
401
419
  action: "polymorphic association cleanup"
402
420
  }))?.affected ?? 0) > 0) {
@@ -455,7 +473,7 @@ async function runCascadeDelete(db, registry, target, deleteSelf) {
455
473
  };
456
474
  };
457
475
  if (typeof db.transaction !== "function") return runWithoutReferences(db);
458
- return withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), runWithoutReferences, isFrameworkTransactionHandle(db));
476
+ return runInOwningCascadeTransaction(db, runWithoutReferences);
459
477
  }
460
478
  const run = async (bound) => {
461
479
  const result = await cascadeReferencesTo(bound, registry, {
@@ -467,9 +485,80 @@ async function runCascadeDelete(db, registry, target, deleteSelf) {
467
485
  return result;
468
486
  };
469
487
  if (typeof db.transaction !== "function") return run(db);
470
- return withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), run, isFrameworkTransactionHandle(db));
488
+ return runInOwningCascadeTransaction(db, run);
489
+ }
490
+ /**
491
+ * Message of the statement that aborts a caller-owned native DuckDB
492
+ * transaction after a cascade failed part-way through it (#2824).
493
+ */
494
+ var CASCADE_ABORTED_CALLER_TRANSACTION = "smrt: delete cascade failed inside a caller-owned DuckDB transaction; the transaction is aborted";
495
+ /**
496
+ * Own the cascade's atomic scope (#2824).
497
+ *
498
+ * The cascade and the target's own `DELETE` run in a transaction opened on
499
+ * `db`. When `db` is already a transaction handle on an engine without
500
+ * `SAVEPOINT`, the SDK refuses the nested `transaction()` before running the
501
+ * callback or touching the enclosing transaction (`NestedTransactionError`).
502
+ * The cascade then joins that enclosing transaction directly instead of
503
+ * failing, which is the only atomic scope there is:
504
+ *
505
+ * - framework-owned handles (`withTransaction()`, the bulk junction path) —
506
+ * unchanged since #2818;
507
+ * - caller-owned native DuckDB transactions (`db.transaction(tx => …)` or
508
+ * `beginTransaction()` handed to `withDatabase()`). Commit and rollback stay
509
+ * the caller's: a rollback discards the cascade with everything else.
510
+ *
511
+ * Joining without a savepoint means a cascade that fails after issuing some
512
+ * of its writes cannot be undone on its own. On native DuckDB the framework
513
+ * therefore aborts the joined transaction before rethrowing when the cascade
514
+ * already issued a write (a refusal before the first write, such as the
515
+ * depth-0 RESTRICT / NO ACTION preflight, leaves it untouched). DuckDB's
516
+ * `delete()` reports a constant affected count and owned-row cleanup always
517
+ * runs, so in practice any failure after that preflight aborts — DuckDB then
518
+ * refuses every later statement in it and its `COMMIT` persists nothing — so a
519
+ * caller that swallows the error can never commit a half-applied cascade. The
520
+ * callback runs at most once: the nested refusal happens before it starts, and
521
+ * nothing is replayed after it has.
522
+ *
523
+ * Other no-savepoint adapters (the JSON adapter) keep the detectable
524
+ * `NestedTransactionError` refusal for caller-owned transactions by default:
525
+ * they offer no statement that poisons the enclosing transaction, so joining
526
+ * could commit a partial cascade. The compatible bulk junction path (#2818)
527
+ * passes `reuseUnsupportedNested: true` to keep its established join-any
528
+ * behavior, and still gets the DuckDB abort-on-failure guard.
529
+ *
530
+ * @param db - Database the cascade is bound to (a root or a transaction handle)
531
+ * @param run - Cascade plus the target's own delete, on the transaction-bound db
532
+ * @param reuseUnsupportedNested - Join any enclosing transaction that refuses
533
+ * nesting; defaults to framework-owned handles and native DuckDB
534
+ */
535
+ async function runInOwningCascadeTransaction(db, run, reuseUnsupportedNested) {
536
+ const nativeDuckDb = isKnownNativeDuckDbHandle(db);
537
+ return withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), async (bound) => {
538
+ if (bound !== db || !nativeDuckDb) return run(bound);
539
+ const writesBefore = cascadeWrites.get(bound) ?? 0;
540
+ try {
541
+ return await run(bound);
542
+ } catch (error) {
543
+ if ((cascadeWrites.get(bound) ?? 0) > writesBefore) await abortJoinedDuckDbTransaction(bound);
544
+ throw error;
545
+ }
546
+ }, reuseUnsupportedNested ?? (isFrameworkTransactionHandle(db) || nativeDuckDb));
547
+ }
548
+ /**
549
+ * Put a joined native DuckDB transaction into DuckDB's aborted state.
550
+ *
551
+ * A failing statement is the engine's own mechanism: after it, DuckDB rejects
552
+ * every statement with "Current transaction is aborted" until the transaction
553
+ * ends, and ending it discards its writes. The statement is expected to throw;
554
+ * its error is not the one the caller needs, so it is dropped.
555
+ */
556
+ async function abortJoinedDuckDbTransaction(db) {
557
+ try {
558
+ await db.query(`SELECT error('${CASCADE_ABORTED_CALLER_TRANSACTION.replaceAll("'", "''")}')`);
559
+ } catch {}
471
560
  }
472
561
  //#endregion
473
- export { buildCascadePlan, cascadeReferencesTo, normalizeOnDelete, runCascadeDelete };
562
+ export { CASCADE_ABORTED_CALLER_TRANSACTION, buildCascadePlan, cascadeReferencesTo, normalizeOnDelete, runCascadeDelete, runInOwningCascadeTransaction };
474
563
 
475
564
  //# sourceMappingURL=cascade.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cascade.js","names":[],"sources":["../src/cascade.ts"],"sourcesContent":["/**\n * App-side referential integrity for `SmrtObject.delete()` (#2371).\n *\n * Same-package `@foreignKey` fields now emit database constraints where the\n * engine can enforce the declared policy. Cross-package references and older\n * schemas still rely on this application-side path. Before this module,\n * `delete()` removed the object's own row and nothing else: junction rows,\n * polymorphic association rows, `_smrt_embeddings` and `_smrt_contexts` entries\n * were all left pointing at an id that no longer resolved, and\n * `@foreignKey(..., { onDelete })` was metadata nobody read.\n *\n * This module implements those semantics in the application layer. It mirrors\n * what a DB-level constraint would do, deliberately including the parts that\n * make it *unlike* a model-layer delete:\n *\n * - Cascaded rows are removed with set-based statements. Their `beforeDelete` /\n * `afterDelete` hooks and interceptors do **not** run, and no change-feed\n * tombstone is written for them — exactly as `ON DELETE CASCADE` behaves.\n * Only the object `delete()` was called on runs the full lifecycle.\n * - Every delete runs inside a single transaction when the adapter exposes\n * one, including cleanup of framework-owned context and embedding rows, so\n * a partial cascade cannot survive a failure. A `metaType` column can point\n * at any class at runtime, so a polymorphic association class is always\n * plausibly relevant.\n *\n * ## Which references are followed\n *\n * A reference is any `@foreignKey` / `@crossPackageRef` field on a registered\n * class whose target resolves to the deleted object's class (or one of its\n * registered STI ancestors), plus the polymorphic `(metaType, metaId)` pair on\n * {@link SmrtPolymorphicAssociation} subclasses.\n *\n * The action comes from the field's declared `onDelete`. When it is not\n * declared, the default is derived from the referencing class's natural key:\n *\n * | Reference | Default when `onDelete` is absent |\n * |---|---|\n * | Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |\n * | Polymorphic `(metaType, metaId)` association row | `CASCADE` |\n * | Ordinary same-package reference | `NO ACTION` (deletion is refused while references remain) |\n * | Every `@tenantId()` field | Excluded: tenant scope is not an ownership edge |\n *\n * The natural-key rule is what makes junction rows work without any\n * per-package annotation: a junction declares\n * `@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so\n * `content_id` identifies the row and the row cannot outlive the content it\n * links. An ordinary child (`Order.customerId`) is keyed by `(slug, context)`,\n * so it defaults to immediate `NO ACTION` unless it opts in with\n * `@foreignKey(Customer, { onDelete: 'CASCADE' })`.\n *\n * `@tenantId()` fields are excluded from the natural-key rule even though\n * `@happyvertical/smrt-tenancy` leads a tenant-scoped class's *default*\n * `conflictColumns` with the tenant column (#2360): the tenant column scopes\n * ownership, it does not identify the row the way a junction's foreign key\n * does, and it targets a class (`Tenant`) that is virtually always\n * referenced. Without this exclusion, deleting one `Tenant` row would\n * recursively CASCADE through every tenant-scoped table that has not\n * declared its own `conflictColumns` — the overwhelming majority. The field\n * is detected via the `__tenancy.isTenantIdField` marker `@tenantId()`\n * attaches to its own registration (`FieldMeta.__tenancy`, read structurally\n * so `smrt-core` never depends on `smrt-tenancy`). `@tenantId()` exposes no\n * `onDelete` option today, so this cannot currently be overridden per field.\n *\n * @see https://github.com/happyvertical/smrt/issues/2371\n * @module\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { classifyDatabaseError } from './db-errors.js';\nimport {\n isEmbeddedDatabase,\n isFrameworkTransactionHandle,\n isPostgresDatabase,\n withEmbeddedWriteTransaction,\n} from './embedded-write-queue.js';\nimport { ConfigurationError, DatabaseError } from './errors.js';\nimport { buildWhere } from './host.js';\n// Type-only: erased at runtime, so it cannot re-enter the\n// `registry → object → cascade` import cycle.\nimport type { ObjectRegistry } from './registry.js';\nimport {\n normalizeForeignKeyAction,\n resolveForeignKeyDeleteAction,\n} from './schema/foreign-key-policy.js';\nimport type { ForeignKeyAction } from './schema/types.js';\nimport { chunkArray, IN_LIST_CHUNK_SIZE } from './utils/chunk.js';\nimport { toSnakeCase } from './utils/naming.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Referential action applied to rows pointing at a deleted object.\n *\n * Same vocabulary as SQL's `ON DELETE`, enforced by the framework before the\n * engine. `NO ACTION` is preflighted like an immediate restrictive action so\n * application-side and database-side enforcement agree.\n */\nexport type OnDeleteAction = ForeignKeyAction;\n\n/** Table holding framework-managed per-object memory entries. */\nconst CONTEXTS_TABLE = '_smrt_contexts';\n\n/** Table holding framework-managed per-object embedding vectors. */\nconst EMBEDDINGS_TABLE = '_smrt_embeddings';\n\n/**\n * Reserved `_smrt_contexts.owner_id` used by `SmrtCollection.remember()` for\n * collection-scoped memory. A real object id is a UUID and can never equal it,\n * but the guard keeps a malformed id from wiping every sibling's defaults.\n */\nconst COLLECTION_OWNER_SENTINEL = '__collection__';\n\n/**\n * Maximum number of cascade levels followed from the object `delete()` was\n * called on. A cycle between two classes that both declare `onDelete: 'CASCADE'`\n * is broken by the visited set; this bound catches unbounded *chains*.\n */\nconst MAX_CASCADE_DEPTH = 10;\n\n/** One reference that must be resolved before the target row can be removed. */\nexport interface CascadeReference {\n /** Registered name of the class holding the referencing column. */\n className: string;\n /** Table holding the referencing rows. */\n tableName: string;\n /** Field name (camelCase) on the referencing class. */\n fieldName: string;\n /** Column (snake_case) holding the reference. */\n column: string;\n /** Resolved action. `NO ACTION` is preflighted like an immediate RESTRICT. */\n action: OnDeleteAction;\n /** `true` when the action was declared rather than derived from the key. */\n declared: boolean;\n}\n\n/** A polymorphic association table that can point at any class. */\nexport interface CascadePolymorphicReference {\n /** Registered name of the association class. */\n className: string;\n /** Table holding the association rows. */\n tableName: string;\n}\n\n/** Everything that must happen before rows of one class can be deleted. */\nexport interface CascadePlan {\n /** Typed `@foreignKey` / `@crossPackageRef` references, action-resolved. */\n references: CascadeReference[];\n /** Polymorphic association tables that may point at this class. */\n polymorphic: CascadePolymorphicReference[];\n /** `meta_type` values that identify this class in an association row. */\n metaTypes: string[];\n /** `true` when nothing references this class and nothing has to be visited. */\n isEmpty: boolean;\n}\n\n/**\n * Read-only slice of `ObjectRegistry` this module needs.\n *\n * Taking it as a parameter — rather than importing the registry as a value —\n * keeps `cascade.ts` out of the `registry → object → cascade` import cycle,\n * and lets tests drive the planner from a hand-built registry.\n */\nexport type CascadeRegistryView = Pick<\n typeof ObjectRegistry,\n | 'getRelationshipMap'\n | 'getFields'\n | 'getConflictColumns'\n | 'getTableName'\n | 'getSelfReferableNames'\n | 'getClass'\n | 'getSTIBase'\n | 'getDescendants'\n>;\n\n/**\n * Normalize a declared `onDelete` value.\n *\n * Accepts any casing and both `SET NULL` and `SET_NULL`, matching what the\n * schema generator has always carried in the manifest. Returns `undefined` for\n * an unset or unrecognized value so the caller can fall back to the default.\n */\nexport function normalizeOnDelete(value: unknown): OnDeleteAction | undefined {\n return normalizeForeignKeyAction(value);\n}\n\nfunction isPolymorphicAssociationClass(fields: Map<string, unknown>): boolean {\n // The three columns `SmrtPolymorphicAssociation` contributes. Requiring all\n // of them keeps an unrelated class that happens to carry a `metaType` from\n // being treated as an association table.\n return fields.has('metaType') && fields.has('metaId') && fields.has('role');\n}\n\n/**\n * Build the cascade plan for one class.\n *\n * The plan is derived entirely from registry metadata, so it is valid for any\n * database the class is used against. It is rebuilt per delete rather than\n * cached: registration is incremental (manifests load lazily, tests register\n * classes between cases) and a stale plan would silently skip a table.\n *\n * @param registry - Registry view (`ObjectRegistry` satisfies it)\n * @param className - Class whose incoming references should be resolved\n */\nexport function buildCascadePlan(\n registry: CascadeRegistryView,\n className: string,\n): CascadePlan {\n // Keep canonical self/ancestor identities from the registry. Never resolve\n // their display aliases again: a shared simple name can select another\n // package. Legacy relationship views without canonical metadata still use\n // the simple aliases already included by getSelfReferableNames().\n const targetNames = new Set(registry.getSelfReferableNames(className));\n\n const references: CascadeReference[] = [];\n const polymorphic: CascadePolymorphicReference[] = [];\n const visitedBuckets = new Set<unknown>();\n\n const relationshipMap = registry.getRelationshipMap();\n for (const [sourceClass, relationships] of relationshipMap) {\n const simpleSource = relationships[0]?.sourceClass;\n if (\n simpleSource &&\n sourceClass !== simpleSource &&\n relationshipMap.get(simpleSource) === relationships\n ) {\n continue;\n }\n if (visitedBuckets.has(relationships)) continue;\n visitedBuckets.add(relationships);\n const fields = registry.getFields(sourceClass);\n\n if (isPolymorphicAssociationClass(fields)) {\n const tableName = registry.getTableName(sourceClass);\n if (tableName) {\n polymorphic.push({ className: sourceClass, tableName });\n }\n }\n\n if (relationships.length === 0) continue;\n\n let conflictColumns: Set<string> | undefined;\n\n for (const relationship of relationships) {\n if (\n relationship.type !== 'foreignKey' &&\n relationship.type !== 'crossPackageRef'\n ) {\n continue;\n }\n const target =\n relationship.targetQualifiedClass === undefined\n ? relationship.targetClass\n : relationship.targetQualifiedClass;\n if (target === null || !targetNames.has(target)) continue;\n\n // An explicit app-side-only relationship is archival metadata, not a\n // referential-integrity rule. Its identifier is deliberately allowed to\n // outlive the parent, so delete planning must not block, null, or remove\n // the retained row (#2413).\n if (\n relationship.type === 'foreignKey' &&\n relationship.options?.constraint === false\n ) {\n continue;\n }\n\n const tableName = registry.getTableName(sourceClass);\n if (!tableName) continue;\n\n const column = toSnakeCase(relationship.fieldName);\n if (!conflictColumns) {\n conflictColumns = new Set(registry.getConflictColumns(sourceClass));\n }\n\n // A `@tenantId()` field is a structural scoping marker, not a\n // junction/ownership key — it lands in `conflictColumns` only because\n // #2360 leads every tenant-scoped class's *default* natural key with\n // the tenant column, not because the referencing row is *identified*\n // by its tenant the way a junction row is identified by its parent.\n // Without this guard, deleting a `Tenant` would silently CASCADE\n // through the tenant column of every tenant-scoped class in the\n // schema that has not declared its own `conflictColumns` — the\n // overwhelming majority. `@tenantId()` exposes no `onDelete` today, so\n // an explicit declaration can never widen this back to CASCADE; that\n // is deliberate until tenant-delete cascade is an explicit decision.\n const isTenantIdField =\n relationship.options?.__tenancy?.isTenantIdField === true;\n if (isTenantIdField) continue;\n\n const { action, declared } = resolveForeignKeyDeleteAction({\n declared: relationship.options?.onDelete,\n isConflictColumn: conflictColumns.has(column),\n isTenantIdField: false,\n columnName: column,\n });\n\n if (\n action === 'SET NULL' &&\n fields.get(relationship.fieldName)?.required\n ) {\n // Fail on the declaration, not later on a NOT NULL violation from the\n // engine — the message there names a column, not the decorator.\n throw ConfigurationError.invalidConfiguration(\n `${sourceClass}.${relationship.fieldName} onDelete: 'SET NULL'`,\n relationship.fieldName,\n \"a nullable field (mark it `nullable: true`, or use 'CASCADE' / 'RESTRICT')\",\n );\n }\n\n references.push({\n className: sourceClass,\n tableName,\n fieldName: relationship.fieldName,\n column,\n action,\n declared,\n });\n }\n }\n\n // Both forms are matched deliberately: an association row written before a\n // class was package-qualified still carries the simple `meta_type`. The\n // qualified form alone is unambiguous; the simple-name fallback can still\n // match a same-simple-name sibling class's row if it also shares a\n // `meta_id` — a real, not just theoretical, risk for a class declaring\n // `idType: 'text'` (non-UUID, not guaranteed globally unique), the same gap\n // `deleteSystemRows()` closed for `_smrt_contexts` / `_smrt_embeddings`\n // (review fix) by adding a class-name filter. Closing it here needs the\n // same STI-and-legacy-name-aware filter this module already builds for\n // `deleteSystemRows()` narrowed further to registry-unambiguous simple\n // names; tracked under #2419 alongside the other same-simple-name\n // collision gaps rather than reworked here.\n // A class's deprecated qualified names (#3338) still identify it in rows\n // written before it moved package, so deleting it removes those too.\n const metaTypes: string[] = [];\n const registered = registry.getClass(className);\n const qualified = registered?.qualifiedName;\n if (qualified) metaTypes.push(qualified);\n for (const alias of registered?.config?.previousQualifiedNames ?? []) {\n if (typeof alias === 'string' && !metaTypes.includes(alias)) {\n metaTypes.push(alias);\n }\n }\n const simple = registered?.name ?? className;\n if (!metaTypes.includes(simple)) metaTypes.push(simple);\n\n return {\n references,\n polymorphic,\n metaTypes,\n isEmpty: references.length === 0 && polymorphic.length === 0,\n };\n}\n\n/** Per-delete state shared by every level of the cascade. */\ninterface CascadeContext {\n db: DatabaseInterface;\n registry: CascadeRegistryView;\n /** `table:id` pairs already expanded, so a reference cycle terminates. */\n visited: Set<string>;\n /** Tables touched by the cascade, for read-cache invalidation. */\n affectedTables: Set<string>;\n /**\n * Qualified name of the class that owns each affected table, so the\n * caller can resolve *that* class's own `@smrt({ cache })` config —\n * cross-process cache invalidation is a per-class opt-in, and a table\n * cascaded into belongs to a different class than the one `delete()` was\n * called on.\n */\n affectedTableClasses: Map<string, string>;\n}\n\n/** Resolve a registry-recorded class name to its qualified form, when known. */\nfunction toQualifiedClassName(\n registry: CascadeRegistryView,\n className: string,\n): string {\n return registry.getClass(className)?.qualifiedName ?? className;\n}\n\n/**\n * Every value `_smrt_contexts.owner_class` / `_smrt_embeddings.object_class`\n * could plausibly hold for a row belonging to an id of `className` (review\n * fix): both name forms (qualified + simple — older rows predate package\n * qualification) of `className` itself, plus every other member of its STI\n * hierarchy sharing its table. A CASCADE-collected id can belong to any\n * concrete STI subclass of the declaring reference, which stamps its own\n * (more specific) runtime class name at write time, not the reference's.\n *\n * Narrowing by this set — instead of matching by id alone — closes a\n * cross-class collision: two unrelated classes using `idType: 'text'`\n * (non-UUID, not guaranteed globally unique) could otherwise share an id\n * value and have one's `remember()`/embeddings rows deleted by the other's\n * cascade.\n */\nfunction ownerClassCandidates(\n registry: CascadeRegistryView,\n className: string,\n): string[] {\n const stiBase = registry.getSTIBase(className) ?? className;\n const members = new Set<string>([\n className,\n stiBase,\n ...registry.getDescendants(stiBase),\n ]);\n\n const names = new Set<string>();\n for (const member of members) {\n names.add(member);\n const registered = registry.getClass(member);\n if (registered?.qualifiedName) names.add(registered.qualifiedName);\n if (registered?.name) names.add(registered.name);\n }\n return [...names];\n}\n\n/**\n * Build the `where` clause matching `column` against one or more ids.\n *\n * A single id uses equality so the planner can use a plain index; multiple ids\n * use the adapter's `in` operator.\n */\nfunction idPredicate(column: string, ids: string[]): Record<string, unknown> {\n return ids.length === 1 ? { [column]: ids[0] } : { [`${column} in`]: ids };\n}\n\n/**\n * Run a cascade statement against a *referencing* table, tolerating the\n * table (or an expected column) not existing in this database.\n *\n * The cascade plan is built from the in-process registry, which can carry a\n * class from any imported package — including one whose table this specific\n * database was never migrated to include (a partially adopted feature, a\n * package pulled in for its types, or, in a test process, a fixture some\n * other test file registered). A missing referencing table trivially has no\n * rows to act on, so the correct behaviour is identical to the table\n * existing and being empty — this must never abort an otherwise valid\n * delete. Any other failure (a real constraint violation, a lock timeout, a\n * genuine SQL error unrelated to the table's existence) still propagates.\n */\nasync function tolerateMissingTable<T>(\n operation: () => Promise<T>,\n fallback: T,\n context: { table: string; action: string },\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (classifyDatabaseError(error).kind === 'undefined_object') {\n logger.warn(\n `Cascade delete skipped ${context.action} on '${context.table}': ` +\n 'table or column not found in this database.',\n { error: error instanceof Error ? error.message : String(error) },\n );\n return fallback;\n }\n throw error;\n }\n}\n\n/**\n * System-table cleanup may ignore an absent table for databases created before\n * that subsystem existed, but never a missing column in a table that does\n * exist. The shared `undefined_object` classification intentionally combines\n * both cases, so this security boundary must retain the narrower driver signal.\n */\nfunction normalizeMissingTableName(value: string): string {\n const unquoted = value.replace(/[\"'`[\\]]/g, '');\n return (unquoted.split('.').at(-1) ?? unquoted)\n .replace(/[^A-Za-z0-9_$-]/g, '')\n .toLowerCase();\n}\n\nfunction missingTableNames(messages: readonly string[]): string[] {\n const names: string[] = [];\n const patterns = [\n /no such table:\\s*([^\\s,;]+)/giu,\n /(?:relation|table)\\s+((?:\"[^\"]+\"(?:\\.\"[^\"]+\")*)|(?:[A-Za-z0-9_.$-]+))\\s+does not exist/giu,\n /table with name\\s+((?:\"[^\"]+\")|(?:[A-Za-z0-9_.$-]+))\\s+does not exist/giu,\n ];\n for (const message of messages) {\n for (const pattern of patterns) {\n for (const match of message.matchAll(pattern)) {\n if (match[1]) names.push(normalizeMissingTableName(match[1]));\n }\n }\n }\n return names;\n}\n\nfunction isMissingTableError(error: unknown, expectedTable: string): boolean {\n const classification = classifyDatabaseError(error);\n if (classification.kind !== 'undefined_object') return false;\n if (\n classification.sqlstate === '42703' ||\n classification.driverCode === '42703' ||\n classification.driverCodes.includes('42703')\n ) {\n return false;\n }\n const names = missingTableNames(classification.driverMessages);\n const expected = normalizeMissingTableName(expectedTable);\n return (\n names.length > 0 &&\n names.every((missingTableName) => missingTableName === expected)\n );\n}\n\nasync function tolerateMissingSystemTable<T>(\n operation: () => Promise<T>,\n fallback: T,\n context: { table: string; action: string },\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (!isMissingTableError(error, context.table)) throw error;\n logger.warn(\n `Cascade delete skipped ${context.action} on '${context.table}': ` +\n 'table not found in this database.',\n { error: error instanceof Error ? error.message : String(error) },\n );\n return fallback;\n }\n}\n\nasync function selectIds(\n db: DatabaseInterface,\n tableName: string,\n column: string,\n ids: string[],\n): Promise<string[]> {\n const found: string[] = [];\n for (const batch of chunkArray(ids, IN_LIST_CHUNK_SIZE)) {\n const dialect = isPostgresDatabase(db) ? 'postgres' : 'sqlite';\n const where = buildWhere(idPredicate(column, batch), 1, dialect);\n const table = `\"${tableName.replaceAll('\"', '\"\"')}\"`;\n const sql = `SELECT CAST(\"id\" AS VARCHAR) AS \"id\" FROM ${table} ${where.sql}`;\n const rows = await tolerateMissingTable(\n () => db.query(sql, ...where.values).then((result) => result.rows),\n [] as Record<string, unknown>[],\n { table: tableName, action: 'CASCADE select' },\n );\n for (const row of rows) {\n const id = row?.id;\n if (typeof id === 'string' && id.length > 0) found.push(id);\n }\n }\n return found;\n}\n\nasync function deleteByIds(\n db: DatabaseInterface,\n tableName: string,\n ids: string[],\n): Promise<void> {\n for (const batch of chunkArray(ids, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingTable(\n () => db.delete(tableName, idPredicate('id', batch)),\n undefined,\n { table: tableName, action: 'CASCADE delete' },\n );\n }\n}\n\n/**\n * Remove the framework-managed side rows owned by the given object ids.\n *\n * `_smrt_contexts` and `_smrt_embeddings` are keyed by `(owner_class, owner_id)`\n * and `(object_class, object_id)` respectively. Matched by id *and* class\n * (review fix): id alone would let two unrelated classes using\n * `idType: 'text'` (non-UUID, not guaranteed globally unique) collide on a\n * shared id value and delete each other's memory/embeddings rows. `classNames`\n * is the STI-hierarchy-expanded candidate set from\n * {@link ownerClassCandidates} — the class column stores the *runtime*\n * constructor name, which for an STI hierarchy is a concrete subclass rather\n * than the class the cascade was planned from, so a single exact name is not\n * enough.\n *\n * A missing system table is not an error — an application database may predate\n * the table. Every other cleanup failure propagates so the surrounding delete\n * transaction rolls back rather than orphaning tenant-sensitive recall data.\n */\nasync function deleteSystemRows(\n db: DatabaseInterface,\n ids: string[],\n classNames: string[],\n): Promise<void> {\n const ownerIds = ids.filter(\n (id) =>\n typeof id === 'string' &&\n id.length > 0 &&\n id !== COLLECTION_OWNER_SENTINEL,\n );\n if (ownerIds.length === 0 || classNames.length === 0) return;\n\n for (const [table, idColumn, classColumn] of [\n [CONTEXTS_TABLE, 'owner_id', 'owner_class'],\n [EMBEDDINGS_TABLE, 'object_id', 'object_class'],\n ] as const) {\n for (const batch of chunkArray(ownerIds, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingSystemTable(\n () =>\n db.delete(table, {\n ...idPredicate(idColumn, batch),\n ...idPredicate(classColumn, classNames),\n }),\n undefined,\n { table, action: 'framework-owned row cleanup' },\n );\n }\n }\n}\n\n/**\n * Resolve every reference pointing at `ids` of `className`, recursively.\n *\n * Does **not** delete the rows identified by `ids` — the caller owns that, so\n * `SmrtObject.delete()` keeps issuing its own final statement and its own\n * lifecycle hooks.\n */\nasync function resolveReferences(\n ctx: CascadeContext,\n className: string,\n tableName: string,\n ids: string[],\n depth: number,\n): Promise<void> {\n const pending = ids.filter((id) => {\n const key = `${tableName}:${id}`;\n if (ctx.visited.has(key)) return false;\n ctx.visited.add(key);\n return true;\n });\n if (pending.length === 0) return;\n\n if (depth > MAX_CASCADE_DEPTH) {\n throw DatabaseError.constraintViolation(\n `cascade delete from ${className} exceeded the maximum depth of ` +\n `${MAX_CASCADE_DEPTH}; check for a chain of onDelete: 'CASCADE' ` +\n 'references that never terminates',\n className,\n );\n }\n\n const plan = buildCascadePlan(ctx.registry, className);\n\n // SQL NO ACTION is immediate on every supported SMRT migration path, so the\n // app-side belt preflights it exactly like RESTRICT before any mutation.\n for (const reference of plan.references) {\n if (reference.action !== 'RESTRICT' && reference.action !== 'NO ACTION') {\n continue;\n }\n let remaining = 0;\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n remaining += await tolerateMissingTable(\n () =>\n ctx.db.count(\n reference.tableName,\n idPredicate(reference.column, batch),\n ),\n 0,\n { table: reference.tableName, action: `${reference.action} check` },\n );\n if (remaining > 0) break;\n }\n if (remaining > 0) {\n throw DatabaseError.constraintViolation(\n `${reference.className}.${reference.fieldName} ` +\n `${reference.declared ? 'declares' : 'resolves to'} onDelete: ` +\n `'${reference.action}' and ${remaining} row(s) still reference this ` +\n `${className}`,\n reference.column,\n );\n }\n }\n\n for (const reference of plan.references) {\n if (reference.action === 'SET NULL') {\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingTable(\n () =>\n ctx.db.update(\n reference.tableName,\n idPredicate(reference.column, batch),\n { [reference.column]: null },\n ),\n undefined,\n { table: reference.tableName, action: 'SET NULL' },\n );\n }\n ctx.affectedTables.add(reference.tableName);\n ctx.affectedTableClasses.set(\n reference.tableName,\n toQualifiedClassName(ctx.registry, reference.className),\n );\n continue;\n }\n\n if (reference.action !== 'CASCADE') continue;\n\n const childIds = await selectIds(\n ctx.db,\n reference.tableName,\n reference.column,\n pending,\n );\n if (childIds.length === 0) continue;\n\n await resolveReferences(\n ctx,\n reference.className,\n reference.tableName,\n childIds,\n depth + 1,\n );\n await deleteSystemRows(\n ctx.db,\n childIds,\n ownerClassCandidates(ctx.registry, reference.className),\n );\n await deleteByIds(ctx.db, reference.tableName, childIds);\n ctx.affectedTables.add(reference.tableName);\n ctx.affectedTableClasses.set(\n reference.tableName,\n toQualifiedClassName(ctx.registry, reference.className),\n );\n }\n\n for (const association of plan.polymorphic) {\n // A caught missing-table DELETE still aborts PostgreSQL's transaction.\n // Optional/abstract association tables must be checked before mutation.\n if (!(await ctx.db.tableExists(association.tableName))) continue;\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n const where: Record<string, unknown> = {\n ...idPredicate('meta_id', batch),\n };\n if (plan.metaTypes.length === 1) {\n where.meta_type = plan.metaTypes[0];\n } else {\n where['meta_type in'] = plan.metaTypes;\n }\n const result = await tolerateMissingTable(\n () => ctx.db.delete(association.tableName, where),\n undefined,\n {\n table: association.tableName,\n action: 'polymorphic association cleanup',\n },\n );\n if ((result?.affected ?? 0) > 0) {\n ctx.affectedTables.add(association.tableName);\n ctx.affectedTableClasses.set(\n association.tableName,\n toQualifiedClassName(ctx.registry, association.className),\n );\n }\n }\n }\n}\n\n/** Outcome of a cascade run, returned so the caller can invalidate caches. */\nexport interface CascadeResult {\n /** Tables whose rows were removed or nulled, excluding the target's own. */\n affectedTables: Set<string>;\n /**\n * Qualified class name that owns each entry in {@link affectedTables},\n * where resolvable — lets the caller check *that* class's own\n * cross-process cache config rather than only its own.\n */\n affectedTableClasses: Map<string, string>;\n}\n\n/**\n * Resolve every reference to `ids` of `className` and clean their framework\n * side rows, then hand control back so the caller can delete the rows.\n *\n * Callers are expected to run this on a transaction-bound `db` — see\n * {@link runCascadeDelete}, which owns that decision.\n */\nexport async function cascadeReferencesTo(\n db: DatabaseInterface,\n registry: CascadeRegistryView,\n target: { className: string; tableName: string; ids: string[] },\n): Promise<CascadeResult> {\n const ctx: CascadeContext = {\n db,\n registry,\n visited: new Set(),\n affectedTables: new Set(),\n affectedTableClasses: new Map(),\n };\n await resolveReferences(\n ctx,\n target.className,\n target.tableName,\n target.ids,\n 0,\n );\n await deleteSystemRows(\n db,\n target.ids,\n ownerClassCandidates(registry, target.className),\n );\n return {\n affectedTables: ctx.affectedTables,\n affectedTableClasses: ctx.affectedTableClasses,\n };\n}\n\ntype TransactionCapable = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\n/**\n * Run the cascade and the target row's own deletion atomically.\n *\n * When the adapter exposes `transaction()`, the whole sequence runs inside one\n * — including framework-owned row cleanup and the caller's `deleteSelf`\n * statement, so a failure part-way through cannot leave either side orphaned.\n * Adapters without transaction support run the same statements sequentially;\n * this is the documented degradation, not a silent one.\n *\n * @param db - Database the object is bound to\n * @param registry - Registry view used to build cascade plans\n * @param target - Class, table and id of the object being deleted\n * @param deleteSelf - Issues the target row's own `DELETE`, on the tx-bound db\n * @returns Tables affected by the cascade, for read-cache invalidation\n */\nexport async function runCascadeDelete(\n db: DatabaseInterface,\n registry: CascadeRegistryView,\n target: {\n className: string;\n tableName: string;\n /** An unsaved object has none; the cascade is then a no-op. */\n id: string | null | undefined;\n },\n deleteSelf: (db: DatabaseInterface) => Promise<void>,\n): Promise<CascadeResult> {\n const ids = target.id ? [target.id] : [];\n\n // Nothing references this class AND no polymorphic association class is\n // registered anywhere in the process. `plan.polymorphic` is unconditionally\n // every registered `SmrtPolymorphicAssociation` subclass — a `metaType`\n // column can point at any class at runtime, so there is no static metadata\n // to scope it by, unlike a typed `@foreignKey`/`@crossPackageRef`. This\n // branch is therefore common for a class with no incoming references in an\n // app with no polymorphic associations at all, but rare — not \"the\n // overwhelmingly common case\" — once even one polymorphic class exists\n // anywhere in the process, since every delete's plan then carries it.\n // Framework-owned context and embedding rows still need atomic cleanup even\n // when the registry has no typed or polymorphic references for this class.\n if (buildCascadePlan(registry, target.className).isEmpty) {\n const runWithoutReferences = async (\n bound: DatabaseInterface,\n ): Promise<CascadeResult> => {\n await deleteSystemRows(\n bound,\n ids,\n ownerClassCandidates(registry, target.className),\n );\n await deleteSelf(bound);\n return { affectedTables: new Set(), affectedTableClasses: new Map() };\n };\n const transaction = (db as TransactionCapable).transaction;\n if (typeof transaction !== 'function') return runWithoutReferences(db);\n return withEmbeddedWriteTransaction(\n db,\n isEmbeddedDatabase(db),\n runWithoutReferences,\n isFrameworkTransactionHandle(db),\n );\n }\n\n const run = async (bound: DatabaseInterface): Promise<CascadeResult> => {\n const result = await cascadeReferencesTo(bound, registry, {\n className: target.className,\n tableName: target.tableName,\n ids,\n });\n await deleteSelf(bound);\n return result;\n };\n\n const transaction = (db as TransactionCapable).transaction;\n if (typeof transaction !== 'function') {\n return run(db);\n }\n\n return withEmbeddedWriteTransaction(\n db,\n isEmbeddedDatabase(db),\n run,\n isFrameworkTransactionHandle(db),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cascade.js","names":[],"sources":["../src/cascade.ts"],"sourcesContent":["/**\n * App-side referential integrity for `SmrtObject.delete()` (#2371).\n *\n * Same-package `@foreignKey` fields now emit database constraints where the\n * engine can enforce the declared policy. Cross-package references and older\n * schemas still rely on this application-side path. Before this module,\n * `delete()` removed the object's own row and nothing else: junction rows,\n * polymorphic association rows, `_smrt_embeddings` and `_smrt_contexts` entries\n * were all left pointing at an id that no longer resolved, and\n * `@foreignKey(..., { onDelete })` was metadata nobody read.\n *\n * This module implements those semantics in the application layer. It mirrors\n * what a DB-level constraint would do, deliberately including the parts that\n * make it *unlike* a model-layer delete:\n *\n * - Cascaded rows are removed with set-based statements. Their `beforeDelete` /\n * `afterDelete` hooks and interceptors do **not** run, and no change-feed\n * tombstone is written for them — exactly as `ON DELETE CASCADE` behaves.\n * Only the object `delete()` was called on runs the full lifecycle.\n * - Every delete runs inside a single transaction when the adapter exposes\n * one, including cleanup of framework-owned context and embedding rows, so\n * a partial cascade cannot survive a failure. A `metaType` column can point\n * at any class at runtime, so a polymorphic association class is always\n * plausibly relevant.\n *\n * ## Which references are followed\n *\n * A reference is any `@foreignKey` / `@crossPackageRef` field on a registered\n * class whose target resolves to the deleted object's class (or one of its\n * registered STI ancestors), plus the polymorphic `(metaType, metaId)` pair on\n * {@link SmrtPolymorphicAssociation} subclasses.\n *\n * The action comes from the field's declared `onDelete`. When it is not\n * declared, the default is derived from the referencing class's natural key:\n *\n * | Reference | Default when `onDelete` is absent |\n * |---|---|\n * | Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |\n * | Polymorphic `(metaType, metaId)` association row | `CASCADE` |\n * | Ordinary same-package reference | `NO ACTION` (deletion is refused while references remain) |\n * | Every `@tenantId()` field | Excluded: tenant scope is not an ownership edge |\n *\n * The natural-key rule is what makes junction rows work without any\n * per-package annotation: a junction declares\n * `@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so\n * `content_id` identifies the row and the row cannot outlive the content it\n * links. An ordinary child (`Order.customerId`) is keyed by `(slug, context)`,\n * so it defaults to immediate `NO ACTION` unless it opts in with\n * `@foreignKey(Customer, { onDelete: 'CASCADE' })`.\n *\n * `@tenantId()` fields are excluded from the natural-key rule even though\n * `@happyvertical/smrt-tenancy` leads a tenant-scoped class's *default*\n * `conflictColumns` with the tenant column (#2360): the tenant column scopes\n * ownership, it does not identify the row the way a junction's foreign key\n * does, and it targets a class (`Tenant`) that is virtually always\n * referenced. Without this exclusion, deleting one `Tenant` row would\n * recursively CASCADE through every tenant-scoped table that has not\n * declared its own `conflictColumns` — the overwhelming majority. The field\n * is detected via the `__tenancy.isTenantIdField` marker `@tenantId()`\n * attaches to its own registration (`FieldMeta.__tenancy`, read structurally\n * so `smrt-core` never depends on `smrt-tenancy`). `@tenantId()` exposes no\n * `onDelete` option today, so this cannot currently be overridden per field.\n *\n * @see https://github.com/happyvertical/smrt/issues/2371\n * @module\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { classifyDatabaseError } from './db-errors.js';\nimport {\n isEmbeddedDatabase,\n isFrameworkTransactionHandle,\n isPostgresDatabase,\n withEmbeddedWriteTransaction,\n} from './embedded-write-queue.js';\nimport { ConfigurationError, DatabaseError } from './errors.js';\nimport { buildWhere } from './host.js';\nimport { isKnownNativeDuckDbHandle } from './native-duckdb.js';\n// Type-only: erased at runtime, so it cannot re-enter the\n// `registry → object → cascade` import cycle.\nimport type { ObjectRegistry } from './registry.js';\nimport {\n normalizeForeignKeyAction,\n resolveForeignKeyDeleteAction,\n} from './schema/foreign-key-policy.js';\nimport type { ForeignKeyAction } from './schema/types.js';\nimport { chunkArray, IN_LIST_CHUNK_SIZE } from './utils/chunk.js';\nimport { toSnakeCase } from './utils/naming.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Referential action applied to rows pointing at a deleted object.\n *\n * Same vocabulary as SQL's `ON DELETE`, enforced by the framework before the\n * engine. `NO ACTION` is preflighted like an immediate restrictive action so\n * application-side and database-side enforcement agree.\n */\nexport type OnDeleteAction = ForeignKeyAction;\n\n/** Table holding framework-managed per-object memory entries. */\nconst CONTEXTS_TABLE = '_smrt_contexts';\n\n/** Table holding framework-managed per-object embedding vectors. */\nconst EMBEDDINGS_TABLE = '_smrt_embeddings';\n\n/**\n * Reserved `_smrt_contexts.owner_id` used by `SmrtCollection.remember()` for\n * collection-scoped memory. A real object id is a UUID and can never equal it,\n * but the guard keeps a malformed id from wiping every sibling's defaults.\n */\nconst COLLECTION_OWNER_SENTINEL = '__collection__';\n\n/**\n * Maximum number of cascade levels followed from the object `delete()` was\n * called on. A cycle between two classes that both declare `onDelete: 'CASCADE'`\n * is broken by the visited set; this bound catches unbounded *chains*.\n */\nconst MAX_CASCADE_DEPTH = 10;\n\n/** One reference that must be resolved before the target row can be removed. */\nexport interface CascadeReference {\n /** Registered name of the class holding the referencing column. */\n className: string;\n /** Table holding the referencing rows. */\n tableName: string;\n /** Field name (camelCase) on the referencing class. */\n fieldName: string;\n /** Column (snake_case) holding the reference. */\n column: string;\n /** Resolved action. `NO ACTION` is preflighted like an immediate RESTRICT. */\n action: OnDeleteAction;\n /** `true` when the action was declared rather than derived from the key. */\n declared: boolean;\n}\n\n/** A polymorphic association table that can point at any class. */\nexport interface CascadePolymorphicReference {\n /** Registered name of the association class. */\n className: string;\n /** Table holding the association rows. */\n tableName: string;\n}\n\n/** Everything that must happen before rows of one class can be deleted. */\nexport interface CascadePlan {\n /** Typed `@foreignKey` / `@crossPackageRef` references, action-resolved. */\n references: CascadeReference[];\n /** Polymorphic association tables that may point at this class. */\n polymorphic: CascadePolymorphicReference[];\n /** `meta_type` values that identify this class in an association row. */\n metaTypes: string[];\n /** `true` when nothing references this class and nothing has to be visited. */\n isEmpty: boolean;\n}\n\n/**\n * Read-only slice of `ObjectRegistry` this module needs.\n *\n * Taking it as a parameter — rather than importing the registry as a value —\n * keeps `cascade.ts` out of the `registry → object → cascade` import cycle,\n * and lets tests drive the planner from a hand-built registry.\n */\nexport type CascadeRegistryView = Pick<\n typeof ObjectRegistry,\n | 'getRelationshipMap'\n | 'getFields'\n | 'getConflictColumns'\n | 'getTableName'\n | 'getSelfReferableNames'\n | 'getClass'\n | 'getSTIBase'\n | 'getDescendants'\n>;\n\n/**\n * Normalize a declared `onDelete` value.\n *\n * Accepts any casing and both `SET NULL` and `SET_NULL`, matching what the\n * schema generator has always carried in the manifest. Returns `undefined` for\n * an unset or unrecognized value so the caller can fall back to the default.\n */\nexport function normalizeOnDelete(value: unknown): OnDeleteAction | undefined {\n return normalizeForeignKeyAction(value);\n}\n\nfunction isPolymorphicAssociationClass(fields: Map<string, unknown>): boolean {\n // The three columns `SmrtPolymorphicAssociation` contributes. Requiring all\n // of them keeps an unrelated class that happens to carry a `metaType` from\n // being treated as an association table.\n return fields.has('metaType') && fields.has('metaId') && fields.has('role');\n}\n\n/**\n * Build the cascade plan for one class.\n *\n * The plan is derived entirely from registry metadata, so it is valid for any\n * database the class is used against. It is rebuilt per delete rather than\n * cached: registration is incremental (manifests load lazily, tests register\n * classes between cases) and a stale plan would silently skip a table.\n *\n * @param registry - Registry view (`ObjectRegistry` satisfies it)\n * @param className - Class whose incoming references should be resolved\n */\nexport function buildCascadePlan(\n registry: CascadeRegistryView,\n className: string,\n): CascadePlan {\n // Keep canonical self/ancestor identities from the registry. Never resolve\n // their display aliases again: a shared simple name can select another\n // package. Legacy relationship views without canonical metadata still use\n // the simple aliases already included by getSelfReferableNames().\n const targetNames = new Set(registry.getSelfReferableNames(className));\n\n const references: CascadeReference[] = [];\n const polymorphic: CascadePolymorphicReference[] = [];\n const visitedBuckets = new Set<unknown>();\n\n const relationshipMap = registry.getRelationshipMap();\n for (const [sourceClass, relationships] of relationshipMap) {\n const simpleSource = relationships[0]?.sourceClass;\n if (\n simpleSource &&\n sourceClass !== simpleSource &&\n relationshipMap.get(simpleSource) === relationships\n ) {\n continue;\n }\n if (visitedBuckets.has(relationships)) continue;\n visitedBuckets.add(relationships);\n const fields = registry.getFields(sourceClass);\n\n if (isPolymorphicAssociationClass(fields)) {\n const tableName = registry.getTableName(sourceClass);\n if (tableName) {\n polymorphic.push({ className: sourceClass, tableName });\n }\n }\n\n if (relationships.length === 0) continue;\n\n let conflictColumns: Set<string> | undefined;\n\n for (const relationship of relationships) {\n if (\n relationship.type !== 'foreignKey' &&\n relationship.type !== 'crossPackageRef'\n ) {\n continue;\n }\n const target =\n relationship.targetQualifiedClass === undefined\n ? relationship.targetClass\n : relationship.targetQualifiedClass;\n if (target === null || !targetNames.has(target)) continue;\n\n // An explicit app-side-only relationship is archival metadata, not a\n // referential-integrity rule. Its identifier is deliberately allowed to\n // outlive the parent, so delete planning must not block, null, or remove\n // the retained row (#2413).\n if (\n relationship.type === 'foreignKey' &&\n relationship.options?.constraint === false\n ) {\n continue;\n }\n\n const tableName = registry.getTableName(sourceClass);\n if (!tableName) continue;\n\n const column = toSnakeCase(relationship.fieldName);\n if (!conflictColumns) {\n conflictColumns = new Set(registry.getConflictColumns(sourceClass));\n }\n\n // A `@tenantId()` field is a structural scoping marker, not a\n // junction/ownership key — it lands in `conflictColumns` only because\n // #2360 leads every tenant-scoped class's *default* natural key with\n // the tenant column, not because the referencing row is *identified*\n // by its tenant the way a junction row is identified by its parent.\n // Without this guard, deleting a `Tenant` would silently CASCADE\n // through the tenant column of every tenant-scoped class in the\n // schema that has not declared its own `conflictColumns` — the\n // overwhelming majority. `@tenantId()` exposes no `onDelete` today, so\n // an explicit declaration can never widen this back to CASCADE; that\n // is deliberate until tenant-delete cascade is an explicit decision.\n const isTenantIdField =\n relationship.options?.__tenancy?.isTenantIdField === true;\n if (isTenantIdField) continue;\n\n const { action, declared } = resolveForeignKeyDeleteAction({\n declared: relationship.options?.onDelete,\n isConflictColumn: conflictColumns.has(column),\n isTenantIdField: false,\n columnName: column,\n });\n\n if (\n action === 'SET NULL' &&\n fields.get(relationship.fieldName)?.required\n ) {\n // Fail on the declaration, not later on a NOT NULL violation from the\n // engine — the message there names a column, not the decorator.\n throw ConfigurationError.invalidConfiguration(\n `${sourceClass}.${relationship.fieldName} onDelete: 'SET NULL'`,\n relationship.fieldName,\n \"a nullable field (mark it `nullable: true`, or use 'CASCADE' / 'RESTRICT')\",\n );\n }\n\n references.push({\n className: sourceClass,\n tableName,\n fieldName: relationship.fieldName,\n column,\n action,\n declared,\n });\n }\n }\n\n // Both forms are matched deliberately: an association row written before a\n // class was package-qualified still carries the simple `meta_type`. The\n // qualified form alone is unambiguous; the simple-name fallback can still\n // match a same-simple-name sibling class's row if it also shares a\n // `meta_id` — a real, not just theoretical, risk for a class declaring\n // `idType: 'text'` (non-UUID, not guaranteed globally unique), the same gap\n // `deleteSystemRows()` closed for `_smrt_contexts` / `_smrt_embeddings`\n // (review fix) by adding a class-name filter. Closing it here needs the\n // same STI-and-legacy-name-aware filter this module already builds for\n // `deleteSystemRows()` narrowed further to registry-unambiguous simple\n // names; tracked under #2419 alongside the other same-simple-name\n // collision gaps rather than reworked here.\n // A class's deprecated qualified names (#3338) still identify it in rows\n // written before it moved package, so deleting it removes those too.\n const metaTypes: string[] = [];\n const registered = registry.getClass(className);\n const qualified = registered?.qualifiedName;\n if (qualified) metaTypes.push(qualified);\n for (const alias of registered?.config?.previousQualifiedNames ?? []) {\n if (typeof alias === 'string' && !metaTypes.includes(alias)) {\n metaTypes.push(alias);\n }\n }\n const simple = registered?.name ?? className;\n if (!metaTypes.includes(simple)) metaTypes.push(simple);\n\n return {\n references,\n polymorphic,\n metaTypes,\n isEmpty: references.length === 0 && polymorphic.length === 0,\n };\n}\n\n/** Per-delete state shared by every level of the cascade. */\ninterface CascadeContext {\n db: DatabaseInterface;\n registry: CascadeRegistryView;\n /** `table:id` pairs already expanded, so a reference cycle terminates. */\n visited: Set<string>;\n /** Tables touched by the cascade, for read-cache invalidation. */\n affectedTables: Set<string>;\n /**\n * Qualified name of the class that owns each affected table, so the\n * caller can resolve *that* class's own `@smrt({ cache })` config —\n * cross-process cache invalidation is a per-class opt-in, and a table\n * cascaded into belongs to a different class than the one `delete()` was\n * called on.\n */\n affectedTableClasses: Map<string, string>;\n}\n\n/** Resolve a registry-recorded class name to its qualified form, when known. */\nfunction toQualifiedClassName(\n registry: CascadeRegistryView,\n className: string,\n): string {\n return registry.getClass(className)?.qualifiedName ?? className;\n}\n\n/**\n * Every value `_smrt_contexts.owner_class` / `_smrt_embeddings.object_class`\n * could plausibly hold for a row belonging to an id of `className` (review\n * fix): both name forms (qualified + simple — older rows predate package\n * qualification) of `className` itself, plus every other member of its STI\n * hierarchy sharing its table. A CASCADE-collected id can belong to any\n * concrete STI subclass of the declaring reference, which stamps its own\n * (more specific) runtime class name at write time, not the reference's.\n *\n * Narrowing by this set — instead of matching by id alone — closes a\n * cross-class collision: two unrelated classes using `idType: 'text'`\n * (non-UUID, not guaranteed globally unique) could otherwise share an id\n * value and have one's `remember()`/embeddings rows deleted by the other's\n * cascade.\n */\nfunction ownerClassCandidates(\n registry: CascadeRegistryView,\n className: string,\n): string[] {\n const stiBase = registry.getSTIBase(className) ?? className;\n const members = new Set<string>([\n className,\n stiBase,\n ...registry.getDescendants(stiBase),\n ]);\n\n const names = new Set<string>();\n for (const member of members) {\n names.add(member);\n const registered = registry.getClass(member);\n if (registered?.qualifiedName) names.add(registered.qualifiedName);\n if (registered?.name) names.add(registered.name);\n }\n return [...names];\n}\n\n/**\n * Build the `where` clause matching `column` against one or more ids.\n *\n * A single id uses equality so the planner can use a plain index; multiple ids\n * use the adapter's `in` operator.\n */\nfunction idPredicate(column: string, ids: string[]): Record<string, unknown> {\n return ids.length === 1 ? { [column]: ids[0] } : { [`${column} in`]: ids };\n}\n\n/**\n * Count of cascade write statements issued per transaction-bound handle\n * (#2824). A joined no-savepoint transaction is aborted on failure only when\n * the cascade actually changed something in it; a refusal raised before any\n * write — the RESTRICT / NO ACTION preflight — leaves the caller's transaction\n * usable, exactly as a refusal on a savepoint-capable engine would.\n */\nconst cascadeWrites = new WeakMap<DatabaseInterface, number>();\n\n/**\n * Record a cascade write on `db`. A result that reports `affected: 0` changed\n * nothing; anything else (including adapters that report no count) counts.\n */\nfunction recordCascadeWrite<T>(db: DatabaseInterface, result: T): T {\n const affected = (result as { affected?: unknown } | undefined)?.affected;\n if (typeof affected !== 'number' || affected > 0) {\n cascadeWrites.set(db, (cascadeWrites.get(db) ?? 0) + 1);\n }\n return result;\n}\n\n/**\n * Run a cascade statement against a *referencing* table, tolerating the\n * table (or an expected column) not existing in this database.\n *\n * The cascade plan is built from the in-process registry, which can carry a\n * class from any imported package — including one whose table this specific\n * database was never migrated to include (a partially adopted feature, a\n * package pulled in for its types, or, in a test process, a fixture some\n * other test file registered). A missing referencing table trivially has no\n * rows to act on, so the correct behaviour is identical to the table\n * existing and being empty — this must never abort an otherwise valid\n * delete. Any other failure (a real constraint violation, a lock timeout, a\n * genuine SQL error unrelated to the table's existence) still propagates.\n */\nasync function tolerateMissingTable<T>(\n operation: () => Promise<T>,\n fallback: T,\n context: { table: string; action: string },\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (classifyDatabaseError(error).kind === 'undefined_object') {\n logger.warn(\n `Cascade delete skipped ${context.action} on '${context.table}': ` +\n 'table or column not found in this database.',\n { error: error instanceof Error ? error.message : String(error) },\n );\n return fallback;\n }\n throw error;\n }\n}\n\n/**\n * System-table cleanup may ignore an absent table for databases created before\n * that subsystem existed, but never a missing column in a table that does\n * exist. The shared `undefined_object` classification intentionally combines\n * both cases, so this security boundary must retain the narrower driver signal.\n */\nfunction normalizeMissingTableName(value: string): string {\n const unquoted = value.replace(/[\"'`[\\]]/g, '');\n return (unquoted.split('.').at(-1) ?? unquoted)\n .replace(/[^A-Za-z0-9_$-]/g, '')\n .toLowerCase();\n}\n\nfunction missingTableNames(messages: readonly string[]): string[] {\n const names: string[] = [];\n const patterns = [\n /no such table:\\s*([^\\s,;]+)/giu,\n /(?:relation|table)\\s+((?:\"[^\"]+\"(?:\\.\"[^\"]+\")*)|(?:[A-Za-z0-9_.$-]+))\\s+does not exist/giu,\n /table with name\\s+((?:\"[^\"]+\")|(?:[A-Za-z0-9_.$-]+))\\s+does not exist/giu,\n ];\n for (const message of messages) {\n for (const pattern of patterns) {\n for (const match of message.matchAll(pattern)) {\n if (match[1]) names.push(normalizeMissingTableName(match[1]));\n }\n }\n }\n return names;\n}\n\nfunction isMissingTableError(error: unknown, expectedTable: string): boolean {\n const classification = classifyDatabaseError(error);\n if (classification.kind !== 'undefined_object') return false;\n if (\n classification.sqlstate === '42703' ||\n classification.driverCode === '42703' ||\n classification.driverCodes.includes('42703')\n ) {\n return false;\n }\n const names = missingTableNames(classification.driverMessages);\n const expected = normalizeMissingTableName(expectedTable);\n return (\n names.length > 0 &&\n names.every((missingTableName) => missingTableName === expected)\n );\n}\n\nasync function tolerateMissingSystemTable<T>(\n operation: () => Promise<T>,\n fallback: T,\n context: { table: string; action: string },\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (!isMissingTableError(error, context.table)) throw error;\n logger.warn(\n `Cascade delete skipped ${context.action} on '${context.table}': ` +\n 'table not found in this database.',\n { error: error instanceof Error ? error.message : String(error) },\n );\n return fallback;\n }\n}\n\nasync function selectIds(\n db: DatabaseInterface,\n tableName: string,\n column: string,\n ids: string[],\n): Promise<string[]> {\n const found: string[] = [];\n for (const batch of chunkArray(ids, IN_LIST_CHUNK_SIZE)) {\n const dialect = isPostgresDatabase(db) ? 'postgres' : 'sqlite';\n const where = buildWhere(idPredicate(column, batch), 1, dialect);\n const table = `\"${tableName.replaceAll('\"', '\"\"')}\"`;\n const sql = `SELECT CAST(\"id\" AS VARCHAR) AS \"id\" FROM ${table} ${where.sql}`;\n const rows = await tolerateMissingTable(\n () => db.query(sql, ...where.values).then((result) => result.rows),\n [] as Record<string, unknown>[],\n { table: tableName, action: 'CASCADE select' },\n );\n for (const row of rows) {\n const id = row?.id;\n if (typeof id === 'string' && id.length > 0) found.push(id);\n }\n }\n return found;\n}\n\nasync function deleteByIds(\n db: DatabaseInterface,\n tableName: string,\n ids: string[],\n): Promise<void> {\n for (const batch of chunkArray(ids, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingTable(\n () =>\n db\n .delete(tableName, idPredicate('id', batch))\n .then((result) => recordCascadeWrite(db, result)),\n undefined,\n { table: tableName, action: 'CASCADE delete' },\n );\n }\n}\n\n/**\n * Remove the framework-managed side rows owned by the given object ids.\n *\n * `_smrt_contexts` and `_smrt_embeddings` are keyed by `(owner_class, owner_id)`\n * and `(object_class, object_id)` respectively. Matched by id *and* class\n * (review fix): id alone would let two unrelated classes using\n * `idType: 'text'` (non-UUID, not guaranteed globally unique) collide on a\n * shared id value and delete each other's memory/embeddings rows. `classNames`\n * is the STI-hierarchy-expanded candidate set from\n * {@link ownerClassCandidates} — the class column stores the *runtime*\n * constructor name, which for an STI hierarchy is a concrete subclass rather\n * than the class the cascade was planned from, so a single exact name is not\n * enough.\n *\n * A missing system table is not an error — an application database may predate\n * the table. Every other cleanup failure propagates so the surrounding delete\n * transaction rolls back rather than orphaning tenant-sensitive recall data.\n */\nasync function deleteSystemRows(\n db: DatabaseInterface,\n ids: string[],\n classNames: string[],\n): Promise<void> {\n const ownerIds = ids.filter(\n (id) =>\n typeof id === 'string' &&\n id.length > 0 &&\n id !== COLLECTION_OWNER_SENTINEL,\n );\n if (ownerIds.length === 0 || classNames.length === 0) return;\n\n for (const [table, idColumn, classColumn] of [\n [CONTEXTS_TABLE, 'owner_id', 'owner_class'],\n [EMBEDDINGS_TABLE, 'object_id', 'object_class'],\n ] as const) {\n for (const batch of chunkArray(ownerIds, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingSystemTable(\n () =>\n db\n .delete(table, {\n ...idPredicate(idColumn, batch),\n ...idPredicate(classColumn, classNames),\n })\n .then((result) => recordCascadeWrite(db, result)),\n undefined,\n { table, action: 'framework-owned row cleanup' },\n );\n }\n }\n}\n\n/**\n * Resolve every reference pointing at `ids` of `className`, recursively.\n *\n * Does **not** delete the rows identified by `ids` — the caller owns that, so\n * `SmrtObject.delete()` keeps issuing its own final statement and its own\n * lifecycle hooks.\n */\nasync function resolveReferences(\n ctx: CascadeContext,\n className: string,\n tableName: string,\n ids: string[],\n depth: number,\n): Promise<void> {\n const pending = ids.filter((id) => {\n const key = `${tableName}:${id}`;\n if (ctx.visited.has(key)) return false;\n ctx.visited.add(key);\n return true;\n });\n if (pending.length === 0) return;\n\n if (depth > MAX_CASCADE_DEPTH) {\n throw DatabaseError.constraintViolation(\n `cascade delete from ${className} exceeded the maximum depth of ` +\n `${MAX_CASCADE_DEPTH}; check for a chain of onDelete: 'CASCADE' ` +\n 'references that never terminates',\n className,\n );\n }\n\n const plan = buildCascadePlan(ctx.registry, className);\n\n // SQL NO ACTION is immediate on every supported SMRT migration path, so the\n // app-side belt preflights it exactly like RESTRICT before any mutation.\n for (const reference of plan.references) {\n if (reference.action !== 'RESTRICT' && reference.action !== 'NO ACTION') {\n continue;\n }\n let remaining = 0;\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n remaining += await tolerateMissingTable(\n () =>\n ctx.db.count(\n reference.tableName,\n idPredicate(reference.column, batch),\n ),\n 0,\n { table: reference.tableName, action: `${reference.action} check` },\n );\n if (remaining > 0) break;\n }\n if (remaining > 0) {\n throw DatabaseError.constraintViolation(\n `${reference.className}.${reference.fieldName} ` +\n `${reference.declared ? 'declares' : 'resolves to'} onDelete: ` +\n `'${reference.action}' and ${remaining} row(s) still reference this ` +\n `${className}`,\n reference.column,\n );\n }\n }\n\n for (const reference of plan.references) {\n if (reference.action === 'SET NULL') {\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n await tolerateMissingTable(\n () =>\n ctx.db\n .update(\n reference.tableName,\n idPredicate(reference.column, batch),\n { [reference.column]: null },\n )\n .then((result) => recordCascadeWrite(ctx.db, result)),\n undefined,\n { table: reference.tableName, action: 'SET NULL' },\n );\n }\n ctx.affectedTables.add(reference.tableName);\n ctx.affectedTableClasses.set(\n reference.tableName,\n toQualifiedClassName(ctx.registry, reference.className),\n );\n continue;\n }\n\n if (reference.action !== 'CASCADE') continue;\n\n const childIds = await selectIds(\n ctx.db,\n reference.tableName,\n reference.column,\n pending,\n );\n if (childIds.length === 0) continue;\n\n await resolveReferences(\n ctx,\n reference.className,\n reference.tableName,\n childIds,\n depth + 1,\n );\n await deleteSystemRows(\n ctx.db,\n childIds,\n ownerClassCandidates(ctx.registry, reference.className),\n );\n await deleteByIds(ctx.db, reference.tableName, childIds);\n ctx.affectedTables.add(reference.tableName);\n ctx.affectedTableClasses.set(\n reference.tableName,\n toQualifiedClassName(ctx.registry, reference.className),\n );\n }\n\n for (const association of plan.polymorphic) {\n // A caught missing-table DELETE still aborts PostgreSQL's transaction.\n // Optional/abstract association tables must be checked before mutation.\n if (!(await ctx.db.tableExists(association.tableName))) continue;\n for (const batch of chunkArray(pending, IN_LIST_CHUNK_SIZE)) {\n const where: Record<string, unknown> = {\n ...idPredicate('meta_id', batch),\n };\n if (plan.metaTypes.length === 1) {\n where.meta_type = plan.metaTypes[0];\n } else {\n where['meta_type in'] = plan.metaTypes;\n }\n const result = await tolerateMissingTable(\n () =>\n ctx.db\n .delete(association.tableName, where)\n .then((result) => recordCascadeWrite(ctx.db, result)),\n undefined,\n {\n table: association.tableName,\n action: 'polymorphic association cleanup',\n },\n );\n if ((result?.affected ?? 0) > 0) {\n ctx.affectedTables.add(association.tableName);\n ctx.affectedTableClasses.set(\n association.tableName,\n toQualifiedClassName(ctx.registry, association.className),\n );\n }\n }\n }\n}\n\n/** Outcome of a cascade run, returned so the caller can invalidate caches. */\nexport interface CascadeResult {\n /** Tables whose rows were removed or nulled, excluding the target's own. */\n affectedTables: Set<string>;\n /**\n * Qualified class name that owns each entry in {@link affectedTables},\n * where resolvable — lets the caller check *that* class's own\n * cross-process cache config rather than only its own.\n */\n affectedTableClasses: Map<string, string>;\n}\n\n/**\n * Resolve every reference to `ids` of `className` and clean their framework\n * side rows, then hand control back so the caller can delete the rows.\n *\n * Callers are expected to run this on a transaction-bound `db` — see\n * {@link runCascadeDelete}, which owns that decision.\n */\nexport async function cascadeReferencesTo(\n db: DatabaseInterface,\n registry: CascadeRegistryView,\n target: { className: string; tableName: string; ids: string[] },\n): Promise<CascadeResult> {\n const ctx: CascadeContext = {\n db,\n registry,\n visited: new Set(),\n affectedTables: new Set(),\n affectedTableClasses: new Map(),\n };\n await resolveReferences(\n ctx,\n target.className,\n target.tableName,\n target.ids,\n 0,\n );\n await deleteSystemRows(\n db,\n target.ids,\n ownerClassCandidates(registry, target.className),\n );\n return {\n affectedTables: ctx.affectedTables,\n affectedTableClasses: ctx.affectedTableClasses,\n };\n}\n\ntype TransactionCapable = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\n/**\n * Run the cascade and the target row's own deletion atomically.\n *\n * When the adapter exposes `transaction()`, the whole sequence runs inside one\n * — including framework-owned row cleanup and the caller's `deleteSelf`\n * statement, so a failure part-way through cannot leave either side orphaned.\n * Adapters without transaction support run the same statements sequentially;\n * this is the documented degradation, not a silent one.\n *\n * @param db - Database the object is bound to\n * @param registry - Registry view used to build cascade plans\n * @param target - Class, table and id of the object being deleted\n * @param deleteSelf - Issues the target row's own `DELETE`, on the tx-bound db\n * @returns Tables affected by the cascade, for read-cache invalidation\n */\nexport async function runCascadeDelete(\n db: DatabaseInterface,\n registry: CascadeRegistryView,\n target: {\n className: string;\n tableName: string;\n /** An unsaved object has none; the cascade is then a no-op. */\n id: string | null | undefined;\n },\n deleteSelf: (db: DatabaseInterface) => Promise<void>,\n): Promise<CascadeResult> {\n const ids = target.id ? [target.id] : [];\n\n // Nothing references this class AND no polymorphic association class is\n // registered anywhere in the process. `plan.polymorphic` is unconditionally\n // every registered `SmrtPolymorphicAssociation` subclass — a `metaType`\n // column can point at any class at runtime, so there is no static metadata\n // to scope it by, unlike a typed `@foreignKey`/`@crossPackageRef`. This\n // branch is therefore common for a class with no incoming references in an\n // app with no polymorphic associations at all, but rare — not \"the\n // overwhelmingly common case\" — once even one polymorphic class exists\n // anywhere in the process, since every delete's plan then carries it.\n // Framework-owned context and embedding rows still need atomic cleanup even\n // when the registry has no typed or polymorphic references for this class.\n if (buildCascadePlan(registry, target.className).isEmpty) {\n const runWithoutReferences = async (\n bound: DatabaseInterface,\n ): Promise<CascadeResult> => {\n await deleteSystemRows(\n bound,\n ids,\n ownerClassCandidates(registry, target.className),\n );\n await deleteSelf(bound);\n return { affectedTables: new Set(), affectedTableClasses: new Map() };\n };\n const transaction = (db as TransactionCapable).transaction;\n if (typeof transaction !== 'function') return runWithoutReferences(db);\n return runInOwningCascadeTransaction(db, runWithoutReferences);\n }\n\n const run = async (bound: DatabaseInterface): Promise<CascadeResult> => {\n const result = await cascadeReferencesTo(bound, registry, {\n className: target.className,\n tableName: target.tableName,\n ids,\n });\n await deleteSelf(bound);\n return result;\n };\n\n const transaction = (db as TransactionCapable).transaction;\n if (typeof transaction !== 'function') {\n return run(db);\n }\n\n return runInOwningCascadeTransaction(db, run);\n}\n\n/**\n * Message of the statement that aborts a caller-owned native DuckDB\n * transaction after a cascade failed part-way through it (#2824).\n */\nexport const CASCADE_ABORTED_CALLER_TRANSACTION =\n 'smrt: delete cascade failed inside a caller-owned DuckDB transaction; the transaction is aborted';\n\n/**\n * Own the cascade's atomic scope (#2824).\n *\n * The cascade and the target's own `DELETE` run in a transaction opened on\n * `db`. When `db` is already a transaction handle on an engine without\n * `SAVEPOINT`, the SDK refuses the nested `transaction()` before running the\n * callback or touching the enclosing transaction (`NestedTransactionError`).\n * The cascade then joins that enclosing transaction directly instead of\n * failing, which is the only atomic scope there is:\n *\n * - framework-owned handles (`withTransaction()`, the bulk junction path) —\n * unchanged since #2818;\n * - caller-owned native DuckDB transactions (`db.transaction(tx => …)` or\n * `beginTransaction()` handed to `withDatabase()`). Commit and rollback stay\n * the caller's: a rollback discards the cascade with everything else.\n *\n * Joining without a savepoint means a cascade that fails after issuing some\n * of its writes cannot be undone on its own. On native DuckDB the framework\n * therefore aborts the joined transaction before rethrowing when the cascade\n * already issued a write (a refusal before the first write, such as the\n * depth-0 RESTRICT / NO ACTION preflight, leaves it untouched). DuckDB's\n * `delete()` reports a constant affected count and owned-row cleanup always\n * runs, so in practice any failure after that preflight aborts — DuckDB then\n * refuses every later statement in it and its `COMMIT` persists nothing — so a\n * caller that swallows the error can never commit a half-applied cascade. The\n * callback runs at most once: the nested refusal happens before it starts, and\n * nothing is replayed after it has.\n *\n * Other no-savepoint adapters (the JSON adapter) keep the detectable\n * `NestedTransactionError` refusal for caller-owned transactions by default:\n * they offer no statement that poisons the enclosing transaction, so joining\n * could commit a partial cascade. The compatible bulk junction path (#2818)\n * passes `reuseUnsupportedNested: true` to keep its established join-any\n * behavior, and still gets the DuckDB abort-on-failure guard.\n *\n * @param db - Database the cascade is bound to (a root or a transaction handle)\n * @param run - Cascade plus the target's own delete, on the transaction-bound db\n * @param reuseUnsupportedNested - Join any enclosing transaction that refuses\n * nesting; defaults to framework-owned handles and native DuckDB\n */\nexport async function runInOwningCascadeTransaction<T>(\n db: DatabaseInterface,\n run: (bound: DatabaseInterface) => Promise<T>,\n reuseUnsupportedNested?: boolean,\n): Promise<T> {\n const nativeDuckDb = isKnownNativeDuckDbHandle(db);\n return withEmbeddedWriteTransaction(\n db,\n isEmbeddedDatabase(db),\n async (bound) => {\n // `bound === db` only when the nested transaction was refused and the\n // cascade joined the enclosing one; an opened transaction is a new scope.\n if (bound !== db || !nativeDuckDb) return run(bound);\n const writesBefore = cascadeWrites.get(bound) ?? 0;\n try {\n return await run(bound);\n } catch (error) {\n // Only a cascade that already changed rows is half-applied; a refusal\n // before its first write (RESTRICT / NO ACTION) leaves the caller's\n // transaction intact and usable.\n if ((cascadeWrites.get(bound) ?? 0) > writesBefore) {\n await abortJoinedDuckDbTransaction(bound);\n }\n throw error;\n }\n },\n reuseUnsupportedNested ??\n (isFrameworkTransactionHandle(db) || nativeDuckDb),\n );\n}\n\n/**\n * Put a joined native DuckDB transaction into DuckDB's aborted state.\n *\n * A failing statement is the engine's own mechanism: after it, DuckDB rejects\n * every statement with \"Current transaction is aborted\" until the transaction\n * ends, and ending it discards its writes. The statement is expected to throw;\n * its error is not the one the caller needs, so it is dropped.\n */\nasync function abortJoinedDuckDbTransaction(\n db: DatabaseInterface,\n): Promise<void> {\n try {\n await db.query(\n `SELECT error('${CASCADE_ABORTED_CALLER_TRANSACTION.replaceAll(\"'\", \"''\")}')`,\n );\n } catch {\n // Expected: this statement exists to fail.\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-feed.d.ts","sourceRoot":"","sources":["../src/change-feed.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"change-feed.d.ts","sourceRoot":"","sources":["../src/change-feed.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@ import { publishChangeSignal } from "./change-signals.js";
6
6
  import { resolveDispatchTenantScope } from "./dispatch/tenant-resolver.js";
7
7
  import { GlobalInterceptors } from "./interceptors.js";
8
8
  import { detectEngine } from "./schema/ddl/index.js";
9
- import { CREATE_SMRT_CHANGES_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES } from "./system/schema.js";
9
+ import { CREATE_SMRT_CHANGES_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES } from "./system/schema.js";
10
10
  import { SYSTEM_TABLE_NAMES } from "./schema/system-table-shapes.js";
11
11
  import { createLogger } from "@happyvertical/logger";
12
12
  //#region src/change-feed.ts
@@ -371,8 +371,9 @@ async function postgresChangeFeedHelpersCurrent(db) {
371
371
  ${currentHelperProbe(POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY)} AS function_name,
372
372
  ${currentHelperProbe(POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY)} AS batch_function_name,
373
373
  ${currentHelperProbe(POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY)} AS drain_function_name,
374
+ ${currentHelperProbe(POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY)} AS compat_drain_function_name,
374
375
  to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') AS pending_table_name`));
375
- return Boolean(rows[0]?.function_name && rows[0]?.batch_function_name && rows[0]?.drain_function_name && rows[0]?.pending_table_name);
376
+ return Boolean(rows[0]?.function_name && rows[0]?.batch_function_name && rows[0]?.drain_function_name && rows[0]?.compat_drain_function_name && rows[0]?.pending_table_name);
376
377
  }
377
378
  async function getPostgresChangeFeedSchemaState(db) {
378
379
  const rows = getQueryRows(await db.query(`SELECT
@@ -381,6 +382,7 @@ async function getPostgresChangeFeedSchemaState(db) {
381
382
  ${currentHelperProbe(POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY)} AS batch_function_name,
382
383
  to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') AS pending_table_name,
383
384
  ${currentHelperProbe(POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY)} AS drain_function_name,
385
+ ${currentHelperProbe(POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY)} AS compat_drain_function_name,
384
386
  (
385
387
  SELECT data_type
386
388
  FROM information_schema.columns
@@ -393,7 +395,7 @@ async function getPostgresChangeFeedSchemaState(db) {
393
395
  functionExists: Boolean(rows[0]?.function_name),
394
396
  batchFunctionExists: Boolean(rows[0]?.batch_function_name),
395
397
  pendingTableExists: Boolean(rows[0]?.pending_table_name),
396
- drainFunctionExists: Boolean(rows[0]?.drain_function_name),
398
+ drainFunctionExists: Boolean(rows[0]?.drain_function_name && rows[0]?.compat_drain_function_name),
397
399
  createdAtType: rows[0]?.created_at_type ? String(rows[0].created_at_type) : null
398
400
  };
399
401
  }
@@ -717,7 +719,8 @@ async function drainChangeFeedDetailed(db, options = {}) {
717
719
  const unsettledSignals = [];
718
720
  const maxPasses = Math.max(1, options.maxPasses ?? MAX_DRAIN_PASSES);
719
721
  for (let pass = 0; pass < maxPasses; pass++) {
720
- const rows = getQueryRows(await db.query(`SELECT * FROM ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}()`));
722
+ const p = placeholders(db);
723
+ const rows = getQueryRows(await db.query(`SELECT * FROM ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(${POSTGRES_CHANGE_FEED_DRAIN_BATCH}, ARRAY(SELECT jsonb_array_elements_text(${p(1)}::jsonb)))`, JSON.stringify(getChangeFeedSensitiveTables())));
721
724
  const failure = rows.find((row) => row.error_code != null);
722
725
  if (failure) {
723
726
  const error = new Error(String(failure.error_message || "PostgreSQL change-feed drain failed"));