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
@@ -2,8 +2,6 @@
2
2
 
3
3
  const { QueryTransformation } = require('./QueryTransformation');
4
4
  const { isMySql } = require('../../utils/knexUtils');
5
- const { once } = require('../../utils/objectUtils');
6
- const getJoinBuilder = once(() => require('../JoinBuilder').JoinBuilder);
7
5
 
8
6
  /**
9
7
  * Mysql doesn't allow queries like this:
@@ -32,11 +30,10 @@ class WrapMysqlModifySubqueryTransformation extends QueryTransformation {
32
30
  return query;
33
31
  }
34
32
 
35
- // This transformation should not apply to join builder, otherwise it causes:
36
- // "TypeError: parentQuery.isUpdate is not a function"
37
- const JoinBuilder = getJoinBuilder();
38
- if (parentQuery instanceof JoinBuilder) {
39
- return query;
33
+ // Subqueries in where groups and join clauses are converted with a
34
+ // partial builder as their parent. Use the query that they belong to.
35
+ while (parentQuery.isPartial() && parentQuery.parentQuery()) {
36
+ parentQuery = parentQuery.parentQuery();
40
37
  }
41
38
 
42
39
  // This transformation only applies to update and delete queries.
@@ -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) {
@@ -103,6 +104,7 @@ class Relation {
103
104
  relation.joinTableRelatedProp = this.joinTableRelatedProp;
104
105
  relation.joinTableBeforeInsert = this.joinTableBeforeInsert;
105
106
  relation.joinTableExtras = this.joinTableExtras;
107
+ relation.joinTableModify = this.joinTableModify;
106
108
 
107
109
  return relation;
108
110
  }
@@ -156,6 +158,8 @@ class Relation {
156
158
  if (relatedJoinSelect.isSelectAll()) {
157
159
  // No need to join a subquery if the query is `select * from "RelatedTable"`.
158
160
  relatedJoinSelect = aliasedTableName(relatedTable, relatedTableAlias);
161
+ } else {
162
+ withoutOrder(relatedJoinSelect);
159
163
  }
160
164
 
161
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) {
@@ -12,7 +12,6 @@ class RelationFindOperation extends FindOperation {
12
12
  this.alwaysReturnArray = false;
13
13
  this.assignResultToOwner = true;
14
14
  this.relationProperty = opt.relationProperty || this.relation.name;
15
- this.omitProps = [];
16
15
  this.alias = null;
17
16
  }
18
17
 
@@ -21,10 +20,9 @@ class RelationFindOperation extends FindOperation {
21
20
  this.relation.findQuery(builder, this.owner);
22
21
 
23
22
  if (this.owner.isModels) {
24
- this.warnOnMissingOwnerProps();
23
+ this.warnOnInvalidOwnerProps();
25
24
 
26
25
  if (this.assignResultToOwner) {
27
- this.warnOnArrayOwnerProps();
28
26
  this.selectMissingJoinColumns(builder);
29
27
  }
30
28
  }
@@ -70,11 +68,6 @@ class RelationFindOperation extends FindOperation {
70
68
 
71
69
  onAfter3(builder, related) {
72
70
  const isOneToOne = this.relation.isOneToOne();
73
- const intOpt = builder.internalOptions();
74
-
75
- if (!intOpt.keepImplicitJoinProps) {
76
- this.omitImplicitJoinProps(related);
77
- }
78
71
 
79
72
  if (!this.alwaysReturnArray && isOneToOne && related.length <= 1) {
80
73
  related = related[0] || undefined;
@@ -83,6 +76,19 @@ class RelationFindOperation extends FindOperation {
83
76
  return super.onAfter3(builder, related);
84
77
  }
85
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
+
84
+ warnOnInvalidOwnerProps() {
85
+ this.warnOnMissingOwnerProps();
86
+
87
+ if (this.assignResultToOwner) {
88
+ this.warnOnArrayOwnerProps();
89
+ }
90
+ }
91
+
86
92
  // Owner models without the join columns, e.g. loaded with a partial
87
93
  // `select()`, silently get no related models (#1832). Only `undefined`
88
94
  // is reported, as `null` is a legit value for a missing relation, and
@@ -97,7 +103,7 @@ class RelationFindOperation extends FindOperation {
97
103
  }
98
104
 
99
105
  if (this.owner.modelArray.some((owner) => ownerProp.getProp(owner, c) === undefined)) {
100
- deprecate(
106
+ warnOnce(
101
107
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
102
108
  `some owner models are missing the join property "${ownerProp.propDescription(c)}", ` +
103
109
  'so no related models are found for them. Select the column in the owner query.',
@@ -111,15 +117,19 @@ class RelationFindOperation extends FindOperation {
111
117
  // assigned to their owners, so all owners silently get no related models
112
118
  // (#2667). `$relatedQuery()` without assigning the results isn't affected.
113
119
  warnOnArrayOwnerProps() {
114
- const { ownerProp } = this.relation;
120
+ const { ownerProp, joinTable } = this.relation;
121
+ // Relations through a join table already are the alternative to arrays.
122
+ const advice = joinTable
123
+ ? 'Map the relation from a column that holds a single key instead.'
124
+ : 'Use a ManyToManyRelation through a join table instead.';
115
125
 
116
126
  for (let c = 0, lc = ownerProp.size; c < lc; ++c) {
117
127
  if (this.owner.modelArray.some((owner) => Array.isArray(ownerProp.getProp(owner, c)))) {
118
- deprecate(
128
+ warnOnce(
119
129
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
120
130
  `the join property "${ownerProp.propDescription(c)}" of some owner models holds ` +
121
131
  'an array, which is not supported, so no related models are found for them. ' +
122
- 'Use a ManyToManyRelation through a join table instead.',
132
+ advice,
123
133
  );
124
134
  }
125
135
  }
@@ -134,7 +144,7 @@ class RelationFindOperation extends FindOperation {
134
144
 
135
145
  for (let c = 0, lc = relatedProp.size; c < lc; ++c) {
136
146
  if (related.some((rel) => relatedProp.getProp(rel, c) === undefined)) {
137
- deprecate(
147
+ warnOnce(
138
148
  `Fetching relation "${this.relation.name}" of ${this.relation.ownerModelClass.name}: ` +
139
149
  `some related models are missing the join property "${relatedProp.propDescription(c)}", ` +
140
150
  'so they are not assigned to any owner. Check that the relation mapping uses the ' +
@@ -154,7 +164,7 @@ class RelationFindOperation extends FindOperation {
154
164
  const col = relatedProp.cols[c];
155
165
 
156
166
  if (!builder.hasSelectionAs(fullCol, col) && addedSelects.indexOf(fullCol) === -1) {
157
- this.omitProps.push(prop);
167
+ this.omitFromResult(builder, prop);
158
168
  addedSelects.push(fullCol);
159
169
  }
160
170
  }
@@ -170,34 +180,12 @@ class RelationFindOperation extends FindOperation {
170
180
  }
171
181
  }
172
182
 
173
- omitImplicitJoinProps(related) {
174
- const relatedModelClass = this.relation.relatedModelClass;
175
-
176
- if (!this.omitProps.length || !related) {
177
- return related;
178
- }
179
-
180
- if (!Array.isArray(related)) {
181
- return this.omitImplicitJoinPropsFromOne(relatedModelClass, related);
182
- }
183
-
184
- if (!related.length) {
185
- return related;
186
- }
187
-
188
- for (let i = 0, l = related.length; i < l; ++i) {
189
- this.omitImplicitJoinPropsFromOne(relatedModelClass, related[i]);
190
- }
191
-
192
- return related;
193
- }
194
-
195
- omitImplicitJoinPropsFromOne(relatedModelClass, model) {
196
- for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
197
- relatedModelClass.omitImpl(model, this.omitProps[c]);
183
+ // Omits a property that was selected only to assign the related models to
184
+ // their owners from the result, before any `runAfter()` hooks are called.
185
+ omitFromResult(builder, prop) {
186
+ if (!builder.internalOptions().keepImplicitJoinProps) {
187
+ builder.addInternalSelection(prop);
198
188
  }
199
-
200
- return model;
201
189
  }
202
190
 
203
191
  clone() {
@@ -206,7 +194,6 @@ class RelationFindOperation extends FindOperation {
206
194
  clone.alwaysReturnArray = this.alwaysReturnArray;
207
195
  clone.assignResultToOwner = this.assignResultToOwner;
208
196
  clone.relationProperty = this.relationProperty;
209
- clone.omitProps = this.omitProps.slice();
210
197
  clone.alias = this.alias;
211
198
 
212
199
  return clone;
@@ -3,8 +3,10 @@
3
3
  const { RelationDeleteOperation } = require('../RelationDeleteOperation');
4
4
 
5
5
  class BelongsToOneDeleteOperation extends RelationDeleteOperation {
6
- onAfter1(_, result) {
7
- if (this.owner.isModels) {
6
+ onAfter1(builder, result) {
7
+ // A query resolved without executing it, e.g. with `none()`, deleted
8
+ // nothing, so the owners are left untouched.
9
+ if (this.owner.isModels && !builder.isExplicitlyResolvedOrRejected()) {
8
10
  const ownerProp = this.relation.ownerProp;
9
11
 
10
12
  for (const owner of this.owner.modelArray) {
@@ -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.
@@ -16,26 +17,42 @@ const ManyToManyModifyMixin = (Operation) => {
16
17
  constructor(...args) {
17
18
  super(...args);
18
19
  this.modifyFilterSubquery = null;
20
+ this.joinRowFilterSubquery = null;
19
21
  this.skipRelationFilter = false;
20
22
  }
21
23
 
22
- // Updates of rows that are known to be related, like the patches of
23
- // `upsertGraph()`, keep their own filters (#135).
24
+ // Unrelate doesn't execute the main query, and updates of rows that are
25
+ // known to be related, like the patches of `upsertGraph()`, keep their own
26
+ // filters (#135).
24
27
  get modifyMainQuery() {
25
- return !this.skipRelationFilter;
28
+ return !this.modifiesJoinTableOnly && !this.skipRelationFilter;
26
29
  }
27
30
 
28
31
  // At this point `builder` should only have the user's own wheres and joins. There can
29
32
  // be other operations (like orderBy) too, but those are meaningless with modify operations.
30
33
  onBuild(builder) {
31
- this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
34
+ if (this.modifiesJoinTable && !this.skipRelationFilter) {
35
+ this.joinRowFilterSubquery = this.createJoinRowFilterSubquery(builder);
36
+ }
32
37
 
33
38
  if (this.modifyMainQuery) {
39
+ this.modifyFilterSubquery = this.createModifyFilterSubquery(builder);
40
+
34
41
  // We can now remove the where and join statements from the main query.
35
42
  this.removeFiltersFromMainQuery(builder);
36
43
 
37
- // Add a single where clause that uses the created subquery.
38
- 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
+ );
39
56
  }
40
57
 
41
58
  return super.onBuild(builder);
@@ -57,7 +74,7 @@ const ManyToManyModifyMixin = (Operation) => {
57
74
  .copyFrom(builder, builderClass.WhereSelector)
58
75
  .copyFrom(builder, builderClass.JoinSelector);
59
76
 
60
- return modifyFilterSubquery.clearSelect();
77
+ return withoutOrder(modifyFilterSubquery.clearSelect());
61
78
  }
62
79
 
63
80
  removeFiltersFromMainQuery(builder) {
@@ -74,22 +91,46 @@ const ManyToManyModifyMixin = (Operation) => {
74
91
  return builder.whereInComposite(idRefs, subquery);
75
92
  }
76
93
 
77
- // Modifies the join rows of all related rows that match the filters. If the filters
78
- // reference join table columns, this also modifies join rows that don't match them
79
- // but share a related row with one that does (#1853). The dialect specific mixins
80
- // override this to only modify the join rows that match the filters themselves.
94
+ applyIdFilterForRelatedTable(builder, ids) {
95
+ const idRefs = this.relation.relatedModelClass.getIdRelationProperty().refs(builder);
96
+ return builder.whereInComposite(idRefs, ids);
97
+ }
98
+
99
+ // Only modifies the join rows that match the filters (#1853).
81
100
  applyModifyFilterForJoinTable(builder) {
82
101
  const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
83
- const joinTableRelatedRefs = this.relation.joinTableRelatedProp.refs(builder);
84
-
85
- const relatedRefs = this.relation.relatedProp.refs(builder);
86
102
  const ownerValues = this.owner.getProps(this.relation);
87
103
 
88
- const subquery = this.modifyFilterSubquery.clone().select(relatedRefs);
89
-
90
104
  return builder
91
- .whereInComposite(joinTableRelatedRefs, subquery)
92
- .whereInComposite(joinTableOwnerRefs, ownerValues);
105
+ .whereInComposite(joinTableOwnerRefs, ownerValues)
106
+ .whereExists(this.joinRowFilterSubquery);
107
+ }
108
+
109
+ // Selects the related rows of a join row that match the filters. The join
110
+ // table isn't joined, so that the filters on its columns apply to the join
111
+ // row of the outer query.
112
+ createJoinRowFilterSubquery(builder) {
113
+ const { relation } = this;
114
+ const builderClass = builder.constructor;
115
+ const subquery = relation.relatedModelClass.query().childQueryOf(builder);
116
+ const joinTableRef = builder.tableRefFor(relation.joinTable);
117
+
118
+ addWhereGroup(subquery, () => {
119
+ relation.relatedProp.forEach((i) => {
120
+ const relatedRef = relation.relatedProp.ref(subquery, i);
121
+ const joinTableRelatedRef = relation.joinTableRelatedProp.ref(subquery, i);
122
+
123
+ subquery.where(relatedRef, joinTableRelatedRef.table(joinTableRef));
124
+ });
125
+ });
126
+
127
+ return withoutOrder(
128
+ relation
129
+ .applyModify(subquery)
130
+ .copyFrom(builder, builderClass.WhereSelector)
131
+ .copyFrom(builder, builderClass.JoinSelector)
132
+ .select(1),
133
+ );
93
134
  }
94
135
 
95
136
  toFindOperation() {
@@ -111,6 +152,7 @@ const ManyToManyModifyMixin = (Operation) => {
111
152
  clone() {
112
153
  const clone = super.clone();
113
154
  clone.modifyFilterSubquery = this.modifyFilterSubquery;
155
+ clone.joinRowFilterSubquery = this.joinRowFilterSubquery;
114
156
  clone.skipRelationFilter = this.skipRelationFilter;
115
157
  return clone;
116
158
  }
@@ -119,9 +161,13 @@ const ManyToManyModifyMixin = (Operation) => {
119
161
 
120
162
  class ManyToManyModifyFindOperation extends ManyToManyFindOperation {
121
163
  onBuild(builder) {
164
+ const { modifyOperation } = this.opt;
122
165
  super.onBuild(builder);
166
+
123
167
  addWhereGroup(builder, () =>
124
- this.opt.modifyOperation.applyModifyFilterForRelatedTable(builder),
168
+ modifyOperation.relatedIds
169
+ ? modifyOperation.applyIdFilterForRelatedTable(builder, modifyOperation.relatedIds)
170
+ : modifyOperation.applyModifyFilterForRelatedTable(builder),
125
171
  );
126
172
  }
127
173
  }
@@ -1,66 +1,55 @@
1
1
  'use strict';
2
2
 
3
3
  const { ManyToManyModifyMixin } = require('./ManyToManyModifyMixin');
4
- const { isObject } = require('../../utils/objectUtils');
5
4
 
6
5
  // We need to override this mixin for mysql because mysql doesn't allow a
7
- // statement that modifies the join table to read the related table in a
8
- // subquery, if a trigger on the join table modifies the related table:
6
+ // statement that modifies the join table to read the join table in a
7
+ // subquery (ER_UPDATE_TABLE_USED), e.g. through the user's filters, or the
8
+ // related table if a trigger on the join table modifies it
9
+ // (ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG).
9
10
  //
10
- // ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG
11
- //
12
- // So the join rows that match the filters are fetched in a separate query
13
- // before the main query, which then only modifies these rows (#1853). Mysql
14
- // has no row ids and the join table's primary key isn't known, so the rows are
15
- // identified by the values of all their columns.
11
+ // So the owner and related ids of the join rows that match the filters are
12
+ // fetched in a separate query before the statement, which then only modifies
13
+ // the join rows with these ids. Unlike the subquery, these can't tell apart
14
+ // join rows that only differ in their other columns (#1853).
16
15
  //
17
16
  // https://stackoverflow.com/a/2314264/3729316
18
17
  const ManyToManyMySqlModifyMixin = (Operation) => {
19
18
  return class extends ManyToManyModifyMixin(Operation) {
20
19
  applyModifyFilterForJoinTable(builder) {
21
- const joinTableOwnerRefs = this.relation.joinTableOwnerProp.refs(builder);
22
- const tableRef = builder.tableRefFor(this.relation.getJoinModelClass(builder));
23
-
20
+ const { joinTableOwnerProp, joinTableRelatedProp } = this.relation;
24
21
  const ownerValues = this.owner.getProps(this.relation);
25
- const subquery = this.modifyFilterSubquery.clone().select(`${tableRef}.*`);
22
+ const subquery = this.joinRowFilterSubquery;
26
23
 
27
24
  return builder
25
+ .whereInComposite(joinTableOwnerProp.refs(builder), ownerValues)
28
26
  .runBefore(async (_, builder) => {
27
+ const idRefs = [
28
+ ...joinTableOwnerProp.refs(builder),
29
+ ...joinTableRelatedProp.refs(builder),
30
+ ];
31
+
29
32
  // Fetch the raw rows: no models, so that no hooks run for them.
30
- const rows = await subquery.toKnexQuery();
33
+ const rows = await builder
34
+ .modelClass()
35
+ .query()
36
+ .childQueryOf(builder)
37
+ .copyFrom(builder, builder.constructor.WhereSelector)
38
+ .whereExists(subquery)
39
+ .select(idRefs)
40
+ .toKnexQuery();
31
41
 
32
- if (rows.length) {
33
- builder.where((builder) => {
34
- for (const row of rows) {
35
- builder.orWhere((builder) => {
36
- for (const [column, value] of Object.entries(row)) {
37
- builder.whereRaw('?? <=> ?', [
38
- `${tableRef}.${column}`,
39
- toComparableValue(value),
40
- ]);
41
- }
42
- });
43
- }
44
- });
45
- } else {
46
- // Still execute the query so that it returns the expected
47
- // result for the dialect, e.g. a count of 0 for deletes.
48
- builder.where(false);
49
- }
50
- })
51
- .whereInComposite(joinTableOwnerRefs, ownerValues);
42
+ // An empty list still executes the query, so that it returns the
43
+ // expected result for the dialect, e.g. a count of 0 for deletes.
44
+ builder.whereInComposite(
45
+ idRefs,
46
+ rows.map((row) => Object.values(row)),
47
+ );
48
+ });
52
49
  }
53
50
  };
54
51
  };
55
52
 
56
- // Some drivers return JSON columns parsed. Mysql parses strings that are
57
- // compared to JSON columns, so the stringified value matches them again.
58
- function toComparableValue(value) {
59
- return isObject(value) && !(value instanceof Date) && !Buffer.isBuffer(value)
60
- ? JSON.stringify(value)
61
- : value;
62
- }
63
-
64
53
  module.exports = {
65
54
  ManyToManyMySqlModifyMixin,
66
55
  };