@mikro-orm/knex 6.0.0-dev.30 → 6.0.0-dev.301

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 +47 -17
  3. package/AbstractSqlDriver.d.ts +30 -24
  4. package/AbstractSqlDriver.js +590 -264
  5. package/AbstractSqlPlatform.d.ts +9 -5
  6. package/AbstractSqlPlatform.js +25 -29
  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 -5
  15. package/SqlEntityRepository.js +11 -7
  16. package/index.mjs +24 -3
  17. package/package.json +8 -8
  18. package/query/ArrayCriteriaNode.d.ts +3 -2
  19. package/query/ArrayCriteriaNode.js +7 -2
  20. package/query/CriteriaNode.d.ts +6 -4
  21. package/query/CriteriaNode.js +22 -9
  22. package/query/CriteriaNodeFactory.d.ts +1 -1
  23. package/query/CriteriaNodeFactory.js +27 -11
  24. package/query/ObjectCriteriaNode.d.ts +3 -2
  25. package/query/ObjectCriteriaNode.js +72 -28
  26. package/query/QueryBuilder.d.ts +58 -26
  27. package/query/QueryBuilder.js +435 -139
  28. package/query/QueryBuilderHelper.d.ts +13 -18
  29. package/query/QueryBuilderHelper.js +196 -169
  30. package/query/ScalarCriteriaNode.d.ts +3 -2
  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 +369 -42
  38. package/schema/SchemaComparator.d.ts +7 -5
  39. package/schema/SchemaComparator.js +69 -35
  40. package/schema/SchemaHelper.d.ts +7 -5
  41. package/schema/SchemaHelper.js +30 -11
  42. package/schema/SqlSchemaGenerator.d.ts +3 -4
  43. package/schema/SqlSchemaGenerator.js +66 -23
  44. package/typings.d.ts +27 -8
@@ -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);
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,28 +57,25 @@ 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)) {
@@ -79,52 +85,64 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
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 [];
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;
87
93
  }
88
- if (typeof meta.expression === 'string') {
89
- return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options);
94
+ if (hint.children && prop.targetMeta) {
95
+ return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
90
96
  }
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);
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,133 @@ 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] ??= [];
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 targetProps = ref
235
+ ? meta2.getPrimaryProps()
236
+ : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
237
+ for (const prop of targetProps) {
238
+ if (prop.fieldNames.length > 1) { // composite keys
239
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
240
+ const pk = core_1.Utils.mapFlatCompositePrimaryKey(fk, prop);
241
+ relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
242
+ }
243
+ else if (prop.runtimeType === 'Date') {
244
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
245
+ relationPojo[prop.name] = (typeof root[alias] === 'string' ? new Date(root[alias]) : root[alias]);
246
+ }
247
+ else {
248
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
249
+ relationPojo[prop.name] = root[alias];
250
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
251
+ const item = (0, core_1.parseJsonSafe)(relationPojo[prop.name]);
252
+ if (Array.isArray(item)) {
253
+ relationPojo[prop.name] = item.map(row => row == null ? row : this.comparator.mapResult(prop.type, row));
254
+ }
255
+ else {
256
+ relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.type, item);
257
+ }
258
+ }
259
+ }
190
260
  }
191
- else {
192
- map[key] = result;
261
+ // properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
262
+ // so we need to delete them after everything is mapped from given level
263
+ for (const prop of targetProps) {
264
+ prop.fieldNames.map(name => delete root[`${relationAlias}__${name}`]);
193
265
  }
194
- if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(relation.kind)) {
195
- result[relation.name] ??= [];
196
- this.appendToCollection(meta2, result[relation.name], relationPojo);
266
+ if (ref) {
267
+ const tmp = Object.values(relationPojo);
268
+ /* istanbul ignore next */
269
+ relationPojo = (meta2.compositePK ? tmp : tmp[0]);
270
+ }
271
+ if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
272
+ result[prop.name] ??= [];
273
+ result[prop.name].push(relationPojo);
197
274
  }
198
275
  else {
199
- result[relation.name] = relationPojo;
276
+ result[prop.name] = relationPojo;
200
277
  }
201
- const populateChildren = p.children || [];
278
+ const populateChildren = hint.children || [];
202
279
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
203
280
  });
204
281
  }
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
282
  async count(entityName, where, options = {}) {
217
283
  const meta = this.metadata.find(entityName);
218
284
  if (meta?.virtual) {
219
285
  return this.countVirtual(entityName, where, options);
220
286
  }
221
287
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false)
288
+ .indexHint(options.indexHint)
289
+ .comment(options.comments)
290
+ .hintComment(options.hintComments)
222
291
  .groupBy(options.groupBy)
223
292
  .having(options.having)
224
293
  .populate(options.populate ?? [])
@@ -249,14 +318,18 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
249
318
  async nativeInsertMany(entityName, data, options = {}) {
250
319
  options.processCollections ??= true;
251
320
  options.convertCustomTypes ??= true;
252
- const meta = this.metadata.find(entityName);
321
+ const meta = this.metadata.find(entityName)?.root;
253
322
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
254
323
  const pks = this.getPrimaryKeyFields(entityName);
255
324
  const set = new Set();
256
325
  data.forEach(row => core_1.Utils.keys(row).forEach(k => set.add(k)));
257
326
  const props = [...set].map(name => meta?.properties[name] ?? { name, fieldNames: [name] });
258
- const fields = core_1.Utils.flatten(props.map(prop => prop.fieldNames));
327
+ let fields = core_1.Utils.flatten(props.map(prop => prop.fieldNames));
328
+ const duplicates = core_1.Utils.findDuplicates(fields);
259
329
  const params = [];
330
+ if (duplicates.length) {
331
+ fields = core_1.Utils.unique(fields);
332
+ }
260
333
  /* istanbul ignore next */
261
334
  const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
262
335
  let sql = `insert into ${tableName} `;
@@ -267,29 +340,62 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
267
340
  else {
268
341
  sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
269
342
  }
343
+ const addParams = (prop, row) => {
344
+ let value = row[prop.name];
345
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED && prop.object) {
346
+ if (prop.array) {
347
+ for (let i = 0; i < value.length; i++) {
348
+ const item = value[i];
349
+ value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
350
+ }
351
+ }
352
+ else {
353
+ value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
354
+ }
355
+ }
356
+ if (options.convertCustomTypes && prop.customType) {
357
+ params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
358
+ return;
359
+ }
360
+ params.push(value);
361
+ };
270
362
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
271
363
  sql += data.map(row => {
272
364
  const keys = [];
365
+ const usedDups = [];
273
366
  props.forEach(prop => {
274
367
  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]);
368
+ const param = core_1.Utils.flatten([...row[prop.name] ?? prop.fieldNames.map(() => null)]);
369
+ const key = param.map(() => '?');
370
+ prop.fieldNames.forEach((field, idx) => {
371
+ if (!duplicates.includes(field) || !usedDups.includes(field)) {
372
+ params.push(param[idx]);
373
+ keys.push(key[idx]);
374
+ usedDups.push(field);
375
+ }
376
+ });
281
377
  }
282
378
  else {
283
- params.push(row[prop.name]);
284
- keys.push('?');
379
+ const field = prop.fieldNames[0];
380
+ if (!duplicates.includes(field) || !usedDups.includes(field)) {
381
+ if (prop.customType && 'convertToDatabaseValueSQL' in prop.customType && !this.platform.isRaw(row[prop.name])) {
382
+ keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
383
+ }
384
+ else {
385
+ keys.push('?');
386
+ }
387
+ addParams(prop, row);
388
+ usedDups.push(field);
389
+ }
285
390
  }
286
391
  });
287
392
  return '(' + (keys.join(', ') || 'default') + ')';
288
393
  }).join(', ');
289
394
  }
290
- if (this.platform.usesReturningStatement()) {
291
- /* istanbul ignore next */
292
- const returningProps = meta.hydrateProps.filter(prop => prop.persist !== false && (prop.primary || prop.defaultRaw));
395
+ if (meta && this.platform.usesReturningStatement()) {
396
+ const returningProps = meta.props
397
+ .filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
398
+ .filter(prop => !(prop.name in data[0]) || core_1.Utils.isRawSql(data[0][prop.name]));
293
399
  const returningFields = core_1.Utils.flatten(returningProps.map(prop => prop.fieldNames));
294
400
  /* istanbul ignore next */
295
401
  sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
@@ -326,14 +432,27 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
326
432
  .withSchema(this.getSchemaName(meta, options));
327
433
  if (options.upsert) {
328
434
  /* istanbul ignore next */
329
- const uniqueFields = core_1.Utils.isPlainObject(where) ? core_1.Utils.keys(where) : meta.primaryKeys;
330
- /* istanbul ignore next */
435
+ const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where) ? core_1.Utils.keys(where) : meta.primaryKeys);
436
+ const returning = (0, core_1.getOnConflictReturningFields)(meta, data, uniqueFields, options);
331
437
  qb.insert(data)
332
- .onConflict(uniqueFields.map(p => meta?.properties[p]?.fieldNames[0] ?? p))
333
- .merge(core_1.Utils.keys(data).filter(f => !uniqueFields.includes(f)));
438
+ .onConflict(uniqueFields)
439
+ .returning(returning);
440
+ if (!options.onConflictAction || options.onConflictAction === 'merge') {
441
+ const fields = (0, core_1.getOnConflictFields)(data, uniqueFields, options);
442
+ qb.merge(fields);
443
+ }
444
+ if (options.onConflictAction === 'ignore') {
445
+ qb.ignore();
446
+ }
334
447
  }
335
448
  else {
336
449
  qb.update(data).where(where);
450
+ // reload generated columns and version fields
451
+ const returning = [];
452
+ meta?.props
453
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
454
+ .forEach(prop => returning.push(prop.name));
455
+ qb.returning(returning);
337
456
  }
338
457
  res = await this.rethrow(qb.execute('run', false));
339
458
  }
@@ -347,22 +466,61 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
347
466
  options.convertCustomTypes ??= true;
348
467
  const meta = this.metadata.get(entityName);
349
468
  if (options.upsert) {
350
- const uniqueFields = core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]) : meta.primaryKeys;
469
+ const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => core_1.Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
351
470
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
471
+ const returning = (0, core_1.getOnConflictReturningFields)(meta, data[0], uniqueFields, options);
352
472
  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);
473
+ .onConflict(uniqueFields)
474
+ .returning(returning);
475
+ if (!options.onConflictAction || options.onConflictAction === 'merge') {
476
+ const fields = (0, core_1.getOnConflictFields)(data[0], uniqueFields, options);
477
+ qb.merge(fields);
478
+ }
479
+ if (options.onConflictAction === 'ignore') {
480
+ qb.ignore();
481
+ }
482
+ return this.rethrow(qb.execute('run', false));
356
483
  }
357
484
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
358
485
  const keys = new Set();
359
- data.forEach(row => core_1.Utils.keys(row).forEach(k => keys.add(k)));
486
+ const fields = new Set();
487
+ const returning = new Set();
488
+ data.forEach(row => {
489
+ core_1.Utils.keys(row).forEach(k => {
490
+ keys.add(k);
491
+ if (core_1.Utils.isRawSql(row[k])) {
492
+ returning.add(k);
493
+ }
494
+ });
495
+ });
496
+ // reload generated columns and version fields
497
+ meta?.props
498
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
499
+ .forEach(prop => returning.add(prop.name));
360
500
  const pkCond = core_1.Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames)).map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ');
361
501
  const params = [];
362
502
  let sql = `update ${this.getTableName(meta, options)} set `;
503
+ const addParams = (prop, value) => {
504
+ if (prop.kind === core_1.ReferenceKind.EMBEDDED && prop.object) {
505
+ if (prop.array) {
506
+ for (let i = 0; i < value.length; i++) {
507
+ const item = value[i];
508
+ value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
509
+ }
510
+ }
511
+ else {
512
+ value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
513
+ }
514
+ }
515
+ params.push(value);
516
+ };
363
517
  keys.forEach(key => {
364
518
  const prop = meta.properties[key];
365
519
  prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
520
+ if (fields.has(fieldName)) {
521
+ return;
522
+ }
523
+ fields.add(fieldName);
366
524
  sql += `${this.platform.quoteIdentifier(fieldName)} = case`;
367
525
  where.forEach((cond, idx) => {
368
526
  if (key in data[idx]) {
@@ -374,7 +532,8 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
374
532
  else {
375
533
  sql += '?';
376
534
  }
377
- params.push(...pks, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
535
+ params.push(...pks);
536
+ addParams(prop, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
378
537
  }
379
538
  });
380
539
  sql += ` else ${this.platform.quoteIdentifier(fieldName)} end, `;
@@ -385,7 +544,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
385
544
  const versionProperty = meta.properties[meta.versionProperty];
386
545
  const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
387
546
  sql += `${quotedFieldName} = `;
388
- if (versionProperty.type.toLowerCase() === 'date') {
547
+ if (versionProperty.runtimeType === 'Date') {
389
548
  sql += this.platform.getCurrentTimestampSQL(versionProperty.length);
390
549
  }
391
550
  else {
@@ -416,6 +575,11 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
416
575
  return '?';
417
576
  });
418
577
  sql += ` in (${conds.join(', ')})`;
578
+ if (this.platform.usesReturningStatement() && returning.size > 0) {
579
+ const returningFields = core_1.Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
580
+ /* istanbul ignore next */
581
+ sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
582
+ }
419
583
  const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx));
420
584
  for (let i = 0; i < collections.length; i++) {
421
585
  await this.processManyToMany(meta, where[i], collections[i], false, options);
@@ -431,49 +595,84 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
431
595
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false).delete(where).withSchema(this.getSchemaName(meta, options));
432
596
  return this.rethrow(qb.execute('run', false));
433
597
  }
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 === '*') {
598
+ async syncCollections(collections, options) {
599
+ const groups = {};
600
+ for (const coll of collections) {
601
+ const wrapped = (0, core_1.helper)(coll.owner);
602
+ const meta = wrapped.__meta;
603
+ const pks = wrapped.getPrimaryKeys(true);
604
+ const snap = coll.getSnapshot();
605
+ const includes = (arr, item) => !!arr.find(i => core_1.Utils.equals(i, item));
606
+ const snapshot = snap ? snap.map(item => (0, core_1.helper)(item).getPrimaryKeys(true)) : [];
607
+ const current = coll.getItems(false).map(item => (0, core_1.helper)(item).getPrimaryKeys(true));
608
+ const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
609
+ const insertDiff = current.filter(item => !includes(snapshot, item));
610
+ const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
611
+ const equals = core_1.Utils.equals(current, target);
612
+ // wrong order if we just delete and insert to the end (only owning sides can have fixed order)
613
+ if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
614
+ deleteDiff.length = insertDiff.length = 0;
615
+ deleteDiff.push(...snapshot);
616
+ insertDiff.push(...current);
617
+ }
618
+ if (coll.property.kind === core_1.ReferenceKind.ONE_TO_MANY) {
619
+ const cols = coll.property.referencedColumnNames;
620
+ const qb = this.createQueryBuilder(coll.property.type, options?.ctx, 'write')
621
+ .withSchema(this.getSchemaName(meta, options));
622
+ if (coll.getSnapshot() === undefined) {
623
+ if (coll.property.orphanRemoval) {
624
+ const kqb = qb.delete({ [coll.property.mappedBy]: pks })
625
+ .getKnexQuery()
626
+ .whereNotIn(cols, insertDiff);
627
+ await this.rethrow(this.execute(kqb));
628
+ continue;
629
+ }
630
+ const kqb = qb.update({ [coll.property.mappedBy]: null })
631
+ .getKnexQuery()
632
+ .whereNotIn(cols, insertDiff);
633
+ await this.rethrow(this.execute(kqb));
634
+ continue;
635
+ }
636
+ const kqb = qb.update({ [coll.property.mappedBy]: pks })
637
+ .getKnexQuery()
638
+ .whereIn(cols, insertDiff);
639
+ await this.rethrow(this.execute(kqb));
640
+ continue;
641
+ }
466
642
  /* istanbul ignore next */
467
- options ??= {};
468
- options.schema = ownerSchema;
643
+ const pivotMeta = this.metadata.find(coll.property.pivotEntity);
644
+ let schema = pivotMeta.schema;
645
+ if (schema === '*') {
646
+ const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema();
647
+ schema = coll.property.owner ? ownerSchema : this.config.get('schema');
648
+ }
649
+ else if (schema == null) {
650
+ schema = this.config.get('schema');
651
+ }
652
+ const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
653
+ const persister = groups[tableName] ??= new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, schema);
654
+ persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks);
655
+ }
656
+ for (const persister of core_1.Utils.values(groups)) {
657
+ await this.rethrow(persister.execute());
469
658
  }
470
- return this.rethrow(this.updateCollectionDiff(meta, coll.property, pks, deleteDiff, insertDiff, options));
471
659
  }
472
- async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options) {
473
- const pivotProp2 = this.getPivotInverseProperty(prop);
474
- const ownerMeta = this.metadata.find(pivotProp2.type);
660
+ async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
475
661
  const pivotMeta = this.metadata.find(prop.pivotEntity);
476
- const cond = { [`${prop.pivotEntity}.${pivotProp2.name}`]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) } };
662
+ const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
663
+ const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
664
+ const ownerMeta = this.metadata.find(pivotProp2.type);
665
+ options = { ...options };
666
+ const qb = this.createQueryBuilder(prop.pivotEntity, ctx, options.connectionType)
667
+ .withSchema(this.getSchemaName(pivotMeta, options))
668
+ .indexHint(options.indexHint)
669
+ .comment(options.comments)
670
+ .hintComment(options.hintComments);
671
+ const pivotAlias = qb.alias;
672
+ const pivotKey = pivotProp2.joinColumns.map(column => `${pivotAlias}.${column}`).join(core_1.Utils.PK_SEPARATOR);
673
+ const cond = {
674
+ [pivotKey]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
675
+ };
477
676
  /* istanbul ignore if */
478
677
  if (!core_1.Utils.isEmpty(where) && Object.keys(where).every(k => core_1.Utils.isOperator(k, false))) {
479
678
  where = cond;
@@ -481,29 +680,51 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
481
680
  else {
482
681
  where = { ...where, ...cond };
483
682
  }
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;
683
+ orderBy = this.getPivotOrderBy(prop, pivotProp1, pivotAlias, orderBy);
684
+ const populate = this.autoJoinOneToOneOwner(prop.targetMeta, []);
685
+ const fields = [];
686
+ const k1 = !prop.owner ? 'joinColumns' : 'inverseJoinColumns';
687
+ const k2 = prop.owner ? 'joinColumns' : 'inverseJoinColumns';
688
+ const cols = [
689
+ ...prop[k1].map(col => `${pivotAlias}.${col} as fk__${col}`),
690
+ ...prop[k2].map(col => `${pivotAlias}.${col} as fk__${col}`),
691
+ ];
692
+ fields.push(...cols);
693
+ if (!pivotJoin) {
694
+ const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
695
+ const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
696
+ qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
697
+ const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options);
698
+ for (const field of targetFields) {
699
+ const f = field.toString();
700
+ fields.unshift(f.includes('.') ? field : `${targetAlias}.${f}`);
701
+ }
702
+ // we need to handle 1:1 owner auto-joins explicitly, as the QB type is the pivot table, not the target
703
+ populate.forEach(hint => {
704
+ const alias = qb.getNextAlias(prop.targetMeta.tableName);
705
+ qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
706
+ // eslint-disable-next-line dot-notation
707
+ Object.values(qb['_joins']).forEach(join => {
708
+ const [propName] = hint.field.split(':', 2);
709
+ if (join.alias === alias && join.prop.name === propName) {
710
+ fields.push(...qb.helper.mapJoinColumns(qb.type, join));
711
+ }
712
+ });
500
713
  });
501
714
  }
502
- const items = owners.length ? await this.rethrow(qb.execute('all')) : [];
715
+ qb.select(fields)
716
+ .where({ [pivotProp1.name]: where })
717
+ .orderBy(orderBy)
718
+ .setLockMode(options.lockMode, options.lockTableAliases);
719
+ if (owners.length === 1 && (options.offset != null || options.limit != null)) {
720
+ qb.limit(options.limit, options.offset);
721
+ }
722
+ const res = owners.length ? await this.rethrow(qb.execute('all', { mergeResults: false, mapResults: false })) : [];
723
+ const items = res.map((row) => super.mapResult(row, prop.targetMeta));
503
724
  const map = {};
504
725
  const pkProps = ownerMeta.getPrimaryProps();
505
726
  owners.forEach(owner => {
506
- const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((col, idx) => {
727
+ const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((_col, idx) => {
507
728
  const pkProp = pkProps[idx];
508
729
  return pkProp.customType ? pkProp.customType.convertToJSValue(owner[idx], this.platform) : owner[idx];
509
730
  }));
@@ -516,10 +737,25 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
516
737
  }));
517
738
  map[key].push(item);
518
739
  prop.joinColumns.forEach(col => delete item[`fk__${col}`]);
519
- prop.inverseJoinColumns.forEach(col => delete item[`fk__${col}`]);
740
+ prop.inverseJoinColumns.forEach((col, idx) => {
741
+ core_1.Utils.renameKey(item, `fk__${col}`, prop.targetMeta.primaryKeys[idx]);
742
+ });
520
743
  });
521
744
  return map;
522
745
  }
746
+ getPivotOrderBy(prop, pivotProp, pivotAlias, orderBy) {
747
+ // FIXME this is ignoring the rest of the array items
748
+ if (!core_1.Utils.isEmpty(orderBy)) {
749
+ return [{ [pivotProp.name]: core_1.Utils.asArray(orderBy)[0] }];
750
+ }
751
+ if (!core_1.Utils.isEmpty(prop.orderBy)) {
752
+ return [{ [pivotProp.name]: core_1.Utils.asArray(prop.orderBy)[0] }];
753
+ }
754
+ if (prop.fixedOrder) {
755
+ return [{ [`${pivotAlias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC }];
756
+ }
757
+ return [];
758
+ }
523
759
  async execute(queryOrKnex, params = [], method = 'all', ctx) {
524
760
  return this.rethrow(this.connection.execute(queryOrKnex, params, method, ctx));
525
761
  }
@@ -534,67 +770,115 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
534
770
  const toPopulate = meta.relations
535
771
  .filter(prop => prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner && !relationsToPopulate.includes(prop.name))
536
772
  .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 }));
773
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
538
774
  return [...populate, ...toPopulate];
539
775
  }
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;
776
+ /**
777
+ * @internal
778
+ */
779
+ joinedProps(meta, populate, options) {
780
+ return populate.filter(hint => {
781
+ const [propName, ref] = hint.field.split(':', 2);
782
+ const prop = meta.properties[propName] || {};
783
+ if ((options?.strategy || hint.strategy || prop.strategy || this.config.get('loadStrategy')) !== core_1.LoadStrategy.JOINED) {
784
+ return false;
785
+ }
786
+ if (ref) {
787
+ return [core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind);
788
+ }
789
+ return ![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind);
544
790
  });
545
791
  }
546
792
  /**
547
793
  * @internal
548
794
  */
549
- mergeJoinedResult(rawResults, meta) {
550
- // group by the root entity primary key first
551
- const map = {};
795
+ mergeJoinedResult(rawResults, meta, joinedProps) {
552
796
  const res = [];
553
- rawResults.forEach(item => {
797
+ const map = {};
798
+ for (const item of rawResults) {
554
799
  const pk = core_1.Utils.getCompositeKeyHash(item, meta);
555
800
  if (map[pk]) {
556
- map[pk].push(item);
801
+ for (const hint of joinedProps) {
802
+ const [propName, ref] = hint.field.split(':', 2);
803
+ const prop = meta.properties[propName];
804
+ if (!item[propName]) {
805
+ continue;
806
+ }
807
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
808
+ map[pk][propName] = [...map[pk][propName], ...item[propName]];
809
+ continue;
810
+ }
811
+ switch (prop.kind) {
812
+ case core_1.ReferenceKind.ONE_TO_MANY:
813
+ case core_1.ReferenceKind.MANY_TO_MANY:
814
+ map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
815
+ break;
816
+ case core_1.ReferenceKind.MANY_TO_ONE:
817
+ case core_1.ReferenceKind.ONE_TO_ONE:
818
+ map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
819
+ break;
820
+ }
821
+ }
557
822
  }
558
823
  else {
559
- map[pk] = [item];
824
+ map[pk] = item;
560
825
  res.push(item);
561
826
  }
562
- });
827
+ }
563
828
  return res;
564
829
  }
565
- getFieldsForJoinedLoad(qb, meta, explicitFields, populate = [], parentTableAlias, parentJoinPath) {
830
+ getFieldsForJoinedLoad(qb, meta, explicitFields, exclude, populate = [], options, parentTableAlias, parentJoinPath) {
566
831
  const fields = [];
567
- const joinedProps = this.joinedProps(meta, populate);
832
+ const joinedProps = this.joinedProps(meta, populate, options);
568
833
  if (explicitFields?.includes('*')) {
569
834
  fields.push('*');
570
835
  }
571
836
  const shouldHaveColumn = (prop, populate, fields) => {
572
- if (!this.platform.shouldHaveColumn(prop, populate)) {
837
+ if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
573
838
  return false;
574
839
  }
575
840
  if (!fields || prop.primary) {
576
841
  return !fields?.includes('*');
577
842
  }
578
- return fields.includes(prop.name);
843
+ return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
579
844
  };
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];
845
+ const populateWhereAll = options?._populateWhere === 'all' || core_1.Utils.isEmpty(options?._populateWhere);
846
+ // root entity is already handled, skip that
847
+ if (parentJoinPath) {
848
+ // alias all fields in the primary table
849
+ meta.props
850
+ .filter(prop => shouldHaveColumn(prop, populate, explicitFields))
851
+ .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias)));
852
+ }
853
+ joinedProps.forEach(hint => {
854
+ const [propName, ref] = hint.field.split(':', 2);
855
+ const prop = meta.properties[propName];
586
856
  const meta2 = this.metadata.find(prop.type);
857
+ const pivotRefJoin = prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref;
587
858
  const tableAlias = qb.getNextAlias(prop.name);
588
859
  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);
860
+ let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
861
+ if (!parentJoinPath && populateWhereAll && !path.startsWith('[populate]')) {
862
+ path = '[populate]' + path;
863
+ }
864
+ const joinType = pivotRefJoin ? query_1.JoinType.pivotJoin : query_1.JoinType.leftJoin;
865
+ qb.join(field, tableAlias, {}, joinType, path);
866
+ if (pivotRefJoin) {
867
+ 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}`)));
868
+ }
869
+ if (prop.kind === core_1.ReferenceKind.ONE_TO_MANY && ref) {
870
+ fields.push(...this.getFieldsForJoinedLoad(qb, meta2, prop.referencedColumnNames, undefined, hint.children, options, tableAlias, path));
871
+ }
591
872
  const childExplicitFields = explicitFields?.filter(f => core_1.Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
592
873
  explicitFields?.forEach(f => {
593
874
  if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
594
875
  childExplicitFields.push(f.substring(prop.name.length + 1));
595
876
  }
596
877
  });
597
- fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, relation.children, tableAlias, path));
878
+ const childExclude = exclude ? core_1.Utils.extractChildElements(exclude, prop.name) : exclude;
879
+ if (!ref) {
880
+ fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, childExclude, hint.children, options, tableAlias, path));
881
+ }
598
882
  });
599
883
  return fields;
600
884
  }
@@ -602,24 +886,35 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
602
886
  * @internal
603
887
  */
604
888
  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()];
889
+ const knex = this.connection.getKnex();
890
+ const aliased = knex.ref(tableAlias ? `${tableAlias}__${prop.fieldNames[0]}` : prop.fieldNames[0]).toString();
891
+ if (tableAlias && prop.customTypes?.some(type => type?.convertToJSValueSQL)) {
892
+ return prop.fieldNames.map((col, idx) => {
893
+ if (!prop.customTypes[idx]?.convertToJSValueSQL) {
894
+ return col;
895
+ }
896
+ const prefixed = knex.ref(col).withSchema(tableAlias).toString();
897
+ const aliased = knex.ref(`${tableAlias}__${col}`).toString();
898
+ return (0, core_1.raw)(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
899
+ });
900
+ }
901
+ if (tableAlias && prop.customType?.convertToJSValueSQL) {
902
+ const prefixed = knex.ref(prop.fieldNames[0]).withSchema(tableAlias).toString();
903
+ return [(0, core_1.raw)(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
609
904
  }
610
905
  if (prop.formula) {
611
- const alias = qb.ref(tableAlias ?? qb.alias).toString();
612
- return [`${prop.formula(alias)} as ${aliased}`];
906
+ const alias = knex.ref(tableAlias ?? qb.alias).toString();
907
+ return [(0, core_1.raw)(`${prop.formula(alias)} as ${aliased}`)];
613
908
  }
614
909
  if (tableAlias) {
615
- return prop.fieldNames.map(fieldName => qb.ref(fieldName).withSchema(tableAlias).as(`${tableAlias}__${fieldName}`));
910
+ return prop.fieldNames.map(fieldName => knex.ref(fieldName).withSchema(tableAlias).as(`${tableAlias}__${fieldName}`));
616
911
  }
617
912
  return prop.fieldNames;
618
913
  }
619
914
  /** @internal */
620
- createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes) {
915
+ createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext) {
621
916
  const connectionType = this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
622
- const qb = new query_1.QueryBuilder(entityName, this.metadata, this, ctx, undefined, connectionType);
917
+ const qb = new query_1.QueryBuilder(entityName, this.metadata, this, ctx, undefined, connectionType, undefined, loggerContext);
623
918
  if (!convertCustomTypes) {
624
919
  qb.unsetFlag(core_1.QueryFlag.CONVERT_CUSTOM_TYPES);
625
920
  }
@@ -656,77 +951,100 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
656
951
  }
657
952
  for (const prop of meta.relations) {
658
953
  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);
954
+ const pivotMeta = this.metadata.find(prop.pivotEntity);
955
+ const persister = new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema);
956
+ persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
957
+ await this.rethrow(persister.execute());
674
958
  }
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
959
  }
704
960
  }
705
961
  async lockPessimistic(entity, options) {
706
962
  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);
963
+ const qb = this.createQueryBuilder(entity.constructor.name, options.ctx).withSchema(options.schema ?? meta.schema);
708
964
  const cond = core_1.Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
709
- qb.select('1').where(cond).setLockMode(options.lockMode, options.lockTableAliases);
965
+ qb.select((0, core_1.raw)('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
710
966
  await this.rethrow(qb.execute());
711
967
  }
712
- buildJoinedPropsOrderBy(entityName, qb, meta, populate, parentPath) {
968
+ buildOrderBy(qb, meta, populate, options) {
969
+ const joinedProps = this.joinedProps(meta, populate, options);
970
+ // `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
971
+ // as `options.populateWhere` will be always recomputed to respect filters
972
+ const populateWhereAll = options._populateWhere !== 'infer' && !core_1.Utils.isEmpty(options._populateWhere);
973
+ const path = (populateWhereAll ? '[populate]' : '') + meta.className;
974
+ const populateOrderBy = this.buildPopulateOrderBy(qb, meta, core_1.Utils.asArray(options.populateOrderBy ?? options.orderBy), path);
975
+ const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
976
+ return [...core_1.Utils.asArray(options.orderBy), ...populateOrderBy, ...joinedPropsOrderBy];
977
+ }
978
+ buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, parentAlias = qb.alias) {
713
979
  const orderBy = [];
714
- const joinedProps = this.joinedProps(meta, populate);
715
- joinedProps.forEach(relation => {
716
- const prop = meta.properties[relation.field];
980
+ for (let i = 0; i < populateOrderBy.length; i++) {
981
+ const orderHint = populateOrderBy[i];
982
+ for (const propName of core_1.Utils.keys(orderHint)) {
983
+ const prop = meta.properties[propName];
984
+ if (!prop) {
985
+ throw new Error(`Trying to order by not existing property ${meta.className}.${propName}`);
986
+ }
987
+ const meta2 = this.metadata.find(prop.type);
988
+ const childOrder = orderHint[prop.name];
989
+ let path = `${parentPath}.${propName}`;
990
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
991
+ path += '[pivot]';
992
+ }
993
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
994
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
995
+ if (!join && parentAlias === qb.alias) {
996
+ continue;
997
+ }
998
+ if (![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
999
+ const children = this.buildPopulateOrderBy(qb, meta2, core_1.Utils.asArray(childOrder), path, propAlias);
1000
+ orderBy.push(...children);
1001
+ continue;
1002
+ }
1003
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && join) {
1004
+ if (prop.fixedOrderColumn) {
1005
+ orderBy.push({ [`${join.alias}.${prop.fixedOrderColumn}`]: childOrder });
1006
+ }
1007
+ else {
1008
+ for (const col of prop.inverseJoinColumns) {
1009
+ orderBy.push({ [`${join.ownerAlias}.${col}`]: childOrder });
1010
+ }
1011
+ }
1012
+ continue;
1013
+ }
1014
+ const order = typeof childOrder === 'object' ? childOrder[propName] : childOrder;
1015
+ orderBy.push({ [`${propAlias}.${propName}`]: order });
1016
+ }
1017
+ }
1018
+ return orderBy;
1019
+ }
1020
+ buildJoinedPropsOrderBy(qb, meta, populate, options, parentPath) {
1021
+ const orderBy = [];
1022
+ const joinedProps = this.joinedProps(meta, populate, options);
1023
+ for (const hint of joinedProps) {
1024
+ const [propName, ref] = hint.field.split(':', 2);
1025
+ const prop = meta.properties[propName];
717
1026
  const propOrderBy = prop.orderBy;
718
- const path = `${parentPath ? parentPath : entityName}.${relation.field}`;
719
- const propAlias = qb.getAliasForJoinPath(path);
1027
+ let path = `${parentPath}.${propName}`;
1028
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
1029
+ path += '[pivot]';
1030
+ }
1031
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1032
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
1033
+ const meta2 = this.metadata.find(prop.type);
1034
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1035
+ const alias = ref ? propAlias : join.ownerAlias;
1036
+ orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC });
1037
+ }
720
1038
  if (propOrderBy) {
721
1039
  core_1.Utils.keys(propOrderBy).forEach(field => {
722
1040
  orderBy.push({ [`${propAlias}.${field}`]: propOrderBy[field] });
723
1041
  });
724
1042
  }
725
- if (relation.children) {
726
- const meta2 = this.metadata.find(prop.type);
727
- orderBy.push(...this.buildJoinedPropsOrderBy(prop.name, qb, meta2, relation.children, path));
1043
+ if (hint.children) {
1044
+ const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1045
+ orderBy.push(...buildJoinedPropsOrderBy);
728
1046
  }
729
- });
1047
+ }
730
1048
  return orderBy;
731
1049
  }
732
1050
  normalizeFields(fields, prefix = '') {
@@ -750,7 +1068,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
750
1068
  }
751
1069
  if (prop.kind === core_1.ReferenceKind.EMBEDDED) {
752
1070
  if (prop.object) {
753
- ret.push(prop.fieldNames[0]);
1071
+ ret.push(prop.name);
754
1072
  return;
755
1073
  }
756
1074
  const parts = field.split('.');
@@ -762,21 +1080,18 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
762
1080
  }
763
1081
  return;
764
1082
  }
765
- if (prop.fieldNames) {
766
- ret.push(...prop.fieldNames);
767
- }
1083
+ ret.push(prop.name);
768
1084
  }
769
- buildFields(meta, populate, joinedProps, qb, fields) {
1085
+ buildFields(meta, populate, joinedProps, qb, alias, options) {
770
1086
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all));
771
1087
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
772
- const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL);
773
- const hasExplicitFields = !!fields;
1088
+ const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
1089
+ const hasExplicitFields = !!options.fields;
774
1090
  const ret = [];
775
- if (joinedProps.length > 0) {
776
- ret.push(...this.getFieldsForJoinedLoad(qb, meta, fields, populate));
777
- }
778
- else if (fields) {
779
- for (const field of this.normalizeFields(fields)) {
1091
+ let addFormulas = false;
1092
+ // handle root entity properties first, this is used for both strategies in the same way
1093
+ if (options.fields) {
1094
+ for (const field of this.normalizeFields(options.fields)) {
780
1095
  if (field === '*') {
781
1096
  ret.push('*');
782
1097
  continue;
@@ -792,28 +1107,39 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
792
1107
  });
793
1108
  this.processField(meta, prop, parts.join('.'), ret, populate, joinedProps, qb);
794
1109
  }
795
- ret.unshift(...meta.primaryKeys.filter(pk => !fields.includes(pk)));
1110
+ if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
1111
+ ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
1112
+ }
796
1113
  }
797
- else if (lazyProps.filter(p => !p.formula).length > 0) {
798
- const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, false));
1114
+ else if (!core_1.Utils.isEmpty(options.exclude) || lazyProps.some(p => !p.formula)) {
1115
+ const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, options.exclude, false));
799
1116
  ret.push(...core_1.Utils.flatten(props.filter(p => !lazyProps.includes(p)).map(p => p.fieldNames)));
1117
+ addFormulas = true;
800
1118
  }
801
1119
  else if (hasLazyFormulas || requiresSQLConversion) {
802
1120
  ret.push('*');
1121
+ addFormulas = true;
803
1122
  }
804
- if (ret.length > 0 && !hasExplicitFields) {
1123
+ else {
1124
+ ret.push('*');
1125
+ }
1126
+ if (ret.length > 0 && !hasExplicitFields && addFormulas) {
805
1127
  meta.props
806
1128
  .filter(prop => prop.formula && !lazyProps.includes(prop))
807
1129
  .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}`);
1130
+ const a = this.connection.getKnex().ref(alias).toString();
1131
+ const aliased = this.connection.getKnex().ref(prop.fieldNames[0]).toString();
1132
+ ret.push((0, core_1.raw)(`${prop.formula(a)} as ${aliased}`));
811
1133
  });
812
1134
  meta.props
813
- .filter(prop => prop.customType?.convertToDatabaseValueSQL || prop.customType?.convertToJSValueSQL)
1135
+ .filter(prop => prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)
814
1136
  .forEach(prop => ret.push(prop.name));
815
1137
  }
816
- return ret.length > 0 ? core_1.Utils.unique(ret) : ['*'];
1138
+ // add joined relations after the root entity fields
1139
+ if (joinedProps.length > 0) {
1140
+ ret.push(...this.getFieldsForJoinedLoad(qb, meta, options.fields, options.exclude, populate, options, alias));
1141
+ }
1142
+ return core_1.Utils.unique(ret);
817
1143
  }
818
1144
  }
819
1145
  exports.AbstractSqlDriver = AbstractSqlDriver;