@mikro-orm/sql 7.2.6-dev.3 → 7.2.6-dev.30
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.
- package/AbstractSqlConnection.js +7 -0
- package/AbstractSqlDriver.js +16 -13
- package/dialects/mysql/MySqlSchemaHelper.js +12 -2
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +2 -3
- package/package.json +3 -3
- package/query/ObjectCriteriaNode.js +2 -0
- package/query/QueryBuilder.js +2 -1
- package/schema/DatabaseTable.js +11 -0
- package/schema/SchemaComparator.js +4 -10
- package/schema/SchemaHelper.js +3 -1
package/AbstractSqlConnection.js
CHANGED
|
@@ -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();
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -467,9 +467,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
467
467
|
return;
|
|
468
468
|
}
|
|
469
469
|
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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),
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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}"
|
|
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
|
+
"version": "7.2.6-dev.30",
|
|
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.
|
|
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.
|
|
56
|
+
"@mikro-orm/core": "7.2.6-dev.30"
|
|
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)
|
package/query/QueryBuilder.js
CHANGED
|
@@ -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;
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -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
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
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) {
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -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 =>
|
|
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
|
}
|