@warlock.js/cascade 5.12.0 → 5.14.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 (68) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/cjs/index.cjs +685 -128
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/database-driver.contract.d.mts +44 -7
  5. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  6. package/esm/contracts/index.d.mts +2 -2
  7. package/esm/contracts/query-builder.contract.d.mts +61 -1
  8. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  9. package/esm/drivers/mongodb/mongodb-driver.d.mts +8 -4
  10. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-driver.mjs +16 -6
  12. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +32 -7
  14. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-query-builder.mjs +56 -8
  16. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +39 -12
  18. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  19. package/esm/drivers/mongodb/mongodb-query-parser.mjs +143 -55
  20. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  21. package/esm/drivers/mongodb/mongodb-update-translator.mjs +25 -0
  22. package/esm/drivers/mongodb/mongodb-update-translator.mjs.map +1 -0
  23. package/esm/drivers/mongodb/pipeline-stage-object.mjs +24 -0
  24. package/esm/drivers/mongodb/pipeline-stage-object.mjs.map +1 -0
  25. package/esm/drivers/mongodb/types.d.mts +7 -1
  26. package/esm/drivers/mongodb/types.d.mts.map +1 -1
  27. package/esm/drivers/postgres/postgres-driver.d.mts +52 -9
  28. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  29. package/esm/drivers/postgres/postgres-driver.mjs +170 -38
  30. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  31. package/esm/drivers/postgres/postgres-query-builder.d.mts +17 -1
  32. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  33. package/esm/drivers/postgres/postgres-query-builder.mjs +31 -0
  34. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  35. package/esm/drivers/postgres/postgres-update-validator.mjs +36 -0
  36. package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -0
  37. package/esm/errors/unsupported-lean-operation.error.d.mts +25 -0
  38. package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -0
  39. package/esm/errors/unsupported-lean-operation.error.mjs +31 -0
  40. package/esm/errors/unsupported-lean-operation.error.mjs.map +1 -0
  41. package/esm/errors/unsupported-query-operation.error.d.mts +30 -0
  42. package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -0
  43. package/esm/errors/unsupported-query-operation.error.mjs +37 -0
  44. package/esm/errors/unsupported-query-operation.error.mjs.map +1 -0
  45. package/esm/errors/unsupported-update-operation.error.d.mts +30 -0
  46. package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -0
  47. package/esm/errors/unsupported-update-operation.error.mjs +37 -0
  48. package/esm/errors/unsupported-update-operation.error.mjs.map +1 -0
  49. package/esm/index.d.mts +6 -3
  50. package/esm/index.mjs +4 -1
  51. package/esm/model/methods/query-methods.mjs +22 -6
  52. package/esm/model/methods/query-methods.mjs.map +1 -1
  53. package/esm/model/model.d.mts +28 -11
  54. package/esm/model/model.d.mts.map +1 -1
  55. package/esm/model/model.mjs +30 -13
  56. package/esm/model/model.mjs.map +1 -1
  57. package/esm/query-builder/lean-records.mjs +34 -0
  58. package/esm/query-builder/lean-records.mjs.map +1 -0
  59. package/esm/query-builder/query-builder.d.mts +13 -1
  60. package/esm/query-builder/query-builder.d.mts.map +1 -1
  61. package/esm/query-builder/query-builder.mjs +16 -0
  62. package/esm/query-builder/query-builder.mjs.map +1 -1
  63. package/llms-full.txt +72 -5
  64. package/llms.txt +3 -3
  65. package/package.json +4 -4
  66. package/skills/aggregate-data/SKILL.md +24 -1
  67. package/skills/perform-atomic-ops/SKILL.md +38 -3
  68. package/skills/query-data/SKILL.md +10 -1
@@ -5,7 +5,7 @@ import { DatabaseDriver } from "../../utils/connect-to-database.mjs";
5
5
  import { SQLSerializer } from "../../migration/sql-serializer.mjs";
6
6
  import { QueryBuilderContract } from "../../contracts/query-builder.contract.mjs";
7
7
  import { SyncAdapterContract } from "../../contracts/sync-adapter.contract.mjs";
8
- import { CreateDatabaseOptions, DriverContract, DriverEventListener, DriverTransactionContract, DropDatabaseOptions, InsertResult, TransactionContext, UpdateOperations, UpdateResult } from "../../contracts/database-driver.contract.mjs";
8
+ import { AtomicUpdate, CreateDatabaseOptions, DriverAtomicUpdateOptions, DriverContract, DriverEventListener, DriverTransactionContract, DropDatabaseOptions, InsertResult, TransactionContext, UpdateOperations, UpdateResult } from "../../contracts/database-driver.contract.mjs";
9
9
  import { SqlDatabaseDirtyTracker } from "../../sql-database-dirty-tracker.mjs";
10
10
  import { PostgresDialect } from "./postgres-dialect.mjs";
11
11
  import { PostgresPoolConfig, PostgresQueryResult, PostgresTransactionOptions } from "./types.mjs";
@@ -115,6 +115,8 @@ declare class PostgresDriver implements DriverContract {
115
115
  * @see PostgresPoolConfig.nativeArrayColumns
116
116
  */
117
117
  private readonly _nativeArrayColumns;
118
+ /** Unique keys per table, resolved from `pg_index` for upsert conflict targets. */
119
+ private readonly _uniqueKeys;
118
120
  /**
119
121
  * Native-array columns discovered by introspecting the live schema on
120
122
  * connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
@@ -289,14 +291,44 @@ declare class PostgresDriver implements DriverContract {
289
291
  */
290
292
  update(table: string, filter: Record<string, unknown>, update: UpdateOperations, _options?: Record<string, unknown>): Promise<UpdateResult>;
291
293
  /**
292
- * Find one and update a single row matching the filter and return the updated row
293
- * @param table - Target table name
294
- * @param filter - Filter conditions
295
- * @param update - Update operations ($set, $unset, $inc)
296
- * @param options - Optional update options
297
- * @returns The updated row or null
294
+ * Find one row matching the filter, update it, and return it.
295
+ *
296
+ * - `upsert`: `INSERT ON CONFLICT (target) DO UPDATE … RETURNING *`, where
297
+ * the target is the primary key or a unique index whose columns are all
298
+ * equality keys of the filter (see {@link executeUpsert}).
299
+ * - `returnDocument` (default `"after"`): `"before"` returns the row as it
300
+ * was, and is not available together with `upsert`.
301
+ *
302
+ * The row is chosen with `SELECT … LIMIT 1 FOR UPDATE` and matched by primary
303
+ * key with the filter repeated, so concurrent callers re-check the filter
304
+ * against the latest committed row instead of overshooting it.
305
+ *
306
+ * @throws UnsupportedUpdateOperationError for pipeline updates, `arrayFilters`,
307
+ * array operators, `before` + `upsert`, or an underivable conflict target
308
+ */
309
+ findOneAndUpdate<T = unknown>(table: string, filter: Record<string, unknown>, update: AtomicUpdate, options?: DriverAtomicUpdateOptions): Promise<T | null>;
310
+ /**
311
+ * Upsert through `INSERT … ON CONFLICT (target) DO UPDATE SET … [WHERE rest] RETURNING *`.
312
+ *
313
+ * Conflict target rule: the first unique index of the table (primary key
314
+ * first; partial and expression indexes excluded) whose columns are ALL
315
+ * equality keys of the filter — a non-null value that is not an operator
316
+ * object. Any other filter predicate becomes the `DO UPDATE … WHERE`, so an
317
+ * existing row that fails it is left untouched and no row is returned.
318
+ *
319
+ * Inserted row: filter equality values, then `$setOnInsert`, then `$set`,
320
+ * then `$inc` (n) / `$dec` (-n); `$unset` columns are omitted.
321
+ *
322
+ * @throws UnsupportedUpdateOperationError when no conflict target can be derived
298
323
  */
299
- findOneAndUpdate<T = unknown>(table: string, filter: Record<string, unknown>, update: UpdateOperations, _options?: Record<string, unknown>): Promise<T | null>;
324
+ private executeUpsert;
325
+ /**
326
+ * Unique keys of a table (primary key first), read from `pg_index`.
327
+ *
328
+ * Partial and expression indexes are excluded — they cannot be an
329
+ * `ON CONFLICT (columns)` target. Non-empty results are cached per table.
330
+ */
331
+ private getUniqueKeys;
300
332
  /**
301
333
  * Update multiple rows matching the filter.
302
334
  *
@@ -415,7 +447,7 @@ declare class PostgresDriver implements DriverContract {
415
447
  * @param options - Optional options
416
448
  * @returns Update result
417
449
  */
418
- atomic(table: string, filter: Record<string, unknown>, operations: UpdateOperations, _options?: Record<string, unknown>): Promise<UpdateResult>;
450
+ atomic(table: string, filter: Record<string, unknown>, operations: AtomicUpdate, options?: DriverAtomicUpdateOptions): Promise<UpdateResult>;
419
451
  /**
420
452
  * Get the sync adapter for bulk denormalized updates.
421
453
  *
@@ -471,6 +503,16 @@ declare class PostgresDriver implements DriverContract {
471
503
  * payloads) keep their existing bind-as-value behavior.
472
504
  */
473
505
  private isOperatorFilter;
506
+ /**
507
+ * Build the SET clauses for an update.
508
+ *
509
+ * @param table - Target table name (for value serialization)
510
+ * @param update - Update operations (`$setOnInsert` is not part of SET)
511
+ * @param startIndex - First placeholder index
512
+ * @param qualifier - Quoted table name to qualify right-hand column references
513
+ * (needed where another relation with the same columns is in scope)
514
+ */
515
+ private buildSetClauses;
474
516
  /**
475
517
  * Build an UPDATE query from update operations.
476
518
  *
@@ -479,6 +521,7 @@ declare class PostgresDriver implements DriverContract {
479
521
  * @param update - Update operations
480
522
  * @param limit - Optional limit (for single row update)
481
523
  * @returns Object with SQL and parameters
524
+ * @throws UnsupportedUpdateOperationError for operations SQL cannot express
482
525
  */
483
526
  private buildUpdateQuery;
484
527
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-driver.d.mts","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-driver.ts"],"mappings":"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"}
1
+ {"version":3,"file":"postgres-driver.d.mts","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-driver.ts"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { databaseTransactionContext } from "../../context/database-transaction-context.mjs";
2
2
  import { TransactionRollbackError } from "../../errors/transaction-rollback.error.mjs";
3
+ import { UnsupportedUpdateOperationError } from "../../errors/unsupported-update-operation.error.mjs";
3
4
  import { isValidDateValue } from "../../utils/is-valid-date-value.mjs";
4
5
  import { PostgresBlueprint } from "./postgres-blueprint.mjs";
5
6
  import { PostgresDialect } from "./postgres-dialect.mjs";
@@ -8,6 +9,7 @@ import { PostgresMigrationDriver } from "./postgres-migration-driver.mjs";
8
9
  import { PostgresQueryBuilder } from "./postgres-query-builder.mjs";
9
10
  import { PostgresSQLSerializer } from "./postgres-sql-serializer.mjs";
10
11
  import { PostgresSyncAdapter } from "./postgres-sync-adapter.mjs";
12
+ import { assertPostgresUpdate } from "./postgres-update-validator.mjs";
11
13
  import { colors } from "@mongez/copper";
12
14
  import { log } from "@warlock.js/logger";
13
15
 
@@ -165,6 +167,8 @@ var PostgresDriver = class {
165
167
  * @see PostgresPoolConfig.nativeArrayColumns
166
168
  */
167
169
  _nativeArrayColumns;
170
+ /** Unique keys per table, resolved from `pg_index` for upsert conflict targets. */
171
+ _uniqueKeys = /* @__PURE__ */ new Map();
168
172
  /**
169
173
  * Native-array columns discovered by introspecting the live schema on
170
174
  * connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
@@ -486,17 +490,120 @@ var PostgresDriver = class {
486
490
  }
487
491
  }
488
492
  /**
489
- * Find one and update a single row matching the filter and return the updated row
490
- * @param table - Target table name
491
- * @param filter - Filter conditions
492
- * @param update - Update operations ($set, $unset, $inc)
493
- * @param options - Optional update options
494
- * @returns The updated row or null
493
+ * Find one row matching the filter, update it, and return it.
494
+ *
495
+ * - `upsert`: `INSERT ON CONFLICT (target) DO UPDATE … RETURNING *`, where
496
+ * the target is the primary key or a unique index whose columns are all
497
+ * equality keys of the filter (see {@link executeUpsert}).
498
+ * - `returnDocument` (default `"after"`): `"before"` returns the row as it
499
+ * was, and is not available together with `upsert`.
500
+ *
501
+ * The row is chosen with `SELECT … LIMIT 1 FOR UPDATE` and matched by primary
502
+ * key with the filter repeated, so concurrent callers re-check the filter
503
+ * against the latest committed row instead of overshooting it.
504
+ *
505
+ * @throws UnsupportedUpdateOperationError for pipeline updates, `arrayFilters`,
506
+ * array operators, `before` + `upsert`, or an underivable conflict target
495
507
  */
496
- async findOneAndUpdate(table, filter, update, _options) {
497
- const { sql, params } = this.buildUpdateQuery(table, filter, update, 1);
498
- const sqlWithReturning = `${sql} RETURNING *`;
499
- return (await this.query(sqlWithReturning, params)).rows[0] ?? null;
508
+ async findOneAndUpdate(table, filter, update, options) {
509
+ const operations = assertPostgresUpdate(update, options);
510
+ const returnBefore = options?.returnDocument === "before";
511
+ if (options?.upsert) {
512
+ if (returnBefore) throw new UnsupportedUpdateOperationError("returnDocument: before with upsert", "postgres", "Use returnDocument: \"after\", or read the row before upserting inside a transaction.");
513
+ const { row } = await this.executeUpsert(table, filter, operations);
514
+ return row ?? null;
515
+ }
516
+ const primaryKey = (await this.getUniqueKeys(table)).find((key) => key.primary)?.columns;
517
+ if (!primaryKey) {
518
+ if (returnBefore) throw new UnsupportedUpdateOperationError("returnDocument: before without a primary key", "postgres");
519
+ const { sql, params } = this.buildUpdateQuery(table, filter, operations, 1);
520
+ return (await this.query(`${sql} RETURNING *`, params)).rows[0] ?? null;
521
+ }
522
+ const quotedTable = this.dialect.quoteIdentifier(table);
523
+ const keyList = (qualifier) => primaryKey.map((column) => (qualifier ? `${qualifier}.` : "") + this.dialect.quoteIdentifier(column)).join(", ");
524
+ const set = this.buildSetClauses(table, operations, 1, returnBefore ? quotedTable : void 0);
525
+ if (set.clauses.length === 0) throw new Error("No update operations specified");
526
+ const inner = this.buildWhereClause(filter, set.nextIndex);
527
+ const outer = this.buildWhereClause(filter, set.nextIndex + inner.whereParams.length, quotedTable);
528
+ const params = [
529
+ ...set.params,
530
+ ...inner.whereParams,
531
+ ...outer.whereParams
532
+ ];
533
+ const outerCondition = outer.condition ? ` AND ${outer.condition}` : "";
534
+ if (returnBefore) {
535
+ const before = this.dialect.quoteIdentifier("__cascade_before");
536
+ const sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} FROM (SELECT * FROM ${quotedTable} ${inner.whereClause} LIMIT 1 FOR UPDATE) AS ${before} WHERE (${keyList(quotedTable)}) = (${keyList(before)})${outerCondition} RETURNING ${before}.*`;
537
+ return (await this.query(sql, params)).rows[0] ?? null;
538
+ }
539
+ const sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} WHERE (${keyList(quotedTable)}) = (SELECT ${keyList()} FROM ${quotedTable} ${inner.whereClause} LIMIT 1 FOR UPDATE)${outerCondition} RETURNING *`;
540
+ return (await this.query(sql, params)).rows[0] ?? null;
541
+ }
542
+ /**
543
+ * Upsert through `INSERT … ON CONFLICT (target) DO UPDATE SET … [WHERE rest] RETURNING *`.
544
+ *
545
+ * Conflict target rule: the first unique index of the table (primary key
546
+ * first; partial and expression indexes excluded) whose columns are ALL
547
+ * equality keys of the filter — a non-null value that is not an operator
548
+ * object. Any other filter predicate becomes the `DO UPDATE … WHERE`, so an
549
+ * existing row that fails it is left untouched and no row is returned.
550
+ *
551
+ * Inserted row: filter equality values, then `$setOnInsert`, then `$set`,
552
+ * then `$inc` (n) / `$dec` (-n); `$unset` columns are omitted.
553
+ *
554
+ * @throws UnsupportedUpdateOperationError when no conflict target can be derived
555
+ */
556
+ async executeUpsert(table, filter, operations) {
557
+ const equalityKeys = new Set(Object.entries(filter).filter(([, value]) => value !== null && value !== void 0 && !this.isOperatorFilter(value)).map(([key]) => key));
558
+ const target = (await this.getUniqueKeys(table)).find((key) => key.columns.every((column) => equalityKeys.has(column)));
559
+ if (!target) throw new UnsupportedUpdateOperationError("upsert without a unique conflict target", "postgres", `Filter on every column of the primary key or of a unique index of "${table}".`);
560
+ const insertRow = {};
561
+ for (const key of equalityKeys) insertRow[key] = filter[key];
562
+ Object.assign(insertRow, operations.$setOnInsert, operations.$set);
563
+ for (const [key, amount] of Object.entries(operations.$inc ?? {})) insertRow[key] = amount;
564
+ for (const [key, amount] of Object.entries(operations.$dec ?? {})) insertRow[key] = -amount;
565
+ for (const key of Object.keys(operations.$unset ?? {})) delete insertRow[key];
566
+ const columns = Object.keys(insertRow).filter((key) => insertRow[key] !== void 0);
567
+ const params = columns.map((key) => this.serializeValue(key, insertRow[key], table));
568
+ const quotedTable = this.dialect.quoteIdentifier(table);
569
+ const quotedColumns = columns.map((column) => this.dialect.quoteIdentifier(column)).join(", ");
570
+ const placeholders = columns.map((_, index) => this.dialect.placeholder(index + 1)).join(", ");
571
+ const set = this.buildSetClauses(table, operations, columns.length + 1, quotedTable);
572
+ params.push(...set.params);
573
+ const firstTargetColumn = this.dialect.quoteIdentifier(target.columns[0]);
574
+ const doUpdateSet = set.clauses.length > 0 ? set.clauses.join(", ") : `${firstTargetColumn} = ${quotedTable}.${firstTargetColumn}`;
575
+ const remainingFilter = Object.fromEntries(Object.entries(filter).filter(([key]) => !target.columns.includes(key)));
576
+ const where = this.buildWhereClause(remainingFilter, set.nextIndex, quotedTable);
577
+ params.push(...where.whereParams);
578
+ const conflictColumns = target.columns.map((column) => this.dialect.quoteIdentifier(column)).join(", ");
579
+ const insertedFlag = "__cascade_inserted";
580
+ const sql = `INSERT INTO ${quotedTable} (${quotedColumns}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns}) DO UPDATE SET ${doUpdateSet} ${where.whereClause} RETURNING *, (xmax = 0) AS "${insertedFlag}"`;
581
+ const returned = (await this.query(sql, params)).rows[0];
582
+ if (!returned) return { inserted: false };
583
+ const { [insertedFlag]: inserted, ...row } = returned;
584
+ return {
585
+ row,
586
+ inserted: inserted === true
587
+ };
588
+ }
589
+ /**
590
+ * Unique keys of a table (primary key first), read from `pg_index`.
591
+ *
592
+ * Partial and expression indexes are excluded — they cannot be an
593
+ * `ON CONFLICT (columns)` target. Non-empty results are cached per table.
594
+ */
595
+ async getUniqueKeys(table) {
596
+ const cached = this._uniqueKeys.get(table);
597
+ if (cached) return cached;
598
+ const result = await this.query(`SELECT array_agg(a.attname::text ORDER BY k.ord) AS columns, i.indisprimary AS primary
599
+ FROM pg_index i
600
+ CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord)
601
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
602
+ WHERE i.indrelid = to_regclass($1) AND i.indisunique AND i.indpred IS NULL AND i.indexprs IS NULL
603
+ GROUP BY i.indexrelid, i.indisprimary
604
+ ORDER BY i.indisprimary DESC, i.indexrelid`, [this.dialect.quoteIdentifier(table)]);
605
+ if (result.rows.length > 0) this._uniqueKeys.set(table, result.rows);
606
+ return result.rows;
500
607
  }
501
608
  /**
502
609
  * Update multiple rows matching the filter.
@@ -703,8 +810,16 @@ var PostgresDriver = class {
703
810
  * @param options - Optional options
704
811
  * @returns Update result
705
812
  */
706
- async atomic(table, filter, operations, _options) {
707
- const { sql, params } = this.buildUpdateQuery(table, filter, operations);
813
+ async atomic(table, filter, operations, options) {
814
+ const update = assertPostgresUpdate(operations, options);
815
+ if (options?.upsert) {
816
+ const { row, inserted } = await this.executeUpsert(table, filter, update);
817
+ return {
818
+ modifiedCount: row && !inserted ? 1 : 0,
819
+ upsertedCount: row && inserted ? 1 : 0
820
+ };
821
+ }
822
+ const { sql, params } = this.buildUpdateQuery(table, filter, update);
708
823
  return { modifiedCount: (await this.query(sql, params)).rowCount ?? 0 };
709
824
  }
710
825
  /**
@@ -818,12 +933,12 @@ var PostgresDriver = class {
818
933
  * @param startParamIndex - Starting parameter index
819
934
  * @returns Object with WHERE clause string and parameters
820
935
  */
821
- buildWhereClause(filter, startParamIndex) {
936
+ buildWhereClause(filter, startParamIndex, qualifier) {
822
937
  const conditions = [];
823
938
  const params = [];
824
939
  let paramIndex = startParamIndex;
825
940
  for (const [key, value] of Object.entries(filter)) {
826
- const quotedKey = this.dialect.quoteIdentifier(key);
941
+ const quotedKey = (qualifier ? `${qualifier}.` : "") + this.dialect.quoteIdentifier(key);
827
942
  if (value === null) conditions.push(`${quotedKey} IS NULL`);
828
943
  else if (this.isOperatorFilter(value)) for (const [operator, operand] of Object.entries(value)) switch (operator) {
829
944
  case "$in":
@@ -873,9 +988,11 @@ var PostgresDriver = class {
873
988
  params.push(value);
874
989
  }
875
990
  }
991
+ const condition = conditions.join(" AND ");
876
992
  return {
877
- whereClause: conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
878
- whereParams: params
993
+ whereClause: condition ? `WHERE ${condition}` : "",
994
+ whereParams: params,
995
+ condition
879
996
  };
880
997
  }
881
998
  /**
@@ -890,39 +1007,54 @@ var PostgresDriver = class {
890
1007
  return keys.length > 0 && keys.every((key) => key.startsWith("$"));
891
1008
  }
892
1009
  /**
893
- * Build an UPDATE query from update operations.
1010
+ * Build the SET clauses for an update.
894
1011
  *
895
- * @param table - Target table name
896
- * @param filter - Filter conditions
897
- * @param update - Update operations
898
- * @param limit - Optional limit (for single row update)
899
- * @returns Object with SQL and parameters
1012
+ * @param table - Target table name (for value serialization)
1013
+ * @param update - Update operations (`$setOnInsert` is not part of SET)
1014
+ * @param startIndex - First placeholder index
1015
+ * @param qualifier - Quoted table name to qualify right-hand column references
1016
+ * (needed where another relation with the same columns is in scope)
900
1017
  */
901
- buildUpdateQuery(table, filter, update, limit) {
902
- const setClauses = [];
1018
+ buildSetClauses(table, update, startIndex, qualifier) {
1019
+ const clauses = [];
903
1020
  const params = [];
904
- let paramIndex = 1;
1021
+ let paramIndex = startIndex;
1022
+ const reference = (quotedKey) => qualifier ? `${qualifier}.${quotedKey}` : quotedKey;
905
1023
  if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
906
- setClauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
1024
+ clauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
907
1025
  params.push(value === void 0 ? value : this.serializeValue(key, value, table));
908
1026
  }
909
- if (update.$unset) for (const key of Object.keys(update.$unset)) setClauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
910
- if (update.$inc) for (const [key, amount] of Object.entries(update.$inc)) {
911
- const quotedKey = this.dialect.quoteIdentifier(key);
912
- setClauses.push(`${quotedKey} = COALESCE(${quotedKey}, 0) + ${this.dialect.placeholder(paramIndex++)}`);
913
- params.push(amount);
914
- }
915
- if (update.$dec) for (const [key, amount] of Object.entries(update.$dec)) {
1027
+ if (update.$unset) for (const key of Object.keys(update.$unset)) clauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
1028
+ for (const [operator, sign] of [["$inc", "+"], ["$dec", "-"]]) for (const [key, amount] of Object.entries(update[operator] ?? {})) {
916
1029
  const quotedKey = this.dialect.quoteIdentifier(key);
917
- setClauses.push(`${quotedKey} = COALESCE(${quotedKey}, 0) - ${this.dialect.placeholder(paramIndex++)}`);
1030
+ clauses.push(`${quotedKey} = COALESCE(${reference(quotedKey)}, 0) ${sign} ${this.dialect.placeholder(paramIndex++)}`);
918
1031
  params.push(amount);
919
1032
  }
920
- if (setClauses.length === 0) throw new Error("No update operations specified");
1033
+ return {
1034
+ clauses,
1035
+ params,
1036
+ nextIndex: paramIndex
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Build an UPDATE query from update operations.
1041
+ *
1042
+ * @param table - Target table name
1043
+ * @param filter - Filter conditions
1044
+ * @param update - Update operations
1045
+ * @param limit - Optional limit (for single row update)
1046
+ * @returns Object with SQL and parameters
1047
+ * @throws UnsupportedUpdateOperationError for operations SQL cannot express
1048
+ */
1049
+ buildUpdateQuery(table, filter, update, limit) {
1050
+ const set = this.buildSetClauses(table, assertPostgresUpdate(update), 1);
1051
+ if (set.clauses.length === 0) throw new Error("No update operations specified");
1052
+ const params = [...set.params];
921
1053
  const quotedTable = this.dialect.quoteIdentifier(table);
922
- const { whereClause, whereParams } = this.buildWhereClause(filter, paramIndex);
1054
+ const { whereClause, whereParams } = this.buildWhereClause(filter, set.nextIndex);
923
1055
  params.push(...whereParams);
924
- let sql = `UPDATE ${quotedTable} SET ${setClauses.join(", ")} ${whereClause}`;
925
- if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${setClauses.join(", ")} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1)`;
1056
+ let sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} ${whereClause}`;
1057
+ if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1)`;
926
1058
  return {
927
1059
  sql,
928
1060
  params