@warlock.js/cascade 4.5.0 → 4.6.1
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 +33 -0
- package/cjs/index.cjs +1012 -104
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/database-driver.contract.d.mts +27 -4
- package/esm/contracts/database-driver.contract.d.mts.map +1 -1
- package/esm/contracts/database-id-generator.contract.d.mts +38 -0
- package/esm/contracts/database-id-generator.contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/query-builder.contract.d.mts +27 -0
- package/esm/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/data-source/data-source.d.mts +21 -1
- package/esm/data-source/data-source.d.mts.map +1 -1
- package/esm/data-source/data-source.mjs +22 -0
- package/esm/data-source/data-source.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.mts +2 -2
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.mjs +5 -4
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts +98 -48
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.mjs +153 -59
- package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +25 -0
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +32 -0
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +36 -0
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +80 -1
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.mts +32 -4
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.mjs +57 -4
- package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.mts +83 -1
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +129 -16
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.mts +32 -0
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs +47 -1
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.mts +13 -2
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.mjs +21 -4
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
- package/esm/drivers/postgres/types.d.mts +15 -0
- package/esm/drivers/postgres/types.d.mts.map +1 -1
- package/esm/drivers/sql/sql-dialect.contract.d.mts +20 -0
- package/esm/drivers/sql/sql-dialect.contract.d.mts.map +1 -1
- package/esm/expressions/aggregate-expressions.d.mts +71 -34
- package/esm/expressions/aggregate-expressions.d.mts.map +1 -1
- package/esm/expressions/aggregate-expressions.mjs +80 -7
- package/esm/expressions/aggregate-expressions.mjs.map +1 -1
- package/esm/expressions/column-expressions.d.mts +193 -0
- package/esm/expressions/column-expressions.d.mts.map +1 -0
- package/esm/expressions/column-expressions.mjs +152 -0
- package/esm/expressions/column-expressions.mjs.map +1 -0
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +2 -1
- package/esm/model/methods/write-methods.d.mts +29 -0
- package/esm/model/methods/write-methods.d.mts.map +1 -0
- package/esm/model/methods/write-methods.mjs +164 -2
- package/esm/model/methods/write-methods.mjs.map +1 -1
- package/esm/model/model.d.mts +64 -3
- package/esm/model/model.d.mts.map +1 -1
- package/esm/model/model.mjs +65 -3
- package/esm/model/model.mjs.map +1 -1
- package/esm/writer/database-writer.d.mts.map +1 -1
- package/esm/writer/database-writer.mjs +4 -3
- package/esm/writer/database-writer.mjs.map +1 -1
- package/llms-full.txt +111 -8
- package/llms.txt +3 -3
- package/package.json +4 -4
- package/skills/README.md +3 -3
- package/skills/aggregate-data/SKILL.md +43 -3
- package/skills/manage-transactions/SKILL.md +45 -2
- package/skills/perform-atomic-ops/SKILL.md +23 -3
package/cjs/index.cjs
CHANGED
|
@@ -196,6 +196,28 @@ var DataSource = class {
|
|
|
196
196
|
const driver = this.driver;
|
|
197
197
|
if (typeof driver.getIdGenerator === "function") return driver.getIdGenerator();
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Execute a raw query against this data source's driver.
|
|
201
|
+
*
|
|
202
|
+
* Thin passthrough to `driver.query`, so it is transaction-aware: inside a
|
|
203
|
+
* `transaction()` scope the query auto-joins the active transaction
|
|
204
|
+
* client/session.
|
|
205
|
+
*
|
|
206
|
+
* @typeParam T - The expected row shape.
|
|
207
|
+
* @param sql - Raw SQL string (driver dialect).
|
|
208
|
+
* @param params - Optional positional query parameters.
|
|
209
|
+
* @returns The driver's raw query result (`rows` + `rowCount`).
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const { rows } = await dataSource.raw<{ count: number }>(
|
|
214
|
+
* "SELECT COUNT(*)::int AS count FROM users",
|
|
215
|
+
* );
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
raw(sql, params) {
|
|
219
|
+
return this.driver.query(sql, params);
|
|
220
|
+
}
|
|
199
221
|
};
|
|
200
222
|
|
|
201
223
|
//#endregion
|
|
@@ -3862,13 +3884,14 @@ var DatabaseWriter = class {
|
|
|
3862
3884
|
mode: isInsert ? "insert" : "update"
|
|
3863
3885
|
});
|
|
3864
3886
|
if (options.skipValidation || !this.schema) return;
|
|
3865
|
-
const
|
|
3887
|
+
const systemColumns = {
|
|
3866
3888
|
id: _warlock_js_seal.v.scalar().optional(),
|
|
3867
3889
|
_id: _warlock_js_seal.v.any().optional(),
|
|
3868
3890
|
...(0, _mongez_reinforcements.when)(this.ctor.createdAtColumn, () => ({ [this.ctor.createdAtColumn]: _warlock_js_seal.v.date().optional() })),
|
|
3869
3891
|
...(0, _mongez_reinforcements.when)(this.ctor.updatedAtColumn, () => ({ [this.ctor.updatedAtColumn]: _warlock_js_seal.v.date().optional() })),
|
|
3870
3892
|
...(0, _mongez_reinforcements.when)(this.ctor.deletedAtColumn, () => ({ [this.ctor.deletedAtColumn]: _warlock_js_seal.v.date().optional() }))
|
|
3871
|
-
}
|
|
3893
|
+
};
|
|
3894
|
+
const validationSchema = isInsert ? this.schema.clone().extend(systemColumns) : this.schema.clone(Object.keys(this.model.data)).extend(systemColumns);
|
|
3872
3895
|
if (this.strictMode === "strip") validationSchema.stripUnknown();
|
|
3873
3896
|
else if (this.strictMode === "fail") validationSchema.allowUnknown(false);
|
|
3874
3897
|
else if (this.strictMode === "allow") validationSchema.allowUnknown(true);
|
|
@@ -3899,7 +3922,7 @@ var DatabaseWriter = class {
|
|
|
3899
3922
|
await this.generateNextId();
|
|
3900
3923
|
const dataToInsert = this.model.data;
|
|
3901
3924
|
const createdAtColumn = this.ctor.createdAtColumn;
|
|
3902
|
-
if (createdAtColumn) dataToInsert[createdAtColumn] = /* @__PURE__ */ new Date();
|
|
3925
|
+
if (createdAtColumn && dataToInsert[createdAtColumn] == null) dataToInsert[createdAtColumn] = /* @__PURE__ */ new Date();
|
|
3903
3926
|
const updatedAtColumn = this.ctor.updatedAtColumn;
|
|
3904
3927
|
if (updatedAtColumn) dataToInsert[updatedAtColumn] = /* @__PURE__ */ new Date();
|
|
3905
3928
|
if (!options.skipEvents) await this.model.emitEvent("creating");
|
|
@@ -4857,6 +4880,16 @@ function getGlobalEvents() {
|
|
|
4857
4880
|
|
|
4858
4881
|
//#endregion
|
|
4859
4882
|
//#region ../@warlock.js/cascade/src/model/methods/write-methods.ts
|
|
4883
|
+
/**
|
|
4884
|
+
* Default number of rows processed per chunk by {@link createManyRecords}.
|
|
4885
|
+
*
|
|
4886
|
+
* Chunking keeps a single huge `createMany` call from flooding the driver:
|
|
4887
|
+
* the default path runs each chunk's `save()` calls under one `Promise.all`,
|
|
4888
|
+
* and the bulk path emits one multi-row `insertMany` per chunk so the
|
|
4889
|
+
* generated `INSERT ... VALUES (...),(...)` stays under the database's bind
|
|
4890
|
+
* parameter ceiling.
|
|
4891
|
+
*/
|
|
4892
|
+
const DEFAULT_CREATE_MANY_BATCH_SIZE = 500;
|
|
4860
4893
|
async function saveModel(model, options) {
|
|
4861
4894
|
if (options?.merge) model.merge(options.merge);
|
|
4862
4895
|
await new DatabaseWriter(model).save(options);
|
|
@@ -4867,8 +4900,160 @@ async function createRecord(ModelClass, data) {
|
|
|
4867
4900
|
await model.save();
|
|
4868
4901
|
return model;
|
|
4869
4902
|
}
|
|
4870
|
-
|
|
4871
|
-
|
|
4903
|
+
/**
|
|
4904
|
+
* Split an array into sequential chunks of at most `size` elements.
|
|
4905
|
+
*
|
|
4906
|
+
* @param items - The array to split.
|
|
4907
|
+
* @param size - Maximum chunk length (callers pass a value `>= 1`).
|
|
4908
|
+
* @returns An array of chunks preserving the original order.
|
|
4909
|
+
*/
|
|
4910
|
+
function chunkArray(items, size) {
|
|
4911
|
+
const chunks = [];
|
|
4912
|
+
for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
|
|
4913
|
+
return chunks;
|
|
4914
|
+
}
|
|
4915
|
+
/**
|
|
4916
|
+
* Create many records, chunking the work so a huge array can't overwhelm the
|
|
4917
|
+
* driver.
|
|
4918
|
+
*
|
|
4919
|
+
* Two strategies are available via `options.bulk`:
|
|
4920
|
+
*
|
|
4921
|
+
* - **Default (`bulk` falsy):** each row is persisted through `save()`, so
|
|
4922
|
+
* model hooks, lifecycle events, casts and generated ids are all preserved.
|
|
4923
|
+
* Rows are processed in sequential chunks of `batchSize`, each chunk running
|
|
4924
|
+
* under a single `Promise.all`, so an array of millions of rows can't open
|
|
4925
|
+
* millions of concurrent inserts at once.
|
|
4926
|
+
*
|
|
4927
|
+
* - **Bulk (`bulk: true`):** each chunk is routed to the driver's native
|
|
4928
|
+
* multi-row insert (`insertMany`) for 10–100× throughput. This SKIPS the
|
|
4929
|
+
* per-row save lifecycle (no `saving` / `creating` / `created` / `saved`
|
|
4930
|
+
* events, hooks, or sync). Rows are still prepped through the writer
|
|
4931
|
+
* pipeline (validation, casting, timestamps, defaults, id-generation) so the
|
|
4932
|
+
* persisted columns match the default path, and the values returned by the
|
|
4933
|
+
* driver (generated `_id`, timestamps, SQL `RETURNING *`) are merged back
|
|
4934
|
+
* onto the returned models.
|
|
4935
|
+
*
|
|
4936
|
+
* Both strategies return the created model instances (an empty array is a
|
|
4937
|
+
* no-op that never touches the driver).
|
|
4938
|
+
*
|
|
4939
|
+
* @param ModelClass - The model class to create records for.
|
|
4940
|
+
* @param data - The rows to insert.
|
|
4941
|
+
* @param options - Chunking / bulk options.
|
|
4942
|
+
* @returns The created model instances.
|
|
4943
|
+
*
|
|
4944
|
+
* @example
|
|
4945
|
+
* ```typescript
|
|
4946
|
+
* // Default: per-row save(), chunked in batches of 500
|
|
4947
|
+
* const users = await User.createMany(rows);
|
|
4948
|
+
*
|
|
4949
|
+
* // Bulk: one multi-row INSERT per 1000-row chunk (skips per-row hooks)
|
|
4950
|
+
* const users = await User.createMany(rows, { bulk: true, batchSize: 1000 });
|
|
4951
|
+
* ```
|
|
4952
|
+
*/
|
|
4953
|
+
async function createManyRecords(ModelClass, data, options = {}) {
|
|
4954
|
+
if (data.length === 0) return [];
|
|
4955
|
+
const chunks = chunkArray(data, options.batchSize && options.batchSize > 0 ? options.batchSize : 500);
|
|
4956
|
+
if (options.bulk === true) return await createManyBulk(ModelClass, chunks);
|
|
4957
|
+
const created = [];
|
|
4958
|
+
for (const chunk of chunks) {
|
|
4959
|
+
const models = chunk.map((item) => new ModelClass(item));
|
|
4960
|
+
await assignIdBlock(ModelClass, models);
|
|
4961
|
+
const chunkModels = await Promise.all(models.map(async (model) => {
|
|
4962
|
+
await model.save();
|
|
4963
|
+
return model;
|
|
4964
|
+
}));
|
|
4965
|
+
created.push(...chunkModels);
|
|
4966
|
+
}
|
|
4967
|
+
return created;
|
|
4968
|
+
}
|
|
4969
|
+
/**
|
|
4970
|
+
* Reserve a single contiguous id block for one chunk and pre-assign the ids to
|
|
4971
|
+
* the models that don't already carry one, so the per-row writer's id
|
|
4972
|
+
* generation no-ops (its `model.get("id")` guard short-circuits). This turns N
|
|
4973
|
+
* per-row counter round-trips into ONE per chunk.
|
|
4974
|
+
*
|
|
4975
|
+
* Falls back to per-row generation (a no-op here) when:
|
|
4976
|
+
* - the model disables auto-generation (`autoGenerateId === false`) — e.g. SQL,
|
|
4977
|
+
* which uses native `SERIAL`;
|
|
4978
|
+
* - the data source's id generator has no batch API (`generateNextIds`) — the
|
|
4979
|
+
* optional contract member is feature-detected;
|
|
4980
|
+
* - the model uses a random initial id / increment — a random stride can't form
|
|
4981
|
+
* a contiguous block, so those models keep per-row generation.
|
|
4982
|
+
*
|
|
4983
|
+
* Only models WITHOUT a caller-supplied id consume a block slot, so a chunk
|
|
4984
|
+
* mixing supplied and auto ids allocates exactly the right number (no gaps).
|
|
4985
|
+
*
|
|
4986
|
+
* @param ModelClass - The model class being inserted.
|
|
4987
|
+
* @param models - The freshly-constructed (unsaved) models for one chunk.
|
|
4988
|
+
*/
|
|
4989
|
+
async function assignIdBlock(ModelClass, models) {
|
|
4990
|
+
if (!ModelClass.autoGenerateId || ModelClass.randomInitialId || ModelClass.randomIncrement) return;
|
|
4991
|
+
const idGenerator = ModelClass.getDataSource().idGenerator;
|
|
4992
|
+
if (!idGenerator || typeof idGenerator.generateNextIds !== "function") return;
|
|
4993
|
+
const idLessModels = models.filter((model) => !model.get("id"));
|
|
4994
|
+
if (idLessModels.length === 0) return;
|
|
4995
|
+
const ids = await idGenerator.generateNextIds({
|
|
4996
|
+
table: ModelClass.table,
|
|
4997
|
+
initialId: ModelClass.initialId || 1,
|
|
4998
|
+
incrementIdBy: ModelClass.incrementIdBy || 1,
|
|
4999
|
+
count: idLessModels.length
|
|
5000
|
+
});
|
|
5001
|
+
if (ids.length !== idLessModels.length) throw new Error(`generateNextIds returned ${ids.length} id(s) for ${idLessModels.length} row(s) on "${ModelClass.table}" — a batch id generator must return exactly the requested count.`);
|
|
5002
|
+
idLessModels.forEach((model, index) => {
|
|
5003
|
+
model.set("id", ids[index]);
|
|
5004
|
+
});
|
|
5005
|
+
}
|
|
5006
|
+
/**
|
|
5007
|
+
* Bulk variant of {@link createManyRecords}: prep each row through the writer
|
|
5008
|
+
* pipeline (validation, casting, timestamps, defaults, id-generation) without
|
|
5009
|
+
* touching the database, then flush each chunk with a single
|
|
5010
|
+
* `driver.insertMany` call.
|
|
5011
|
+
*
|
|
5012
|
+
* The writer reuse is achieved by swapping the driver's single-row `insert`
|
|
5013
|
+
* for a capturing stub for the lifetime of the bulk operation: running
|
|
5014
|
+
* `writer.save({ skipEvents, skipSync })` exercises the real prep but records
|
|
5015
|
+
* the prepared document instead of issuing N single-row inserts. The original
|
|
5016
|
+
* `insert` is always restored in `finally`.
|
|
5017
|
+
*
|
|
5018
|
+
* @param ModelClass - The model class to create records for.
|
|
5019
|
+
* @param chunks - Pre-chunked rows.
|
|
5020
|
+
* @returns The created model instances with driver-returned values merged in.
|
|
5021
|
+
*/
|
|
5022
|
+
async function createManyBulk(ModelClass, chunks) {
|
|
5023
|
+
const driver = ModelClass.getDataSource().driver;
|
|
5024
|
+
const table = ModelClass.table;
|
|
5025
|
+
const originalInsert = driver.insert;
|
|
5026
|
+
driver.insert = (async (_table, document) => {
|
|
5027
|
+
return { document };
|
|
5028
|
+
});
|
|
5029
|
+
const created = [];
|
|
5030
|
+
try {
|
|
5031
|
+
for (const chunk of chunks) {
|
|
5032
|
+
const models = chunk.map((item) => new ModelClass(item));
|
|
5033
|
+
await assignIdBlock(ModelClass, models);
|
|
5034
|
+
const preparedDocuments = await Promise.all(models.map(async (model) => {
|
|
5035
|
+
await new DatabaseWriter(model).save({
|
|
5036
|
+
skipEvents: true,
|
|
5037
|
+
skipSync: true
|
|
5038
|
+
});
|
|
5039
|
+
return { ...model.data };
|
|
5040
|
+
}));
|
|
5041
|
+
const results = await driver.insertMany(table, preparedDocuments);
|
|
5042
|
+
models.forEach((model, index) => {
|
|
5043
|
+
const result = results[index];
|
|
5044
|
+
if (result) {
|
|
5045
|
+
const returnedDocument = "document" in result ? result.document : result;
|
|
5046
|
+
model.merge(returnedDocument);
|
|
5047
|
+
}
|
|
5048
|
+
model.dirtyTracker.reset();
|
|
5049
|
+
model.isNew = false;
|
|
5050
|
+
});
|
|
5051
|
+
created.push(...models);
|
|
5052
|
+
}
|
|
5053
|
+
} finally {
|
|
5054
|
+
driver.insert = originalInsert;
|
|
5055
|
+
}
|
|
5056
|
+
return created;
|
|
4872
5057
|
}
|
|
4873
5058
|
async function findOrCreateRecord(ModelClass, filter, data) {
|
|
4874
5059
|
const existing = await ModelClass.first(filter);
|
|
@@ -5760,6 +5945,39 @@ var Model = class Model {
|
|
|
5760
5945
|
return this.getDataSource().driver;
|
|
5761
5946
|
}
|
|
5762
5947
|
/**
|
|
5948
|
+
* Execute a raw query against this model's driver.
|
|
5949
|
+
*
|
|
5950
|
+
* Delegates to `getDriver().query`, so it inherits the driver's
|
|
5951
|
+
* transaction awareness: when called inside an active `transaction()`
|
|
5952
|
+
* scope the query auto-joins that transaction's client/session, otherwise
|
|
5953
|
+
* it runs on the pool. The return value is the driver's raw result
|
|
5954
|
+
* ({@link RawQueryResult}) — `rows` typed as `T` plus `rowCount` — not
|
|
5955
|
+
* hydrated model instances.
|
|
5956
|
+
*
|
|
5957
|
+
* Note: MongoDB drivers throw, as they do not support raw SQL.
|
|
5958
|
+
*
|
|
5959
|
+
* @typeParam T - The expected row shape.
|
|
5960
|
+
* @param sql - Raw SQL string (driver dialect).
|
|
5961
|
+
* @param params - Optional positional query parameters.
|
|
5962
|
+
* @returns The driver's raw query result.
|
|
5963
|
+
*
|
|
5964
|
+
* @example
|
|
5965
|
+
* ```typescript
|
|
5966
|
+
* const { rows } = await User.raw<{ id: number; total: number }>(
|
|
5967
|
+
* "SELECT id, COUNT(*) AS total FROM orders WHERE user_id = $1 GROUP BY id",
|
|
5968
|
+
* [userId],
|
|
5969
|
+
* );
|
|
5970
|
+
*
|
|
5971
|
+
* // Auto-joins the active transaction() scope:
|
|
5972
|
+
* await User.transaction(async () => {
|
|
5973
|
+
* await User.raw("UPDATE users SET active = true WHERE id = $1", [id]);
|
|
5974
|
+
* });
|
|
5975
|
+
* ```
|
|
5976
|
+
*/
|
|
5977
|
+
static raw(sql, params) {
|
|
5978
|
+
return this.getDriver().query(sql, params);
|
|
5979
|
+
}
|
|
5980
|
+
/**
|
|
5763
5981
|
* Generate next id and set it to current model's id
|
|
5764
5982
|
*/
|
|
5765
5983
|
async generateNextId() {
|
|
@@ -6297,10 +6515,39 @@ var Model = class Model {
|
|
|
6297
6515
|
return createRecord(this, data);
|
|
6298
6516
|
}
|
|
6299
6517
|
/**
|
|
6300
|
-
* Create many
|
|
6518
|
+
* Create many records and return an array of created models.
|
|
6519
|
+
*
|
|
6520
|
+
* The work is always chunked into batches of `options.batchSize`
|
|
6521
|
+
* (default 500) so a huge input array can't overwhelm the driver.
|
|
6522
|
+
*
|
|
6523
|
+
* **Default path** (no options, or `bulk` falsy): each row is persisted
|
|
6524
|
+
* through `save()`, preserving model hooks, lifecycle events, casts and
|
|
6525
|
+
* generated ids. Each chunk runs under a single `Promise.all`.
|
|
6526
|
+
*
|
|
6527
|
+
* **Bulk path** (`bulk: true`): each chunk is routed to the driver's native
|
|
6528
|
+
* multi-row insert (`insertMany`) for 10–100× throughput. This SKIPS the
|
|
6529
|
+
* per-row save lifecycle (no `saving` / `creating` / `created` / `saved`
|
|
6530
|
+
* events, hooks, or sync). Casts, timestamps, defaults and id-generation are
|
|
6531
|
+
* still applied (rows are prepped through the same writer pipeline) so the
|
|
6532
|
+
* persisted columns match the default path.
|
|
6533
|
+
*
|
|
6534
|
+
* An empty array is a no-op that never touches the driver.
|
|
6535
|
+
*
|
|
6536
|
+
* @param data - The rows to insert.
|
|
6537
|
+
* @param options - Chunking / bulk options.
|
|
6538
|
+
* @returns The created model instances.
|
|
6539
|
+
*
|
|
6540
|
+
* @example
|
|
6541
|
+
* ```typescript
|
|
6542
|
+
* // Default: per-row save(), chunked in batches of 500
|
|
6543
|
+
* const users = await User.createMany(rows);
|
|
6544
|
+
*
|
|
6545
|
+
* // Bulk: one multi-row INSERT per 1000-row chunk (skips per-row hooks)
|
|
6546
|
+
* const users = await User.createMany(rows, { bulk: true, batchSize: 1000 });
|
|
6547
|
+
* ```
|
|
6301
6548
|
*/
|
|
6302
|
-
static async createMany(data) {
|
|
6303
|
-
return createManyRecords(this, data);
|
|
6549
|
+
static async createMany(data, options) {
|
|
6550
|
+
return createManyRecords(this, data, options);
|
|
6304
6551
|
}
|
|
6305
6552
|
/**
|
|
6306
6553
|
* Find a record or create it if not found.
|
|
@@ -6699,26 +6946,99 @@ const $agg = {
|
|
|
6699
6946
|
};
|
|
6700
6947
|
},
|
|
6701
6948
|
/**
|
|
6702
|
-
*
|
|
6949
|
+
* Count the number of distinct values of a field in each group.
|
|
6950
|
+
*
|
|
6951
|
+
* @param field - The field name to count distinct values of
|
|
6952
|
+
* @returns Abstract count-distinct expression
|
|
6953
|
+
*
|
|
6954
|
+
* @example
|
|
6955
|
+
* ```typescript
|
|
6956
|
+
* query.groupBy("country", {
|
|
6957
|
+
* uniqueCities: $agg.countDistinct("city")
|
|
6958
|
+
* });
|
|
6959
|
+
* ```
|
|
6960
|
+
*
|
|
6961
|
+
* **Cross-driver.** Translates to:
|
|
6962
|
+
* - SQL: `COUNT(DISTINCT city)`
|
|
6963
|
+
* - MongoDB: `{ $addToSet: "$city" }` in the `$group` stage, finalized with
|
|
6964
|
+
* `{ $size: "$uniqueCities" }` in the renaming `$project` (the standard
|
|
6965
|
+
* distinct-count-per-group pattern, since `$size` is not a `$group`
|
|
6966
|
+
* accumulator).
|
|
6967
|
+
*/
|
|
6968
|
+
countDistinct(field) {
|
|
6969
|
+
return {
|
|
6970
|
+
__agg: "countDistinct",
|
|
6971
|
+
__field: field
|
|
6972
|
+
};
|
|
6973
|
+
},
|
|
6974
|
+
/**
|
|
6975
|
+
* Sum a numeric field — or a composed arithmetic expression — across
|
|
6976
|
+
* documents in each group.
|
|
6703
6977
|
*
|
|
6704
|
-
*
|
|
6978
|
+
* Pass a bare column name to sum a single field (unchanged from v1), or a
|
|
6979
|
+
* typed `$expr` node (`$expr.mul` / `$expr.add` / `$expr.sub` / `$expr.div` /
|
|
6980
|
+
* `$expr.col` / `$expr.lit`) to sum a computed value such as `price * quantity`.
|
|
6981
|
+
*
|
|
6982
|
+
* @param input - A field name or a typed `ColumnExpression`
|
|
6705
6983
|
* @returns Abstract sum expression
|
|
6706
6984
|
*
|
|
6707
6985
|
* @example
|
|
6708
6986
|
* ```typescript
|
|
6987
|
+
* import { $agg, $expr } from "@warlock.js/cascade";
|
|
6988
|
+
*
|
|
6989
|
+
* // Bare column
|
|
6990
|
+
* query.groupBy("type", { totalDuration: $agg.sum("duration") });
|
|
6991
|
+
*
|
|
6992
|
+
* // Composed expression: SUM(price * quantity)
|
|
6993
|
+
* query.groupBy("type", { revenue: $agg.sum($expr.mul("price", "quantity")) });
|
|
6994
|
+
* ```
|
|
6995
|
+
*
|
|
6996
|
+
* Translates to:
|
|
6997
|
+
* - SQL: `SUM("duration")` / `SUM(("price" * "quantity"))`
|
|
6998
|
+
* - MongoDB: `{ $sum: "$duration" }` / `{ $sum: { $multiply: ["$price", "$quantity"] } }`
|
|
6999
|
+
*/
|
|
7000
|
+
sum(input) {
|
|
7001
|
+
if (typeof input === "string") return {
|
|
7002
|
+
__agg: "sum",
|
|
7003
|
+
__field: input
|
|
7004
|
+
};
|
|
7005
|
+
return {
|
|
7006
|
+
__agg: "sum",
|
|
7007
|
+
__field: null,
|
|
7008
|
+
__expr: input
|
|
7009
|
+
};
|
|
7010
|
+
},
|
|
7011
|
+
/**
|
|
7012
|
+
* Sum a raw, driver-native expression escape hatch.
|
|
7013
|
+
*
|
|
7014
|
+
* Equivalent to `$agg.sum($expr.raw(expression))`. The raw string is emitted
|
|
7015
|
+
* verbatim into the generated query — never build it from untrusted input.
|
|
7016
|
+
* Reach for the typed {@link sum} form first; this exists only for fragments
|
|
7017
|
+
* the typed combinators can't express.
|
|
7018
|
+
*
|
|
7019
|
+
* @param expression - A raw expression fragment (e.g. `"price * quantity"`)
|
|
7020
|
+
* @returns Abstract sum expression wrapping the raw fragment
|
|
7021
|
+
*
|
|
7022
|
+
* @example
|
|
7023
|
+
* ```typescript
|
|
6709
7024
|
* query.groupBy("type", {
|
|
6710
|
-
*
|
|
7025
|
+
* net: $agg.sumRaw("price * quantity * (1 - discount)"),
|
|
6711
7026
|
* });
|
|
6712
7027
|
* ```
|
|
6713
7028
|
*
|
|
6714
7029
|
* Translates to:
|
|
6715
|
-
* -
|
|
6716
|
-
* -
|
|
7030
|
+
* - SQL: `SUM(price * quantity * (1 - discount))`
|
|
7031
|
+
* - MongoDB: throws — raw SQL fragments are not portable to a pipeline; use
|
|
7032
|
+
* the typed {@link sum} form (or `groupByRaw`) on MongoDB instead.
|
|
6717
7033
|
*/
|
|
6718
|
-
|
|
7034
|
+
sumRaw(expression) {
|
|
6719
7035
|
return {
|
|
6720
7036
|
__agg: "sum",
|
|
6721
|
-
__field:
|
|
7037
|
+
__field: null,
|
|
7038
|
+
__expr: {
|
|
7039
|
+
__expr: "raw",
|
|
7040
|
+
expression
|
|
7041
|
+
}
|
|
6722
7042
|
};
|
|
6723
7043
|
},
|
|
6724
7044
|
/**
|
|
@@ -6891,6 +7211,156 @@ const $agg = {
|
|
|
6891
7211
|
}
|
|
6892
7212
|
};
|
|
6893
7213
|
|
|
7214
|
+
//#endregion
|
|
7215
|
+
//#region ../@warlock.js/cascade/src/expressions/column-expressions.ts
|
|
7216
|
+
/**
|
|
7217
|
+
* Type guard: is `value` one of the typed column-expression nodes?
|
|
7218
|
+
*
|
|
7219
|
+
* @param value - The value to test
|
|
7220
|
+
* @returns `true` when `value` is a `ColumnExpression` node
|
|
7221
|
+
*/
|
|
7222
|
+
function isColumnExpression(value) {
|
|
7223
|
+
return typeof value === "object" && value !== null && typeof value.__expr === "string";
|
|
7224
|
+
}
|
|
7225
|
+
/**
|
|
7226
|
+
* Reference a column/field by name.
|
|
7227
|
+
*
|
|
7228
|
+
* @param column - The column/field name
|
|
7229
|
+
* @returns A column-reference expression node
|
|
7230
|
+
*/
|
|
7231
|
+
function col(column) {
|
|
7232
|
+
return {
|
|
7233
|
+
__expr: "column",
|
|
7234
|
+
column
|
|
7235
|
+
};
|
|
7236
|
+
}
|
|
7237
|
+
/**
|
|
7238
|
+
* A scalar literal (number or boolean) operand.
|
|
7239
|
+
*
|
|
7240
|
+
* @param value - The literal value
|
|
7241
|
+
* @returns A literal expression node
|
|
7242
|
+
*/
|
|
7243
|
+
function lit(value) {
|
|
7244
|
+
return {
|
|
7245
|
+
__expr: "literal",
|
|
7246
|
+
value
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
7249
|
+
/**
|
|
7250
|
+
* Multiply two or more operands.
|
|
7251
|
+
*
|
|
7252
|
+
* @param operands - Column names, literals, or nested expression nodes
|
|
7253
|
+
* @returns A multiply expression node
|
|
7254
|
+
*/
|
|
7255
|
+
function mul(...operands) {
|
|
7256
|
+
return {
|
|
7257
|
+
__expr: "multiply",
|
|
7258
|
+
operands: operands.map(toColumnExpression)
|
|
7259
|
+
};
|
|
7260
|
+
}
|
|
7261
|
+
/**
|
|
7262
|
+
* Add two or more operands.
|
|
7263
|
+
*
|
|
7264
|
+
* @param operands - Column names, literals, or nested expression nodes
|
|
7265
|
+
* @returns An add expression node
|
|
7266
|
+
*/
|
|
7267
|
+
function add(...operands) {
|
|
7268
|
+
return {
|
|
7269
|
+
__expr: "add",
|
|
7270
|
+
operands: operands.map(toColumnExpression)
|
|
7271
|
+
};
|
|
7272
|
+
}
|
|
7273
|
+
/**
|
|
7274
|
+
* Subtract `right` from `left`.
|
|
7275
|
+
*
|
|
7276
|
+
* @param left - The minuend
|
|
7277
|
+
* @param right - The subtrahend
|
|
7278
|
+
* @returns A subtract expression node
|
|
7279
|
+
*/
|
|
7280
|
+
function sub(left, right) {
|
|
7281
|
+
return {
|
|
7282
|
+
__expr: "subtract",
|
|
7283
|
+
operands: [toColumnExpression(left), toColumnExpression(right)]
|
|
7284
|
+
};
|
|
7285
|
+
}
|
|
7286
|
+
/**
|
|
7287
|
+
* Divide `left` by `right`.
|
|
7288
|
+
*
|
|
7289
|
+
* @param left - The dividend
|
|
7290
|
+
* @param right - The divisor
|
|
7291
|
+
* @returns A divide expression node
|
|
7292
|
+
*/
|
|
7293
|
+
function div(left, right) {
|
|
7294
|
+
return {
|
|
7295
|
+
__expr: "divide",
|
|
7296
|
+
operands: [toColumnExpression(left), toColumnExpression(right)]
|
|
7297
|
+
};
|
|
7298
|
+
}
|
|
7299
|
+
/**
|
|
7300
|
+
* Wrap a raw expression string as an escape hatch.
|
|
7301
|
+
*
|
|
7302
|
+
* The string is emitted verbatim — never build it from untrusted input. Use
|
|
7303
|
+
* this only when the typed `$expr.mul` / `$expr.add` / `$expr.sub` /
|
|
7304
|
+
* `$expr.div` / `$expr.col` / `$expr.lit` combinators can't express what you
|
|
7305
|
+
* need.
|
|
7306
|
+
*
|
|
7307
|
+
* @param expression - A raw, driver-native expression fragment
|
|
7308
|
+
* @returns A raw expression node
|
|
7309
|
+
*/
|
|
7310
|
+
function raw(expression) {
|
|
7311
|
+
return {
|
|
7312
|
+
__expr: "raw",
|
|
7313
|
+
expression
|
|
7314
|
+
};
|
|
7315
|
+
}
|
|
7316
|
+
/**
|
|
7317
|
+
* Normalize an expression input to a `ColumnExpression` node. A bare string
|
|
7318
|
+
* becomes a `$expr.col(...)` reference; an existing node passes through
|
|
7319
|
+
* unchanged.
|
|
7320
|
+
*
|
|
7321
|
+
* @param input - A typed expression node or a bare column-name string
|
|
7322
|
+
* @returns The corresponding `ColumnExpression` node
|
|
7323
|
+
*/
|
|
7324
|
+
function toColumnExpression(input) {
|
|
7325
|
+
return typeof input === "string" ? col(input) : input;
|
|
7326
|
+
}
|
|
7327
|
+
/**
|
|
7328
|
+
* Typed, database-agnostic column-expression builders.
|
|
7329
|
+
*
|
|
7330
|
+
* Grouped under one object (like `$agg`) so the scalar arithmetic that feeds
|
|
7331
|
+
* an aggregate reads as a single, discoverable namespace: `$agg.sum($expr.mul(
|
|
7332
|
+
* "price", "quantity"))`. Each builder returns a plain, closed `ColumnExpression`
|
|
7333
|
+
* node — the driver translates it to native SQL / a Mongo pipeline.
|
|
7334
|
+
*
|
|
7335
|
+
* @example
|
|
7336
|
+
* ```typescript
|
|
7337
|
+
* import { $agg, $expr } from "@warlock.js/cascade";
|
|
7338
|
+
*
|
|
7339
|
+
* // SUM(price * quantity)
|
|
7340
|
+
* query.groupBy("product_id", { revenue: $agg.sum($expr.mul("price", "quantity")) });
|
|
7341
|
+
*
|
|
7342
|
+
* // SUM((1 - discount) * price)
|
|
7343
|
+
* query.groupBy("product_id", {
|
|
7344
|
+
* net: $agg.sum($expr.mul($expr.sub($expr.lit(1), "discount"), "price")),
|
|
7345
|
+
* });
|
|
7346
|
+
*
|
|
7347
|
+
* // price * 1.2 (e.g. tax)
|
|
7348
|
+
* $agg.sum($expr.mul("price", $expr.lit(1.2)));
|
|
7349
|
+
*
|
|
7350
|
+
* // escape hatch (Postgres-only, emitted verbatim — never from user input)
|
|
7351
|
+
* $agg.sum($expr.raw("price * quantity * (1 - discount)"));
|
|
7352
|
+
* ```
|
|
7353
|
+
*/
|
|
7354
|
+
const $expr = {
|
|
7355
|
+
col,
|
|
7356
|
+
lit,
|
|
7357
|
+
mul,
|
|
7358
|
+
add,
|
|
7359
|
+
sub,
|
|
7360
|
+
div,
|
|
7361
|
+
raw
|
|
7362
|
+
};
|
|
7363
|
+
|
|
6894
7364
|
//#endregion
|
|
6895
7365
|
//#region ../@warlock.js/cascade/src/utils/is-valid-date-value.ts
|
|
6896
7366
|
const isoRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/;
|
|
@@ -6963,43 +7433,54 @@ var MongoDBBlueprint = class {
|
|
|
6963
7433
|
|
|
6964
7434
|
//#endregion
|
|
6965
7435
|
//#region ../@warlock.js/cascade/src/drivers/mongodb/mongodb-id-generator.ts
|
|
7436
|
+
/** Number of times a reservation retries after a duplicate-key (E11000) error. */
|
|
7437
|
+
const MAX_RESERVE_ATTEMPTS = 3;
|
|
7438
|
+
/** MongoDB duplicate-key error code. */
|
|
7439
|
+
const DUPLICATE_KEY_ERROR_CODE = 11e3;
|
|
7440
|
+
/**
|
|
7441
|
+
* Is `error` a MongoDB duplicate-key (E11000) error?
|
|
7442
|
+
*
|
|
7443
|
+
* Raised when two concurrent first-time upserts race to create the same
|
|
7444
|
+
* counter document once the unique index on `{ collection: 1 }` exists.
|
|
7445
|
+
*/
|
|
7446
|
+
function isDuplicateKeyError(error) {
|
|
7447
|
+
return typeof error === "object" && error !== null && error.code === DUPLICATE_KEY_ERROR_CODE;
|
|
7448
|
+
}
|
|
6966
7449
|
/**
|
|
6967
7450
|
* MongoDB-specific ID generator for auto-incrementing integer IDs.
|
|
6968
7451
|
*
|
|
6969
|
-
* Maintains a separate collection that tracks the last generated ID for each
|
|
6970
|
-
*
|
|
7452
|
+
* Maintains a separate collection that tracks the last generated ID for each
|
|
7453
|
+
* table, mimicking SQL's `AUTO_INCREMENT` / `SERIAL`.
|
|
6971
7454
|
*
|
|
6972
7455
|
* **Collection Structure:**
|
|
6973
7456
|
* ```json
|
|
6974
|
-
* {
|
|
6975
|
-
* "collection": "users",
|
|
6976
|
-
* "id": 12345
|
|
6977
|
-
* }
|
|
7457
|
+
* { "collection": "users", "id": 12345 }
|
|
6978
7458
|
* ```
|
|
6979
7459
|
*
|
|
6980
|
-
* **
|
|
6981
|
-
* -
|
|
6982
|
-
*
|
|
6983
|
-
*
|
|
6984
|
-
*
|
|
7460
|
+
* **Atomicity & concurrency:**
|
|
7461
|
+
* - Each id (or block of ids) is reserved with a SINGLE atomic
|
|
7462
|
+
* `findOneAndUpdate` on one document; MongoDB serializes concurrent writes to
|
|
7463
|
+
* the same document, so concurrent callers receive distinct, non-overlapping
|
|
7464
|
+
* ids/blocks.
|
|
7465
|
+
* - A unique index on `{ collection: 1 }` is ensured lazily (once per instance)
|
|
7466
|
+
* so two concurrent first-time upserts for a brand-new table can't create
|
|
7467
|
+
* duplicate counter documents; the loser of that race gets an E11000 and is
|
|
7468
|
+
* retried (by then the document exists, so it takes the increment branch).
|
|
7469
|
+
*
|
|
7470
|
+
* **Transactions — IMPORTANT:** the counter write is its OWN standalone,
|
|
7471
|
+
* immediately-durable operation. It does NOT join an ambient
|
|
7472
|
+
* `transaction()` session (it calls `findOneAndUpdate` directly without a
|
|
7473
|
+
* session). This is intentional and matches SQL `SERIAL` semantics: if the
|
|
7474
|
+
* surrounding transaction rolls back, the inserted records are undone but the
|
|
7475
|
+
* consumed id(s) are NOT — they remain a gap in the sequence. Do not rely on
|
|
7476
|
+
* id reservation being rolled back with a transaction.
|
|
6985
7477
|
*
|
|
6986
7478
|
* @example
|
|
6987
7479
|
* ```typescript
|
|
6988
|
-
* const mongoDriver = new MongoDbDriver(config);
|
|
6989
7480
|
* const idGenerator = new MongoIdGenerator(mongoDriver);
|
|
6990
7481
|
*
|
|
6991
|
-
* const
|
|
6992
|
-
*
|
|
6993
|
-
* driver: mongoDriver,
|
|
6994
|
-
* idGenerator,
|
|
6995
|
-
* });
|
|
6996
|
-
*
|
|
6997
|
-
* // Generate IDs with custom configuration
|
|
6998
|
-
* const id = await idGenerator.generateNextId({
|
|
6999
|
-
* table: "users",
|
|
7000
|
-
* initialId: 1000,
|
|
7001
|
-
* incrementIdBy: 1
|
|
7002
|
-
* });
|
|
7482
|
+
* const id = await idGenerator.generateNextId({ table: "users" }); // one id
|
|
7483
|
+
* const ids = await idGenerator.generateNextIds({ table: "users", count: 100 }); // a block
|
|
7003
7484
|
* ```
|
|
7004
7485
|
*/
|
|
7005
7486
|
var MongoIdGenerator = class {
|
|
@@ -7012,15 +7493,15 @@ var MongoIdGenerator = class {
|
|
|
7012
7493
|
*/
|
|
7013
7494
|
counterCollection = "MasterMind";
|
|
7014
7495
|
/**
|
|
7496
|
+
* Memoized "ensure unique index" promise — the index is created at most once
|
|
7497
|
+
* per generator instance, before the first reservation completes.
|
|
7498
|
+
*/
|
|
7499
|
+
indexEnsured;
|
|
7500
|
+
/**
|
|
7015
7501
|
* Create a new MongoDB ID generator instance.
|
|
7016
7502
|
*
|
|
7017
7503
|
* @param driver - The MongoDB driver instance
|
|
7018
7504
|
* @param counterCollection - Name of the collection storing ID counters (default: "MasterMind")
|
|
7019
|
-
*
|
|
7020
|
-
* @example
|
|
7021
|
-
* ```typescript
|
|
7022
|
-
* const idGenerator = new MongoIdGenerator(mongoDriver, "id_counters");
|
|
7023
|
-
* ```
|
|
7024
7505
|
*/
|
|
7025
7506
|
constructor(driver, counterCollection) {
|
|
7026
7507
|
this.driver = driver;
|
|
@@ -7029,66 +7510,149 @@ var MongoIdGenerator = class {
|
|
|
7029
7510
|
/**
|
|
7030
7511
|
* Generate the next ID for a table.
|
|
7031
7512
|
*
|
|
7032
|
-
*
|
|
7033
|
-
*
|
|
7513
|
+
* Reserves a single id via one atomic `findOneAndUpdate` (see the class doc
|
|
7514
|
+
* for the atomicity / transaction contract). Equivalent to a block of size 1.
|
|
7034
7515
|
*
|
|
7035
7516
|
* @param options - Configuration for ID generation
|
|
7036
7517
|
* @returns The generated ID
|
|
7037
7518
|
*
|
|
7038
7519
|
* @example
|
|
7039
7520
|
* ```typescript
|
|
7040
|
-
*
|
|
7041
|
-
* const id = await idGenerator.generateNextId({ table: "users" });
|
|
7042
|
-
*
|
|
7043
|
-
* // With custom initial ID
|
|
7044
|
-
* const id = await idGenerator.generateNextId({
|
|
7045
|
-
* table: "products",
|
|
7046
|
-
* initialId: 1000,
|
|
7047
|
-
* incrementIdBy: 1
|
|
7048
|
-
* });
|
|
7521
|
+
* const id = await idGenerator.generateNextId({ table: "users", initialId: 1000 });
|
|
7049
7522
|
* ```
|
|
7050
7523
|
*/
|
|
7051
7524
|
async generateNextId(options) {
|
|
7052
7525
|
const { table, initialId = 1, incrementIdBy = 1 } = options;
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
if: { $or: [{ $eq: ["$id", null] }, { $not: "$id" }] },
|
|
7056
|
-
then: initialId,
|
|
7057
|
-
else: { $add: ["$id", incrementIdBy] }
|
|
7058
|
-
} },
|
|
7059
|
-
collection: table
|
|
7060
|
-
} }], {
|
|
7061
|
-
upsert: true,
|
|
7062
|
-
returnDocument: "after"
|
|
7063
|
-
}))?.id ?? initialId;
|
|
7526
|
+
await this.ensureIndexes();
|
|
7527
|
+
return await this.reserveBlock(table, initialId, incrementIdBy, 1);
|
|
7064
7528
|
}
|
|
7065
7529
|
/**
|
|
7066
|
-
*
|
|
7530
|
+
* Reserve a contiguous block of `count` ids in a single atomic operation.
|
|
7067
7531
|
*
|
|
7068
|
-
*
|
|
7069
|
-
*
|
|
7532
|
+
* Advances the counter by `count * incrementIdBy` in one `findOneAndUpdate`
|
|
7533
|
+
* (so the stored last id becomes the block's last id) and returns the block
|
|
7534
|
+
* in ascending order. See the class doc for the non-transactional contract.
|
|
7535
|
+
*
|
|
7536
|
+
* @param options - `GenerateIdOptions` plus the block `count`
|
|
7537
|
+
* @returns The reserved ids in ascending order (length `count`)
|
|
7070
7538
|
*
|
|
7071
7539
|
* @example
|
|
7072
7540
|
* ```typescript
|
|
7073
|
-
* const
|
|
7074
|
-
*
|
|
7541
|
+
* const ids = await idGenerator.generateNextIds({ table: "users", count: 100 });
|
|
7542
|
+
* // ids[0] is the first id, ids[99] the last; getLastId("users") === ids[99]
|
|
7075
7543
|
* ```
|
|
7076
7544
|
*/
|
|
7545
|
+
async generateNextIds(options) {
|
|
7546
|
+
const { table, initialId = 1, incrementIdBy = 1, count } = options;
|
|
7547
|
+
if (count <= 0) return [];
|
|
7548
|
+
await this.ensureIndexes();
|
|
7549
|
+
const firstId = await this.reserveBlock(table, initialId, incrementIdBy, count) - (count - 1) * incrementIdBy;
|
|
7550
|
+
return Array.from({ length: count }, (_, index) => firstId + index * incrementIdBy);
|
|
7551
|
+
}
|
|
7552
|
+
/**
|
|
7553
|
+
* Atomically advance the counter for `table` by a whole block and return the
|
|
7554
|
+
* block's LAST id.
|
|
7555
|
+
*
|
|
7556
|
+
* One `findOneAndUpdate` with an aggregation pipeline:
|
|
7557
|
+
* - **Cold start** (counter field missing or null): seed `initialId + (count - 1) * incrementIdBy`
|
|
7558
|
+
* so the FIRST id of the block equals `initialId` (the `count - 1` here vs
|
|
7559
|
+
* `count` in the steady-state branch is deliberate — the counter stores the
|
|
7560
|
+
* last-issued id and the very first id must be exactly `initialId`).
|
|
7561
|
+
* - **Steady state**: add `count * incrementIdBy` to the stored counter.
|
|
7562
|
+
*
|
|
7563
|
+
* Retries on a duplicate-key error from the cold-start upsert race (see
|
|
7564
|
+
* {@link isDuplicateKeyError}).
|
|
7565
|
+
*
|
|
7566
|
+
* @param table - The table/collection the block is for
|
|
7567
|
+
* @param initialId - The first id ever issued for this table
|
|
7568
|
+
* @param incrementIdBy - The fixed stride between ids
|
|
7569
|
+
* @param count - Block size (`>= 1`)
|
|
7570
|
+
* @returns The last id of the reserved block
|
|
7571
|
+
*/
|
|
7572
|
+
async reserveBlock(table, initialId, incrementIdBy, count) {
|
|
7573
|
+
const run = async () => {
|
|
7574
|
+
const lastId = (await this.driver.getDatabase().collection(this.counterCollection).findOneAndUpdate({ collection: table }, [{ $set: {
|
|
7575
|
+
id: { $cond: {
|
|
7576
|
+
if: { $or: [{ $eq: [{ $type: "$id" }, "missing"] }, { $eq: ["$id", null] }] },
|
|
7577
|
+
then: initialId + (count - 1) * incrementIdBy,
|
|
7578
|
+
else: { $add: ["$id", count * incrementIdBy] }
|
|
7579
|
+
} },
|
|
7580
|
+
collection: table
|
|
7581
|
+
} }], {
|
|
7582
|
+
upsert: true,
|
|
7583
|
+
returnDocument: "after"
|
|
7584
|
+
}))?.id;
|
|
7585
|
+
if (typeof lastId !== "number") throw new Error(`Failed to reserve an id block for "${table}": counter returned no id.`);
|
|
7586
|
+
return lastId;
|
|
7587
|
+
};
|
|
7588
|
+
return await this.withDuplicateKeyRetry(run);
|
|
7589
|
+
}
|
|
7590
|
+
/**
|
|
7591
|
+
* Run `operation`, retrying on a duplicate-key (E11000) error up to
|
|
7592
|
+
* {@link MAX_RESERVE_ATTEMPTS} times. Only the cold-start upsert race throws
|
|
7593
|
+
* E11000 (once the unique index exists); on retry the counter document
|
|
7594
|
+
* already exists, so the increment branch runs and succeeds. Any other error
|
|
7595
|
+
* is rethrown immediately.
|
|
7596
|
+
*
|
|
7597
|
+
* @param operation - The reservation to run
|
|
7598
|
+
* @returns The operation's result
|
|
7599
|
+
*/
|
|
7600
|
+
async withDuplicateKeyRetry(operation) {
|
|
7601
|
+
let lastError;
|
|
7602
|
+
for (let attempt = 1; attempt <= MAX_RESERVE_ATTEMPTS; attempt++) try {
|
|
7603
|
+
return await operation();
|
|
7604
|
+
} catch (error) {
|
|
7605
|
+
if (!isDuplicateKeyError(error)) throw error;
|
|
7606
|
+
lastError = error;
|
|
7607
|
+
}
|
|
7608
|
+
throw lastError;
|
|
7609
|
+
}
|
|
7610
|
+
/**
|
|
7611
|
+
* Ensure the unique index on `{ collection: 1 }` exists (once per instance).
|
|
7612
|
+
*
|
|
7613
|
+
* Best-effort: a pre-existing duplicate counter document (created before this
|
|
7614
|
+
* index existed) makes index creation fail with E11000. We swallow that
|
|
7615
|
+
* rather than block app boot — without the index the generator degrades to
|
|
7616
|
+
* its prior (un-indexed) behavior, and the retry path becomes a no-op since
|
|
7617
|
+
* E11000 can no longer be raised by the upsert.
|
|
7618
|
+
*/
|
|
7619
|
+
async ensureIndexes() {
|
|
7620
|
+
if (!this.indexEnsured) this.indexEnsured = this.createCounterIndex();
|
|
7621
|
+
return await this.indexEnsured;
|
|
7622
|
+
}
|
|
7623
|
+
/**
|
|
7624
|
+
* Create the unique index on the counter collection's `collection` field.
|
|
7625
|
+
* Failures are swallowed (see {@link ensureIndexes}).
|
|
7626
|
+
*/
|
|
7627
|
+
async createCounterIndex() {
|
|
7628
|
+
try {
|
|
7629
|
+
await this.driver.getDatabase().collection(this.counterCollection).createIndex({ collection: 1 }, {
|
|
7630
|
+
unique: true,
|
|
7631
|
+
name: "collection_unique"
|
|
7632
|
+
});
|
|
7633
|
+
} catch {}
|
|
7634
|
+
}
|
|
7635
|
+
/**
|
|
7636
|
+
* Get the last generated ID for a table.
|
|
7637
|
+
*
|
|
7638
|
+
* @param table - The table/collection name
|
|
7639
|
+
* @returns The last generated ID, or 0 if none exists
|
|
7640
|
+
*/
|
|
7077
7641
|
async getLastId(table) {
|
|
7078
7642
|
return (await this.driver.queryBuilder(this.counterCollection).where("collection", table).first())?.id ?? 0;
|
|
7079
7643
|
}
|
|
7080
7644
|
/**
|
|
7081
7645
|
* Set the last ID for a table.
|
|
7082
7646
|
*
|
|
7083
|
-
* Creates or updates the counter document for the specified table.
|
|
7084
|
-
*
|
|
7647
|
+
* Creates or updates the counter document for the specified table. Useful for
|
|
7648
|
+
* seeding or resetting ID sequences.
|
|
7085
7649
|
*
|
|
7086
7650
|
* @param table - The table/collection name
|
|
7087
7651
|
* @param id - The ID to set as the last generated ID
|
|
7088
7652
|
*
|
|
7089
7653
|
* @example
|
|
7090
7654
|
* ```typescript
|
|
7091
|
-
* // Reset user IDs to start from 1000
|
|
7655
|
+
* // Reset user IDs to start from 1000 (next generated id is 1001)
|
|
7092
7656
|
* await idGenerator.setLastId("users", 1000);
|
|
7093
7657
|
* ```
|
|
7094
7658
|
*/
|
|
@@ -9024,6 +9588,13 @@ var MongoQueryParser = class {
|
|
|
9024
9588
|
*/
|
|
9025
9589
|
groupFieldNames = /* @__PURE__ */ new Map();
|
|
9026
9590
|
/**
|
|
9591
|
+
* Track `countDistinct` aggregate aliases per group stage. The renaming
|
|
9592
|
+
* `$project` finalizes these with `{ $size: "$alias" }` over the set built
|
|
9593
|
+
* by `$addToSet` in the `$group` stage.
|
|
9594
|
+
* Maps pipeline index to the set of aliases needing `$size` finalization.
|
|
9595
|
+
*/
|
|
9596
|
+
countDistinctAliases = /* @__PURE__ */ new Map();
|
|
9597
|
+
/**
|
|
9027
9598
|
* Create a new MongoDB query parser.
|
|
9028
9599
|
*
|
|
9029
9600
|
* @param options - Configuration options for the parser
|
|
@@ -9100,10 +9671,24 @@ var MongoQueryParser = class {
|
|
|
9100
9671
|
if (op.type === "groupByWithAggregates" && op.data.fields) {
|
|
9101
9672
|
const fieldNames = this.extractGroupFieldNames(op.data.fields);
|
|
9102
9673
|
if (fieldNames) this.groupFieldNames.set(stageIndex, fieldNames);
|
|
9674
|
+
this.trackCountDistinctAliases(stageIndex, op.data.aggregates);
|
|
9675
|
+
} else if (op.type === "groupByDate") {
|
|
9676
|
+
this.groupFieldNames.set(stageIndex, op.data.column);
|
|
9677
|
+
this.trackCountDistinctAliases(stageIndex, op.data.aggregates ?? {});
|
|
9103
9678
|
}
|
|
9104
9679
|
}
|
|
9105
9680
|
}
|
|
9106
9681
|
/**
|
|
9682
|
+
* Record which aggregate aliases in a group stage are `countDistinct` so the
|
|
9683
|
+
* renaming `$project` can finalize them with `{ $size: "$alias" }` over the
|
|
9684
|
+
* set built by `$addToSet` (the standard distinct-count-per-group pattern).
|
|
9685
|
+
*/
|
|
9686
|
+
trackCountDistinctAliases(stageIndex, aggregates) {
|
|
9687
|
+
const distinctAliases = /* @__PURE__ */ new Set();
|
|
9688
|
+
for (const [alias, expression] of Object.entries(aggregates)) if (isAggregateExpression(expression) && expression.__agg === "countDistinct") distinctAliases.add(alias);
|
|
9689
|
+
if (distinctAliases.size > 0) this.countDistinctAliases.set(stageIndex, distinctAliases);
|
|
9690
|
+
}
|
|
9691
|
+
/**
|
|
9107
9692
|
* Post-process pipeline to rename _id fields after $group stages.
|
|
9108
9693
|
*
|
|
9109
9694
|
* This automatically renames MongoDB's `_id` field to the actual field name(s)
|
|
@@ -9122,8 +9707,10 @@ var MongoQueryParser = class {
|
|
|
9122
9707
|
const projection = {};
|
|
9123
9708
|
if (typeof fieldNames === "string") projection[fieldNames] = "$_id";
|
|
9124
9709
|
else if (Array.isArray(fieldNames) && fieldNames.length > 0) for (const fieldName of fieldNames) projection[fieldName] = `$_id.${fieldName}`;
|
|
9710
|
+
const distinctAliases = this.countDistinctAliases.get(i);
|
|
9125
9711
|
const aggregateFields = Object.keys(stage.$group).filter((key) => key !== "_id");
|
|
9126
|
-
for (const field of aggregateFields) projection[field] =
|
|
9712
|
+
for (const field of aggregateFields) if (distinctAliases?.has(field)) projection[field] = { $size: `$${field}` };
|
|
9713
|
+
else projection[field] = 1;
|
|
9127
9714
|
if (Object.keys(projection).length > 0) {
|
|
9128
9715
|
projection._id = 0;
|
|
9129
9716
|
processed.push({ $project: projection });
|
|
@@ -9827,6 +10414,11 @@ var MongoQueryParser = class {
|
|
|
9827
10414
|
if (stage) return stage;
|
|
9828
10415
|
break;
|
|
9829
10416
|
}
|
|
10417
|
+
case "groupByDate": {
|
|
10418
|
+
const stage = this.buildGroupByDateStage(op.data.column, op.data.unit, op.data.aggregates ?? {});
|
|
10419
|
+
if (stage) return stage;
|
|
10420
|
+
break;
|
|
10421
|
+
}
|
|
9830
10422
|
case "groupByRaw": {
|
|
9831
10423
|
const expression = op.data.expression;
|
|
9832
10424
|
if (expression && typeof expression === "object") return { $group: expression };
|
|
@@ -9863,6 +10455,24 @@ var MongoQueryParser = class {
|
|
|
9863
10455
|
return { $group: groupStage };
|
|
9864
10456
|
}
|
|
9865
10457
|
/**
|
|
10458
|
+
* Build a `$group` stage that buckets documents by a `$dateTrunc` of a date
|
|
10459
|
+
* field, optionally running aggregates over each bucket.
|
|
10460
|
+
*
|
|
10461
|
+
* @param column - The date field to bucket
|
|
10462
|
+
* @param unit - The bucket granularity
|
|
10463
|
+
* @param aggregates - Aggregate operations (abstract or raw)
|
|
10464
|
+
* @returns The `$group` stage
|
|
10465
|
+
*/
|
|
10466
|
+
buildGroupByDateStage(column, unit, aggregates) {
|
|
10467
|
+
const groupStage = { _id: { $dateTrunc: {
|
|
10468
|
+
date: `$${column}`,
|
|
10469
|
+
unit
|
|
10470
|
+
} } };
|
|
10471
|
+
for (const [alias, expression] of Object.entries(aggregates)) if (isAggregateExpression(expression)) groupStage[alias] = this.translateAggregateExpression(expression);
|
|
10472
|
+
else groupStage[alias] = expression;
|
|
10473
|
+
return { $group: groupStage };
|
|
10474
|
+
}
|
|
10475
|
+
/**
|
|
9866
10476
|
* Extract field names from GroupByInput for renaming _id.
|
|
9867
10477
|
*
|
|
9868
10478
|
* @param fields - The grouping fields
|
|
@@ -9886,7 +10496,11 @@ var MongoQueryParser = class {
|
|
|
9886
10496
|
translateAggregateExpression(expr) {
|
|
9887
10497
|
switch (expr.__agg) {
|
|
9888
10498
|
case "count": return { $sum: 1 };
|
|
10499
|
+
case "countDistinct":
|
|
10500
|
+
if (!expr.__field) throw new Error("Count distinct aggregate requires a field name");
|
|
10501
|
+
return { $addToSet: `$${expr.__field}` };
|
|
9889
10502
|
case "sum":
|
|
10503
|
+
if (expr.__expr) return { $sum: this.columnExpressionToMongo(expr.__expr) };
|
|
9890
10504
|
if (!expr.__field) throw new Error("Sum aggregate requires a field name");
|
|
9891
10505
|
return { $sum: `$${expr.__field}` };
|
|
9892
10506
|
case "avg":
|
|
@@ -9913,6 +10527,35 @@ var MongoQueryParser = class {
|
|
|
9913
10527
|
default: throw new Error(`Unknown aggregate function: ${expr.__agg}`);
|
|
9914
10528
|
}
|
|
9915
10529
|
}
|
|
10530
|
+
/**
|
|
10531
|
+
* Compile a typed {@link ColumnExpression} tree into a MongoDB aggregation
|
|
10532
|
+
* expression.
|
|
10533
|
+
*
|
|
10534
|
+
* Column references become `$field` paths; literals are emitted verbatim;
|
|
10535
|
+
* arithmetic ops map to `$add` / `$subtract` / `$multiply` / `$divide`. The
|
|
10536
|
+
* `raw` node is rejected — a raw SQL fragment is not portable to a MongoDB
|
|
10537
|
+
* pipeline, so callers must use the typed combinators (or `groupByRaw`) here.
|
|
10538
|
+
*
|
|
10539
|
+
* @param expression - The expression tree to compile
|
|
10540
|
+
* @returns A MongoDB aggregation expression (e.g. `{ $multiply: ["$price", "$quantity"] }`)
|
|
10541
|
+
*/
|
|
10542
|
+
columnExpressionToMongo(expression) {
|
|
10543
|
+
switch (expression.__expr) {
|
|
10544
|
+
case "column": return `$${expression.column}`;
|
|
10545
|
+
case "literal": return expression.value;
|
|
10546
|
+
case "raw": throw new Error("$agg.sumRaw / $expr.raw is not portable to a MongoDB pipeline — a raw SQL fragment has no MongoDB equivalent. Use the typed $expr combinators ($expr.mul / $expr.add / $expr.sub / $expr.div / $expr.col / $expr.lit) or groupByRaw instead.");
|
|
10547
|
+
case "add":
|
|
10548
|
+
case "subtract":
|
|
10549
|
+
case "multiply":
|
|
10550
|
+
case "divide": return { [{
|
|
10551
|
+
add: "$add",
|
|
10552
|
+
subtract: "$subtract",
|
|
10553
|
+
multiply: "$multiply",
|
|
10554
|
+
divide: "$divide"
|
|
10555
|
+
}[expression.__expr]]: expression.operands.map((operand) => this.columnExpressionToMongo(operand)) };
|
|
10556
|
+
default: throw new Error(`Unsupported column expression node: ${JSON.stringify(expression)}`);
|
|
10557
|
+
}
|
|
10558
|
+
}
|
|
9916
10559
|
buildGroupId(fields) {
|
|
9917
10560
|
if (!fields) return null;
|
|
9918
10561
|
if (typeof fields === "string") return `$${fields}`;
|
|
@@ -11055,6 +11698,38 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
|
|
|
11055
11698
|
return this;
|
|
11056
11699
|
}
|
|
11057
11700
|
/**
|
|
11701
|
+
* Portable date-bucketed grouping.
|
|
11702
|
+
*
|
|
11703
|
+
* Groups documents by `column` truncated to the given bucket
|
|
11704
|
+
* (`day` / `week` / `month` / `year`) via MongoDB's `$dateTrunc`, and runs
|
|
11705
|
+
* the optional aggregates over each bucket. The bucket is exposed under the
|
|
11706
|
+
* column's own name in the output (the `_id` is renamed in a follow-up
|
|
11707
|
+
* `$project`, the same convention as `groupBy`).
|
|
11708
|
+
*
|
|
11709
|
+
* @param column - The date field to bucket
|
|
11710
|
+
* @param unit - The bucket granularity
|
|
11711
|
+
* @param aggregates - Optional aggregate operations keyed by output alias
|
|
11712
|
+
*
|
|
11713
|
+
* @example
|
|
11714
|
+
* ```typescript
|
|
11715
|
+
* import { $agg, $expr } from "@warlock.js/cascade";
|
|
11716
|
+
*
|
|
11717
|
+
* query.groupByDate("created_at", "month", {
|
|
11718
|
+
* revenue: $agg.sum($expr.mul("price", "quantity")),
|
|
11719
|
+
* });
|
|
11720
|
+
* // $group: { _id: { $dateTrunc: { date: "$created_at", unit: "month" } },
|
|
11721
|
+
* // revenue: { $sum: { $multiply: ["$price", "$quantity"] } } }
|
|
11722
|
+
* ```
|
|
11723
|
+
*/
|
|
11724
|
+
groupByDate(column, unit, aggregates) {
|
|
11725
|
+
this.operationsHelper.addGroupOperation("groupByDate", {
|
|
11726
|
+
column,
|
|
11727
|
+
unit,
|
|
11728
|
+
aggregates: aggregates ?? {}
|
|
11729
|
+
}, false);
|
|
11730
|
+
return this;
|
|
11731
|
+
}
|
|
11732
|
+
/**
|
|
11058
11733
|
* Groups documents using a raw MongoDB expression.
|
|
11059
11734
|
* @param expression - The raw grouping expression
|
|
11060
11735
|
* @param bindings - Optional parameter bindings
|
|
@@ -12360,7 +13035,10 @@ var MongoDbDriver = class {
|
|
|
12360
13035
|
* @throws {Error} If transaction fails, is explicitly rolled back, or replica set not configured
|
|
12361
13036
|
*/
|
|
12362
13037
|
async transaction(fn, options) {
|
|
12363
|
-
|
|
13038
|
+
const ctx = { rollback(reason) {
|
|
13039
|
+
throw new TransactionRollbackError(reason);
|
|
13040
|
+
} };
|
|
13041
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
12364
13042
|
await this.ensureReplicaSetAvailable();
|
|
12365
13043
|
const session = this.getClientInstance().startSession();
|
|
12366
13044
|
try {
|
|
@@ -12370,9 +13048,7 @@ var MongoDbDriver = class {
|
|
|
12370
13048
|
});
|
|
12371
13049
|
databaseTransactionContext.enter({ session });
|
|
12372
13050
|
try {
|
|
12373
|
-
const result = await fn(
|
|
12374
|
-
throw new TransactionRollbackError(reason);
|
|
12375
|
-
} });
|
|
13051
|
+
const result = await fn(ctx);
|
|
12376
13052
|
await session.commitTransaction();
|
|
12377
13053
|
return result;
|
|
12378
13054
|
} catch (error) {
|
|
@@ -12897,18 +13573,24 @@ var PostgresDialect = class {
|
|
|
12897
13573
|
/**
|
|
12898
13574
|
* Translate a database-agnostic aggregate expression to PostgreSQL SQL.
|
|
12899
13575
|
*
|
|
12900
|
-
* The
|
|
12901
|
-
* `floor`, `first` and `last` are
|
|
12902
|
-
* single-scalar `GROUP BY` equivalent on
|
|
12903
|
-
* of emitting a silently-different semantic
|
|
13576
|
+
* The scalar aggregates map to their ANSI SQL function, and `countDistinct`
|
|
13577
|
+
* maps to `COUNT(DISTINCT …)`. `distinct`, `floor`, `first` and `last` are
|
|
13578
|
+
* MongoDB-only for v1 — none has a single-scalar `GROUP BY` equivalent on
|
|
13579
|
+
* PostgreSQL, so they throw instead of emitting a silently-different semantic
|
|
13580
|
+
* (the footgun this guards).
|
|
12904
13581
|
*
|
|
12905
13582
|
* @param expression - The abstract aggregate (`$agg.*`) to translate
|
|
12906
13583
|
* @returns The SQL fragment (e.g. `SUM("amount")`, `COUNT(*)`)
|
|
12907
13584
|
*/
|
|
12908
13585
|
aggregateToSql(expression) {
|
|
13586
|
+
if (expression.__expr) {
|
|
13587
|
+
if (expression.__agg !== "sum") throw new Error(`$agg.${expression.__agg} does not support a composed column expression yet — only $agg.sum(...) does. Use a bare column name here.`);
|
|
13588
|
+
return `SUM(${this.columnExpressionToSql(expression.__expr)})`;
|
|
13589
|
+
}
|
|
12909
13590
|
const column = expression.__field === null ? "*" : this.quoteIdentifier(expression.__field);
|
|
12910
13591
|
switch (expression.__agg) {
|
|
12911
13592
|
case "count": return "COUNT(*)";
|
|
13593
|
+
case "countDistinct": return `COUNT(DISTINCT ${column})`;
|
|
12912
13594
|
case "sum": return `SUM(${column})`;
|
|
12913
13595
|
case "avg": return `AVG(${column})`;
|
|
12914
13596
|
case "min": return `MIN(${column})`;
|
|
@@ -12916,6 +13598,53 @@ var PostgresDialect = class {
|
|
|
12916
13598
|
default: throw new Error(`$agg.${expression.__agg} is MongoDB-only and not supported on a PostgreSQL groupBy. Use selectRaw / havingRaw with the equivalent SQL (window function / DISTINCT / FLOOR) if you need it here.`);
|
|
12917
13599
|
}
|
|
12918
13600
|
}
|
|
13601
|
+
/**
|
|
13602
|
+
* Compile a typed {@link ColumnExpression} tree into a SQL fragment.
|
|
13603
|
+
*
|
|
13604
|
+
* Column references flow through {@link quoteIdentifier} (so user-supplied
|
|
13605
|
+
* names are quoted/escaped, never raw-interpolated); literals are emitted as
|
|
13606
|
+
* numeric/boolean SQL constants; arithmetic ops are parenthesised. The `raw`
|
|
13607
|
+
* node is the only path that emits a string verbatim — it is opt-in by name.
|
|
13608
|
+
*
|
|
13609
|
+
* @param expression - The expression tree to compile
|
|
13610
|
+
* @returns A SQL fragment (e.g. `("price" * "quantity")`)
|
|
13611
|
+
*/
|
|
13612
|
+
columnExpressionToSql(expression) {
|
|
13613
|
+
switch (expression.__expr) {
|
|
13614
|
+
case "column": return this.quoteIdentifier(expression.column);
|
|
13615
|
+
case "literal": return typeof expression.value === "boolean" ? this.booleanLiteral(expression.value) : String(expression.value);
|
|
13616
|
+
case "raw": return expression.expression;
|
|
13617
|
+
case "add":
|
|
13618
|
+
case "subtract":
|
|
13619
|
+
case "multiply":
|
|
13620
|
+
case "divide": {
|
|
13621
|
+
const operator = {
|
|
13622
|
+
add: "+",
|
|
13623
|
+
subtract: "-",
|
|
13624
|
+
multiply: "*",
|
|
13625
|
+
divide: "/"
|
|
13626
|
+
}[expression.__expr];
|
|
13627
|
+
return `(${expression.operands.map((operand) => this.columnExpressionToSql(operand)).join(` ${operator} `)})`;
|
|
13628
|
+
}
|
|
13629
|
+
default: throw new Error(`Unsupported column expression node: ${JSON.stringify(expression)}`);
|
|
13630
|
+
}
|
|
13631
|
+
}
|
|
13632
|
+
/**
|
|
13633
|
+
* Build a `date_trunc` bucket expression for a portable `groupByDate`.
|
|
13634
|
+
*
|
|
13635
|
+
* @param column - The date/timestamp column to truncate
|
|
13636
|
+
* @param unit - The bucket granularity
|
|
13637
|
+
* @returns The SQL fragment (e.g. `date_trunc('month', "created_at")`)
|
|
13638
|
+
*
|
|
13639
|
+
* @example
|
|
13640
|
+
* ```typescript
|
|
13641
|
+
* dialect.dateTruncSql("created_at", "month");
|
|
13642
|
+
* // date_trunc('month', "created_at")
|
|
13643
|
+
* ```
|
|
13644
|
+
*/
|
|
13645
|
+
dateTruncSql(column, unit) {
|
|
13646
|
+
return `date_trunc('${unit}', ${this.quoteIdentifier(column)})`;
|
|
13647
|
+
}
|
|
12919
13648
|
};
|
|
12920
13649
|
|
|
12921
13650
|
//#endregion
|
|
@@ -13764,10 +14493,16 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13764
14493
|
*/
|
|
13765
14494
|
orderClauses = [];
|
|
13766
14495
|
/**
|
|
13767
|
-
* GROUP BY columns.
|
|
14496
|
+
* GROUP BY columns (bare identifiers — get quoted).
|
|
13768
14497
|
*/
|
|
13769
14498
|
groupColumns = [];
|
|
13770
14499
|
/**
|
|
14500
|
+
* Raw GROUP BY expressions (emitted verbatim — e.g. a `date_trunc(...)`
|
|
14501
|
+
* bucket). Kept separate from `groupColumns` so they bypass identifier
|
|
14502
|
+
* quoting while still landing in the same `GROUP BY` clause.
|
|
14503
|
+
*/
|
|
14504
|
+
groupRawExpressions = [];
|
|
14505
|
+
/**
|
|
13771
14506
|
* HAVING clauses.
|
|
13772
14507
|
*/
|
|
13773
14508
|
havingClauses = [];
|
|
@@ -13938,6 +14673,9 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13938
14673
|
case "groupBy":
|
|
13939
14674
|
this.processGroupBy(data);
|
|
13940
14675
|
break;
|
|
14676
|
+
case "groupByRaw":
|
|
14677
|
+
this.processGroupByRaw(data);
|
|
14678
|
+
break;
|
|
13941
14679
|
case "having":
|
|
13942
14680
|
this.processHaving(data);
|
|
13943
14681
|
break;
|
|
@@ -13972,9 +14710,9 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13972
14710
|
parts.push(fromClause);
|
|
13973
14711
|
if (this.joinClauses.length > 0) parts.push(this.joinClauses.join(" "));
|
|
13974
14712
|
if (this.whereClauses.length > 0) parts.push(`WHERE ${this.whereClauses.join(" ")}`);
|
|
13975
|
-
if (this.groupColumns.length > 0) {
|
|
13976
|
-
const
|
|
13977
|
-
parts.push(`GROUP BY ${
|
|
14713
|
+
if (this.groupColumns.length > 0 || this.groupRawExpressions.length > 0) {
|
|
14714
|
+
const allGroupTerms = [...this.groupColumns.map((c) => this.dialect.quoteIdentifier(c)), ...this.groupRawExpressions];
|
|
14715
|
+
parts.push(`GROUP BY ${allGroupTerms.join(", ")}`);
|
|
13978
14716
|
}
|
|
13979
14717
|
if (this.havingClauses.length > 0) parts.push(`HAVING ${this.havingClauses.join(" AND ")}`);
|
|
13980
14718
|
if (this.orderClauses.length > 0) parts.push(`ORDER BY ${this.orderClauses.join(", ")}`);
|
|
@@ -14432,6 +15170,14 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14432
15170
|
this.groupColumns.push(...columns);
|
|
14433
15171
|
}
|
|
14434
15172
|
/**
|
|
15173
|
+
* Process a raw GROUP BY expression (emitted verbatim, no quoting). Used by
|
|
15174
|
+
* `groupByDate` to add a `date_trunc(...)` bucket to the GROUP BY clause.
|
|
15175
|
+
*/
|
|
15176
|
+
processGroupByRaw(data) {
|
|
15177
|
+
const expression = data.expression;
|
|
15178
|
+
this.groupRawExpressions.push(expression);
|
|
15179
|
+
}
|
|
15180
|
+
/**
|
|
14435
15181
|
* Process HAVING operation.
|
|
14436
15182
|
*/
|
|
14437
15183
|
processHaving(data) {
|
|
@@ -14612,6 +15358,53 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
14612
15358
|
if (!aggregates) return super.groupBy(fields);
|
|
14613
15359
|
const fieldList = Array.isArray(fields) ? fields : [fields];
|
|
14614
15360
|
this.addOperation("select", { fields: fieldList });
|
|
15361
|
+
this.projectGroupAggregates(aggregates);
|
|
15362
|
+
return super.groupBy(fields);
|
|
15363
|
+
}
|
|
15364
|
+
/**
|
|
15365
|
+
* Portable date-bucketed GROUP BY.
|
|
15366
|
+
*
|
|
15367
|
+
* Truncates `column` to the given bucket (`day` / `week` / `month` / `year`)
|
|
15368
|
+
* via PostgreSQL's `date_trunc`, projects the bucket under the column's own
|
|
15369
|
+
* name, and groups by the truncated expression. Optional `aggregates` follow
|
|
15370
|
+
* the same rules as the two-arg `groupBy` (`$agg.*` helpers or raw SQL
|
|
15371
|
+
* strings; MongoDB operator objects and MongoDB-only aggregates throw).
|
|
15372
|
+
*
|
|
15373
|
+
* Emits (for `groupByDate("created_at", "month", { revenue: $agg.sum(...) })`):
|
|
15374
|
+
* `SELECT date_trunc('month', "created_at") AS "created_at", SUM(...) AS "revenue"
|
|
15375
|
+
* FROM ... GROUP BY date_trunc('month', "created_at")`
|
|
15376
|
+
*
|
|
15377
|
+
* @param column - The date/timestamp column to bucket
|
|
15378
|
+
* @param unit - The bucket granularity
|
|
15379
|
+
* @param aggregates - Optional aggregate projections keyed by output alias
|
|
15380
|
+
*
|
|
15381
|
+
* @example
|
|
15382
|
+
* Order.query().groupByDate("created_at", "month", {
|
|
15383
|
+
* revenue: $agg.sum($expr.mul("price", "quantity")),
|
|
15384
|
+
* });
|
|
15385
|
+
*/
|
|
15386
|
+
groupByDate(column, unit, aggregates) {
|
|
15387
|
+
const bucketSql = this.driver.dialect.dateTruncSql(column, unit);
|
|
15388
|
+
this.addOperation("selectRaw", {
|
|
15389
|
+
expression: `${bucketSql} AS ${this.driver.dialect.quoteIdentifier(column)}`,
|
|
15390
|
+
bindings: []
|
|
15391
|
+
});
|
|
15392
|
+
if (aggregates) this.projectGroupAggregates(aggregates);
|
|
15393
|
+
this.addOperation("groupByRaw", {
|
|
15394
|
+
expression: bucketSql,
|
|
15395
|
+
bindings: []
|
|
15396
|
+
});
|
|
15397
|
+
return this;
|
|
15398
|
+
}
|
|
15399
|
+
/**
|
|
15400
|
+
* Translate a `{ alias: aggregate }` map into `selectRaw` projections and
|
|
15401
|
+
* record each alias → SQL so `applyGroupByAggregates` can later rewrite a
|
|
15402
|
+
* `having()` on the alias. Shared by `groupBy` and `groupByDate`.
|
|
15403
|
+
*
|
|
15404
|
+
* `$agg.distinct/floor/first/last` throw (MongoDB-only on Postgres v1);
|
|
15405
|
+
* MongoDB operator objects throw (not portable to SQL).
|
|
15406
|
+
*/
|
|
15407
|
+
projectGroupAggregates(aggregates) {
|
|
14615
15408
|
for (const [alias, expression] of Object.entries(aggregates)) {
|
|
14616
15409
|
let sql;
|
|
14617
15410
|
if (isAggregateExpression(expression)) sql = this.driver.dialect.aggregateToSql(expression);
|
|
@@ -14623,7 +15416,6 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
14623
15416
|
bindings: []
|
|
14624
15417
|
});
|
|
14625
15418
|
}
|
|
14626
|
-
return super.groupBy(fields);
|
|
14627
15419
|
}
|
|
14628
15420
|
/**
|
|
14629
15421
|
* Nearest-neighbour vector similarity search via pgvector cosine distance.
|
|
@@ -16134,12 +16926,29 @@ var PostgresDriver = class {
|
|
|
16134
16926
|
*/
|
|
16135
16927
|
_syncAdapter;
|
|
16136
16928
|
/**
|
|
16929
|
+
* Explicit, table-agnostic override list of column names that hold native
|
|
16930
|
+
* PostgreSQL arrays (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text
|
|
16931
|
+
* encoded. Merged with (and superseded per-table by) the schema
|
|
16932
|
+
* introspection below; kept as a manual escape hatch.
|
|
16933
|
+
*
|
|
16934
|
+
* @see PostgresPoolConfig.nativeArrayColumns
|
|
16935
|
+
*/
|
|
16936
|
+
_nativeArrayColumns;
|
|
16937
|
+
/**
|
|
16938
|
+
* Native-array columns discovered by introspecting the live schema on
|
|
16939
|
+
* connect, keyed `table → { column, … }`. Authoritative and table-scoped, so
|
|
16940
|
+
* a column that is `TEXT[]` in one table and `jsonb` in another is encoded
|
|
16941
|
+
* correctly for each — no app configuration required.
|
|
16942
|
+
*/
|
|
16943
|
+
_introspectedArrayColumns = /* @__PURE__ */ new Map();
|
|
16944
|
+
/**
|
|
16137
16945
|
* Create a new PostgreSQL driver instance.
|
|
16138
16946
|
*
|
|
16139
16947
|
* @param config - PostgreSQL connection configuration
|
|
16140
16948
|
*/
|
|
16141
16949
|
constructor(config) {
|
|
16142
16950
|
this.config = config;
|
|
16951
|
+
this._nativeArrayColumns = new Set(config.nativeArrayColumns ?? []);
|
|
16143
16952
|
}
|
|
16144
16953
|
/**
|
|
16145
16954
|
* Get the connection pool instance.
|
|
@@ -16198,6 +17007,7 @@ var PostgresDriver = class {
|
|
|
16198
17007
|
(await this._pool.connect()).release();
|
|
16199
17008
|
_warlock_js_logger.log.success("database.postgres", "connection", `Connected to database ${_mongez_copper.colors.bold(_mongez_copper.colors.yellowBright(this.config.database))}`);
|
|
16200
17009
|
this._isConnected = true;
|
|
17010
|
+
await this.loadNativeArrayColumns();
|
|
16201
17011
|
this.emit("connected");
|
|
16202
17012
|
} catch (error) {
|
|
16203
17013
|
_warlock_js_logger.log.fatal("database.postgres", "connection", "Failed to connect to database");
|
|
@@ -16234,21 +17044,115 @@ var PostgresDriver = class {
|
|
|
16234
17044
|
* that need special handling for PostgreSQL storage.
|
|
16235
17045
|
*
|
|
16236
17046
|
* @param data - The data object to serialize
|
|
17047
|
+
* @param table - Optional table name; when given, columns introspected as
|
|
17048
|
+
* native arrays on that table are bound raw (see {@link serializeValue}).
|
|
16237
17049
|
* @returns Serialized data ready for PostgreSQL
|
|
16238
17050
|
*/
|
|
16239
|
-
serialize(data) {
|
|
17051
|
+
serialize(data, table) {
|
|
16240
17052
|
const serialized = {};
|
|
16241
17053
|
for (const [key, value] of Object.entries(data)) {
|
|
16242
17054
|
if (value === void 0) continue;
|
|
16243
|
-
|
|
16244
|
-
else if (typeof value === "bigint") serialized[key] = value.toString();
|
|
16245
|
-
else if (typeof value === "object" && value !== null && !Array.isArray(value)) serialized[key] = value;
|
|
16246
|
-
else if (Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === "number")) serialized[key] = `[${value.join(",")}]`;
|
|
16247
|
-
else serialized[key] = value;
|
|
17055
|
+
serialized[key] = this.serializeValue(key, value, table);
|
|
16248
17056
|
}
|
|
16249
17057
|
return serialized;
|
|
16250
17058
|
}
|
|
16251
17059
|
/**
|
|
17060
|
+
* Serialize a single column value into a node-pg bindable parameter.
|
|
17061
|
+
*
|
|
17062
|
+
* Shared by {@link serialize} (INSERT path) and {@link buildUpdateQuery}
|
|
17063
|
+
* `$set` (UPDATE path) so both encode `json` / `jsonb` columns identically.
|
|
17064
|
+
*
|
|
17065
|
+
* Encoding rules (in order):
|
|
17066
|
+
* - `Date` → ISO string.
|
|
17067
|
+
* - `bigint` → decimal string (node-pg has no native bigint binding).
|
|
17068
|
+
* - all-number array → pgvector literal `'[n1,n2,...]'`. node-pg would
|
|
17069
|
+
* otherwise emit a `{n1,n2,...}` array literal, which the `vector` type
|
|
17070
|
+
* rejects. This branch is preserved exactly.
|
|
17071
|
+
* - any other array (object-array, string-array, mixed, empty `[]`) →
|
|
17072
|
+
* `JSON.stringify`. node-pg renders a raw JS array as a PostgreSQL array
|
|
17073
|
+
* literal `{...}` (and `[]` as `{}`), which a `json` / `jsonb` column
|
|
17074
|
+
* rejects — so we bind the value as JSON text instead, the form those
|
|
17075
|
+
* columns accept. Columns known to be native arrays — via schema
|
|
17076
|
+
* introspection or the `nativeArrayColumns` config — are exempt: their raw
|
|
17077
|
+
* array is passed through so node-pg emits the `{...}` literal a genuine
|
|
17078
|
+
* `JSONB[]` / `TEXT[]` column needs.
|
|
17079
|
+
* - plain object → `JSON.stringify`. Equivalent to node-pg's own object
|
|
17080
|
+
* handling, made explicit so both write paths agree.
|
|
17081
|
+
* - everything else (scalars: string, number, boolean, null) → untouched.
|
|
17082
|
+
*
|
|
17083
|
+
* Distinguishing native-array from `json` / `jsonb` columns: a value alone
|
|
17084
|
+
* can't tell them apart, so the driver introspects the live schema on connect
|
|
17085
|
+
* (see {@link loadNativeArrayColumns}) and consults that per-table map here
|
|
17086
|
+
* via {@link isNativeArrayColumn}. The explicit `nativeArrayColumns` config
|
|
17087
|
+
* still works as a table-agnostic override. No `::jsonb` placeholder cast is
|
|
17088
|
+
* added: a JSON-text string binds correctly to `json` / `jsonb` without one,
|
|
17089
|
+
* and a blind cast would misfire on columns we cannot positively identify as
|
|
17090
|
+
* jsonb.
|
|
17091
|
+
*
|
|
17092
|
+
* @param key - Column name (used to resolve native-array columns)
|
|
17093
|
+
* @param value - The raw value to serialize (never `undefined`)
|
|
17094
|
+
* @param table - Optional table name; enables the per-table native-array lookup
|
|
17095
|
+
* @returns The value ready to bind as a query parameter
|
|
17096
|
+
*/
|
|
17097
|
+
serializeValue(key, value, table) {
|
|
17098
|
+
if (value instanceof Date) return value.toISOString();
|
|
17099
|
+
if (typeof value === "bigint") return value.toString();
|
|
17100
|
+
if (Array.isArray(value)) {
|
|
17101
|
+
if (value.length > 0 && value.every((v) => typeof v === "number")) return `[${value.join(",")}]`;
|
|
17102
|
+
if (this.isNativeArrayColumn(table, key)) return value;
|
|
17103
|
+
return JSON.stringify(value);
|
|
17104
|
+
}
|
|
17105
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
17106
|
+
return value;
|
|
17107
|
+
}
|
|
17108
|
+
/**
|
|
17109
|
+
* Whether `column` on `table` is a native PostgreSQL array. True when the
|
|
17110
|
+
* connect-time schema introspection saw it as `data_type = 'ARRAY'` for that
|
|
17111
|
+
* table (authoritative, per-table), or when it's listed in the table-agnostic
|
|
17112
|
+
* `nativeArrayColumns` config override.
|
|
17113
|
+
*/
|
|
17114
|
+
isNativeArrayColumn(table, column) {
|
|
17115
|
+
if (table && this._introspectedArrayColumns.get(table)?.has(column)) return true;
|
|
17116
|
+
return this._nativeArrayColumns.has(column);
|
|
17117
|
+
}
|
|
17118
|
+
/**
|
|
17119
|
+
* Introspect the live schema for native-array columns so array values bind
|
|
17120
|
+
* correctly with zero app configuration.
|
|
17121
|
+
*
|
|
17122
|
+
* A JS array must be bound two opposite ways depending on the column: as JSON
|
|
17123
|
+
* text for a `json` / `jsonb` column, but as a raw array (which node-pg
|
|
17124
|
+
* renders `{...}`) for a native `TEXT[]` / `JSONB[]` / `INTEGER[]` column. The
|
|
17125
|
+
* serializer sees values, not types, so without this it JSON-stringifies
|
|
17126
|
+
* every array — which a native-array column rejects with "malformed array
|
|
17127
|
+
* literal". One `information_schema` query at connect, cached for the
|
|
17128
|
+
* connection lifetime, removes the need to hand-list `nativeArrayColumns`.
|
|
17129
|
+
*
|
|
17130
|
+
* Best-effort: any failure (e.g. restricted catalog access) is logged and
|
|
17131
|
+
* leaves the map empty so the config override still applies — it never blocks
|
|
17132
|
+
* connect. A schema change made within a live connection isn't reflected
|
|
17133
|
+
* until the next connect.
|
|
17134
|
+
*/
|
|
17135
|
+
async loadNativeArrayColumns() {
|
|
17136
|
+
try {
|
|
17137
|
+
const result = await this.query(`SELECT table_name, column_name
|
|
17138
|
+
FROM information_schema.columns
|
|
17139
|
+
WHERE table_schema = ANY (current_schemas(false))
|
|
17140
|
+
AND data_type = 'ARRAY'`);
|
|
17141
|
+
const map = /* @__PURE__ */ new Map();
|
|
17142
|
+
for (const { table_name, column_name } of result.rows) {
|
|
17143
|
+
let columns = map.get(table_name);
|
|
17144
|
+
if (!columns) {
|
|
17145
|
+
columns = /* @__PURE__ */ new Set();
|
|
17146
|
+
map.set(table_name, columns);
|
|
17147
|
+
}
|
|
17148
|
+
columns.add(column_name);
|
|
17149
|
+
}
|
|
17150
|
+
this._introspectedArrayColumns = map;
|
|
17151
|
+
} catch {
|
|
17152
|
+
_warlock_js_logger.log.warn("database.postgres", "introspection", "Could not introspect native-array columns; using the nativeArrayColumns config only");
|
|
17153
|
+
}
|
|
17154
|
+
}
|
|
17155
|
+
/**
|
|
16252
17156
|
* Get the dirty tracker for this driver.
|
|
16253
17157
|
*/
|
|
16254
17158
|
getDirtyTracker(data) {
|
|
@@ -16297,7 +17201,7 @@ var PostgresDriver = class {
|
|
|
16297
17201
|
* @returns The inserted document
|
|
16298
17202
|
*/
|
|
16299
17203
|
async insert(table, document, _options) {
|
|
16300
|
-
const serialized = this.serialize(document);
|
|
17204
|
+
const serialized = this.serialize(document, table);
|
|
16301
17205
|
const filteredData = Object.fromEntries(Object.entries(serialized).filter(([key, value]) => {
|
|
16302
17206
|
if (key === "id" && (value === null || value === void 0)) return false;
|
|
16303
17207
|
return true;
|
|
@@ -16324,7 +17228,7 @@ var PostgresDriver = class {
|
|
|
16324
17228
|
if (documents.length === 0) return [];
|
|
16325
17229
|
const allColumns = /* @__PURE__ */ new Set();
|
|
16326
17230
|
for (const doc of documents) {
|
|
16327
|
-
const serialized = this.serialize(doc);
|
|
17231
|
+
const serialized = this.serialize(doc, table);
|
|
16328
17232
|
Object.keys(serialized).forEach((key) => allColumns.add(key));
|
|
16329
17233
|
}
|
|
16330
17234
|
const columns = Array.from(allColumns);
|
|
@@ -16334,7 +17238,7 @@ var PostgresDriver = class {
|
|
|
16334
17238
|
const params = [];
|
|
16335
17239
|
let paramIndex = 1;
|
|
16336
17240
|
for (const doc of documents) {
|
|
16337
|
-
const serialized = this.serialize(doc);
|
|
17241
|
+
const serialized = this.serialize(doc, table);
|
|
16338
17242
|
const rowPlaceholders = [];
|
|
16339
17243
|
for (const col of columns) if (col in serialized) {
|
|
16340
17244
|
rowPlaceholders.push(this.dialect.placeholder(paramIndex++));
|
|
@@ -16401,7 +17305,7 @@ var PostgresDriver = class {
|
|
|
16401
17305
|
* @returns The replaced document or null
|
|
16402
17306
|
*/
|
|
16403
17307
|
async replace(table, filter, document, _options) {
|
|
16404
|
-
const serialized = this.serialize(document);
|
|
17308
|
+
const serialized = this.serialize(document, table);
|
|
16405
17309
|
const columns = Object.keys(serialized);
|
|
16406
17310
|
const values = Object.values(serialized);
|
|
16407
17311
|
const quotedTable = this.dialect.quoteIdentifier(table);
|
|
@@ -16423,7 +17327,7 @@ var PostgresDriver = class {
|
|
|
16423
17327
|
* @returns The upserted row
|
|
16424
17328
|
*/
|
|
16425
17329
|
async upsert(table, filter, document, options) {
|
|
16426
|
-
const serialized = this.serialize(document);
|
|
17330
|
+
const serialized = this.serialize(document, table);
|
|
16427
17331
|
const columns = Object.keys(serialized);
|
|
16428
17332
|
const values = Object.values(serialized);
|
|
16429
17333
|
if (columns.length === 0) throw new Error("Cannot upsert empty document");
|
|
@@ -16549,13 +17453,14 @@ var PostgresDriver = class {
|
|
|
16549
17453
|
* @throws {Error} If transaction fails or is explicitly rolled back
|
|
16550
17454
|
*/
|
|
16551
17455
|
async transaction(fn, options) {
|
|
16552
|
-
|
|
17456
|
+
const ctx = { rollback(reason) {
|
|
17457
|
+
throw new TransactionRollbackError(reason);
|
|
17458
|
+
} };
|
|
17459
|
+
if (databaseTransactionContext.hasActiveTransaction()) return fn(ctx);
|
|
16553
17460
|
const tx = await this.beginTransaction(options);
|
|
16554
17461
|
databaseTransactionContext.enter({ session: tx.context });
|
|
16555
17462
|
try {
|
|
16556
|
-
const result = await fn(
|
|
16557
|
-
throw new TransactionRollbackError(reason);
|
|
16558
|
-
} });
|
|
17463
|
+
const result = await fn(ctx);
|
|
16559
17464
|
await tx.commit();
|
|
16560
17465
|
return result;
|
|
16561
17466
|
} catch (error) {
|
|
@@ -16717,7 +17622,7 @@ var PostgresDriver = class {
|
|
|
16717
17622
|
let paramIndex = 1;
|
|
16718
17623
|
if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
|
|
16719
17624
|
setClauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
16720
|
-
params.push(value);
|
|
17625
|
+
params.push(value === void 0 ? value : this.serializeValue(key, value, table));
|
|
16721
17626
|
}
|
|
16722
17627
|
if (update.$unset) for (const key of Object.keys(update.$unset)) setClauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
|
|
16723
17628
|
if (update.$inc) for (const [key, amount] of Object.entries(update.$inc)) {
|
|
@@ -21301,6 +22206,7 @@ async function listExecutedMigrations() {
|
|
|
21301
22206
|
|
|
21302
22207
|
//#endregion
|
|
21303
22208
|
exports.$agg = $agg;
|
|
22209
|
+
exports.$expr = $expr;
|
|
21304
22210
|
exports.BelongsTo = BelongsTo;
|
|
21305
22211
|
exports.BelongsToMany = BelongsToMany;
|
|
21306
22212
|
exports.ColumnBuilder = ColumnBuilder;
|
|
@@ -21384,6 +22290,7 @@ exports.int = int;
|
|
|
21384
22290
|
exports.integer = integer;
|
|
21385
22291
|
exports.ipAddress = ipAddress;
|
|
21386
22292
|
exports.isAggregateExpression = isAggregateExpression;
|
|
22293
|
+
exports.isColumnExpression = isColumnExpression;
|
|
21387
22294
|
exports.isMongoDBDriverLoaded = isMongoDBDriverLoaded;
|
|
21388
22295
|
exports.json = json;
|
|
21389
22296
|
exports.lineString = lineString;
|
|
@@ -21414,6 +22321,7 @@ exports.time = time;
|
|
|
21414
22321
|
exports.timestamp = timestamp;
|
|
21415
22322
|
exports.tinyInt = tinyInt;
|
|
21416
22323
|
exports.tinyInteger = tinyInteger;
|
|
22324
|
+
exports.toColumnExpression = toColumnExpression;
|
|
21417
22325
|
exports.transaction = transaction;
|
|
21418
22326
|
exports.tryResolveModelClass = tryResolveModelClass;
|
|
21419
22327
|
exports.ulid = ulid;
|