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.
- package/lib/model/AjvValidator.js +132 -52
- package/lib/model/modelClone.js +0 -2
- package/lib/queryBuilder/QueryBuilder.js +128 -24
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/RelationExpression.js +6 -4
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +36 -79
- package/lib/queryBuilder/graph/GraphUpsert.js +35 -7
- package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
- package/lib/queryBuilder/join/RelationJoiner.js +104 -12
- package/lib/queryBuilder/join/utils.js +2 -2
- package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
- package/lib/queryBuilder/operations/FirstOperation.js +5 -3
- package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +8 -2
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
- package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
- package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
- package/lib/queryBuilder/operations/select/Selection.js +21 -20
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
- package/lib/queryBuilder/parsers/relationExpressionParser.js +6 -4
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +7 -3
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +4 -0
- package/lib/relations/RelationFindOperation.js +30 -43
- package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +65 -19
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
- package/lib/relations/manyToMany/ManyToManyRelation.js +15 -16
- package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +9 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +92 -16
- package/lib/utils/identifierMapping.js +10 -19
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/objectUtils.js +38 -6
- package/lib/utils/parseFieldExpression.js +22 -3
- package/lib/utils/warnings.js +26 -0
- package/lib/utils/wrapDbError.js +34 -20
- package/package.json +5 -4
- package/typings/objection/index.d.ts +54 -26
- package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
- 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
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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,
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
wrapOperations(builder, group, ops);
|
|
80
|
+
whereGroupOps.set(ops[0], new WhereGroupOperation('where', { operations: ops }));
|
|
81
|
+
}
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!
|
|
177
|
-
|
|
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(
|
|
7
|
-
|
|
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
|
-
//
|
|
23
|
-
// `upsertGraph()`, keep their own
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
92
|
-
.
|
|
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
|
-
|
|
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
|
|
8
|
-
// subquery,
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
};
|