objection 3.8.0 → 3.9.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 +22 -6
- package/lib/model/Model.js +2 -0
- package/lib/model/graph/ModelGraphNode.js +2 -1
- package/lib/model/modelJsonAttributes.js +20 -11
- package/lib/model/modelQueryProps.js +49 -5
- package/lib/model/modelSet.js +6 -8
- package/lib/model/modelToJson.js +17 -2
- package/lib/model/modelValidate.js +2 -1
- package/lib/model/modelValues.js +43 -50
- package/lib/queryBuilder/QueryBuilder.js +17 -7
- package/lib/queryBuilder/graph/GraphData.js +39 -0
- package/lib/queryBuilder/graph/GraphFetcher.js +9 -1
- package/lib/queryBuilder/graph/GraphOptions.js +66 -12
- package/lib/queryBuilder/graph/GraphUpsert.js +82 -4
- package/lib/queryBuilder/graph/delete/GraphDelete.js +55 -2
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +13 -5
- package/lib/queryBuilder/graph/insert/GraphInsert.js +6 -1
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +3 -2
- package/lib/queryBuilder/join/JoinResultParser.js +13 -8
- package/lib/queryBuilder/join/RelationJoiner.js +3 -2
- package/lib/queryBuilder/join/TableNode.js +6 -68
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -0
- package/lib/queryBuilder/operations/InsertOperation.js +2 -2
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +2 -0
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +22 -15
- package/lib/queryBuilder/operations/UpdateOperation.js +26 -3
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +1 -1
- package/lib/queryBuilder/operations/select/Selection.js +25 -3
- package/lib/relations/RelationFindOperation.js +7 -4
- package/lib/relations/RelationOwner.js +2 -5
- package/lib/relations/RelationProperty.js +3 -14
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +5 -1
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +1 -1
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +19 -12
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +1 -16
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +43 -7
- package/lib/utils/knexUtils.js +7 -0
- package/lib/utils/objectUtils.js +5 -0
- package/lib/utils/parseFieldExpression.js +2 -5
- package/package.json +1 -1
- package/typings/objection/index.d.ts +21 -11
|
@@ -5,6 +5,7 @@ const { RangeOperation } = require('./RangeOperation');
|
|
|
5
5
|
const { FirstOperation } = require('./FirstOperation');
|
|
6
6
|
const { isString, isNumber } = require('../../utils/objectUtils');
|
|
7
7
|
const { hasWindowFunctions, isKnexRaw } = require('../../utils/knexUtils');
|
|
8
|
+
const { Selection, isSameTable, withoutSchema } = require('./select/Selection');
|
|
8
9
|
|
|
9
10
|
// Without underscores, so that column name mappers don't change it.
|
|
10
11
|
const rowNumberColumn = 'objectiontmprownum';
|
|
@@ -55,23 +56,18 @@ class LimitPerParentOperation extends QueryBuilderOperation {
|
|
|
55
56
|
// table, so that the order can refer to the selected columns and their
|
|
56
57
|
// aliases as in the query itself, e.g. to `id` when a joined table has
|
|
57
58
|
// an `id` too. The order moves into the window function, as SQL Server
|
|
58
|
-
// doesn't allow it in subqueries.
|
|
59
|
+
// doesn't allow it in subqueries. An alias can't have a schema.
|
|
60
|
+
const table = withoutSchema(builder.tableRef());
|
|
61
|
+
// knex only accepts raw partitions wrapped in objects:
|
|
62
|
+
const partition = this.findOperation.ownerKeys(builder, table).map((column) => ({ column }));
|
|
59
63
|
const numberedQuery = knex
|
|
60
64
|
.queryBuilder()
|
|
61
65
|
.select('*')
|
|
62
66
|
.rowNumber(rowNumberColumn, (window) =>
|
|
63
|
-
window
|
|
64
|
-
.orderBy(getWindowOrder(builder, knex))
|
|
65
|
-
.partitionBy(this.findOperation.ownerKeyColumns()),
|
|
67
|
+
window.orderBy(getWindowOrder(builder, knex, table)).partitionBy(partition),
|
|
66
68
|
)
|
|
67
69
|
.from(
|
|
68
|
-
knexBuilder
|
|
69
|
-
.clone()
|
|
70
|
-
.clear('with')
|
|
71
|
-
.clear('limit')
|
|
72
|
-
.clear('offset')
|
|
73
|
-
.clear('order')
|
|
74
|
-
.as(builder.tableRef()),
|
|
70
|
+
knexBuilder.clone().clear('with').clear('limit').clear('offset').clear('order').as(table),
|
|
75
71
|
);
|
|
76
72
|
|
|
77
73
|
// Select the rows in range with the query itself, to keep its settings
|
|
@@ -103,9 +99,18 @@ class LimitPerParentOperation extends QueryBuilderOperation {
|
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
// Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
|
|
106
|
-
// of knex's window functions
|
|
107
|
-
|
|
102
|
+
// of knex's window functions, which can only refer to the related table by its
|
|
103
|
+
// alias `table`.
|
|
104
|
+
function getWindowOrder(builder, knex, table) {
|
|
108
105
|
const order = [];
|
|
106
|
+
const tableRef = builder.tableRef();
|
|
107
|
+
|
|
108
|
+
const getAliasedColumn = (column) => {
|
|
109
|
+
const selection = isString(column) && Selection.create(column);
|
|
110
|
+
return selection?.table && isSameTable(builder, selection.table, tableRef)
|
|
111
|
+
? `${table}.${selection.column}`
|
|
112
|
+
: column;
|
|
113
|
+
};
|
|
109
114
|
|
|
110
115
|
builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
|
|
111
116
|
const args = op.getKnexArgs(builder);
|
|
@@ -120,18 +125,20 @@ function getWindowOrder(builder, knex) {
|
|
|
120
125
|
: [{ column, order: direction, nulls }];
|
|
121
126
|
|
|
122
127
|
for (const item of items) {
|
|
128
|
+
const aliasedColumn = getAliasedColumn(item.column);
|
|
129
|
+
|
|
123
130
|
if (item.nulls) {
|
|
124
131
|
// Not all databases support `nulls first` and `nulls last`:
|
|
125
132
|
order.push({
|
|
126
133
|
column: knex.raw('(case when ? is null then 0 else 1 end)', [
|
|
127
|
-
isString(
|
|
134
|
+
isString(aliasedColumn) ? knex.ref(aliasedColumn) : aliasedColumn,
|
|
128
135
|
]),
|
|
129
136
|
order: item.nulls === 'first' ? 'asc' : 'desc',
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
// Unlike in `orderBy()`, knex adds the direction to the window as it is:
|
|
134
|
-
order.push({ column:
|
|
141
|
+
order.push({ column: aliasedColumn, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
});
|
|
@@ -10,6 +10,7 @@ const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
|
|
|
10
10
|
const { QueryBuilderOperation } = require('./QueryBuilderOperation');
|
|
11
11
|
const { warnIfNotPostgres } = require('./jsonApi/postgresJsonApi');
|
|
12
12
|
const { StaticHookArguments } = require('../StaticHookArguments');
|
|
13
|
+
const { ValidationErrorType } = require('../../model/ValidationError');
|
|
13
14
|
|
|
14
15
|
class UpdateOperation extends QueryBuilderOperation {
|
|
15
16
|
constructor(name, opt) {
|
|
@@ -110,7 +111,8 @@ async function callStaticAfterUpdate(builder, result) {
|
|
|
110
111
|
|
|
111
112
|
function convertFieldExpressionsToRaw(builder, model, json) {
|
|
112
113
|
const knex = builder.knex();
|
|
113
|
-
const
|
|
114
|
+
const modelClass = builder.modelClass();
|
|
115
|
+
const declaredColumns = getDeclaredColumns(modelClass);
|
|
114
116
|
const convertedJson = {};
|
|
115
117
|
|
|
116
118
|
for (const key of Object.keys(json)) {
|
|
@@ -118,10 +120,30 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
118
120
|
|
|
119
121
|
if (isFieldExpressionKey(key, declaredColumns)) {
|
|
120
122
|
// 'col:attr' : ref('other:lol') is transformed to
|
|
121
|
-
// "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
123
|
+
// "col" : raw(`jsonb_set("table"."col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
|
|
124
|
+
// with the column qualified, as it is ambiguous with `excluded` in merge().
|
|
122
125
|
warnIfNotPostgres(knex);
|
|
123
126
|
|
|
124
127
|
let parsed = ref(key);
|
|
128
|
+
|
|
129
|
+
// The field expression would be lost or produce invalid SQL next to a
|
|
130
|
+
// value for the whole json column. Report it with the property names:
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(json, parsed.column)) {
|
|
132
|
+
const property = modelClass.columnNameToPropertyName(parsed.column);
|
|
133
|
+
throw modelClass.createValidationError({
|
|
134
|
+
type: ValidationErrorType.ModelValidation,
|
|
135
|
+
data: {
|
|
136
|
+
[modelClass.columnNameToPropertyName(key)]: [
|
|
137
|
+
{
|
|
138
|
+
message: `can't be combined with a value for the json column '${property}'`,
|
|
139
|
+
keyword: null,
|
|
140
|
+
params: { property },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
125
147
|
let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
|
|
126
148
|
let valuePlaceholder = '?';
|
|
127
149
|
|
|
@@ -133,7 +155,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
|
|
|
133
155
|
|
|
134
156
|
convertedJson[parsed.column] = knex.raw(
|
|
135
157
|
`jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
|
|
136
|
-
[convertedJson[parsed.column] || parsed.column
|
|
158
|
+
[convertedJson[parsed.column] || `${builder.tableRef()}.${parsed.column}`, val],
|
|
137
159
|
);
|
|
138
160
|
|
|
139
161
|
delete model[key];
|
|
@@ -170,4 +192,5 @@ function getDeclaredColumns(modelClass) {
|
|
|
170
192
|
module.exports = {
|
|
171
193
|
UpdateOperation,
|
|
172
194
|
convertFieldExpressionsToRaw,
|
|
195
|
+
getDeclaredColumns,
|
|
173
196
|
};
|
|
@@ -47,7 +47,7 @@ const { warnOnce } = require('../../../utils/warnings');
|
|
|
47
47
|
|
|
48
48
|
// JSON field expressions and the `whereJson*()` methods generate Postgres-only
|
|
49
49
|
// SQL (ditojs#113). As some of it happens to run on other databases, only warn.
|
|
50
|
-
// TODO: Consider throwing an error instead in objection 4.0.
|
|
50
|
+
// TODO: Consider throwing an error instead in objection 4.0 (#272).
|
|
51
51
|
function warnIfNotPostgres(knex) {
|
|
52
52
|
if (!isPostgres(knex)) {
|
|
53
53
|
warnOnce(
|
|
@@ -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 isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
|
|
67
67
|
} else {
|
|
68
68
|
return (
|
|
69
69
|
selectionToTest.table === null ||
|
|
70
|
-
|
|
70
|
+
isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
} else {
|
|
@@ -82,7 +82,7 @@ class Selection {
|
|
|
82
82
|
return (
|
|
83
83
|
isSameIdentifier(builder, selectionToTest.column, selectionInBuilder.column) &&
|
|
84
84
|
(selectionToTest.table === null ||
|
|
85
|
-
|
|
85
|
+
isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -110,6 +110,26 @@ function isSameIdentifier(builder, identifier1, identifier2) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// As in SQL, a table name without a schema refers to the table with a schema:
|
|
114
|
+
// `persons` and `public.persons` are the same table.
|
|
115
|
+
function isSameTable(builder, table1, table2) {
|
|
116
|
+
if (isSameIdentifier(builder, table1, table2)) {
|
|
117
|
+
return true;
|
|
118
|
+
} else if (!table1 || !table2 || hasSchema(table1) === hasSchema(table2)) {
|
|
119
|
+
return false;
|
|
120
|
+
} else {
|
|
121
|
+
return isSameIdentifier(builder, withoutSchema(table1), withoutSchema(table2));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasSchema(table) {
|
|
126
|
+
return table.includes('.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function withoutSchema(table) {
|
|
130
|
+
return table.slice(table.lastIndexOf('.') + 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
113
133
|
function mapIdentifier(builder, identifier) {
|
|
114
134
|
const knex = builder.unsafeKnex();
|
|
115
135
|
const wrapIdentifier = knex && knex.client && knex.client.config.wrapIdentifier;
|
|
@@ -211,5 +231,7 @@ function createSelectionFromString(selection) {
|
|
|
211
231
|
module.exports = {
|
|
212
232
|
Selection,
|
|
213
233
|
isSameIdentifier,
|
|
234
|
+
isSameTable,
|
|
214
235
|
mapIdentifier,
|
|
236
|
+
withoutSchema,
|
|
215
237
|
};
|
|
@@ -76,9 +76,12 @@ class RelationFindOperation extends FindOperation {
|
|
|
76
76
|
return super.onAfter3(builder, related);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// The
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
// The expressions of the result rows in `table` that hold the keys of their
|
|
80
|
+
// owners, including json references:
|
|
81
|
+
ownerKeys(builder, table) {
|
|
82
|
+
return this.relation.relatedProp
|
|
83
|
+
.refs(builder)
|
|
84
|
+
.map((ref) => ref.table(table).toKnexRaw(builder));
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
warnOnInvalidOwnerProps() {
|
|
@@ -93,7 +96,7 @@ class RelationFindOperation extends FindOperation {
|
|
|
93
96
|
// `select()`, silently get no related models (#1832). Only `undefined`
|
|
94
97
|
// is reported, as `null` is a legit value for a missing relation, and
|
|
95
98
|
// owner props that may legitimately be unset are skipped (#132).
|
|
96
|
-
// TODO: Consider throwing an error instead in objection 4.0.
|
|
99
|
+
// TODO: Consider throwing an error instead in objection 4.0 (#272).
|
|
97
100
|
warnOnMissingOwnerProps() {
|
|
98
101
|
const { ownerProp } = this.relation;
|
|
99
102
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { isObject, asArray, asSingle, uniqBy } = require('../utils/objectUtils');
|
|
4
4
|
const { normalizeIds } = require('../utils/normalizeIds');
|
|
5
|
+
const { valuesKey } = require('../model/modelValues');
|
|
5
6
|
|
|
6
7
|
const Type = {
|
|
7
8
|
Models: 'Models',
|
|
@@ -123,7 +124,7 @@ class RelationOwner {
|
|
|
123
124
|
return null;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
return uniqBy(props,
|
|
127
|
+
return uniqBy(props, valuesKey);
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
_getPropsFromIdentifiers(relation, ownerProp) {
|
|
@@ -218,10 +219,6 @@ function containsNonNull(arr) {
|
|
|
218
219
|
return false;
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
function join(id) {
|
|
222
|
-
return id.map((x) => (Buffer.isBuffer(x) ? x.toString('hex') : x)).join(',');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
222
|
function isIdProp(relationProp) {
|
|
226
223
|
const idProp = relationProp.modelClass.getIdRelationProperty();
|
|
227
224
|
return idProp.props.every((prop, i) => prop === relationProp.props[i]);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { asArray, isObject, uniqBy, get, set } = require('../utils/objectUtils');
|
|
4
4
|
const { ref: createRef } = require('../queryBuilder/ReferenceBuilder');
|
|
5
|
-
const {
|
|
5
|
+
const { valuesKey } = require('../model/modelValues');
|
|
6
6
|
|
|
7
7
|
class ModelNotFoundError extends Error {
|
|
8
8
|
constructor(tableName) {
|
|
@@ -86,20 +86,9 @@ class RelationProperty {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Creates a
|
|
89
|
+
// Creates a key from the property values of the given object.
|
|
90
90
|
propKey(obj) {
|
|
91
|
-
|
|
92
|
-
let key = PROP_KEY_PREFIX;
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < size; ++i) {
|
|
95
|
-
key += propToStr(this.getProp(obj, i));
|
|
96
|
-
|
|
97
|
-
if (i !== size - 1) {
|
|
98
|
-
key += ',';
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return key;
|
|
91
|
+
return valuesKey(this.getProps(obj));
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
// Returns the property values of the given object as an array.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { RelationInsertOperation } = require('../RelationInsertOperation');
|
|
4
4
|
const { BelongsToOneRelateOperation } = require('./BelongsToOneRelateOperation');
|
|
5
|
+
const { InsertGraphOperation } = require('../../queryBuilder/operations/InsertGraphOperation');
|
|
5
6
|
|
|
6
7
|
class BelongsToOneInsertOperation extends RelationInsertOperation {
|
|
7
8
|
onAdd(builder, args) {
|
|
@@ -17,7 +18,10 @@ class BelongsToOneInsertOperation extends RelationInsertOperation {
|
|
|
17
18
|
async onAfter1(builder, ret) {
|
|
18
19
|
const inserted = await super.onAfter1(builder, ret);
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// A query resolved without executing it, e.g. with `resolve()`, inserted
|
|
22
|
+
// nothing. `insertGraph()` resolves the query itself, but always inserts
|
|
23
|
+
// the graph.
|
|
24
|
+
if (!builder.isExecutable() && !builder.has(InsertGraphOperation)) {
|
|
21
25
|
return inserted;
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { hasBatchInsert } = require('../../../utils/knexUtils');
|
|
3
4
|
const { RelationInsertOperation } = require('../../RelationInsertOperation');
|
|
4
5
|
const { ManyToManyRelateOperation } = require('../relate/ManyToManyRelateOperation');
|
|
5
6
|
|
|
@@ -19,13 +20,6 @@ class ManyToManyInsertOperation extends RelationInsertOperation {
|
|
|
19
20
|
async onAfter1(builder, ret) {
|
|
20
21
|
const inserted = await super.onAfter1(builder, ret);
|
|
21
22
|
|
|
22
|
-
const relateOp = new ManyToManyRelateOperation('relate', {
|
|
23
|
-
dontCopyReturning: true,
|
|
24
|
-
dontCopyOnConflict: true,
|
|
25
|
-
relation: this.relation,
|
|
26
|
-
owner: this.owner,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
23
|
const modelsToRelate = inserted.filter((it) => {
|
|
30
24
|
return this.relation.relatedProp.hasProps(it);
|
|
31
25
|
});
|
|
@@ -36,13 +30,26 @@ class ManyToManyInsertOperation extends RelationInsertOperation {
|
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
if (modelsToRelate.length
|
|
40
|
-
|
|
33
|
+
if (modelsToRelate.length > 0) {
|
|
34
|
+
// Without batch inserts, `insertGraph()` inserts the related models one at
|
|
35
|
+
// a time, and so we need to do the same with their join rows:
|
|
36
|
+
const batches = hasBatchInsert(builder.knex())
|
|
37
|
+
? [modelsToRelate]
|
|
38
|
+
: modelsToRelate.map((model) => [model]);
|
|
39
|
+
|
|
40
|
+
for (const batch of batches) {
|
|
41
|
+
const relateOp = new ManyToManyRelateOperation('relate', {
|
|
42
|
+
dontCopyReturning: true,
|
|
43
|
+
dontCopyOnConflict: true,
|
|
44
|
+
relation: this.relation,
|
|
45
|
+
owner: this.owner,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
relateOp.onAdd(builder, [batch]);
|
|
49
|
+
await relateOp.queryExecutor(builder);
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
relateOp.onAdd(builder, [modelsToRelate]);
|
|
44
|
-
await relateOp.queryExecutor(builder);
|
|
45
|
-
|
|
46
53
|
return inserted;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
@@ -3,24 +3,9 @@
|
|
|
3
3
|
const { ManyToManyUpdateOperationBase } = require('./ManyToManyUpdateOperationBase');
|
|
4
4
|
const { ManyToManyMySqlModifyMixin } = require('../ManyToManyMySqlModifyMixin');
|
|
5
5
|
|
|
6
|
-
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
7
|
-
|
|
8
6
|
class ManyToManyUpdateMySqlOperation extends ManyToManyMySqlModifyMixin(
|
|
9
7
|
ManyToManyUpdateOperationBase,
|
|
10
|
-
) {
|
|
11
|
-
// MySQL is the only database that supports a limit in updates. Updates that
|
|
12
|
-
// modify the join rows too update the related rows by their ids, so the limit
|
|
13
|
-
// and the order pick these ids instead of limiting each batch of them. Without
|
|
14
|
-
// a limit or an offset, the order picks nothing and the query stays as it is.
|
|
15
|
-
onBuild(builder) {
|
|
16
|
-
super.onBuild(builder);
|
|
17
|
-
|
|
18
|
-
if (this.relatedIdsQuery && builder.has(/^(limit|offset)$/)) {
|
|
19
|
-
this.relatedIdsQuery.copyFrom(builder, LimitSelector);
|
|
20
|
-
builder.clear(LimitSelector);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
8
|
+
) {}
|
|
24
9
|
|
|
25
10
|
module.exports = {
|
|
26
11
|
ManyToManyUpdateMySqlOperation,
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
const { UpdateOperation } = require('../../../queryBuilder/operations/UpdateOperation');
|
|
4
4
|
const { addWhereGroup } = require('../../../queryBuilder/whereGroups');
|
|
5
5
|
const { chunk } = require('../../../utils/objectUtils');
|
|
6
|
+
const { isMySql, isMsSql } = require('../../../utils/knexUtils');
|
|
6
7
|
|
|
7
8
|
// Stay below the limits of bound values per statement of SQLite before 3.32
|
|
8
9
|
// (999) and SQL Server (2100), leaving room for the patched values:
|
|
9
10
|
const maxBatchValues = 500;
|
|
10
11
|
|
|
12
|
+
const LimitSelector = /^(orderBy\w*|limit|offset)$/;
|
|
13
|
+
|
|
11
14
|
class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
12
15
|
constructor(name, opt) {
|
|
13
16
|
super(name, opt);
|
|
@@ -59,9 +62,21 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
59
62
|
|
|
60
63
|
onBuild(builder) {
|
|
61
64
|
if (this.modifiesJoinTable && this.modifyFilterSubquery) {
|
|
65
|
+
// MySQL and SQL Server are the only databases that support a limit in
|
|
66
|
+
// updates. As the related rows are updated by their ids, the limit and
|
|
67
|
+
// the order pick these ids instead of limiting each batch of them.
|
|
68
|
+
// Without a limit or an offset, the order picks nothing and the query
|
|
69
|
+
// stays as it is.
|
|
70
|
+
const knex = builder.knex();
|
|
71
|
+
const hasLimit = (isMySql(knex) || isMsSql(knex)) && builder.has(/^(limit|offset)$/);
|
|
72
|
+
|
|
62
73
|
// The related rows to update are identified by their ids first, see
|
|
63
74
|
// `onAfterBuild()`.
|
|
64
|
-
this.relatedIdsQuery = this.createRelatedIdsQuery(builder);
|
|
75
|
+
this.relatedIdsQuery = this.createRelatedIdsQuery(builder, hasLimit);
|
|
76
|
+
if (hasLimit) {
|
|
77
|
+
this.relatedIdsQuery.copyFrom(builder, LimitSelector);
|
|
78
|
+
builder.clear(LimitSelector);
|
|
79
|
+
}
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
return super.onBuild(builder);
|
|
@@ -69,11 +84,23 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
69
84
|
|
|
70
85
|
// Selects the ids of the related rows to update. Fetch raw rows, so that no
|
|
71
86
|
// hooks run for them.
|
|
72
|
-
createRelatedIdsQuery(builder) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
createRelatedIdsQuery(builder, hasLimit) {
|
|
88
|
+
const { relatedModelClass } = this.relation;
|
|
89
|
+
|
|
90
|
+
if (hasLimit) {
|
|
91
|
+
// The limit picks related rows, not join rows, so filter the related
|
|
92
|
+
// table by the ids of the matching join rows:
|
|
93
|
+
const relatedIdsQuery = relatedModelClass.query().childQueryOf(builder);
|
|
94
|
+
const idRefs = relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
95
|
+
return this.applyModifyFilterForRelatedTable(relatedIdsQuery).select(idRefs);
|
|
96
|
+
} else {
|
|
97
|
+
// Select the ids from the filter subquery itself. Filtering the related
|
|
98
|
+
// table by them again lets Postgres pick plans that run the subquery
|
|
99
|
+
// once per related row, which takes minutes for a few thousand (#161).
|
|
100
|
+
const relatedIdsQuery = this.modifyFilterSubquery.clone();
|
|
101
|
+
const idRefs = relatedModelClass.getIdRelationProperty().refs(relatedIdsQuery);
|
|
102
|
+
return relatedIdsQuery.distinct(idRefs);
|
|
103
|
+
}
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
// Update the join rows before the related rows, while the filters still
|
|
@@ -88,6 +115,7 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
88
115
|
this.relatedIds = rows.map((row) => Object.values(row));
|
|
89
116
|
}
|
|
90
117
|
|
|
118
|
+
let numJoinRowsUpdated;
|
|
91
119
|
if (this.relatedIdsQuery?.has(/^(limit|offset)$/)) {
|
|
92
120
|
// The filters match the join rows of more related rows than the limit
|
|
93
121
|
// picks, so only update the join rows of the picked ones.
|
|
@@ -95,7 +123,15 @@ class ManyToManyUpdateOperationBase extends UpdateOperation {
|
|
|
95
123
|
await this.updateJoinRows(builder, ids);
|
|
96
124
|
}
|
|
97
125
|
} else {
|
|
98
|
-
await this.updateJoinRows(builder);
|
|
126
|
+
numJoinRowsUpdated = await this.updateJoinRows(builder);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.hasNothingToUpdate) {
|
|
130
|
+
// Only the join rows change, so count the related rows that they
|
|
131
|
+
// belong to, as the update of the related rows does. Without their
|
|
132
|
+
// ids, the join rows all belong to the updated model, see
|
|
133
|
+
// `applyIdFilterForJoinTable()`.
|
|
134
|
+
builder.resolve(this.relatedIds?.length ?? Math.min(numJoinRowsUpdated, 1));
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
137
|
}
|
package/lib/utils/knexUtils.js
CHANGED
|
@@ -36,6 +36,12 @@ function isMsSql(knex) {
|
|
|
36
36
|
return getDialect(knex) === 'mssql';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Only Postgres and SQL Server return the identifiers of all the rows inserted
|
|
40
|
+
// by one query.
|
|
41
|
+
function hasBatchInsert(knex) {
|
|
42
|
+
return isPostgres(knex) || isMsSql(knex);
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
// MySQL supports window functions since 8.0, MariaDB since 10.2. Without a
|
|
40
46
|
// version, knex assumes the latest one, and so do we.
|
|
41
47
|
function hasWindowFunctions(knex) {
|
|
@@ -82,6 +88,7 @@ module.exports = {
|
|
|
82
88
|
isSqlite,
|
|
83
89
|
isMsSql,
|
|
84
90
|
isOracle,
|
|
91
|
+
hasBatchInsert,
|
|
85
92
|
hasWindowFunctions,
|
|
86
93
|
isKnexQueryBuilder,
|
|
87
94
|
isKnexJoinBuilder,
|
package/lib/utils/objectUtils.js
CHANGED
|
@@ -16,6 +16,10 @@ function isObject(value) {
|
|
|
16
16
|
return value !== null && typeof value === 'object';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function hasOwn(obj, prop) {
|
|
20
|
+
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
// Quick and dirty check if an object is a plain object and not
|
|
20
24
|
// for example an instance of some class.
|
|
21
25
|
function isPlainObject(value) {
|
|
@@ -502,6 +506,7 @@ module.exports = {
|
|
|
502
506
|
isString,
|
|
503
507
|
isRegExp,
|
|
504
508
|
isObject,
|
|
509
|
+
hasOwn,
|
|
505
510
|
isNumber,
|
|
506
511
|
isFunction,
|
|
507
512
|
jsonEquals,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const jsonFieldExpressionParser = require('../queryBuilder/parsers/jsonFieldExpressionParser');
|
|
4
|
-
const { isObject } = require('./objectUtils');
|
|
4
|
+
const { isObject, hasOwn } = require('./objectUtils');
|
|
5
5
|
const cache = new Map();
|
|
6
6
|
|
|
7
7
|
function parseFieldExpression(expr) {
|
|
@@ -51,10 +51,7 @@ function freezeParsedExpr(parsedExpr) {
|
|
|
51
51
|
// `'meta:a.b'` that write into json columns, unless they are `declared`: the
|
|
52
52
|
// `properties` of the schema, or an object with their column names as keys.
|
|
53
53
|
function isFieldExpressionKey(key, declared) {
|
|
54
|
-
return (
|
|
55
|
-
key.includes(':') &&
|
|
56
|
-
!(isObject(declared) && Object.prototype.hasOwnProperty.call(declared, key))
|
|
57
|
-
);
|
|
54
|
+
return key.includes(':') && !(isObject(declared) && hasOwn(declared, key));
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
// Formats the json path of a parsed field expression as a Postgres text array
|
package/package.json
CHANGED
|
@@ -374,20 +374,30 @@ declare namespace Objection {
|
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
376
|
* Converts the query builder NQB to the same result kind as QB:
|
|
377
|
-
* array,
|
|
377
|
+
* array, page, single or maybe-single. Any other result type, e.g. after
|
|
378
378
|
* `castTo()` or `patch()`, falls back to QB, without narrowing.
|
|
379
|
+
* QB's model type M and result type R are separate parameters so that QB
|
|
380
|
+
* only appears in the result, and TypeScript can relate the kinds of two
|
|
381
|
+
* query builders, e.g. of a custom query builder and its base class, through
|
|
382
|
+
* QB alone. The page and `object` checks come before the single ones, so
|
|
383
|
+
* that results like `number` or `Page<M>` are ruled out for a generic M too.
|
|
379
384
|
*/
|
|
380
|
-
type WithResultKind<
|
|
381
|
-
|
|
382
|
-
|
|
385
|
+
type WithResultKind<
|
|
386
|
+
QB extends AnyQueryBuilder,
|
|
387
|
+
NQB extends AnyQueryBuilder,
|
|
388
|
+
M extends Model = ModelType<QB>,
|
|
389
|
+
R = ResultType<QB>,
|
|
390
|
+
> = [R] extends [M[]]
|
|
383
391
|
? ArrayQueryBuilder<NQB>
|
|
384
|
-
: [
|
|
385
|
-
?
|
|
386
|
-
: [
|
|
387
|
-
?
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
392
|
+
: [R] extends [Page<M>]
|
|
393
|
+
? PageQueryBuilder<NQB>
|
|
394
|
+
: [R] extends [object | undefined]
|
|
395
|
+
? [R] extends [M]
|
|
396
|
+
? SingleQueryBuilder<NQB>
|
|
397
|
+
: [R] extends [M | undefined]
|
|
398
|
+
? MaybeSingleQueryBuilder<NQB>
|
|
399
|
+
: QB
|
|
400
|
+
: QB;
|
|
391
401
|
|
|
392
402
|
/**
|
|
393
403
|
* If T is an array, returns the item type, otherwise returns T.
|