@mikro-orm/sql 7.2.6-dev.3 → 7.2.6-dev.31

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.
@@ -130,6 +130,10 @@ export class AbstractSqlConnection extends Connection {
130
130
  }
131
131
  catch (rollbackError) {
132
132
  this.logger.warn('query', `Failed to roll back transaction: ${rollbackError.message}`);
133
+ // the inner writes are still part of the outer transaction, so it must not commit them
134
+ if (options.ctx) {
135
+ options.ctx.failedSavepointRollback ??= new Error(`Cannot commit the transaction, rolling back to savepoint "${trx.savepointName}" failed: ${rollbackError.message}`, { cause: rollbackError });
136
+ }
133
137
  }
134
138
  throw error;
135
139
  }
@@ -203,6 +207,9 @@ export class AbstractSqlConnection extends Connection {
203
207
  if (ctx.isRolledBack) {
204
208
  return;
205
209
  }
210
+ if ('failedSavepointRollback' in ctx) {
211
+ throw ctx.failedSavepointRollback;
212
+ }
206
213
  await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionCommit, ctx);
207
214
  if ('savepointName' in ctx) {
208
215
  await ctx.releaseSavepoint(ctx.savepointName).execute();
@@ -467,9 +467,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
467
467
  return;
468
468
  }
469
469
  const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
470
- const targetProps = mapToPk
471
- ? meta2.getPrimaryProps()
472
- : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
470
+ // all rows of a result share their columns, so resolve the mapped props once per join path
471
+ const joinedTargets = (map.joinedTargets ??= {});
472
+ const { targetProps, virtualProps } = (joinedTargets[path] ??= {
473
+ targetProps: (mapToPk
474
+ ? meta2.getPrimaryProps()
475
+ : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []))).filter(prop => prop.fieldNames.some(name => typeof root[`${relationAlias}__${name}`] !== 'undefined')),
476
+ virtualProps: meta2.props.filter(prop => !ref && prop.persist === false && prop.fieldNames),
477
+ });
473
478
  // If the primary key value for the relation is null, we know we haven't joined to anything
474
479
  // and therefore we don't return any record (since all values would be null)
475
480
  const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
@@ -490,9 +495,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
490
495
  return;
491
496
  }
492
497
  let relationPojo = {};
493
- meta2.props
494
- .filter(prop => !ref && prop.persist === false && prop.fieldNames)
495
- .forEach(prop => {
498
+ virtualProps.forEach(prop => {
496
499
  /* v8 ignore next */
497
500
  if (prop.fieldNames.length > 1) {
498
501
  // composite keys
@@ -825,11 +828,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
825
828
  value = this.platform.cloneEmbeddable(value);
826
829
  for (let i = 0; i < value.length; i++) {
827
830
  const item = value[i];
828
- value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
831
+ value[i] = this.mapDataToFieldNames(item, false, this.variantProps(prop, item), options.convertCustomTypes);
829
832
  }
830
833
  }
831
834
  else {
832
- value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
835
+ value = this.mapDataToFieldNames(value, false, this.variantProps(prop, value), options.convertCustomTypes);
833
836
  }
834
837
  }
835
838
  if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
@@ -1095,11 +1098,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1095
1098
  if (prop.array && value) {
1096
1099
  for (let i = 0; i < value.length; i++) {
1097
1100
  const item = value[i];
1098
- value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
1101
+ value[i] = this.mapDataToFieldNames(item, false, this.variantProps(prop, item), options.convertCustomTypes);
1099
1102
  }
1100
1103
  }
1101
1104
  else {
1102
- value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
1105
+ value = this.mapDataToFieldNames(value, false, this.variantProps(prop, value), options.convertCustomTypes);
1103
1106
  }
1104
1107
  }
1105
1108
  params.push(value ?? null);
@@ -2115,7 +2118,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
2115
2118
  if (prop.formula) {
2116
2119
  const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
2117
2120
  const table = this.createFormulaTable(quotedAlias, meta, schema);
2118
- const columns = meta.createColumnMappingObject(p => qb.helper.getTPTAliasForProperty(p.name, tableAlias), tableAlias);
2121
+ const columns = meta.createColumnMappingObject(p => qb.helper.getTPTAliasForProperty(p.name, tableAlias), quotedAlias);
2119
2122
  return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
2120
2123
  }
2121
2124
  if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
@@ -2539,9 +2542,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
2539
2542
  ret.push('*');
2540
2543
  }
2541
2544
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
2542
- // Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
2545
+ // `toString()` must yield the quoted alias like `QueryBuilder.finalize()`, which dedupes formulas by rendered SQL
2543
2546
  const quotedAlias = this.platform.quoteIdentifier(alias);
2544
- const columns = meta.createColumnMappingObject(alias);
2547
+ const columns = meta.createColumnMappingObject(alias, quotedAlias.toString());
2545
2548
  const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
2546
2549
  for (const prop of meta.props) {
2547
2550
  if (lazyProps.includes(prop)) {
@@ -1,4 +1,4 @@
1
- import { EnumType, StringType, TextType } from '@mikro-orm/core';
1
+ import { EnumType, JsonType, StringType, TextType, } from '@mikro-orm/core';
2
2
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
3
3
  export class MySqlSchemaHelper extends SchemaHelper {
4
4
  #cache = {};
@@ -601,7 +601,17 @@ export class MySqlSchemaHelper extends SchemaHelper {
601
601
  return super.normalizeDefaultValue(defaultValue, length, MySqlSchemaHelper.DEFAULT_VALUES);
602
602
  }
603
603
  wrap(val, type) {
604
+ if (typeof val !== 'string' || val.length === 0) {
605
+ return val;
606
+ }
607
+ // text/blob literal defaults come back escaped twice with a charset introducer, e.g. `_utf8mb4\'it\\\'s\'` (JSON is parsed by the comparator)
608
+ const expression = type instanceof JsonType ? null : /^_\w+\\'(.*)\\'$/s.exec(val);
609
+ if (expression) {
610
+ const unescape = (str, escapes = {}) => str.replace(/\\(.)/gs, (_, char) => escapes[char] ?? char);
611
+ const sqlEscapes = { 0: '\0', b: '\b', n: '\n', r: '\r', t: '\t', Z: '\x1a' };
612
+ return this.platform.quoteValue(unescape(unescape(expression[1]), sqlEscapes));
613
+ }
604
614
  const stringType = type instanceof StringType || type instanceof TextType || type instanceof EnumType;
605
- return typeof val === 'string' && val.length > 0 && stringType ? this.platform.quoteValue(val) : val;
615
+ return stringType ? this.platform.quoteValue(val) : val;
606
616
  }
607
617
  }
@@ -330,10 +330,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
330
330
  return columnDefs.map(colDef => {
331
331
  const name = unquote(colDef);
332
332
  const result = { name };
333
- // Find this column in the expression and extract modifiers
334
- // Create a pattern that matches the column name (quoted or unquoted) followed by modifiers
333
+ // Match the column (quoted or unquoted) as a whole list item, so `id` never matches inside `author_id`
335
334
  const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
336
- const colPattern = new RegExp(`"?${escapedName}"?\\s*([^,)]*?)(?:,|$)`, 'i');
335
+ const colPattern = new RegExp(`(?:^|,)\\s*"?${escapedName}"?(?=\\s|,|$)\\s*([^,)]*?)(?:,|$)`, 'i');
337
336
  const colMatch = columnsStr.match(colPattern);
338
337
  if (colMatch) {
339
338
  const modifiers = colMatch[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.2.6-dev.3",
3
+ "version": "7.2.6-dev.31",
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",
@@ -47,13 +47,13 @@
47
47
  "copy": "node ../../scripts/copy.mjs"
48
48
  },
49
49
  "dependencies": {
50
- "kysely": "0.29.6"
50
+ "kysely": "0.29.7"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@mikro-orm/core": "^7.2.5"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.6-dev.3"
56
+ "@mikro-orm/core": "7.2.6-dev.31"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -57,6 +57,8 @@ export class ObjectCriteriaNode extends CriteriaNode {
57
57
  // schema-less entities the schema is resolved dynamically and needs to be carried over
58
58
  const fixedSchema = parentMeta.schema && parentMeta.schema !== '*';
59
59
  const qb2 = qb.clone(true, fixedSchema ? undefined : ['schema']);
60
+ // the payload was already converted when processing the parent query
61
+ qb2.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
60
62
  const joinAlias = qb2.getNextAlias(this.prop.targetMeta.class);
61
63
  const sub = qb2
62
64
  .from(parentMeta.class)
@@ -1157,8 +1157,9 @@ export class QueryBuilder {
1157
1157
  const hash = (data) => {
1158
1158
  return Utils.getPrimaryKeyHash(meta.primaryKeys.map(pk => data[pk]));
1159
1159
  };
1160
+ const map = {};
1160
1161
  for await (const row of res) {
1161
- const mapped = this.driver.mapResult(row, meta, this.#state.populate, this);
1162
+ const mapped = this.driver.mapResult(row, meta, this.#state.populate, this, map);
1162
1163
  if (!options.mergeResults || joinedProps.length === 0) {
1163
1164
  yield this.mapResult(mapped, options.mapResults);
1164
1165
  continue;
@@ -924,6 +924,17 @@ export class DatabaseTable {
924
924
  };
925
925
  return [expand(rootProp, 1)];
926
926
  }
927
+ // object embedded property index, the JSON keys are the field names of the embedded properties
928
+ if (rootProp?.embeddable && rootProp.object && rootProp.embeddedProps[parts[1]]) {
929
+ let p = rootProp.embeddedProps[parts[1]];
930
+ let i = 2;
931
+ while (i < parts.length && p.embeddedProps?.[parts[i]]) {
932
+ p = p.embeddedProps[parts[i]];
933
+ i++;
934
+ }
935
+ // a path below the last embedded property targets plain JSON keys, kept as they are
936
+ return [[...p.embeddedPath, ...parts.slice(i)].join('.')];
937
+ }
927
938
  // json index, we need to rename the column only
928
939
  if (rootProp) {
929
940
  return [prop.replace(root, rootProp.fieldNames[0])];
@@ -866,16 +866,10 @@ export class SchemaComparator {
866
866
  if (cols1.length === 0 && cols2.length === 0) {
867
867
  return true;
868
868
  }
869
- // If only one has column options, they don't match
870
- if (cols1.length !== cols2.length) {
871
- return false;
872
- }
873
- // Compare each column's options
874
- // Note: We don't check c1.name !== c2.name because the indexes already have matching columnNames
875
- // and the columns array is derived from those same column names
876
- for (let i = 0; i < cols1.length; i++) {
877
- const c1 = cols1[i];
878
- const c2 = cols2[i];
869
+ // introspection may omit entries for columns with default options, so match them by name
870
+ for (const name of index1.columnNames) {
871
+ const c1 = cols1.find(c => c.name === name) ?? { name };
872
+ const c2 = cols2.find(c => c.name === name) ?? { name };
879
873
  const sort1 = c1.sort?.toUpperCase() ?? 'ASC';
880
874
  const sort2 = c2.sort?.toUpperCase() ?? 'ASC';
881
875
  if (sort1 !== sort2) {
@@ -614,6 +614,8 @@ export class SchemaHelper {
614
614
  }
615
615
  if (useDefault) {
616
616
  // https://dev.mysql.com/doc/refman/9.0/en/data-type-defaults.html
617
+ // `tinytext`, `mediumtext`, `longblob` etc. follow the same rule as `text` and `blob`
618
+ const baseType = column.type.toLowerCase().replace(/^(tiny|medium|long)/, '');
617
619
  const needsExpression = [
618
620
  'blob',
619
621
  'text',
@@ -625,7 +627,7 @@ export class SchemaHelper {
625
627
  'multilinestring',
626
628
  'multipolygon',
627
629
  'geometrycollection',
628
- ].some(type => column.type.toLowerCase().startsWith(type));
630
+ ].some(type => baseType.startsWith(type));
629
631
  const defaultSql = needsExpression && !column.default.startsWith('(') ? `(${column.default})` : column.default;
630
632
  col.push(`default ${defaultSql}`);
631
633
  }