@mikro-orm/sql 7.2.2-dev.2 → 7.2.2-dev.21

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.
@@ -17,7 +17,11 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
17
17
  /** Evaluates a formula callback, handling both string and Raw return values. */
18
18
  evaluateFormula(formula: (...args: any[]) => string | Raw, columns: any, table: FormulaTable): string;
19
19
  /** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
20
- private getTableProps;
20
+ protected getTableProps<T extends object>(meta: EntityMetadata<T>): EntityProperty<T>[];
21
+ /** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
22
+ protected getUpdateReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
23
+ /** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
24
+ protected getReturningFieldSQL(prop: EntityProperty, fieldName: string, index: number): string;
21
25
  /** Creates a FormulaTable object for use in formula callbacks. */
22
26
  private createFormulaTable;
23
27
  private validateSqlOptions;
@@ -49,6 +49,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
49
49
  getTableProps(meta) {
50
50
  return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
51
51
  }
52
+ /** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
53
+ getUpdateReturningProperties(meta, data) {
54
+ const returning = new Set();
55
+ for (const row of data) {
56
+ for (const k of Utils.keys(row)) {
57
+ if (isRaw(row[k])) {
58
+ returning.add(meta.properties[k] ?? meta.root.properties[k]);
59
+ }
60
+ }
61
+ }
62
+ meta.getPrimaryProps().forEach(prop => returning.add(prop));
63
+ // a mixed STI batch shares one table, so the child columns live on the root
64
+ this.getTableProps(meta.root.discriminatorColumn ? meta.root : meta)
65
+ .filter(prop => prop.generated || prop.version || prop.returning)
66
+ .forEach(prop => returning.add(prop));
67
+ return [...returning];
68
+ }
69
+ /** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
70
+ getReturningFieldSQL(prop, fieldName, index) {
71
+ const quoted = this.platform.quoteIdentifier(fieldName);
72
+ const customType = prop.customTypes?.[index] ?? prop.customType;
73
+ if (prop.hasConvertToJSValueSQL && customType?.convertToJSValueSQL) {
74
+ return `${customType.convertToJSValueSQL(quoted, this.platform)} as ${quoted}`;
75
+ }
76
+ return quoted;
77
+ }
52
78
  /** Creates a FormulaTable object for use in formula callbacks. */
53
79
  createFormulaTable(alias, meta, schema) {
54
80
  const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
@@ -946,7 +972,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
946
972
  /* v8 ignore next */
947
973
  const uniqueFields = options.onConflictFields ??
948
974
  (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
949
- const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
975
+ const returning = getOnConflictReturningFields(meta, data, uniqueFields, options, this.platform);
950
976
  qb.insert(data)
951
977
  .onConflict(uniqueFields)
952
978
  .returning(returning);
@@ -963,11 +989,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
963
989
  }
964
990
  else {
965
991
  qb.update(data).where(where);
966
- // reload generated columns and version fields
967
992
  const returning = [];
968
993
  this.getTableProps(meta)
969
- .filter(prop => (prop.generated && !prop.primary) || prop.version)
970
- .forEach(prop => returning.push(prop.name));
994
+ .filter(prop => (prop.generated && !prop.primary) || prop.version || prop.returning || isRaw(data[prop.name]))
995
+ .forEach(prop => returning.push(...prop.fieldNames));
971
996
  qb.returning(returning);
972
997
  }
973
998
  res = await this.rethrow(qb.execute('run', false));
@@ -1003,7 +1028,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1003
1028
  : meta.primaryKeys);
1004
1029
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
1005
1030
  qb.setAbortOptions(pickAbortOptions(options));
1006
- let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
1031
+ let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options, this.platform);
1007
1032
  if (meta.inheritanceType === 'tpt') {
1008
1033
  // each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
1009
1034
  const own = (key) => meta.primaryKeys.includes(key) ||
@@ -1032,20 +1057,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1032
1057
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
1033
1058
  const keys = new Set();
1034
1059
  const fields = new Set();
1035
- const returning = new Set();
1060
+ const returning = this.getUpdateReturningProperties(meta, data);
1036
1061
  for (const row of data) {
1037
1062
  for (const k of Utils.keys(row)) {
1038
1063
  keys.add(k);
1039
- if (isRaw(row[k])) {
1040
- returning.add(k);
1041
- }
1042
1064
  }
1043
1065
  }
1044
- // reload generated columns and version fields
1045
- meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
1046
- this.getTableProps(meta)
1047
- .filter(prop => prop.generated || prop.version)
1048
- .forEach(prop => returning.add(prop.name));
1049
1066
  const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
1050
1067
  .map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
1051
1068
  .join(' and ');
@@ -1115,7 +1132,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1115
1132
  }
1116
1133
  sql += `, `;
1117
1134
  }
1118
- sql = sql.substring(0, sql.length - 2) + ' where ';
1135
+ sql = sql.substring(0, sql.length - 2);
1136
+ if (this.platform.usesOutputStatement() && returning.length > 0) {
1137
+ const fields = returning.flatMap(prop => prop.fieldNames);
1138
+ sql += ` output ${fields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`;
1139
+ }
1140
+ sql += ' where ';
1119
1141
  const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
1120
1142
  const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
1121
1143
  const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
@@ -1151,13 +1173,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1151
1173
  else {
1152
1174
  sql += conds.join(' or ');
1153
1175
  }
1154
- if (this.platform.usesReturningStatement() && returning.size > 0) {
1155
- const returningFields = Utils.flatten([...returning].map(prop => (meta.properties[prop] ?? meta.root.properties[prop]).fieldNames));
1156
- /* v8 ignore next */
1157
- sql +=
1158
- returningFields.length > 0
1159
- ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
1160
- : '';
1176
+ if (this.platform.usesReturningStatement() && returning.length > 0) {
1177
+ const returningFields = returning.flatMap(prop => prop.fieldNames.map((field, idx) => this.getReturningFieldSQL(prop, field, idx)));
1178
+ sql += ` returning ${returningFields.join(', ')}`;
1161
1179
  }
1162
1180
  if (transform) {
1163
1181
  sql = transform(sql, params);
@@ -6,6 +6,7 @@ export declare class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
6
6
  params: unknown[];
7
7
  };
8
8
  protected compileInsert(): void;
9
+ protected addOutputClause(type: 'inserted' | 'deleted'): void;
9
10
  private appendOutputTable;
10
11
  private compileUpsert;
11
12
  protected compileSelect(): void;
@@ -22,6 +22,9 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
22
22
  this.compileCtes();
23
23
  if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) {
24
24
  this.compileUpsert();
25
+ if (suffix) {
26
+ this.parts.push(suffix);
27
+ }
25
28
  }
26
29
  else {
27
30
  switch (this.type) {
@@ -72,31 +75,34 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
72
75
  return;
73
76
  }
74
77
  const parts = this.processInsertData();
75
- if (this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
76
- this.parts[this.parts.length - 2] += ' into #out ';
77
- }
78
78
  this.parts.push(parts.join(', '));
79
79
  }
80
+ addOutputClause(type) {
81
+ super.addOutputClause(type);
82
+ if (this.options.returning?.length && this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
83
+ this.parts[this.parts.length - 1] += ' into #out';
84
+ }
85
+ }
80
86
  appendOutputTable() {
81
- if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
87
+ if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE) || !this.options.returning?.length) {
82
88
  return { prefix: '', suffix: '' };
83
89
  }
84
90
  const returningFields = this.options.returning;
85
91
  const selections = returningFields.map(field => `[t].${this.platform.quoteIdentifier(field)}`).join(',');
86
92
  return {
87
93
  prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`,
88
- suffix: `select ${selections} from #out as t; drop table #out`,
94
+ suffix: `select ${selections} from #out as t; select @@rowcount as [__mikro_orm_row_count__]; drop table #out`,
89
95
  };
90
96
  }
91
97
  compileUpsert() {
92
98
  const clause = this.options.onConflict;
93
99
  const dataAsArray = Utils.asArray(this.options.data);
94
- const keys = Object.keys(dataAsArray[0]);
100
+ const keys = [...new Set(dataAsArray.flatMap(data => Object.keys(data)))];
95
101
  const values = keys.map(() => '?');
96
102
  const parts = [];
97
103
  for (const data of dataAsArray) {
98
104
  for (const key of keys) {
99
- this.params.push(data[key]);
105
+ this.params.push(data[key] ?? null);
100
106
  }
101
107
  parts.push(`(${values.join(', ')})`);
102
108
  }
@@ -419,7 +419,11 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
419
419
  if ((col.udt_name === 'geometry' || col.udt_name === 'geography') && col.format_type) {
420
420
  type = col.format_type;
421
421
  }
422
- if (type === 'vector' && col.length == null && col.custom_length != null && col.custom_length !== -1) {
422
+ // pgvector stores the dimension in atttypmod for all three of its vector types
423
+ if (['vector', 'halfvec', 'sparsevec'].includes(type) &&
424
+ col.length == null &&
425
+ col.custom_length != null &&
426
+ col.custom_length !== -1) {
423
427
  col.length = col.custom_length;
424
428
  }
425
429
  if (col.length != null && !type.endsWith(`(${col.length})`) && !['text', 'date'].includes(type)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.2.2-dev.2",
3
+ "version": "7.2.2-dev.21",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.2.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.2-dev.2"
56
+ "@mikro-orm/core": "7.2.2-dev.21"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -127,7 +127,7 @@ export declare class NativeQueryBuilder implements Subquery {
127
127
  as(alias: string): this;
128
128
  toRaw(): RawQueryFragment;
129
129
  protected compileSelect(): void;
130
- /** Whether this COUNT query needs a subquery wrapper for multi-column distinct. */
130
+ /** Whether this COUNT query needs a subquery wrapper: multi-column distinct, or `distinct on` (keeps the null group). */
131
131
  protected needsCountSubquery(): boolean;
132
132
  protected getFields(countSubquery?: boolean): string;
133
133
  protected compileInsert(): void;
@@ -330,12 +330,12 @@ export class NativeQueryBuilder {
330
330
  this.parts.push(`)${asKeyword}${this.quote('dcnt')}`);
331
331
  }
332
332
  }
333
- /** Whether this COUNT query needs a subquery wrapper for multi-column distinct. */
333
+ /** Whether this COUNT query needs a subquery wrapper: multi-column distinct, or `distinct on` (keeps the null group). */
334
334
  needsCountSubquery() {
335
335
  return (this.type === QueryType.COUNT &&
336
336
  !!this.options.distinct &&
337
- this.options.select.length > 1 &&
338
- !this.platform.supportsMultiColumnCountDistinct());
337
+ ((this.options.select.length > 1 && !this.platform.supportsMultiColumnCountDistinct()) ||
338
+ !!this.options.distinctOn));
339
339
  }
340
340
  getFields(countSubquery) {
341
341
  if (!this.options.select || this.options.select.length === 0) {
@@ -257,7 +257,11 @@ export class QueryBuilder {
257
257
  if (field) {
258
258
  this.#state.fields = Utils.asArray(field);
259
259
  }
260
- else if (distinct || this.hasToManyJoins()) {
260
+ else if (this.#state.distinctOn) {
261
+ this.#state.fields = this.#state.distinctOn;
262
+ distinct = true;
263
+ }
264
+ else if (distinct || this.#state.flags.has(QueryFlag.DISTINCT) || this.hasToManyJoins()) {
261
265
  this.#state.fields = this.mainAlias.meta.primaryKeys;
262
266
  }
263
267
  else {
@@ -916,18 +920,13 @@ export class QueryBuilder {
916
920
  return;
917
921
  }
918
922
  if (this.type === QueryType.UPDATE && data) {
919
- const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
923
+ const returningProps = meta.hydrateProps
924
+ .filter(prop => prop.returning || (prop.fieldNames && isRaw(data[prop.fieldNames[0]])))
925
+ // a TPT table can only return its own columns
926
+ .filter(prop => meta.inheritanceType !== 'tpt' || prop.primary || meta.ownProps.some(p => p.name === prop.name));
920
927
  if (returningProps.length > 0) {
921
- qb.returning(returningProps.flatMap((prop) => {
922
- if (prop.hasConvertToJSValueSQL) {
923
- const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
924
- const sql = prop.customType.convertToJSValueSQL(aliased, this.platform) +
925
- ' as ' +
926
- this.platform.quoteIdentifier(prop.fieldNames[0]);
927
- return [raw(sql)];
928
- }
929
- return prop.fieldNames;
930
- }));
928
+ const fields = returningProps.flatMap(prop => prop.fieldNames);
929
+ qb.returning(fields.map(field => this.helper.mapper(field, this.type)));
931
930
  }
932
931
  }
933
932
  }
@@ -1571,7 +1570,7 @@ export class QueryBuilder {
1571
1570
  return;
1572
1571
  }
1573
1572
  if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
1574
- const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
1573
+ const name = prop.formula ? prop.name : (prop.embeddedPath?.join('.') ?? prop.fieldNames[0]);
1575
1574
  const aliased = this.#state.aliases[a] ? `${a}.${name}` : name;
1576
1575
  ret.push(getFieldName(aliased, customAlias));
1577
1576
  return;
@@ -1586,11 +1585,11 @@ export class QueryBuilder {
1586
1585
  }
1587
1586
  const nest = (prop) => {
1588
1587
  for (const childProp of Object.values(prop.embeddedProps)) {
1589
- if (childProp.persist === false) {
1588
+ if (childProp.persist === false && !childProp.formula) {
1590
1589
  continue;
1591
1590
  }
1592
1591
  if (childProp.fieldNames && (childProp.kind !== ReferenceKind.EMBEDDED || childProp.object)) {
1593
- const name = childProp.fieldNames[0];
1592
+ const name = childProp.formula ? childProp.name : childProp.fieldNames[0];
1594
1593
  ret.push(getFieldName(this.#state.aliases[a] ? `${a}.${name}` : name));
1595
1594
  }
1596
1595
  else {
@@ -1671,7 +1670,8 @@ export class QueryBuilder {
1671
1670
  return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
1672
1671
  }
1673
1672
  }
1674
- return `${currentAlias}.${embeddedProp?.fieldNames?.[0] ?? propName}`;
1673
+ const name = embeddedProp?.formula ? embeddedProp.name : (embeddedProp?.fieldNames?.[0] ?? propName);
1674
+ return `${currentAlias}.${name}`;
1675
1675
  }
1676
1676
  // Handle relations - auto-join if not the last part
1677
1677
  if (prop.kind === ReferenceKind.MANY_TO_ONE ||
@@ -1764,6 +1764,9 @@ export class QueryBuilder {
1764
1764
  case QueryType.COUNT: {
1765
1765
  const fields = this.#state.fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
1766
1766
  qb.count(fields, this.#state.flags.has(QueryFlag.DISTINCT));
1767
+ if (this.#state.distinctOn && this.#state.flags.has(QueryFlag.DISTINCT)) {
1768
+ qb.distinctOn(this.prepareFields(this.#state.distinctOn, 'where', schema));
1769
+ }
1767
1770
  this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
1768
1771
  break;
1769
1772
  }
@@ -2327,7 +2330,10 @@ export class QueryBuilder {
2327
2330
  const fieldName = this.helper.mapper(field, this.type, undefined, null);
2328
2331
  // virtual fields (e.g. `qb.as(...)`) have no column to reference inside `min()`; inline their
2329
2332
  // expression instead, as a select alias is not resolvable there on some dialects (e.g. PostgreSQL)
2330
- const virtual = !prop?.persist && !prop?.formula && !prop?.hasConvertToJSValueSQL && !pks.includes(fieldName)
2333
+ const virtual = (!prop || prop.persist === false) &&
2334
+ !prop?.formula &&
2335
+ !prop?.hasConvertToJSValueSQL &&
2336
+ !pks.includes(fieldName)
2331
2337
  ? this.resolveVirtualField(f, fieldName)
2332
2338
  : undefined;
2333
2339
  const expr = virtual ? virtual.expr : this.platform.quoteIdentifier(fieldName);
@@ -112,7 +112,7 @@ export class QueryBuilderHelper {
112
112
  const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
113
113
  const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
114
114
  const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2;
115
- if (a === prop?.embedded?.[0]) {
115
+ if (a === prop?.embedded?.[0] && !prop.formula) {
116
116
  return aliasPrefix + prop.fieldNames[fkIdx];
117
117
  }
118
118
  const noPrefix = prop?.persist === false;
@@ -120,29 +120,23 @@ export class QueryBuilderHelper {
120
120
  return raw(this.prefix(field, isTableNameAliasRequired));
121
121
  }
122
122
  if (prop?.formula) {
123
- const alias2 = this.#platform.quoteIdentifier(a).toString();
123
+ const formulaAlias = isTableAlias ? a : this.#alias;
124
+ const alias2 = this.#platform.quoteIdentifier(formulaAlias).toString();
124
125
  const aliasName = alias === undefined ? prop.fieldNames[0] : alias;
125
126
  const as = aliasName === null ? '' : ` as ${this.#platform.quoteIdentifier(aliasName)}`;
126
127
  const meta = this.#aliasMap[a]?.meta ?? this.#metadata.get(this.#entityName);
127
128
  const table = this.createFormulaTable(alias2, meta, schema);
128
- const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2);
129
+ const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, formulaAlias), alias2);
129
130
  let value = this.#driver.evaluateFormula(prop.formula, columns, table);
130
131
  if (!this.isTableNameAliasRequired(type)) {
131
132
  value = value.replaceAll(alias2 + '.', '');
132
133
  }
133
134
  return raw(`${value}${as}`);
134
135
  }
135
- if (prop?.hasConvertToJSValueSQL && type !== QueryType.UPSERT) {
136
- let valueSQL;
137
- if (prop.fieldNames.length > 1 && fkIdx !== -1) {
138
- const fk = prop.targetMeta.getPrimaryProps()[fkIdx];
139
- const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
140
- valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.#platform);
141
- }
142
- else {
143
- const prefixed = this.prefix(field, isTableNameAliasRequired, true);
144
- valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.#platform);
145
- }
136
+ const customType = prop?.customTypes?.[fkIdx] ?? prop?.customType;
137
+ if (prop?.hasConvertToJSValueSQL && customType?.convertToJSValueSQL && type !== QueryType.UPSERT) {
138
+ const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
139
+ const valueSQL = customType.convertToJSValueSQL(prefixed, this.#platform);
146
140
  if (alias === null) {
147
141
  return raw(valueSQL);
148
142
  }