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
|
@@ -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.
|