objection 3.6.0 → 3.8.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.
Files changed (60) hide show
  1. package/lib/model/AjvValidator.js +132 -52
  2. package/lib/model/modelClone.js +0 -2
  3. package/lib/queryBuilder/QueryBuilder.js +128 -24
  4. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  5. package/lib/queryBuilder/RelationExpression.js +6 -4
  6. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  7. package/lib/queryBuilder/graph/GraphOptions.js +36 -79
  8. package/lib/queryBuilder/graph/GraphUpsert.js +35 -7
  9. package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
  10. package/lib/queryBuilder/join/RelationJoiner.js +104 -12
  11. package/lib/queryBuilder/join/utils.js +2 -2
  12. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  13. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  14. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  15. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  16. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  17. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +8 -2
  18. package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
  19. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  20. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  21. package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
  22. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  23. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  24. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  25. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
  26. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  27. package/lib/queryBuilder/operations/select/Selection.js +21 -20
  28. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
  29. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
  30. package/lib/queryBuilder/parsers/relationExpressionParser.js +6 -4
  31. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +7 -3
  32. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
  33. package/lib/queryBuilder/whereGroups.js +35 -40
  34. package/lib/queryBuilder/withoutOrder.js +33 -0
  35. package/lib/relations/Relation.js +4 -0
  36. package/lib/relations/RelationFindOperation.js +30 -43
  37. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
  38. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +65 -19
  39. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
  40. package/lib/relations/manyToMany/ManyToManyRelation.js +15 -16
  41. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
  42. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +9 -5
  43. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
  44. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
  45. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
  46. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
  47. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
  48. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +92 -16
  49. package/lib/utils/identifierMapping.js +10 -19
  50. package/lib/utils/knexUtils.js +14 -0
  51. package/lib/utils/objectUtils.js +38 -6
  52. package/lib/utils/parseFieldExpression.js +22 -3
  53. package/lib/utils/warnings.js +26 -0
  54. package/lib/utils/wrapDbError.js +34 -20
  55. package/package.json +5 -4
  56. package/typings/objection/index.d.ts +54 -26
  57. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
  58. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
  59. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
  60. package/lib/utils/deprecate.js +0 -21
@@ -5,7 +5,7 @@ const addFormats = require('ajv-formats');
5
5
  const { Validator } = require('./Validator');
6
6
  const { ValidationErrorType } = require('../model/ValidationError');
7
7
  const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
8
- const { parseFieldExpression } = require('../utils/parseFieldExpression');
8
+ const { parseFieldExpression, isFieldExpressionKey } = require('../utils/parseFieldExpression');
9
9
  const { getQueryPropKeys } = require('./modelQueryProps');
10
10
 
11
11
  class AjvValidator extends Validator {
@@ -79,27 +79,60 @@ class AjvValidator extends Validator {
79
79
  return json;
80
80
  }
81
81
 
82
- const modelClass = model.constructor;
83
- const validator = this.getValidator(
84
- modelClass,
85
- ctx.jsonSchema,
86
- !!options.patch,
87
- options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
88
- );
89
-
90
82
  // We need to clone the input json if we are about to set default values.
91
83
  if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
92
84
  json = cloneDeep(json);
93
85
  }
94
86
 
95
- // Patch objects can contain field expression keys like `'meta:b'` that update
96
- // a single property inside a json column. Validate them against the nested
97
- // schema by converting them into nested objects like `{ meta: { b: value } }`.
98
- const fieldExpressions = options.patch ? nestFieldExpressions(json) : null;
99
- const validationJson = fieldExpressions ? fieldExpressions.json : json;
87
+ // Patch and update objects can contain field expression keys like `'meta:b'`
88
+ // that update a single property inside a json column. Validate them against
89
+ // the nested schema by converting them into nested objects like
90
+ // `{ meta: { b: value } }`.
91
+ const queryPropKeys = getQueryPropKeys(model);
92
+ const fieldExpressions = nestFieldExpressions(json, queryPropKeys, ctx.jsonSchema);
93
+ let validationJson = fieldExpressions ? fieldExpressions.json : json;
94
+
95
+ const modelClass = model.constructor;
96
+ const validate = (json, isPatchObject, givenKeys = []) => {
97
+ const validator = this.getValidator(
98
+ modelClass,
99
+ ctx.jsonSchema,
100
+ isPatchObject,
101
+ isPatchObject ? null : getRequiredKeysGivenOutside(json, ctx.jsonSchema, givenKeys),
102
+ );
103
+ validator.call(model, json);
104
+ return validator.errors || [];
105
+ };
106
+
107
+ let errors;
108
+
109
+ if (options.patch) {
110
+ errors = validate(validationJson, true);
111
+ } else if (fieldExpressions && fieldExpressions.columns.length > 0) {
112
+ // Field expressions only write parts of their json columns. Validate these
113
+ // parts like patches, and the rest normally, with the columns counted as
114
+ // present like the query properties.
115
+ const { columns } = fieldExpressions;
116
+ const columnsJson = Object.fromEntries(
117
+ columns
118
+ .filter((column) => hasOwn(validationJson, column))
119
+ .map((column) => [column, validationJson[column]]),
120
+ );
121
+ const restJson = omit(validationJson, columns);
122
+
123
+ errors = [
124
+ ...validate(columnsJson, true),
125
+ ...validate(restJson, false, [...queryPropKeys, ...columns]),
126
+ ];
127
+
128
+ // Combine the parts again, without the defaults that Ajv set for the
129
+ // columns in `restJson`.
130
+ validationJson = Object.assign(restJson, columnsJson);
131
+ } else {
132
+ errors = validate(validationJson, false, queryPropKeys);
133
+ }
100
134
 
101
- validator.call(model, validationJson);
102
- const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
135
+ const error = parseValidationError(errors, modelClass, options, this.ajvOptions);
103
136
 
104
137
  if (error) {
105
138
  throw error;
@@ -107,7 +140,7 @@ class AjvValidator extends Validator {
107
140
 
108
141
  if (fieldExpressions) {
109
142
  // Copy back the changes made by Ajv options like `coerceTypes` and `removeAdditional`.
110
- unnestFieldExpressions(json, fieldExpressions);
143
+ unnestFieldExpressions(json, validationJson, fieldExpressions);
111
144
  }
112
145
 
113
146
  return json;
@@ -148,7 +181,8 @@ class AjvValidator extends Validator {
148
181
  normalValidator: null,
149
182
 
150
183
  // Validators created for the schema object without the `required`
151
- // properties that are given as query properties, keyed by their names.
184
+ // properties that are given outside the json, as query properties or
185
+ // json columns written by field expressions, keyed by their names.
152
186
  queryPropValidators: null,
153
187
  };
154
188
 
@@ -215,7 +249,22 @@ class AjvValidator extends Validator {
215
249
  this.setupAjv(this.ajvQueryProps);
216
250
  }
217
251
 
218
- return this.ajvQueryProps.compile(jsonSchema);
252
+ for (;;) {
253
+ try {
254
+ return this.ajvQueryProps.compile(jsonSchema);
255
+ } catch (err) {
256
+ // The schemas of other model classes that `$ref`s point to aren't added
257
+ // to this instance. Copy them over from the normal instance when needed.
258
+ const validator =
259
+ err instanceof Ajv.MissingRefError && this.ajv.getSchema(err.missingSchema);
260
+
261
+ if (!validator) {
262
+ throw err;
263
+ }
264
+
265
+ this.ajvQueryProps.addSchema(validator.schema, err.missingSchema);
266
+ }
267
+ }
219
268
  }
220
269
  }
221
270
 
@@ -283,10 +332,11 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
283
332
  });
284
333
  }
285
334
 
286
- function nestFieldExpressions(json) {
335
+ function nestFieldExpressions(json, queryPropKeys, jsonSchema) {
336
+ const isFieldExpression = (key) => isFieldExpressionKey(key, jsonSchema.properties);
287
337
  const keys = Object.keys(json);
288
338
 
289
- if (!keys.some(isFieldExpression)) {
339
+ if (!keys.some(isFieldExpression) && !queryPropKeys.some(isFieldExpression)) {
290
340
  return null;
291
341
  }
292
342
 
@@ -295,6 +345,15 @@ function nestFieldExpressions(json) {
295
345
  // The objects created here for the nested paths. Other objects are values
296
346
  // of the input json and must not be modified.
297
347
  const createdObjects = new Set();
348
+ // The json columns that are only written partially, by field expressions in
349
+ // the json or the query properties.
350
+ const columns = new Set();
351
+
352
+ const addColumn = (expression) => {
353
+ if (expression && !hasOwn(json, expression.column)) {
354
+ columns.add(expression.column);
355
+ }
356
+ };
298
357
 
299
358
  for (const key of keys) {
300
359
  if (!isFieldExpression(key)) {
@@ -305,7 +364,10 @@ function nestFieldExpressions(json) {
305
364
 
306
365
  for (const key of keys) {
307
366
  if (isFieldExpression(key)) {
308
- const path = fieldExpressionPath(key);
367
+ const expression = parseFieldExpressionKey(key);
368
+ // Not a valid field expression, validate it as a top-level property:
369
+ const path = expression ? expression.path : [key];
370
+ addColumn(expression);
309
371
 
310
372
  // Field expressions with array access, and those that conflict with other
311
373
  // keys of the patch, can't be validated without the current value of the
@@ -316,10 +378,16 @@ function nestFieldExpressions(json) {
316
378
  }
317
379
  }
318
380
 
319
- return { json: nested, paths };
381
+ for (const key of queryPropKeys) {
382
+ if (isFieldExpression(key)) {
383
+ addColumn(parseFieldExpressionKey(key));
384
+ }
385
+ }
386
+
387
+ return { json: nested, paths, columns: [...columns] };
320
388
  }
321
389
 
322
- function unnestFieldExpressions(json, { json: nested, paths }) {
390
+ function unnestFieldExpressions(json, nested, { paths, columns }) {
323
391
  for (const [key, path] of paths) {
324
392
  let value = nested;
325
393
  let found = true;
@@ -341,27 +409,35 @@ function unnestFieldExpressions(json, { json: nested, paths }) {
341
409
  json[key] = value;
342
410
  }
343
411
  }
344
- }
345
412
 
346
- function isFieldExpression(key) {
347
- return key.indexOf(':') !== -1;
413
+ // Copy over the default values that Ajv set for missing properties, but not
414
+ // the json columns written by the field expressions.
415
+ for (const key of Object.keys(nested)) {
416
+ if (!paths.has(key) && !columns.includes(key)) {
417
+ json[key] = nested[key];
418
+ }
419
+ }
348
420
  }
349
421
 
350
- function fieldExpressionPath(key) {
422
+ // Parse a field expression key into its json column and nested path, with the
423
+ // path set to `null` if the key accesses array elements.
424
+ function parseFieldExpressionKey(key) {
351
425
  let parsed;
352
426
 
353
427
  try {
354
428
  parsed = parseFieldExpression(key);
355
429
  } catch (err) {
356
- // Not a valid field expression, validate it as a top-level property.
357
- return [key];
358
- }
359
-
360
- if (parsed.access.some((access) => access.type !== 'object')) {
361
430
  return null;
362
431
  }
363
432
 
364
- return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
433
+ const { column, access } = parsed;
434
+ // Numeric keys like the `0` in `tags:0` also access array elements when the
435
+ // json value is an array, so treat them like `[0]`.
436
+ const isArrayAccess = access.some(({ type, ref }) => type !== 'object' || /^-?\d+$/.test(ref));
437
+ return {
438
+ column,
439
+ path: isArrayAccess ? null : [column, ...access.map(({ ref }) => String(ref))],
440
+ };
365
441
  }
366
442
 
367
443
  function setPath(obj, path, value, createdObjects) {
@@ -445,22 +521,20 @@ function objectHasDefaults(obj) {
445
521
  return false;
446
522
  }
447
523
 
448
- // Returns the keys of the query properties of `model` (`raw()`, `val()`,
449
- // `ref()`, subqueries...) that are required by the schema but missing from
450
- // `json`, sorted, or `null` if there are none. Query properties are split from
451
- // the json before validation, but they do provide a value for the property.
452
- function getMissingQueryPropKeys(model, json, jsonSchema) {
524
+ // Returns the `keys` that are required by the schema but given outside of
525
+ // `json`, sorted, or `null` if there are none. Query properties (`raw()`,
526
+ // `val()`, `ref()`, subqueries...) are split from the json before validation,
527
+ // but they do provide a value for the property, and so do field expressions
528
+ // for their json columns.
529
+ function getRequiredKeysGivenOutside(json, jsonSchema, keys) {
453
530
  const required = jsonSchema.required;
454
531
 
455
532
  if (!Array.isArray(required) || required.length === 0) {
456
533
  return null;
457
534
  }
458
535
 
459
- const keys = getQueryPropKeys(model).filter(
460
- (key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
461
- );
462
-
463
- return keys.length > 0 ? keys.sort() : null;
536
+ const givenKeys = required.filter((key) => keys.includes(key) && !hasOwn(json, key));
537
+ return givenKeys.length > 0 ? givenKeys.sort() : null;
464
538
  }
465
539
 
466
540
  function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
@@ -470,6 +544,11 @@ function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
470
544
  }
471
545
 
472
546
  function jsonSchemaWithoutRequired(jsonSchema) {
547
+ // Boolean schemas (`true` / `false`) have no `required` to strip.
548
+ if (!isObject(jsonSchema)) {
549
+ return jsonSchema;
550
+ }
551
+
473
552
  // `not` is intentionally left untouched: Stripping `required` inside it
474
553
  // would turn it into a schema that rejects everything (e.g.
475
554
  // `not: { required: ['a'] }` -> `not: {}`). Kept as is, it still holds for
@@ -506,17 +585,18 @@ function definitionsWithoutRequired(jsonSchema, prop) {
506
585
  }
507
586
 
508
587
  function subSchemaWithoutRequired(jsonSchema, prop) {
509
- if (jsonSchema[prop]) {
588
+ if (jsonSchema[prop] !== undefined) {
510
589
  if (Array.isArray(jsonSchema[prop])) {
511
590
  const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
512
- const nonEmptySchemaArray = schemaArray.filter(isNotEmptyObject);
591
+ const nonEmptySchemaArray = schemaArray.filter((schema) => !matchesAnything(schema));
513
592
 
514
593
  // An `anyOf` or `oneOf` option that only consisted of `required` is
515
594
  // empty once `required` is stripped, and an empty schema matches any
516
- // patch, just like the stripped `required` keywords elsewhere. Drop the
517
- // whole keyword then: requiring the patch to match one of the remaining
518
- // options would reject valid patches, and with `oneOf` keeping the empty
519
- // option would fail every patch that also matches another option.
595
+ // patch, just like the stripped `required` keywords elsewhere and the
596
+ // `true` schema. Drop the whole keyword then: requiring the patch to
597
+ // match one of the remaining options would reject valid patches, and
598
+ // with `oneOf` keeping the empty option would fail every patch that also
599
+ // matches another option.
520
600
  if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
521
601
  return {};
522
602
  } else if (nonEmptySchemaArray.length !== 0) {
@@ -548,8 +628,8 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
548
628
  }
549
629
  }
550
630
 
551
- function isNotEmptyObject(obj) {
552
- return Object.keys(obj).length !== 0;
631
+ function matchesAnything(schema) {
632
+ return schema === true || (isObject(schema) && Object.keys(schema).length === 0);
553
633
  }
554
634
 
555
635
  module.exports = {
@@ -81,8 +81,6 @@ function cloneObject(value) {
81
81
  return cloneArray(value);
82
82
  } else if (value.$isObjectionModel) {
83
83
  return clone(value, false, false);
84
- } else if (Buffer.isBuffer(value)) {
85
- return new Buffer(value);
86
84
  } else {
87
85
  return cloneDeep(value);
88
86
  }
@@ -8,8 +8,10 @@ const { ValidationErrorType } = require('../model/ValidationError');
8
8
  const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
9
9
  const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
10
10
  const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
11
+ const { warnOnce } = require('../utils/warnings');
12
+ const { isMsSql } = require('../utils/knexUtils');
11
13
 
12
- const { Selection } = require('./operations/select/Selection');
14
+ const { Selection, isSameIdentifier } = require('./operations/select/Selection');
13
15
 
14
16
  const { QueryBuilderContext } = require('./QueryBuilderContext');
15
17
  const { QueryBuilderBase } = require('./QueryBuilderBase');
@@ -44,9 +46,10 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
44
46
  const { RangeOperation } = require('./operations/RangeOperation');
45
47
  const { FirstOperation } = require('./operations/FirstOperation');
46
48
  const { NoneOperation } = require('./operations/NoneOperation');
49
+ const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
47
50
  const { FromOperation } = require('./operations/FromOperation');
48
51
  const { KnexOperation } = require('./operations/KnexOperation');
49
- const { wrapWhereGroups } = require('./whereGroups');
52
+ const { clearUnsupportedOrder } = require('./withoutOrder');
50
53
 
51
54
  class QueryBuilder extends QueryBuilderBase {
52
55
  static init(self, modelClass) {
@@ -55,6 +58,7 @@ class QueryBuilder extends QueryBuilderBase {
55
58
  self._resultModelClass = null;
56
59
  self._explicitRejectValue = null;
57
60
  self._explicitResolveValue = null;
61
+ self._queryBatch = null;
58
62
  self._modifiers = {};
59
63
 
60
64
  self._allowedGraphExpression = null;
@@ -176,7 +180,7 @@ class QueryBuilder extends QueryBuilderBase {
176
180
  }
177
181
 
178
182
  isExplicitlyResolvedOrRejected() {
179
- return !!(this._explicitRejectValue || this._explicitResolveValue);
183
+ return this._explicitRejectValue !== null || this._explicitResolveValue !== null;
180
184
  }
181
185
 
182
186
  isExecutable() {
@@ -256,12 +260,27 @@ class QueryBuilder extends QueryBuilderBase {
256
260
  // The `joinOperation` option only applies to the relations of this call,
257
261
  // see `RelationJoiner`. All other options are shared by all calls.
258
262
  const { joinOperation, ...graphOptions } = options;
263
+ const isJoined = eagerOp instanceof JoinEagerOperation;
259
264
 
260
265
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
261
266
  eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
262
267
 
263
268
  if (joinOperation) {
264
- eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
269
+ if (isJoined) {
270
+ eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
271
+ } else {
272
+ warnOnce(
273
+ 'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
274
+ `fetched relations "${parsedExp.toString()}".`,
275
+ );
276
+ }
277
+ }
278
+
279
+ if (graphOptions.limitPerParent && isJoined) {
280
+ warnOnce(
281
+ 'The limitPerParent option only applies to withGraphFetched() and is ignored for the ' +
282
+ `joined relations "${parsedExp.toString()}".`,
283
+ );
265
284
  }
266
285
 
267
286
  checkMixedEager(this);
@@ -379,7 +398,7 @@ class QueryBuilder extends QueryBuilderBase {
379
398
  return this.everyOperation((op) => {
380
399
  if (op.constructor === SelectOperation) {
381
400
  // SelectOperations with zero selections are the ones that only have
382
- // raw items or other non-trivial selections.
401
+ // raw items or other non-trivial selections, or `distinctOn()`.
383
402
  return (
384
403
  op.selections.length > 0 &&
385
404
  op.selections.every((select) => {
@@ -498,20 +517,38 @@ class QueryBuilder extends QueryBuilderBase {
498
517
 
499
518
  if (joinEagerOp) {
500
519
  // `withGraphJoined` needs the table metadata to build its selects.
501
- await joinEagerOp.onBefore3(builder);
520
+ await joinEagerOp.fetchTableMetadata(builder);
521
+
522
+ // To-many joins return one row per joined child, so the root models are
523
+ // counted by their distinct ids, see below.
524
+ if (joinEagerOp.joiner.hasToManyJoins(builder)) {
525
+ assertHasIdColumn(builder.modelClass());
526
+ }
502
527
  }
503
528
 
504
- const countQuery = knex.count('* as count').from((knexBuilder) => {
505
- // Also clear the order on the built knex query, since `orderBy` calls
506
- // can be added at build time, e.g. by a relation's `modify`.
507
- knexBuilder = builder.toKnexQuery(knexBuilder).clearOrder();
529
+ const idColumns = builder.modelClass().getIdColumnArray();
530
+
531
+ // Also clear the order on the built knex query, since `orderBy` calls
532
+ // can be added at build time, e.g. by a relation's `modify`.
533
+ const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
508
534
 
509
- if (joinEagerOp) {
510
- // The joins of `withGraphJoined` return one row per joined child.
511
- // Count the distinct root models instead.
535
+ const countQuery = knex.count('* as count').from((knexBuilder) => {
536
+ if (!joinEagerOp || idColumns.length === 0) {
537
+ // Without ids, the joins are all to-one and return one row per root
538
+ // model.
539
+ buildQuery(knexBuilder);
540
+ } else if (refersToSelections(builder)) {
541
+ // `having`, and `groupBy` with an alias, may refer to aliased
542
+ // selections, so keep them and select the distinct ids from the
543
+ // result instead.
544
+ knexBuilder.distinct(idColumns).from((knexBuilder) => {
545
+ buildQuery(knexBuilder).as('inner');
546
+ });
547
+ } else {
512
548
  const tableRef = builder.tableRef();
513
- const idColumns = builder.modelClass().getIdColumnArray();
514
- knexBuilder.clearSelect().distinct(idColumns.map((col) => `${tableRef}.${col}`));
549
+ buildQuery(knexBuilder)
550
+ .clearSelect()
551
+ .distinct(idColumns.map((col) => `${tableRef}.${col}`));
515
552
  }
516
553
 
517
554
  knexBuilder.as('temp');
@@ -563,7 +600,7 @@ class QueryBuilder extends QueryBuilderBase {
563
600
  let selectionInstance = null;
564
601
 
565
602
  this.forEachOperation(true, (op) => {
566
- if (op.constructor === SelectOperation) {
603
+ if (op.constructor === SelectOperation && !op.isDistinctOn) {
567
604
  selectionInstance = op.findSelection(this, selection);
568
605
  noSelectStatements = false;
569
606
 
@@ -615,9 +652,9 @@ class QueryBuilder extends QueryBuilderBase {
615
652
  } else {
616
653
  if (foundSelection.column === '*') {
617
654
  // * selects the columns with their column names as aliases.
618
- return selection.column === alias;
655
+ return isSameIdentifier(this, selection.column, alias);
619
656
  } else {
620
- return foundSelection.name === alias;
657
+ return isSameIdentifier(this, foundSelection.name, alias);
621
658
  }
622
659
  }
623
660
  }
@@ -1171,6 +1208,29 @@ function getEagerAlgorithmByName(builder, name) {
1171
1208
  }
1172
1209
  }
1173
1210
 
1211
+ // Returns true if the `having` or `groupBy` clauses of `builder` may refer to
1212
+ // aliased selections (#2329).
1213
+ function refersToSelections(builder) {
1214
+ if (builder.has(/having/)) {
1215
+ return true;
1216
+ }
1217
+
1218
+ const aliases = builder
1219
+ .findAllSelections()
1220
+ .map((selection) => selection.alias)
1221
+ .filter((alias) => !!alias);
1222
+
1223
+ let refersToAlias = false;
1224
+
1225
+ builder.forEachOperation(/^groupBy$/, (op) => {
1226
+ if (flatten(op.args).some((arg) => aliases.includes(arg))) {
1227
+ refersToAlias = true;
1228
+ }
1229
+ });
1230
+
1231
+ return refersToAlias;
1232
+ }
1233
+
1174
1234
  function getEagerAlgorithmOfOperation(builder, eagerOp) {
1175
1235
  if (eagerOp instanceof JoinEagerOperation) {
1176
1236
  return getJoinEagerAlgorithm(builder);
@@ -1198,7 +1258,8 @@ function groupByEagerAlgorithm(builder, expression) {
1198
1258
 
1199
1259
  for (const childName of node.$childNames) {
1200
1260
  const eagerOp = findEagerOperations(builder).find(
1201
- (op) => op.expression.node.$allRecursive || op.expression.node[childName],
1261
+ (op) =>
1262
+ op.expression.node.$allRecursive || op.expression.node.$childNames.includes(childName),
1202
1263
  );
1203
1264
 
1204
1265
  const algorithm = eagerOp ? getEagerAlgorithmOfOperation(builder, eagerOp) : defaultAlgorithm;
@@ -1279,14 +1340,14 @@ function checkMixedEager(builder) {
1279
1340
 
1280
1341
  if (joinNode.$allRecursive || fetchNode.$allRecursive) {
1281
1342
  throw new Error(
1282
- 'relation expression `*` cannot be used when mixing withGraphJoined and withGraphFetched',
1343
+ 'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
1283
1344
  );
1284
1345
  }
1285
1346
 
1286
1347
  for (const childName of joinNode.$childNames) {
1287
1348
  if (fetchNode.$childNames.includes(childName)) {
1288
1349
  throw new Error(
1289
- `relation \`${childName}\` cannot be loaded with both withGraphJoined and withGraphFetched`,
1350
+ `relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
1290
1351
  );
1291
1352
  }
1292
1353
  }
@@ -1336,10 +1397,13 @@ function beforeExecute(builder) {
1336
1397
  return promise;
1337
1398
  }
1338
1399
 
1339
- function doExecute(builder) {
1400
+ async function doExecute(builder) {
1340
1401
  let promise = Promise.resolve();
1341
1402
 
1342
1403
  builder = callOnBuildHooks(builder);
1404
+ // Unlike `onBuild`, `onAfterBuild` can be async, as it only runs when the
1405
+ // query is executed, not when it's only built, e.g. with `toKnexQuery()`:
1406
+ await chainOperationHooks(promise, builder, 'onAfterBuild');
1343
1407
  const queryExecutorOperation = findQueryExecutorOperation(builder);
1344
1408
 
1345
1409
  const explicitRejectValue = builder._explicitRejectValue;
@@ -1352,7 +1416,7 @@ function doExecute(builder) {
1352
1416
  } else if (queryExecutorOperation !== null) {
1353
1417
  promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
1354
1418
  } else {
1355
- promise = Promise.resolve(buildKnexQuery(builder));
1419
+ promise = executeKnexQuery(builder);
1356
1420
 
1357
1421
  promise = chainOperationHooks(promise, builder, 'onRawResult');
1358
1422
  promise = promise.then((result) => createModels(result, builder));
@@ -1361,6 +1425,30 @@ function doExecute(builder) {
1361
1425
  return promise;
1362
1426
  }
1363
1427
 
1428
+ // An operation can split the query into batches, e.g. to stay below the number
1429
+ // of values a database accepts in one statement. Build and execute the knex
1430
+ // query once per batch then, with the operation reading the current batch from
1431
+ // `builder._queryBatch`, and merge the counts or rows of all batches.
1432
+ async function executeKnexQuery(builder) {
1433
+ const operation = builder.findOperation((op) => op.hasQueryBatches());
1434
+ const batches = operation?.queryBatches(builder);
1435
+
1436
+ if (batches) {
1437
+ const results = [];
1438
+
1439
+ for (const batch of batches) {
1440
+ builder._queryBatch = batch;
1441
+ results.push(await buildKnexQuery(builder));
1442
+ }
1443
+
1444
+ return results.reduce((merged, result) =>
1445
+ Array.isArray(merged) ? merged.concat(result) : merged + result,
1446
+ );
1447
+ } else {
1448
+ return buildKnexQuery(builder);
1449
+ }
1450
+ }
1451
+
1364
1452
  function afterExecute(builder, result) {
1365
1453
  let promise = Promise.resolve(result);
1366
1454
 
@@ -1514,7 +1602,13 @@ function callOnBuildHooks(builder) {
1514
1602
  callOnBuildFuncs(builder, builder.internalContext().onBuild);
1515
1603
 
1516
1604
  builder.executeOnBuild();
1517
- return wrapWhereGroups(builder);
1605
+
1606
+ // Clear the order after all hooks, as any of them may add one.
1607
+ if (builder.isUpdate()) {
1608
+ clearUnsupportedOrder(builder);
1609
+ }
1610
+
1611
+ return builder;
1518
1612
  }
1519
1613
 
1520
1614
  function callOnBuildFuncs(builder, func) {
@@ -1549,6 +1643,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
1549
1643
  }
1550
1644
  }
1551
1645
 
1646
+ const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
1647
+
1648
+ if (limitPerParentOperation) {
1649
+ knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
1650
+ }
1651
+
1552
1652
  return knexBuilder;
1553
1653
  }
1554
1654
 
@@ -1558,6 +1658,10 @@ function setDefaultTable(builder, knexBuilder) {
1558
1658
 
1559
1659
  if (table === tableRef) {
1560
1660
  return knexBuilder.table(table);
1661
+ } else if (isMsSql(builder.knex()) && (builder.isUpdate() || builder.isDelete())) {
1662
+ // SQL Server can't alias the table of updates and deletes, but it can modify
1663
+ // it through a common table expression named like the alias.
1664
+ return knexBuilder.with(tableRef, (query) => query.select('*').from(table)).table(tableRef);
1561
1665
  } else {
1562
1666
  return knexBuilder.table(`${table} as ${tableRef}`);
1563
1667
  }
@@ -3,7 +3,8 @@
3
3
  const { isString, isFunction, isRegExp, mergeMaps, last } = require('../utils/objectUtils');
4
4
  const { QueryBuilderContextBase } = require('./QueryBuilderContextBase');
5
5
  const { QueryBuilderUserContext } = require('./QueryBuilderUserContext');
6
- const { deprecate } = require('../utils/deprecate');
6
+ const { deprecate } = require('../utils/warnings');
7
+ const { getWhereGroupOperations } = require('./whereGroups');
7
8
 
8
9
  const AllSelector = () => true;
9
10
  const SelectSelector =
@@ -489,8 +490,19 @@ class QueryBuilderOperationSupport {
489
490
  }
490
491
 
491
492
  executeOnBuildKnex(knexBuilder) {
492
- this.forEachOperation(true, (op) => {
493
- if (op.hasOnBuildKnex()) {
493
+ const whereGroupOps = getWhereGroupOperations(this);
494
+
495
+ // Visit the operations while building, so that operations added by
496
+ // `onBuildKnex` hooks are built too. Build each where group in place of its
497
+ // first operation, and skip the operations that the groups build.
498
+ this.forEachOperation(true, (it) => {
499
+ const op = whereGroupOps.has(it)
500
+ ? whereGroupOps.get(it)
501
+ : it.isAncestorInSet(whereGroupOps)
502
+ ? null
503
+ : it;
504
+
505
+ if (op?.hasOnBuildKnex()) {
494
506
  const newKnexBuilder = this.callOperationMethod(op, 'onBuildKnex', [knexBuilder, this]);
495
507
  // Default to the input knex builder for backwards compatibility
496
508
  // with QueryBuilder.onBuildKnex hooks.
@@ -129,7 +129,8 @@ class RelationExpression {
129
129
  return new RelationExpression(this.node, this.recursionDepth + 1);
130
130
  }
131
131
 
132
- const child = this.node[childName];
132
+ // Don't mistake inherited properties like `constructor` for children.
133
+ const child = this.node.$childNames.includes(childName) ? this.node[childName] : null;
133
134
 
134
135
  if (child) {
135
136
  return new RelationExpression(child);
@@ -292,7 +293,7 @@ function modelToNode(model, node) {
292
293
  const relName = relationNames[r];
293
294
 
294
295
  if (model[relName] !== undefined) {
295
- let childNode = node[relName];
296
+ let childNode = node.$childNames.includes(relName) ? node[relName] : null;
296
297
 
297
298
  if (!childNode) {
298
299
  childNode = newNode(relName);
@@ -402,8 +403,9 @@ function mergeNodes(node1, node2) {
402
403
  node.$childNames = union(node1.$childNames, node2.$childNames);
403
404
 
404
405
  for (const childName of node.$childNames) {
405
- const child1 = node1[childName];
406
- const child2 = node2[childName];
406
+ // Don't mistake inherited properties like `constructor` for children.
407
+ const child1 = node1.$childNames.includes(childName) ? node1[childName] : null;
408
+ const child2 = node2.$childNames.includes(childName) ? node2[childName] : null;
407
409
 
408
410
  if (child1 && child2) {
409
411
  node[childName] = mergeNodes(child1, child2);