objection 3.2.0 → 3.3.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 (52) hide show
  1. package/README.md +13 -13
  2. package/lib/model/AjvValidator.js +133 -3
  3. package/lib/queryBuilder/QueryBuilder.js +186 -5
  4. package/lib/queryBuilder/QueryBuilderOperationSupport.js +5 -0
  5. package/lib/queryBuilder/ReferenceBuilder.js +5 -3
  6. package/lib/queryBuilder/RelationExpression.js +13 -1
  7. package/lib/queryBuilder/StaticHookArguments.js +12 -1
  8. package/lib/queryBuilder/graph/GraphUpsert.js +22 -0
  9. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +57 -0
  10. package/lib/queryBuilder/graph/insert/GraphInsert.js +107 -8
  11. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +8 -1
  12. package/lib/queryBuilder/join/JoinResultParser.js +22 -0
  13. package/lib/queryBuilder/join/RelationJoiner.js +71 -22
  14. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +7 -1
  15. package/lib/queryBuilder/operations/InsertGraphOperation.js +2 -2
  16. package/lib/queryBuilder/operations/InsertOperation.js +121 -12
  17. package/lib/queryBuilder/operations/QueryBuilderOperation.js +4 -0
  18. package/lib/queryBuilder/operations/ReturningOperation.js +28 -3
  19. package/lib/queryBuilder/operations/UpdateOperation.js +3 -2
  20. package/lib/queryBuilder/operations/UpsertGraphOperation.js +5 -0
  21. package/lib/queryBuilder/operations/WhereGroupOperation.js +41 -0
  22. package/lib/queryBuilder/operations/eager/EagerOperation.js +4 -0
  23. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +2 -6
  24. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +10 -33
  25. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +3 -3
  26. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -1
  27. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +41 -34
  28. package/lib/queryBuilder/operations/select/Selection.js +95 -4
  29. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +6 -0
  30. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +5 -1
  31. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +9 -0
  32. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +81 -43
  33. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.pegjs +5 -4
  34. package/lib/queryBuilder/whereGroups.js +125 -0
  35. package/lib/relations/Relation.js +5 -2
  36. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -2
  37. package/lib/relations/hasMany/HasManyUnrelateOperation.js +5 -2
  38. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +26 -3
  39. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +66 -0
  40. package/lib/relations/manyToMany/ManyToManyPostgresModifyMixin.js +27 -0
  41. package/lib/relations/manyToMany/ManyToManyRelation.js +70 -48
  42. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +16 -0
  43. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +5 -1
  44. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelatePostgresOperation.js +16 -0
  45. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +12 -0
  46. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +8 -2
  47. package/lib/relations/manyToMany/update/ManyToManyUpdatePostgresOperation.js +12 -0
  48. package/lib/utils/deprecate.js +6 -0
  49. package/lib/utils/identifierMapping.js +15 -1
  50. package/lib/utils/parseFieldExpression.js +28 -0
  51. package/package.json +10 -6
  52. package/typings/objection/index.d.ts +280 -28
@@ -1,14 +1,27 @@
1
1
  'use strict';
2
2
 
3
- const { flatten } = require('../../utils/objectUtils');
3
+ const { flatten, isPlainObject } = require('../../utils/objectUtils');
4
4
  const { ObjectionToKnexConvertingOperation } = require('./ObjectionToKnexConvertingOperation');
5
5
 
6
- // This class's only purpose is to normalize the arguments into an array.
6
+ // This class's purpose is to normalize the arguments into an array and to
7
+ // pass an optional trailing options object (e.g. `includeTriggerModifications`)
8
+ // on to knex.
7
9
  //
8
10
  // In knex, if a single column is given to `returning` it returns an array with the that column's value
9
11
  // in it. If an array is given with a one item inside, the return value is an object.
10
12
  class ReturningOperation extends ObjectionToKnexConvertingOperation {
13
+ constructor(name, opt) {
14
+ super(name, opt);
15
+ this.options = null;
16
+ }
17
+
11
18
  onAdd(builder, args) {
19
+ args = Array.from(args);
20
+
21
+ if (args.length > 1 && isPlainObject(args[args.length - 1])) {
22
+ this.options = args.pop();
23
+ }
24
+
12
25
  args = flatten(args);
13
26
 
14
27
  // Don't add an empty returning list.
@@ -21,7 +34,19 @@ class ReturningOperation extends ObjectionToKnexConvertingOperation {
21
34
 
22
35
  onBuildKnex(knexBuilder, builder) {
23
36
  // Always pass an array of columns to knex.returning.
24
- return knexBuilder.returning(this.getKnexArgs(builder));
37
+ const columns = this.getKnexArgs(builder);
38
+
39
+ if (this.options) {
40
+ return knexBuilder.returning(columns, this.options);
41
+ } else {
42
+ return knexBuilder.returning(columns);
43
+ }
44
+ }
45
+
46
+ clone() {
47
+ const clone = super.clone();
48
+ clone.options = this.options;
49
+ return clone;
25
50
  }
26
51
  }
27
52
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { ref } = require('../../queryBuilder/ReferenceBuilder');
4
4
  const { isEmpty } = require('../../utils/objectUtils');
5
+ const { toPostgresJsonPathLiteral } = require('../../utils/parseFieldExpression');
5
6
  const { isKnexRaw, isKnexQueryBuilder } = require('../../utils/knexUtils');
6
7
  const { QueryBuilderOperation } = require('./QueryBuilderOperation');
7
8
  const { StaticHookArguments } = require('../StaticHookArguments');
@@ -104,7 +105,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
104
105
  // "col" : raw(`jsonb_set("col", '{attr}', to_jsonb("other"#>'{lol}'), true)`)
105
106
 
106
107
  let parsed = ref(key);
107
- let jsonRefs = '{' + parsed.parsedExpr.access.map((it) => it.ref).join(',') + '}';
108
+ let jsonPath = toPostgresJsonPathLiteral(parsed.parsedExpr.access);
108
109
  let valuePlaceholder = '?';
109
110
 
110
111
  if (isKnexQueryBuilder(val) || isKnexRaw(val)) {
@@ -114,7 +115,7 @@ function convertFieldExpressionsToRaw(builder, model, json) {
114
115
  }
115
116
 
116
117
  convertedJson[parsed.column] = knex.raw(
117
- `jsonb_set(??, '${jsonRefs}', ${valuePlaceholder}, true)`,
118
+ `jsonb_set(??, ${jsonPath}, ${valuePlaceholder}, true)`,
118
119
  [convertedJson[parsed.column] || parsed.column, val],
119
120
  );
120
121
 
@@ -40,6 +40,11 @@ class UpsertGraphOperation extends QueryBuilderOperation {
40
40
  return true;
41
41
  }
42
42
 
43
+ onBefore1(builder, result) {
44
+ GraphUpsert.warnOnConflict(builder, this.name);
45
+ return result;
46
+ }
47
+
43
48
  onAfter1(builder) {
44
49
  if (hasOtherSqlModifyingQueryBuilderCalls(builder)) {
45
50
  throw new Error(
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { QueryBuilderOperation } = require('./QueryBuilderOperation');
4
+
5
+ // Builds a set of where operations inside a single knex `where(callback)`
6
+ // group, i.e. wraps them in parentheses. See `wrapWhereGroups`.
7
+ class WhereGroupOperation extends QueryBuilderOperation {
8
+ constructor(name, opt) {
9
+ super(name, opt);
10
+ this.operations = [];
11
+ }
12
+
13
+ onBuildKnex(knexBuilder, builder) {
14
+ const operations = this.operations;
15
+
16
+ return knexBuilder.where(function () {
17
+ let knex = this;
18
+
19
+ const build = (op) => {
20
+ if (op.hasOnBuildKnex()) {
21
+ knex = builder.callOperationMethod(op, 'onBuildKnex', [knex, builder]) || knex;
22
+ }
23
+ };
24
+
25
+ for (const op of operations) {
26
+ build(op);
27
+ op.forEachDescendantOperation(build);
28
+ }
29
+ });
30
+ }
31
+
32
+ clone() {
33
+ const clone = super.clone();
34
+ clone.operations = this.operations.map((op) => op.clone());
35
+ return clone;
36
+ }
37
+ }
38
+
39
+ module.exports = {
40
+ WhereGroupOperation,
41
+ };
@@ -9,6 +9,9 @@ class EagerOperation extends QueryBuilderOperation {
9
9
 
10
10
  this.expression = RelationExpression.create();
11
11
  this.graphOptions = this.opt.defaultGraphOptions;
12
+ // The `joinOperation` options passed to the `withGraphJoined` calls,
13
+ // together with the expressions of the calls.
14
+ this.joinOperations = [];
12
15
  }
13
16
 
14
17
  buildFinalExpression(builder) {
@@ -41,6 +44,7 @@ class EagerOperation extends QueryBuilderOperation {
41
44
  cloneFrom(eagerOp) {
42
45
  this.expression = eagerOp.expression.clone();
43
46
  this.graphOptions = Object.assign({}, eagerOp.graphOptions);
47
+ this.joinOperations = eagerOp.joinOperations.slice();
44
48
  }
45
49
 
46
50
  clone() {
@@ -24,6 +24,7 @@ class JoinEagerOperation extends EagerOperation {
24
24
  .setExpression(this.buildFinalExpression(builder))
25
25
  .setModifiers(this.buildFinalModifiers(builder))
26
26
  .setOptions(this.graphOptions)
27
+ .setJoinOperations(this.joinOperations)
27
28
  .fetchColumnInfo(builder);
28
29
  }
29
30
 
@@ -32,15 +33,10 @@ class JoinEagerOperation extends EagerOperation {
32
33
  .setExpression(this.buildFinalExpression(builder))
33
34
  .setModifiers(this.buildFinalModifiers(builder))
34
35
  .setOptions(this.graphOptions)
36
+ .setJoinOperations(this.joinOperations)
35
37
  .build(builder);
36
38
  }
37
39
 
38
- // Stops the joiner from omitting an internally selected column of the root
39
- // table from the result. Returns true if such a selection existed.
40
- releaseInternalRootSelection(column) {
41
- return !!this.joiner && this.joiner.releaseInternalRootSelection(column);
42
- }
43
-
44
40
  onRawResult(builder, rows) {
45
41
  return this.joiner.parseResult(builder, rows);
46
42
  }
@@ -3,7 +3,6 @@
3
3
  const promiseUtils = require('../../../utils/promiseUtils');
4
4
 
5
5
  const { EagerOperation } = require('./EagerOperation');
6
- const { JoinEagerOperation } = require('./JoinEagerOperation');
7
6
  const { isMsSql, isOracle, isSqlite } = require('../../../utils/knexUtils');
8
7
  const { isObject, asArray, flatten, chunk } = require('../../../utils/objectUtils');
9
8
  const { ValidationErrorType } = require('../../../model/ValidationError');
@@ -16,7 +15,6 @@ class WhereInEagerOperation extends EagerOperation {
16
15
  super(name, opt);
17
16
 
18
17
  this.relationsToFetch = [];
19
- this.omitProps = [];
20
18
  }
21
19
 
22
20
  batchSize(knex) {
@@ -51,8 +49,15 @@ class WhereInEagerOperation extends EagerOperation {
51
49
  builder.select(selectionsToAdd);
52
50
  }
53
51
 
52
+ // Omit the implicitly selected columns from the result once the relations
53
+ // have been fetched.
54
+ if (!builder.internalOptions().keepImplicitJoinProps) {
55
+ for (const prop of selectedProps) {
56
+ builder.addInternalSelection(prop);
57
+ }
58
+ }
59
+
54
60
  this.relationsToFetch = relationsToFetch;
55
- this.omitProps = selectedProps;
56
61
  }
57
62
 
58
63
  async onAfter2(builder, result) {
@@ -75,22 +80,6 @@ class WhereInEagerOperation extends EagerOperation {
75
80
  { concurrency: modelClass.getConcurrency(builder.unsafeKnex()) },
76
81
  );
77
82
 
78
- const intOpt = builder.internalOptions();
79
-
80
- if (!this.omitProps.length || intOpt.keepImplicitJoinProps) {
81
- return result;
82
- }
83
-
84
- // Now that relations have been fetched for `models` we can omit the
85
- // columns that were implicitly selected by this class.
86
- for (let i = 0, l = result.length; i < l; ++i) {
87
- const model = result[i];
88
-
89
- for (let c = 0, lc = this.omitProps.length; c < lc; ++c) {
90
- modelClass.omitImpl(model, this.omitProps[c]);
91
- }
92
- }
93
-
94
83
  return result;
95
84
  }
96
85
 
@@ -205,7 +194,6 @@ class WhereInEagerOperation extends EagerOperation {
205
194
  const clone = super.clone();
206
195
 
207
196
  clone.relationsToFetch = this.relationsToFetch.slice();
208
- clone.omitProps = this.omitProps.slice();
209
197
 
210
198
  return clone;
211
199
  }
@@ -238,11 +226,6 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
238
226
  const selectionsToAdd = [];
239
227
  const selectedProps = [];
240
228
 
241
- // When mixed with `withGraphJoined`, the join operation may have selected
242
- // some of the needed columns internally, in which case it would omit them
243
- // from the result models before we get to use them.
244
- const joinOp = builder.findOperation(JoinEagerOperation);
245
-
246
229
  // Collect columns that need to be selected for the eager fetch
247
230
  // to work that are not currently selected.
248
231
  for (const { relation } of relationsToFetch) {
@@ -253,15 +236,9 @@ function findRelationPropsToSelect(builder, relationsToFetch) {
253
236
  const prop = ownerProp.props[c];
254
237
  const col = ownerProp.cols[c];
255
238
 
256
- if (!builder.hasSelectionAs(fullCol, col)) {
257
- if (selectionsToAdd.indexOf(fullCol) === -1) {
258
- selectedProps.push(prop);
259
- selectionsToAdd.push(fullCol);
260
- }
261
- } else if (joinOp && joinOp.releaseInternalRootSelection(col)) {
262
- // The join operation no longer omits the column, so we need to
263
- // omit the property once the relations have been fetched.
239
+ if (!builder.hasSelectionAs(fullCol, col) && selectionsToAdd.indexOf(fullCol) === -1) {
264
240
  selectedProps.push(prop);
241
+ selectionsToAdd.push(fullCol);
265
242
  }
266
243
  }
267
244
  }
@@ -7,7 +7,7 @@ class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
7
7
  onBuildKnex(knexBuilder, builder) {
8
8
  const args = this.getKnexArgs(builder);
9
9
 
10
- const sql = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
10
+ const rawArgs = jsonApi.whereJsonFieldRightStringArrayOnLeftQuery(
11
11
  builder.knex(),
12
12
  args[0],
13
13
  this.opt.operator,
@@ -15,9 +15,9 @@ class WhereJsonHasPostgresOperation extends ObjectionToKnexConvertingOperation {
15
15
  );
16
16
 
17
17
  if (this.opt.bool === 'or') {
18
- knexBuilder = knexBuilder.orWhereRaw(sql);
18
+ knexBuilder = knexBuilder.orWhereRaw(...rawArgs);
19
19
  } else {
20
- knexBuilder = knexBuilder.whereRaw(sql);
20
+ knexBuilder = knexBuilder.whereRaw(...rawArgs);
21
21
  }
22
22
 
23
23
  return knexBuilder;
@@ -18,7 +18,7 @@ class WhereJsonNotObjectPostgresOperation extends ObjectionToKnexConvertingOpera
18
18
  'not',
19
19
  );
20
20
 
21
- builder.orWhereRaw(jsonApi.whereJsonFieldQuery(knex, fieldExpression, 'IS', null));
21
+ builder.orWhereRaw(...jsonApi.whereJsonFieldQuery(knex, fieldExpression, 'IS', null));
22
22
  };
23
23
 
24
24
  if (this.opt.bool === 'or') {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const parser = require('../../../utils/parseFieldExpression');
4
4
  const { asArray, isObject, isString } = require('../../../utils/objectUtils');
5
+ const { isKnexRaw, isKnexQueryBuilder } = require('../../../utils/knexUtils');
5
6
 
6
7
  /**
7
8
  * @typedef {String} FieldExpression
@@ -38,22 +39,24 @@ const { asArray, isObject, isString } = require('../../../utils/objectUtils');
38
39
  * 4. Keys containing square brackets and quotes
39
40
  * `objectColumn:['Double."Quote".[]']` and `objectColumn:["Sinlge.'Quote'.[]"]`
40
41
  * Column `{ "Double.\"Quote\".[]" : "I was referred", "Single.'Quote'.[]" : "Mee too!" }`
42
+ * 5. Empty keys `objectColumn:[""]` or `objectColumn:['']` Column `{ "" : "I was referred" }`
41
43
  * 99. Keys containing dots, square brackets, single quotes and double quotes in one json key is
42
44
  * not currently supported
43
45
  */
44
46
 
47
+ // Returns the SQL and bindings of a field expression. The column is passed as an
48
+ // identifier binding (`??`), so that knex's `wrapIdentifier` (e.g. used by
49
+ // `knexSnakeCaseMappers()`) is applied to it.
45
50
  function parseFieldExpression(expression, extractAsText) {
46
51
  let parsed = parser.parseFieldExpression(expression);
47
- let middleQuotedColumnName = parsed.columnName.split('.').join('"."');
48
- let column = `"${middleQuotedColumnName}"`;
52
+ let bindings = [parsed.columnName];
49
53
  // Reference the bare column when there is no json path, so that Postgres can
50
54
  // use indexes on it. `#>> '{}'` is still needed to extract the value as text.
51
55
  if (parsed.access.length === 0 && !extractAsText) {
52
- return column;
56
+ return ['??', bindings];
53
57
  }
54
- let jsonRefs = parsed.access.map((it) => it.ref).join(',');
55
58
  let extractor = extractAsText ? '#>>' : '#>';
56
- return `${column}${extractor}'{${jsonRefs}}'`;
59
+ return [`??${extractor}${parser.toPostgresJsonPathLiteral(parsed.access)}`, bindings];
57
60
  }
58
61
 
59
62
  function whereJsonbRefOnLeftJsonbValOrRefOnRight(
@@ -78,36 +81,36 @@ function whereJsonbRefOnLeftJsonbValOrRefOnRightRawQueryParams(
78
81
  jsonObjectOrFieldExpression,
79
82
  queryPrefix,
80
83
  ) {
81
- let fieldReference = parseFieldExpression(fieldExpression);
84
+ let [fieldReference, bindings] = parseFieldExpression(fieldExpression);
85
+ let [rightHandSql, rightHandBindings] = rightHandExpression(jsonObjectOrFieldExpression);
86
+ let query = ['(', fieldReference, ')::jsonb', operator, rightHandSql];
82
87
 
88
+ if (queryPrefix) {
89
+ query.unshift(queryPrefix);
90
+ }
91
+
92
+ return [query.join(' '), [...bindings, ...rightHandBindings]];
93
+ }
94
+
95
+ function rightHandExpression(jsonObjectOrFieldExpression) {
83
96
  if (isString(jsonObjectOrFieldExpression)) {
84
- let rightHandReference = parseFieldExpression(jsonObjectOrFieldExpression);
85
- let refRefQuery = [
86
- '(',
87
- fieldReference,
88
- ')::jsonb',
89
- operator,
90
- '(',
91
- rightHandReference,
92
- ')::jsonb',
93
- ];
94
- if (queryPrefix) {
95
- refRefQuery.unshift(queryPrefix);
96
- }
97
- return [refRefQuery.join(' ')];
97
+ let [rightHandReference, bindings] = parseFieldExpression(jsonObjectOrFieldExpression);
98
+ return [`( ${rightHandReference} )::jsonb`, bindings];
99
+ } else if (
100
+ isKnexRaw(jsonObjectOrFieldExpression) ||
101
+ isKnexQueryBuilder(jsonObjectOrFieldExpression)
102
+ ) {
103
+ // ref(), val(), raw() and subqueries, already converted to knex by the operation.
104
+ return ['( ? )::jsonb', [jsonObjectOrFieldExpression]];
98
105
  } else if (isObject(jsonObjectOrFieldExpression)) {
99
- let refValQuery = ['(', fieldReference, ')::jsonb', operator, '?::jsonb'];
100
- if (queryPrefix) {
101
- refValQuery.unshift(queryPrefix);
102
- }
103
- return [refValQuery.join(' '), JSON.stringify(jsonObjectOrFieldExpression)];
106
+ return ['?::jsonb', [JSON.stringify(jsonObjectOrFieldExpression)]];
104
107
  }
105
108
 
106
109
  throw new Error('Invalid right hand expression.');
107
110
  }
108
111
 
109
112
  function whereJsonFieldRightStringArrayOnLeftQuery(knex, fieldExpression, operator, keys) {
110
- let fieldReference = parseFieldExpression(fieldExpression);
113
+ let [fieldReference, bindings] = parseFieldExpression(fieldExpression);
111
114
  keys = asArray(keys);
112
115
 
113
116
  let questionMarksArray = keys.map((key) => {
@@ -117,19 +120,19 @@ function whereJsonFieldRightStringArrayOnLeftQuery(knex, fieldExpression, operat
117
120
  return '?';
118
121
  });
119
122
 
120
- let rawSqlTemplateString = 'array[' + questionMarksArray.join(',') + ']';
121
- let rightHandExpression = knex.raw(rawSqlTemplateString, keys);
122
-
123
- return `${fieldReference} ${operator.replace('?', '\\?')} ${rightHandExpression}`;
123
+ return [
124
+ `${fieldReference} ${operator.replace('?', '\\?')} array[${questionMarksArray.join(',')}]`,
125
+ [...bindings, ...keys],
126
+ ];
124
127
  }
125
128
 
126
129
  function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
127
- let fieldReference = parseFieldExpression(fieldExpression, true);
130
+ let [fieldReference, bindings] = parseFieldExpression(fieldExpression, true);
128
131
  let normalizedOperator = normalizeOperator(knex, operator);
129
132
 
130
133
  // json type comparison takes json type in string format
131
134
  let cast;
132
- let escapedValue = knex.raw(' ?', [value]);
135
+ let valuePlaceholder = '?';
133
136
  let type = typeof value;
134
137
 
135
138
  if (type === 'number') {
@@ -140,12 +143,16 @@ function whereJsonFieldQuery(knex, fieldExpression, operator, value) {
140
143
  cast = '::TEXT';
141
144
  } else if (value === null) {
142
145
  cast = '::TEXT';
143
- escapedValue = 'NULL';
146
+ valuePlaceholder = 'NULL';
144
147
  } else {
145
148
  throw new Error('Value must be string, number, boolean or null.');
146
149
  }
147
150
 
148
- return `(${fieldReference})${cast} ${normalizedOperator} ${escapedValue}`;
151
+ // Bind the value so that `?` in it isn't read as a binding placeholder.
152
+ return [
153
+ `(${fieldReference})${cast} ${normalizedOperator} ${valuePlaceholder}`,
154
+ value === null ? bindings : [...bindings, value],
155
+ ];
149
156
  }
150
157
 
151
158
  function normalizeOperator(knex, operator) {
@@ -1,14 +1,21 @@
1
1
  'use strict';
2
2
 
3
- const { isString, isObject } = require('../../../utils/objectUtils');
3
+ const { isString, isObject, isPlainObject } = require('../../../utils/objectUtils');
4
+ const { isKnexQueryBuilder, isKnexRaw } = require('../../../utils/knexUtils');
4
5
 
5
6
  const ALIAS_REGEX = /\s+as\s+/i;
6
7
 
8
+ // Matches an alias at the end of a raw sql string: `as "alias"`, `as \`alias\``,
9
+ // `as [alias]`, `as ??`, `as :name:` or `as alias`.
10
+ const RAW_ALIAS_REGEX =
11
+ /\s+as\s+(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|(\?\?)|:(\w+):|([a-z_][\w$]*))\s*$/i;
12
+
7
13
  class Selection {
8
14
  constructor(table, column, alias) {
9
15
  this.table = table;
10
16
  this.column = column;
11
17
  this.alias = alias;
18
+ this.keepsSelectAll = false;
12
19
  }
13
20
 
14
21
  get name() {
@@ -23,6 +30,12 @@ class Selection {
23
30
  return createSelectionFromReference(selection);
24
31
  } else if (selection.isObjectionRawBuilder) {
25
32
  return createSelectionFromRaw(selection);
33
+ } else if (selection.isObjectionQueryBuilderBase) {
34
+ return createSelectionFromQueryBuilder(selection);
35
+ } else if (isKnexQueryBuilder(selection)) {
36
+ return createSelectionFromAlias(selection._single.as);
37
+ } else if (isKnexRaw(selection)) {
38
+ return createSelectionFromAlias(parseRawAlias(selection.sql, selection.bindings));
26
39
  } else {
27
40
  return null;
28
41
  }
@@ -50,10 +63,11 @@ class Selection {
50
63
  if (selectionInBuilder.column === '*') {
51
64
  if (selectionInBuilder.table) {
52
65
  if (selectionToTest.column === '*') {
53
- return selectionToTest.table === selectionInBuilder.table;
66
+ return isSameTable(builder, selectionToTest.table, selectionInBuilder.table);
54
67
  } else {
55
68
  return (
56
- selectionToTest.table === null || selectionToTest.table === selectionInBuilder.table
69
+ selectionToTest.table === null ||
70
+ isSameTable(builder, selectionToTest.table, selectionInBuilder.table)
57
71
  );
58
72
  }
59
73
  } else {
@@ -67,7 +81,8 @@ class Selection {
67
81
  } else {
68
82
  return (
69
83
  selectionToTest.column === selectionInBuilder.column &&
70
- (selectionToTest.table === null || selectionToTest.table === selectionInBuilderTable)
84
+ (selectionToTest.table === null ||
85
+ isSameTable(builder, selectionToTest.table, selectionInBuilderTable))
71
86
  );
72
87
  }
73
88
  }
@@ -82,6 +97,36 @@ Object.defineProperties(Selection.prototype, {
82
97
  },
83
98
  });
84
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:
102
+ // `dataExtension` and `data_extension` both end up as `data_extension`.
103
+ function isSameTable(builder, table1, table2) {
104
+ if (table1 === table2) {
105
+ return true;
106
+ } else if (!table1 || !table2) {
107
+ return false;
108
+ }
109
+
110
+ const knex = builder.unsafeKnex();
111
+ const wrapIdentifier = knex && knex.client && knex.client.config.wrapIdentifier;
112
+
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);
124
+ }
125
+
126
+ function identity(value) {
127
+ return value;
128
+ }
129
+
85
130
  function createSelectionFromReference(ref) {
86
131
  return new Selection(ref.tableName, ref.column, ref.alias);
87
132
  }
@@ -89,6 +134,52 @@ function createSelectionFromReference(ref) {
89
134
  function createSelectionFromRaw(raw) {
90
135
  if (raw.alias) {
91
136
  return new Selection(null, null, raw.alias);
137
+ } else {
138
+ return createSelectionFromAlias(parseRawAlias(raw._sql, raw._args));
139
+ }
140
+ }
141
+
142
+ function parseRawAlias(sql, bindings) {
143
+ const match = isString(sql) && RAW_ALIAS_REGEX.exec(sql);
144
+
145
+ if (!match) {
146
+ return null;
147
+ }
148
+
149
+ const [, doubleQuoted, backQuoted, bracketQuoted, binding, namedBinding, unquoted] = match;
150
+
151
+ if (binding) {
152
+ return Array.isArray(bindings) ? bindings[bindings.length - 1] : null;
153
+ } else if (namedBinding) {
154
+ if (Array.isArray(bindings) && bindings.length === 1) {
155
+ // `raw(sql, { name: value })` of objection.
156
+ bindings = bindings[0];
157
+ }
158
+
159
+ return isPlainObject(bindings) ? bindings[namedBinding] : null;
160
+ } else if (unquoted) {
161
+ // Some databases, like postgres, fold unquoted identifiers to lower case
162
+ // and others don't, so only lower case ones can be used reliably.
163
+ return unquoted === unquoted.toLowerCase() ? unquoted : null;
164
+ } else {
165
+ return doubleQuoted || backQuoted || bracketQuoted;
166
+ }
167
+ }
168
+
169
+ // Subqueries aliased using `as()` select a column with the alias as name.
170
+ function createSelectionFromQueryBuilder(builder) {
171
+ const asOperation = builder.findLastOperation('as');
172
+ return createSelectionFromAlias(asOperation && asOperation.args[0]);
173
+ }
174
+
175
+ function createSelectionFromAlias(alias) {
176
+ if (isString(alias)) {
177
+ const selection = new Selection(null, null, alias);
178
+ // These selections weren't recognized in the past. To not change which
179
+ // other columns `withGraphJoined` selects, they don't count as explicit
180
+ // selections when deciding whether all columns are selected.
181
+ selection.keepsSelectAll = true;
182
+ return selection;
92
183
  } else {
93
184
  return null;
94
185
  }
@@ -26,6 +26,12 @@ class WhereInCompositeMsSqlOperation extends ObjectionToKnexConvertingOperation
26
26
  }
27
27
 
28
28
  buildComposite(knex, knexBuilder, columns, values) {
29
+ if (Array.isArray(values) && values.length === 0) {
30
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
31
+ // into `1 = 1`.
32
+ return this.whereIn(knexBuilder, columns, []);
33
+ }
34
+
29
35
  const helperColumns = columns.map((_, index) => getTempColumn(index));
30
36
 
31
37
  if (Array.isArray(values)) {
@@ -48,7 +48,11 @@ function buildCompositeArgs(knex, columns, values) {
48
48
  }
49
49
 
50
50
  function buildCompositeValueArgs(columns, values) {
51
- if (!Array.isArray(values[0])) {
51
+ if (values.length === 0) {
52
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
53
+ // into `1 = 1`.
54
+ return [columns, []];
55
+ } else if (!Array.isArray(values[0])) {
52
56
  return [columns, [values]];
53
57
  } else {
54
58
  return [columns, values];
@@ -54,6 +54,15 @@ function buildCompositeArgs(columns, values) {
54
54
  throw new Error(`sqlite doesn't support multi-column where in clauses`);
55
55
  }
56
56
 
57
+ if (values.length === 0) {
58
+ // knex turns an empty `whereIn` into `1 = 0` and an empty `whereNotIn`
59
+ // into `1 = 1`.
60
+ return {
61
+ method: 'whereIn',
62
+ args: [columns, []],
63
+ };
64
+ }
65
+
57
66
  // Sqlite doesn't support the `where in` syntax for multiple columns but
58
67
  // we can emulate it using grouped `or` clauses.
59
68
  return {