velocious 1.0.582 → 1.0.583
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/README.md +2 -2
- package/build/configuration-types.js +2 -0
- package/build/database/drivers/base.js +75 -0
- package/build/database/query/preloader/belongs-to.js +32 -31
- package/build/database/query/preloader/has-many.js +50 -26
- package/build/database/query/preloader/has-one.js +20 -12
- package/build/database/query/query-data.js +23 -8
- package/build/database/query/with-count.js +47 -11
- package/build/src/configuration-types.d.ts +10 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +3 -1
- package/build/src/database/drivers/base.d.ts +39 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +70 -1
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.js +33 -31
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +48 -28
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.js +21 -13
- package/build/src/database/query/query-data.js +21 -8
- package/build/src/database/query/with-count.js +41 -12
- package/package.json +2 -1
- package/src/configuration-types.js +2 -0
- package/src/database/drivers/base.js +75 -0
- package/src/database/query/preloader/belongs-to.js +32 -31
- package/src/database/query/preloader/has-many.js +50 -26
- package/src/database/query/preloader/has-one.js +20 -12
- package/src/database/query/query-data.js +23 -8
- package/src/database/query/with-count.js +47 -11
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
24
24
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
25
25
|
* Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, and linting (see [docs/factories.md](docs/factories.md))
|
|
26
|
-
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters
|
|
27
|
-
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
26
|
+
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
27
|
+
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
28
28
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
29
29
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
30
30
|
* Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
|
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
* @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
|
|
76
76
|
* @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
|
|
77
77
|
* @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
|
|
78
|
+
* @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
|
|
79
|
+
* @property {number} [maxQuerySqlBytes] - Maximum serialized SQL size, in bytes, for a single cohort query used by preloads, association counts, and queryData aggregates. Defaults to 1 MiB (1048576).
|
|
78
80
|
* @property {string} [password] - Password for the database user.
|
|
79
81
|
* @property {number} [port] - Database port.
|
|
80
82
|
* @property {string} [primaryKeyType] - Default type for implicit migration primary keys and references. Defaults to `uuid`.
|
|
@@ -806,10 +806,82 @@ export default class VelociousDatabaseDriversBase {
|
|
|
806
806
|
return optionalPositiveInteger(this.getArgs().maxInsertSqlBytes, "maxInsertSqlBytes") ?? 1048576
|
|
807
807
|
}
|
|
808
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Maximum values in a single `IN (...)` cohort used by preloads, association
|
|
811
|
+
* counts, and queryData aggregates. The default stays under SQLite's default
|
|
812
|
+
* `MAX_VARIABLE_NUMBER` compile-time limit.
|
|
813
|
+
*
|
|
814
|
+
* Override via `maxInClauseValues` in the database configuration.
|
|
815
|
+
* @returns {number} - Maximum values per IN clause cohort.
|
|
816
|
+
*/
|
|
817
|
+
maxInClauseValues() {
|
|
818
|
+
return optionalPositiveInteger(this.getArgs().maxInClauseValues, "maxInClauseValues") ?? 999
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Maximum serialized SQL size, in bytes, for a single cohort query used by
|
|
823
|
+
* preloads, association counts, and queryData aggregates. Cohort chunking
|
|
824
|
+
* stops when the next value would push the generated string over this threshold.
|
|
825
|
+
*
|
|
826
|
+
* Override via `maxQuerySqlBytes` in the database configuration.
|
|
827
|
+
* @returns {number} - Maximum bytes per cohort query.
|
|
828
|
+
*/
|
|
829
|
+
maxQuerySqlBytes() {
|
|
830
|
+
return optionalPositiveInteger(this.getArgs().maxQuerySqlBytes, "maxQuerySqlBytes") ?? 1048576
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Splits `values` into cohort chunks that stay within both `maxCount` and
|
|
835
|
+
* `maxBytes` while preserving order.
|
|
836
|
+
*
|
|
837
|
+
* A chunk always contains at least one value, even if that single value exceeds
|
|
838
|
+
* the byte limit, so progress is guaranteed.
|
|
839
|
+
* @template T
|
|
840
|
+
* @param {Array<T>} values - Values to chunk.
|
|
841
|
+
* @param {(values: Array<T>) => string} buildSql - Function that builds the full SQL for a candidate chunk.
|
|
842
|
+
* @param {{maxCount?: number, maxBytes?: number}} [options] - Chunking bounds.
|
|
843
|
+
* @returns {Array<Array<T>>} - Value cohorts.
|
|
844
|
+
*/
|
|
845
|
+
chunkValues(values, buildSql, {maxCount = this.maxInClauseValues(), maxBytes = this.maxQuerySqlBytes()} = {}) {
|
|
846
|
+
if (values.length === 0) return []
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Chunks.
|
|
850
|
+
* @type {Array<Array<T>>} */
|
|
851
|
+
const chunks = []
|
|
852
|
+
/**
|
|
853
|
+
* Current chunk.
|
|
854
|
+
* @type {Array<T>} */
|
|
855
|
+
let currentChunk = []
|
|
856
|
+
|
|
857
|
+
for (const value of values) {
|
|
858
|
+
const candidate = [...currentChunk, value]
|
|
859
|
+
const candidateBytes = Buffer.byteLength(buildSql(candidate), "utf8")
|
|
860
|
+
|
|
861
|
+
if (currentChunk.length > 0 && (candidate.length > maxCount || candidateBytes > maxBytes)) {
|
|
862
|
+
chunks.push(currentChunk)
|
|
863
|
+
currentChunk = [value]
|
|
864
|
+
} else {
|
|
865
|
+
currentChunk = candidate
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (currentChunk.length > 0) {
|
|
870
|
+
chunks.push(currentChunk)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return chunks
|
|
874
|
+
}
|
|
875
|
+
|
|
809
876
|
/**
|
|
810
877
|
* Splits `rows` into chunks that stay within both {@link maxRowsPerInsert}
|
|
811
878
|
* and {@link maxInsertSqlBytes} while preserving order.
|
|
812
879
|
*
|
|
880
|
+
* Byte accounting is incremental: `buildSql` is called once with `[]` to
|
|
881
|
+
* measure the statement prefix and once per row with `[row]` to measure the
|
|
882
|
+
* row's values tuple. This keeps chunking linear in the number of rows
|
|
883
|
+
* instead of rebuilding the full multi-row SQL for every candidate.
|
|
884
|
+
*
|
|
813
885
|
* A chunk always contains at least one row, even if that single row exceeds
|
|
814
886
|
* the byte limit, so progress is guaranteed.
|
|
815
887
|
* @param {Array<Array<ReturnType<typeof JSON.parse>>>} rows - Rows to insert.
|
|
@@ -824,6 +896,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
824
896
|
const prefix = `${emptySql} VALUES `
|
|
825
897
|
const baseByteLength = Buffer.byteLength(prefix, "utf8")
|
|
826
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Current chunk.
|
|
901
|
+
* @type {Array<Array<ReturnType<typeof JSON.parse>>>} */
|
|
827
902
|
let currentChunk = []
|
|
828
903
|
let currentBytes = 0
|
|
829
904
|
|
|
@@ -87,25 +87,27 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
|
|
|
87
87
|
if (foreignKeyValues.size > 0) {
|
|
88
88
|
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), modelsToLoad[0])
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const whereArgs = {}
|
|
90
|
+
// Build the query once with scope and selection, then clone it per cohort so
|
|
91
|
+
// the IN-list size stays within driver limits without rebuilding shared state.
|
|
92
|
+
let baseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
95
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [primaryKey]})
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const driver = baseQuery.driver
|
|
98
|
+
const cohorts = driver.chunkValues([...foreignKeyValues], (chunk) => baseQuery.clone().where({[primaryKey]: chunk}).toSql())
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
for (const cohort of cohorts) {
|
|
101
|
+
const cohortQuery = baseQuery.clone().where({[primaryKey]: cohort})
|
|
102
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
targetModels.push(...foundTargetModels)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
for (const targetModel of foundTargetModels) {
|
|
107
|
+
const primaryKeyValue = /** @type {string | number} */ (targetModel.readColumn(primaryKey))
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
targetModelsById[primaryKeyValue] = targetModel
|
|
110
|
+
}
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -205,33 +207,32 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
|
|
|
205
207
|
|
|
206
208
|
await ensureModelClassInitialized(targetModelClass, configuration, this.models[0])
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
* Where args.
|
|
210
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
211
|
-
const whereArgs = {}
|
|
210
|
+
let baseQuery = preloadQueryForModel(this.models, targetModelClass)
|
|
212
211
|
|
|
213
|
-
|
|
212
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
213
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [primaryKey]})
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
const driver = baseQuery.driver
|
|
216
|
+
const cohorts = driver.chunkValues([...foreignKeyValuesByType[targetType]], (chunk) => baseQuery.clone().where({[primaryKey]: chunk}).toSql())
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [primaryKey]})
|
|
218
|
+
targetModelsByTypeAndId[targetType] = {}
|
|
219
219
|
|
|
220
|
-
const
|
|
220
|
+
for (const cohort of cohorts) {
|
|
221
|
+
const cohortQuery = baseQuery.clone().where({[primaryKey]: cohort})
|
|
222
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
targetModels.push(...foundTargetModels)
|
|
223
225
|
|
|
224
|
-
|
|
226
|
+
const className = targetModelClass.getModelName()
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
if (!targetModelsByClassName[className]) targetModelsByClassName[className] = []
|
|
229
|
+
targetModelsByClassName[className].push(...foundTargetModels)
|
|
228
230
|
|
|
229
|
-
|
|
231
|
+
for (const targetModel of foundTargetModels) {
|
|
232
|
+
const primaryKeyValue = /** @type {string | number} */ (targetModel.readColumn(primaryKey))
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
targetModelsByTypeAndId[targetType][primaryKeyValue] = targetModel
|
|
234
|
+
targetModelsByTypeAndId[targetType][primaryKeyValue] = targetModel
|
|
235
|
+
}
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -155,10 +155,11 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
155
155
|
modelsByPrimaryKeyValue[primaryKeyValue].push(model)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// Step 1: Query the through table to build parent→target ID mapping
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
// Step 1: Query the through table to build parent→target ID mapping.
|
|
159
|
+
// Chunk the parent PK cohort so the through query's IN-list stays bounded.
|
|
160
|
+
const throughBaseQuery = preloadQueryForModel(modelsToLoad, throughModelClass)
|
|
161
|
+
const throughDriver = throughBaseQuery.driver
|
|
162
|
+
const throughCohorts = throughDriver.chunkValues([...modelsPrimaryKeyValues], (chunk) => throughBaseQuery.clone().where({[throughForeignKey]: chunk}).toSql())
|
|
162
163
|
|
|
163
164
|
/**
|
|
164
165
|
* Parent to target ids.
|
|
@@ -170,28 +171,43 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
170
171
|
* @type {Set<string | number>} */
|
|
171
172
|
const allTargetIds = new Set()
|
|
172
173
|
|
|
173
|
-
for (const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
174
|
+
for (const cohort of throughCohorts) {
|
|
175
|
+
const throughQuery = throughBaseQuery.clone().where({[throughForeignKey]: cohort})
|
|
176
|
+
const throughModels = await throughQuery.toArray()
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
for (const throughModel of throughModels) {
|
|
179
|
+
const parentId = /** @type {string | number} */ (throughModel.readColumn(throughForeignKey))
|
|
180
|
+
const throughId = /** @type {string | number} */ (throughModel.readColumn(throughModelClass.primaryKey()))
|
|
178
181
|
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
if (!(parentId in parentToTargetIds)) parentToTargetIds[parentId] = []
|
|
183
|
+
|
|
184
|
+
parentToTargetIds[parentId].push(throughId)
|
|
185
|
+
allTargetIds.add(throughId)
|
|
186
|
+
}
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
// Step 2: Load target models by the foreign key that points to the through table
|
|
189
|
+
// Step 2: Load target models by the foreign key that points to the through table.
|
|
190
|
+
// Chunk the target ID cohort so the target query's IN-list stays bounded.
|
|
184
191
|
/**
|
|
185
192
|
* Target models.
|
|
186
193
|
* @type {import("../../record/index.js").default[]} */
|
|
187
194
|
let targetModels = []
|
|
188
195
|
|
|
189
196
|
if (allTargetIds.size > 0) {
|
|
190
|
-
let
|
|
197
|
+
let targetBaseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
198
|
+
|
|
199
|
+
targetBaseQuery = this.relationship.applyScope(targetBaseQuery)
|
|
200
|
+
targetBaseQuery = this.selection.applyToQuery({query: targetBaseQuery, targetModelClass, mappingColumns: [targetForeignKey]})
|
|
201
|
+
|
|
202
|
+
const targetDriver = targetBaseQuery.driver
|
|
203
|
+
const targetCohorts = targetDriver.chunkValues([...allTargetIds], (chunk) => targetBaseQuery.clone().where({[targetForeignKey]: chunk}).toSql())
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
for (const cohort of targetCohorts) {
|
|
206
|
+
const cohortQuery = targetBaseQuery.clone().where({[targetForeignKey]: cohort})
|
|
207
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
208
|
+
|
|
209
|
+
targetModels.push(...foundTargetModels)
|
|
210
|
+
}
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
// Step 3: Index target models by their foreign key (maps to through model ID)
|
|
@@ -285,27 +301,35 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
285
301
|
modelsByPrimaryKeyValue[primaryKeyValue].push(model)
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
* Where args.
|
|
290
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
291
|
-
const whereArgs = {}
|
|
304
|
+
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), modelsToLoad[0])
|
|
292
305
|
|
|
293
|
-
|
|
306
|
+
// Build the query once with the polymorphic type constant (when present),
|
|
307
|
+
// relationship scope, and selection. The parent ID IN-list is cloned per cohort
|
|
308
|
+
// so the generated SQL stays within driver limits.
|
|
309
|
+
let baseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
294
310
|
|
|
295
311
|
if (this.relationship.getPolymorphic()) {
|
|
296
312
|
const typeColumn = this.relationship.getPolymorphicTypeColumn()
|
|
297
313
|
|
|
298
|
-
|
|
314
|
+
baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
|
|
317
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
318
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
|
|
302
319
|
|
|
303
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Target models.
|
|
322
|
+
* @type {import("../../record/index.js").default[]} */
|
|
323
|
+
const targetModels = []
|
|
324
|
+
const driver = baseQuery.driver
|
|
325
|
+
const cohorts = driver.chunkValues([...modelsPrimaryKeyValues], (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
304
326
|
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
for (const cohort of cohorts) {
|
|
328
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
329
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
307
330
|
|
|
308
|
-
|
|
331
|
+
targetModels.push(...foundTargetModels)
|
|
332
|
+
}
|
|
309
333
|
|
|
310
334
|
for (const targetModel of targetModels) {
|
|
311
335
|
const foreignKeyValue = /** @type {string | number} */ (targetModel.readColumn(foreignKey))
|
|
@@ -72,28 +72,36 @@ export default class VelociousDatabaseQueryPreloaderHasOne {
|
|
|
72
72
|
|
|
73
73
|
if (modelsPrimaryKeyValues.size == 0) return satisfiedTargets
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
* Where args.
|
|
77
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
78
|
-
const whereArgs = {}
|
|
75
|
+
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), this.models[0])
|
|
79
76
|
|
|
80
|
-
|
|
77
|
+
// Load target models to be preloaded on the given models.
|
|
78
|
+
// Build the query once with the polymorphic type constant (when present),
|
|
79
|
+
// relationship scope, and selection. The parent ID IN-list is cloned per cohort
|
|
80
|
+
// so the generated SQL stays within driver limits.
|
|
81
|
+
let baseQuery = preloadQueryForModel(this.models, targetModelClass)
|
|
81
82
|
|
|
82
83
|
if (this.relationship.getPolymorphic()) {
|
|
83
84
|
const typeColumn = this.relationship.getPolymorphicTypeColumn()
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
90
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Target models.
|
|
94
|
+
* @type {import("../../record/index.js").default[]} */
|
|
95
|
+
const targetModels = []
|
|
96
|
+
const driver = baseQuery.driver
|
|
97
|
+
const cohorts = driver.chunkValues([...modelsPrimaryKeyValues], (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
for (const cohort of cohorts) {
|
|
100
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
101
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
targetModels.push(...foundTargetModels)
|
|
104
|
+
}
|
|
97
105
|
|
|
98
106
|
for (const targetModel of targetModels) {
|
|
99
107
|
const foreignKeyValue = /** @type {string | number} */ (targetModel.readColumn(foreignKey))
|
|
@@ -227,7 +227,7 @@ export async function runQueryData({rootModelClass, rootModels, entries}) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
for (const {query} of queryGroups) {
|
|
230
|
-
await
|
|
230
|
+
await executeChunkedEntryQuery({primaryKey, query, rootIds, rootModels})
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -267,13 +267,6 @@ function prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass, s
|
|
|
267
267
|
const rootTable = rootModelClass.tableName()
|
|
268
268
|
const rootPkSql = `${driver.quoteTable(rootTable)}.${driver.quoteColumn(primaryKey)}`
|
|
269
269
|
|
|
270
|
-
/**
|
|
271
|
-
* Root where.
|
|
272
|
-
* @type {Record<string, ReturnType<typeof JSON.parse>>} */
|
|
273
|
-
const rootWhere = {}
|
|
274
|
-
rootWhere[primaryKey] = rootIds
|
|
275
|
-
query.where(rootWhere)
|
|
276
|
-
|
|
277
270
|
const joinDescriptor = buildNestedJoinDescriptor(entry.chain)
|
|
278
271
|
|
|
279
272
|
if (joinDescriptor !== true) {
|
|
@@ -370,3 +363,25 @@ async function executeEntryQuery({primaryKey, query, rootModels}) {
|
|
|
370
363
|
}
|
|
371
364
|
}
|
|
372
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Executes one compatible queryData group in cohorts so the root ID IN-list
|
|
369
|
+
* stays within driver limits, attaching each selected alias to the matching
|
|
370
|
+
* root record.
|
|
371
|
+
* @param {object} args - Options.
|
|
372
|
+
* @param {string} args.primaryKey - Root model primary key column.
|
|
373
|
+
* @param {import("./model-class-query.js").default} args.query - Prepared grouped query.
|
|
374
|
+
* @param {Array<string | number>} args.rootIds - Root primary-key values.
|
|
375
|
+
* @param {import("../record/index.js").default[]} args.rootModels - Loaded root records.
|
|
376
|
+
* @returns {Promise<void>}
|
|
377
|
+
*/
|
|
378
|
+
async function executeChunkedEntryQuery({primaryKey, query, rootIds, rootModels}) {
|
|
379
|
+
const driver = query.driver
|
|
380
|
+
const cohorts = driver.chunkValues(rootIds, (chunk) => query.clone().where({[primaryKey]: chunk}).toSql())
|
|
381
|
+
|
|
382
|
+
for (const cohort of cohorts) {
|
|
383
|
+
const cohortQuery = query.clone().where({[primaryKey]: cohort})
|
|
384
|
+
|
|
385
|
+
await executeEntryQuery({primaryKey, query: cohortQuery, rootModels})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -115,19 +115,19 @@ export async function runWithCount({models, modelClass, entries}) {
|
|
|
115
115
|
const queryGroups = new Map()
|
|
116
116
|
|
|
117
117
|
for (const entry of entries) {
|
|
118
|
-
const
|
|
119
|
-
const sql =
|
|
118
|
+
const {baseQuery, foreignKey} = queryForEntry({entry, modelClass, sourceModel})
|
|
119
|
+
const sql = baseQuery.toSql()
|
|
120
120
|
const existingGroup = queryGroups.get(sql)
|
|
121
121
|
|
|
122
122
|
if (existingGroup) {
|
|
123
123
|
existingGroup.entries.push(entry)
|
|
124
124
|
} else {
|
|
125
|
-
queryGroups.set(sql, {
|
|
125
|
+
queryGroups.set(sql, {baseQuery, entries: [entry], foreignKey})
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
for (const {
|
|
130
|
-
const counts = await
|
|
129
|
+
for (const {baseQuery, entries: groupedEntries, foreignKey} of queryGroups.values()) {
|
|
130
|
+
const counts = await executeChunkedCountQuery({baseQuery, foreignKey, parentIds})
|
|
131
131
|
|
|
132
132
|
for (const entry of groupedEntries) attachCounts({counts, entry, models, primaryKey})
|
|
133
133
|
}
|
|
@@ -135,14 +135,16 @@ export async function runWithCount({models, modelClass, entries}) {
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Builds the grouped count query for an entry.
|
|
138
|
+
*
|
|
139
|
+
* The returned query does NOT yet filter by parent IDs; callers chunk the
|
|
140
|
+
* parent cohort and apply the foreign-key IN clause per chunk.
|
|
138
141
|
* @param {object} args - Options.
|
|
139
142
|
* @param {WithCountEntry} args.entry - Entry being evaluated.
|
|
140
143
|
* @param {typeof import("../record/index.js").default} args.modelClass - Parent model class.
|
|
141
|
-
* @param {Array<string | number>} args.parentIds - Primary keys of the loaded parents.
|
|
142
144
|
* @param {import("../record/index.js").default} args.sourceModel - Loaded operation owner.
|
|
143
|
-
* @returns {import("./model-class-query.js").default} - Prepared count query.
|
|
145
|
+
* @returns {{baseQuery: import("./model-class-query.js").default, foreignKey: string}} - Prepared count query and its foreign key.
|
|
144
146
|
*/
|
|
145
|
-
function queryForEntry({entry, modelClass,
|
|
147
|
+
function queryForEntry({entry, modelClass, sourceModel}) {
|
|
146
148
|
const relationship = modelClass.getRelationshipByName(entry.relationshipName)
|
|
147
149
|
|
|
148
150
|
if (!relationship) {
|
|
@@ -163,7 +165,7 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
163
165
|
/**
|
|
164
166
|
* Mandatory cohort conditions.
|
|
165
167
|
* @type {Record<string, ReturnType<typeof JSON.parse>>} */
|
|
166
|
-
const mandatoryWhereConditions = {
|
|
168
|
+
const mandatoryWhereConditions = {}
|
|
167
169
|
|
|
168
170
|
if (relationship.getPolymorphic && relationship.getPolymorphic()) {
|
|
169
171
|
const typeColumn = relationship.getPolymorphicTypeColumn()
|
|
@@ -171,8 +173,12 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
const baseQuery = sourceModel.queryForModel(targetModelClass)
|
|
176
|
+
|
|
174
177
|
baseQuery._forceQualifyBaseTable = true
|
|
175
|
-
|
|
178
|
+
|
|
179
|
+
if (Object.keys(mandatoryWhereConditions).length > 0) {
|
|
180
|
+
baseQuery.where(mandatoryWhereConditions)
|
|
181
|
+
}
|
|
176
182
|
|
|
177
183
|
if (entry.where) {
|
|
178
184
|
baseQuery.where(entry.where)
|
|
@@ -192,7 +198,7 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
192
198
|
countQuery.select(`${qualifiedForeignKey} AS parent_id`)
|
|
193
199
|
countQuery.select("COUNT(*) AS count_value")
|
|
194
200
|
|
|
195
|
-
return countQuery
|
|
201
|
+
return {baseQuery: countQuery, foreignKey}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
/**
|
|
@@ -219,6 +225,36 @@ async function executeCountQuery(countQuery) {
|
|
|
219
225
|
return counts
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Executes a grouped count query in cohorts so the parent ID IN-list stays
|
|
230
|
+
* within driver limits, merging per-parent counts across chunks.
|
|
231
|
+
* @param {object} args - Options.
|
|
232
|
+
* @param {import("./model-class-query.js").default} args.baseQuery - Prepared count query without parent IDs.
|
|
233
|
+
* @param {string} args.foreignKey - Foreign key used to join to the parents.
|
|
234
|
+
* @param {Array<string | number>} args.parentIds - Primary keys of the loaded parents.
|
|
235
|
+
* @returns {Promise<Map<string | number, number>>} - Map of parent pk → count.
|
|
236
|
+
*/
|
|
237
|
+
async function executeChunkedCountQuery({baseQuery, foreignKey, parentIds}) {
|
|
238
|
+
const driver = baseQuery.driver
|
|
239
|
+
const cohorts = driver.chunkValues(parentIds, (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Counts.
|
|
243
|
+
* @type {Map<string | number, number>} */
|
|
244
|
+
const counts = new Map()
|
|
245
|
+
|
|
246
|
+
for (const cohort of cohorts) {
|
|
247
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
248
|
+
const cohortCounts = await executeCountQuery(cohortQuery)
|
|
249
|
+
|
|
250
|
+
for (const [parentId, count] of cohortCounts) {
|
|
251
|
+
counts.set(parentId, count)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return counts
|
|
256
|
+
}
|
|
257
|
+
|
|
222
258
|
/**
|
|
223
259
|
* Attaches one entry's resolved counts to the loaded models.
|
|
224
260
|
* @param {object} args - Options.
|
|
@@ -165,6 +165,14 @@ export type DatabaseConfigurationType = {
|
|
|
165
165
|
* - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
|
|
166
166
|
*/
|
|
167
167
|
maxInsertSqlBytes?: number;
|
|
168
|
+
/**
|
|
169
|
+
* - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
|
|
170
|
+
*/
|
|
171
|
+
maxInClauseValues?: number;
|
|
172
|
+
/**
|
|
173
|
+
* - Maximum serialized SQL size, in bytes, for a single cohort query used by preloads, association counts, and queryData aggregates. Defaults to 1 MiB (1048576).
|
|
174
|
+
*/
|
|
175
|
+
maxQuerySqlBytes?: number;
|
|
168
176
|
/**
|
|
169
177
|
* - Password for the database user.
|
|
170
178
|
*/
|
|
@@ -1645,6 +1653,8 @@ export type ConfigurationArgsType = {
|
|
|
1645
1653
|
* @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
|
|
1646
1654
|
* @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
|
|
1647
1655
|
* @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
|
|
1656
|
+
* @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
|
|
1657
|
+
* @property {number} [maxQuerySqlBytes] - Maximum serialized SQL size, in bytes, for a single cohort query used by preloads, association counts, and queryData aggregates. Defaults to 1 MiB (1048576).
|
|
1648
1658
|
* @property {string} [password] - Password for the database user.
|
|
1649
1659
|
* @property {number} [port] - Database port.
|
|
1650
1660
|
* @property {string} [primaryKeyType] - Default type for implicit migration primary keys and references. Defaults to `uuid`.
|