@warlock.js/cascade 4.6.0 → 4.7.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 +28 -0
- package/cjs/index.cjs +372 -56
- 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 +10 -4
- 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 +69 -13
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +155 -27
- 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 +41 -7
- package/llms.txt +1 -1
- package/package.json +4 -4
- package/skills/README.md +1 -1
- package/skills/manage-transactions/SKILL.md +41 -7
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
|
*/
|
|
@@ -13035,7 +13132,10 @@ var MongoDbDriver = class {
|
|
|
13035
13132
|
* @throws {Error} If transaction fails, is explicitly rolled back, or replica set not configured
|
|
13036
13133
|
*/
|
|
13037
13134
|
async transaction(fn, options) {
|
|
13038
|
-
|
|
13135
|
+
const ctx = { rollback(reason) {
|
|
13136
|
+
throw new TransactionRollbackError(reason);
|
|
13137
|
+
} };
|
|
13138
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
13039
13139
|
await this.ensureReplicaSetAvailable();
|
|
13040
13140
|
const session = this.getClientInstance().startSession();
|
|
13041
13141
|
try {
|
|
@@ -13045,9 +13145,7 @@ var MongoDbDriver = class {
|
|
|
13045
13145
|
});
|
|
13046
13146
|
databaseTransactionContext.enter({ session });
|
|
13047
13147
|
try {
|
|
13048
|
-
const result = await fn(
|
|
13049
|
-
throw new TransactionRollbackError(reason);
|
|
13050
|
-
} });
|
|
13148
|
+
const result = await fn(ctx);
|
|
13051
13149
|
await session.commitTransaction();
|
|
13052
13150
|
return result;
|
|
13053
13151
|
} catch (error) {
|
|
@@ -13158,6 +13256,11 @@ var MongoDbDriver = class {
|
|
|
13158
13256
|
return baseOptions;
|
|
13159
13257
|
}
|
|
13160
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
|
+
/**
|
|
13161
13264
|
* Return a SQL serializer for this driver's dialect.
|
|
13162
13265
|
* Not supported for MongoDB.
|
|
13163
13266
|
*/
|
|
@@ -14518,6 +14621,11 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14518
14621
|
*/
|
|
14519
14622
|
isDistinct = false;
|
|
14520
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
|
+
/**
|
|
14521
14629
|
* Whether the query has any JOIN operations (pre-scanned before processing).
|
|
14522
14630
|
* Used by qualifyColumn() to decide whether to prefix columns with the main table.
|
|
14523
14631
|
*/
|
|
@@ -14687,6 +14795,13 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14687
14795
|
case "offset":
|
|
14688
14796
|
this.offsetValue = data.value;
|
|
14689
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
|
+
}
|
|
14690
14805
|
case "distinct":
|
|
14691
14806
|
this.isDistinct = true;
|
|
14692
14807
|
break;
|
|
@@ -14717,6 +14832,7 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14717
14832
|
if (this.orderClauses.length > 0) parts.push(`ORDER BY ${this.orderClauses.join(", ")}`);
|
|
14718
14833
|
const limitOffset = this.dialect.limitOffset(this.limitValue, this.offsetValue);
|
|
14719
14834
|
if (limitOffset) parts.push(limitOffset);
|
|
14835
|
+
if (this.lockClause) parts.push(this.lockClause);
|
|
14720
14836
|
return parts.join(" ");
|
|
14721
14837
|
}
|
|
14722
14838
|
/**
|
|
@@ -15707,38 +15823,45 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15707
15823
|
/** SUM a numeric field. */
|
|
15708
15824
|
async sum(field) {
|
|
15709
15825
|
this.applyPendingScopes();
|
|
15826
|
+
this.hydrateCallback = void 0;
|
|
15710
15827
|
const result = await this.selectRaw(`SUM(${field}) as sum`).first();
|
|
15711
15828
|
return parseFloat(result?.sum ?? "0");
|
|
15712
15829
|
}
|
|
15713
15830
|
/** AVG of a numeric field. */
|
|
15714
15831
|
async avg(field) {
|
|
15715
15832
|
this.applyPendingScopes();
|
|
15833
|
+
this.hydrateCallback = void 0;
|
|
15716
15834
|
const result = await this.selectRaw(`AVG(${field}) as avg`).first();
|
|
15717
15835
|
return parseFloat(result?.avg ?? "0");
|
|
15718
15836
|
}
|
|
15719
15837
|
/** MIN of a numeric field. */
|
|
15720
15838
|
async min(field) {
|
|
15721
15839
|
this.applyPendingScopes();
|
|
15840
|
+
this.hydrateCallback = void 0;
|
|
15722
15841
|
const result = await this.selectRaw(`MIN(${field}) as min`).first();
|
|
15723
15842
|
return parseFloat(result?.min ?? "0");
|
|
15724
15843
|
}
|
|
15725
15844
|
/** MAX of a numeric field. */
|
|
15726
15845
|
async max(field) {
|
|
15727
15846
|
this.applyPendingScopes();
|
|
15847
|
+
this.hydrateCallback = void 0;
|
|
15728
15848
|
const result = await this.selectRaw(`MAX(${field}) as max`).first();
|
|
15729
15849
|
return parseFloat(result?.max ?? "0");
|
|
15730
15850
|
}
|
|
15731
15851
|
/** Get distinct values for a field. */
|
|
15732
15852
|
async distinct(field) {
|
|
15853
|
+
this.hydrateCallback = void 0;
|
|
15733
15854
|
this.distinctValues(field);
|
|
15734
15855
|
return (await this.get()).map((row) => row[field]);
|
|
15735
15856
|
}
|
|
15736
15857
|
/** Get array of all values for a single field. */
|
|
15737
15858
|
async pluck(field) {
|
|
15859
|
+
this.hydrateCallback = void 0;
|
|
15738
15860
|
return (await this.select([field]).get()).map((row) => row[field]);
|
|
15739
15861
|
}
|
|
15740
15862
|
/** Get a single scalar value. */
|
|
15741
15863
|
async value(field) {
|
|
15864
|
+
this.hydrateCallback = void 0;
|
|
15742
15865
|
return (await this.select([field]).first())?.[field] ?? null;
|
|
15743
15866
|
}
|
|
15744
15867
|
/** Check whether any matching rows exist. */
|
|
@@ -15751,6 +15874,7 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15751
15874
|
}
|
|
15752
15875
|
/** COUNT DISTINCT a field. */
|
|
15753
15876
|
async countDistinct(field) {
|
|
15877
|
+
this.hydrateCallback = void 0;
|
|
15754
15878
|
const result = await this.selectRaw(`COUNT(DISTINCT ${field}) as count`).first();
|
|
15755
15879
|
return parseInt(result?.count ?? "0", 10);
|
|
15756
15880
|
}
|
|
@@ -15875,21 +15999,49 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
15875
15999
|
const deleteSql = `DELETE FROM ${this.driver.dialect.quoteIdentifier(this.table)} ${sql}`;
|
|
15876
16000
|
return (await this.driver.query(deleteSql, params)).rowCount ?? 0;
|
|
15877
16001
|
}
|
|
15878
|
-
/**
|
|
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
|
+
*/
|
|
15879
16008
|
async deleteOne() {
|
|
15880
|
-
|
|
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;
|
|
15881
16014
|
}
|
|
15882
|
-
/**
|
|
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
|
+
*/
|
|
15883
16019
|
async update(fields) {
|
|
15884
16020
|
this.applyPendingScopes();
|
|
15885
|
-
|
|
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;
|
|
15886
16033
|
}
|
|
15887
|
-
/**
|
|
16034
|
+
/**
|
|
16035
|
+
* Unset (NULL out) fields from matching rows. Honors every chained `where*`
|
|
16036
|
+
* (and applied scopes), like `update()`.
|
|
16037
|
+
*/
|
|
15888
16038
|
async unset(...fields) {
|
|
15889
16039
|
this.applyPendingScopes();
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
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;
|
|
15893
16045
|
}
|
|
15894
16046
|
/**
|
|
15895
16047
|
* Return the SQL + bindings without executing.
|
|
@@ -16458,8 +16610,11 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16458
16610
|
case "dropForeignKey": return this.dropForeignKey(table, operation.payload);
|
|
16459
16611
|
case "addPrimaryKey": return this.addPrimaryKey(table, operation.payload);
|
|
16460
16612
|
case "dropPrimaryKey": return this.dropPrimaryKey(table);
|
|
16461
|
-
case "addCheck":
|
|
16462
|
-
|
|
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);
|
|
16463
16618
|
case "createTimestamps": return this.createTimestamps(table);
|
|
16464
16619
|
case "rawStatement": return operation.payload;
|
|
16465
16620
|
case "setSchemaValidation":
|
|
@@ -16513,8 +16668,10 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16513
16668
|
if (column.primary) sql += " PRIMARY KEY";
|
|
16514
16669
|
if (column.unique) sql += " UNIQUE";
|
|
16515
16670
|
}
|
|
16516
|
-
|
|
16517
|
-
|
|
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;
|
|
16518
16675
|
}
|
|
16519
16676
|
dropColumn(table, column) {
|
|
16520
16677
|
return `ALTER TABLE ${this.dialect.quoteIdentifier(table)} DROP COLUMN ${this.dialect.quoteIdentifier(column)}`;
|
|
@@ -16639,6 +16796,12 @@ var PostgresSQLSerializer = class extends SQLSerializer {
|
|
|
16639
16796
|
const constraintName = `pk_${table}`;
|
|
16640
16797
|
return `ALTER TABLE ${quotedTable} DROP CONSTRAINT ${this.dialect.quoteIdentifier(constraintName)}`;
|
|
16641
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
|
+
}
|
|
16642
16805
|
mapForeignKeyAction(action) {
|
|
16643
16806
|
switch (action) {
|
|
16644
16807
|
case "cascade": return "CASCADE";
|
|
@@ -16925,13 +17088,22 @@ var PostgresDriver = class {
|
|
|
16925
17088
|
*/
|
|
16926
17089
|
_syncAdapter;
|
|
16927
17090
|
/**
|
|
16928
|
-
*
|
|
16929
|
-
* (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text
|
|
17091
|
+
* Explicit, table-agnostic override list of column names that hold native
|
|
17092
|
+
* PostgreSQL arrays (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text
|
|
17093
|
+
* encoded. Merged with (and superseded per-table by) the schema
|
|
17094
|
+
* introspection below; kept as a manual escape hatch.
|
|
16930
17095
|
*
|
|
16931
17096
|
* @see PostgresPoolConfig.nativeArrayColumns
|
|
16932
17097
|
*/
|
|
16933
17098
|
_nativeArrayColumns;
|
|
16934
17099
|
/**
|
|
17100
|
+
* Native-array columns discovered by introspecting the live schema on
|
|
17101
|
+
* connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
|
|
17102
|
+
* a column that is `TEXT[]` in one table and `jsonb` in another is encoded
|
|
17103
|
+
* correctly for each — no app configuration required.
|
|
17104
|
+
*/
|
|
17105
|
+
_introspectedArrayColumns = /* @__PURE__ */ new Map();
|
|
17106
|
+
/**
|
|
16935
17107
|
* Create a new PostgreSQL driver instance.
|
|
16936
17108
|
*
|
|
16937
17109
|
* @param config - PostgreSQL connection configuration
|
|
@@ -16997,6 +17169,7 @@ var PostgresDriver = class {
|
|
|
16997
17169
|
(await this._pool.connect()).release();
|
|
16998
17170
|
_warlock_js_logger.log.success("database.postgres", "connection", `Connected to database ${_mongez_copper.colors.bold(_mongez_copper.colors.yellowBright(this.config.database))}`);
|
|
16999
17171
|
this._isConnected = true;
|
|
17172
|
+
await this.loadNativeArrayColumns();
|
|
17000
17173
|
this.emit("connected");
|
|
17001
17174
|
} catch (error) {
|
|
17002
17175
|
_warlock_js_logger.log.fatal("database.postgres", "connection", "Failed to connect to database");
|
|
@@ -17033,13 +17206,15 @@ var PostgresDriver = class {
|
|
|
17033
17206
|
* that need special handling for PostgreSQL storage.
|
|
17034
17207
|
*
|
|
17035
17208
|
* @param data - The data object to serialize
|
|
17209
|
+
* @param table - Optional table name; when given, columns introspected as
|
|
17210
|
+
* native arrays on that table are bound raw (see {@link serializeValue}).
|
|
17036
17211
|
* @returns Serialized data ready for PostgreSQL
|
|
17037
17212
|
*/
|
|
17038
|
-
serialize(data) {
|
|
17213
|
+
serialize(data, table) {
|
|
17039
17214
|
const serialized = {};
|
|
17040
17215
|
for (const [key, value] of Object.entries(data)) {
|
|
17041
17216
|
if (value === void 0) continue;
|
|
17042
|
-
serialized[key] = this.serializeValue(key, value);
|
|
17217
|
+
serialized[key] = this.serializeValue(key, value, table);
|
|
17043
17218
|
}
|
|
17044
17219
|
return serialized;
|
|
17045
17220
|
}
|
|
@@ -17059,36 +17234,87 @@ var PostgresDriver = class {
|
|
|
17059
17234
|
* `JSON.stringify`. node-pg renders a raw JS array as a PostgreSQL array
|
|
17060
17235
|
* literal `{...}` (and `[]` as `{}`), which a `json` / `jsonb` column
|
|
17061
17236
|
* rejects — so we bind the value as JSON text instead, the form those
|
|
17062
|
-
* columns accept. Columns
|
|
17063
|
-
*
|
|
17064
|
-
*
|
|
17237
|
+
* columns accept. Columns known to be native arrays — via schema
|
|
17238
|
+
* introspection or the `nativeArrayColumns` config — are exempt: their raw
|
|
17239
|
+
* array is passed through so node-pg emits the `{...}` literal a genuine
|
|
17240
|
+
* `JSONB[]` / `TEXT[]` column needs.
|
|
17065
17241
|
* - plain object → `JSON.stringify`. Equivalent to node-pg's own object
|
|
17066
17242
|
* handling, made explicit so both write paths agree.
|
|
17067
17243
|
* - everything else (scalars: string, number, boolean, null) → untouched.
|
|
17068
17244
|
*
|
|
17069
|
-
*
|
|
17070
|
-
*
|
|
17071
|
-
*
|
|
17072
|
-
*
|
|
17073
|
-
*
|
|
17074
|
-
*
|
|
17245
|
+
* Distinguishing native-array from `json` / `jsonb` columns: a value alone
|
|
17246
|
+
* can't tell them apart, so the driver introspects the live schema on connect
|
|
17247
|
+
* (see {@link loadNativeArrayColumns}) and consults that per-table map here
|
|
17248
|
+
* via {@link isNativeArrayColumn}. The explicit `nativeArrayColumns` config
|
|
17249
|
+
* still works as a table-agnostic override. No `::jsonb` placeholder cast is
|
|
17250
|
+
* added: a JSON-text string binds correctly to `json` / `jsonb` without one,
|
|
17251
|
+
* and a blind cast would misfire on columns we cannot positively identify as
|
|
17252
|
+
* jsonb.
|
|
17075
17253
|
*
|
|
17076
|
-
* @param key - Column name (used to
|
|
17254
|
+
* @param key - Column name (used to resolve native-array columns)
|
|
17077
17255
|
* @param value - The raw value to serialize (never `undefined`)
|
|
17256
|
+
* @param table - Optional table name; enables the per-table native-array lookup
|
|
17078
17257
|
* @returns The value ready to bind as a query parameter
|
|
17079
17258
|
*/
|
|
17080
|
-
serializeValue(key, value) {
|
|
17259
|
+
serializeValue(key, value, table) {
|
|
17081
17260
|
if (value instanceof Date) return value.toISOString();
|
|
17082
17261
|
if (typeof value === "bigint") return value.toString();
|
|
17083
17262
|
if (Array.isArray(value)) {
|
|
17084
17263
|
if (value.length > 0 && value.every((v) => typeof v === "number")) return `[${value.join(",")}]`;
|
|
17085
|
-
if (this.
|
|
17264
|
+
if (this.isNativeArrayColumn(table, key)) return value;
|
|
17086
17265
|
return JSON.stringify(value);
|
|
17087
17266
|
}
|
|
17088
17267
|
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
17089
17268
|
return value;
|
|
17090
17269
|
}
|
|
17091
17270
|
/**
|
|
17271
|
+
* Whether `column` on `table` is a native PostgreSQL array. True when the
|
|
17272
|
+
* connect-time schema introspection saw it as `data_type = 'ARRAY'` for that
|
|
17273
|
+
* table (authoritative, per-table), or when it's listed in the table-agnostic
|
|
17274
|
+
* `nativeArrayColumns` config override.
|
|
17275
|
+
*/
|
|
17276
|
+
isNativeArrayColumn(table, column) {
|
|
17277
|
+
if (table && this._introspectedArrayColumns.get(table)?.has(column)) return true;
|
|
17278
|
+
return this._nativeArrayColumns.has(column);
|
|
17279
|
+
}
|
|
17280
|
+
/**
|
|
17281
|
+
* Introspect the live schema for native-array columns so array values bind
|
|
17282
|
+
* correctly with zero app configuration.
|
|
17283
|
+
*
|
|
17284
|
+
* A JS array must be bound two opposite ways depending on the column: as JSON
|
|
17285
|
+
* text for a `json` / `jsonb` column, but as a raw array (which node-pg
|
|
17286
|
+
* renders `{...}`) for a native `TEXT[]` / `JSONB[]` / `INTEGER[]` column. The
|
|
17287
|
+
* serializer sees values, not types, so without this it JSON-stringifies
|
|
17288
|
+
* every array — which a native-array column rejects with "malformed array
|
|
17289
|
+
* literal". One `information_schema` query at connect, cached for the
|
|
17290
|
+
* connection lifetime, removes the need to hand-list `nativeArrayColumns`.
|
|
17291
|
+
*
|
|
17292
|
+
* Best-effort: any failure (e.g. restricted catalog access) is logged and
|
|
17293
|
+
* leaves the map empty so the config override still applies — it never blocks
|
|
17294
|
+
* connect. A schema change made within a live connection isn't reflected
|
|
17295
|
+
* until the next connect.
|
|
17296
|
+
*/
|
|
17297
|
+
async loadNativeArrayColumns() {
|
|
17298
|
+
try {
|
|
17299
|
+
const result = await this.query(`SELECT table_name, column_name
|
|
17300
|
+
FROM information_schema.columns
|
|
17301
|
+
WHERE table_schema = ANY (current_schemas(false))
|
|
17302
|
+
AND data_type = 'ARRAY'`);
|
|
17303
|
+
const map = /* @__PURE__ */ new Map();
|
|
17304
|
+
for (const { table_name, column_name } of result.rows) {
|
|
17305
|
+
let columns = map.get(table_name);
|
|
17306
|
+
if (!columns) {
|
|
17307
|
+
columns = /* @__PURE__ */ new Set();
|
|
17308
|
+
map.set(table_name, columns);
|
|
17309
|
+
}
|
|
17310
|
+
columns.add(column_name);
|
|
17311
|
+
}
|
|
17312
|
+
this._introspectedArrayColumns = map;
|
|
17313
|
+
} catch {
|
|
17314
|
+
_warlock_js_logger.log.warn("database.postgres", "introspection", "Could not introspect native-array columns; using the nativeArrayColumns config only");
|
|
17315
|
+
}
|
|
17316
|
+
}
|
|
17317
|
+
/**
|
|
17092
17318
|
* Get the dirty tracker for this driver.
|
|
17093
17319
|
*/
|
|
17094
17320
|
getDirtyTracker(data) {
|
|
@@ -17137,7 +17363,7 @@ var PostgresDriver = class {
|
|
|
17137
17363
|
* @returns The inserted document
|
|
17138
17364
|
*/
|
|
17139
17365
|
async insert(table, document, _options) {
|
|
17140
|
-
const serialized = this.serialize(document);
|
|
17366
|
+
const serialized = this.serialize(document, table);
|
|
17141
17367
|
const filteredData = Object.fromEntries(Object.entries(serialized).filter(([key, value]) => {
|
|
17142
17368
|
if (key === "id" && (value === null || value === void 0)) return false;
|
|
17143
17369
|
return true;
|
|
@@ -17164,7 +17390,7 @@ var PostgresDriver = class {
|
|
|
17164
17390
|
if (documents.length === 0) return [];
|
|
17165
17391
|
const allColumns = /* @__PURE__ */ new Set();
|
|
17166
17392
|
for (const doc of documents) {
|
|
17167
|
-
const serialized = this.serialize(doc);
|
|
17393
|
+
const serialized = this.serialize(doc, table);
|
|
17168
17394
|
Object.keys(serialized).forEach((key) => allColumns.add(key));
|
|
17169
17395
|
}
|
|
17170
17396
|
const columns = Array.from(allColumns);
|
|
@@ -17174,7 +17400,7 @@ var PostgresDriver = class {
|
|
|
17174
17400
|
const params = [];
|
|
17175
17401
|
let paramIndex = 1;
|
|
17176
17402
|
for (const doc of documents) {
|
|
17177
|
-
const serialized = this.serialize(doc);
|
|
17403
|
+
const serialized = this.serialize(doc, table);
|
|
17178
17404
|
const rowPlaceholders = [];
|
|
17179
17405
|
for (const col of columns) if (col in serialized) {
|
|
17180
17406
|
rowPlaceholders.push(this.dialect.placeholder(paramIndex++));
|
|
@@ -17241,7 +17467,7 @@ var PostgresDriver = class {
|
|
|
17241
17467
|
* @returns The replaced document or null
|
|
17242
17468
|
*/
|
|
17243
17469
|
async replace(table, filter, document, _options) {
|
|
17244
|
-
const serialized = this.serialize(document);
|
|
17470
|
+
const serialized = this.serialize(document, table);
|
|
17245
17471
|
const columns = Object.keys(serialized);
|
|
17246
17472
|
const values = Object.values(serialized);
|
|
17247
17473
|
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
@@ -17263,7 +17489,7 @@ var PostgresDriver = class {
|
|
|
17263
17489
|
* @returns The upserted row
|
|
17264
17490
|
*/
|
|
17265
17491
|
async upsert(table, filter, document, options) {
|
|
17266
|
-
const serialized = this.serialize(document);
|
|
17492
|
+
const serialized = this.serialize(document, table);
|
|
17267
17493
|
const columns = Object.keys(serialized);
|
|
17268
17494
|
const values = Object.values(serialized);
|
|
17269
17495
|
if (columns.length === 0) throw new Error("Cannot upsert empty document");
|
|
@@ -17389,13 +17615,14 @@ var PostgresDriver = class {
|
|
|
17389
17615
|
* @throws {Error} If transaction fails or is explicitly rolled back
|
|
17390
17616
|
*/
|
|
17391
17617
|
async transaction(fn, options) {
|
|
17392
|
-
|
|
17618
|
+
const ctx = { rollback(reason) {
|
|
17619
|
+
throw new TransactionRollbackError(reason);
|
|
17620
|
+
} };
|
|
17621
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
17393
17622
|
const tx = await this.beginTransaction(options);
|
|
17394
17623
|
databaseTransactionContext.enter({ session: tx.context });
|
|
17395
17624
|
try {
|
|
17396
|
-
const result = await fn(
|
|
17397
|
-
throw new TransactionRollbackError(reason);
|
|
17398
|
-
} });
|
|
17625
|
+
const result = await fn(ctx);
|
|
17399
17626
|
await tx.commit();
|
|
17400
17627
|
return result;
|
|
17401
17628
|
} catch (error) {
|
|
@@ -17410,6 +17637,9 @@ var PostgresDriver = class {
|
|
|
17410
17637
|
* Perform an atomic update operation.
|
|
17411
17638
|
*
|
|
17412
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.
|
|
17413
17643
|
*
|
|
17414
17644
|
* @param table - Target table name
|
|
17415
17645
|
* @param filter - Filter conditions
|
|
@@ -17418,7 +17648,7 @@ var PostgresDriver = class {
|
|
|
17418
17648
|
* @returns Update result
|
|
17419
17649
|
*/
|
|
17420
17650
|
async atomic(table, filter, operations, _options) {
|
|
17421
|
-
const { sql, params } = this.buildUpdateQuery(table, filter, operations
|
|
17651
|
+
const { sql, params } = this.buildUpdateQuery(table, filter, operations);
|
|
17422
17652
|
return { modifiedCount: (await this.query(sql, params)).rowCount ?? 0 };
|
|
17423
17653
|
}
|
|
17424
17654
|
/**
|
|
@@ -17521,6 +17751,13 @@ var PostgresDriver = class {
|
|
|
17521
17751
|
/**
|
|
17522
17752
|
* Build a simple WHERE clause from a filter object.
|
|
17523
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
|
+
*
|
|
17524
17761
|
* @param filter - Filter conditions
|
|
17525
17762
|
* @param startParamIndex - Starting parameter index
|
|
17526
17763
|
* @returns Object with WHERE clause string and parameters
|
|
@@ -17532,6 +17769,49 @@ var PostgresDriver = class {
|
|
|
17532
17769
|
for (const [key, value] of Object.entries(filter)) {
|
|
17533
17770
|
const quotedKey = this.dialect.quoteIdentifier(key);
|
|
17534
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
|
+
}
|
|
17535
17815
|
else {
|
|
17536
17816
|
conditions.push(`${quotedKey} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
17537
17817
|
params.push(value);
|
|
@@ -17543,6 +17823,17 @@ var PostgresDriver = class {
|
|
|
17543
17823
|
};
|
|
17544
17824
|
}
|
|
17545
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
|
+
/**
|
|
17546
17837
|
* Build an UPDATE query from update operations.
|
|
17547
17838
|
*
|
|
17548
17839
|
* @param table - Target table name
|
|
@@ -17557,7 +17848,7 @@ var PostgresDriver = class {
|
|
|
17557
17848
|
let paramIndex = 1;
|
|
17558
17849
|
if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
|
|
17559
17850
|
setClauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
17560
|
-
params.push(value === void 0 ? value : this.serializeValue(key, value));
|
|
17851
|
+
params.push(value === void 0 ? value : this.serializeValue(key, value, table));
|
|
17561
17852
|
}
|
|
17562
17853
|
if (update.$unset) for (const key of Object.keys(update.$unset)) setClauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
|
|
17563
17854
|
if (update.$inc) for (const [key, amount] of Object.entries(update.$inc)) {
|
|
@@ -19382,10 +19673,13 @@ var Migration = class {
|
|
|
19382
19673
|
return this.driver.driver.name;
|
|
19383
19674
|
}
|
|
19384
19675
|
/**
|
|
19385
|
-
* 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.
|
|
19386
19682
|
*
|
|
19387
|
-
* @deprecated Use toSQL() instead — migrations now generate SQL rather than
|
|
19388
|
-
* executing DDL directly through the driver.
|
|
19389
19683
|
* @internal
|
|
19390
19684
|
*/
|
|
19391
19685
|
async execute() {
|
|
@@ -21616,6 +21910,19 @@ var MigrationRunner = class {
|
|
|
21616
21910
|
_warlock_js_logger.log.warn("database", "migration", "Nothing to migrate.");
|
|
21617
21911
|
return results;
|
|
21618
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
|
+
}
|
|
21619
21926
|
_warlock_js_logger.log.info("database", "migration", `Found ${pending.length} pending migration(s). Generating SQL pool...`);
|
|
21620
21927
|
const nextBatch = await this.getNextBatchNumber();
|
|
21621
21928
|
const taggedStatements = [];
|
|
@@ -21713,6 +22020,7 @@ var MigrationRunner = class {
|
|
|
21713
22020
|
* By default, it exports all registered migrations. Use `pendingOnly: true` to export only pending ones.
|
|
21714
22021
|
*/
|
|
21715
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.");
|
|
21716
22024
|
const migrationsToExport = options.pendingOnly ? await this.getPendingMigrations() : this.migrations;
|
|
21717
22025
|
if (migrationsToExport.length === 0) {
|
|
21718
22026
|
_warlock_js_logger.log.warn("database", "migration", "No migrations to export.");
|
|
@@ -21892,17 +22200,25 @@ var MigrationRunner = class {
|
|
|
21892
22200
|
const shouldUseTransaction = migration.transactional ?? this.getDataSource().migrations?.transactional ?? driver.getDefaultTransactional();
|
|
21893
22201
|
if (direction === "up") await migration.up();
|
|
21894
22202
|
else await migration.down();
|
|
21895
|
-
const sqlStatements = migration.toSQL();
|
|
21896
22203
|
const databaseDriver = this.getDataSource().driver;
|
|
21897
|
-
|
|
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
|
+
}
|
|
21898
22211
|
for (const sql of sqlStatements) await databaseDriver.query(sql);
|
|
22212
|
+
};
|
|
22213
|
+
if (shouldUseTransaction && databaseDriver.transaction) await databaseDriver.transaction(async () => {
|
|
22214
|
+
await applyMigration();
|
|
21899
22215
|
if (record) if (direction === "up") {
|
|
21900
22216
|
const batch = options.batch ?? await this.getNextBatchNumber();
|
|
21901
22217
|
await this.recordMigration(name, batch, MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : /* @__PURE__ */ new Date());
|
|
21902
22218
|
} else await this.removeMigrationRecord(name);
|
|
21903
22219
|
});
|
|
21904
22220
|
else {
|
|
21905
|
-
|
|
22221
|
+
await applyMigration();
|
|
21906
22222
|
if (record) if (direction === "up") {
|
|
21907
22223
|
const batch = options.batch ?? await this.getNextBatchNumber();
|
|
21908
22224
|
await this.recordMigration(name, batch, MigrationClass.createdAt ? parseCreatedAt(MigrationClass.createdAt) : /* @__PURE__ */ new Date());
|