@warlock.js/cascade 5.19.1 → 5.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/cjs/index.cjs +2356 -1039
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/context/database-transaction-context.d.mts +10 -0
  5. package/esm/context/database-transaction-context.d.mts.map +1 -1
  6. package/esm/context/database-transaction-context.mjs +17 -0
  7. package/esm/context/database-transaction-context.mjs.map +1 -1
  8. package/esm/contracts/query-builder.contract.d.mts +30 -3
  9. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  10. package/esm/data-source/data-source-registry.d.mts +33 -1
  11. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  12. package/esm/data-source/data-source-registry.mjs +75 -5
  13. package/esm/data-source/data-source-registry.mjs.map +1 -1
  14. package/esm/database-dirty-tracker.d.mts +5 -0
  15. package/esm/database-dirty-tracker.d.mts.map +1 -1
  16. package/esm/database-dirty-tracker.mjs +16 -0
  17. package/esm/database-dirty-tracker.mjs.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-driver.d.mts +12 -0
  19. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  20. package/esm/drivers/mongodb/mongodb-driver.mjs +58 -19
  21. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  22. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
  23. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  24. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +15 -18
  25. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  26. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +13 -3
  27. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  28. package/esm/drivers/mongodb/mongodb-query-builder.mjs +72 -19
  29. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  30. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +6 -0
  31. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  32. package/esm/drivers/mongodb/mongodb-query-parser.mjs +128 -30
  33. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  34. package/esm/drivers/postgres/index.d.mts +2 -3
  35. package/esm/drivers/postgres/index.mjs +1 -2
  36. package/esm/drivers/postgres/postgres-blueprint.mjs +4 -4
  37. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  38. package/esm/drivers/postgres/postgres-dialect.d.mts +5 -1
  39. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  40. package/esm/drivers/postgres/postgres-dialect.mjs +38 -8
  41. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  42. package/esm/drivers/postgres/postgres-driver.d.mts +52 -29
  43. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  44. package/esm/drivers/postgres/postgres-driver.mjs +196 -58
  45. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  46. package/esm/drivers/postgres/postgres-migration-driver.d.mts +2 -1
  47. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  48. package/esm/drivers/postgres/postgres-migration-driver.mjs +8 -6
  49. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  50. package/esm/drivers/postgres/postgres-query-builder.d.mts +18 -7
  51. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  52. package/esm/drivers/postgres/postgres-query-builder.mjs +146 -31
  53. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  54. package/esm/drivers/postgres/postgres-query-parser.d.mts +23 -1
  55. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  56. package/esm/drivers/postgres/postgres-query-parser.mjs +160 -43
  57. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  58. package/esm/drivers/postgres/postgres-sql-serializer.mjs +3 -2
  59. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  60. package/esm/drivers/postgres/postgres-type-converters.mjs +57 -0
  61. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -0
  62. package/esm/events/model-events.d.mts +1 -1
  63. package/esm/events/model-events.d.mts.map +1 -1
  64. package/esm/events/model-events.mjs.map +1 -1
  65. package/esm/index.d.mts +6 -6
  66. package/esm/index.mjs +5 -5
  67. package/esm/migration/column-builder.d.mts.map +1 -1
  68. package/esm/migration/column-builder.mjs +2 -2
  69. package/esm/migration/column-builder.mjs.map +1 -1
  70. package/esm/migration/index.d.mts +2 -2
  71. package/esm/migration/index.mjs +1 -1
  72. package/esm/migration/migration-order.mjs +5 -2
  73. package/esm/migration/migration-order.mjs.map +1 -1
  74. package/esm/migration/migration-runner.d.mts +50 -5
  75. package/esm/migration/migration-runner.d.mts.map +1 -1
  76. package/esm/migration/migration-runner.mjs +370 -147
  77. package/esm/migration/migration-runner.mjs.map +1 -1
  78. package/esm/migration/migration.d.mts +36 -3
  79. package/esm/migration/migration.d.mts.map +1 -1
  80. package/esm/migration/migration.mjs +62 -5
  81. package/esm/migration/migration.mjs.map +1 -1
  82. package/esm/model/methods/accessor-methods.mjs +1 -3
  83. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  84. package/esm/model/methods/delete-methods.mjs +6 -2
  85. package/esm/model/methods/delete-methods.mjs.map +1 -1
  86. package/esm/model/methods/hydration-methods.mjs +16 -2
  87. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  88. package/esm/model/methods/instance-event-methods.mjs +2 -1
  89. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  90. package/esm/model/methods/meta-methods.mjs +53 -16
  91. package/esm/model/methods/meta-methods.mjs.map +1 -1
  92. package/esm/model/methods/query-methods.mjs +86 -15
  93. package/esm/model/methods/query-methods.mjs.map +1 -1
  94. package/esm/model/methods/static-event-methods.mjs +16 -1
  95. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  96. package/esm/model/methods/write-methods.d.mts.map +1 -1
  97. package/esm/model/methods/write-methods.mjs +50 -50
  98. package/esm/model/methods/write-methods.mjs.map +1 -1
  99. package/esm/model/model.d.mts +16 -1
  100. package/esm/model/model.d.mts.map +1 -1
  101. package/esm/model/model.mjs +39 -5
  102. package/esm/model/model.mjs.map +1 -1
  103. package/esm/model/register-model.d.mts +6 -1
  104. package/esm/model/register-model.d.mts.map +1 -1
  105. package/esm/model/register-model.mjs +16 -1
  106. package/esm/model/register-model.mjs.map +1 -1
  107. package/esm/query-builder/query-builder.d.mts +21 -2
  108. package/esm/query-builder/query-builder.d.mts.map +1 -1
  109. package/esm/query-builder/query-builder.mjs +97 -20
  110. package/esm/query-builder/query-builder.mjs.map +1 -1
  111. package/esm/relations/key-conventions.mjs +13 -3
  112. package/esm/relations/key-conventions.mjs.map +1 -1
  113. package/esm/relations/pivot-operations.mjs +1 -1
  114. package/esm/relations/relation-loader.d.mts +15 -5
  115. package/esm/relations/relation-loader.d.mts.map +1 -1
  116. package/esm/relations/relation-loader.mjs +51 -16
  117. package/esm/relations/relation-loader.mjs.map +1 -1
  118. package/esm/remover/database-remover.d.mts.map +1 -1
  119. package/esm/remover/database-remover.mjs +21 -7
  120. package/esm/remover/database-remover.mjs.map +1 -1
  121. package/esm/restorer/database-restorer.d.mts +12 -2
  122. package/esm/restorer/database-restorer.d.mts.map +1 -1
  123. package/esm/restorer/database-restorer.mjs +43 -13
  124. package/esm/restorer/database-restorer.mjs.map +1 -1
  125. package/esm/sync/model-events.mjs +63 -3
  126. package/esm/sync/model-events.mjs.map +1 -1
  127. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  128. package/esm/sync/model-sync-operation.mjs +3 -2
  129. package/esm/sync/model-sync-operation.mjs.map +1 -1
  130. package/esm/sync/sync-manager.d.mts +3 -42
  131. package/esm/sync/sync-manager.d.mts.map +1 -1
  132. package/esm/sync/sync-manager.mjs +37 -126
  133. package/esm/sync/sync-manager.mjs.map +1 -1
  134. package/esm/transactions/after-commit.d.mts +24 -0
  135. package/esm/transactions/after-commit.d.mts.map +1 -0
  136. package/esm/transactions/after-commit.mjs +47 -0
  137. package/esm/transactions/after-commit.mjs.map +1 -0
  138. package/esm/utils/connect-to-database.d.mts.map +1 -1
  139. package/esm/utils/connect-to-database.mjs +4 -4
  140. package/esm/utils/connect-to-database.mjs.map +1 -1
  141. package/esm/utils/database-writer.utils.d.mts +1 -1
  142. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  143. package/esm/utils/database-writer.utils.mjs +1 -0
  144. package/esm/utils/database-writer.utils.mjs.map +1 -1
  145. package/esm/utils/define-model.d.mts.map +1 -1
  146. package/esm/utils/define-model.mjs +2 -10
  147. package/esm/utils/define-model.mjs.map +1 -1
  148. package/esm/utils/is-valid-date-value.mjs +3 -2
  149. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  150. package/esm/utils/once-connected.d.mts.map +1 -1
  151. package/esm/utils/once-connected.mjs +12 -2
  152. package/esm/utils/once-connected.mjs.map +1 -1
  153. package/esm/validation/mutators/embed-mutator.mjs +19 -11
  154. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  155. package/esm/validation/plugins/embed-validator-plugin.d.mts +2 -2
  156. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  157. package/esm/validation/plugins/embed-validator-plugin.mjs +8 -2
  158. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  159. package/esm/validation/rules/database-model-rule.mjs +6 -5
  160. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  161. package/esm/validation/rules/exists-rule.mjs +6 -4
  162. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  163. package/esm/validation/rules/unique-rule.mjs +5 -2
  164. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  165. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  166. package/esm/validation/validators/embed-validator.mjs +1 -1
  167. package/esm/validation/validators/embed-validator.mjs.map +1 -1
  168. package/esm/writer/database-writer.d.mts +27 -0
  169. package/esm/writer/database-writer.d.mts.map +1 -1
  170. package/esm/writer/database-writer.mjs +84 -17
  171. package/esm/writer/database-writer.mjs.map +1 -1
  172. package/llms-full.txt +2783 -2766
  173. package/llms.txt +15 -15
  174. package/package.json +6 -6
  175. package/skills/aggregate-data/SKILL.md +165 -165
  176. package/skills/alter-migration/SKILL.md +531 -531
  177. package/skills/configure-delete-strategy/SKILL.md +130 -130
  178. package/skills/define-model/SKILL.md +205 -205
  179. package/skills/define-relations/SKILL.md +171 -171
  180. package/skills/index.md +23 -0
  181. package/skills/manage-data-sources/SKILL.md +140 -140
  182. package/skills/manage-transactions/SKILL.md +208 -191
  183. package/skills/paginate-results/SKILL.md +122 -122
  184. package/skills/perform-atomic-ops/SKILL.md +156 -156
  185. package/skills/query-data/SKILL.md +213 -213
  186. package/skills/run-cascade-cli/SKILL.md +125 -125
  187. package/skills/search-by-vector/SKILL.md +127 -127
  188. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  189. package/skills/track-changes/SKILL.md +127 -127
  190. package/skills/write-migration/SKILL.md +206 -206
  191. package/esm/drivers/postgres/postgres-sync-adapter.d.mts +0 -79
  192. package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +0 -1
  193. package/esm/drivers/postgres/postgres-sync-adapter.mjs +0 -162
  194. package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +0 -1
@@ -1,17 +1,17 @@
1
1
  import { databaseTransactionContext } from "../../context/database-transaction-context.mjs";
2
+ import { flushAfterCommit } from "../../transactions/after-commit.mjs";
2
3
  import { TransactionRollbackError } from "../../errors/transaction-rollback.error.mjs";
3
4
  import { UnsupportedUpdateOperationError } from "../../errors/unsupported-update-operation.error.mjs";
4
- import { isValidDateValue } from "../../utils/is-valid-date-value.mjs";
5
5
  import { PostgresBlueprint } from "./postgres-blueprint.mjs";
6
6
  import { PostgresDialect } from "./postgres-dialect.mjs";
7
7
  import { SqlDatabaseDirtyTracker } from "../../sql-database-dirty-tracker.mjs";
8
+ import { convertPostgresValue } from "./postgres-type-converters.mjs";
8
9
  import { PostgresMigrationDriver } from "./postgres-migration-driver.mjs";
9
10
  import { PostgresQueryBuilder } from "./postgres-query-builder.mjs";
10
11
  import { PostgresSQLSerializer } from "./postgres-sql-serializer.mjs";
11
- import { PostgresSyncAdapter } from "./postgres-sync-adapter.mjs";
12
12
  import { assertPostgresUpdate } from "./postgres-update-validator.mjs";
13
- import { colors } from "@mongez/copper";
14
13
  import { log } from "@warlock.js/logger";
14
+ import { colors } from "@mongez/copper";
15
15
 
16
16
  //#region ../cascade/src/drivers/postgres/postgres-driver.ts
17
17
  /**
@@ -54,6 +54,7 @@ function buildPostgresPoolConfig(config) {
54
54
  idleTimeoutMillis: config.idleTimeoutMillis ?? 3e4,
55
55
  connectionTimeoutMillis: config.connectionTimeoutMillis ?? 1e4,
56
56
  keepAlive: config.keepAlive ?? true,
57
+ maxUses: config.maxUses,
57
58
  application_name: config.application_name ?? "cascade",
58
59
  ssl: config.ssl
59
60
  };
@@ -78,6 +79,10 @@ async function loadPg() {
78
79
  }
79
80
  }
80
81
  /**
82
+ * Transaction session -> the driver (data source) that opened it.
83
+ */
84
+ const transactionOwners = /* @__PURE__ */ new WeakMap();
85
+ /**
81
86
  * PostgreSQL database driver implementing the Cascade DriverContract.
82
87
  *
83
88
  * Provides connection pooling, CRUD operations, transactions, and
@@ -106,6 +111,11 @@ async function loadPg() {
106
111
  * await driver.disconnect();
107
112
  * ```
108
113
  */
114
+ /** Summarise bound parameters as count and types, never values. */
115
+ function describeParams(params) {
116
+ const types = params.map((param) => param === null ? "null" : typeof param);
117
+ return `Params: ${params.length} (${types.join(", ")})`;
118
+ }
109
119
  var PostgresDriver = class {
110
120
  config;
111
121
  /**
@@ -168,6 +178,8 @@ var PostgresDriver = class {
168
178
  * @see PostgresPoolConfig.nativeArrayColumns
169
179
  */
170
180
  _nativeArrayColumns;
181
+ /** Column `dataTypeID`s per result row, filled by `query()` for `deserialize()`. */
182
+ rowColumnTypes = /* @__PURE__ */ new WeakMap();
171
183
  /** Unique keys per table, resolved from `pg_index` for upsert conflict targets. */
172
184
  _uniqueKeys = /* @__PURE__ */ new Map();
173
185
  /**
@@ -342,6 +354,16 @@ var PostgresDriver = class {
342
354
  return this._nativeArrayColumns.has(column);
343
355
  }
344
356
  /**
357
+ * Drop the cached unique keys and re-read the native-array columns.
358
+ *
359
+ * Called by the migration driver after DDL so an in-process migration is
360
+ * visible to the serializer and to upserts without reconnecting.
361
+ */
362
+ async invalidateSchemaCaches() {
363
+ this._uniqueKeys.clear();
364
+ await this.loadNativeArrayColumns();
365
+ }
366
+ /**
345
367
  * Introspect the live schema for native-array columns so array values bind
346
368
  * correctly with zero app configuration.
347
369
  *
@@ -393,26 +415,11 @@ var PostgresDriver = class {
393
415
  * @returns Deserialized JavaScript object
394
416
  */
395
417
  deserialize(data) {
418
+ const columnTypes = this.rowColumnTypes.get(data);
419
+ if (!columnTypes) return data;
396
420
  for (const [key, value] of Object.entries(data)) {
397
421
  if (typeof value !== "string") continue;
398
- if (isValidDateValue(value)) {
399
- data[key] = new Date(value);
400
- continue;
401
- }
402
- if (value.charCodeAt(0) === 91 && value.charCodeAt(value.length - 1) === 93) {
403
- const parts = value.slice(1, -1).split(",");
404
- const nums = new Array(parts.length);
405
- let isNumericVector = parts.length > 0;
406
- for (let i = 0; i < parts.length; i++) {
407
- const n = +(parts[i] ?? "");
408
- if (!Number.isFinite(n)) {
409
- isNumericVector = false;
410
- break;
411
- }
412
- nums[i] = n;
413
- }
414
- if (isNumericVector) data[key] = nums;
415
- }
422
+ data[key] = convertPostgresValue(value, columnTypes.get(key));
416
423
  }
417
424
  return data;
418
425
  }
@@ -489,7 +496,15 @@ var PostgresDriver = class {
489
496
  try {
490
497
  return { modifiedCount: (await this.query(sql, params)).rowCount ?? 0 };
491
498
  } catch (error) {
492
- console.log("PG Query Error in:", sql, params);
499
+ log.error({
500
+ module: "database.postgres",
501
+ action: "update.error",
502
+ message: `${sql} | ${describeParams(params)}`,
503
+ context: {
504
+ sql,
505
+ paramCount: params.length
506
+ }
507
+ });
493
508
  throw error;
494
509
  }
495
510
  }
@@ -666,7 +681,8 @@ var PostgresDriver = class {
666
681
  const conflictColumns = options?.conflictColumns ?? Object.keys(filter);
667
682
  if (conflictColumns.length === 0) throw new Error("Upsert requires conflictColumns option or filter with columns");
668
683
  const quotedConflictColumns = conflictColumns.map((c) => this.dialect.quoteIdentifier(c)).join(", ");
669
- const setClauses = columns.filter((col) => !conflictColumns.includes(col)).map((col, i) => {
684
+ const createdAtColumn = options?.createdAtColumn ?? this.modelDefaults.createdAtColumn;
685
+ const setClauses = columns.filter((col) => !conflictColumns.includes(col) && col !== createdAtColumn).map((col, i) => {
670
686
  const valueIndex = columns.indexOf(col) + 1;
671
687
  return `${this.dialect.quoteIdentifier(col)} = ${this.dialect.placeholder(valueIndex)}`;
672
688
  }).join(", ");
@@ -684,7 +700,7 @@ var PostgresDriver = class {
684
700
  async findOneAndDelete(table, filter, _options) {
685
701
  const quotedTable = this.dialect.quoteIdentifier(table);
686
702
  const { whereClause, whereParams } = this.buildWhereClause(filter, 1);
687
- const sql = `DELETE FROM ${quotedTable} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1) RETURNING *`;
703
+ const sql = `${this.singleRowTarget("DELETE FROM " + quotedTable, quotedTable, filter, whereClause)} RETURNING *`;
688
704
  const result = await this.query(sql, whereParams);
689
705
  return result.rows[0] ? result.rows[0] : null;
690
706
  }
@@ -699,7 +715,7 @@ var PostgresDriver = class {
699
715
  async delete(table, filter, _options) {
700
716
  const quotedTable = this.dialect.quoteIdentifier(table);
701
717
  const { whereClause, whereParams } = this.buildWhereClause(filter ?? {}, 1);
702
- const sql = `DELETE FROM ${quotedTable} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1)`;
718
+ const sql = this.singleRowTarget("DELETE FROM " + quotedTable, quotedTable, filter ?? {}, whereClause);
703
719
  return (await this.query(sql, whereParams)).rowCount ?? 0;
704
720
  }
705
721
  /**
@@ -800,27 +816,34 @@ var PostgresDriver = class {
800
816
  const ctx = { rollback(reason) {
801
817
  throw new TransactionRollbackError(reason);
802
818
  } };
803
- if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
819
+ if (this.canJoinActiveTransaction()) return fn(ctx);
804
820
  const tx = await this.beginTransaction(options);
805
- databaseTransactionContext.enter({ session: tx.context });
821
+ transactionOwners.set(tx.context, this);
806
822
  let commitStarted = false;
807
- try {
808
- const result = await fn(ctx);
809
- commitStarted = true;
810
- await tx.commit();
811
- return result;
812
- } catch (error) {
813
- if (commitStarted) log.error(`database.postgress`, "transaction", "Transaction COMMIT failed; client discarded, no rollback issued");
814
- else try {
815
- await tx.rollback();
816
- log.error(`database.postgress`, "transaction", "Transaction operation failed, rolled back everything");
817
- } catch (rollbackError) {
818
- log.error(`database.postgress`, "transaction", rollbackError);
823
+ let committedCallbacks = [];
824
+ let result;
825
+ await databaseTransactionContext.run({
826
+ session: tx.context,
827
+ afterCommit: []
828
+ }, async () => {
829
+ try {
830
+ result = await fn(ctx);
831
+ commitStarted = true;
832
+ await tx.commit();
833
+ committedCallbacks = databaseTransactionContext.takeAfterCommit();
834
+ } catch (error) {
835
+ if (commitStarted) log.error(`database.postgress`, "transaction", "Transaction COMMIT failed; client discarded, no rollback issued");
836
+ else try {
837
+ await tx.rollback();
838
+ log.error(`database.postgress`, "transaction", "Transaction operation failed, rolled back everything");
839
+ } catch (rollbackError) {
840
+ log.error(`database.postgress`, "transaction", rollbackError);
841
+ }
842
+ throw error;
819
843
  }
820
- throw error;
821
- } finally {
822
- databaseTransactionContext.exit();
823
- }
844
+ });
845
+ await flushAfterCommit(committedCallbacks);
846
+ return result;
824
847
  }
825
848
  /**
826
849
  * Perform an atomic update operation.
@@ -851,13 +874,94 @@ var PostgresDriver = class {
851
874
  /**
852
875
  * Get the sync adapter for bulk denormalized updates.
853
876
  *
877
+ * Sync instructions arrive in the portable Mongo shape (`$set` with a `field.$`
878
+ * positional key, `$pull`, `$unset`, dotted filter keys), so they are translated
879
+ * to jsonb SQL here instead of being handed to `updateMany`, which would quote
880
+ * `"tags.$"` as a column name.
881
+ *
854
882
  * @returns Sync adapter instance
855
883
  */
856
884
  syncAdapter() {
857
- if (!this._syncAdapter) this._syncAdapter = new PostgresSyncAdapter(this);
885
+ if (!this._syncAdapter) {
886
+ const executeOne = (instruction) => this.executeSyncInstruction(instruction);
887
+ this._syncAdapter = {
888
+ executeOne,
889
+ executeArrayUpdate: executeOne,
890
+ executeBatch: async (instructions) => {
891
+ let affected = 0;
892
+ for (const instruction of instructions) affected += await executeOne(instruction);
893
+ return affected;
894
+ }
895
+ };
896
+ }
858
897
  return this._syncAdapter;
859
898
  }
860
899
  /**
900
+ * Execute one sync instruction as a jsonb UPDATE.
901
+ *
902
+ * @param instruction - Sync instruction in the portable Mongo shape
903
+ * @returns Number of affected rows
904
+ */
905
+ async executeSyncInstruction(instruction) {
906
+ const { sql, params } = this.buildSyncStatement(instruction);
907
+ return (await this.query(sql, params)).rowCount ?? 0;
908
+ }
909
+ /**
910
+ * Translate a sync instruction to a jsonb UPDATE statement.
911
+ *
912
+ * @param instruction - Sync instruction
913
+ * @returns SQL and parameters
914
+ * @throws Error for update operators a sync cannot express
915
+ */
916
+ buildSyncStatement(instruction) {
917
+ const params = [];
918
+ const bind = (value) => {
919
+ params.push(value);
920
+ return this.dialect.placeholder(params.length);
921
+ };
922
+ const quote = (name) => this.dialect.quoteIdentifier(name);
923
+ const elementMatch = (idField, idValue, operator) => `(e->>${bind(idField)}::text) ${operator} ${bind(String(idValue))}::text`;
924
+ const clauses = [];
925
+ for (const [key, value] of Object.entries(instruction.update)) if (key === "$set") for (const [path, embed] of Object.entries(value)) if (path.endsWith(".$")) {
926
+ const column = quote(path.slice(0, -2));
927
+ const { identifierField, identifierValue } = instruction;
928
+ if (!identifierField || identifierValue === void 0) throw new Error(`Positional update on "${path}" requires an identifier field/value.`);
929
+ const match = elementMatch(identifierField, identifierValue, "=");
930
+ clauses.push(`${column} = (SELECT COALESCE(jsonb_agg(CASE WHEN ${match} THEN ${bind(JSON.stringify(embed))}::jsonb ELSE e END), '[]'::jsonb) FROM jsonb_array_elements(${column}::jsonb) e)`);
931
+ } else if (path.includes(".")) {
932
+ const [head, ...rest] = path.split(".");
933
+ const column = quote(head);
934
+ clauses.push(`${column} = jsonb_set(COALESCE(${column}::jsonb, '{}'::jsonb), ${bind(rest)}::text[], ${bind(JSON.stringify(embed))}::jsonb)`);
935
+ } else clauses.push(`${quote(path)} = ${bind(JSON.stringify(embed))}::jsonb`);
936
+ else if (key === "$pull") for (const [field, criteria] of Object.entries(value)) {
937
+ const column = quote(field);
938
+ const entries = Object.entries(criteria);
939
+ const [idField, idValue] = entries[0] ?? [];
940
+ if (entries.length !== 1 || !idField) throw new Error(`$pull on "${field}" must match a single identifier field.`);
941
+ const match = elementMatch(idField, idValue, "IS DISTINCT FROM");
942
+ clauses.push(`${column} = (SELECT COALESCE(jsonb_agg(e), '[]'::jsonb) FROM jsonb_array_elements(${column}::jsonb) e WHERE ${match})`);
943
+ }
944
+ else if (key === "$unset") for (const field of Object.keys(value)) clauses.push(`${quote(field)} = NULL`);
945
+ else throw new Error(`Unsupported sync update operator "${key}" on the Postgres driver.`);
946
+ if (clauses.length === 0) throw new Error("No update operations specified for the sync instruction.");
947
+ const conditions = [];
948
+ for (const [key, value] of Object.entries(instruction.filter)) {
949
+ if (!key.includes(".")) {
950
+ conditions.push(`${quote(key)} = ${bind(value)}`);
951
+ continue;
952
+ }
953
+ const [head, ...rest] = key.split(".");
954
+ const column = quote(head);
955
+ const path = bind(rest);
956
+ conditions.push(instruction.isArrayUpdate && head === instruction.arrayField ? `EXISTS (SELECT 1 FROM jsonb_array_elements(${column}::jsonb) e WHERE (e #>> ${path}::text[]) = ${bind(String(value))}::text)` : `(${column}::jsonb #>> ${path}::text[]) = ${bind(String(value))}::text`);
957
+ }
958
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
959
+ return {
960
+ sql: `UPDATE ${quote(instruction.targetTable)} SET ${clauses.join(", ")}${where}`,
961
+ params
962
+ };
963
+ }
964
+ /**
861
965
  * Get the migration driver for schema operations.
862
966
  *
863
967
  * @returns Migration driver instance
@@ -874,6 +978,25 @@ var PostgresDriver = class {
874
978
  return new PostgresSQLSerializer(this.dialect);
875
979
  }
876
980
  /**
981
+ * Whether an active transaction can be joined: one exists and it is not
982
+ * owned by a different data source (sessions opened outside `transaction()`
983
+ * carry no owner tag and are joinable).
984
+ */
985
+ canJoinActiveTransaction() {
986
+ const session = databaseTransactionContext.getSession();
987
+ if (!session) return false;
988
+ const owner = transactionOwners.get(session);
989
+ return owner === void 0 || owner === this;
990
+ }
991
+ /**
992
+ * The ambient transaction session, only when it belongs to this driver's
993
+ * data source.
994
+ */
995
+ getOwnSession() {
996
+ const session = databaseTransactionContext.getSession();
997
+ return session && transactionOwners.get(session) === this ? session : void 0;
998
+ }
999
+ /**
877
1000
  * Execute a raw SQL query.
878
1001
  *
879
1002
  * Automatically uses the transaction client if one is active.
@@ -882,15 +1005,15 @@ var PostgresDriver = class {
882
1005
  * @param params - Query parameters
883
1006
  * @returns Query result
884
1007
  */
1008
+ recordColumnTypes(result) {
1009
+ if (!result.fields?.length || !result.rows?.length) return;
1010
+ const columnTypes = new Map(result.fields.map((field) => [field.name, field.dataTypeID]));
1011
+ for (const row of result.rows) if (row && typeof row === "object") this.rowColumnTypes.set(row, columnTypes);
1012
+ }
885
1013
  async query(sql, params = []) {
886
- const txClient = databaseTransactionContext.getSession();
1014
+ const txClient = this.getOwnSession();
887
1015
  const startTime = this.config.logging ? performance.now() : 0;
888
- let paramsString = "";
889
- if (this.config.logging && params.length > 0) {
890
- paramsString = JSON.stringify(params);
891
- if (paramsString.length > 300) paramsString = paramsString.substring(0, 300) + "...";
892
- paramsString = ` | Params: ${paramsString}`;
893
- }
1016
+ const paramsString = this.config.logging && params.length > 0 ? ` | ${describeParams(params)}` : "";
894
1017
  try {
895
1018
  let result;
896
1019
  if (this.config.logging) log.info({
@@ -898,8 +1021,8 @@ var PostgresDriver = class {
898
1021
  action: "query.executing",
899
1022
  message: `${sql}${paramsString}`,
900
1023
  context: {
901
- params,
902
- sql
1024
+ sql,
1025
+ paramCount: params.length
903
1026
  }
904
1027
  });
905
1028
  if (txClient) result = await txClient.query(sql, params);
@@ -911,12 +1034,13 @@ var PostgresDriver = class {
911
1034
  action: "query.executed",
912
1035
  message: `[${duration}ms] ${sql}${paramsString}`,
913
1036
  context: {
914
- params,
915
1037
  sql,
1038
+ paramCount: params.length,
916
1039
  duration
917
1040
  }
918
1041
  });
919
1042
  }
1043
+ this.recordColumnTypes(result);
920
1044
  return result;
921
1045
  } catch (error) {
922
1046
  if (this.config.logging) {
@@ -927,7 +1051,7 @@ var PostgresDriver = class {
927
1051
  message: `[${duration}ms] ${sql}${paramsString}`,
928
1052
  context: {
929
1053
  sql,
930
- params,
1054
+ paramCount: params.length,
931
1055
  error: error instanceof Error ? error.message : String(error)
932
1056
  }
933
1057
  });
@@ -1079,14 +1203,28 @@ var PostgresDriver = class {
1079
1203
  const quotedTable = this.dialect.quoteIdentifier(table);
1080
1204
  const { whereClause, whereParams } = this.buildWhereClause(filter, set.nextIndex);
1081
1205
  params.push(...whereParams);
1082
- let sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} ${whereClause}`;
1083
- if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1)`;
1206
+ const head = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")}`;
1084
1207
  return {
1085
- sql,
1208
+ sql: limit === 1 && whereClause ? this.singleRowTarget(head, quotedTable, filter, whereClause) : `${head} ${whereClause}`,
1086
1209
  params
1087
1210
  };
1088
1211
  }
1089
1212
  /**
1213
+ * Build the `<head> WHERE ...` statement for a single-row UPDATE/DELETE.
1214
+ *
1215
+ * A filter pinned to the primary key (`id = <value>`) already matches at most
1216
+ * one row, so it is used as-is: a ctid subquery would re-check a stale tuple
1217
+ * after waiting on a concurrent write and silently update 0 rows.
1218
+ * Without a PK, the ctid subquery locks its candidate with
1219
+ * `FOR UPDATE SKIP LOCKED` so concurrent callers claim different rows
1220
+ * instead of losing the write.
1221
+ */
1222
+ singleRowTarget(head, quotedTable, filter, whereClause) {
1223
+ const id = filter.id;
1224
+ if (id !== void 0 && id !== null && (typeof id !== "object" || id instanceof Date)) return `${head} ${whereClause}`;
1225
+ return `${head} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1 FOR UPDATE SKIP LOCKED)`;
1226
+ }
1227
+ /**
1090
1228
  * Create a new database.
1091
1229
  *
1092
1230
  * Note: This requires connecting to a system database (like 'postgres')