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.
- package/lib/model/AjvValidator.js +101 -44
- package/lib/queryBuilder/QueryBuilder.js +91 -33
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
- package/lib/queryBuilder/StaticHookArguments.js +4 -1
- package/lib/queryBuilder/graph/GraphOptions.js +11 -26
- package/lib/queryBuilder/graph/GraphUpsert.js +16 -10
- 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 +2 -1
- 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 +29 -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/parsers/relationExpressionParser.js +5 -3
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +6 -2
- package/lib/queryBuilder/whereGroups.js +35 -40
- package/lib/queryBuilder/withoutOrder.js +33 -0
- package/lib/relations/Relation.js +3 -0
- package/lib/relations/RelationFindOperation.js +9 -4
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +31 -9
- package/lib/relations/manyToMany/ManyToManyRelation.js +7 -0
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +7 -2
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +87 -17
- package/lib/utils/identifierMapping.js +2 -2
- package/lib/utils/knexUtils.js +14 -0
- package/lib/utils/parseFieldExpression.js +12 -0
- 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 +10 -2
- 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 {
|
|
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
|
|
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 `
|
|
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 {
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
const
|
|
43
|
+
if (!this.isDistinctOn) {
|
|
44
|
+
for (const selection of selections) {
|
|
45
|
+
const selectionInstance = Selection.create(selection);
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
66
|
+
return isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table);
|
|
67
67
|
} else {
|
|
68
68
|
return (
|
|
69
69
|
selectionToTest.table === null ||
|
|
70
|
-
|
|
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
|
|
83
|
+
isSameIdentifier(builder, selectionToTest.column, selectionInBuilder.column) &&
|
|
84
84
|
(selectionToTest.table === null ||
|
|
85
|
-
|
|
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
|
-
//
|
|
101
|
-
// (for example `knexSnakeCaseMappers()`) cancels out
|
|
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
|
|
104
|
-
if (
|
|
103
|
+
function isSameIdentifier(builder, identifier1, identifier2) {
|
|
104
|
+
if (identifier1 === identifier2) {
|
|
105
105
|
return true;
|
|
106
|
-
} else if (!
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
948
|
-
`Duplicate relation "${expr.$name}" in a relation expression
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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) {
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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) => {
|