@warlock.js/cascade 4.4.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -3
- package/cjs/index.cjs +933 -90
- 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.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 +42 -0
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.mjs +55 -6
- 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 +106 -7
- 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 +40 -1
- 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.
|
|
6703
6950
|
*
|
|
6704
|
-
* @param field - The field name to
|
|
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.
|
|
6977
|
+
*
|
|
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
|
|
@@ -12897,18 +13572,24 @@ var PostgresDialect = class {
|
|
|
12897
13572
|
/**
|
|
12898
13573
|
* Translate a database-agnostic aggregate expression to PostgreSQL SQL.
|
|
12899
13574
|
*
|
|
12900
|
-
* The
|
|
12901
|
-
* `floor`, `first` and `last` are
|
|
12902
|
-
* single-scalar `GROUP BY` equivalent on
|
|
12903
|
-
* of emitting a silently-different semantic
|
|
13575
|
+
* The scalar aggregates map to their ANSI SQL function, and `countDistinct`
|
|
13576
|
+
* maps to `COUNT(DISTINCT …)`. `distinct`, `floor`, `first` and `last` are
|
|
13577
|
+
* MongoDB-only for v1 — none has a single-scalar `GROUP BY` equivalent on
|
|
13578
|
+
* PostgreSQL, so they throw instead of emitting a silently-different semantic
|
|
13579
|
+
* (the footgun this guards).
|
|
12904
13580
|
*
|
|
12905
13581
|
* @param expression - The abstract aggregate (`$agg.*`) to translate
|
|
12906
13582
|
* @returns The SQL fragment (e.g. `SUM("amount")`, `COUNT(*)`)
|
|
12907
13583
|
*/
|
|
12908
13584
|
aggregateToSql(expression) {
|
|
13585
|
+
if (expression.__expr) {
|
|
13586
|
+
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.`);
|
|
13587
|
+
return `SUM(${this.columnExpressionToSql(expression.__expr)})`;
|
|
13588
|
+
}
|
|
12909
13589
|
const column = expression.__field === null ? "*" : this.quoteIdentifier(expression.__field);
|
|
12910
13590
|
switch (expression.__agg) {
|
|
12911
13591
|
case "count": return "COUNT(*)";
|
|
13592
|
+
case "countDistinct": return `COUNT(DISTINCT ${column})`;
|
|
12912
13593
|
case "sum": return `SUM(${column})`;
|
|
12913
13594
|
case "avg": return `AVG(${column})`;
|
|
12914
13595
|
case "min": return `MIN(${column})`;
|
|
@@ -12916,6 +13597,53 @@ var PostgresDialect = class {
|
|
|
12916
13597
|
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
13598
|
}
|
|
12918
13599
|
}
|
|
13600
|
+
/**
|
|
13601
|
+
* Compile a typed {@link ColumnExpression} tree into a SQL fragment.
|
|
13602
|
+
*
|
|
13603
|
+
* Column references flow through {@link quoteIdentifier} (so user-supplied
|
|
13604
|
+
* names are quoted/escaped, never raw-interpolated); literals are emitted as
|
|
13605
|
+
* numeric/boolean SQL constants; arithmetic ops are parenthesised. The `raw`
|
|
13606
|
+
* node is the only path that emits a string verbatim — it is opt-in by name.
|
|
13607
|
+
*
|
|
13608
|
+
* @param expression - The expression tree to compile
|
|
13609
|
+
* @returns A SQL fragment (e.g. `("price" * "quantity")`)
|
|
13610
|
+
*/
|
|
13611
|
+
columnExpressionToSql(expression) {
|
|
13612
|
+
switch (expression.__expr) {
|
|
13613
|
+
case "column": return this.quoteIdentifier(expression.column);
|
|
13614
|
+
case "literal": return typeof expression.value === "boolean" ? this.booleanLiteral(expression.value) : String(expression.value);
|
|
13615
|
+
case "raw": return expression.expression;
|
|
13616
|
+
case "add":
|
|
13617
|
+
case "subtract":
|
|
13618
|
+
case "multiply":
|
|
13619
|
+
case "divide": {
|
|
13620
|
+
const operator = {
|
|
13621
|
+
add: "+",
|
|
13622
|
+
subtract: "-",
|
|
13623
|
+
multiply: "*",
|
|
13624
|
+
divide: "/"
|
|
13625
|
+
}[expression.__expr];
|
|
13626
|
+
return `(${expression.operands.map((operand) => this.columnExpressionToSql(operand)).join(` ${operator} `)})`;
|
|
13627
|
+
}
|
|
13628
|
+
default: throw new Error(`Unsupported column expression node: ${JSON.stringify(expression)}`);
|
|
13629
|
+
}
|
|
13630
|
+
}
|
|
13631
|
+
/**
|
|
13632
|
+
* Build a `date_trunc` bucket expression for a portable `groupByDate`.
|
|
13633
|
+
*
|
|
13634
|
+
* @param column - The date/timestamp column to truncate
|
|
13635
|
+
* @param unit - The bucket granularity
|
|
13636
|
+
* @returns The SQL fragment (e.g. `date_trunc('month', "created_at")`)
|
|
13637
|
+
*
|
|
13638
|
+
* @example
|
|
13639
|
+
* ```typescript
|
|
13640
|
+
* dialect.dateTruncSql("created_at", "month");
|
|
13641
|
+
* // date_trunc('month', "created_at")
|
|
13642
|
+
* ```
|
|
13643
|
+
*/
|
|
13644
|
+
dateTruncSql(column, unit) {
|
|
13645
|
+
return `date_trunc('${unit}', ${this.quoteIdentifier(column)})`;
|
|
13646
|
+
}
|
|
12919
13647
|
};
|
|
12920
13648
|
|
|
12921
13649
|
//#endregion
|
|
@@ -13764,10 +14492,16 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13764
14492
|
*/
|
|
13765
14493
|
orderClauses = [];
|
|
13766
14494
|
/**
|
|
13767
|
-
* GROUP BY columns.
|
|
14495
|
+
* GROUP BY columns (bare identifiers — get quoted).
|
|
13768
14496
|
*/
|
|
13769
14497
|
groupColumns = [];
|
|
13770
14498
|
/**
|
|
14499
|
+
* Raw GROUP BY expressions (emitted verbatim — e.g. a `date_trunc(...)`
|
|
14500
|
+
* bucket). Kept separate from `groupColumns` so they bypass identifier
|
|
14501
|
+
* quoting while still landing in the same `GROUP BY` clause.
|
|
14502
|
+
*/
|
|
14503
|
+
groupRawExpressions = [];
|
|
14504
|
+
/**
|
|
13771
14505
|
* HAVING clauses.
|
|
13772
14506
|
*/
|
|
13773
14507
|
havingClauses = [];
|
|
@@ -13938,6 +14672,9 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13938
14672
|
case "groupBy":
|
|
13939
14673
|
this.processGroupBy(data);
|
|
13940
14674
|
break;
|
|
14675
|
+
case "groupByRaw":
|
|
14676
|
+
this.processGroupByRaw(data);
|
|
14677
|
+
break;
|
|
13941
14678
|
case "having":
|
|
13942
14679
|
this.processHaving(data);
|
|
13943
14680
|
break;
|
|
@@ -13972,9 +14709,9 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
13972
14709
|
parts.push(fromClause);
|
|
13973
14710
|
if (this.joinClauses.length > 0) parts.push(this.joinClauses.join(" "));
|
|
13974
14711
|
if (this.whereClauses.length > 0) parts.push(`WHERE ${this.whereClauses.join(" ")}`);
|
|
13975
|
-
if (this.groupColumns.length > 0) {
|
|
13976
|
-
const
|
|
13977
|
-
parts.push(`GROUP BY ${
|
|
14712
|
+
if (this.groupColumns.length > 0 || this.groupRawExpressions.length > 0) {
|
|
14713
|
+
const allGroupTerms = [...this.groupColumns.map((c) => this.dialect.quoteIdentifier(c)), ...this.groupRawExpressions];
|
|
14714
|
+
parts.push(`GROUP BY ${allGroupTerms.join(", ")}`);
|
|
13978
14715
|
}
|
|
13979
14716
|
if (this.havingClauses.length > 0) parts.push(`HAVING ${this.havingClauses.join(" AND ")}`);
|
|
13980
14717
|
if (this.orderClauses.length > 0) parts.push(`ORDER BY ${this.orderClauses.join(", ")}`);
|
|
@@ -14432,6 +15169,14 @@ var PostgresQueryParser = class PostgresQueryParser {
|
|
|
14432
15169
|
this.groupColumns.push(...columns);
|
|
14433
15170
|
}
|
|
14434
15171
|
/**
|
|
15172
|
+
* Process a raw GROUP BY expression (emitted verbatim, no quoting). Used by
|
|
15173
|
+
* `groupByDate` to add a `date_trunc(...)` bucket to the GROUP BY clause.
|
|
15174
|
+
*/
|
|
15175
|
+
processGroupByRaw(data) {
|
|
15176
|
+
const expression = data.expression;
|
|
15177
|
+
this.groupRawExpressions.push(expression);
|
|
15178
|
+
}
|
|
15179
|
+
/**
|
|
14435
15180
|
* Process HAVING operation.
|
|
14436
15181
|
*/
|
|
14437
15182
|
processHaving(data) {
|
|
@@ -14612,6 +15357,53 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
14612
15357
|
if (!aggregates) return super.groupBy(fields);
|
|
14613
15358
|
const fieldList = Array.isArray(fields) ? fields : [fields];
|
|
14614
15359
|
this.addOperation("select", { fields: fieldList });
|
|
15360
|
+
this.projectGroupAggregates(aggregates);
|
|
15361
|
+
return super.groupBy(fields);
|
|
15362
|
+
}
|
|
15363
|
+
/**
|
|
15364
|
+
* Portable date-bucketed GROUP BY.
|
|
15365
|
+
*
|
|
15366
|
+
* Truncates `column` to the given bucket (`day` / `week` / `month` / `year`)
|
|
15367
|
+
* via PostgreSQL's `date_trunc`, projects the bucket under the column's own
|
|
15368
|
+
* name, and groups by the truncated expression. Optional `aggregates` follow
|
|
15369
|
+
* the same rules as the two-arg `groupBy` (`$agg.*` helpers or raw SQL
|
|
15370
|
+
* strings; MongoDB operator objects and MongoDB-only aggregates throw).
|
|
15371
|
+
*
|
|
15372
|
+
* Emits (for `groupByDate("created_at", "month", { revenue: $agg.sum(...) })`):
|
|
15373
|
+
* `SELECT date_trunc('month', "created_at") AS "created_at", SUM(...) AS "revenue"
|
|
15374
|
+
* FROM ... GROUP BY date_trunc('month', "created_at")`
|
|
15375
|
+
*
|
|
15376
|
+
* @param column - The date/timestamp column to bucket
|
|
15377
|
+
* @param unit - The bucket granularity
|
|
15378
|
+
* @param aggregates - Optional aggregate projections keyed by output alias
|
|
15379
|
+
*
|
|
15380
|
+
* @example
|
|
15381
|
+
* Order.query().groupByDate("created_at", "month", {
|
|
15382
|
+
* revenue: $agg.sum($expr.mul("price", "quantity")),
|
|
15383
|
+
* });
|
|
15384
|
+
*/
|
|
15385
|
+
groupByDate(column, unit, aggregates) {
|
|
15386
|
+
const bucketSql = this.driver.dialect.dateTruncSql(column, unit);
|
|
15387
|
+
this.addOperation("selectRaw", {
|
|
15388
|
+
expression: `${bucketSql} AS ${this.driver.dialect.quoteIdentifier(column)}`,
|
|
15389
|
+
bindings: []
|
|
15390
|
+
});
|
|
15391
|
+
if (aggregates) this.projectGroupAggregates(aggregates);
|
|
15392
|
+
this.addOperation("groupByRaw", {
|
|
15393
|
+
expression: bucketSql,
|
|
15394
|
+
bindings: []
|
|
15395
|
+
});
|
|
15396
|
+
return this;
|
|
15397
|
+
}
|
|
15398
|
+
/**
|
|
15399
|
+
* Translate a `{ alias: aggregate }` map into `selectRaw` projections and
|
|
15400
|
+
* record each alias → SQL so `applyGroupByAggregates` can later rewrite a
|
|
15401
|
+
* `having()` on the alias. Shared by `groupBy` and `groupByDate`.
|
|
15402
|
+
*
|
|
15403
|
+
* `$agg.distinct/floor/first/last` throw (MongoDB-only on Postgres v1);
|
|
15404
|
+
* MongoDB operator objects throw (not portable to SQL).
|
|
15405
|
+
*/
|
|
15406
|
+
projectGroupAggregates(aggregates) {
|
|
14615
15407
|
for (const [alias, expression] of Object.entries(aggregates)) {
|
|
14616
15408
|
let sql;
|
|
14617
15409
|
if (isAggregateExpression(expression)) sql = this.driver.dialect.aggregateToSql(expression);
|
|
@@ -14623,7 +15415,6 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
|
|
|
14623
15415
|
bindings: []
|
|
14624
15416
|
});
|
|
14625
15417
|
}
|
|
14626
|
-
return super.groupBy(fields);
|
|
14627
15418
|
}
|
|
14628
15419
|
/**
|
|
14629
15420
|
* Nearest-neighbour vector similarity search via pgvector cosine distance.
|
|
@@ -16134,12 +16925,20 @@ var PostgresDriver = class {
|
|
|
16134
16925
|
*/
|
|
16135
16926
|
_syncAdapter;
|
|
16136
16927
|
/**
|
|
16928
|
+
* Lookup set of column names that hold native PostgreSQL arrays
|
|
16929
|
+
* (`JSONB[]`, `TEXT[]`, …) and must NOT be JSON-text encoded.
|
|
16930
|
+
*
|
|
16931
|
+
* @see PostgresPoolConfig.nativeArrayColumns
|
|
16932
|
+
*/
|
|
16933
|
+
_nativeArrayColumns;
|
|
16934
|
+
/**
|
|
16137
16935
|
* Create a new PostgreSQL driver instance.
|
|
16138
16936
|
*
|
|
16139
16937
|
* @param config - PostgreSQL connection configuration
|
|
16140
16938
|
*/
|
|
16141
16939
|
constructor(config) {
|
|
16142
16940
|
this.config = config;
|
|
16941
|
+
this._nativeArrayColumns = new Set(config.nativeArrayColumns ?? []);
|
|
16143
16942
|
}
|
|
16144
16943
|
/**
|
|
16145
16944
|
* Get the connection pool instance.
|
|
@@ -16240,15 +17039,56 @@ var PostgresDriver = class {
|
|
|
16240
17039
|
const serialized = {};
|
|
16241
17040
|
for (const [key, value] of Object.entries(data)) {
|
|
16242
17041
|
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;
|
|
17042
|
+
serialized[key] = this.serializeValue(key, value);
|
|
16248
17043
|
}
|
|
16249
17044
|
return serialized;
|
|
16250
17045
|
}
|
|
16251
17046
|
/**
|
|
17047
|
+
* Serialize a single column value into a node-pg bindable parameter.
|
|
17048
|
+
*
|
|
17049
|
+
* Shared by {@link serialize} (INSERT path) and {@link buildUpdateQuery}
|
|
17050
|
+
* `$set` (UPDATE path) so both encode `json` / `jsonb` columns identically.
|
|
17051
|
+
*
|
|
17052
|
+
* Encoding rules (in order):
|
|
17053
|
+
* - `Date` → ISO string.
|
|
17054
|
+
* - `bigint` → decimal string (node-pg has no native bigint binding).
|
|
17055
|
+
* - all-number array → pgvector literal `'[n1,n2,...]'`. node-pg would
|
|
17056
|
+
* otherwise emit a `{n1,n2,...}` array literal, which the `vector` type
|
|
17057
|
+
* rejects. This branch is preserved exactly.
|
|
17058
|
+
* - any other array (object-array, string-array, mixed, empty `[]`) →
|
|
17059
|
+
* `JSON.stringify`. node-pg renders a raw JS array as a PostgreSQL array
|
|
17060
|
+
* literal `{...}` (and `[]` as `{}`), which a `json` / `jsonb` column
|
|
17061
|
+
* rejects — so we bind the value as JSON text instead, the form those
|
|
17062
|
+
* columns accept. Columns listed in `nativeArrayColumns` are exempt:
|
|
17063
|
+
* their raw array is passed through so node-pg emits the `{...}` literal
|
|
17064
|
+
* a genuine `JSONB[]` / `TEXT[]` column needs.
|
|
17065
|
+
* - plain object → `JSON.stringify`. Equivalent to node-pg's own object
|
|
17066
|
+
* handling, made explicit so both write paths agree.
|
|
17067
|
+
* - everything else (scalars: string, number, boolean, null) → untouched.
|
|
17068
|
+
*
|
|
17069
|
+
* Boundary note: the serializer has no access to the table schema, so it
|
|
17070
|
+
* cannot tell a `json` / `jsonb` column from a native-array column purely
|
|
17071
|
+
* from the value. `nativeArrayColumns` is the explicit, opt-in escape hatch
|
|
17072
|
+
* for the latter. No `::jsonb` placeholder cast is added: a JSON-text string
|
|
17073
|
+
* binds correctly to `json` / `jsonb` without one, and a blind cast would
|
|
17074
|
+
* misfire on columns we cannot positively identify as jsonb.
|
|
17075
|
+
*
|
|
17076
|
+
* @param key - Column name (used to honour `nativeArrayColumns`)
|
|
17077
|
+
* @param value - The raw value to serialize (never `undefined`)
|
|
17078
|
+
* @returns The value ready to bind as a query parameter
|
|
17079
|
+
*/
|
|
17080
|
+
serializeValue(key, value) {
|
|
17081
|
+
if (value instanceof Date) return value.toISOString();
|
|
17082
|
+
if (typeof value === "bigint") return value.toString();
|
|
17083
|
+
if (Array.isArray(value)) {
|
|
17084
|
+
if (value.length > 0 && value.every((v) => typeof v === "number")) return `[${value.join(",")}]`;
|
|
17085
|
+
if (this._nativeArrayColumns.has(key)) return value;
|
|
17086
|
+
return JSON.stringify(value);
|
|
17087
|
+
}
|
|
17088
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
17089
|
+
return value;
|
|
17090
|
+
}
|
|
17091
|
+
/**
|
|
16252
17092
|
* Get the dirty tracker for this driver.
|
|
16253
17093
|
*/
|
|
16254
17094
|
getDirtyTracker(data) {
|
|
@@ -16717,7 +17557,7 @@ var PostgresDriver = class {
|
|
|
16717
17557
|
let paramIndex = 1;
|
|
16718
17558
|
if (update.$set) for (const [key, value] of Object.entries(update.$set)) {
|
|
16719
17559
|
setClauses.push(`${this.dialect.quoteIdentifier(key)} = ${this.dialect.placeholder(paramIndex++)}`);
|
|
16720
|
-
params.push(value);
|
|
17560
|
+
params.push(value === void 0 ? value : this.serializeValue(key, value));
|
|
16721
17561
|
}
|
|
16722
17562
|
if (update.$unset) for (const key of Object.keys(update.$unset)) setClauses.push(`${this.dialect.quoteIdentifier(key)} = NULL`);
|
|
16723
17563
|
if (update.$inc) for (const [key, amount] of Object.entries(update.$inc)) {
|
|
@@ -21301,6 +22141,7 @@ async function listExecutedMigrations() {
|
|
|
21301
22141
|
|
|
21302
22142
|
//#endregion
|
|
21303
22143
|
exports.$agg = $agg;
|
|
22144
|
+
exports.$expr = $expr;
|
|
21304
22145
|
exports.BelongsTo = BelongsTo;
|
|
21305
22146
|
exports.BelongsToMany = BelongsToMany;
|
|
21306
22147
|
exports.ColumnBuilder = ColumnBuilder;
|
|
@@ -21384,6 +22225,7 @@ exports.int = int;
|
|
|
21384
22225
|
exports.integer = integer;
|
|
21385
22226
|
exports.ipAddress = ipAddress;
|
|
21386
22227
|
exports.isAggregateExpression = isAggregateExpression;
|
|
22228
|
+
exports.isColumnExpression = isColumnExpression;
|
|
21387
22229
|
exports.isMongoDBDriverLoaded = isMongoDBDriverLoaded;
|
|
21388
22230
|
exports.json = json;
|
|
21389
22231
|
exports.lineString = lineString;
|
|
@@ -21414,6 +22256,7 @@ exports.time = time;
|
|
|
21414
22256
|
exports.timestamp = timestamp;
|
|
21415
22257
|
exports.tinyInt = tinyInt;
|
|
21416
22258
|
exports.tinyInteger = tinyInteger;
|
|
22259
|
+
exports.toColumnExpression = toColumnExpression;
|
|
21417
22260
|
exports.transaction = transaction;
|
|
21418
22261
|
exports.tryResolveModelClass = tryResolveModelClass;
|
|
21419
22262
|
exports.ulid = ulid;
|