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
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const { ref } = require('../../queryBuilder/ReferenceBuilder');
4
- const { isEmpty } = require('../../utils/objectUtils');
5
- const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
4
+ const { isEmpty, isObject } = require('../../utils/objectUtils');
5
+ const {
6
+ isFieldExpressionKey,
7
+ toPostgresJsonPathLiteral,
8
+ } = require('../../utils/parseFieldExpression');
6
9
  const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
7
10
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
8
11
  const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
@@ -107,12 +110,13 @@ async function callStaticAfterUpdate(builder, result) {
107
110
 
108
111
  function convertFieldExpressionsToRaw(builder, model, json) {
109
112
  const knex = builder.knex();
113
+ const declaredColumns = getDeclaredColumns(builder.modelClass());
110
114
  const convertedJson = {};
111
115
 
112
116
  for (const key of Object.keys(json)) {
113
117
  let val = json[key];
114
118
 
115
- if (key.indexOf(':') > -1) {
119
+ if (isFieldExpressionKey(key, declaredColumns)) {
116
120
  // 'col:attr' : ref('other:lol') is transformed to
117
121
  // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
118
122
  warnIfNotPostgres(knex);
@@ -141,6 +145,28 @@ function convertFieldExpressionsToRaw(builder, model, json) {
141
145
  return convertedJson;
142
146
  }
143
147
 
148
+ const declaredColumnsByModelClass = new WeakMap();
149
+
150
+ // The keys of `json` are column names, so map the properties with a ':' that the
151
+ // schema declares to their columns, once per model class. Properties that only
152
+ // the schema returned by `$beforeValidate()` declares aren't known here.
153
+ function getDeclaredColumns(modelClass) {
154
+ let declaredColumns = declaredColumnsByModelClass.get(modelClass);
155
+
156
+ if (!declaredColumns) {
157
+ const jsonSchema = modelClass.getJsonSchema();
158
+ const properties = jsonSchema && jsonSchema.properties;
159
+ declaredColumns = Object.fromEntries(
160
+ (isObject(properties) ? Object.keys(properties) : [])
161
+ .filter((key) => key.includes(':'))
162
+ .map((key) => [modelClass.propertyNameToColumnName(key), true]),
163
+ );
164
+ declaredColumnsByModelClass.set(modelClass, declaredColumns);
165
+ }
166
+
167
+ return declaredColumns;
168
+ }
169
+
144
170
  module.exports = {
145
171
  UpdateOperation,
146
172
  convertFieldExpressionsToRaw,
@@ -3,11 +3,11 @@
3
3
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
4
 
5
5
  // Builds a set of where operations inside a single knex `where(callback)`
6
- // group, i.e. wraps them in parentheses. See `wrapWhereGroups`.
6
+ // group, i.e. wraps them in parentheses. See `getWhereGroupOperations()`.
7
7
  class WhereGroupOperation extends QueryBuilderOperation {
8
8
  constructor(name, opt) {
9
9
  super(name, opt);
10
- this.operations = [];
10
+ this.operations = opt.operations;
11
11
  }
12
12
 
13
13
  onBuildKnex(knexBuilder, builder) {
@@ -28,12 +28,6 @@ class WhereGroupOperation extends QueryBuilderOperation {
28
28
  }
29
29
  });
30
30
  }
31
-
32
- clone() {
33
- const clone = super.clone();
34
- clone.operations = this.operations.map((op) => op.clone());
35
- return clone;
36
- }
37
31
  }
38
32
 
39
33
  module.exports = {
@@ -20,6 +20,11 @@ class JoinEagerOperation extends EagerOperation {
20
20
  }
21
21
 
22
22
  onBefore3(builder) {
23
+ return this.fetchTableMetadata(builder);
24
+ }
25
+
26
+ // Also called by `resultSize()`, which builds the query without executing it.
27
+ fetchTableMetadata(builder) {
23
28
  return this.joiner
24
29
  .setExpression(this.buildFinalExpression(builder))
25
30
  .setModifiers(this.buildFinalModifiers(builder))
@@ -3,7 +3,8 @@
3
3
  const promiseUtils = require('../../../utils/promiseUtils');
4
4
 
5
5
  const { EagerOperation } = require('./EagerOperation');
6
- const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
6
+ const { LimitPerParentOperation } = require('../LimitPerParentOperation');
7
+ const { isMsSql, isOracle, isSqlite, hasWindowFunctions } = require('../../../utils/knexUtils');
7
8
  const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
8
9
  const { ValidationErrorType } = require('../../../model/ValidationError');
9
10
  const { createModifier } = require('../../../utils/createModifier');
@@ -85,7 +86,9 @@ class WhereInEagerOperation extends EagerOperation {
85
86
 
86
87
  async fetchRelation(builder, models, relation, expr) {
87
88
  const modelClass = builder.resultModelClass();
88
- const batchSize = this.batchSize(builder.knex());
89
+ const batchSize = this.shouldFetchPerParent(builder, models, relation, expr)
90
+ ? 1
91
+ : this.batchSize(builder.knex());
89
92
  const modelBatches = chunk(models, batchSize);
90
93
 
91
94
  const result = await promiseUtils.map(
@@ -136,9 +139,32 @@ class WhereInEagerOperation extends EagerOperation {
136
139
  }
137
140
  }
138
141
 
142
+ if (this.graphOptions.limitPerParent) {
143
+ queryBuilder.addOperation(
144
+ new LimitPerParentOperation('limitPerParent', { findOperation }),
145
+ [],
146
+ );
147
+ }
148
+
139
149
  return queryBuilder;
140
150
  }
141
151
 
152
+ // Without window functions, the related models can only be limited per
153
+ // parent with one query per parent.
154
+ shouldFetchPerParent(builder, models, relation, expr) {
155
+ if (this.graphOptions.limitPerParent && !hasWindowFunctions(builder.knex())) {
156
+ // Build the query to find its limits, also those of the relation's
157
+ // `modify` and of `onBuildKnex()` hooks. This runs its modifiers and
158
+ // build hooks once more than for fetching the relation.
159
+ const query = this.fetchRelationBatch(builder, models, relation, expr);
160
+ query.executeOnBuild();
161
+ const knexQuery = query.executeOnBuildKnex(query.knex().queryBuilder());
162
+ return !!query.findOperation(LimitPerParentOperation)?.getRange(knexQuery, query);
163
+ } else {
164
+ return false;
165
+ }
166
+ }
167
+
142
168
  shouldSkipFetched(models, relation, expr) {
143
169
  if (!this.graphOptions.skipFetched) {
144
170
  return false;
@@ -3,7 +3,7 @@
3
3
  const parser = require('../../../utils/parseFieldExpression');
4
4
  const { asArray, isObject, isString } = require('../../../utils/objectUtils');
5
5
  const { isKnexRaw, isKnexQueryBuilder, isPostgres } = require('../../../utils/knexUtils');
6
- const { deprecate } = require('../../../utils/deprecate');
6
+ const { warnOnce } = require('../../../utils/warnings');
7
7
 
8
8
  /**
9
9
  * @typedef {String} FieldExpression
@@ -50,7 +50,7 @@ const { deprecate } = require('../../../utils/deprecate');
50
50
  // TODO: Consider throwing an error instead in objection 4.0.
51
51
  function warnIfNotPostgres(knex) {
52
52
  if (!isPostgres(knex)) {
53
- deprecate(
53
+ warnOnce(
54
54
  'JSON field expressions and the whereJson*() methods of objection are only supported ' +
55
55
  'on PostgreSQL. Use the JSON methods of knex like whereJsonPath() or jsonExtract() ' +
56
56
  'with other databases.',
@@ -25,6 +25,8 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
25
25
  // The selections that aggregates select, by their aliases. `null` for
26
26
  // non-aggregate operations.
27
27
  this.aggregateSelections = AGGREGATE_METHODS.has(name) ? [] : null;
28
+ // `distinctOn()` makes the rows distinct on its columns, it doesn't select them.
29
+ this.isDistinctOn = name === 'distinctOn';
28
30
  }
29
31
 
30
32
  onAdd(builder, args) {
@@ -38,11 +40,13 @@ class SelectOperation extends ObjectionToKnexConvertingOperation {
38
40
 
39
41
  const ret = super.onAdd(builder, selections);
40
42
 
41
- for (const selection of selections) {
42
- const selectionInstance = Selection.create(selection);
43
+ if (!this.isDistinctOn) {
44
+ for (const selection of selections) {
45
+ const selectionInstance = Selection.create(selection);
43
46
 
44
- if (selectionInstance) {
45
- this.selections.push(selectionInstance);
47
+ if (selectionInstance) {
48
+ this.selections.push(selectionInstance);
49
+ }
46
50
  }
47
51
  }
48
52
 
@@ -63,11 +63,11 @@ class Selection {
63
63
  if (selectionInBuilder.column === '*') {
64
64
  if (selectionInBuilder.table) {
65
65
  if (selectionToTest.column === '*') {
66
- return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
66
+ return isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table);
67
67
  } else {
68
68
  return (
69
69
  selectionToTest.table === null ||
70
- isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
70
+ isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table)
71
71
  );
72
72
  }
73
73
  } else {
@@ -80,9 +80,9 @@ class Selection {
80
80
  return false;
81
81
  } else {
82
82
  return (
83
- selectionToTest.column === selectionInBuilder.column &&
83
+ isSameIdentifier(builder, selectionToTest.column, selectionInBuilder.column) &&
84
84
  (selectionToTest.table === null ||
85
- isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
85
+ isSameIdentifier(builder, selectionToTest.table, selectionInBuilderTable))
86
86
  );
87
87
  }
88
88
  }
@@ -97,30 +97,29 @@ Object.defineProperties(Selection.prototype, {
97
97
  },
98
98
  });
99
99
 
100
- // Table names that only differ in a way that knex's `wrapIdentifier` mapping
101
- // (for example `knexSnakeCaseMappers()`) cancels out refer to the same table:
100
+ // Identifiers that only differ in a way that knex's `wrapIdentifier` mapping
101
+ // (for example `knexSnakeCaseMappers()`) cancels out are the same identifier:
102
102
  // `dataExtension` and `data_extension` both end up as `data_extension`.
103
- function isSameTable(builder, table1, table2) {
104
- if (table1 === table2) {
103
+ function isSameIdentifier(builder, identifier1, identifier2) {
104
+ if (identifier1 === identifier2) {
105
105
  return true;
106
- } else if (!table1 || !table2) {
106
+ } else if (!identifier1 || !identifier2) {
107
107
  return false;
108
+ } else {
109
+ return mapIdentifier(builder, identifier1) === mapIdentifier(builder, identifier2);
108
110
  }
111
+ }
109
112
 
113
+ function mapIdentifier(builder, identifier) {
110
114
  const knex = builder.unsafeKnex();
111
115
  const wrapIdentifier = knex && knex.client && knex.client.config.wrapIdentifier;
112
116
 
113
- if (!wrapIdentifier) {
114
- return false;
115
- }
116
-
117
- const mapTable = (table) =>
118
- table
119
- .split('.')
120
- .map((part) => wrapIdentifier(part, identity))
121
- .join('.');
122
-
123
- return mapTable(table1) === mapTable(table2);
117
+ return wrapIdentifier
118
+ ? identifier
119
+ .split('.')
120
+ .map((part) => wrapIdentifier(part, identity))
121
+ .join('.')
122
+ : identifier;
124
123
  }
125
124
 
126
125
  function identity(value) {
@@ -211,4 +210,6 @@ function createSelectionFromString(selection) {
211
210
 
212
211
  module.exports = {
213
212
  Selection,
213
+ isSameIdentifier,
214
+ mapIdentifier,
214
215
  };
@@ -4,6 +4,8 @@
4
4
 
5
5
  'use strict';
6
6
 
7
+ const { warnOnce } = require('../../utils/warnings');
8
+
7
9
  class peg$SyntaxError extends SyntaxError {
8
10
  constructor(message, expected, found, location) {
9
11
  super(message);
@@ -944,11 +946,11 @@ function peg$parse(input, options) {
944
946
 
945
947
  function assertDuplicateRelation(node, expr) {
946
948
  if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
947
- console.warn(
948
- `Duplicate relation "${expr.$name}" in a relation expression. You should use "a.[b, c]" instead of "[a.b, a.c]". This will cause an error in objection 2.0`,
949
+ warnOnce(
950
+ `Duplicate relation "${expr.$name}" in a relation expression, only the last one is used. Use "a.[b, c]" instead of "[a.b, a.c]". This will throw an error in objection 4.0.`,
949
951
  );
950
952
 
951
- // TODO: enable for v2.0.
953
+ // Throws in objection 4.0, see ditojs/objection#53.
952
954
  // const err = new Error();
953
955
  // err.duplicateRelationName = expr.$name;
954
956
  // throw err;
@@ -1,9 +1,13 @@
1
+ {{
2
+ const { warnOnce } = require('../../utils/warnings');
3
+ }}
4
+
1
5
  {
2
6
  function assertDuplicateRelation(node, expr) {
3
7
  if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
4
- console.warn(`Duplicate relation "${expr.$name}" in a relation expression. You should use "a.[b, c]" instead of "[a.b, a.c]". This will cause an error in objection 2.0`);
8
+ warnOnce(`Duplicate relation "${expr.$name}" in a relation expression, only the last one is used. Use "a.[b, c]" instead of "[a.b, a.c]". This will throw an error in objection 4.0.`);
5
9
 
6
- // TODO: enable for v2.0.
10
+ // Throws in objection 4.0, see ditojs/objection#53.
7
11
  // const err = new Error();
8
12
  // err.duplicateRelationName = expr.$name;
9
13
  // throw err;
@@ -5,21 +5,23 @@ const { isObject, isFunction } = require('../utils/objectUtils');
5
5
  const { isKnexRaw } = require('../utils/knexUtils');
6
6
 
7
7
  // Where operations of a query can come from different sources: the user, a
8
- // relation's owner condition, the relation's `modify`/`filter` and so on. Each
9
- // source forms its own "where group". All operations the user adds belong to
10
- // the default group, and `addWhereGroup` assigns the operations added inside
11
- // its callback to a new, separate group.
8
+ // relation's owner condition, the relation's `modify`/`filter`, the id
9
+ // condition of `patchAndFetchById()` and `$query()` and so on. Each source
10
+ // forms its own "where group". All operations the user adds belong to the
11
+ // default group, and `addWhereGroup` assigns the operations added inside its
12
+ // callback to a new, separate group.
12
13
  //
13
14
  // Knex simply joins all where clauses with their `and` or `or` boolean. Since
14
15
  // `and` binds tighter than `or`, a user's `orWhere` would escape the relation's
15
16
  // owner condition: `where "ownerId" in (1) and a or b`. This would find, update
16
17
  // and delete rows of other owners too. See issues #2191 and #1909.
17
18
  //
18
- // To prevent this, `wrapWhereGroups` is called after all `onBuild` hooks have
19
- // run. If a query contains more than one where group, every group that may
20
- // contain an `or` at its top level is wrapped in parentheses, so that the
21
- // groups are always ANDed together: `where "ownerId" in (1) and (a or b)`.
22
- // Groups without an `or` are left alone so that their SQL doesn't change.
19
+ // To prevent this, the where groups are computed when the knex query is built.
20
+ // If a query contains more than one where group, every group that may contain
21
+ // an `or` at its top level is wrapped in parentheses, so that the groups are
22
+ // always ANDed together: `where "ownerId" in (1) and (a or b)`. Groups without
23
+ // an `or` are left alone so that their SQL doesn't change. The operations
24
+ // aren't moved, so that a built query can be cloned and built again.
23
25
  function addWhereGroup(builder, callback) {
24
26
  const parentWhereGroup = builder._whereGroup;
25
27
  builder._whereGroup = {};
@@ -31,7 +33,17 @@ function addWhereGroup(builder, callback) {
31
33
  }
32
34
  }
33
35
 
34
- function wrapWhereGroups(builder) {
36
+ // Assign an operation that isn't added through a builder, like the result of
37
+ // `toFindOperation()`, to a new, separate group.
38
+ function assignWhereGroup(operation) {
39
+ operation.whereGroup = {};
40
+ return operation;
41
+ }
42
+
43
+ // Return a map from each where operation that needs to be wrapped in
44
+ // parentheses to the `WhereGroupOperation` that builds its group in place of
45
+ // the group's first operation, or to null for the group's other operations.
46
+ function getWhereGroupOperations(builder) {
35
47
  const WhereSelector = builder.constructor.WhereSelector;
36
48
  const whereOps = new Set();
37
49
  const groups = new Map();
@@ -56,17 +68,21 @@ function wrapWhereGroups(builder) {
56
68
  ops.push(op);
57
69
  });
58
70
 
59
- if (groups.size < 2) {
60
- return builder;
61
- }
71
+ const whereGroupOps = new Map();
72
+
73
+ if (groups.size > 1) {
74
+ for (const ops of groups.values()) {
75
+ if (ops.some(mayContainTopLevelOr)) {
76
+ for (const op of ops) {
77
+ whereGroupOps.set(op, null);
78
+ }
62
79
 
63
- for (const [group, ops] of groups) {
64
- if (ops.some(mayContainTopLevelOr)) {
65
- wrapOperations(builder, group, ops);
80
+ whereGroupOps.set(ops[0], new WhereGroupOperation('where', { operations: ops }));
81
+ }
66
82
  }
67
83
  }
68
84
 
69
- return builder;
85
+ return whereGroupOps;
70
86
  }
71
87
 
72
88
  function getWhereGroup(op) {
@@ -80,10 +96,6 @@ function getWhereGroup(op) {
80
96
  }
81
97
 
82
98
  function mayContainTopLevelOr(op) {
83
- if (op instanceof WhereGroupOperation) {
84
- return false;
85
- }
86
-
87
99
  // orWhere, orWhereIn, orWhereRaw, orWhereExists, orWhereJsonSupersetOf etc.
88
100
  if (/^or[A-Z]/.test(op.name)) {
89
101
  return true;
@@ -101,25 +113,8 @@ function isRaw(arg) {
101
113
  return isKnexRaw(arg) || (isObject(arg) && isFunction(arg.toKnexRaw));
102
114
  }
103
115
 
104
- function wrapOperations(builder, group, ops) {
105
- const groupOp = new WhereGroupOperation('where');
106
- groupOp.whereGroup = group;
107
-
108
- builder.replaceOperation(ops[0], groupOp);
109
-
110
- for (let i = 1; i < ops.length; ++i) {
111
- builder.removeOperation(ops[i]);
112
- }
113
-
114
- for (const op of ops) {
115
- op.parentOperation = null;
116
- op.adderHookName = null;
117
- }
118
-
119
- groupOp.operations = ops;
120
- }
121
-
122
116
  module.exports = {
123
117
  addWhereGroup,
124
- wrapWhereGroups,
118
+ assignWhereGroup,
119
+ getWhereGroupOperations,
125
120
  };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const { FirstOperation } = require('./operations/FirstOperation');
4
+ const { isMsSql } = require('../utils/knexUtils');
5
+
6
+ // Removes the order of a subquery that only selects or joins rows. The order
7
+ // can come from a relation's `modify` or an `onBuild` hook of the context, so
8
+ // it's removed when the subquery is built.
9
+ function withoutOrder(subquery) {
10
+ return subquery.onBuild(clearUnsupportedOrder);
11
+ }
12
+
13
+ // Without a limit, an order doesn't matter in subqueries and updates, and SQL
14
+ // Server rejects it there. With a limit it decides the rows, so we keep it, and
15
+ // SQL Server rejects an update rather than updating arbitrary rows.
16
+ function clearUnsupportedOrder(builder) {
17
+ if (isMsSql(builder.knex()) && !hasLimit(builder)) {
18
+ builder.clearOrder();
19
+ }
20
+ }
21
+
22
+ function hasLimit(builder) {
23
+ return (
24
+ builder.has(/limit|offset/) ||
25
+ // `first()` only adds its limit when the knex query is built.
26
+ builder.findOperation(FirstOperation)?.shouldLimit(builder)
27
+ );
28
+ }
29
+
30
+ module.exports = {
31
+ withoutOrder,
32
+ clearUnsupportedOrder,
33
+ };
@@ -11,6 +11,7 @@ const { get, isFunction } = require('../utils/objectUtils');
11
11
  const { mapAfterAllReturn } = require('../utils/promiseUtils');
12
12
  const { createModifier } = require('../utils/createModifier');
13
13
  const { addWhereGroup } = require('../queryBuilder/whereGroups');
14
+ const { withoutOrder } = require('../queryBuilder/withoutOrder');
14
15
 
15
16
  class Relation {
16
17
  constructor(relationName, OwnerClass) {
@@ -157,6 +158,8 @@ class Relation {
157
158
  if (relatedJoinSelect.isSelectAll()) {
158
159
  // No need to join a subquery if the query is `select * from "RelatedTable"`.
159
160
  relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
161
+ } else {
162
+ withoutOrder(relatedJoinSelect);
160
163
  }
161
164
 
162
165
  return builder[joinOperation](relatedJoinSelect, (join) => {
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { FindOperation } = require('../queryBuilder/operations/FindOperation');
4
- const { deprecate } = require('../utils/deprecate');
4
+ const { warnOnce } = require('../utils/warnings');
5
5
 
6
6
  class RelationFindOperation extends FindOperation {
7
7
  constructor(name, opt) {
@@ -76,6 +76,11 @@ class RelationFindOperation extends FindOperation {
76
76
  return super.onAfter3(builder, related);
77
77
  }
78
78
 
79
+ // The columns of the result rows that hold the keys of their owners:
80
+ ownerKeyColumns() {
81
+ return this.relation.relatedProp.cols;
82
+ }
83
+
79
84
  warnOnInvalidOwnerProps() {
80
85
  this.warnOnMissingOwnerProps();
81
86
 
@@ -98,7 +103,7 @@ class RelationFindOperation extends FindOperation {
98
103
  }
99
104
 
100
105
  if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
101
- deprecate(
106
+ warnOnce(
102
107
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
103
108
  `some owner models are missing the join property "${ownerProp.propDescription(c)}", ` +
104
109
  'so no related models are found for them. Select the column in the owner query.',
@@ -120,7 +125,7 @@ class RelationFindOperation extends FindOperation {
120
125
 
121
126
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
122
127
  if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
123
- deprecate(
128
+ warnOnce(
124
129
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
125
130
  `the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
126
131
  'an array, which is not supported, so no related models are found for them. ' +
@@ -139,7 +144,7 @@ class RelationFindOperation extends FindOperation {
139
144
 
140
145
  for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
141
146
  if (related.some((rel) => relatedProp.getProp(rel, c) === undefined)) {
142
- deprecate(
147
+ warnOnce(
143
148
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
144
149
  `some related models are missing the join property "${relatedProp.propDescription(c)}", ` +
145
150
  'so they are not assigned to any owner. Check that the relation mapping uses the ' +
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
4
4
  const { addWhereGroup } = require('../../queryBuilder/whereGroups');
5
+ const { withoutOrder } = require('../../queryBuilder/withoutOrder');
5
6
 
6
7
  // This mixin contains the shared code for all modify operations (update, delete, relate, unrelate)
7
8
  // for ManyToManyRelation operations.
@@ -40,8 +41,18 @@ const ManyToManyModifyMixin = (Operation) => {
40
41
  // We can now remove the where and join statements from the main query.
41
42
  this.removeFiltersFromMainQuery(builder);
42
43
 
43
- // Add a single where clause that uses the created subquery.
44
- addWhereGroup(builder, () => this.applyModifyFilterForRelatedTable(builder));
44
+ // Add a single where clause that uses the created subquery. Updates
45
+ // that modify the join rows too are executed in batches of the ids
46
+ // that `onAfterBuild` fetches, see `queryBatches()` (#161).
47
+ addWhereGroup(builder, () =>
48
+ this.modifiesJoinTable
49
+ ? builder.where((whereBuilder) =>
50
+ builder._queryBatch
51
+ ? this.applyIdFilterForRelatedTable(whereBuilder, builder._queryBatch)
52
+ : this.applyModifyFilterForRelatedTable(whereBuilder),
53
+ )
54
+ : this.applyModifyFilterForRelatedTable(builder),
55
+ );
45
56
  }
46
57
 
47
58
  return super.onBuild(builder);
@@ -63,7 +74,7 @@ const ManyToManyModifyMixin = (Operation) => {
63
74
  .copyFrom(builder, builderClass.WhereSelector)
64
75
  .copyFrom(builder, builderClass.JoinSelector);
65
76
 
66
- return modifyFilterSubquery.clearSelect();
77
+ return withoutOrder(modifyFilterSubquery.clearSelect());
67
78
  }
68
79
 
69
80
  removeFiltersFromMainQuery(builder) {
@@ -80,6 +91,11 @@ const ManyToManyModifyMixin = (Operation) => {
80
91
  return builder.whereInComposite(idRefs, subquery);
81
92
  }
82
93
 
94
+ applyIdFilterForRelatedTable(builder, ids) {
95
+ const idRefs = this.relation.relatedModelClass.getIdRelationProperty().refs(builder);
96
+ return builder.whereInComposite(idRefs, ids);
97
+ }
98
+
83
99
  // Only modifies the join rows that match the filters (#1853).
84
100
  applyModifyFilterForJoinTable(builder) {
85
101
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
@@ -108,11 +124,13 @@ const ManyToManyModifyMixin = (Operation) => {
108
124
  });
109
125
  });
110
126
 
111
- return relation
112
- .applyModify(subquery)
113
- .copyFrom(builder, builderClass.WhereSelector)
114
- .copyFrom(builder, builderClass.JoinSelector)
115
- .select(1);
127
+ return withoutOrder(
128
+ relation
129
+ .applyModify(subquery)
130
+ .copyFrom(builder, builderClass.WhereSelector)
131
+ .copyFrom(builder, builderClass.JoinSelector)
132
+ .select(1),
133
+ );
116
134
  }
117
135
 
118
136
  toFindOperation() {
@@ -143,9 +161,13 @@ const ManyToManyModifyMixin = (Operation) => {
143
161
 
144
162
  class ManyToManyModifyFindOperation extends ManyToManyFindOperation {
145
163
  onBuild(builder) {
164
+ const { modifyOperation } = this.opt;
146
165
  super.onBuild(builder);
166
+
147
167
  addWhereGroup(builder, () =>
148
- this.opt.modifyOperation.applyModifyFilterForRelatedTable(builder),
168
+ modifyOperation.relatedIds
169
+ ? modifyOperation.applyIdFilterForRelatedTable(builder, modifyOperation.relatedIds)
170
+ : modifyOperation.applyModifyFilterForRelatedTable(builder),
149
171
  );
150
172
  }
151
173
  }
@@ -11,6 +11,7 @@ const { mapAfterAllReturn } = require('../../utils/promiseUtils');
11
11
  const { isFunction, isObject, isString } = require('../../utils/objectUtils');
12
12
  const { createModifier } = require('../../utils/createModifier');
13
13
  const { addWhereGroup } = require('../../queryBuilder/whereGroups');
14
+ const { withoutOrder } = require('../../queryBuilder/withoutOrder');
14
15
 
15
16
  const { ManyToManyFindOperation } = require('./find/ManyToManyFindOperation');
16
17
  const { ManyToManyInsertOperation } = require('./insert/ManyToManyInsertOperation');
@@ -113,6 +114,8 @@ class ManyToManyRelation extends Relation {
113
114
  .as(joinTableAlias);
114
115
  if (joinTableSelect.isSelectAll()) {
115
116
  joinTableSelect = aliasedTableName(joinTable, joinTableAlias);
117
+ } else {
118
+ withoutOrder(joinTableSelect);
116
119
  }
117
120
 
118
121
  builder.join(joinTableSelect, (join) => {
@@ -144,6 +147,8 @@ class ManyToManyRelation extends Relation {
144
147
  if (relatedJoinSelect.isSelectAll()) {
145
148
  // No need to join a subquery if the query is `select * from "RelatedTable"`.
146
149
  relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
150
+ } else {
151
+ withoutOrder(relatedJoinSelect);
147
152
  }
148
153
 
149
154
  let joinTableSelect = this.joinTableModelClass
@@ -153,6 +158,8 @@ class ManyToManyRelation extends Relation {
153
158
  .as(joinTableAlias);
154
159
  if (joinTableSelect.isSelectAll()) {
155
160
  joinTableSelect = aliasedTableName(this.joinTable, joinTableAlias);
161
+ } else {
162
+ withoutOrder(joinTableSelect);
156
163
  }
157
164
 
158
165
  return builder[joinOperation](joinTableSelect, (join) => {