@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
package/cjs/index.cjs
CHANGED
|
@@ -475,6 +475,105 @@ var UnsafeRawExpressionError = class UnsafeRawExpressionError extends Error {
|
|
|
475
475
|
}
|
|
476
476
|
};
|
|
477
477
|
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../cascade/src/errors/unsupported-lean-operation.error.ts
|
|
480
|
+
/**
|
|
481
|
+
* Error thrown when a query in lean read mode (`.lean()`) asks for something
|
|
482
|
+
* that needs hydrated Model instances — eager-loading relations with `with()`
|
|
483
|
+
* or `joinWith()`.
|
|
484
|
+
*
|
|
485
|
+
* Relations are attached to Model instances and shaped (including the related
|
|
486
|
+
* model's `hidden` fields) by the Model layer, so lean mode refuses them rather
|
|
487
|
+
* than returning a half-loaded or unfiltered result.
|
|
488
|
+
*/
|
|
489
|
+
var UnsupportedLeanOperationError = class UnsupportedLeanOperationError extends Error {
|
|
490
|
+
/**
|
|
491
|
+
* The rejected builder operation (e.g. `"with"`, `"joinWith"`).
|
|
492
|
+
*/
|
|
493
|
+
operation;
|
|
494
|
+
/**
|
|
495
|
+
* Creates a new UnsupportedLeanOperationError.
|
|
496
|
+
*
|
|
497
|
+
* @param operation - The rejected builder operation
|
|
498
|
+
*/
|
|
499
|
+
constructor(operation) {
|
|
500
|
+
super(`${operation}() cannot be combined with lean(): eager-loaded relations need hydrated models. Drop lean() for this query, or load the related records with a second lean query.`);
|
|
501
|
+
this.name = "UnsupportedLeanOperationError";
|
|
502
|
+
this.operation = operation;
|
|
503
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, UnsupportedLeanOperationError);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region ../cascade/src/errors/unsupported-query-operation.error.ts
|
|
509
|
+
/**
|
|
510
|
+
* Error thrown when a query builder is asked for a read operation its driver
|
|
511
|
+
* cannot express — e.g. a MongoDB-only aggregation stage such as `unwind()` or
|
|
512
|
+
* `addFields()` on a SQL driver.
|
|
513
|
+
*
|
|
514
|
+
* Builders throw this instead of silently dropping the stage, so a caller
|
|
515
|
+
* never receives rows shaped differently from what the query asked for.
|
|
516
|
+
*/
|
|
517
|
+
var UnsupportedQueryOperationError = class UnsupportedQueryOperationError extends Error {
|
|
518
|
+
/**
|
|
519
|
+
* The operation that was rejected (e.g. `"unwind"`, `"addFields"`).
|
|
520
|
+
*/
|
|
521
|
+
operation;
|
|
522
|
+
/**
|
|
523
|
+
* The driver that rejected it (e.g. `"postgres"`).
|
|
524
|
+
*/
|
|
525
|
+
driver;
|
|
526
|
+
/**
|
|
527
|
+
* Creates a new UnsupportedQueryOperationError.
|
|
528
|
+
*
|
|
529
|
+
* @param operation - The rejected operation
|
|
530
|
+
* @param driver - The driver name
|
|
531
|
+
* @param hint - Optional guidance appended to the message
|
|
532
|
+
*/
|
|
533
|
+
constructor(operation, driver, hint) {
|
|
534
|
+
super(`The ${driver} driver does not support ${operation}() in queries.` + (hint ? ` ${hint}` : ""));
|
|
535
|
+
this.name = "UnsupportedQueryOperationError";
|
|
536
|
+
this.operation = operation;
|
|
537
|
+
this.driver = driver;
|
|
538
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, UnsupportedQueryOperationError);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region ../cascade/src/errors/unsupported-update-operation.error.ts
|
|
544
|
+
/**
|
|
545
|
+
* Error thrown when a driver is asked to perform an update it cannot express
|
|
546
|
+
* natively — e.g. an aggregation-pipeline update, `arrayFilters` or `$addToSet`
|
|
547
|
+
* on a SQL driver, or an upsert whose conflict target cannot be derived.
|
|
548
|
+
*
|
|
549
|
+
* Drivers throw this instead of silently dropping the operation, so a caller
|
|
550
|
+
* never believes a write happened that did not.
|
|
551
|
+
*/
|
|
552
|
+
var UnsupportedUpdateOperationError = class UnsupportedUpdateOperationError extends Error {
|
|
553
|
+
/**
|
|
554
|
+
* The operation that was rejected (e.g. `"pipeline update"`, `"$addToSet"`).
|
|
555
|
+
*/
|
|
556
|
+
operation;
|
|
557
|
+
/**
|
|
558
|
+
* The driver that rejected it (e.g. `"postgres"`).
|
|
559
|
+
*/
|
|
560
|
+
driver;
|
|
561
|
+
/**
|
|
562
|
+
* Creates a new UnsupportedUpdateOperationError.
|
|
563
|
+
*
|
|
564
|
+
* @param operation - The rejected operation
|
|
565
|
+
* @param driver - The driver name
|
|
566
|
+
* @param hint - Optional guidance appended to the message
|
|
567
|
+
*/
|
|
568
|
+
constructor(operation, driver, hint) {
|
|
569
|
+
super(`The ${driver} driver does not support ${operation} in atomic updates.` + (hint ? ` ${hint}` : ""));
|
|
570
|
+
this.name = "UnsupportedUpdateOperationError";
|
|
571
|
+
this.operation = operation;
|
|
572
|
+
this.driver = driver;
|
|
573
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, UnsupportedUpdateOperationError);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
478
577
|
//#endregion
|
|
479
578
|
//#region ../cascade/src/database-dirty-tracker.ts
|
|
480
579
|
function canBeFlatten(object) {
|
|
@@ -4707,18 +4806,34 @@ function increaseField(ModelClass, filter, field, amount) {
|
|
|
4707
4806
|
function decreaseField(ModelClass, filter, field, amount) {
|
|
4708
4807
|
return ModelClass.query().where(filter).decrement(field, amount);
|
|
4709
4808
|
}
|
|
4710
|
-
|
|
4711
|
-
return
|
|
4809
|
+
function resolveFilter(filter, options) {
|
|
4810
|
+
return options?.trustedFilter ? filter : sanitizeFilter(filter);
|
|
4811
|
+
}
|
|
4812
|
+
function toDriverOptions(options) {
|
|
4813
|
+
if (!options) return;
|
|
4814
|
+
const { trustedFilter: _trustedFilter, ...rest } = options;
|
|
4815
|
+
return rest;
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Run an atomic update and return the number of documents modified plus the
|
|
4819
|
+
* number inserted by an `upsert`.
|
|
4820
|
+
*/
|
|
4821
|
+
async function performAtomic(ModelClass, filter, operations, options) {
|
|
4822
|
+
const result = await ModelClass.getDriver().atomic(ModelClass.table, resolveFilter(filter, options), operations, toDriverOptions(options));
|
|
4823
|
+
return result.modifiedCount + (result.upsertedCount ?? 0);
|
|
4712
4824
|
}
|
|
4713
4825
|
async function updateById(ModelClass, id, data) {
|
|
4714
4826
|
return (await ModelClass.getDriver().update(ModelClass.table, { [ModelClass.primaryKey]: id }, { $set: data })).modifiedCount;
|
|
4715
4827
|
}
|
|
4716
|
-
async function findAndUpdateRecords(ModelClass, filter, update) {
|
|
4717
|
-
await performAtomic(ModelClass, filter, update
|
|
4828
|
+
async function findAndUpdateRecords(ModelClass, filter, update, options) {
|
|
4829
|
+
await performAtomic(ModelClass, filter, update, {
|
|
4830
|
+
...options,
|
|
4831
|
+
trustedFilter: false
|
|
4832
|
+
});
|
|
4718
4833
|
return await ModelClass.query().where(filter).get();
|
|
4719
4834
|
}
|
|
4720
|
-
async function findOneAndUpdateRecord(ModelClass, filter, update) {
|
|
4721
|
-
const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table,
|
|
4835
|
+
async function findOneAndUpdateRecord(ModelClass, filter, update, options) {
|
|
4836
|
+
const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, resolveFilter(filter, options), update, toDriverOptions(options));
|
|
4722
4837
|
if (!result) return null;
|
|
4723
4838
|
return new ModelClass(result);
|
|
4724
4839
|
}
|
|
@@ -6548,15 +6663,21 @@ var Model = class Model {
|
|
|
6548
6663
|
return decreaseField(this, filter, field, amount);
|
|
6549
6664
|
}
|
|
6550
6665
|
/**
|
|
6551
|
-
* Perform atomic
|
|
6552
|
-
*
|
|
6666
|
+
* Perform an atomic update on every document/row matching the filter.
|
|
6667
|
+
*
|
|
6668
|
+
* @param filter - Filter conditions (operator keys rejected unless `trustedFilter`)
|
|
6669
|
+
* @param operations - Update operators, or an aggregation pipeline (MongoDB only)
|
|
6670
|
+
* @param options - `upsert`, `arrayFilters` (MongoDB only), `trustedFilter`
|
|
6671
|
+
* @returns Number of documents modified plus the number inserted by an upsert
|
|
6553
6672
|
*
|
|
6673
|
+
* @example
|
|
6554
6674
|
* ```typescript
|
|
6555
|
-
*
|
|
6556
|
-
*
|
|
6675
|
+
* await User.atomic({ id: 1 }, { $inc: { age: 1 } });
|
|
6676
|
+
* await Counter.atomic({ key: "signups" }, { $inc: { count: 1 } }, { upsert: true });
|
|
6677
|
+
* ```
|
|
6557
6678
|
*/
|
|
6558
|
-
static async atomic(filter, operations) {
|
|
6559
|
-
return performAtomic(this, filter, operations);
|
|
6679
|
+
static async atomic(filter, operations, options) {
|
|
6680
|
+
return performAtomic(this, filter, operations, options);
|
|
6560
6681
|
}
|
|
6561
6682
|
/**
|
|
6562
6683
|
* Perform an atomic update for the given id
|
|
@@ -6567,20 +6688,31 @@ var Model = class Model {
|
|
|
6567
6688
|
/**
|
|
6568
6689
|
* Find one and update multiple records that matches the provided filter and return the updated record
|
|
6569
6690
|
* @param filter - Filter conditions
|
|
6570
|
-
* @param update - Update
|
|
6691
|
+
* @param update - Update operators, or an aggregation pipeline (MongoDB only)
|
|
6692
|
+
* @param options - `upsert`, `arrayFilters` (MongoDB only)
|
|
6571
6693
|
* @returns The updated records
|
|
6572
6694
|
*/
|
|
6573
|
-
static async findAndUpdate(filter, update) {
|
|
6574
|
-
return findAndUpdateRecords(this, filter, update);
|
|
6695
|
+
static async findAndUpdate(filter, update, options) {
|
|
6696
|
+
return findAndUpdateRecords(this, filter, update, options);
|
|
6575
6697
|
}
|
|
6576
6698
|
/**
|
|
6577
6699
|
* Find one and update a single record that matches the provided filter and return the updated record
|
|
6578
6700
|
* @param filter - Filter conditions
|
|
6579
|
-
* @param update - Update
|
|
6580
|
-
* @
|
|
6701
|
+
* @param update - Update operators, or an aggregation pipeline (MongoDB only)
|
|
6702
|
+
* @param options - `upsert`, `returnDocument` (default `"after"`), `arrayFilters`, `trustedFilter`
|
|
6703
|
+
* @returns The record after (or before) the update, or null when nothing matched
|
|
6704
|
+
*
|
|
6705
|
+
* @example
|
|
6706
|
+
* ```typescript
|
|
6707
|
+
* const counter = await Counter.findOneAndUpdate(
|
|
6708
|
+
* { key: "signups" },
|
|
6709
|
+
* { $inc: { count: 1 }, $setOnInsert: { startedAt: new Date() } },
|
|
6710
|
+
* { upsert: true },
|
|
6711
|
+
* );
|
|
6712
|
+
* ```
|
|
6581
6713
|
*/
|
|
6582
|
-
static async findOneAndUpdate(filter, update) {
|
|
6583
|
-
return findOneAndUpdateRecord(this, filter, update);
|
|
6714
|
+
static async findOneAndUpdate(filter, update, options) {
|
|
6715
|
+
return findOneAndUpdateRecord(this, filter, update, options);
|
|
6584
6716
|
}
|
|
6585
6717
|
/**
|
|
6586
6718
|
* Find and replace the entire document that matches the provided filter and return the replaced document
|
|
@@ -8526,6 +8658,36 @@ var MongoMigrationDriver = class {
|
|
|
8526
8658
|
}
|
|
8527
8659
|
};
|
|
8528
8660
|
|
|
8661
|
+
//#endregion
|
|
8662
|
+
//#region ../cascade/src/query-builder/lean-records.ts
|
|
8663
|
+
/**
|
|
8664
|
+
* Throw when a lean query also asks for eager-loaded relations.
|
|
8665
|
+
*
|
|
8666
|
+
* Called before the query executes, so nothing is fetched for a request that
|
|
8667
|
+
* cannot be answered.
|
|
8668
|
+
*
|
|
8669
|
+
* @throws UnsupportedLeanOperationError
|
|
8670
|
+
*/
|
|
8671
|
+
function assertLeanCompatible(state) {
|
|
8672
|
+
if (state.eagerLoadRelations.size > 0) throw new UnsupportedLeanOperationError("with");
|
|
8673
|
+
if (state.joinRelations && state.joinRelations.size > 0) throw new UnsupportedLeanOperationError("joinWith");
|
|
8674
|
+
}
|
|
8675
|
+
/**
|
|
8676
|
+
* Remove the model's `static hidden` top-level fields from lean rows, in place.
|
|
8677
|
+
*
|
|
8678
|
+
* The rows are fresh objects the driver just built, so mutating them is safe
|
|
8679
|
+
* and avoids a copy per row. Returns the same array.
|
|
8680
|
+
*/
|
|
8681
|
+
function stripHiddenFromLeanRecords(records, modelClass) {
|
|
8682
|
+
const hidden = modelClass?.hidden;
|
|
8683
|
+
if (!hidden || hidden.length === 0) return records;
|
|
8684
|
+
for (const record of records) {
|
|
8685
|
+
if (record === null || typeof record !== "object") continue;
|
|
8686
|
+
for (const field of hidden) delete record[field];
|
|
8687
|
+
}
|
|
8688
|
+
return records;
|
|
8689
|
+
}
|
|
8690
|
+
|
|
8529
8691
|
//#endregion
|
|
8530
8692
|
//#region ../cascade/src/query-builder/query-builder.ts
|
|
8531
8693
|
/**
|
|
@@ -8580,6 +8742,8 @@ var QueryBuilder = class QueryBuilder {
|
|
|
8580
8742
|
relationDefinitions;
|
|
8581
8743
|
/** The Model class reference, required for relation resolution. */
|
|
8582
8744
|
modelClass;
|
|
8745
|
+
/** True once `lean()` was called — execution skips Model hydration. */
|
|
8746
|
+
isLean = false;
|
|
8583
8747
|
/**
|
|
8584
8748
|
* Append an operation to `operations[]` and update `opIndex`.
|
|
8585
8749
|
* Every fluent method calls this.
|
|
@@ -8671,8 +8835,22 @@ var QueryBuilder = class QueryBuilder {
|
|
|
8671
8835
|
cloned.countRelations = new Map(this.countRelations);
|
|
8672
8836
|
cloned.relationDefinitions = this.relationDefinitions;
|
|
8673
8837
|
cloned.modelClass = this.modelClass;
|
|
8838
|
+
cloned.isLean = this.isLean;
|
|
8674
8839
|
return cloned;
|
|
8675
8840
|
}
|
|
8841
|
+
/**
|
|
8842
|
+
* Switch to lean read mode: execution returns the plain driver rows — no
|
|
8843
|
+
* Model hydration, no driver deserialization, no hydrating/fetched
|
|
8844
|
+
* callbacks. The model's `static hidden` fields are still stripped.
|
|
8845
|
+
* Eager-loading relations in lean mode throws `UnsupportedLeanOperationError`.
|
|
8846
|
+
*
|
|
8847
|
+
* @example
|
|
8848
|
+
* const rows = await User.query().where("isActive", true).lean().get();
|
|
8849
|
+
*/
|
|
8850
|
+
lean() {
|
|
8851
|
+
this.isLean = true;
|
|
8852
|
+
return this;
|
|
8853
|
+
}
|
|
8676
8854
|
/** Disable one or more named global scopes for this query. */
|
|
8677
8855
|
withoutGlobalScope(...scopeNames) {
|
|
8678
8856
|
scopeNames.forEach((name) => this.disabledGlobalScopes.add(name));
|
|
@@ -10020,6 +10198,28 @@ function resolveLikePattern(pattern) {
|
|
|
10020
10198
|
return pattern instanceof RegExp ? pattern.source : likePatternToRegexSource(pattern);
|
|
10021
10199
|
}
|
|
10022
10200
|
|
|
10201
|
+
//#endregion
|
|
10202
|
+
//#region ../cascade/src/drivers/mongodb/pipeline-stage-object.ts
|
|
10203
|
+
/**
|
|
10204
|
+
* Whether a value looks like one MongoDB aggregation stage: a plain object
|
|
10205
|
+
* with exactly one key, and that key is a `$`-prefixed stage name
|
|
10206
|
+
* (`{ $lookup: { ... } }`, `{ $addFields: { ... } }`).
|
|
10207
|
+
*
|
|
10208
|
+
* Used to validate caller-built stages (`joinRaw()`, `raw()` results) before
|
|
10209
|
+
* they reach the pipeline, so a SQL string or a filter object is rejected with
|
|
10210
|
+
* a named error instead of failing at execution time or being ignored.
|
|
10211
|
+
*
|
|
10212
|
+
* @param value - The candidate stage
|
|
10213
|
+
* @returns `true` when the value is a single-stage object
|
|
10214
|
+
*/
|
|
10215
|
+
function isPipelineStageObject(value) {
|
|
10216
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10217
|
+
const prototype = Object.getPrototypeOf(value);
|
|
10218
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
10219
|
+
const keys = Object.keys(value);
|
|
10220
|
+
return keys.length === 1 && keys[0].startsWith("$");
|
|
10221
|
+
}
|
|
10222
|
+
|
|
10023
10223
|
//#endregion
|
|
10024
10224
|
//#region ../cascade/src/drivers/mongodb/mongodb-query-parser.ts
|
|
10025
10225
|
/**
|
|
@@ -10086,42 +10286,110 @@ var MongoQueryParser = class {
|
|
|
10086
10286
|
* ```
|
|
10087
10287
|
*/
|
|
10088
10288
|
parse() {
|
|
10289
|
+
this.groupFieldNames.clear();
|
|
10290
|
+
this.countDistinctAliases.clear();
|
|
10089
10291
|
const pipeline = [];
|
|
10090
10292
|
let currentStage = null;
|
|
10091
10293
|
let currentBuffer = [];
|
|
10092
|
-
|
|
10093
|
-
|
|
10294
|
+
let trailingProjectIndex = -1;
|
|
10295
|
+
const emit = (stage, operations) => {
|
|
10296
|
+
if (stage === "$raw") {
|
|
10297
|
+
for (const op of operations) this.applyRawOperation(pipeline, op);
|
|
10298
|
+
trailingProjectIndex = -1;
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
const builtStage = this.buildStage(stage, operations);
|
|
10302
|
+
if (!builtStage) return;
|
|
10303
|
+
if (builtStage.$sort && trailingProjectIndex >= 0 && trailingProjectIndex === pipeline.length - 1) {
|
|
10304
|
+
const projectStage = pipeline.pop();
|
|
10305
|
+
pipeline.push(...this.placeSortAroundProjection(projectStage, builtStage));
|
|
10306
|
+
trailingProjectIndex = -1;
|
|
10307
|
+
return;
|
|
10308
|
+
}
|
|
10309
|
+
const stageIndex = pipeline.length;
|
|
10310
|
+
pipeline.push(builtStage);
|
|
10311
|
+
trailingProjectIndex = stage === "$project" ? stageIndex : -1;
|
|
10312
|
+
this.trackGroupFieldNames(stage, operations, stageIndex);
|
|
10313
|
+
const renameStage = this.buildGroupRenameStage(builtStage, stageIndex);
|
|
10314
|
+
if (renameStage) pipeline.push(renameStage);
|
|
10315
|
+
};
|
|
10316
|
+
for (const op of this.orderStages(this.operations)) {
|
|
10317
|
+
if (op.mergeable && op.stage === currentStage) {
|
|
10318
|
+
currentBuffer.push(op);
|
|
10319
|
+
continue;
|
|
10320
|
+
}
|
|
10094
10321
|
if (currentBuffer.length > 0) {
|
|
10095
|
-
|
|
10096
|
-
if (builtStage) {
|
|
10097
|
-
const stageIndex = pipeline.length;
|
|
10098
|
-
pipeline.push(builtStage);
|
|
10099
|
-
this.trackGroupFieldNames(currentStage, currentBuffer, stageIndex);
|
|
10100
|
-
}
|
|
10322
|
+
emit(currentStage, currentBuffer);
|
|
10101
10323
|
currentBuffer = [];
|
|
10102
10324
|
}
|
|
10103
10325
|
if (op.mergeable) {
|
|
10104
10326
|
currentStage = op.stage;
|
|
10105
10327
|
currentBuffer.push(op);
|
|
10106
10328
|
} else {
|
|
10107
|
-
|
|
10108
|
-
if (builtStage) {
|
|
10109
|
-
const stageIndex = pipeline.length;
|
|
10110
|
-
pipeline.push(builtStage);
|
|
10111
|
-
this.trackGroupFieldNames(op.stage, [op], stageIndex);
|
|
10112
|
-
}
|
|
10329
|
+
emit(op.stage, [op]);
|
|
10113
10330
|
currentStage = null;
|
|
10114
10331
|
}
|
|
10115
10332
|
}
|
|
10116
|
-
if (currentBuffer.length > 0)
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10333
|
+
if (currentBuffer.length > 0) emit(currentStage, currentBuffer);
|
|
10334
|
+
return pipeline;
|
|
10335
|
+
}
|
|
10336
|
+
/**
|
|
10337
|
+
* Apply a raw escape-hatch operation to the pipeline built so far.
|
|
10338
|
+
*
|
|
10339
|
+
* - `joinRaw`: appends the caller-built stages verbatim.
|
|
10340
|
+
* - `raw`: calls the callback with the pipeline array. A returned array
|
|
10341
|
+
* replaces the pipeline; `undefined` keeps the (possibly mutated) array.
|
|
10342
|
+
*
|
|
10343
|
+
* @throws UnsupportedQueryOperationError when a `raw()` callback leaves
|
|
10344
|
+
* something other than an array of stage objects
|
|
10345
|
+
*/
|
|
10346
|
+
applyRawOperation(pipeline, op) {
|
|
10347
|
+
if (op.type === "joinRaw") {
|
|
10348
|
+
pipeline.push(...op.data.stages);
|
|
10349
|
+
return;
|
|
10123
10350
|
}
|
|
10124
|
-
|
|
10351
|
+
const result = op.data.builder(pipeline);
|
|
10352
|
+
const next = result === void 0 ? pipeline : result;
|
|
10353
|
+
if (!Array.isArray(next) || !next.every(isPipelineStageObject)) throw new UnsupportedQueryOperationError("raw", "mongodb", "The raw() callback receives the aggregation pipeline array and must return an array of stage objects, or mutate that array and return nothing.");
|
|
10354
|
+
if (next !== pipeline) pipeline.splice(0, pipeline.length, ...next);
|
|
10355
|
+
}
|
|
10356
|
+
/**
|
|
10357
|
+
* Order a `$sort` against the select `$project` it follows.
|
|
10358
|
+
*
|
|
10359
|
+
* A `$sort` after a `$project` cannot see fields the projection dropped, so
|
|
10360
|
+
* `select(["title"]).orderBy("likes")` would not sort. The sort moves before
|
|
10361
|
+
* the projection, unless it sorts by a field the projection computes
|
|
10362
|
+
* (`selectRaw({ score: ... }).orderBy("score")`), which only exists after it:
|
|
10363
|
+
*
|
|
10364
|
+
* - no computed sort key: `$sort`, `$project`
|
|
10365
|
+
* - only computed or selected sort keys: `$project`, `$sort` (unchanged)
|
|
10366
|
+
* - computed AND unselected sort keys: `$addFields` (the computed keys),
|
|
10367
|
+
* `$sort`, `$project` (those keys passed through)
|
|
10368
|
+
*/
|
|
10369
|
+
placeSortAroundProjection(projectStage, sortStage) {
|
|
10370
|
+
const projection = projectStage.$project;
|
|
10371
|
+
const sortKeys = Object.keys(sortStage.$sort);
|
|
10372
|
+
const isInclusionFlag = (value) => value === 1 || value === true || value === 0 || value === false;
|
|
10373
|
+
const computedKeys = sortKeys.filter((key) => {
|
|
10374
|
+
const value = projection[key];
|
|
10375
|
+
return value !== void 0 && !isInclusionFlag(value) && value !== `$${key}`;
|
|
10376
|
+
});
|
|
10377
|
+
if (computedKeys.length === 0) return [sortStage, projectStage];
|
|
10378
|
+
if (!sortKeys.some((key) => {
|
|
10379
|
+
const value = projection[key];
|
|
10380
|
+
return value === void 0 || value === 0 || value === false;
|
|
10381
|
+
})) return [projectStage, sortStage];
|
|
10382
|
+
const addFields = {};
|
|
10383
|
+
const passThrough = { ...projection };
|
|
10384
|
+
for (const key of computedKeys) {
|
|
10385
|
+
addFields[key] = projection[key];
|
|
10386
|
+
passThrough[key] = 1;
|
|
10387
|
+
}
|
|
10388
|
+
return [
|
|
10389
|
+
{ $addFields: addFields },
|
|
10390
|
+
sortStage,
|
|
10391
|
+
{ $project: passThrough }
|
|
10392
|
+
];
|
|
10125
10393
|
}
|
|
10126
10394
|
/**
|
|
10127
10395
|
* Reorder operations so filters run before projections, mirroring SQL
|
|
@@ -10130,11 +10398,13 @@ var MongoQueryParser = class {
|
|
|
10130
10398
|
* strips the filter column would run before the `$match` and silently drop
|
|
10131
10399
|
* every document (`select(["a"]).where("b", x)` → `[]`).
|
|
10132
10400
|
*
|
|
10133
|
-
* Only *mergeable*
|
|
10134
|
-
*
|
|
10135
|
-
*
|
|
10136
|
-
*
|
|
10137
|
-
*
|
|
10401
|
+
* Only *mergeable* operations move, and only within a segment of neighboring
|
|
10402
|
+
* mergeable `$match` / `$project` / `$sort` operations. Inside a segment the
|
|
10403
|
+
* order is `$match`, then `$project`, then `$sort` (each keeping call order),
|
|
10404
|
+
* and {@link placeSortAroundProjection} then decides whether the sort runs
|
|
10405
|
+
* before the projection. Any other operation — `$group`, `$lookup`,
|
|
10406
|
+
* `$limit`, `$skip`, `$setWindowFields`, raw escapes, having-style
|
|
10407
|
+
* post-group matches, `$sample` — is a barrier: nothing moves across it.
|
|
10138
10408
|
* So `groupBy(...).where(...)` still filters AFTER the group, and
|
|
10139
10409
|
* `limit(...)` / `random()` keep their call-order meaning.
|
|
10140
10410
|
*/
|
|
@@ -10144,7 +10414,8 @@ var MongoQueryParser = class {
|
|
|
10144
10414
|
const flushSegment = () => {
|
|
10145
10415
|
if (segment.length === 0) return;
|
|
10146
10416
|
reordered.push(...segment.filter((op) => op.stage === "$match"));
|
|
10147
|
-
reordered.push(...segment.filter((op) => op.stage
|
|
10417
|
+
reordered.push(...segment.filter((op) => op.stage === "$project"));
|
|
10418
|
+
reordered.push(...segment.filter((op) => op.stage === "$sort"));
|
|
10148
10419
|
segment = [];
|
|
10149
10420
|
};
|
|
10150
10421
|
for (const op of operations) if (op.mergeable && (op.stage === "$match" || op.stage === "$project" || op.stage === "$sort")) segment.push(op);
|
|
@@ -10183,35 +10454,26 @@ var MongoQueryParser = class {
|
|
|
10183
10454
|
if (distinctAliases.size > 0) this.countDistinctAliases.set(stageIndex, distinctAliases);
|
|
10184
10455
|
}
|
|
10185
10456
|
/**
|
|
10186
|
-
*
|
|
10187
|
-
*
|
|
10188
|
-
*
|
|
10189
|
-
*
|
|
10190
|
-
*
|
|
10191
|
-
* @param
|
|
10192
|
-
* @returns The
|
|
10193
|
-
*/
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
for (const field of aggregateFields) if (distinctAliases?.has(field)) projection[field] = { $size: `$${field}` };
|
|
10207
|
-
else projection[field] = 1;
|
|
10208
|
-
if (Object.keys(projection).length > 0) {
|
|
10209
|
-
projection._id = 0;
|
|
10210
|
-
processed.push({ $project: projection });
|
|
10211
|
-
}
|
|
10212
|
-
} else processed.push(stage);
|
|
10213
|
-
}
|
|
10214
|
-
return processed;
|
|
10457
|
+
* Build the `$project` that renames `_id` back to the grouped field name(s)
|
|
10458
|
+
* right after a `$group` stage with tracked field names, so results carry
|
|
10459
|
+
* the grouping columns instead of MongoDB's `_id`.
|
|
10460
|
+
*
|
|
10461
|
+
* @param stage - The stage just emitted
|
|
10462
|
+
* @param stageIndex - Its index in the pipeline
|
|
10463
|
+
* @returns The renaming `$project` stage, or null when none is needed
|
|
10464
|
+
*/
|
|
10465
|
+
buildGroupRenameStage(stage, stageIndex) {
|
|
10466
|
+
if (!stage.$group || !this.groupFieldNames.has(stageIndex)) return null;
|
|
10467
|
+
const fieldNames = this.groupFieldNames.get(stageIndex);
|
|
10468
|
+
const projection = {};
|
|
10469
|
+
if (typeof fieldNames === "string") projection[fieldNames] = "$_id";
|
|
10470
|
+
else if (Array.isArray(fieldNames) && fieldNames.length > 0) for (const fieldName of fieldNames) projection[fieldName] = `$_id.${fieldName}`;
|
|
10471
|
+
const distinctAliases = this.countDistinctAliases.get(stageIndex);
|
|
10472
|
+
const aggregateFields = Object.keys(stage.$group).filter((key) => key !== "_id");
|
|
10473
|
+
for (const field of aggregateFields) projection[field] = distinctAliases?.has(field) ? { $size: `$${field}` } : 1;
|
|
10474
|
+
if (Object.keys(projection).length === 0) return null;
|
|
10475
|
+
projection._id = 0;
|
|
10476
|
+
return { $project: projection };
|
|
10215
10477
|
}
|
|
10216
10478
|
/**
|
|
10217
10479
|
* Convert the parsed pipeline to a pretty-printed string for debugging.
|
|
@@ -10311,6 +10573,23 @@ var MongoQueryParser = class {
|
|
|
10311
10573
|
const [first] = operations;
|
|
10312
10574
|
return first === void 0 ? null : { $setWindowFields: first.data.spec };
|
|
10313
10575
|
}
|
|
10576
|
+
case "$unwind": {
|
|
10577
|
+
const [first] = operations;
|
|
10578
|
+
if (first === void 0) return null;
|
|
10579
|
+
const { path, ...options } = first.data;
|
|
10580
|
+
return Object.keys(options).length === 0 ? { $unwind: path } : { $unwind: {
|
|
10581
|
+
path,
|
|
10582
|
+
...options
|
|
10583
|
+
} };
|
|
10584
|
+
}
|
|
10585
|
+
case "$addFields": {
|
|
10586
|
+
const [first] = operations;
|
|
10587
|
+
return first === void 0 ? null : { $addFields: first.data };
|
|
10588
|
+
}
|
|
10589
|
+
case "$vectorSearch": {
|
|
10590
|
+
const [first] = operations;
|
|
10591
|
+
return first === void 0 ? null : { $vectorSearch: first.data };
|
|
10592
|
+
}
|
|
10314
10593
|
default: return null;
|
|
10315
10594
|
}
|
|
10316
10595
|
}
|
|
@@ -11127,6 +11406,13 @@ var MongoQueryParser = class {
|
|
|
11127
11406
|
const op = operations[0];
|
|
11128
11407
|
if (op === void 0) return null;
|
|
11129
11408
|
const options = op.data;
|
|
11409
|
+
if (Array.isArray(options.pipeline)) return { $lookup: {
|
|
11410
|
+
from: options.table,
|
|
11411
|
+
...options.localField !== void 0 ? { localField: options.localField } : {},
|
|
11412
|
+
...options.foreignField !== void 0 ? { foreignField: options.foreignField } : {},
|
|
11413
|
+
as: options.alias || options.table,
|
|
11414
|
+
pipeline: options.pipeline
|
|
11415
|
+
} };
|
|
11130
11416
|
return { $lookup: {
|
|
11131
11417
|
from: options.table,
|
|
11132
11418
|
localField: options.localField,
|
|
@@ -12385,23 +12671,35 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
12385
12671
|
return this;
|
|
12386
12672
|
}
|
|
12387
12673
|
/**
|
|
12388
|
-
* Performs a raw join using
|
|
12674
|
+
* Performs a raw join using caller-built aggregation stages.
|
|
12389
12675
|
*
|
|
12390
|
-
*
|
|
12676
|
+
* The expression is one pipeline stage (typically `{ $lookup: { ... } }`) or
|
|
12677
|
+
* an array of stages. They are emitted verbatim in call order, so a later
|
|
12678
|
+
* `where()` / `orderBy()` can use the joined fields.
|
|
12391
12679
|
*
|
|
12392
|
-
* @param expression -
|
|
12393
|
-
* @param _bindings -
|
|
12680
|
+
* @param expression - A pipeline stage object, or an array of stage objects
|
|
12681
|
+
* @param _bindings - Not used on MongoDB; kept for API consistency
|
|
12682
|
+
* @throws UnsupportedQueryOperationError when the expression is not a stage
|
|
12683
|
+
* object or an array of them (e.g. a SQL `JOIN` string)
|
|
12394
12684
|
*/
|
|
12395
12685
|
joinRaw(expression, _bindings) {
|
|
12396
|
-
|
|
12686
|
+
const stages = Array.isArray(expression) ? expression : [expression];
|
|
12687
|
+
if (stages.length === 0 || !stages.every(isPipelineStageObject)) throw new UnsupportedQueryOperationError("joinRaw", "mongodb", "Pass a pipeline stage object such as { $lookup: { ... } }, or an array of stage objects.");
|
|
12688
|
+
this.operationsHelper.addOperation("$raw", "joinRaw", { stages });
|
|
12397
12689
|
return this;
|
|
12398
12690
|
}
|
|
12399
12691
|
/**
|
|
12400
|
-
* Allows direct manipulation of the native MongoDB
|
|
12401
|
-
*
|
|
12692
|
+
* Allows direct manipulation of the native MongoDB aggregation pipeline.
|
|
12693
|
+
*
|
|
12694
|
+
* The callback receives the pipeline array built from the operations
|
|
12695
|
+
* recorded before this call. Return a new array of stages to replace it, or
|
|
12696
|
+
* mutate the array in place and return nothing. Operations recorded after
|
|
12697
|
+
* `raw()` are appended to that result.
|
|
12698
|
+
*
|
|
12699
|
+
* @param builder - Receives the pipeline array; returns a replacement or nothing
|
|
12402
12700
|
*/
|
|
12403
12701
|
raw(builder) {
|
|
12404
|
-
this.operationsHelper.
|
|
12702
|
+
this.operationsHelper.addOperation("$raw", "raw", { builder });
|
|
12405
12703
|
return this;
|
|
12406
12704
|
}
|
|
12407
12705
|
/**
|
|
@@ -12424,6 +12722,8 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
12424
12722
|
cloned.fetchingCallback = this.fetchingCallback?.bind(cloned);
|
|
12425
12723
|
cloned.hydratingCallback = this.hydratingCallback?.bind(cloned);
|
|
12426
12724
|
cloned.fetchedCallback = this.fetchedCallback?.bind(cloned);
|
|
12725
|
+
cloned.isLean = this.isLean;
|
|
12726
|
+
cloned.modelClass = this.modelClass;
|
|
12427
12727
|
cloned.pendingGlobalScopes = this.pendingGlobalScopes;
|
|
12428
12728
|
cloned.availableLocalScopes = this.availableLocalScopes;
|
|
12429
12729
|
cloned.disabledGlobalScopes = new Set(this.disabledGlobalScopes);
|
|
@@ -12432,6 +12732,13 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
12432
12732
|
return cloned;
|
|
12433
12733
|
}
|
|
12434
12734
|
/**
|
|
12735
|
+
* Switch to lean read mode — see `QueryBuilderContract.lean()`.
|
|
12736
|
+
*/
|
|
12737
|
+
lean() {
|
|
12738
|
+
super.lean();
|
|
12739
|
+
return this;
|
|
12740
|
+
}
|
|
12741
|
+
/**
|
|
12435
12742
|
* Row-level locking is a SQL capability — MongoDB has no
|
|
12436
12743
|
* `SELECT ... FOR UPDATE`. Throwing (instead of silently ignoring the call)
|
|
12437
12744
|
* keeps a queue-claim pattern from silently running unlocked.
|
|
@@ -12468,6 +12775,10 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
12468
12775
|
async get() {
|
|
12469
12776
|
const startTime = Date.now();
|
|
12470
12777
|
if (this.fetchingCallback) await this.fetchingCallback(this);
|
|
12778
|
+
if (this.isLean) {
|
|
12779
|
+
assertLeanCompatible(this);
|
|
12780
|
+
return stripHiddenFromLeanRecords(await this.execute(), this.modelClass);
|
|
12781
|
+
}
|
|
12471
12782
|
const rawRecords = await this.execute();
|
|
12472
12783
|
if (this.hydratingCallback) await this.hydratingCallback(rawRecords, {
|
|
12473
12784
|
query: this,
|
|
@@ -12967,6 +13278,26 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
12967
13278
|
return this;
|
|
12968
13279
|
}
|
|
12969
13280
|
/**
|
|
13281
|
+
* Deconstruct an array field into one document per element (`$unwind`).
|
|
13282
|
+
* The short form is emitted when no option is given. See
|
|
13283
|
+
* `QueryBuilderContract.unwind()`.
|
|
13284
|
+
*/
|
|
13285
|
+
unwind(field, options) {
|
|
13286
|
+
const data = { path: field.startsWith("$") ? field : `$${field}` };
|
|
13287
|
+
if (options?.preserveNullAndEmptyArrays !== void 0) data.preserveNullAndEmptyArrays = options.preserveNullAndEmptyArrays;
|
|
13288
|
+
if (options?.includeArrayIndex !== void 0) data.includeArrayIndex = options.includeArrayIndex;
|
|
13289
|
+
this.operationsHelper.addOperation("$unwind", "unwind", data, false);
|
|
13290
|
+
return this;
|
|
13291
|
+
}
|
|
13292
|
+
/**
|
|
13293
|
+
* Add computed fields while keeping every existing field (`$addFields`).
|
|
13294
|
+
* See `QueryBuilderContract.addFields()`.
|
|
13295
|
+
*/
|
|
13296
|
+
addFields(fields) {
|
|
13297
|
+
this.operationsHelper.addOperation("$addFields", "addFields", { ...fields }, false);
|
|
13298
|
+
return this;
|
|
13299
|
+
}
|
|
13300
|
+
/**
|
|
12970
13301
|
* Nearest-neighbour vector similarity search via MongoDB Atlas $vectorSearch.
|
|
12971
13302
|
*
|
|
12972
13303
|
* Adds two pipeline stages:
|
|
@@ -13124,6 +13455,29 @@ var MongoSyncAdapter = class {
|
|
|
13124
13455
|
}
|
|
13125
13456
|
};
|
|
13126
13457
|
|
|
13458
|
+
//#endregion
|
|
13459
|
+
//#region ../cascade/src/drivers/mongodb/mongodb-update-translator.ts
|
|
13460
|
+
/**
|
|
13461
|
+
* Translate cascade's driver-agnostic update into MongoDB's native form.
|
|
13462
|
+
*
|
|
13463
|
+
* Every operator is native except `$dec`, which MongoDB does not have: it is
|
|
13464
|
+
* folded into `$inc` with negated amounts. A pipeline (array-form) update is
|
|
13465
|
+
* returned untouched.
|
|
13466
|
+
*
|
|
13467
|
+
* @param update - Operator object or aggregation pipeline
|
|
13468
|
+
* @returns The update MongoDB accepts
|
|
13469
|
+
*/
|
|
13470
|
+
function toMongoUpdate(update) {
|
|
13471
|
+
if (Array.isArray(update) || !update.$dec) return update;
|
|
13472
|
+
const { $dec, ...rest } = update;
|
|
13473
|
+
const increments = { ...rest.$inc };
|
|
13474
|
+
for (const [field, amount] of Object.entries($dec)) increments[field] = (increments[field] ?? 0) - amount;
|
|
13475
|
+
return {
|
|
13476
|
+
...rest,
|
|
13477
|
+
$inc: increments
|
|
13478
|
+
};
|
|
13479
|
+
}
|
|
13480
|
+
|
|
13127
13481
|
//#endregion
|
|
13128
13482
|
//#region ../cascade/src/drivers/mongodb/mongodb-driver.ts
|
|
13129
13483
|
const DEFAULT_TRANSACTION_OPTIONS = {
|
|
@@ -13426,14 +13780,17 @@ var MongoDbDriver = class {
|
|
|
13426
13780
|
return (await this.getDatabaseInstance().collection(table).findOneAndReplace(filter, document))?.value;
|
|
13427
13781
|
}
|
|
13428
13782
|
/**
|
|
13429
|
-
* Find one and update a single document that matches the provided filter
|
|
13783
|
+
* Find one and update a single document that matches the provided filter.
|
|
13784
|
+
*
|
|
13785
|
+
* Native `upsert`, `arrayFilters`, pipeline updates and `returnDocument`
|
|
13786
|
+
* (default `"after"`).
|
|
13430
13787
|
*/
|
|
13431
13788
|
async findOneAndUpdate(table, filter, update, options) {
|
|
13432
13789
|
const collection = this.getDatabaseInstance().collection(table);
|
|
13433
13790
|
const mongoOptions = this.withSession(options);
|
|
13434
|
-
return await collection.findOneAndUpdate(filter, update, {
|
|
13435
|
-
|
|
13436
|
-
|
|
13791
|
+
return await collection.findOneAndUpdate(filter, toMongoUpdate(update), {
|
|
13792
|
+
...mongoOptions,
|
|
13793
|
+
returnDocument: options?.returnDocument ?? "after"
|
|
13437
13794
|
});
|
|
13438
13795
|
}
|
|
13439
13796
|
/**
|
|
@@ -13624,11 +13981,17 @@ var MongoDbDriver = class {
|
|
|
13624
13981
|
* Execute atomic operations (typically $inc/$set style updates) against documents.
|
|
13625
13982
|
*
|
|
13626
13983
|
* Uses `updateMany` so callers can atomically modify any set of documents.
|
|
13984
|
+
* `upsert`, `arrayFilters` and pipeline updates are native.
|
|
13627
13985
|
*/
|
|
13628
13986
|
async atomic(table, filter, operations, options) {
|
|
13629
13987
|
const collection = this.getDatabaseInstance().collection(table);
|
|
13630
|
-
const
|
|
13631
|
-
|
|
13988
|
+
const { returnDocument: _returnDocument, ...updateOptions } = options ?? {};
|
|
13989
|
+
const mongoOptions = this.withSession(updateOptions);
|
|
13990
|
+
const result = await collection.updateMany(filter, toMongoUpdate(operations), mongoOptions);
|
|
13991
|
+
return {
|
|
13992
|
+
modifiedCount: result.modifiedCount,
|
|
13993
|
+
upsertedCount: result.upsertedCount
|
|
13994
|
+
};
|
|
13632
13995
|
}
|
|
13633
13996
|
/**
|
|
13634
13997
|
* Lazily create (and cache) the Mongo sync adapter.
|
|
@@ -15884,6 +16247,13 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15884
16247
|
get driver() {
|
|
15885
16248
|
return this.dataSource.driver;
|
|
15886
16249
|
}
|
|
16250
|
+
/**
|
|
16251
|
+
* Switch to lean read mode — see `QueryBuilderContract.lean()`.
|
|
16252
|
+
*/
|
|
16253
|
+
lean() {
|
|
16254
|
+
super.lean();
|
|
16255
|
+
return this;
|
|
16256
|
+
}
|
|
15887
16257
|
clone() {
|
|
15888
16258
|
const cloned = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
15889
16259
|
cloned.operations = [...this.operations];
|
|
@@ -15895,6 +16265,7 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15895
16265
|
cloned.countRelations = new Map(this.countRelations);
|
|
15896
16266
|
cloned.relationDefinitions = this.relationDefinitions;
|
|
15897
16267
|
cloned.modelClass = this.modelClass;
|
|
16268
|
+
cloned.isLean = this.isLean;
|
|
15898
16269
|
cloned.hydrateCallback = this.hydrateCallback;
|
|
15899
16270
|
cloned.joinRelations = new Map(this.joinRelations);
|
|
15900
16271
|
cloned.joinRelationsApplied = this.joinRelationsApplied;
|
|
@@ -16013,6 +16384,22 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
16013
16384
|
* .get<VectorRow & { score: number }>();
|
|
16014
16385
|
* ```
|
|
16015
16386
|
*/
|
|
16387
|
+
/**
|
|
16388
|
+
* MongoDB-only stage. Always throws — see `QueryBuilderContract.unwind()`.
|
|
16389
|
+
*
|
|
16390
|
+
* @throws UnsupportedQueryOperationError
|
|
16391
|
+
*/
|
|
16392
|
+
unwind(_field, _options) {
|
|
16393
|
+
throw new UnsupportedQueryOperationError("unwind", "postgres", "Query the array column with jsonb_array_elements()/unnest() through selectRaw/joinRaw, or model the elements as a related table.");
|
|
16394
|
+
}
|
|
16395
|
+
/**
|
|
16396
|
+
* MongoDB-only stage. Always throws — see `QueryBuilderContract.addFields()`.
|
|
16397
|
+
*
|
|
16398
|
+
* @throws UnsupportedQueryOperationError
|
|
16399
|
+
*/
|
|
16400
|
+
addFields(_fields) {
|
|
16401
|
+
throw new UnsupportedQueryOperationError("addFields", "postgres", "Add computed columns with selectRaw() alongside the table columns, e.g. selectRaw('\"posts\".*').selectRaw(\"likes + shares AS score\").");
|
|
16402
|
+
}
|
|
16016
16403
|
similarTo(column, embedding, alias = "score") {
|
|
16017
16404
|
const literal = `[${embedding.join(",")}]`;
|
|
16018
16405
|
const quotedCol = this.driver.dialect.quoteIdentifier(column);
|
|
@@ -16210,6 +16597,7 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
16210
16597
|
this.applyHasRelations();
|
|
16211
16598
|
this.applyCountRelations();
|
|
16212
16599
|
this.applyGroupByAggregates();
|
|
16600
|
+
if (this.isLean) assertLeanCompatible(this);
|
|
16213
16601
|
if (this.fetchingCallback) await this.fetchingCallback(this);
|
|
16214
16602
|
const { query = "", bindings = [] } = new PostgresQueryParser({
|
|
16215
16603
|
table: this.table,
|
|
@@ -16217,6 +16605,10 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
16217
16605
|
}).parse();
|
|
16218
16606
|
try {
|
|
16219
16607
|
let records = (await this.driver.query(query, bindings)).rows;
|
|
16608
|
+
if (this.isLean) {
|
|
16609
|
+
this.operations = [];
|
|
16610
|
+
return stripHiddenFromLeanRecords(records, this.modelClass);
|
|
16611
|
+
}
|
|
16220
16612
|
const joinedData = this.extractJoinedRelationData(records);
|
|
16221
16613
|
if (this.hydratingCallback) await this.hydratingCallback(records, {});
|
|
16222
16614
|
if (this.hydrateCallback) records = records.map((row, index) => this.hydrateCallback(row, index));
|
|
@@ -17434,6 +17826,38 @@ var PostgresSyncAdapter = class {
|
|
|
17434
17826
|
}
|
|
17435
17827
|
};
|
|
17436
17828
|
|
|
17829
|
+
//#endregion
|
|
17830
|
+
//#region ../cascade/src/drivers/postgres/postgres-update-validator.ts
|
|
17831
|
+
/**
|
|
17832
|
+
* Update operators the Postgres driver can express in SQL.
|
|
17833
|
+
*
|
|
17834
|
+
* `$setOnInsert` only contributes to the INSERT half of an upsert; on a plain
|
|
17835
|
+
* update it is a no-op, exactly as in MongoDB.
|
|
17836
|
+
*/
|
|
17837
|
+
const SUPPORTED_OPERATORS = new Set([
|
|
17838
|
+
"$set",
|
|
17839
|
+
"$unset",
|
|
17840
|
+
"$inc",
|
|
17841
|
+
"$dec",
|
|
17842
|
+
"$setOnInsert"
|
|
17843
|
+
]);
|
|
17844
|
+
/**
|
|
17845
|
+
* Reject every part of an update the Postgres driver cannot perform, so no
|
|
17846
|
+
* operator or option is ever silently dropped.
|
|
17847
|
+
*
|
|
17848
|
+
* @param update - The requested update
|
|
17849
|
+
* @param options - The requested update options
|
|
17850
|
+
* @returns The update, narrowed to an operator object
|
|
17851
|
+
* @throws UnsupportedUpdateOperationError for pipeline updates, `arrayFilters`,
|
|
17852
|
+
* and any operator outside {@link SUPPORTED_OPERATORS}
|
|
17853
|
+
*/
|
|
17854
|
+
function assertPostgresUpdate(update, options) {
|
|
17855
|
+
if (Array.isArray(update)) throw new UnsupportedUpdateOperationError("pipeline update", "postgres", "Use an operator object ($set/$unset/$inc/$dec) or a raw query.");
|
|
17856
|
+
if (options?.arrayFilters !== void 0) throw new UnsupportedUpdateOperationError("arrayFilters", "postgres");
|
|
17857
|
+
for (const operator of Object.keys(update)) if (!SUPPORTED_OPERATORS.has(operator)) throw new UnsupportedUpdateOperationError(operator, "postgres", "Supported operators: $set, $unset, $inc, $dec, $setOnInsert.");
|
|
17858
|
+
return update;
|
|
17859
|
+
}
|
|
17860
|
+
|
|
17437
17861
|
//#endregion
|
|
17438
17862
|
//#region ../cascade/src/drivers/postgres/postgres-driver.ts
|
|
17439
17863
|
/**
|
|
@@ -17589,6 +18013,8 @@ var PostgresDriver = class {
|
|
|
17589
18013
|
* @see PostgresPoolConfig.nativeArrayColumns
|
|
17590
18014
|
*/
|
|
17591
18015
|
_nativeArrayColumns;
|
|
18016
|
+
/** Unique keys per table, resolved from `pg_index` for upsert conflict targets. */
|
|
18017
|
+
_uniqueKeys = /* @__PURE__ */ new Map();
|
|
17592
18018
|
/**
|
|
17593
18019
|
* Native-array columns discovered by introspecting the live schema on
|
|
17594
18020
|
* connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
|
|
@@ -17910,17 +18336,120 @@ var PostgresDriver = class {
|
|
|
17910
18336
|
}
|
|
17911
18337
|
}
|
|
17912
18338
|
/**
|
|
17913
|
-
* Find one
|
|
17914
|
-
*
|
|
17915
|
-
*
|
|
17916
|
-
*
|
|
17917
|
-
*
|
|
17918
|
-
*
|
|
18339
|
+
* Find one row matching the filter, update it, and return it.
|
|
18340
|
+
*
|
|
18341
|
+
* - `upsert`: `INSERT … ON CONFLICT (target) DO UPDATE … RETURNING *`, where
|
|
18342
|
+
* the target is the primary key or a unique index whose columns are all
|
|
18343
|
+
* equality keys of the filter (see {@link executeUpsert}).
|
|
18344
|
+
* - `returnDocument` (default `"after"`): `"before"` returns the row as it
|
|
18345
|
+
* was, and is not available together with `upsert`.
|
|
18346
|
+
*
|
|
18347
|
+
* The row is chosen with `SELECT … LIMIT 1 FOR UPDATE` and matched by primary
|
|
18348
|
+
* key with the filter repeated, so concurrent callers re-check the filter
|
|
18349
|
+
* against the latest committed row instead of overshooting it.
|
|
18350
|
+
*
|
|
18351
|
+
* @throws UnsupportedUpdateOperationError for pipeline updates, `arrayFilters`,
|
|
18352
|
+
* array operators, `before` + `upsert`, or an underivable conflict target
|
|
17919
18353
|
*/
|
|
17920
|
-
async findOneAndUpdate(table, filter, update,
|
|
17921
|
-
const
|
|
17922
|
-
const
|
|
17923
|
-
|
|
18354
|
+
async findOneAndUpdate(table, filter, update, options) {
|
|
18355
|
+
const operations = assertPostgresUpdate(update, options);
|
|
18356
|
+
const returnBefore = options?.returnDocument === "before";
|
|
18357
|
+
if (options?.upsert) {
|
|
18358
|
+
if (returnBefore) throw new UnsupportedUpdateOperationError("returnDocument: before with upsert", "postgres", "Use returnDocument: \"after\", or read the row before upserting inside a transaction.");
|
|
18359
|
+
const { row } = await this.executeUpsert(table, filter, operations);
|
|
18360
|
+
return row ?? null;
|
|
18361
|
+
}
|
|
18362
|
+
const primaryKey = (await this.getUniqueKeys(table)).find((key) => key.primary)?.columns;
|
|
18363
|
+
if (!primaryKey) {
|
|
18364
|
+
if (returnBefore) throw new UnsupportedUpdateOperationError("returnDocument: before without a primary key", "postgres");
|
|
18365
|
+
const { sql, params } = this.buildUpdateQuery(table, filter, operations, 1);
|
|
18366
|
+
return (await this.query(`${sql} RETURNING *`, params)).rows[0] ?? null;
|
|
18367
|
+
}
|
|
18368
|
+
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
18369
|
+
const keyList = (qualifier) => primaryKey.map((column) => (qualifier ? `${qualifier}.` : "") + this.dialect.quoteIdentifier(column)).join(", ");
|
|
18370
|
+
const set = this.buildSetClauses(table, operations, 1, returnBefore ? quotedTable : void 0);
|
|
18371
|
+
if (set.clauses.length === 0) throw new Error("No update operations specified");
|
|
18372
|
+
const inner = this.buildWhereClause(filter, set.nextIndex);
|
|
18373
|
+
const outer = this.buildWhereClause(filter, set.nextIndex + inner.whereParams.length, quotedTable);
|
|
18374
|
+
const params = [
|
|
18375
|
+
...set.params,
|
|
18376
|
+
...inner.whereParams,
|
|
18377
|
+
...outer.whereParams
|
|
18378
|
+
];
|
|
18379
|
+
const outerCondition = outer.condition ? ` AND ${outer.condition}` : "";
|
|
18380
|
+
if (returnBefore) {
|
|
18381
|
+
const before = this.dialect.quoteIdentifier("__cascade_before");
|
|
18382
|
+
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}.*`;
|
|
18383
|
+
return (await this.query(sql, params)).rows[0] ?? null;
|
|
18384
|
+
}
|
|
18385
|
+
const sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} WHERE (${keyList(quotedTable)}) = (SELECT ${keyList()} FROM ${quotedTable} ${inner.whereClause} LIMIT 1 FOR UPDATE)${outerCondition} RETURNING *`;
|
|
18386
|
+
return (await this.query(sql, params)).rows[0] ?? null;
|
|
18387
|
+
}
|
|
18388
|
+
/**
|
|
18389
|
+
* Upsert through `INSERT … ON CONFLICT (target) DO UPDATE SET … [WHERE rest] RETURNING *`.
|
|
18390
|
+
*
|
|
18391
|
+
* Conflict target rule: the first unique index of the table (primary key
|
|
18392
|
+
* first; partial and expression indexes excluded) whose columns are ALL
|
|
18393
|
+
* equality keys of the filter — a non-null value that is not an operator
|
|
18394
|
+
* object. Any other filter predicate becomes the `DO UPDATE … WHERE`, so an
|
|
18395
|
+
* existing row that fails it is left untouched and no row is returned.
|
|
18396
|
+
*
|
|
18397
|
+
* Inserted row: filter equality values, then `$setOnInsert`, then `$set`,
|
|
18398
|
+
* then `$inc` (n) / `$dec` (-n); `$unset` columns are omitted.
|
|
18399
|
+
*
|
|
18400
|
+
* @throws UnsupportedUpdateOperationError when no conflict target can be derived
|
|
18401
|
+
*/
|
|
18402
|
+
async executeUpsert(table, filter, operations) {
|
|
18403
|
+
const equalityKeys = new Set(Object.entries(filter).filter(([, value]) => value !== null && value !== void 0 && !this.isOperatorFilter(value)).map(([key]) => key));
|
|
18404
|
+
const target = (await this.getUniqueKeys(table)).find((key) => key.columns.every((column) => equalityKeys.has(column)));
|
|
18405
|
+
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}".`);
|
|
18406
|
+
const insertRow = {};
|
|
18407
|
+
for (const key of equalityKeys) insertRow[key] = filter[key];
|
|
18408
|
+
Object.assign(insertRow, operations.$setOnInsert, operations.$set);
|
|
18409
|
+
for (const [key, amount] of Object.entries(operations.$inc ?? {})) insertRow[key] = amount;
|
|
18410
|
+
for (const [key, amount] of Object.entries(operations.$dec ?? {})) insertRow[key] = -amount;
|
|
18411
|
+
for (const key of Object.keys(operations.$unset ?? {})) delete insertRow[key];
|
|
18412
|
+
const columns = Object.keys(insertRow).filter((key) => insertRow[key] !== void 0);
|
|
18413
|
+
const params = columns.map((key) => this.serializeValue(key, insertRow[key], table));
|
|
18414
|
+
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
18415
|
+
const quotedColumns = columns.map((column) => this.dialect.quoteIdentifier(column)).join(", ");
|
|
18416
|
+
const placeholders = columns.map((_, index) => this.dialect.placeholder(index + 1)).join(", ");
|
|
18417
|
+
const set = this.buildSetClauses(table, operations, columns.length + 1, quotedTable);
|
|
18418
|
+
params.push(...set.params);
|
|
18419
|
+
const firstTargetColumn = this.dialect.quoteIdentifier(target.columns[0]);
|
|
18420
|
+
const doUpdateSet = set.clauses.length > 0 ? set.clauses.join(", ") : `${firstTargetColumn} = ${quotedTable}.${firstTargetColumn}`;
|
|
18421
|
+
const remainingFilter = Object.fromEntries(Object.entries(filter).filter(([key]) => !target.columns.includes(key)));
|
|
18422
|
+
const where = this.buildWhereClause(remainingFilter, set.nextIndex, quotedTable);
|
|
18423
|
+
params.push(...where.whereParams);
|
|
18424
|
+
const conflictColumns = target.columns.map((column) => this.dialect.quoteIdentifier(column)).join(", ");
|
|
18425
|
+
const insertedFlag = "__cascade_inserted";
|
|
18426
|
+
const sql = `INSERT INTO ${quotedTable} (${quotedColumns}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns}) DO UPDATE SET ${doUpdateSet} ${where.whereClause} RETURNING *, (xmax = 0) AS "${insertedFlag}"`;
|
|
18427
|
+
const returned = (await this.query(sql, params)).rows[0];
|
|
18428
|
+
if (!returned) return { inserted: false };
|
|
18429
|
+
const { [insertedFlag]: inserted, ...row } = returned;
|
|
18430
|
+
return {
|
|
18431
|
+
row,
|
|
18432
|
+
inserted: inserted === true
|
|
18433
|
+
};
|
|
18434
|
+
}
|
|
18435
|
+
/**
|
|
18436
|
+
* Unique keys of a table (primary key first), read from `pg_index`.
|
|
18437
|
+
*
|
|
18438
|
+
* Partial and expression indexes are excluded — they cannot be an
|
|
18439
|
+
* `ON CONFLICT (columns)` target. Non-empty results are cached per table.
|
|
18440
|
+
*/
|
|
18441
|
+
async getUniqueKeys(table) {
|
|
18442
|
+
const cached = this._uniqueKeys.get(table);
|
|
18443
|
+
if (cached) return cached;
|
|
18444
|
+
const result = await this.query(`SELECT array_agg(a.attname::text ORDER BY k.ord) AS columns, i.indisprimary AS primary
|
|
18445
|
+
FROM pg_index i
|
|
18446
|
+
CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord)
|
|
18447
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
|
|
18448
|
+
WHERE i.indrelid = to_regclass($1) AND i.indisunique AND i.indpred IS NULL AND i.indexprs IS NULL
|
|
18449
|
+
GROUP BY i.indexrelid, i.indisprimary
|
|
18450
|
+
ORDER BY i.indisprimary DESC, i.indexrelid`, [this.dialect.quoteIdentifier(table)]);
|
|
18451
|
+
if (result.rows.length > 0) this._uniqueKeys.set(table, result.rows);
|
|
18452
|
+
return result.rows;
|
|
17924
18453
|
}
|
|
17925
18454
|
/**
|
|
17926
18455
|
* Update multiple rows matching the filter.
|
|
@@ -18127,8 +18656,16 @@ var PostgresDriver = class {
|
|
|
18127
18656
|
* @param options - Optional options
|
|
18128
18657
|
* @returns Update result
|
|
18129
18658
|
*/
|
|
18130
|
-
async atomic(table, filter, operations,
|
|
18131
|
-
const
|
|
18659
|
+
async atomic(table, filter, operations, options) {
|
|
18660
|
+
const update = assertPostgresUpdate(operations, options);
|
|
18661
|
+
if (options?.upsert) {
|
|
18662
|
+
const { row, inserted } = await this.executeUpsert(table, filter, update);
|
|
18663
|
+
return {
|
|
18664
|
+
modifiedCount: row && !inserted ? 1 : 0,
|
|
18665
|
+
upsertedCount: row && inserted ? 1 : 0
|
|
18666
|
+
};
|
|
18667
|
+
}
|
|
18668
|
+
const { sql, params } = this.buildUpdateQuery(table, filter, update);
|
|
18132
18669
|
return { modifiedCount: (await this.query(sql, params)).rowCount ?? 0 };
|
|
18133
18670
|
}
|
|
18134
18671
|
/**
|
|
@@ -18242,12 +18779,12 @@ var PostgresDriver = class {
|
|
|
18242
18779
|
* @param startParamIndex - Starting parameter index
|
|
18243
18780
|
* @returns Object with WHERE clause string and parameters
|
|
18244
18781
|
*/
|
|
18245
|
-
buildWhereClause(filter, startParamIndex) {
|
|
18782
|
+
buildWhereClause(filter, startParamIndex, qualifier) {
|
|
18246
18783
|
const conditions = [];
|
|
18247
18784
|
const params = [];
|
|
18248
18785
|
let paramIndex = startParamIndex;
|
|
18249
18786
|
for (const [key, value] of Object.entries(filter)) {
|
|
18250
|
-
const quotedKey = this.dialect.quoteIdentifier(key);
|
|
18787
|
+
const quotedKey = (qualifier ? `${qualifier}.` : "") + this.dialect.quoteIdentifier(key);
|
|
18251
18788
|
if (value === null) conditions.push(`${quotedKey} IS NULL`);
|
|
18252
18789
|
else if (this.isOperatorFilter(value)) for (const [operator, operand] of Object.entries(value)) switch (operator) {
|
|
18253
18790
|
case "$in":
|
|
@@ -18297,9 +18834,11 @@ var PostgresDriver = class {
|
|
|
18297
18834
|
params.push(value);
|
|
18298
18835
|
}
|
|
18299
18836
|
}
|
|
18837
|
+
const condition = conditions.join(" AND ");
|
|
18300
18838
|
return {
|
|
18301
|
-
whereClause:
|
|
18302
|
-
whereParams: params
|
|
18839
|
+
whereClause: condition ? `WHERE ${condition}` : "",
|
|
18840
|
+
whereParams: params,
|
|
18841
|
+
condition
|
|
18303
18842
|
};
|
|
18304
18843
|
}
|
|
18305
18844
|
/**
|
|
@@ -18314,39 +18853,54 @@ var PostgresDriver = class {
|
|
|
18314
18853
|
return keys.length > 0 && keys.every((key) => key.startsWith("$"));
|
|
18315
18854
|
}
|
|
18316
18855
|
/**
|
|
18317
|
-
* Build
|
|
18856
|
+
* Build the SET clauses for an update.
|
|
18318
18857
|
*
|
|
18319
|
-
* @param table - Target table name
|
|
18320
|
-
* @param
|
|
18321
|
-
* @param
|
|
18322
|
-
* @param
|
|
18323
|
-
*
|
|
18858
|
+
* @param table - Target table name (for value serialization)
|
|
18859
|
+
* @param update - Update operations (`$setOnInsert` is not part of SET)
|
|
18860
|
+
* @param startIndex - First placeholder index
|
|
18861
|
+
* @param qualifier - Quoted table name to qualify right-hand column references
|
|
18862
|
+
* (needed where another relation with the same columns is in scope)
|
|
18324
18863
|
*/
|
|
18325
|
-
|
|
18326
|
-
const
|
|
18864
|
+
buildSetClauses(table, update, startIndex, qualifier) {
|
|
18865
|
+
const clauses = [];
|
|
18327
18866
|
const params = [];
|
|
18328
|
-
let paramIndex =
|
|
18867
|
+
let paramIndex = startIndex;
|
|
18868
|
+
const reference = (quotedKey) => qualifier ? `${qualifier}.${quotedKey}` : quotedKey;
|
|
18329
18869
|
if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
|
|
18330
|
-
|
|
18870
|
+
clauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
18331
18871
|
params.push(value === void 0 ? value : this.serializeValue(key, value, table));
|
|
18332
18872
|
}
|
|
18333
|
-
if (update.$unset) for (const key of Object.keys(update.$unset))
|
|
18334
|
-
|
|
18335
|
-
const quotedKey = this.dialect.quoteIdentifier(key);
|
|
18336
|
-
setClauses.push(`${quotedKey} = COALESCE(${quotedKey}, 0) + ${this.dialect.placeholder(paramIndex++)}`);
|
|
18337
|
-
params.push(amount);
|
|
18338
|
-
}
|
|
18339
|
-
if (update.$dec) for (const [key, amount] of Object.entries(update.$dec)) {
|
|
18873
|
+
if (update.$unset) for (const key of Object.keys(update.$unset)) clauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
|
|
18874
|
+
for (const [operator, sign] of [["$inc", "+"], ["$dec", "-"]]) for (const [key, amount] of Object.entries(update[operator] ?? {})) {
|
|
18340
18875
|
const quotedKey = this.dialect.quoteIdentifier(key);
|
|
18341
|
-
|
|
18876
|
+
clauses.push(`${quotedKey} = COALESCE(${reference(quotedKey)}, 0) ${sign} ${this.dialect.placeholder(paramIndex++)}`);
|
|
18342
18877
|
params.push(amount);
|
|
18343
18878
|
}
|
|
18344
|
-
|
|
18879
|
+
return {
|
|
18880
|
+
clauses,
|
|
18881
|
+
params,
|
|
18882
|
+
nextIndex: paramIndex
|
|
18883
|
+
};
|
|
18884
|
+
}
|
|
18885
|
+
/**
|
|
18886
|
+
* Build an UPDATE query from update operations.
|
|
18887
|
+
*
|
|
18888
|
+
* @param table - Target table name
|
|
18889
|
+
* @param filter - Filter conditions
|
|
18890
|
+
* @param update - Update operations
|
|
18891
|
+
* @param limit - Optional limit (for single row update)
|
|
18892
|
+
* @returns Object with SQL and parameters
|
|
18893
|
+
* @throws UnsupportedUpdateOperationError for operations SQL cannot express
|
|
18894
|
+
*/
|
|
18895
|
+
buildUpdateQuery(table, filter, update, limit) {
|
|
18896
|
+
const set = this.buildSetClauses(table, assertPostgresUpdate(update), 1);
|
|
18897
|
+
if (set.clauses.length === 0) throw new Error("No update operations specified");
|
|
18898
|
+
const params = [...set.params];
|
|
18345
18899
|
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
18346
|
-
const { whereClause, whereParams } = this.buildWhereClause(filter,
|
|
18900
|
+
const { whereClause, whereParams } = this.buildWhereClause(filter, set.nextIndex);
|
|
18347
18901
|
params.push(...whereParams);
|
|
18348
|
-
let sql = `UPDATE ${quotedTable} SET ${
|
|
18349
|
-
if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${
|
|
18902
|
+
let sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} ${whereClause}`;
|
|
18903
|
+
if (limit === 1 && whereClause) sql = `UPDATE ${quotedTable} SET ${set.clauses.join(", ")} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${whereClause} LIMIT 1)`;
|
|
18350
18904
|
return {
|
|
18351
18905
|
sql,
|
|
18352
18906
|
params
|
|
@@ -23101,6 +23655,9 @@ exports.SyncManager = SyncManager;
|
|
|
23101
23655
|
exports.TransactionRollbackError = TransactionRollbackError;
|
|
23102
23656
|
exports.UnsafeFilterError = UnsafeFilterError;
|
|
23103
23657
|
exports.UnsafeRawExpressionError = UnsafeRawExpressionError;
|
|
23658
|
+
exports.UnsupportedLeanOperationError = UnsupportedLeanOperationError;
|
|
23659
|
+
exports.UnsupportedQueryOperationError = UnsupportedQueryOperationError;
|
|
23660
|
+
exports.UnsupportedUpdateOperationError = UnsupportedUpdateOperationError;
|
|
23104
23661
|
exports.arrayBigInt = arrayBigInt;
|
|
23105
23662
|
exports.arrayBoolean = arrayBoolean;
|
|
23106
23663
|
exports.arrayDate = arrayDate;
|