@warlock.js/cascade 5.11.0 → 5.13.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.
- package/CHANGELOG.md +31 -0
- package/cjs/index.cjs +685 -128
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +44 -7
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/query-builder.contract.d.mts +61 -1
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +8 -4
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +16 -6
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +32 -7
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +56 -8
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +39 -12
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +143 -55
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-update-translator.mjs +25 -0
- package/esm/drivers/mongodb/mongodb-update-translator.mjs.map +1 -0
- package/esm/drivers/mongodb/pipeline-stage-object.mjs +24 -0
- package/esm/drivers/mongodb/pipeline-stage-object.mjs.map +1 -0
- package/esm/drivers/mongodb/types.d.mts +7 -1
- package/esm/drivers/mongodb/types.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +52 -9
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +170 -38
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +17 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +31 -0
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-update-validator.mjs +36 -0
- package/esm/drivers/postgres/postgres-update-validator.mjs.map +1 -0
- package/esm/errors/unsupported-lean-operation.error.d.mts +25 -0
- package/esm/errors/unsupported-lean-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-lean-operation.error.mjs +31 -0
- package/esm/errors/unsupported-lean-operation.error.mjs.map +1 -0
- package/esm/errors/unsupported-query-operation.error.d.mts +30 -0
- package/esm/errors/unsupported-query-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-query-operation.error.mjs +37 -0
- package/esm/errors/unsupported-query-operation.error.mjs.map +1 -0
- package/esm/errors/unsupported-update-operation.error.d.mts +30 -0
- package/esm/errors/unsupported-update-operation.error.d.mts.map +1 -0
- package/esm/errors/unsupported-update-operation.error.mjs +37 -0
- package/esm/errors/unsupported-update-operation.error.mjs.map +1 -0
- package/esm/index.d.mts +6 -3
- package/esm/index.mjs +4 -1
- package/esm/model/methods/query-methods.mjs +22 -6
- package/esm/model/methods/query-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +28 -11
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs +30 -13
- package/esm/model/model.mjs.map +1 -1
- package/esm/query-builder/lean-records.mjs +34 -0
- package/esm/query-builder/lean-records.mjs.map +1 -0
- package/esm/query-builder/query-builder.d.mts +13 -1
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs +16 -0
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/llms-full.txt +72 -5
- package/llms.txt +3 -3
- package/package.json +4 -4
- package/skills/aggregate-data/SKILL.md +24 -1
- package/skills/perform-atomic-ops/SKILL.md +38 -3
- 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
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
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
|
-
|
|
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:
|
|
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":";;;;;;;;;;;;;
|
|
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
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
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,
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
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,
|
|
707
|
-
const
|
|
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:
|
|
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
|
|
1010
|
+
* Build the SET clauses for an update.
|
|
894
1011
|
*
|
|
895
|
-
* @param table - Target table name
|
|
896
|
-
* @param
|
|
897
|
-
* @param
|
|
898
|
-
* @param
|
|
899
|
-
*
|
|
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
|
-
|
|
902
|
-
const
|
|
1018
|
+
buildSetClauses(table, update, startIndex, qualifier) {
|
|
1019
|
+
const clauses = [];
|
|
903
1020
|
const params = [];
|
|
904
|
-
let paramIndex =
|
|
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
|
-
|
|
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))
|
|
910
|
-
|
|
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
|
-
|
|
1030
|
+
clauses.push(`${quotedKey} = COALESCE(${reference(quotedKey)}, 0) ${sign} ${this.dialect.placeholder(paramIndex++)}`);
|
|
918
1031
|
params.push(amount);
|
|
919
1032
|
}
|
|
920
|
-
|
|
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,
|
|
1054
|
+
const { whereClause, whereParams } = this.buildWhereClause(filter, set.nextIndex);
|
|
923
1055
|
params.push(...whereParams);
|
|
924
|
-
let sql = `UPDATE ${quotedTable} SET ${
|
|
925
|
-
if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${
|
|
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
|