@warlock.js/cascade 4.6.1 → 4.8.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 +21 -0
- package/cjs/index.cjs +277 -26
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +8 -0
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/query-builder.contract.d.mts +36 -1
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +5 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +5 -0
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +4 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs +5 -2
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +15 -0
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +24 -0
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +16 -0
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +33 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +16 -0
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +65 -1
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +14 -3
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +44 -8
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +6 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +13 -0
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-sql-serializer.mjs +15 -4
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/migration/migration-runner.d.mts.map +1 -1
- package/esm/migration/migration-runner.mjs +25 -3
- package/esm/migration/migration-runner.mjs.map +1 -1
- package/esm/migration/migration.d.mts +6 -3
- package/esm/migration/migration.d.mts.map +1 -1
- package/esm/migration/migration.mjs +6 -3
- package/esm/migration/migration.mjs.map +1 -1
- package/esm/model/methods/scope-methods.mjs +18 -4
- package/esm/model/methods/scope-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +6 -0
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs +6 -0
- package/esm/model/model.mjs.map +1 -1
- package/esm/query-builder/query-builder.d.mts +12 -1
- package/esm/query-builder/query-builder.d.mts.map +1 -1
- package/esm/query-builder/query-builder.mjs +19 -0
- package/esm/query-builder/query-builder.mjs.map +1 -1
- package/llms-full.txt +31 -1
- package/llms.txt +1 -1
- package/package.json +4 -4
- package/skills/README.md +1 -1
- package/skills/manage-transactions/SKILL.md +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to `@warlock.js/cascade` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 4.7.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `lockForUpdate({ skipLocked?, noWait? })` — row locking on SELECT (`FOR UPDATE [SKIP LOCKED | NOWAIT]`), the concurrent job-queue claim shape; Postgres-only, the MongoDB driver throws
|
|
12
|
+
- `DatabaseDriverContract.supportsSqlSerialization` — capability flag (default `true`); `false` routes the MigrationRunner through direct migration-driver execution
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Postgres model-level `sum`/`avg`/`min`/`max`/`distinct`/`countDistinct`/`pluck`/`value` no longer return `0`/`undefined` — the hydration callback is reset before reading, matching MongoDB
|
|
17
|
+
- Postgres `Model.findAndUpdate` / `Model.atomic` now update every matching row instead of one arbitrary row (a hidden `LIMIT 1`; MongoDB was already multi-row)
|
|
18
|
+
- Postgres query-builder `update()` / `unset()` now honor the chained `where` filter — previously they updated the whole table
|
|
19
|
+
- Postgres query-builder `deleteOne()` deletes exactly one row — the internal `limit(1)` was silently ignored, deleting every matching row
|
|
20
|
+
- Postgres pivot `detach(ids)` (and `sync` / `toggle`) works — the driver translates Mongo-style filter operators (`$in`, `$nin`, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) instead of binding the operator object literally
|
|
21
|
+
- CHECK constraints are no longer silently dropped on the MigrationRunner SQL path — the Postgres serializer emits `ADD CONSTRAINT ... CHECK` for `this.check(...)` and column `.check(...)`
|
|
22
|
+
- MongoDB `with()` eager loading is no longer a silent no-op — `get()` runs the relation loader, same wiring as Postgres
|
|
23
|
+
- MongoDB pipelines order `$match` before `$project` (SQL semantics), so `select()` before `where()` no longer strips the filter column and returns `[]` — fixes pivot `attach` de-duplication and `sync` / `toggle` deltas
|
|
24
|
+
- The MigrationRunner works on MongoDB — migrations execute directly through the migration driver; `exportSQL` stays SQL-only with a clear unsupported error
|
|
25
|
+
- MongoDB `dropIndex(table, name)` honors the literal index name — the string form is no longer rewritten to `<name>_1` (the columns-array form keeps the convention name)
|
|
26
|
+
- `addGlobalScope` / `addLocalScope` register per-subclass — a scope added on one model (e.g. a soft-delete `notDeleted`) no longer leaks onto every other model
|
|
27
|
+
|
|
7
28
|
## 4.6.1
|
|
8
29
|
|
|
9
30
|
### Fixed
|
package/cjs/index.cjs
CHANGED
|
@@ -4812,20 +4812,34 @@ async function restoreAllRecords(ModelClass, options) {
|
|
|
4812
4812
|
|
|
4813
4813
|
//#endregion
|
|
4814
4814
|
//#region ../@warlock.js/cascade/src/model/methods/scope-methods.ts
|
|
4815
|
+
/**
|
|
4816
|
+
* Give the model class its OWN scope map before mutating.
|
|
4817
|
+
*
|
|
4818
|
+
* The `globalScopes` / `localScopes` statics live on the base `Model`; without
|
|
4819
|
+
* this, `ModelClass.globalScopes.set(...)` from any subclass mutates the ONE
|
|
4820
|
+
* inherited Map and the scope leaks onto every other model (a soft-delete
|
|
4821
|
+
* `notDeleted` scope registered on `User` would filter `Post` too). The own
|
|
4822
|
+
* map snapshots the currently-inherited entries so parent scopes registered so
|
|
4823
|
+
* far are kept.
|
|
4824
|
+
*/
|
|
4825
|
+
function ownScopeMap(ModelClass, property) {
|
|
4826
|
+
if (!Object.prototype.hasOwnProperty.call(ModelClass, property)) ModelClass[property] = new Map(ModelClass[property]);
|
|
4827
|
+
return ModelClass[property];
|
|
4828
|
+
}
|
|
4815
4829
|
function addGlobalModelScope(ModelClass, name, callback, options = {}) {
|
|
4816
|
-
ModelClass
|
|
4830
|
+
ownScopeMap(ModelClass, "globalScopes").set(name, {
|
|
4817
4831
|
callback,
|
|
4818
4832
|
timing: options.timing || "before"
|
|
4819
4833
|
});
|
|
4820
4834
|
}
|
|
4821
4835
|
function removeGlobalModelScope(ModelClass, name) {
|
|
4822
|
-
ModelClass
|
|
4836
|
+
ownScopeMap(ModelClass, "globalScopes").delete(name);
|
|
4823
4837
|
}
|
|
4824
4838
|
function addLocalModelScope(ModelClass, name, callback) {
|
|
4825
|
-
ModelClass
|
|
4839
|
+
ownScopeMap(ModelClass, "localScopes").set(name, callback);
|
|
4826
4840
|
}
|
|
4827
4841
|
function removeLocalModelScope(ModelClass, name) {
|
|
4828
|
-
ModelClass
|
|
4842
|
+
ownScopeMap(ModelClass, "localScopes").delete(name);
|
|
4829
4843
|
}
|
|
4830
4844
|
|
|
4831
4845
|
//#endregion
|
|
@@ -5417,11 +5431,17 @@ var Model = class Model {
|
|
|
5417
5431
|
/**
|
|
5418
5432
|
* Global scopes that are automatically applied to all queries.
|
|
5419
5433
|
* These scopes are inherited by child models.
|
|
5434
|
+
*
|
|
5435
|
+
* Registration via `addGlobalScope` is per-subclass: the registering class
|
|
5436
|
+
* gets its own map (seeded with the entries inherited so far), so a scope
|
|
5437
|
+
* added on one model never leaks onto sibling models.
|
|
5420
5438
|
*/
|
|
5421
5439
|
static globalScopes = /* @__PURE__ */ new Map();
|
|
5422
5440
|
/**
|
|
5423
5441
|
* Local scopes that can be manually applied to queries.
|
|
5424
5442
|
* These are reusable query snippets that developers opt into.
|
|
5443
|
+
*
|
|
5444
|
+
* Registration via `addLocalScope` is per-subclass, like `globalScopes`.
|
|
5425
5445
|
*/
|
|
5426
5446
|
static localScopes = /* @__PURE__ */ new Map();
|
|
5427
5447
|
/**
|
|
@@ -7868,12 +7888,15 @@ var MongoMigrationDriver = class {
|
|
|
7868
7888
|
/**
|
|
7869
7889
|
* Drop an index by name or columns.
|
|
7870
7890
|
*
|
|
7891
|
+
* A string is the literal index name and is passed through untouched; a
|
|
7892
|
+
* columns array resolves to the MongoDB convention name
|
|
7893
|
+
* (`"column1_1_column2_1"`) those columns auto-name to.
|
|
7894
|
+
*
|
|
7871
7895
|
* @param indexNameOrColumns - Index name (string) or columns array
|
|
7872
7896
|
*/
|
|
7873
7897
|
async dropIndex(table, indexNameOrColumns) {
|
|
7874
7898
|
const collection = this.db.collection(table);
|
|
7875
|
-
|
|
7876
|
-
const indexName = indexNameOrColumns.map((col) => `${col}_1`).join("_");
|
|
7899
|
+
const indexName = Array.isArray(indexNameOrColumns) ? indexNameOrColumns.map((col) => `${col}_1`).join("_") : indexNameOrColumns;
|
|
7877
7900
|
await collection.dropIndex(indexName);
|
|
7878
7901
|
}
|
|
7879
7902
|
/**
|
|
@@ -9209,6 +9232,25 @@ var QueryBuilder = class QueryBuilder {
|
|
|
9209
9232
|
return this.limit(value);
|
|
9210
9233
|
}
|
|
9211
9234
|
/**
|
|
9235
|
+
* Lock the selected rows for update (`SELECT ... FOR UPDATE`).
|
|
9236
|
+
*
|
|
9237
|
+
* `skipLocked` skips rows other transactions hold locks on (concurrent
|
|
9238
|
+
* queue-claim shape); `noWait` errors immediately instead of waiting. The
|
|
9239
|
+
* two are mutually exclusive. Only meaningful inside a transaction.
|
|
9240
|
+
*
|
|
9241
|
+
* SQL drivers emit the locking clause; drivers without row locking
|
|
9242
|
+
* (MongoDB) override this to throw.
|
|
9243
|
+
*/
|
|
9244
|
+
lockForUpdate(options) {
|
|
9245
|
+
if (options?.skipLocked && options?.noWait) throw new Error("lockForUpdate: `skipLocked` and `noWait` are mutually exclusive.");
|
|
9246
|
+
this.addOperation("lock", {
|
|
9247
|
+
mode: "update",
|
|
9248
|
+
skipLocked: options?.skipLocked ?? false,
|
|
9249
|
+
noWait: options?.noWait ?? false
|
|
9250
|
+
});
|
|
9251
|
+
return this;
|
|
9252
|
+
}
|
|
9253
|
+
/**
|
|
9212
9254
|
* GROUP BY clause.
|
|
9213
9255
|
* @example q.groupBy("status")
|
|
9214
9256
|
* @example q.groupBy(["year", "month"])
|
|
@@ -9628,7 +9670,7 @@ var MongoQueryParser = class {
|
|
|
9628
9670
|
const pipeline = [];
|
|
9629
9671
|
let currentStage = null;
|
|
9630
9672
|
let currentBuffer = [];
|
|
9631
|
-
for (const op of this.operations) if (op.mergeable && op.stage === currentStage) currentBuffer.push(op);
|
|
9673
|
+
for (const op of this.orderStages(this.operations)) if (op.mergeable && op.stage === currentStage) currentBuffer.push(op);
|
|
9632
9674
|
else {
|
|
9633
9675
|
if (currentBuffer.length > 0) {
|
|
9634
9676
|
const builtStage = this.buildStage(currentStage, currentBuffer);
|
|
@@ -9663,6 +9705,38 @@ var MongoQueryParser = class {
|
|
|
9663
9705
|
return this.postProcessGroupStages(pipeline);
|
|
9664
9706
|
}
|
|
9665
9707
|
/**
|
|
9708
|
+
* Reorder operations so filters run before projections, mirroring SQL
|
|
9709
|
+
* semantics: in `select(...).where(...)`, the WHERE always applies to the
|
|
9710
|
+
* source columns regardless of call order. Without this, a `$project` that
|
|
9711
|
+
* strips the filter column would run before the `$match` and silently drop
|
|
9712
|
+
* every document (`select(["a"]).where("b", x)` → `[]`).
|
|
9713
|
+
*
|
|
9714
|
+
* Only *mergeable* `$match` operations are hoisted, and only within a
|
|
9715
|
+
* segment of neighboring mergeable `$match` / `$project` / `$sort`
|
|
9716
|
+
* operations. Any other operation — `$group`, `$lookup`, `$limit`, `$skip`,
|
|
9717
|
+
* `$setWindowFields`, or a non-mergeable op (raw escapes, having-style
|
|
9718
|
+
* post-group matches, `$sample`) — is a barrier: nothing moves across it.
|
|
9719
|
+
* So `groupBy(...).where(...)` still filters AFTER the group, and
|
|
9720
|
+
* `limit(...)` / `random()` keep their call-order meaning.
|
|
9721
|
+
*/
|
|
9722
|
+
orderStages(operations) {
|
|
9723
|
+
const reordered = [];
|
|
9724
|
+
let segment = [];
|
|
9725
|
+
const flushSegment = () => {
|
|
9726
|
+
if (segment.length === 0) return;
|
|
9727
|
+
reordered.push(...segment.filter((op) => op.stage === "$match"));
|
|
9728
|
+
reordered.push(...segment.filter((op) => op.stage !== "$match"));
|
|
9729
|
+
segment = [];
|
|
9730
|
+
};
|
|
9731
|
+
for (const op of operations) if (op.mergeable && (op.stage === "$match" || op.stage === "$project" || op.stage === "$sort")) segment.push(op);
|
|
9732
|
+
else {
|
|
9733
|
+
flushSegment();
|
|
9734
|
+
reordered.push(op);
|
|
9735
|
+
}
|
|
9736
|
+
flushSegment();
|
|
9737
|
+
return reordered;
|
|
9738
|
+
}
|
|
9739
|
+
/**
|
|
9666
9740
|
* Track field names for group stages that need _id renaming.
|
|
9667
9741
|
*/
|
|
9668
9742
|
trackGroupFieldNames(stage, operations, stageIndex) {
|
|
@@ -11894,6 +11968,14 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
11894
11968
|
return cloned;
|
|
11895
11969
|
}
|
|
11896
11970
|
/**
|
|
11971
|
+
* Row-level locking is a SQL capability — MongoDB has no
|
|
11972
|
+
* `SELECT ... FOR UPDATE`. Throwing (instead of silently ignoring the call)
|
|
11973
|
+
* keeps a queue-claim pattern from silently running unlocked.
|
|
11974
|
+
*/
|
|
11975
|
+
lockForUpdate() {
|
|
11976
|
+
throw new Error("lockForUpdate() is not supported by the MongoDB driver — MongoDB has no row-level SELECT locking. Use an atomic claim instead (e.g. findOneAndUpdate with a reservation filter).");
|
|
11977
|
+
}
|
|
11978
|
+
/**
|
|
11897
11979
|
* Executes a callback with the query builder without breaking the chain.
|
|
11898
11980
|
* @param callback - Function to execute with the builder
|
|
11899
11981
|
*/
|
|
@@ -11928,6 +12010,7 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
11928
12010
|
hydrateCallback: this.hydrateCallback
|
|
11929
12011
|
});
|
|
11930
12012
|
const hydratedRecords = this.hydrateCallback ? rawRecords.map(this.hydrateCallback) : rawRecords;
|
|
12013
|
+
await this.applyEagerLoading(hydratedRecords);
|
|
11931
12014
|
if (this.fetchedCallback) await this.fetchedCallback(hydratedRecords, {
|
|
11932
12015
|
query: this,
|
|
11933
12016
|
rawRecords,
|
|
@@ -11936,6 +12019,20 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
11936
12019
|
return hydratedRecords;
|
|
11937
12020
|
}
|
|
11938
12021
|
/**
|
|
12022
|
+
* Run the RelationLoader against the fetched documents for every relation
|
|
12023
|
+
* registered via `with()`. Mutates each model instance in place — attaches
|
|
12024
|
+
* loaded relations onto `model.loadedRelations` and as direct properties.
|
|
12025
|
+
*
|
|
12026
|
+
* Skipped silently when `modelClass` is absent (raw driver-level
|
|
12027
|
+
* `queryBuilder()` usage has no relations map to consult).
|
|
12028
|
+
*/
|
|
12029
|
+
async applyEagerLoading(records) {
|
|
12030
|
+
if (!this.modelClass || this.eagerLoadRelations.size === 0 || records.length === 0) return;
|
|
12031
|
+
const constraints = {};
|
|
12032
|
+
for (const [name, constraint] of this.eagerLoadRelations) if (typeof constraint === "function") constraints[name] = constraint;
|
|
12033
|
+
await new RelationLoader(records, this.modelClass).load([...this.eagerLoadRelations.keys()], constraints);
|
|
12034
|
+
}
|
|
12035
|
+
/**
|
|
11939
12036
|
* Execute the query and get first result
|
|
11940
12037
|
* This is different than `first` as first adds a `limit = 1` to the pipeline
|
|
11941
12038
|
*/
|
|
@@ -13159,6 +13256,11 @@ var MongoDbDriver = class {
|
|
|
13159
13256
|
return baseOptions;
|
|
13160
13257
|
}
|
|
13161
13258
|
/**
|
|
13259
|
+
* MongoDB has no SQL dialect — the MigrationRunner executes migrations
|
|
13260
|
+
* through the migration driver directly instead of Migration.toSQL().
|
|
13261
|
+
*/
|
|
13262
|
+
supportsSqlSerialization = false;
|
|
13263
|
+
/**
|
|
13162
13264
|
* Return a SQL serializer for this driver's dialect.
|
|
13163
13265
|
* Not supported for MongoDB.
|
|
13164
13266
|
*/
|
|
@@ -14519,6 +14621,11 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14519
14621
|
*/
|
|
14520
14622
|
isDistinct = false;
|
|
14521
14623
|
/**
|
|
14624
|
+
* Row-locking clause (`FOR UPDATE [SKIP LOCKED | NOWAIT]`). Appended after
|
|
14625
|
+
* LIMIT/OFFSET — the locking clause is last in PostgreSQL's SELECT grammar.
|
|
14626
|
+
*/
|
|
14627
|
+
lockClause = "";
|
|
14628
|
+
/**
|
|
14522
14629
|
* Whether the query has any JOIN operations (pre-scanned before processing).
|
|
14523
14630
|
* Used by qualifyColumn() to decide whether to prefix columns with the main table.
|
|
14524
14631
|
*/
|
|
@@ -14688,6 +14795,13 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14688
14795
|
case "offset":
|
|
14689
14796
|
this.offsetValue = data.value;
|
|
14690
14797
|
break;
|
|
14798
|
+
case "lock": {
|
|
14799
|
+
let clause = "FOR UPDATE";
|
|
14800
|
+
if (data.skipLocked) clause += " SKIP LOCKED";
|
|
14801
|
+
else if (data.noWait) clause += " NOWAIT";
|
|
14802
|
+
this.lockClause = clause;
|
|
14803
|
+
break;
|
|
14804
|
+
}
|
|
14691
14805
|
case "distinct":
|
|
14692
14806
|
this.isDistinct = true;
|
|
14693
14807
|
break;
|
|
@@ -14718,6 +14832,7 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14718
14832
|
if (this.orderClauses.length > 0) parts.push(`ORDER BY ${this.orderClauses.join(", ")}`);
|
|
14719
14833
|
const limitOffset = this.dialect.limitOffset(this.limitValue, this.offsetValue);
|
|
14720
14834
|
if (limitOffset) parts.push(limitOffset);
|
|
14835
|
+
if (this.lockClause) parts.push(this.lockClause);
|
|
14721
14836
|
return parts.join(" ");
|
|
14722
14837
|
}
|
|
14723
14838
|
/**
|
|
@@ -15708,38 +15823,45 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15708
15823
|
/** SUM a numeric field. */
|
|
15709
15824
|
async sum(field) {
|
|
15710
15825
|
this.applyPendingScopes();
|
|
15826
|
+
this.hydrateCallback = void 0;
|
|
15711
15827
|
const result = await this.selectRaw(`SUM(${field}) as sum`).first();
|
|
15712
15828
|
return parseFloat(result?.sum ?? "0");
|
|
15713
15829
|
}
|
|
15714
15830
|
/** AVG of a numeric field. */
|
|
15715
15831
|
async avg(field) {
|
|
15716
15832
|
this.applyPendingScopes();
|
|
15833
|
+
this.hydrateCallback = void 0;
|
|
15717
15834
|
const result = await this.selectRaw(`AVG(${field}) as avg`).first();
|
|
15718
15835
|
return parseFloat(result?.avg ?? "0");
|
|
15719
15836
|
}
|
|
15720
15837
|
/** MIN of a numeric field. */
|
|
15721
15838
|
async min(field) {
|
|
15722
15839
|
this.applyPendingScopes();
|
|
15840
|
+
this.hydrateCallback = void 0;
|
|
15723
15841
|
const result = await this.selectRaw(`MIN(${field}) as min`).first();
|
|
15724
15842
|
return parseFloat(result?.min ?? "0");
|
|
15725
15843
|
}
|
|
15726
15844
|
/** MAX of a numeric field. */
|
|
15727
15845
|
async max(field) {
|
|
15728
15846
|
this.applyPendingScopes();
|
|
15847
|
+
this.hydrateCallback = void 0;
|
|
15729
15848
|
const result = await this.selectRaw(`MAX(${field}) as max`).first();
|
|
15730
15849
|
return parseFloat(result?.max ?? "0");
|
|
15731
15850
|
}
|
|
15732
15851
|
/** Get distinct values for a field. */
|
|
15733
15852
|
async distinct(field) {
|
|
15853
|
+
this.hydrateCallback = void 0;
|
|
15734
15854
|
this.distinctValues(field);
|
|
15735
15855
|
return (await this.get()).map((row) => row[field]);
|
|
15736
15856
|
}
|
|
15737
15857
|
/** Get array of all values for a single field. */
|
|
15738
15858
|
async pluck(field) {
|
|
15859
|
+
this.hydrateCallback = void 0;
|
|
15739
15860
|
return (await this.select([field]).get()).map((row) => row[field]);
|
|
15740
15861
|
}
|
|
15741
15862
|
/** Get a single scalar value. */
|
|
15742
15863
|
async value(field) {
|
|
15864
|
+
this.hydrateCallback = void 0;
|
|
15743
15865
|
return (await this.select([field]).first())?.[field] ?? null;
|
|
15744
15866
|
}
|
|
15745
15867
|
/** Check whether any matching rows exist. */
|
|
@@ -15752,6 +15874,7 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15752
15874
|
}
|
|
15753
15875
|
/** COUNT DISTINCT a field. */
|
|
15754
15876
|
async countDistinct(field) {
|
|
15877
|
+
this.hydrateCallback = void 0;
|
|
15755
15878
|
const result = await this.selectRaw(`COUNT(DISTINCT ${field}) as count`).first();
|
|
15756
15879
|
return parseInt(result?.count ?? "0", 10);
|
|
15757
15880
|
}
|
|
@@ -15876,21 +15999,49 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15876
15999
|
const deleteSql = `DELETE FROM ${this.driver.dialect.quoteIdentifier(this.table)} ${sql}`;
|
|
15877
16000
|
return (await this.driver.query(deleteSql, params)).rowCount ?? 0;
|
|
15878
16001
|
}
|
|
15879
|
-
/**
|
|
16002
|
+
/**
|
|
16003
|
+
* Delete the first matching row. Wraps the filter in a ctid subquery —
|
|
16004
|
+
* Postgres has no `DELETE ... LIMIT`, and `delete()`'s filter only reads
|
|
16005
|
+
* `where*` ops, so a plain `limit(1)` would be silently ignored and every
|
|
16006
|
+
* matching row deleted (MongoDB's deleteOne() is single-document).
|
|
16007
|
+
*/
|
|
15880
16008
|
async deleteOne() {
|
|
15881
|
-
|
|
16009
|
+
this.applyPendingScopes();
|
|
16010
|
+
const { sql: filterSql, params } = this.buildFilter();
|
|
16011
|
+
const quotedTable = this.driver.dialect.quoteIdentifier(this.table);
|
|
16012
|
+
const deleteSql = `DELETE FROM ${quotedTable} WHERE ctid IN (SELECT ctid FROM ${quotedTable} ${filterSql} LIMIT 1)`;
|
|
16013
|
+
return (await this.driver.query(deleteSql, params)).rowCount ?? 0;
|
|
15882
16014
|
}
|
|
15883
|
-
/**
|
|
16015
|
+
/**
|
|
16016
|
+
* Update matching rows. Honors every chained `where*` (and applied scopes)
|
|
16017
|
+
* — without a filter the whole table is updated, matching SQL semantics.
|
|
16018
|
+
*/
|
|
15884
16019
|
async update(fields) {
|
|
15885
16020
|
this.applyPendingScopes();
|
|
15886
|
-
|
|
16021
|
+
const { sql: filterSql, params: filterParams } = this.buildFilter();
|
|
16022
|
+
const serialized = this.driver.serialize(fields, this.table);
|
|
16023
|
+
let paramIndex = filterParams.length + 1;
|
|
16024
|
+
const setClauses = [];
|
|
16025
|
+
const setParams = [];
|
|
16026
|
+
for (const [key, value] of Object.entries(serialized)) {
|
|
16027
|
+
setClauses.push(`${this.driver.dialect.quoteIdentifier(key)} = ${this.driver.dialect.placeholder(paramIndex++)}`);
|
|
16028
|
+
setParams.push(value);
|
|
16029
|
+
}
|
|
16030
|
+
if (setClauses.length === 0) throw new Error("No update operations specified");
|
|
16031
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${setClauses.join(", ")}` + (filterSql ? ` ${filterSql}` : "");
|
|
16032
|
+
return (await this.driver.query(updateSql, [...filterParams, ...setParams])).rowCount ?? 0;
|
|
15887
16033
|
}
|
|
15888
|
-
/**
|
|
16034
|
+
/**
|
|
16035
|
+
* Unset (NULL out) fields from matching rows. Honors every chained `where*`
|
|
16036
|
+
* (and applied scopes), like `update()`.
|
|
16037
|
+
*/
|
|
15889
16038
|
async unset(...fields) {
|
|
15890
16039
|
this.applyPendingScopes();
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
16040
|
+
if (fields.length === 0) throw new Error("No update operations specified");
|
|
16041
|
+
const { sql: filterSql, params: filterParams } = this.buildFilter();
|
|
16042
|
+
const setClauses = fields.map((field) => `${this.driver.dialect.quoteIdentifier(field)} = NULL`);
|
|
16043
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${setClauses.join(", ")}` + (filterSql ? ` ${filterSql}` : "");
|
|
16044
|
+
return (await this.driver.query(updateSql, filterParams)).rowCount ?? 0;
|
|
15894
16045
|
}
|
|
15895
16046
|
/**
|
|
15896
16047
|
* Return the SQL + bindings without executing.
|
|
@@ -16459,8 +16610,11 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16459
16610
|
case "dropForeignKey": return this.dropForeignKey(table, operation.payload);
|
|
16460
16611
|
case "addPrimaryKey": return this.addPrimaryKey(table, operation.payload);
|
|
16461
16612
|
case "dropPrimaryKey": return this.dropPrimaryKey(table);
|
|
16462
|
-
case "addCheck":
|
|
16463
|
-
|
|
16613
|
+
case "addCheck": {
|
|
16614
|
+
const payload = operation.payload;
|
|
16615
|
+
return this.addCheck(table, payload.name, payload.expression);
|
|
16616
|
+
}
|
|
16617
|
+
case "dropCheck": return this.dropCheck(table, operation.payload);
|
|
16464
16618
|
case "createTimestamps": return this.createTimestamps(table);
|
|
16465
16619
|
case "rawStatement": return operation.payload;
|
|
16466
16620
|
case "setSchemaValidation":
|
|
@@ -16514,8 +16668,10 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16514
16668
|
if (column.primary) sql += " PRIMARY KEY";
|
|
16515
16669
|
if (column.unique) sql += " UNIQUE";
|
|
16516
16670
|
}
|
|
16517
|
-
|
|
16518
|
-
|
|
16671
|
+
const statements = [sql];
|
|
16672
|
+
if (column.checkConstraint) statements.push(this.addCheck(table, column.checkConstraint.name, column.checkConstraint.expression));
|
|
16673
|
+
if (column.type === "vector") return ["CREATE EXTENSION IF NOT EXISTS vector", ...statements];
|
|
16674
|
+
return statements.length === 1 ? statements[0] : statements;
|
|
16519
16675
|
}
|
|
16520
16676
|
dropColumn(table, column) {
|
|
16521
16677
|
return `ALTER TABLE ${this.dialect.quoteIdentifier(table)} DROP COLUMN ${this.dialect.quoteIdentifier(column)}`;
|
|
@@ -16640,6 +16796,12 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16640
16796
|
const constraintName = `pk_${table}`;
|
|
16641
16797
|
return `ALTER TABLE ${quotedTable} DROP CONSTRAINT ${this.dialect.quoteIdentifier(constraintName)}`;
|
|
16642
16798
|
}
|
|
16799
|
+
addCheck(table, name, expression) {
|
|
16800
|
+
return `ALTER TABLE ${this.dialect.quoteIdentifier(table)} ADD CONSTRAINT ${this.dialect.quoteIdentifier(name)} CHECK (${expression})`;
|
|
16801
|
+
}
|
|
16802
|
+
dropCheck(table, name) {
|
|
16803
|
+
return `ALTER TABLE ${this.dialect.quoteIdentifier(table)} DROP CONSTRAINT ${this.dialect.quoteIdentifier(name)}`;
|
|
16804
|
+
}
|
|
16643
16805
|
mapForeignKeyAction(action) {
|
|
16644
16806
|
switch (action) {
|
|
16645
16807
|
case "cascade": return "CASCADE";
|
|
@@ -17475,6 +17637,9 @@ var PostgresDriver = class {
|
|
|
17475
17637
|
* Perform an atomic update operation.
|
|
17476
17638
|
*
|
|
17477
17639
|
* Builds and executes an UPDATE query for the given filter and operations.
|
|
17640
|
+
* Updates EVERY matching row — the MongoDB driver's atomic() delegates to
|
|
17641
|
+
* updateMany, and Model.findAndUpdate documents multi-row semantics, so the
|
|
17642
|
+
* two drivers must agree.
|
|
17478
17643
|
*
|
|
17479
17644
|
* @param table - Target table name
|
|
17480
17645
|
* @param filter - Filter conditions
|
|
@@ -17483,7 +17648,7 @@ var PostgresDriver = class {
|
|
|
17483
17648
|
* @returns Update result
|
|
17484
17649
|
*/
|
|
17485
17650
|
async atomic(table, filter, operations, _options) {
|
|
17486
|
-
const { sql, params } = this.buildUpdateQuery(table, filter, operations
|
|
17651
|
+
const { sql, params } = this.buildUpdateQuery(table, filter, operations);
|
|
17487
17652
|
return { modifiedCount: (await this.query(sql, params)).rowCount ?? 0 };
|
|
17488
17653
|
}
|
|
17489
17654
|
/**
|
|
@@ -17586,6 +17751,13 @@ var PostgresDriver = class {
|
|
|
17586
17751
|
/**
|
|
17587
17752
|
* Build a simple WHERE clause from a filter object.
|
|
17588
17753
|
*
|
|
17754
|
+
* Values are bound as plain equality, except Mongo-style operator objects
|
|
17755
|
+
* (`{ $in: [...] }`, `{ $gt: 5 }`, ...) which are translated to their SQL
|
|
17756
|
+
* equivalents — driver-level callers (e.g. pivot detach) build filters in
|
|
17757
|
+
* that portable form. An unrecognized `$` operator throws instead of being
|
|
17758
|
+
* bound literally, which would only surface as a cryptic type error from
|
|
17759
|
+
* Postgres.
|
|
17760
|
+
*
|
|
17589
17761
|
* @param filter - Filter conditions
|
|
17590
17762
|
* @param startParamIndex - Starting parameter index
|
|
17591
17763
|
* @returns Object with WHERE clause string and parameters
|
|
@@ -17597,6 +17769,49 @@ var PostgresDriver = class {
|
|
|
17597
17769
|
for (const [key, value] of Object.entries(filter)) {
|
|
17598
17770
|
const quotedKey = this.dialect.quoteIdentifier(key);
|
|
17599
17771
|
if (value === null) conditions.push(`${quotedKey} IS NULL`);
|
|
17772
|
+
else if (this.isOperatorFilter(value)) for (const [operator, operand] of Object.entries(value)) switch (operator) {
|
|
17773
|
+
case "$in":
|
|
17774
|
+
case "$nin": {
|
|
17775
|
+
const list = operand;
|
|
17776
|
+
if (list.length === 0) {
|
|
17777
|
+
conditions.push(operator === "$in" ? "FALSE" : "TRUE");
|
|
17778
|
+
break;
|
|
17779
|
+
}
|
|
17780
|
+
const placeholders = list.map(() => this.dialect.placeholder(paramIndex++));
|
|
17781
|
+
params.push(...list);
|
|
17782
|
+
conditions.push(`${quotedKey} ${operator === "$in" ? "IN" : "NOT IN"} (${placeholders.join(", ")})`);
|
|
17783
|
+
break;
|
|
17784
|
+
}
|
|
17785
|
+
case "$eq":
|
|
17786
|
+
if (operand === null) conditions.push(`${quotedKey} IS NULL`);
|
|
17787
|
+
else {
|
|
17788
|
+
conditions.push(`${quotedKey} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
17789
|
+
params.push(operand);
|
|
17790
|
+
}
|
|
17791
|
+
break;
|
|
17792
|
+
case "$ne":
|
|
17793
|
+
if (operand === null) conditions.push(`${quotedKey} IS NOT NULL`);
|
|
17794
|
+
else {
|
|
17795
|
+
conditions.push(`${quotedKey} != ${this.dialect.placeholder(paramIndex++)}`);
|
|
17796
|
+
params.push(operand);
|
|
17797
|
+
}
|
|
17798
|
+
break;
|
|
17799
|
+
case "$gt":
|
|
17800
|
+
case "$gte":
|
|
17801
|
+
case "$lt":
|
|
17802
|
+
case "$lte": {
|
|
17803
|
+
const sqlOperator = {
|
|
17804
|
+
$gt: ">",
|
|
17805
|
+
$gte: ">=",
|
|
17806
|
+
$lt: "<",
|
|
17807
|
+
$lte: "<="
|
|
17808
|
+
}[operator];
|
|
17809
|
+
conditions.push(`${quotedKey} ${sqlOperator} ${this.dialect.placeholder(paramIndex++)}`);
|
|
17810
|
+
params.push(operand);
|
|
17811
|
+
break;
|
|
17812
|
+
}
|
|
17813
|
+
default: throw new Error(`Unsupported filter operator "${operator}" for column "${key}" on the Postgres driver.`);
|
|
17814
|
+
}
|
|
17600
17815
|
else {
|
|
17601
17816
|
conditions.push(`${quotedKey} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
17602
17817
|
params.push(value);
|
|
@@ -17608,6 +17823,17 @@ var PostgresDriver = class {
|
|
|
17608
17823
|
};
|
|
17609
17824
|
}
|
|
17610
17825
|
/**
|
|
17826
|
+
* A filter value is an operator object when it is a plain object whose keys
|
|
17827
|
+
* ALL start with `$`. Arrays, Dates, and value objects (e.g. jsonb equality
|
|
17828
|
+
* payloads) keep their existing bind-as-value behavior.
|
|
17829
|
+
*/
|
|
17830
|
+
isOperatorFilter(value) {
|
|
17831
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
17832
|
+
if (value instanceof Date || Buffer.isBuffer(value)) return false;
|
|
17833
|
+
const keys = Object.keys(value);
|
|
17834
|
+
return keys.length > 0 && keys.every((key) => key.startsWith("$"));
|
|
17835
|
+
}
|
|
17836
|
+
/**
|
|
17611
17837
|
* Build an UPDATE query from update operations.
|
|
17612
17838
|
*
|
|
17613
17839
|
* @param table - Target table name
|
|
@@ -19447,10 +19673,13 @@ var Migration = class {
|
|
|
19447
19673
|
return this.driver.driver.name;
|
|
19448
19674
|
}
|
|
19449
19675
|
/**
|
|
19450
|
-
* Execute all pending operations.
|
|
19676
|
+
* Execute all pending operations directly through the migration driver.
|
|
19677
|
+
*
|
|
19678
|
+
* SQL-capable drivers go through toSQL() + raw query execution instead
|
|
19679
|
+
* (phase-ordering, dry-run, export). This path is how the MigrationRunner
|
|
19680
|
+
* executes migrations on drivers WITHOUT SQL serialization (MongoDB), where
|
|
19681
|
+
* each pending operation maps to a native driver command.
|
|
19451
19682
|
*
|
|
19452
|
-
* @deprecated Use toSQL() instead — migrations now generate SQL rather than
|
|
19453
|
-
* executing DDL directly through the driver.
|
|
19454
19683
|
* @internal
|
|
19455
19684
|
*/
|
|
19456
19685
|
async execute() {
|
|
@@ -21681,6 +21910,19 @@ var MigrationRunner = class {
|
|
|
21681
21910
|
_warlock_js_logger.log.warn("database", "migration", "Nothing to migrate.");
|
|
21682
21911
|
return results;
|
|
21683
21912
|
}
|
|
21913
|
+
if (this.getDataSource().driver.supportsSqlSerialization === false) {
|
|
21914
|
+
const batch = await this.getNextBatchNumber();
|
|
21915
|
+
for (const MigrationClass of pending) {
|
|
21916
|
+
const result = await this.runMigration(MigrationClass, "up", {
|
|
21917
|
+
dryRun,
|
|
21918
|
+
record,
|
|
21919
|
+
batch
|
|
21920
|
+
});
|
|
21921
|
+
results.push(result);
|
|
21922
|
+
if (!result.success) break;
|
|
21923
|
+
}
|
|
21924
|
+
return results;
|
|
21925
|
+
}
|
|
21684
21926
|
_warlock_js_logger.log.info("database", "migration", `Found ${pending.length} pending migration(s). Generating SQL pool...`);
|
|
21685
21927
|
const nextBatch = await this.getNextBatchNumber();
|
|
21686
21928
|
const taggedStatements = [];
|
|
@@ -21778,6 +22020,7 @@ var MigrationRunner = class {
|
|
|
21778
22020
|
* By default, it exports all registered migrations. Use `pendingOnly: true` to export only pending ones.
|
|
21779
22021
|
*/
|
|
21780
22022
|
async exportSQL(options = {}) {
|
|
22023
|
+
if (this.getDataSource().driver.supportsSqlSerialization === false) throw new Error("SQL export is not supported on this data source — its driver has no SQL dialect. Migrations on this driver execute native commands through the migration driver instead.");
|
|
21781
22024
|
const migrationsToExport = options.pendingOnly ? await this.getPendingMigrations() : this.migrations;
|
|
21782
22025
|
if (migrationsToExport.length === 0) {
|
|
21783
22026
|
_warlock_js_logger.log.warn("database", "migration", "No migrations to export.");
|
|
@@ -21957,17 +22200,25 @@ var MigrationRunner = class {
|
|
|
21957
22200
|
const shouldUseTransaction = migration.transactional ?? this.getDataSource().migrations?.transactional ?? driver.getDefaultTransactional();
|
|
21958
22201
|
if (direction === "up") await migration.up();
|
|
21959
22202
|
else await migration.down();
|
|
21960
|
-
const sqlStatements = migration.toSQL();
|
|
21961
22203
|
const databaseDriver = this.getDataSource().driver;
|
|
21962
|
-
|
|
22204
|
+
const directExecution = databaseDriver.supportsSqlSerialization === false;
|
|
22205
|
+
const sqlStatements = directExecution ? [] : migration.toSQL();
|
|
22206
|
+
const applyMigration = async () => {
|
|
22207
|
+
if (directExecution) {
|
|
22208
|
+
await migration.execute();
|
|
22209
|
+
return;
|
|
22210
|
+
}
|
|
21963
22211
|
for (const sql of sqlStatements) await databaseDriver.query(sql);
|
|
22212
|
+
};
|
|
22213
|
+
if (shouldUseTransaction && databaseDriver.transaction) await databaseDriver.transaction(async () => {
|
|
22214
|
+
await applyMigration();
|
|
21964
22215
|
if (record) if (direction === "up") {
|
|
21965
22216
|
const batch = options.batch ?? await this.getNextBatchNumber();
|
|
21966
22217
|
await this.recordMigration(name, batch, MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : /* @__PURE__ */ new Date());
|
|
21967
22218
|
} else await this.removeMigrationRecord(name);
|
|
21968
22219
|
});
|
|
21969
22220
|
else {
|
|
21970
|
-
|
|
22221
|
+
await applyMigration();
|
|
21971
22222
|
if (record) if (direction === "up") {
|
|
21972
22223
|
const batch = options.batch ?? await this.getNextBatchNumber();
|
|
21973
22224
|
await this.recordMigration(name, batch, MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : /* @__PURE__ */ new Date());
|