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