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