objection 3.7.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 (43) hide show
  1. package/lib/model/AjvValidator.js +101 -44
  2. package/lib/queryBuilder/QueryBuilder.js +91 -33
  3. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  4. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  5. package/lib/queryBuilder/graph/GraphOptions.js +11 -26
  6. package/lib/queryBuilder/graph/GraphUpsert.js +16 -10
  7. package/lib/queryBuilder/join/RelationJoiner.js +104 -12
  8. package/lib/queryBuilder/join/utils.js +2 -2
  9. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  10. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  11. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  12. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  13. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  14. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -1
  15. package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
  16. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  17. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  18. package/lib/queryBuilder/operations/UpdateOperation.js +29 -3
  19. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  20. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  21. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  22. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
  23. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  24. package/lib/queryBuilder/operations/select/Selection.js +21 -20
  25. package/lib/queryBuilder/parsers/relationExpressionParser.js +5 -3
  26. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
  27. package/lib/queryBuilder/whereGroups.js +35 -40
  28. package/lib/queryBuilder/withoutOrder.js +33 -0
  29. package/lib/relations/Relation.js +3 -0
  30. package/lib/relations/RelationFindOperation.js +9 -4
  31. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
  32. package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
  33. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
  34. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
  35. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +87 -17
  36. package/lib/utils/identifierMapping.js +2 -2
  37. package/lib/utils/knexUtils.js +14 -0
  38. package/lib/utils/parseFieldExpression.js +12 -0
  39. package/lib/utils/warnings.js +26 -0
  40. package/lib/utils/wrapDbError.js +34 -20
  41. package/package.json +5 -4
  42. package/typings/objection/index.d.ts +10 -2
  43. 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);
100
122
 
101
- validator.call(model, validationJson);
102
- const error = parseValidationError(validator.errors, modelClass, options, this.ajvOptions);
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
+ }
134
+
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
 
@@ -298,10 +332,11 @@ function parseValidationError(errors, modelClass, options, ajvOptions) {
298
332
  });
299
333
  }
300
334
 
301
- function nestFieldExpressions(json) {
335
+ function nestFieldExpressions(json, queryPropKeys, jsonSchema) {
336
+ const isFieldExpression = (key) => isFieldExpressionKey(key, jsonSchema.properties);
302
337
  const keys = Object.keys(json);
303
338
 
304
- if (!keys.some(isFieldExpression)) {
339
+ if (!keys.some(isFieldExpression) && !queryPropKeys.some(isFieldExpression)) {
305
340
  return null;
306
341
  }
307
342
 
@@ -310,6 +345,15 @@ function nestFieldExpressions(json) {
310
345
  // The objects created here for the nested paths. Other objects are values
311
346
  // of the input json and must not be modified.
312
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
+ };
313
357
 
314
358
  for (const key of keys) {
315
359
  if (!isFieldExpression(key)) {
@@ -320,7 +364,10 @@ function nestFieldExpressions(json) {
320
364
 
321
365
  for (const key of keys) {
322
366
  if (isFieldExpression(key)) {
323
- 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);
324
371
 
325
372
  // Field expressions with array access, and those that conflict with other
326
373
  // keys of the patch, can't be validated without the current value of the
@@ -331,10 +378,16 @@ function nestFieldExpressions(json) {
331
378
  }
332
379
  }
333
380
 
334
- 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] };
335
388
  }
336
389
 
337
- function unnestFieldExpressions(json, { json: nested, paths }) {
390
+ function unnestFieldExpressions(json, nested, { paths, columns }) {
338
391
  for (const [key, path] of paths) {
339
392
  let value = nested;
340
393
  let found = true;
@@ -356,29 +409,35 @@ function unnestFieldExpressions(json, { json: nested, paths }) {
356
409
  json[key] = value;
357
410
  }
358
411
  }
359
- }
360
412
 
361
- function isFieldExpression(key) {
362
- 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
+ }
363
420
  }
364
421
 
365
- 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) {
366
425
  let parsed;
367
426
 
368
427
  try {
369
428
  parsed = parseFieldExpression(key);
370
429
  } catch (err) {
371
- // Not a valid field expression, validate it as a top-level property.
372
- return [key];
430
+ return null;
373
431
  }
374
432
 
433
+ const { column, access } = parsed;
375
434
  // Numeric keys like the `0` in `tags:0` also access array elements when the
376
435
  // json value is an array, so treat them like `[0]`.
377
- if (parsed.access.some((access) => access.type !== 'object' || /^-?\d+$/.test(access.ref))) {
378
- return null;
379
- }
380
-
381
- return [parsed.column, ...parsed.access.map((access) => String(access.ref))];
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
+ };
382
441
  }
383
442
 
384
443
  function setPath(obj, path, value, createdObjects) {
@@ -462,22 +521,20 @@ function objectHasDefaults(obj) {
462
521
  return false;
463
522
  }
464
523
 
465
- // Returns the keys of the query properties of `model` (`raw()`, `val()`,
466
- // `ref()`, subqueries...) that are required by the schema but missing from
467
- // `json`, sorted, or `null` if there are none. Query properties are split from
468
- // the json before validation, but they do provide a value for the property.
469
- 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) {
470
530
  const required = jsonSchema.required;
471
531
 
472
532
  if (!Array.isArray(required) || required.length === 0) {
473
533
  return null;
474
534
  }
475
535
 
476
- const keys = getQueryPropKeys(model).filter(
477
- (key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
478
- );
479
-
480
- 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;
481
538
  }
482
539
 
483
540
  function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
@@ -8,9 +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 { deprecate } = require('../utils/deprecate');
11
+ const { warnOnce } = require('../utils/warnings');
12
+ const { isMsSql } = require('../utils/knexUtils');
12
13
 
13
- const { Selection } = require('./operations/select/Selection');
14
+ const { Selection, isSameIdentifier } = require('./operations/select/Selection');
14
15
 
15
16
  const { QueryBuilderContext } = require('./QueryBuilderContext');
16
17
  const { QueryBuilderBase } = require('./QueryBuilderBase');
@@ -45,9 +46,10 @@ const { EagerOperation } = require('./operations/eager/EagerOperation');
45
46
  const { RangeOperation } = require('./operations/RangeOperation');
46
47
  const { FirstOperation } = require('./operations/FirstOperation');
47
48
  const { NoneOperation } = require('./operations/NoneOperation');
49
+ const { LimitPerParentOperation } = require('./operations/LimitPerParentOperation');
48
50
  const { FromOperation } = require('./operations/FromOperation');
49
51
  const { KnexOperation } = require('./operations/KnexOperation');
50
- const { wrapWhereGroups } = require('./whereGroups');
52
+ const { clearUnsupportedOrder } = require('./withoutOrder');
51
53
 
52
54
  class QueryBuilder extends QueryBuilderBase {
53
55
  static init(self, modelClass) {
@@ -56,6 +58,7 @@ class QueryBuilder extends QueryBuilderBase {
56
58
  self._resultModelClass = null;
57
59
  self._explicitRejectValue = null;
58
60
  self._explicitResolveValue = null;
61
+ self._queryBatch = null;
59
62
  self._modifiers = {};
60
63
 
61
64
  self._allowedGraphExpression = null;
@@ -257,21 +260,29 @@ class QueryBuilder extends QueryBuilderBase {
257
260
  // The `joinOperation` option only applies to the relations of this call,
258
261
  // see `RelationJoiner`. All other options are shared by all calls.
259
262
  const { joinOperation, ...graphOptions } = options;
263
+ const isJoined = eagerOp instanceof JoinEagerOperation;
260
264
 
261
265
  eagerOp.expression = eagerOp.expression.merge(parsedExp);
262
266
  eagerOp.graphOptions = { ...eagerOp.graphOptions, ...graphOptions };
263
267
 
264
268
  if (joinOperation) {
265
- if (eagerOp instanceof JoinEagerOperation) {
269
+ if (isJoined) {
266
270
  eagerOp.joinOperations.push({ expression: parsedExp, joinOperation });
267
271
  } else {
268
- deprecate(
272
+ warnOnce(
269
273
  'The joinOperation option only applies to withGraphJoined() and is ignored for the ' +
270
274
  `fetched relations "${parsedExp.toString()}".`,
271
275
  );
272
276
  }
273
277
  }
274
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
+ );
284
+ }
285
+
275
286
  checkMixedEager(this);
276
287
  checkEager(this);
277
288
  return this;
@@ -387,7 +398,7 @@ class QueryBuilder extends QueryBuilderBase {
387
398
  return this.everyOperation((op) => {
388
399
  if (op.constructor === SelectOperation) {
389
400
  // SelectOperations with zero selections are the ones that only have
390
- // raw items or other non-trivial selections.
401
+ // raw items or other non-trivial selections, or `distinctOn()`.
391
402
  return (
392
403
  op.selections.length > 0 &&
393
404
  op.selections.every((select) => {
@@ -506,34 +517,38 @@ class QueryBuilder extends QueryBuilderBase {
506
517
 
507
518
  if (joinEagerOp) {
508
519
  // `withGraphJoined` needs the table metadata to build its selects.
509
- 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
+ }
510
527
  }
511
528
 
529
+ const idColumns = builder.modelClass().getIdColumnArray();
530
+
512
531
  // Also clear the order on the built knex query, since `orderBy` calls
513
532
  // can be added at build time, e.g. by a relation's `modify`.
514
533
  const buildQuery = (knexBuilder) => builder.toKnexQuery(knexBuilder).clearOrder();
515
534
 
516
535
  const countQuery = knex.count('* as count').from((knexBuilder) => {
517
- if (!joinEagerOp) {
536
+ if (!joinEagerOp || idColumns.length === 0) {
537
+ // Without ids, the joins are all to-one and return one row per root
538
+ // model.
518
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
+ });
519
547
  } else {
520
- // The joins of `withGraphJoined` return one row per joined child.
521
- // Count the distinct root models instead.
522
- const idColumns = builder.modelClass().getIdColumnArray();
523
-
524
- if (refersToSelections(builder)) {
525
- // `having`, and `groupBy` with an alias, may refer to aliased
526
- // selections, so keep them and select the distinct ids from the
527
- // result instead.
528
- knexBuilder.distinct(idColumns).from((knexBuilder) => {
529
- buildQuery(knexBuilder).as('inner');
530
- });
531
- } else {
532
- const tableRef = builder.tableRef();
533
- buildQuery(knexBuilder)
534
- .clearSelect()
535
- .distinct(idColumns.map((col) => `${tableRef}.${col}`));
536
- }
548
+ const tableRef = builder.tableRef();
549
+ buildQuery(knexBuilder)
550
+ .clearSelect()
551
+ .distinct(idColumns.map((col) => `${tableRef}.${col}`));
537
552
  }
538
553
 
539
554
  knexBuilder.as('temp');
@@ -585,7 +600,7 @@ class QueryBuilder extends QueryBuilderBase {
585
600
  let selectionInstance = null;
586
601
 
587
602
  this.forEachOperation(true, (op) => {
588
- if (op.constructor === SelectOperation) {
603
+ if (op.constructor === SelectOperation && !op.isDistinctOn) {
589
604
  selectionInstance = op.findSelection(this, selection);
590
605
  noSelectStatements = false;
591
606
 
@@ -637,9 +652,9 @@ class QueryBuilder extends QueryBuilderBase {
637
652
  } else {
638
653
  if (foundSelection.column === '*') {
639
654
  // * selects the columns with their column names as aliases.
640
- return selection.column === alias;
655
+ return isSameIdentifier(this, selection.column, alias);
641
656
  } else {
642
- return foundSelection.name === alias;
657
+ return isSameIdentifier(this, foundSelection.name, alias);
643
658
  }
644
659
  }
645
660
  }
@@ -1325,14 +1340,14 @@ function checkMixedEager(builder) {
1325
1340
 
1326
1341
  if (joinNode.$allRecursive || fetchNode.$allRecursive) {
1327
1342
  throw new Error(
1328
- 'relation expression `*` cannot be used when mixing withGraphJoined and withGraphFetched',
1343
+ 'relation expression "*" cannot be used when mixing withGraphJoined and withGraphFetched',
1329
1344
  );
1330
1345
  }
1331
1346
 
1332
1347
  for (const childName of joinNode.$childNames) {
1333
1348
  if (fetchNode.$childNames.includes(childName)) {
1334
1349
  throw new Error(
1335
- `relation \`${childName}\` cannot be loaded with both withGraphJoined and withGraphFetched`,
1350
+ `relation "${childName}" cannot be loaded with both withGraphJoined and withGraphFetched`,
1336
1351
  );
1337
1352
  }
1338
1353
  }
@@ -1382,10 +1397,13 @@ function beforeExecute(builder) {
1382
1397
  return promise;
1383
1398
  }
1384
1399
 
1385
- function doExecute(builder) {
1400
+ async function doExecute(builder) {
1386
1401
  let promise = Promise.resolve();
1387
1402
 
1388
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');
1389
1407
  const queryExecutorOperation = findQueryExecutorOperation(builder);
1390
1408
 
1391
1409
  const explicitRejectValue = builder._explicitRejectValue;
@@ -1398,7 +1416,7 @@ function doExecute(builder) {
1398
1416
  } else if (queryExecutorOperation !== null) {
1399
1417
  promise = Promise.resolve(queryExecutorOperation.queryExecutor(builder));
1400
1418
  } else {
1401
- promise = Promise.resolve(buildKnexQuery(builder));
1419
+ promise = executeKnexQuery(builder);
1402
1420
 
1403
1421
  promise = chainOperationHooks(promise, builder, 'onRawResult');
1404
1422
  promise = promise.then((result) => createModels(result, builder));
@@ -1407,6 +1425,30 @@ function doExecute(builder) {
1407
1425
  return promise;
1408
1426
  }
1409
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
+
1410
1452
  function afterExecute(builder, result) {
1411
1453
  let promise = Promise.resolve(result);
1412
1454
 
@@ -1560,7 +1602,13 @@ function callOnBuildHooks(builder) {
1560
1602
  callOnBuildFuncs(builder, builder.internalContext().onBuild);
1561
1603
 
1562
1604
  builder.executeOnBuild();
1563
- 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;
1564
1612
  }
1565
1613
 
1566
1614
  function callOnBuildFuncs(builder, func) {
@@ -1595,6 +1643,12 @@ function buildKnexQuery(builder, knexBuilder = builder.knex().queryBuilder()) {
1595
1643
  }
1596
1644
  }
1597
1645
 
1646
+ const limitPerParentOperation = builder.findOperation(LimitPerParentOperation);
1647
+
1648
+ if (limitPerParentOperation) {
1649
+ knexBuilder = limitPerParentOperation.onAfterBuildKnex(knexBuilder, builder);
1650
+ }
1651
+
1598
1652
  return knexBuilder;
1599
1653
  }
1600
1654
 
@@ -1604,6 +1658,10 @@ function setDefaultTable(builder, knexBuilder) {
1604
1658
 
1605
1659
  if (table === tableRef) {
1606
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);
1607
1665
  } else {
1608
1666
  return knexBuilder.table(`${table} as ${tableRef}`);
1609
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.
@@ -26,9 +26,12 @@ class StaticHookArguments {
26
26
  return () => {
27
27
  // The `runAfter()` callbacks of the query being executed (including the
28
28
  // ones installed by `throwIfNotFound()` and `traverse()`) are meant for
29
- // its result, not for the result of this find query.
29
+ // its result, not for the result of this find query. The same goes for
30
+ // the value it was resolved with, e.g. the 0 of an update that only
31
+ // changes the join rows of a many-to-many relation.
30
32
  return this[BUILDER_SYMBOL].toFindQuery()
31
33
  .clear('runAfter')
34
+ .clearResolve()
32
35
  .clearWithGraphFetched()
33
36
  .runAfter(toArray);
34
37
  };
@@ -17,21 +17,6 @@ const FETCH_STRATEGY = 'fetchStrategy';
17
17
  const ALLOW_REFS = 'allowRefs';
18
18
  const ALLOW_MARKERS = 'allowMarkers';
19
19
 
20
- const OPTION_NAMES = [
21
- NO_RELATE,
22
- NO_UNRELATE,
23
- NO_INSERT,
24
- NO_UPDATE,
25
- NO_DELETE,
26
- UPDATE,
27
- RELATE,
28
- UNRELATE,
29
- INSERT_MISSING,
30
- FETCH_STRATEGY,
31
- ALLOW_REFS,
32
- ALLOW_MARKERS,
33
- ];
34
-
35
20
  // The options that are matched against the relation path of each node, see
36
21
  // `_hasOption()`.
37
22
  const NODE_OPTION_NAMES = [
@@ -46,6 +31,8 @@ const NODE_OPTION_NAMES = [
46
31
  INSERT_MISSING,
47
32
  ];
48
33
 
34
+ const OPTION_NAMES = [...NODE_OPTION_NAMES, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
35
+
49
36
  const FetchStrategy = {
50
37
  OnlyIdentifiers: 'OnlyIdentifiers',
51
38
  Everything: 'Everything',
@@ -55,14 +42,17 @@ const FetchStrategy = {
55
42
  class GraphOptions {
56
43
  // `rootRelationPath` is the relation path of the root node of a recursive
57
44
  // upsert in the original graph, against which the relation paths and
58
- // expressions of the options match.
59
- constructor(options, rootRelationPath = []) {
45
+ // expressions of the options match. `insertOnly` is only set by
46
+ // `insertGraph`, which never fetches, updates or removes existing rows.
47
+ constructor(options, rootRelationPath = [], insertOnly = false) {
60
48
  if (options instanceof GraphOptions) {
61
49
  this.options = options.options;
62
50
  this.rootRelationPath = options.rootRelationPath;
51
+ this.insertOnly = options.insertOnly;
63
52
  } else {
64
53
  this.options = parseRelationExpressions(options);
65
54
  this.rootRelationPath = rootRelationPath;
55
+ this.insertOnly = insertOnly;
66
56
  }
67
57
  }
68
58
 
@@ -79,17 +69,12 @@ class GraphOptions {
79
69
  }
80
70
 
81
71
  isInsertOnly() {
82
- // NO_RELATE is not in the list, since the `insert only` mode does
83
- // relate things that can be related using inserts.
84
- // TODO: Use a special key for this.
85
- return [NO_DELETE, NO_UPDATE, NO_UNRELATE, INSERT_MISSING].every((opt) => {
86
- return this.options[opt] === true;
87
- });
72
+ return this.insertOnly;
88
73
  }
89
74
 
90
75
  // Like `shouldRelate` but ignores settings that explicitly disable relate operations.
91
76
  shouldRelateIgnoreDisable(node, graphData) {
92
- if (node.toBeUnrelated || node.toBeDeleted) {
77
+ if (node.toBeUnrelatedOrDeleted) {
93
78
  return false;
94
79
  }
95
80
 
@@ -113,7 +98,7 @@ class GraphOptions {
113
98
 
114
99
  // Like `shouldInsert` but ignores settings that explicitly disable insert operations.
115
100
  shouldInsertIgnoreDisable(node, graphData) {
116
- if (node.toBeUnrelated || node.toBeDeleted) {
101
+ if (node.toBeUnrelatedOrDeleted) {
117
102
  return false;
118
103
  }
119
104
 
@@ -135,7 +120,7 @@ class GraphOptions {
135
120
  // Like `shouldPatch() || shouldUpdate()` but ignores settings that explicitly disable
136
121
  // update or patch operations.
137
122
  shouldPatchOrUpdateIgnoreDisable(node, graphData) {
138
- if (node.toBeUnrelated || node.toBeDeleted) {
123
+ if (node.toBeUnrelatedOrDeleted) {
139
124
  return false;
140
125
  }
141
126