@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/cjs/index.cjs +277 -26
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/database-driver.contract.d.mts +8 -0
  5. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  6. package/esm/contracts/index.d.mts +1 -1
  7. package/esm/contracts/query-builder.contract.d.mts +36 -1
  8. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  9. package/esm/drivers/mongodb/mongodb-driver.d.mts +5 -0
  10. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-driver.mjs +5 -0
  12. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +4 -0
  14. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +5 -2
  16. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +15 -0
  18. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  19. package/esm/drivers/mongodb/mongodb-query-builder.mjs +24 -0
  20. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  21. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +16 -0
  22. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  23. package/esm/drivers/mongodb/mongodb-query-parser.mjs +33 -1
  24. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  25. package/esm/drivers/postgres/postgres-driver.d.mts +16 -0
  26. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  27. package/esm/drivers/postgres/postgres-driver.mjs +65 -1
  28. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  29. package/esm/drivers/postgres/postgres-query-builder.d.mts +14 -3
  30. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  31. package/esm/drivers/postgres/postgres-query-builder.mjs +44 -8
  32. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  33. package/esm/drivers/postgres/postgres-query-parser.d.mts +6 -1
  34. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  35. package/esm/drivers/postgres/postgres-query-parser.mjs +13 -0
  36. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  37. package/esm/drivers/postgres/postgres-sql-serializer.mjs +15 -4
  38. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  39. package/esm/index.d.mts +2 -2
  40. package/esm/migration/migration-runner.d.mts.map +1 -1
  41. package/esm/migration/migration-runner.mjs +25 -3
  42. package/esm/migration/migration-runner.mjs.map +1 -1
  43. package/esm/migration/migration.d.mts +6 -3
  44. package/esm/migration/migration.d.mts.map +1 -1
  45. package/esm/migration/migration.mjs +6 -3
  46. package/esm/migration/migration.mjs.map +1 -1
  47. package/esm/model/methods/scope-methods.mjs +18 -4
  48. package/esm/model/methods/scope-methods.mjs.map +1 -1
  49. package/esm/model/model.d.mts +6 -0
  50. package/esm/model/model.d.mts.map +1 -1
  51. package/esm/model/model.mjs +6 -0
  52. package/esm/model/model.mjs.map +1 -1
  53. package/esm/query-builder/query-builder.d.mts +12 -1
  54. package/esm/query-builder/query-builder.d.mts.map +1 -1
  55. package/esm/query-builder/query-builder.mjs +19 -0
  56. package/esm/query-builder/query-builder.mjs.map +1 -1
  57. package/llms-full.txt +31 -1
  58. package/llms.txt +1 -1
  59. package/package.json +4 -4
  60. package/skills/README.md +1 -1
  61. 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.globalScopes.set(name, {
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.globalScopes.delete(name);
4836
+ ownScopeMap(ModelClass, "globalScopes").delete(name);
4823
4837
  }
4824
4838
  function addLocalModelScope(ModelClass, name, callback) {
4825
- ModelClass.localScopes.set(name, callback);
4839
+ ownScopeMap(ModelClass, "localScopes").set(name, callback);
4826
4840
  }
4827
4841
  function removeLocalModelScope(ModelClass, name) {
4828
- ModelClass.localScopes.delete(name);
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
- if (!Array.isArray(indexNameOrColumns)) indexNameOrColumns = [indexNameOrColumns];
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
- /** Delete the first matching row. */
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
- return this.limit(1).delete();
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
- /** Update matching rows. */
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
- return (await this.driver.updateMany(this.table, {}, { $set: fields })).modifiedCount;
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
- /** Unset fields from matching rows. */
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
- const updateObj = {};
15892
- for (const field of fields) updateObj[field] = 1;
15893
- return (await this.driver.updateMany(this.table, {}, { $unset: updateObj })).modifiedCount;
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": return null;
16463
- case "dropCheck": return null;
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
- if (column.type === "vector") return ["CREATE EXTENSION IF NOT EXISTS vector", sql];
16518
- return sql;
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, 1);
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
- if (shouldUseTransaction && databaseDriver.transaction) await databaseDriver.transaction(async () => {
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
- for (const sql of sqlStatements) await databaseDriver.query(sql);
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());