objection 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/lib/model/AjvValidator.js +132 -52
  2. package/lib/model/modelClone.js +0 -2
  3. package/lib/queryBuilder/QueryBuilder.js +128 -24
  4. package/lib/queryBuilder/QueryBuilderOperationSupport.js +15 -3
  5. package/lib/queryBuilder/RelationExpression.js +6 -4
  6. package/lib/queryBuilder/StaticHookArguments.js +4 -1
  7. package/lib/queryBuilder/graph/GraphOptions.js +36 -79
  8. package/lib/queryBuilder/graph/GraphUpsert.js +35 -7
  9. package/lib/queryBuilder/graph/insert/GraphInsert.js +2 -0
  10. package/lib/queryBuilder/join/RelationJoiner.js +104 -12
  11. package/lib/queryBuilder/join/utils.js +2 -2
  12. package/lib/queryBuilder/operations/DelegateOperation.js +22 -0
  13. package/lib/queryBuilder/operations/FirstOperation.js +5 -3
  14. package/lib/queryBuilder/operations/InsertGraphOperation.js +1 -0
  15. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +2 -1
  16. package/lib/queryBuilder/operations/InstanceFindOperation.js +2 -1
  17. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +8 -2
  18. package/lib/queryBuilder/operations/LimitPerParentOperation.js +145 -0
  19. package/lib/queryBuilder/operations/QueryBuilderOperation.js +31 -0
  20. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +22 -5
  21. package/lib/queryBuilder/operations/UpdateOperation.js +40 -3
  22. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  23. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +5 -0
  24. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +28 -2
  25. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +2 -2
  26. package/lib/queryBuilder/operations/select/SelectOperation.js +8 -4
  27. package/lib/queryBuilder/operations/select/Selection.js +21 -20
  28. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +3 -1
  29. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -9
  30. package/lib/queryBuilder/parsers/relationExpressionParser.js +6 -4
  31. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +7 -3
  32. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +4 -7
  33. package/lib/queryBuilder/whereGroups.js +35 -40
  34. package/lib/queryBuilder/withoutOrder.js +33 -0
  35. package/lib/relations/Relation.js +4 -0
  36. package/lib/relations/RelationFindOperation.js +30 -43
  37. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +4 -2
  38. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +65 -19
  39. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +31 -42
  40. package/lib/relations/manyToMany/ManyToManyRelation.js +15 -16
  41. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +0 -13
  42. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +9 -5
  43. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +1 -5
  44. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +1 -5
  45. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +9 -0
  46. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +1 -5
  47. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +16 -1
  48. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +92 -16
  49. package/lib/utils/identifierMapping.js +10 -19
  50. package/lib/utils/knexUtils.js +14 -0
  51. package/lib/utils/objectUtils.js +38 -6
  52. package/lib/utils/parseFieldExpression.js +22 -3
  53. package/lib/utils/warnings.js +26 -0
  54. package/lib/utils/wrapDbError.js +34 -20
  55. package/package.json +5 -4
  56. package/typings/objection/index.d.ts +54 -26
  57. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +0 -27
  58. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +0 -16
  59. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +0 -12
  60. package/lib/utils/deprecate.js +0 -21
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
+ const { RangeOperation } = require('./RangeOperation');
5
+ const { FirstOperation } = require('./FirstOperation');
6
+ const { isString, isNumber } = require('../../utils/objectUtils');
7
+ const { hasWindowFunctions, isKnexRaw } = require('../../utils/knexUtils');
8
+
9
+ // Without underscores, so that column name mappers don't change it.
10
+ const rowNumberColumn = 'objectiontmprownum';
11
+
12
+ // Applies the `limit()`, `offset()` and `first()` of a relation query that
13
+ // fetches the related models of many parents to each parent separately.
14
+ class LimitPerParentOperation extends QueryBuilderOperation {
15
+ constructor(name, opt) {
16
+ super(name, opt);
17
+ this.findOperation = opt.findOperation;
18
+ }
19
+
20
+ onBuild(builder) {
21
+ builder.addInternalSelection(rowNumberColumn);
22
+ }
23
+
24
+ // Return the limit and offset of the query, built into `knexBuilder` by any
25
+ // of its operations, or `null` if it has none:
26
+ getRange(knexBuilder, builder) {
27
+ // `page()` and `range()` keep limiting all parents, as their total does.
28
+ if (!builder.has(RangeOperation)) {
29
+ const { limit = null, offset = null } = knexBuilder._single;
30
+ // `first()` only limits the query with `useLimitInFirst`:
31
+ const isFirst = builder.has(FirstOperation);
32
+
33
+ if (limit !== null || offset !== null || isFirst) {
34
+ return { limit: isFirst ? 1 : limit, offset: offset ?? 0 };
35
+ }
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ // Called by `buildKnexQuery()` after the default table and select are set.
42
+ onAfterBuildKnex(knexBuilder, builder) {
43
+ const knex = builder.knex();
44
+ const range = this.getRange(knexBuilder, builder);
45
+
46
+ if (!range) {
47
+ return knexBuilder;
48
+ } else if (!hasWindowFunctions(knex)) {
49
+ // `WhereInEagerOperation` fetches the related models of each parent with
50
+ // a separate query then, so only `first()` still needs to be applied:
51
+ return range.limit === null ? knexBuilder : knexBuilder.limit(range.limit);
52
+ } else {
53
+ const { limit, offset } = range;
54
+ // Number the rows of each parent one level up, aliased as the related
55
+ // table, so that the order can refer to the selected columns and their
56
+ // aliases as in the query itself, e.g. to `id` when a joined table has
57
+ // an `id` too. The order moves into the window function, as SQL Server
58
+ // doesn't allow it in subqueries.
59
+ const numberedQuery = knex
60
+ .queryBuilder()
61
+ .select('*')
62
+ .rowNumber(rowNumberColumn, (window) =>
63
+ window
64
+ .orderBy(getWindowOrder(builder, knex))
65
+ .partitionBy(this.findOperation.ownerKeyColumns()),
66
+ )
67
+ .from(
68
+ knexBuilder
69
+ .clone()
70
+ .clear('with')
71
+ .clear('limit')
72
+ .clear('offset')
73
+ .clear('order')
74
+ .as(builder.tableRef()),
75
+ );
76
+
77
+ // Select the rows in range with the query itself, to keep its settings
78
+ // like `timeout()`, and its common table expressions.
79
+ knexBuilder
80
+ .clear('select')
81
+ .clear('where')
82
+ .clear('join')
83
+ .clear('union')
84
+ .clear('group')
85
+ .clear('having')
86
+ .clear('order')
87
+ .clear('limit')
88
+ .clear('offset')
89
+ .select('*')
90
+ .from(numberedQuery.as(rowNumberColumn))
91
+ .where(rowNumberColumn, '>', offset)
92
+ .orderBy(rowNumberColumn);
93
+
94
+ if (limit !== null) {
95
+ // knex also accepts raw offsets:
96
+ const end = isNumber(offset) ? offset + limit : knex.raw('? + ?', [offset, limit]);
97
+ knexBuilder.where(rowNumberColumn, '<=', end);
98
+ }
99
+
100
+ return knexBuilder;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Convert the `orderBy()` and `orderByRaw()` calls of the query to the order
106
+ // of knex's window functions.
107
+ function getWindowOrder(builder, knex) {
108
+ const order = [];
109
+
110
+ builder.forEachOperation(builder.constructor.OrderBySelector, (op) => {
111
+ const args = op.getKnexArgs(builder);
112
+
113
+ if (op.name === 'orderByRaw') {
114
+ const [sql, bindings] = args;
115
+ order.push({ column: isKnexRaw(sql) ? sql : knex.raw(sql, bindings) });
116
+ } else {
117
+ const [column, direction, nulls] = args;
118
+ const items = Array.isArray(column)
119
+ ? column.map((item) => (isString(item) ? { column: item } : item))
120
+ : [{ column, order: direction, nulls }];
121
+
122
+ for (const item of items) {
123
+ if (item.nulls) {
124
+ // Not all databases support `nulls first` and `nulls last`:
125
+ order.push({
126
+ column: knex.raw('(case when ? is null then 0 else 1 end)', [
127
+ isString(item.column) ? knex.ref(item.column) : item.column,
128
+ ]),
129
+ order: item.nulls === 'first' ? 'asc' : 'desc',
130
+ });
131
+ }
132
+
133
+ // Unlike in `orderBy()`, knex adds the direction to the window as it is:
134
+ order.push({ column: item.column, order: /^desc$/i.test(item.order) ? 'desc' : 'asc' });
135
+ }
136
+ }
137
+ });
138
+
139
+ // SQL Server requires an order in the window function:
140
+ return order.length ? order : [{ column: knex.raw('(select null)') }];
141
+ }
142
+
143
+ module.exports = {
144
+ LimitPerParentOperation,
145
+ };
@@ -6,9 +6,11 @@ const hookNameToHasMethodName = {
6
6
  onBefore2: 'hasOnBefore2',
7
7
  onBefore3: 'hasOnBefore3',
8
8
  onBuild: 'hasOnBuild',
9
+ onAfterBuild: 'hasOnAfterBuild',
9
10
  onBuildKnex: 'hasOnBuildKnex',
10
11
  onRawResult: 'hasOnRawResult',
11
12
  queryExecutor: 'hasQueryExecutor',
13
+ queryBatches: 'hasQueryBatches',
12
14
  onAfter1: 'hasOnAfter1',
13
15
  onAfter2: 'hasOnAfter2',
14
16
  onAfter3: 'hasOnAfter3',
@@ -107,6 +109,20 @@ class QueryBuilderOperation {
107
109
  return this.onBuild !== QueryBuilderOperation.prototype.onBuild;
108
110
  }
109
111
 
112
+ // This is called after `onBuild` when the query is executed, right before the
113
+ // knex query is built and executed, but not when the query is only built,
114
+ // e.g. with `toKnexQuery()` or `resultSize()`. Use it for work that needs the
115
+ // built query, like queries that have to run before it. It's also called for
116
+ // queries that are resolved or rejected without executing, e.g. with `none()`
117
+ // or `resolve()`, so check `builder.isExplicitlyResolvedOrRejected()` first.
118
+ //
119
+ // This method can be asynchronous.
120
+ // This method should never call any methods that add operations to the builder.
121
+ onAfterBuild(builder) {}
122
+ hasOnAfterBuild() {
123
+ return this.onAfterBuild !== QueryBuilderOperation.prototype.onAfterBuild;
124
+ }
125
+
110
126
  // This is called when the knex query is built. Here you should only call knex
111
127
  // methods. You may call getters and other immutable methods of the `builder`
112
128
  // but you should never call methods that add QueryBuilderOperations.
@@ -177,6 +193,21 @@ class QueryBuilderOperation {
177
193
  return this.queryExecutor !== QueryBuilderOperation.prototype.queryExecutor;
178
194
  }
179
195
 
196
+ // This is called after `onAfterBuild` when the query is executed. It can
197
+ // return an array of batches to build and execute the knex query once per
198
+ // batch, e.g. when a filter has more values than the database accepts in one
199
+ // statement. The batch being built is in `builder._queryBatch`, and the
200
+ // results of all batches are merged before `onRawResult` is called. Return
201
+ // null to execute the query once.
202
+ //
203
+ // This method must be synchronous.
204
+ queryBatches(builder) {
205
+ return null;
206
+ }
207
+ hasQueryBatches() {
208
+ return this.queryBatches !== QueryBuilderOperation.prototype.queryBatches;
209
+ }
210
+
180
211
  // This is called if an error occurs in the query execution.
181
212
  //
182
213
  // This method must return a QueryBuilder instance.
@@ -3,6 +3,7 @@
3
3
  const { DelegateOperation } = require('./DelegateOperation');
4
4
  const { FindByIdOperation } = require('./FindByIdOperation');
5
5
  const { UpdateOperation } = require('./UpdateOperation');
6
+ const { addWhereGroup, assignWhereGroup } = require('../whereGroups');
6
7
 
7
8
  class UpdateAndFetchOperation extends DelegateOperation {
8
9
  constructor(name, opt) {
@@ -41,7 +42,7 @@ class UpdateAndFetchOperation extends DelegateOperation {
41
42
 
42
43
  onBuild(builder) {
43
44
  if (!this.skipIdWhere) {
44
- builder.findById(this.id);
45
+ addWhereGroup(builder, () => builder.findById(this.id));
45
46
  }
46
47
 
47
48
  super.onBuild(builder);
@@ -78,10 +79,17 @@ class UpdateAndFetchOperation extends DelegateOperation {
78
79
  return fetched ? this.model : undefined;
79
80
  }
80
81
 
81
- toFindOperation() {
82
- return new FindByIdOperation('findById', {
83
- id: this.id,
84
- });
82
+ toFindOperation(builder) {
83
+ // The find operation of the delegate keeps its filters, e.g. the ones that
84
+ // many-to-many updates move to a subquery when the query is built.
85
+ const delegate = this.delegate.toFindOperation(builder);
86
+
87
+ // Keep the id condition in its own where group, like `onBuild()` does. The
88
+ // delegate isn't named `findById`, as that would put all the wheres that it
89
+ // adds in the user's group.
90
+ return delegate
91
+ ? new FindByIdDelegateOperation('find', { delegate, id: this.id })
92
+ : assignWhereGroup(new FindByIdOperation('findById', { id: this.id }));
85
93
  }
86
94
 
87
95
  clone() {
@@ -94,6 +102,15 @@ class UpdateAndFetchOperation extends DelegateOperation {
94
102
  }
95
103
  }
96
104
 
105
+ // Finds the rows of a delegate find operation by id, like `UpdateAndFetchOperation`
106
+ // updates them.
107
+ class FindByIdDelegateOperation extends DelegateOperation {
108
+ onBuild(builder) {
109
+ super.onBuild(builder);
110
+ addWhereGroup(builder, () => builder.findById(this.opt.id));
111
+ }
112
+ }
113
+
97
114
  module.exports = {
98
115
  UpdateAndFetchOperation,
99
116
  };
@@ -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 { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
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');
@@ -14,6 +17,7 @@ class UpdateOperation extends QueryBuilderOperation {
14
17
 
15
18
  this.model = null;
16
19
  this.modelOptions = Object.assign({}, this.opt.modelOptions || {});
20
+ this.hasNothingToUpdate = false;
17
21
  }
18
22
 
19
23
  onAdd(builder, args) {
@@ -34,10 +38,19 @@ class UpdateOperation extends QueryBuilderOperation {
34
38
 
35
39
  if (isEmpty(row)) {
36
40
  // Resolve the query if there is nothing to update.
41
+ this.hasNothingToUpdate = !builder.isExplicitlyResolvedOrRejected();
37
42
  builder.resolve(0);
38
43
  }
39
44
  }
40
45
 
46
+ // Returns true if the query was resolved without updating anything, e.g.
47
+ // with `none()` or `resolve()` in a hook, but not if it was only resolved
48
+ // because there is nothing to update in the table, as the update still
49
+ // applies to everything else (#2184).
50
+ isResolvedWithoutUpdate(builder) {
51
+ return builder.isExplicitlyResolvedOrRejected() && !this.hasNothingToUpdate;
52
+ }
53
+
41
54
  onBuildKnex(knexBuilder, builder) {
42
55
  const json = this.model.$toDatabaseJson(builder);
43
56
  const convertedJson = convertFieldExpressionsToRaw(builder, this.model, json);
@@ -56,6 +69,7 @@ class UpdateOperation extends QueryBuilderOperation {
56
69
  clone() {
57
70
  const clone = super.clone();
58
71
  clone.model = this.model;
72
+ clone.hasNothingToUpdate = this.hasNothingToUpdate;
59
73
  return clone;
60
74
  }
61
75
  }
@@ -96,12 +110,13 @@ async function callStaticAfterUpdate(builder, result) {
96
110
 
97
111
  function convertFieldExpressionsToRaw(builder, model, json) {
98
112
  const knex = builder.knex();
113
+ const declaredColumns = getDeclaredColumns(builder.modelClass());
99
114
  const convertedJson = {};
100
115
 
101
116
  for (const key of Object.keys(json)) {
102
117
  let val = json[key];
103
118
 
104
- if (key.indexOf(':') > -1) {
119
+ if (isFieldExpressionKey(key, declaredColumns)) {
105
120
  // 'col:attr' : ref('other:lol') is transformed to
106
121
  // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
107
122
  warnIfNotPostgres(knex);
@@ -130,6 +145,28 @@ function convertFieldExpressionsToRaw(builder, model, json) {
130
145
  return convertedJson;
131
146
  }
132
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
+
133
170
  module.exports = {
134
171
  UpdateOperation,
135
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 `wrapWhereGroups`.
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 { 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
@@ -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
- 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
 
@@ -63,11 +63,11 @@ class Selection {
63
63
  if (selectionInBuilder.column === '*') {
64
64
  if (selectionInBuilder.table) {
65
65
  if (selectionToTest.column === '*') {
66
- return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
66
+ return isSameIdentifier(builder, selectionToTest.table, selectionInBuilder.table);
67
67
  } else {
68
68
  return (
69
69
  selectionToTest.table === null ||
70
- isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
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 === selectionInBuilder.column &&
83
+ isSameIdentifier(builder, selectionToTest.column, selectionInBuilder.column) &&
84
84
  (selectionToTest.table === null ||
85
- isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
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
- // 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
 
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
- if (!wrapIdentifier) {
114
- return false;
115
- }
116
-
117
- const mapTable = (table) =>
118
- table
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
  };
@@ -42,7 +42,9 @@ class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation
42
42
  knexBuilder,
43
43
  columns,
44
44
  helperColumns,
45
- values.as(knex.raw(`V(${helperColumns.map((_) => '??')})`, helperColumns)),
45
+ // knex only accepts strings as aliases, so the subquery is aliased in
46
+ // a raw query.
47
+ knex.raw(`? AS V(${helperColumns.map((_) => '??')})`, [values, ...helperColumns]),
46
48
  );
47
49
  }
48
50
  }
@@ -48,18 +48,13 @@ function isCompositeKey(columns) {
48
48
  }
49
49
 
50
50
  function buildCompositeArgs(columns, values) {
51
- if (!Array.isArray(values)) {
52
- // If the `values` is not an array of values but a function or a subquery
53
- // we have no way to implement this method.
54
- throw new Error(`sqlite doesn't support multi-column where in clauses`);
55
- }
56
-
57
- if (values.length === 0) {
51
+ if (!Array.isArray(values) || values.length === 0) {
58
52
  // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
59
- // into `1 = 1`.
53
+ // into `1 = 1`. Subqueries are compared as row values, which sqlite
54
+ // supports since version 3.15.
60
55
  return {
61
56
  method: 'whereIn',
62
- args: [columns, []],
57
+ args: [columns, values],
63
58
  };
64
59
  }
65
60
 
@@ -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);
@@ -943,12 +945,12 @@ function peg$parse(input, options) {
943
945
  }
944
946
 
945
947
  function assertDuplicateRelation(node, expr) {
946
- if (expr.$name in node) {
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`,
948
+ if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
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
- if (expr.$name in node) {
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`);
7
+ if (Object.prototype.hasOwnProperty.call(node, expr.$name)) {
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;