@mikro-orm/knex 6.0.0-dev.27 → 6.0.0-dev.270

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