@mikro-orm/knex 6.0.0-dev.20 → 6.0.0-dev.201

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 (44) hide show
  1. package/AbstractSqlConnection.d.ts +8 -4
  2. package/AbstractSqlConnection.js +30 -18
  3. package/AbstractSqlDriver.d.ts +33 -25
  4. package/AbstractSqlDriver.js +575 -268
  5. package/AbstractSqlPlatform.d.ts +5 -4
  6. package/AbstractSqlPlatform.js +34 -5
  7. package/MonkeyPatchable.d.ts +1 -0
  8. package/MonkeyPatchable.js +3 -0
  9. package/PivotCollectionPersister.d.ts +17 -0
  10. package/PivotCollectionPersister.js +128 -0
  11. package/README.md +64 -107
  12. package/SqlEntityManager.d.ts +2 -6
  13. package/SqlEntityManager.js +4 -8
  14. package/SqlEntityRepository.d.ts +7 -6
  15. package/SqlEntityRepository.js +11 -8
  16. package/index.mjs +24 -5
  17. package/package.json +8 -8
  18. package/query/ArrayCriteriaNode.d.ts +5 -4
  19. package/query/ArrayCriteriaNode.js +7 -2
  20. package/query/CriteriaNode.d.ts +11 -9
  21. package/query/CriteriaNode.js +24 -11
  22. package/query/CriteriaNodeFactory.d.ts +6 -6
  23. package/query/CriteriaNodeFactory.js +32 -15
  24. package/query/ObjectCriteriaNode.d.ts +5 -4
  25. package/query/ObjectCriteriaNode.js +57 -23
  26. package/query/QueryBuilder.d.ts +58 -26
  27. package/query/QueryBuilder.js +444 -148
  28. package/query/QueryBuilderHelper.d.ts +13 -18
  29. package/query/QueryBuilderHelper.js +194 -167
  30. package/query/ScalarCriteriaNode.d.ts +4 -3
  31. package/query/ScalarCriteriaNode.js +12 -8
  32. package/query/enums.d.ts +7 -0
  33. package/query/enums.js +10 -2
  34. package/schema/DatabaseSchema.d.ts +5 -2
  35. package/schema/DatabaseSchema.js +28 -10
  36. package/schema/DatabaseTable.d.ts +13 -5
  37. package/schema/DatabaseTable.js +385 -60
  38. package/schema/SchemaComparator.d.ts +5 -5
  39. package/schema/SchemaComparator.js +59 -35
  40. package/schema/SchemaHelper.d.ts +7 -5
  41. package/schema/SchemaHelper.js +25 -11
  42. package/schema/SqlSchemaGenerator.d.ts +3 -6
  43. package/schema/SqlSchemaGenerator.js +71 -36
  44. package/typings.d.ts +30 -11
@@ -4,10 +4,14 @@ exports.AbstractSqlDriver = void 0;
4
4
  const core_1 = require("@mikro-orm/core");
5
5
  const query_1 = require("./query");
6
6
  const SqlEntityManager_1 = require("./SqlEntityManager");
7
+ const PivotCollectionPersister_1 = require("./PivotCollectionPersister");
7
8
  class AbstractSqlDriver extends core_1.DatabaseDriver {
9
+ [core_1.EntityManagerType];
10
+ connection;
11
+ replicas = [];
12
+ platform;
8
13
  constructor(config, platform, connection, connector) {
9
14
  super(config, connector);
10
- this.replicas = [];
11
15
  this.connection = new connection(this.config);
12
16
  this.replicas = this.createReplicas(conf => new connection(this.config, conf, 'read'));
13
17
  this.platform = platform;
@@ -25,30 +29,43 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
25
29
  return this.findVirtual(entityName, where, options);
26
30
  }
27
31
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
28
- const joinedProps = this.joinedProps(meta, populate);
29
- const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false);
30
- const fields = this.buildFields(meta, populate, joinedProps, qb, options.fields);
31
- const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(entityName, qb, meta, joinedProps);
32
+ const joinedProps = this.joinedProps(meta, populate, options);
33
+ const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
34
+ const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options.fields, options);
35
+ const orderBy = this.buildOrderBy(qb, meta, populate, options);
36
+ core_1.Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
32
37
  if (core_1.Utils.isPrimaryKey(where, meta.compositePK)) {
33
38
  where = { [core_1.Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
34
39
  }
40
+ const { first, last, before, after } = options;
41
+ const isCursorPagination = [first, last, before, after].some(v => v != null);
42
+ qb.__populateWhere = options._populateWhere;
35
43
  qb.select(fields)
44
+ // only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
36
45
  .populate(populate, joinedProps.length > 0 ? options.populateWhere : undefined)
37
46
  .where(where)
38
- .orderBy([...core_1.Utils.asArray(options.orderBy), ...joinedPropsOrderBy])
39
47
  .groupBy(options.groupBy)
40
48
  .having(options.having)
49
+ .indexHint(options.indexHint)
50
+ .comment(options.comments)
51
+ .hintComment(options.hintComments)
41
52
  .withSchema(this.getSchemaName(meta, options));
42
- if (options.limit !== undefined) {
53
+ if (isCursorPagination) {
54
+ const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
55
+ qb.andWhere(where).orderBy(newOrderBy);
56
+ }
57
+ else {
58
+ qb.orderBy(orderBy);
59
+ }
60
+ if (options.limit != null || options.offset != null) {
43
61
  qb.limit(options.limit, options.offset);
44
62
  }
45
63
  if (options.lockMode) {
46
64
  qb.setLockMode(options.lockMode, options.lockTableAliases);
47
65
  }
48
- core_1.Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
49
66
  const result = await this.rethrow(qb.execute('all'));
50
- if (joinedProps.length > 0) {
51
- return this.mergeJoinedResult(result, meta);
67
+ if (isCursorPagination && !first && !!last) {
68
+ result.reverse();
52
69
  }
53
70
  return result;
54
71
  }
@@ -56,63 +73,76 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
56
73
  const opts = { populate: [], ...(options || {}) };
57
74
  const meta = this.metadata.find(entityName);
58
75
  const populate = this.autoJoinOneToOneOwner(meta, opts.populate, opts.fields);
59
- const joinedProps = this.joinedProps(meta, populate);
60
- if (joinedProps.length === 0) {
76
+ const joinedProps = this.joinedProps(meta, populate, options);
77
+ const hasToManyJoins = joinedProps.some(hint => this.hasToManyJoins(hint, meta));
78
+ if (joinedProps.length === 0 || !hasToManyJoins) {
61
79
  opts.limit = 1;
62
80
  }
63
81
  if (opts.limit > 0 && !opts.flags?.includes(core_1.QueryFlag.DISABLE_PAGINATE)) {
64
- opts.flags ?? (opts.flags = []);
82
+ opts.flags ??= [];
65
83
  opts.flags.push(core_1.QueryFlag.DISABLE_PAGINATE);
66
84
  }
67
85
  const res = await this.find(entityName, where, opts);
68
86
  return res[0] || null;
69
87
  }
70
- async findVirtual(entityName, where, options) {
71
- const meta = this.metadata.get(entityName);
72
- /* istanbul ignore next */
73
- if (!meta.expression) {
74
- return [];
88
+ hasToManyJoins(hint, meta) {
89
+ const [propName] = hint.field.split(':', 2);
90
+ const prop = meta.properties[propName];
91
+ if (prop && [core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
92
+ return true;
75
93
  }
76
- if (typeof meta.expression === 'string') {
77
- return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options);
78
- }
79
- const em = this.createEntityManager(false);
80
- em.setTransactionContext(options.ctx);
81
- const res = meta.expression(em, where, options);
82
- if (res instanceof (query_1.QueryBuilder)) {
83
- return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options);
94
+ if (hint.children && prop.targetMeta) {
95
+ return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
84
96
  }
85
- /* istanbul ignore next */
86
- return res;
97
+ return false;
98
+ }
99
+ async findVirtual(entityName, where, options) {
100
+ return this.findFromVirtual(entityName, where, options, query_1.QueryType.SELECT);
87
101
  }
88
102
  async countVirtual(entityName, where, options) {
103
+ return this.findFromVirtual(entityName, where, options, query_1.QueryType.COUNT);
104
+ }
105
+ async findFromVirtual(entityName, where, options, type) {
89
106
  const meta = this.metadata.get(entityName);
90
107
  /* istanbul ignore next */
91
108
  if (!meta.expression) {
92
- return 0;
109
+ return type === query_1.QueryType.SELECT ? [] : 0;
93
110
  }
94
111
  if (typeof meta.expression === 'string') {
95
- return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, query_1.QueryType.COUNT);
112
+ return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
96
113
  }
97
114
  const em = this.createEntityManager(false);
98
115
  em.setTransactionContext(options.ctx);
99
116
  const res = meta.expression(em, where, options);
100
- if (res instanceof (query_1.QueryBuilder)) {
101
- return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options, query_1.QueryType.COUNT);
117
+ if (typeof res === 'string') {
118
+ return this.wrapVirtualExpressionInSubquery(meta, res, where, options, type);
119
+ }
120
+ if (res instanceof query_1.QueryBuilder) {
121
+ return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options, type);
122
+ }
123
+ if (core_1.Utils.isObject(res)) {
124
+ const { sql, bindings } = res.toSQL();
125
+ const query = this.platform.formatQuery(sql, bindings);
126
+ return this.wrapVirtualExpressionInSubquery(meta, query, where, options, type);
102
127
  }
103
128
  /* istanbul ignore next */
104
129
  return res;
105
130
  }
106
- async wrapVirtualExpressionInSubquery(meta, expression, where, options, type = query_1.QueryType.SELECT) {
131
+ async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
107
132
  const qb = this.createQueryBuilder(meta.className, options?.ctx, options.connectionType, options.convertCustomTypes)
108
- .limit(options?.limit, options?.offset);
109
- if (options.orderBy) {
110
- qb.orderBy(options.orderBy);
133
+ .indexHint(options.indexHint)
134
+ .comment(options.comments)
135
+ .hintComment(options.hintComments);
136
+ if (type !== query_1.QueryType.COUNT) {
137
+ qb.limit(options?.limit, options?.offset);
138
+ if (options.orderBy) {
139
+ qb.orderBy(options.orderBy);
140
+ }
111
141
  }
112
142
  qb.where(where);
113
143
  const kqb = qb.getKnexQuery().clear('select');
114
144
  if (type === query_1.QueryType.COUNT) {
115
- kqb.select(qb.raw('count(*) as count'));
145
+ kqb.select(this.connection.getKnex().raw('count(*) as count'));
116
146
  }
117
147
  else { // select
118
148
  kqb.select('*');
@@ -131,40 +161,88 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
131
161
  return null;
132
162
  }
133
163
  if (qb) {
164
+ // here we map the aliased results (cartesian product) to an object graph
134
165
  this.mapJoinedProps(ret, meta, populate, qb, ret, map);
135
166
  }
136
167
  return ret;
137
168
  }
138
169
  mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
139
170
  const joinedProps = this.joinedProps(meta, populate);
140
- joinedProps.forEach(p => {
141
- const relation = meta.properties[p.field];
171
+ joinedProps.forEach(hint => {
172
+ const [propName, ref] = hint.field.split(':', 2);
173
+ const prop = meta.properties[propName];
142
174
  /* istanbul ignore next */
143
- if (!relation) {
175
+ if (!prop) {
176
+ return;
177
+ }
178
+ const pivotRefJoin = prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref;
179
+ const meta2 = this.metadata.find(prop.type);
180
+ let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
181
+ if (!parentJoinPath) {
182
+ path = '[populate]' + path;
183
+ }
184
+ if (pivotRefJoin) {
185
+ path += '[pivot]';
186
+ }
187
+ const relationAlias = qb.getAliasForJoinPath(path, { matchPopulateJoins: true });
188
+ // pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
189
+ if (pivotRefJoin) {
190
+ let item;
191
+ if (prop.inverseJoinColumns.length > 1) { // composite keys
192
+ item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
193
+ }
194
+ else {
195
+ const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}`;
196
+ item = root[alias];
197
+ }
198
+ prop.joinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
199
+ prop.inverseJoinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
200
+ result[prop.name] ??= [];
201
+ if (item) {
202
+ result[prop.name].push(item);
203
+ }
144
204
  return;
145
205
  }
146
- const meta2 = this.metadata.find(relation.type);
147
- const path = parentJoinPath ? `${parentJoinPath}.${relation.name}` : `${meta.name}.${relation.name}`;
148
- const relationAlias = qb.getAliasForJoinPath(path);
149
- const relationPojo = {};
150
206
  // If the primary key value for the relation is null, we know we haven't joined to anything
151
207
  // and therefore we don't return any record (since all values would be null)
152
208
  const hasPK = meta2.primaryKeys.every(pk => meta2.properties[pk].fieldNames.every(name => {
153
209
  return root[`${relationAlias}__${name}`] != null;
154
210
  }));
155
211
  if (!hasPK) {
156
- // initialize empty collections
157
- if ([core_1.ReferenceType.MANY_TO_MANY, core_1.ReferenceType.ONE_TO_MANY].includes(relation.reference)) {
158
- result[relation.name] = result[relation.name] || [];
212
+ if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
213
+ result[prop.name] ??= [];
214
+ }
215
+ if ([core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
216
+ result[prop.name] ??= null;
159
217
  }
160
218
  return;
161
219
  }
220
+ const relationPojo = {};
162
221
  meta2.props
163
- .filter(prop => this.platform.shouldHaveColumn(prop, p.children || []))
222
+ .filter(prop => prop.persist === false && prop.fieldNames)
223
+ .filter(prop => !prop.lazy || populate.some(p => p.field === prop.name || p.all))
164
224
  .forEach(prop => {
225
+ /* istanbul ignore if */
165
226
  if (prop.fieldNames.length > 1) { // composite keys
166
227
  relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
228
+ }
229
+ else {
230
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
231
+ relationPojo[prop.name] = root[alias];
232
+ }
233
+ });
234
+ meta2.props
235
+ .filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []))
236
+ .forEach(prop => {
237
+ if (prop.fieldNames.length > 1) { // composite keys
238
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
167
239
  prop.fieldNames.map(name => delete root[`${relationAlias}__${name}`]);
240
+ relationPojo[prop.name] = core_1.Utils.mapFlatCompositePrimaryKey(fk, prop);
241
+ }
242
+ else if (prop.runtimeType === 'Date') {
243
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
244
+ relationPojo[prop.name] = (typeof root[alias] === 'string' ? new Date(root[alias]) : root[alias]);
245
+ delete root[alias];
168
246
  }
169
247
  else {
170
248
  const alias = `${relationAlias}__${prop.fieldNames[0]}`;
@@ -172,41 +250,26 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
172
250
  delete root[alias];
173
251
  }
174
252
  });
175
- const key = `${meta.name}-${(core_1.Utils.getCompositeKeyHash(result, meta))}`;
176
- if (map[key]) {
177
- result[relation.name] = map[key][relation.name];
253
+ if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
254
+ result[prop.name] ??= [];
255
+ result[prop.name].push(relationPojo);
178
256
  }
179
257
  else {
180
- map[key] = result;
181
- }
182
- if ([core_1.ReferenceType.MANY_TO_MANY, core_1.ReferenceType.ONE_TO_MANY].includes(relation.reference)) {
183
- result[relation.name] = result[relation.name] || [];
184
- this.appendToCollection(meta2, result[relation.name], relationPojo);
258
+ result[prop.name] = relationPojo;
185
259
  }
186
- else {
187
- result[relation.name] = relationPojo;
188
- }
189
- const populateChildren = p.children || [];
260
+ const populateChildren = hint.children || [];
190
261
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
191
262
  });
192
263
  }
193
- appendToCollection(meta, collection, relationPojo) {
194
- if (collection.length === 0) {
195
- return void collection.push(relationPojo);
196
- }
197
- const last = collection[collection.length - 1];
198
- const pk1 = core_1.Utils.getCompositeKeyHash(last, meta);
199
- const pk2 = core_1.Utils.getCompositeKeyHash(relationPojo, meta);
200
- if (pk1 !== pk2) {
201
- collection.push(relationPojo);
202
- }
203
- }
204
264
  async count(entityName, where, options = {}) {
205
265
  const meta = this.metadata.find(entityName);
206
266
  if (meta?.virtual) {
207
267
  return this.countVirtual(entityName, where, options);
208
268
  }
209
269
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false)
270
+ .indexHint(options.indexHint)
271
+ .comment(options.comments)
272
+ .hintComment(options.hintComments)
210
273
  .groupBy(options.groupBy)
211
274
  .having(options.having)
212
275
  .populate(options.populate ?? [])
@@ -215,7 +278,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
215
278
  return this.rethrow(qb.getCount());
216
279
  }
217
280
  async nativeInsert(entityName, data, options = {}) {
218
- options.convertCustomTypes ?? (options.convertCustomTypes = true);
281
+ options.convertCustomTypes ??= true;
219
282
  const meta = this.metadata.find(entityName);
220
283
  const collections = this.extractManyToMany(entityName, data);
221
284
  const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
@@ -235,16 +298,20 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
235
298
  return res;
236
299
  }
237
300
  async nativeInsertMany(entityName, data, options = {}) {
238
- options.processCollections ?? (options.processCollections = true);
239
- options.convertCustomTypes ?? (options.convertCustomTypes = true);
240
- const meta = this.metadata.find(entityName);
301
+ options.processCollections ??= true;
302
+ options.convertCustomTypes ??= true;
303
+ const meta = this.metadata.find(entityName)?.root;
241
304
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
242
305
  const pks = this.getPrimaryKeyFields(entityName);
243
306
  const set = new Set();
244
- data.forEach(row => Object.keys(row).forEach(k => set.add(k)));
307
+ data.forEach(row => core_1.Utils.keys(row).forEach(k => set.add(k)));
245
308
  const props = [...set].map(name => meta?.properties[name] ?? { name, fieldNames: [name] });
246
- const fields = core_1.Utils.flatten(props.map(prop => prop.fieldNames));
309
+ let fields = core_1.Utils.flatten(props.map(prop => prop.fieldNames));
310
+ const duplicates = core_1.Utils.findDuplicates(fields);
247
311
  const params = [];
312
+ if (duplicates.length) {
313
+ fields = core_1.Utils.unique(fields);
314
+ }
248
315
  /* istanbul ignore next */
249
316
  const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
250
317
  let sql = `insert into ${tableName} `;
@@ -255,29 +322,62 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
255
322
  else {
256
323
  sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
257
324
  }
325
+ const addParams = (prop, row) => {
326
+ let value = row[prop.name];
327
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED && prop.object) {
328
+ if (prop.array) {
329
+ for (let i = 0; i < value.length; i++) {
330
+ const item = value[i];
331
+ value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
332
+ }
333
+ }
334
+ else {
335
+ value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
336
+ }
337
+ }
338
+ if (options.convertCustomTypes && prop.customType) {
339
+ params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
340
+ return;
341
+ }
342
+ params.push(value);
343
+ };
258
344
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
259
345
  sql += data.map(row => {
260
346
  const keys = [];
347
+ const usedDups = [];
261
348
  props.forEach(prop => {
262
349
  if (prop.fieldNames.length > 1) {
263
- params.push(...(row[prop.name] ?? prop.fieldNames.map(() => null)));
264
- keys.push(...prop.fieldNames.map(_ => '?'));
265
- }
266
- else if (prop.customType && 'convertToDatabaseValueSQL' in prop.customType && !this.platform.isRaw(row[prop.name])) {
267
- keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
268
- params.push(row[prop.name]);
350
+ const param = core_1.Utils.flatten([...row[prop.name] ?? prop.fieldNames.map(() => null)]);
351
+ const key = param.map(() => '?');
352
+ prop.fieldNames.forEach((field, idx) => {
353
+ if (!duplicates.includes(field) || !usedDups.includes(field)) {
354
+ params.push(param[idx]);
355
+ keys.push(key[idx]);
356
+ usedDups.push(field);
357
+ }
358
+ });
269
359
  }
270
360
  else {
271
- params.push(row[prop.name]);
272
- keys.push('?');
361
+ const field = prop.fieldNames[0];
362
+ if (!duplicates.includes(field) || !usedDups.includes(field)) {
363
+ if (prop.customType && 'convertToDatabaseValueSQL' in prop.customType && !this.platform.isRaw(row[prop.name])) {
364
+ keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
365
+ }
366
+ else {
367
+ keys.push('?');
368
+ }
369
+ addParams(prop, row);
370
+ usedDups.push(field);
371
+ }
273
372
  }
274
373
  });
275
374
  return '(' + (keys.join(', ') || 'default') + ')';
276
375
  }).join(', ');
277
376
  }
278
- if (this.platform.usesReturningStatement()) {
279
- /* istanbul ignore next */
280
- const returningProps = meta.hydrateProps.filter(prop => prop.persist !== false && (prop.primary || prop.defaultRaw));
377
+ if (meta && this.platform.usesReturningStatement()) {
378
+ const returningProps = meta.props
379
+ .filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
380
+ .filter(prop => !(prop.name in data[0]) || core_1.Utils.isRawSql(data[0][prop.name]));
281
381
  const returningFields = core_1.Utils.flatten(returningProps.map(prop => prop.fieldNames));
282
382
  /* istanbul ignore next */
283
383
  sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
@@ -289,8 +389,8 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
289
389
  pk = data.map(d => core_1.Utils.getPrimaryKeyCond(d, pks));
290
390
  }
291
391
  else {
292
- res.row ?? (res.row = {});
293
- res.rows ?? (res.rows = []);
392
+ res.row ??= {};
393
+ res.rows ??= [];
294
394
  pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
295
395
  res.insertId = res.insertId || res.row[pks[0]];
296
396
  }
@@ -300,7 +400,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
300
400
  return res;
301
401
  }
302
402
  async nativeUpdate(entityName, where, data, options = {}) {
303
- options.convertCustomTypes ?? (options.convertCustomTypes = true);
403
+ options.convertCustomTypes ??= true;
304
404
  const meta = this.metadata.find(entityName);
305
405
  const pks = this.getPrimaryKeyFields(entityName);
306
406
  const collections = this.extractManyToMany(entityName, data);
@@ -314,14 +414,27 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
314
414
  .withSchema(this.getSchemaName(meta, options));
315
415
  if (options.upsert) {
316
416
  /* istanbul ignore next */
317
- const uniqueFields = core_1.Utils.isPlainObject(where) ? Object.keys(where) : meta.primaryKeys;
318
- /* istanbul ignore next */
417
+ const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where) ? core_1.Utils.keys(where) : meta.primaryKeys);
418
+ const returning = (0, core_1.getOnConflictReturningFields)(meta, data, uniqueFields, options);
319
419
  qb.insert(data)
320
- .onConflict(uniqueFields.map(p => meta?.properties[p]?.fieldNames[0] ?? p))
321
- .merge(Object.keys(data).filter(f => !uniqueFields.includes(f)));
420
+ .onConflict(uniqueFields)
421
+ .returning(returning);
422
+ if (!options.onConflictAction || options.onConflictAction === 'merge') {
423
+ const fields = (0, core_1.getOnConflictFields)(data, uniqueFields, options);
424
+ qb.merge(fields);
425
+ }
426
+ if (options.onConflictAction === 'ignore') {
427
+ qb.ignore();
428
+ }
322
429
  }
323
430
  else {
324
431
  qb.update(data).where(where);
432
+ // reload generated columns and version fields
433
+ const returning = [];
434
+ meta?.props
435
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
436
+ .forEach(prop => returning.push(prop.name));
437
+ qb.returning(returning);
325
438
  }
326
439
  res = await this.rethrow(qb.execute('run', false));
327
440
  }
@@ -331,23 +444,57 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
331
444
  return res;
332
445
  }
333
446
  async nativeUpdateMany(entityName, where, data, options = {}) {
334
- options.processCollections ?? (options.processCollections = true);
335
- options.convertCustomTypes ?? (options.convertCustomTypes = true);
447
+ options.processCollections ??= true;
448
+ options.convertCustomTypes ??= true;
336
449
  const meta = this.metadata.get(entityName);
337
450
  if (options.upsert) {
338
- const uniqueFields = core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]) : meta.primaryKeys;
451
+ const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => core_1.Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
339
452
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
453
+ const returning = (0, core_1.getOnConflictReturningFields)(meta, data[0], uniqueFields, options);
340
454
  qb.insert(data)
341
- .onConflict(uniqueFields.map(p => meta?.properties[p]?.fieldNames[0] ?? p))
342
- .merge(Object.keys(data[0]).filter(f => !uniqueFields.includes(f)));
343
- return qb.execute('run', false);
455
+ .onConflict(uniqueFields)
456
+ .returning(returning);
457
+ if (!options.onConflictAction || options.onConflictAction === 'merge') {
458
+ const fields = (0, core_1.getOnConflictFields)(data[0], uniqueFields, options);
459
+ qb.merge(fields);
460
+ }
461
+ if (options.onConflictAction === 'ignore') {
462
+ qb.ignore();
463
+ }
464
+ return this.rethrow(qb.execute('run', false));
344
465
  }
345
466
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
346
467
  const keys = new Set();
347
- data.forEach(row => Object.keys(row).forEach(k => keys.add(k)));
468
+ const returning = new Set();
469
+ data.forEach(row => {
470
+ core_1.Utils.keys(row).forEach(k => {
471
+ keys.add(k);
472
+ if (core_1.Utils.isRawSql(row[k])) {
473
+ returning.add(k);
474
+ }
475
+ });
476
+ });
477
+ // reload generated columns and version fields
478
+ meta?.props
479
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
480
+ .forEach(prop => returning.add(prop.name));
348
481
  const pkCond = core_1.Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames)).map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ');
349
482
  const params = [];
350
483
  let sql = `update ${this.getTableName(meta, options)} set `;
484
+ const addParams = (prop, value) => {
485
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED && prop.object) {
486
+ if (prop.array) {
487
+ for (let i = 0; i < value.length; i++) {
488
+ const item = value[i];
489
+ value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
490
+ }
491
+ }
492
+ else {
493
+ value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
494
+ }
495
+ }
496
+ params.push(value);
497
+ };
351
498
  keys.forEach(key => {
352
499
  const prop = meta.properties[key];
353
500
  prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
@@ -362,7 +509,8 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
362
509
  else {
363
510
  sql += '?';
364
511
  }
365
- params.push(...pks, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
512
+ params.push(...pks);
513
+ addParams(prop, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
366
514
  }
367
515
  });
368
516
  sql += ` else ${this.platform.quoteIdentifier(fieldName)} end, `;
@@ -373,7 +521,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
373
521
  const versionProperty = meta.properties[meta.versionProperty];
374
522
  const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
375
523
  sql += `${quotedFieldName} = `;
376
- if (versionProperty.type.toLowerCase() === 'date') {
524
+ if (versionProperty.runtimeType === 'Date') {
377
525
  sql += this.platform.getCurrentTimestampSQL(versionProperty.length);
378
526
  }
379
527
  else {
@@ -404,6 +552,11 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
404
552
  return '?';
405
553
  });
406
554
  sql += ` in (${conds.join(', ')})`;
555
+ if (this.platform.usesReturningStatement() && returning.size > 0) {
556
+ const returningFields = core_1.Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
557
+ /* istanbul ignore next */
558
+ sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
559
+ }
407
560
  const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx));
408
561
  for (let i = 0; i < collections.length; i++) {
409
562
  await this.processManyToMany(meta, where[i], collections[i], false, options);
@@ -419,49 +572,81 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
419
572
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false).delete(where).withSchema(this.getSchemaName(meta, options));
420
573
  return this.rethrow(qb.execute('run', false));
421
574
  }
422
- async syncCollection(coll, options) {
423
- const wrapped = (0, core_1.helper)(coll.owner);
424
- const meta = wrapped.__meta;
425
- const pks = wrapped.getPrimaryKeys(true);
426
- const snap = coll.getSnapshot();
427
- const includes = (arr, item) => !!arr.find(i => core_1.Utils.equals(i, item));
428
- const snapshot = snap ? snap.map(item => (0, core_1.helper)(item).getPrimaryKeys(true)) : [];
429
- const current = coll.getItems(false).map(item => (0, core_1.helper)(item).getPrimaryKeys(true));
430
- const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
431
- const insertDiff = current.filter(item => !includes(snapshot, item));
432
- const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
433
- const equals = core_1.Utils.equals(current, target);
434
- const ctx = options?.ctx;
435
- // wrong order if we just delete and insert to the end (only owning sides can have fixed order)
436
- if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
437
- deleteDiff.length = insertDiff.length = 0;
438
- deleteDiff.push(...snapshot);
439
- insertDiff.push(...current);
440
- }
441
- if (coll.property.reference === core_1.ReferenceType.ONE_TO_MANY) {
442
- const cols = coll.property.referencedColumnNames;
443
- const qb = this.createQueryBuilder(coll.property.type, ctx, 'write')
444
- .withSchema(this.getSchemaName(meta, options))
445
- .update({ [coll.property.mappedBy]: pks })
446
- .getKnexQuery()
447
- .whereIn(cols, insertDiff);
448
- return this.rethrow(this.execute(qb));
449
- }
450
- /* istanbul ignore next */
451
- const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema();
452
- const pivotMeta = this.metadata.find(coll.property.pivotEntity);
453
- if (pivotMeta.schema === '*') {
575
+ async syncCollections(collections, options) {
576
+ const groups = {};
577
+ for (const coll of collections) {
578
+ const wrapped = (0, core_1.helper)(coll.owner);
579
+ const meta = wrapped.__meta;
580
+ const pks = wrapped.getPrimaryKeys(true);
581
+ const snap = coll.getSnapshot();
582
+ const includes = (arr, item) => !!arr.find(i => core_1.Utils.equals(i, item));
583
+ const snapshot = snap ? snap.map(item => (0, core_1.helper)(item).getPrimaryKeys(true)) : [];
584
+ const current = coll.getItems(false).map(item => (0, core_1.helper)(item).getPrimaryKeys(true));
585
+ const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
586
+ const insertDiff = current.filter(item => !includes(snapshot, item));
587
+ const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
588
+ const equals = core_1.Utils.equals(current, target);
589
+ // wrong order if we just delete and insert to the end (only owning sides can have fixed order)
590
+ if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
591
+ deleteDiff.length = insertDiff.length = 0;
592
+ deleteDiff.push(...snapshot);
593
+ insertDiff.push(...current);
594
+ }
595
+ if (coll.property.kind === core_1.ReferenceKind.ONE_TO_MANY) {
596
+ const cols = coll.property.referencedColumnNames;
597
+ const qb = this.createQueryBuilder(coll.property.type, options?.ctx, 'write')
598
+ .withSchema(this.getSchemaName(meta, options));
599
+ if (coll.getSnapshot() === undefined) {
600
+ if (coll.property.orphanRemoval) {
601
+ const kqb = qb.delete({ [coll.property.mappedBy]: pks })
602
+ .getKnexQuery()
603
+ .whereNotIn(cols, insertDiff);
604
+ return this.rethrow(this.execute(kqb));
605
+ }
606
+ const kqb = qb.update({ [coll.property.mappedBy]: null })
607
+ .getKnexQuery()
608
+ .whereNotIn(cols, insertDiff);
609
+ return this.rethrow(this.execute(kqb));
610
+ }
611
+ const kqb = qb.update({ [coll.property.mappedBy]: pks })
612
+ .getKnexQuery()
613
+ .whereIn(cols, insertDiff);
614
+ return this.rethrow(this.execute(kqb));
615
+ }
454
616
  /* istanbul ignore next */
455
- options ?? (options = {});
456
- options.schema = ownerSchema;
617
+ const pivotMeta = this.metadata.find(coll.property.pivotEntity);
618
+ let schema = pivotMeta.schema;
619
+ if (schema === '*') {
620
+ const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema();
621
+ schema = coll.property.owner ? ownerSchema : this.config.get('schema');
622
+ }
623
+ else if (schema == null) {
624
+ schema = this.config.get('schema');
625
+ }
626
+ const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
627
+ const persister = groups[tableName] ??= new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, schema);
628
+ persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks);
629
+ }
630
+ for (const persister of core_1.Utils.values(groups)) {
631
+ await this.rethrow(persister.execute());
457
632
  }
458
- return this.rethrow(this.updateCollectionDiff(meta, coll.property, pks, deleteDiff, insertDiff, options));
459
633
  }
460
- async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options) {
461
- const pivotProp2 = this.getPivotInverseProperty(prop);
462
- const ownerMeta = this.metadata.find(pivotProp2.type);
634
+ async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
463
635
  const pivotMeta = this.metadata.find(prop.pivotEntity);
464
- const cond = { [`${prop.pivotEntity}.${pivotProp2.name}`]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) } };
636
+ const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
637
+ const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
638
+ const ownerMeta = this.metadata.find(pivotProp2.type);
639
+ options = { ...options };
640
+ const qb = this.createQueryBuilder(prop.pivotEntity, ctx, options.connectionType)
641
+ .withSchema(this.getSchemaName(pivotMeta, options))
642
+ .indexHint(options.indexHint)
643
+ .comment(options.comments)
644
+ .hintComment(options.hintComments);
645
+ const pivotAlias = qb.alias;
646
+ const pivotKey = pivotProp2.joinColumns.map(column => `${pivotAlias}.${column}`).join(core_1.Utils.PK_SEPARATOR);
647
+ const cond = {
648
+ [pivotKey]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
649
+ };
465
650
  /* istanbul ignore if */
466
651
  if (!core_1.Utils.isEmpty(where) && Object.keys(where).every(k => core_1.Utils.isOperator(k, false))) {
467
652
  where = cond;
@@ -469,29 +654,51 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
469
654
  else {
470
655
  where = { ...where, ...cond };
471
656
  }
472
- orderBy = this.getPivotOrderBy(prop, orderBy);
473
- const qb = this.createQueryBuilder(prop.type, ctx, options?.connectionType)
474
- .unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES)
475
- .withSchema(this.getSchemaName(prop.targetMeta, options));
476
- const populate = this.autoJoinOneToOneOwner(prop.targetMeta, [{ field: prop.pivotEntity }]);
477
- const fields = this.buildFields(prop.targetMeta, (options?.populate ?? []), [], qb, options?.fields);
478
- qb.select(fields).populate(populate).where(where).orderBy(orderBy).setLockMode(options?.lockMode, options?.lockTableAliases);
479
- if (owners.length === 1 && (options?.offset != null || options?.limit != null)) {
480
- qb.limit(options.limit, options.offset);
481
- }
482
- if (prop.targetMeta.schema !== '*' && pivotMeta.schema === '*' && options?.schema) {
483
- // eslint-disable-next-line dot-notation
484
- qb['finalize']();
485
- // eslint-disable-next-line dot-notation
486
- Object.values(qb['_joins']).forEach(join => {
487
- join.schema = options.schema;
657
+ orderBy = this.getPivotOrderBy(prop, pivotProp1, pivotAlias, orderBy);
658
+ const populate = this.autoJoinOneToOneOwner(prop.targetMeta, []);
659
+ const fields = [];
660
+ const k1 = !prop.owner ? 'joinColumns' : 'inverseJoinColumns';
661
+ const k2 = prop.owner ? 'joinColumns' : 'inverseJoinColumns';
662
+ const cols = [
663
+ ...prop[k1].map(col => `${pivotAlias}.${col} as fk__${col}`),
664
+ ...prop[k2].map(col => `${pivotAlias}.${col} as fk__${col}`),
665
+ ];
666
+ fields.push(...cols);
667
+ if (!pivotJoin) {
668
+ const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
669
+ const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
670
+ qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
671
+ const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options.fields, options);
672
+ for (const field of targetFields) {
673
+ const f = field.toString();
674
+ fields.unshift(f.includes('.') ? field : `${targetAlias}.${f}`);
675
+ }
676
+ // we need to handle 1:1 owner auto-joins explicitly, as the QB type is the pivot table, not the target
677
+ populate.forEach(hint => {
678
+ const alias = qb.getNextAlias(prop.targetMeta.tableName);
679
+ qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
680
+ // eslint-disable-next-line dot-notation
681
+ Object.values(qb['_joins']).forEach(join => {
682
+ const [propName] = hint.field.split(':', 2);
683
+ if (join.alias === alias && join.prop.name === propName) {
684
+ fields.push(...qb.helper.mapJoinColumns(qb.type, join));
685
+ }
686
+ });
488
687
  });
489
688
  }
490
- const items = owners.length ? await this.rethrow(qb.execute('all')) : [];
689
+ qb.select(fields)
690
+ .where({ [pivotProp1.name]: where })
691
+ .orderBy(orderBy)
692
+ .setLockMode(options.lockMode, options.lockTableAliases);
693
+ if (owners.length === 1 && (options.offset != null || options.limit != null)) {
694
+ qb.limit(options.limit, options.offset);
695
+ }
696
+ const res = owners.length ? await this.rethrow(qb.execute('all', { mergeResults: false, mapResults: false })) : [];
697
+ const items = res.map((row) => super.mapResult(row, prop.targetMeta));
491
698
  const map = {};
492
699
  const pkProps = ownerMeta.getPrimaryProps();
493
700
  owners.forEach(owner => {
494
- const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((col, idx) => {
701
+ const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((_col, idx) => {
495
702
  const pkProp = pkProps[idx];
496
703
  return pkProp.customType ? pkProp.customType.convertToJSValue(owner[idx], this.platform) : owner[idx];
497
704
  }));
@@ -504,10 +711,25 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
504
711
  }));
505
712
  map[key].push(item);
506
713
  prop.joinColumns.forEach(col => delete item[`fk__${col}`]);
507
- prop.inverseJoinColumns.forEach(col => delete item[`fk__${col}`]);
714
+ prop.inverseJoinColumns.forEach((col, idx) => {
715
+ core_1.Utils.renameKey(item, `fk__${col}`, prop.targetMeta.primaryKeys[idx]);
716
+ });
508
717
  });
509
718
  return map;
510
719
  }
720
+ getPivotOrderBy(prop, pivotProp, pivotAlias, orderBy) {
721
+ // FIXME this is ignoring the rest of the array items
722
+ if (!core_1.Utils.isEmpty(orderBy)) {
723
+ return [{ [pivotProp.name]: core_1.Utils.asArray(orderBy)[0] }];
724
+ }
725
+ if (!core_1.Utils.isEmpty(prop.orderBy)) {
726
+ return [{ [pivotProp.name]: core_1.Utils.asArray(prop.orderBy)[0] }];
727
+ }
728
+ if (prop.fixedOrder) {
729
+ return [{ [`${pivotAlias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC }];
730
+ }
731
+ return [];
732
+ }
511
733
  async execute(queryOrKnex, params = [], method = 'all', ctx) {
512
734
  return this.rethrow(this.connection.execute(queryOrKnex, params, method, ctx));
513
735
  }
@@ -520,39 +742,66 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
520
742
  }
521
743
  const relationsToPopulate = populate.map(({ field }) => field);
522
744
  const toPopulate = meta.relations
523
- .filter(prop => prop.reference === core_1.ReferenceType.ONE_TO_ONE && !prop.owner && !relationsToPopulate.includes(prop.name))
745
+ .filter(prop => prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner && !relationsToPopulate.includes(prop.name))
524
746
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
525
- .map(prop => ({ field: prop.name, strategy: prop.strategy }));
747
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
526
748
  return [...populate, ...toPopulate];
527
749
  }
528
- joinedProps(meta, populate) {
529
- return populate.filter(p => {
530
- const prop = meta.properties[p.field] || {};
531
- return (p.strategy || prop.strategy || this.config.get('loadStrategy')) === core_1.LoadStrategy.JOINED && prop.reference !== core_1.ReferenceType.SCALAR;
750
+ /**
751
+ * @internal
752
+ */
753
+ joinedProps(meta, populate, options) {
754
+ return populate.filter(hint => {
755
+ const [propName, ref] = hint.field.split(':', 2);
756
+ const prop = meta.properties[propName] || {};
757
+ if (ref) {
758
+ // keep only pivot ref joins here, as that only triggers actual join
759
+ return prop.kind === core_1.ReferenceKind.MANY_TO_MANY;
760
+ }
761
+ return (prop.strategy || options?.strategy || hint.strategy || this.config.get('loadStrategy')) === core_1.LoadStrategy.JOINED && ![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind);
532
762
  });
533
763
  }
534
764
  /**
535
765
  * @internal
536
766
  */
537
- mergeJoinedResult(rawResults, meta) {
538
- // group by the root entity primary key first
539
- const map = {};
767
+ mergeJoinedResult(rawResults, meta, joinedProps) {
540
768
  const res = [];
541
- rawResults.forEach(item => {
769
+ const map = {};
770
+ for (const item of rawResults) {
542
771
  const pk = core_1.Utils.getCompositeKeyHash(item, meta);
543
772
  if (map[pk]) {
544
- map[pk].push(item);
773
+ for (const hint of joinedProps) {
774
+ const [propName, ref] = hint.field.split(':', 2);
775
+ const prop = meta.properties[propName];
776
+ if (!item[propName]) {
777
+ continue;
778
+ }
779
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
780
+ map[pk][propName] = [...map[pk][propName], ...item[propName]];
781
+ continue;
782
+ }
783
+ switch (prop.kind) {
784
+ case core_1.ReferenceKind.ONE_TO_MANY:
785
+ case core_1.ReferenceKind.MANY_TO_MANY:
786
+ map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
787
+ break;
788
+ case core_1.ReferenceKind.MANY_TO_ONE:
789
+ case core_1.ReferenceKind.ONE_TO_ONE:
790
+ map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
791
+ break;
792
+ }
793
+ }
545
794
  }
546
795
  else {
547
- map[pk] = [item];
796
+ map[pk] = item;
548
797
  res.push(item);
549
798
  }
550
- });
799
+ }
551
800
  return res;
552
801
  }
553
- getFieldsForJoinedLoad(qb, meta, explicitFields, populate = [], parentTableAlias, parentJoinPath) {
802
+ getFieldsForJoinedLoad(qb, meta, explicitFields, populate = [], options, parentTableAlias, parentJoinPath) {
554
803
  const fields = [];
555
- const joinedProps = this.joinedProps(meta, populate);
804
+ const joinedProps = this.joinedProps(meta, populate, options);
556
805
  if (explicitFields?.includes('*')) {
557
806
  fields.push('*');
558
807
  }
@@ -563,26 +812,41 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
563
812
  if (!fields || prop.primary) {
564
813
  return !fields?.includes('*');
565
814
  }
566
- return fields.includes(prop.name);
815
+ return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
567
816
  };
568
- // alias all fields in the primary table
569
- meta.props
570
- .filter(prop => shouldHaveColumn(prop, populate, explicitFields))
571
- .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias)));
572
- joinedProps.forEach(relation => {
573
- const prop = meta.properties[relation.field];
817
+ const populateWhereAll = options?._populateWhere === 'all' || core_1.Utils.isEmpty(options?._populateWhere);
818
+ // root entity is already handled, skip that
819
+ if (parentJoinPath) {
820
+ // alias all fields in the primary table
821
+ meta.props
822
+ .filter(prop => shouldHaveColumn(prop, populate, explicitFields))
823
+ .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias)));
824
+ }
825
+ joinedProps.forEach(hint => {
826
+ const [propName, ref] = hint.field.split(':', 2);
827
+ const prop = meta.properties[propName];
574
828
  const meta2 = this.metadata.find(prop.type);
829
+ const pivotRefJoin = prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref;
575
830
  const tableAlias = qb.getNextAlias(prop.name);
576
831
  const field = parentTableAlias ? `${parentTableAlias}.${prop.name}` : prop.name;
577
- const path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
578
- qb.join(field, tableAlias, {}, 'leftJoin', path);
832
+ let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
833
+ if (!parentJoinPath && populateWhereAll && !path.startsWith('[populate]')) {
834
+ path = '[populate]' + path;
835
+ }
836
+ const joinType = pivotRefJoin ? query_1.JoinType.pivotJoin : query_1.JoinType.leftJoin;
837
+ qb.join(field, tableAlias, {}, joinType, path);
838
+ if (pivotRefJoin) {
839
+ fields.push(...prop.joinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)), ...prop.inverseJoinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
840
+ }
579
841
  const childExplicitFields = explicitFields?.filter(f => core_1.Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
580
842
  explicitFields?.forEach(f => {
581
843
  if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
582
844
  childExplicitFields.push(f.substring(prop.name.length + 1));
583
845
  }
584
846
  });
585
- fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, relation.children, tableAlias, path));
847
+ if (!ref) {
848
+ fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, hint.children, options, tableAlias, path));
849
+ }
586
850
  });
587
851
  return fields;
588
852
  }
@@ -590,24 +854,35 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
590
854
  * @internal
591
855
  */
592
856
  mapPropToFieldNames(qb, prop, tableAlias) {
593
- const aliased = qb.ref(tableAlias ? `${tableAlias}__${prop.fieldNames[0]}` : prop.fieldNames[0]).toString();
594
- if (prop.customType?.convertToJSValueSQL && tableAlias) {
595
- const prefixed = qb.ref(prop.fieldNames[0]).withSchema(tableAlias).toString();
596
- return [qb.raw(prop.customType.convertToJSValueSQL(prefixed, this.platform) + ' as ' + aliased).toString()];
857
+ const knex = this.connection.getKnex();
858
+ const aliased = knex.ref(tableAlias ? `${tableAlias}__${prop.fieldNames[0]}` : prop.fieldNames[0]).toString();
859
+ if (tableAlias && prop.customTypes?.some(type => type.convertToJSValueSQL)) {
860
+ return prop.fieldNames.map((col, idx) => {
861
+ if (!prop.customTypes[idx].convertToJSValueSQL) {
862
+ return col;
863
+ }
864
+ const prefixed = knex.ref(col).withSchema(tableAlias).toString();
865
+ const aliased = knex.ref(`${tableAlias}__${col}`).toString();
866
+ return (0, core_1.raw)(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
867
+ });
868
+ }
869
+ if (tableAlias && prop.customType?.convertToJSValueSQL) {
870
+ const prefixed = knex.ref(prop.fieldNames[0]).withSchema(tableAlias).toString();
871
+ return [(0, core_1.raw)(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
597
872
  }
598
873
  if (prop.formula) {
599
- const alias = qb.ref(tableAlias ?? qb.alias).toString();
600
- return [`${prop.formula(alias)} as ${aliased}`];
874
+ const alias = knex.ref(tableAlias ?? qb.alias).toString();
875
+ return [(0, core_1.raw)(`${prop.formula(alias)} as ${aliased}`)];
601
876
  }
602
877
  if (tableAlias) {
603
- return prop.fieldNames.map(fieldName => qb.ref(fieldName).withSchema(tableAlias).as(`${tableAlias}__${fieldName}`));
878
+ return prop.fieldNames.map(fieldName => knex.ref(fieldName).withSchema(tableAlias).as(`${tableAlias}__${fieldName}`));
604
879
  }
605
880
  return prop.fieldNames;
606
881
  }
607
882
  /** @internal */
608
- createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes) {
883
+ createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, logging) {
609
884
  const connectionType = this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
610
- const qb = new query_1.QueryBuilder(entityName, this.metadata, this, ctx, undefined, connectionType);
885
+ const qb = new query_1.QueryBuilder(entityName, this.metadata, this, ctx, undefined, connectionType, undefined, logging);
611
886
  if (!convertCustomTypes) {
612
887
  qb.unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES);
613
888
  }
@@ -631,7 +906,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
631
906
  }
632
907
  const ret = {};
633
908
  this.metadata.find(entityName).relations.forEach(prop => {
634
- if (prop.reference === core_1.ReferenceType.MANY_TO_MANY && data[prop.name]) {
909
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && data[prop.name]) {
635
910
  ret[prop.name] = data[prop.name].map((item) => core_1.Utils.asArray(item));
636
911
  delete data[prop.name];
637
912
  }
@@ -644,73 +919,97 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
644
919
  }
645
920
  for (const prop of meta.relations) {
646
921
  if (collections[prop.name]) {
647
- await this.rethrow(this.updateCollectionDiff(meta, prop, pks, clear, collections[prop.name], options));
922
+ const pivotMeta = this.metadata.find(prop.pivotEntity);
923
+ const persister = new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema);
924
+ persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
925
+ await this.rethrow(persister.execute());
648
926
  }
649
927
  }
650
928
  }
651
- async updateCollectionDiff(meta, prop, pks, deleteDiff, insertDiff, options) {
652
- if (!deleteDiff) {
653
- deleteDiff = [];
654
- }
655
- if (deleteDiff === true || deleteDiff.length > 0) {
656
- const qb1 = this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write')
657
- .withSchema(this.getSchemaName(meta, options))
658
- .unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES);
659
- const knex = qb1.getKnex();
660
- if (Array.isArray(deleteDiff)) {
661
- knex.whereIn(prop.inverseJoinColumns, deleteDiff);
662
- }
663
- prop.joinColumns.forEach((joinColumn, idx) => knex.andWhere(joinColumn, pks[idx]));
664
- await this.execute(knex.delete());
665
- }
666
- if (insertDiff.length === 0) {
667
- return;
668
- }
669
- const items = insertDiff.map(item => {
670
- const cond = {};
671
- prop.joinColumns.forEach((joinColumn, idx) => cond[joinColumn] = pks[idx]);
672
- prop.inverseJoinColumns.forEach((inverseJoinColumn, idx) => cond[inverseJoinColumn] = item[idx]);
673
- return cond;
674
- });
675
- /* istanbul ignore else */
676
- if (this.platform.allowsMultiInsert()) {
677
- await this.nativeInsertMany(prop.pivotEntity, items, { ...options, convertCustomTypes: false, processCollections: false });
678
- }
679
- else {
680
- await core_1.Utils.runSerial(items, item => {
681
- return this.createQueryBuilder(prop.pivotEntity, options?.ctx, 'write')
682
- .unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES)
683
- .withSchema(this.getSchemaName(meta, options))
684
- .insert(item)
685
- .execute('run', false);
686
- });
687
- }
688
- }
689
929
  async lockPessimistic(entity, options) {
690
930
  const meta = (0, core_1.helper)(entity).__meta;
691
- const qb = this.createQueryBuilder(entity.constructor.name, options.ctx).unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES).withSchema(options.schema ?? meta.schema);
931
+ const qb = this.createQueryBuilder(entity.constructor.name, options.ctx).withSchema(options.schema ?? meta.schema);
692
932
  const cond = core_1.Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
693
- qb.select('1').where(cond).setLockMode(options.lockMode, options.lockTableAliases);
933
+ qb.select((0, core_1.raw)('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
694
934
  await this.rethrow(qb.execute());
695
935
  }
696
- buildJoinedPropsOrderBy(entityName, qb, meta, populate, parentPath) {
936
+ buildOrderBy(qb, meta, populate, options) {
937
+ const joinedProps = this.joinedProps(meta, populate, options);
938
+ // `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
939
+ // as `options.populateWhere` will be always recomputed to respect filters
940
+ const populateWhereAll = options._populateWhere !== 'infer' && !core_1.Utils.isEmpty(options._populateWhere);
941
+ const path = (populateWhereAll ? '[populate]' : '') + meta.className;
942
+ const populateOrderBy = this.buildPopulateOrderBy(qb, meta, core_1.Utils.asArray(options.populateOrderBy ?? options.orderBy), path);
943
+ const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
944
+ return [...core_1.Utils.asArray(options.orderBy), ...populateOrderBy, ...joinedPropsOrderBy];
945
+ }
946
+ buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, parentAlias = qb.alias) {
697
947
  const orderBy = [];
698
- const joinedProps = this.joinedProps(meta, populate);
699
- joinedProps.forEach(relation => {
700
- const prop = meta.properties[relation.field];
948
+ for (let i = 0; i < populateOrderBy.length; i++) {
949
+ const orderHint = populateOrderBy[i];
950
+ for (const propName of core_1.Utils.keys(orderHint)) {
951
+ const prop = meta.properties[propName];
952
+ const meta2 = this.metadata.find(prop.type);
953
+ const childOrder = orderHint[prop.name];
954
+ let path = `${parentPath}.${propName}`;
955
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
956
+ path += '[pivot]';
957
+ }
958
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
959
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
960
+ if (!join && parentAlias === qb.alias) {
961
+ continue;
962
+ }
963
+ if (![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
964
+ const children = this.buildPopulateOrderBy(qb, meta2, core_1.Utils.asArray(childOrder), path, propAlias);
965
+ orderBy.push(...children);
966
+ continue;
967
+ }
968
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && join) {
969
+ if (prop.fixedOrderColumn) {
970
+ orderBy.push({ [`${join.alias}.${prop.fixedOrderColumn}`]: childOrder });
971
+ }
972
+ else {
973
+ for (const col of prop.inverseJoinColumns) {
974
+ orderBy.push({ [`${join.ownerAlias}.${col}`]: childOrder });
975
+ }
976
+ }
977
+ continue;
978
+ }
979
+ const order = typeof childOrder === 'object' ? childOrder[propName] : childOrder;
980
+ orderBy.push({ [`${propAlias}.${propName}`]: order });
981
+ }
982
+ }
983
+ return orderBy;
984
+ }
985
+ buildJoinedPropsOrderBy(qb, meta, populate, options, parentPath) {
986
+ const orderBy = [];
987
+ const joinedProps = this.joinedProps(meta, populate, options);
988
+ for (const hint of joinedProps) {
989
+ const [propName, ref] = hint.field.split(':', 2);
990
+ const prop = meta.properties[propName];
701
991
  const propOrderBy = prop.orderBy;
702
- const path = `${parentPath ? parentPath : entityName}.${relation.field}`;
703
- const propAlias = qb.getAliasForJoinPath(path);
992
+ let path = `${parentPath}.${propName}`;
993
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
994
+ path += '[pivot]';
995
+ }
996
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
997
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
998
+ const meta2 = this.metadata.find(prop.type);
999
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1000
+ const alias = ref ? propAlias : join.ownerAlias;
1001
+ orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC });
1002
+ }
704
1003
  if (propOrderBy) {
705
- Object.keys(propOrderBy).forEach(field => {
1004
+ core_1.Utils.keys(propOrderBy).forEach(field => {
706
1005
  orderBy.push({ [`${propAlias}.${field}`]: propOrderBy[field] });
707
1006
  });
708
1007
  }
709
- if (relation.children) {
710
- const meta2 = this.metadata.find(prop.type);
711
- orderBy.push(...this.buildJoinedPropsOrderBy(prop.name, qb, meta2, relation.children, path));
1008
+ if (hint.children) {
1009
+ const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1010
+ orderBy.push(...buildJoinedPropsOrderBy);
712
1011
  }
713
- });
1012
+ }
714
1013
  return orderBy;
715
1014
  }
716
1015
  normalizeFields(fields, prefix = '') {
@@ -729,12 +1028,12 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
729
1028
  return ret;
730
1029
  }
731
1030
  processField(meta, prop, field, ret, populate, joinedProps, qb) {
732
- if (!prop || (prop.reference === core_1.ReferenceType.ONE_TO_ONE && !prop.owner)) {
1031
+ if (!prop || (prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner)) {
733
1032
  return;
734
1033
  }
735
- if (prop.reference === core_1.ReferenceType.EMBEDDED) {
1034
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED) {
736
1035
  if (prop.object) {
737
- ret.push(prop.fieldNames[0]);
1036
+ ret.push(prop.name);
738
1037
  return;
739
1038
  }
740
1039
  const parts = field.split('.');
@@ -746,20 +1045,17 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
746
1045
  }
747
1046
  return;
748
1047
  }
749
- if (prop.fieldNames) {
750
- ret.push(prop.fieldNames[0]);
751
- }
1048
+ ret.push(prop.name);
752
1049
  }
753
- buildFields(meta, populate, joinedProps, qb, fields) {
1050
+ buildFields(meta, populate, joinedProps, qb, alias, fields, options) {
754
1051
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all));
755
1052
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
756
- const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL);
1053
+ const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
757
1054
  const hasExplicitFields = !!fields;
758
1055
  const ret = [];
759
- if (joinedProps.length > 0) {
760
- ret.push(...this.getFieldsForJoinedLoad(qb, meta, fields, populate));
761
- }
762
- else if (fields) {
1056
+ let addFormulas = false;
1057
+ // handle root entity properties first, this is used for both strategies in the same way
1058
+ if (fields) {
763
1059
  for (const field of this.normalizeFields(fields)) {
764
1060
  if (field === '*') {
765
1061
  ret.push('*');
@@ -776,28 +1072,39 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
776
1072
  });
777
1073
  this.processField(meta, prop, parts.join('.'), ret, populate, joinedProps, qb);
778
1074
  }
779
- ret.unshift(...meta.primaryKeys.filter(pk => !fields.includes(pk)));
1075
+ if (!fields.includes('*') && !fields.includes(`${qb.alias}.*`)) {
1076
+ ret.unshift(...meta.primaryKeys.filter(pk => !fields.includes(pk)));
1077
+ }
780
1078
  }
781
1079
  else if (lazyProps.filter(p => !p.formula).length > 0) {
782
1080
  const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, false));
783
1081
  ret.push(...core_1.Utils.flatten(props.filter(p => !lazyProps.includes(p)).map(p => p.fieldNames)));
1082
+ addFormulas = true;
784
1083
  }
785
1084
  else if (hasLazyFormulas || requiresSQLConversion) {
786
1085
  ret.push('*');
1086
+ addFormulas = true;
787
1087
  }
788
- if (ret.length > 0 && !hasExplicitFields) {
1088
+ else {
1089
+ ret.push('*');
1090
+ }
1091
+ if (ret.length > 0 && !hasExplicitFields && addFormulas) {
789
1092
  meta.props
790
1093
  .filter(prop => prop.formula && !lazyProps.includes(prop))
791
1094
  .forEach(prop => {
792
- const alias = qb.ref(qb.alias).toString();
793
- const aliased = qb.ref(prop.fieldNames[0]).toString();
794
- ret.push(`${prop.formula(alias)} as ${aliased}`);
1095
+ const a = this.connection.getKnex().ref(alias).toString();
1096
+ const aliased = this.connection.getKnex().ref(prop.fieldNames[0]).toString();
1097
+ ret.push((0, core_1.raw)(`${prop.formula(a)} as ${aliased}`));
795
1098
  });
796
1099
  meta.props
797
- .filter(prop => prop.customType?.convertToDatabaseValueSQL || prop.customType?.convertToJSValueSQL)
1100
+ .filter(prop => prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)
798
1101
  .forEach(prop => ret.push(prop.name));
799
1102
  }
800
- return ret.length > 0 ? core_1.Utils.unique(ret) : ['*'];
1103
+ // add joined relations after the root entity fields
1104
+ if (joinedProps.length > 0) {
1105
+ ret.push(...this.getFieldsForJoinedLoad(qb, meta, fields, populate, options, alias));
1106
+ }
1107
+ return core_1.Utils.unique(ret);
801
1108
  }
802
1109
  }
803
1110
  exports.AbstractSqlDriver = AbstractSqlDriver;