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

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