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.
Files changed (30) hide show
  1. package/README.md +2 -2
  2. package/build/configuration-types.js +2 -0
  3. package/build/database/drivers/base.js +75 -0
  4. package/build/database/query/preloader/belongs-to.js +32 -31
  5. package/build/database/query/preloader/has-many.js +50 -26
  6. package/build/database/query/preloader/has-one.js +20 -12
  7. package/build/database/query/query-data.js +23 -8
  8. package/build/database/query/with-count.js +47 -11
  9. package/build/src/configuration-types.d.ts +10 -0
  10. package/build/src/configuration-types.d.ts.map +1 -1
  11. package/build/src/configuration-types.js +3 -1
  12. package/build/src/database/drivers/base.d.ts +39 -0
  13. package/build/src/database/drivers/base.d.ts.map +1 -1
  14. package/build/src/database/drivers/base.js +70 -1
  15. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  16. package/build/src/database/query/preloader/belongs-to.js +33 -31
  17. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  18. package/build/src/database/query/preloader/has-many.js +48 -28
  19. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  20. package/build/src/database/query/preloader/has-one.js +21 -13
  21. package/build/src/database/query/query-data.js +21 -8
  22. package/build/src/database/query/with-count.js +41 -12
  23. package/package.json +2 -1
  24. package/src/configuration-types.js +2 -0
  25. package/src/database/drivers/base.js +75 -0
  26. package/src/database/query/preloader/belongs-to.js +32 -31
  27. package/src/database/query/preloader/has-many.js +50 -26
  28. package/src/database/query/preloader/has-one.js +20 -12
  29. package/src/database/query/query-data.js +23 -8
  30. 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
- const throughModels = await preloadQueryForModel(modelsToLoad, throughModelClass)
160
- .where({[throughForeignKey]: [...modelsPrimaryKeyValues]})
161
- .toArray()
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 throughModel of throughModels) {
174
- const parentId = /** @type {string | number} */ (throughModel.readColumn(throughForeignKey))
175
- const throughId = /** @type {string | number} */ (throughModel.readColumn(throughModelClass.primaryKey()))
174
+ for (const cohort of throughCohorts) {
175
+ const throughQuery = throughBaseQuery.clone().where({[throughForeignKey]: cohort})
176
+ const throughModels = await throughQuery.toArray()
176
177
 
177
- if (!(parentId in parentToTargetIds)) parentToTargetIds[parentId] = []
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
- parentToTargetIds[parentId].push(throughId)
180
- allTargetIds.add(throughId)
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 query = preloadQueryForModel(modelsToLoad, targetModelClass).where({[targetForeignKey]: [...allTargetIds]})
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
- query = this.relationship.applyScope(query)
193
- query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [targetForeignKey]})
194
- targetModels = await query.toArray()
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
- whereArgs[foreignKey] = [...modelsPrimaryKeyValues]
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
- whereArgs[typeColumn] = this.relationship.getModelClass().getModelName()
314
+ baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
299
315
  }
300
316
 
301
- await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), modelsToLoad[0])
317
+ baseQuery = this.relationship.applyScope(baseQuery)
318
+ baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
302
319
 
303
- let query = preloadQueryForModel(modelsToLoad, targetModelClass).where(whereArgs)
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
- query = this.relationship.applyScope(query)
306
- query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [foreignKey]})
327
+ for (const cohort of cohorts) {
328
+ const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
329
+ const foundTargetModels = await cohortQuery.toArray()
307
330
 
308
- const targetModels = await query.toArray()
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
- whereArgs[foreignKey] = [...modelsPrimaryKeyValues]
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
- whereArgs[typeColumn] = this.relationship.getModelClass().getModelName()
86
+ baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
86
87
  }
87
88
 
88
- await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), this.models[0])
89
+ baseQuery = this.relationship.applyScope(baseQuery)
90
+ baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
89
91
 
90
- // Load target models to be preloaded on the given models
91
- let query = preloadQueryForModel(this.models, targetModelClass).where(whereArgs)
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
- query = this.relationship.applyScope(query)
94
- query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [foreignKey]})
99
+ for (const cohort of cohorts) {
100
+ const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
101
+ const foundTargetModels = await cohortQuery.toArray()
95
102
 
96
- const targetModels = await query.toArray()
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 executeEntryQuery({primaryKey, query, rootModels})
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 countQuery = queryForEntry({entry, modelClass, parentIds, sourceModel})
119
- const sql = countQuery.toSql()
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, {countQuery, entries: [entry]})
125
+ queryGroups.set(sql, {baseQuery, entries: [entry], foreignKey})
126
126
  }
127
127
  }
128
128
 
129
- for (const {countQuery, entries: groupedEntries} of queryGroups.values()) {
130
- const counts = await executeCountQuery(countQuery)
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, parentIds, sourceModel}) {
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 = {[foreignKey]: parentIds}
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
- baseQuery.where(mandatoryWhereConditions)
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.