@mikro-orm/sql 7.0.15-dev.1 → 7.0.15-dev.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.15-dev.1",
3
+ "version": "7.0.15-dev.11",
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.0.14"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.0.15-dev.1"
56
+ "@mikro-orm/core": "7.0.15-dev.11"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -1,5 +1,5 @@
1
- import { type EntityMetadata, type EntityProperty } from '@mikro-orm/core';
2
- import { type CommonTableExpressionNameNode, type DeleteQueryNode, type IdentifierNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, OperationNodeTransformer, TableNode } from 'kysely';
1
+ import { type EntityMetadata, type EntityProperty, type Type } from '@mikro-orm/core';
2
+ import { type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
3
3
  import type { MikroKyselyPluginOptions } from './index.js';
4
4
  import type { SqlEntityManager } from '../SqlEntityManager.js';
5
5
  export declare class MikroTransformer extends OperationNodeTransformer {
@@ -28,10 +28,18 @@ export declare class MikroTransformer extends OperationNodeTransformer {
28
28
  processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
29
29
  processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
30
30
  processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
31
+ processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
32
+ expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
33
+ expandSelection(sel: SelectionNode): SelectionNode[] | null;
34
+ expandStar(meta: EntityMetadata | undefined, table: TableNode | undefined, tableName: string | undefined): SelectionNode[] | null;
35
+ wrapRead(customType: Type<any>, fieldName: string, tableName: string | undefined): SelectionNode;
36
+ wrapWrite(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
37
+ /** Resolve the customType for a specific field name (handles composite-PK FK fan-out via prop.customTypes[]). */
38
+ fieldType(prop: EntityProperty, fieldName: string): Type<any> | undefined;
39
+ findOwnerMeta(name: string | undefined): EntityMetadata | undefined;
31
40
  mapColumnsToProperties(columns: readonly ColumnNode[], meta: EntityMetadata): (EntityProperty | undefined)[];
32
41
  normalizeColumnName(identifier: IdentifierNode): string;
33
42
  findProperty(meta: EntityMetadata | undefined, columnName?: string): EntityProperty | undefined;
34
- shouldConvertValues(): boolean;
35
43
  prepareInputValue(prop: EntityProperty | undefined, value: unknown, enabled: boolean): unknown;
36
44
  /**
37
45
  * Look up a table name/alias in the context stack.
@@ -1,5 +1,11 @@
1
1
  import { ReferenceKind, isRaw, } from '@mikro-orm/core';
2
- import { AliasNode, ColumnNode, ColumnUpdateNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, SchemableIdentifierNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
2
+ import { AliasNode, ColumnNode, ColumnUpdateNode, IdentifierNode, OperationNodeTransformer, PrimitiveValueListNode, RawNode, ReferenceNode, SchemableIdentifierNode, SelectAllNode, SelectionNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
3
+ const EXPANDABLE_KINDS = new Set([
4
+ ReferenceKind.SCALAR,
5
+ ReferenceKind.EMBEDDED,
6
+ ReferenceKind.MANY_TO_ONE,
7
+ ReferenceKind.ONE_TO_ONE,
8
+ ]);
3
9
  export class MikroTransformer extends OperationNodeTransformer {
4
10
  /**
5
11
  * Context stack to support nested queries (subqueries, CTEs)
@@ -64,7 +70,14 @@ export class MikroTransformer extends OperationNodeTransformer {
64
70
  this.processJoinNode(join, currentContext);
65
71
  }
66
72
  }
67
- return super.transformSelectQuery(node, queryId);
73
+ const transformed = super.transformSelectQuery(node, queryId);
74
+ if (this.#options.convertValues && transformed.selections?.length) {
75
+ const selections = this.expandSelections(transformed.selections);
76
+ if (selections !== transformed.selections) {
77
+ return { ...transformed, selections };
78
+ }
79
+ }
80
+ return transformed;
68
81
  }
69
82
  finally {
70
83
  // Pop the context when exiting this query scope
@@ -365,32 +378,38 @@ export class MikroTransformer extends OperationNodeTransformer {
365
378
  return node;
366
379
  }
367
380
  const columnProps = this.mapColumnsToProperties(node.columns, meta);
368
- const shouldConvert = this.shouldConvertValues();
381
+ const fieldNames = node.columns.map(c => this.normalizeColumnName(c.column));
382
+ // hasConvertToDatabaseValueSQL is set by MetadataDiscovery only when the SQL is non-trivial
383
+ // (i.e. it actually wraps `?`), so a no-op cast on sqlite won't force a row upgrade.
384
+ const needsSqlWrap = columnProps.some(p => p?.hasConvertToDatabaseValueSQL);
369
385
  let changed = false;
370
386
  const convertedRows = node.values.values.map(row => {
371
- if (ValueListNode.is(row)) {
372
- if (row.values.length !== columnProps.length) {
373
- return row;
374
- }
387
+ if (ValueListNode.is(row) && row.values.length === columnProps.length) {
375
388
  const values = row.values.map((valueNode, idx) => {
376
389
  if (!ValueNode.is(valueNode)) {
377
390
  return valueNode;
378
391
  }
379
- const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
380
- if (converted === valueNode.value) {
381
- return valueNode;
392
+ const newNode = this.processInputValueNode(columnProps[idx], fieldNames[idx], valueNode);
393
+ if (newNode !== valueNode) {
394
+ changed = true;
382
395
  }
383
- changed = true;
384
- return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
396
+ return newNode;
385
397
  });
386
398
  return ValueListNode.create(values);
387
399
  }
388
- if (PrimitiveValueListNode.is(row)) {
389
- if (row.values.length !== columnProps.length) {
390
- return row;
400
+ if (PrimitiveValueListNode.is(row) && row.values.length === columnProps.length) {
401
+ // upgrade to ValueListNode when any column needs SQL-side wrapping, since
402
+ // PrimitiveValueListNode can only hold primitives
403
+ if (needsSqlWrap) {
404
+ changed = true;
405
+ return ValueListNode.create(row.values.map((value, idx) => {
406
+ const prop = columnProps[idx];
407
+ const converted = this.prepareInputValue(prop, value, true);
408
+ return this.wrapWrite(prop, fieldNames[idx], ValueNode.create(converted));
409
+ }));
391
410
  }
392
411
  const values = row.values.map((value, idx) => {
393
- const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert);
412
+ const converted = this.prepareInputValue(columnProps[idx], value, true);
394
413
  if (converted !== value) {
395
414
  changed = true;
396
415
  }
@@ -412,7 +431,6 @@ export class MikroTransformer extends OperationNodeTransformer {
412
431
  if (!node.updates?.length) {
413
432
  return node;
414
433
  }
415
- const shouldConvert = this.shouldConvertValues();
416
434
  let changed = false;
417
435
  const updates = node.updates.map(updateNode => {
418
436
  if (!ValueNode.is(updateNode.value)) {
@@ -422,18 +440,12 @@ export class MikroTransformer extends OperationNodeTransformer {
422
440
  ? this.normalizeColumnName(updateNode.column.column)
423
441
  : undefined;
424
442
  const property = this.findProperty(meta, columnName);
425
- const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
426
- if (converted === updateNode.value.value) {
443
+ const newValue = this.processInputValueNode(property, columnName, updateNode.value);
444
+ if (newValue === updateNode.value) {
427
445
  return updateNode;
428
446
  }
429
447
  changed = true;
430
- const newValueNode = updateNode.value.immediate
431
- ? ValueNode.createImmediate(converted)
432
- : ValueNode.create(converted);
433
- return {
434
- ...updateNode,
435
- value: newValueNode,
436
- };
448
+ return { ...updateNode, value: newValue };
437
449
  });
438
450
  if (!changed) {
439
451
  return node;
@@ -443,6 +455,106 @@ export class MikroTransformer extends OperationNodeTransformer {
443
455
  updates,
444
456
  };
445
457
  }
458
+ processInputValueNode(prop, fieldName, valueNode) {
459
+ const converted = this.prepareInputValue(prop, valueNode.value, true);
460
+ const newValueNode = converted === valueNode.value
461
+ ? valueNode
462
+ : valueNode.immediate
463
+ ? ValueNode.createImmediate(converted)
464
+ : ValueNode.create(converted);
465
+ return this.wrapWrite(prop, fieldName, newValueNode);
466
+ }
467
+ expandSelections(selections) {
468
+ const out = [];
469
+ let changed = false;
470
+ for (const sel of selections) {
471
+ const replaced = this.expandSelection(sel);
472
+ if (replaced) {
473
+ out.push(...replaced);
474
+ changed = true;
475
+ }
476
+ else {
477
+ out.push(sel);
478
+ }
479
+ }
480
+ return changed ? out : selections;
481
+ }
482
+ expandSelection(sel) {
483
+ const inner = sel.selection;
484
+ if (SelectAllNode.is(inner)) {
485
+ return this.expandStar(this.findOwnerMeta(undefined), undefined, undefined);
486
+ }
487
+ if (!ReferenceNode.is(inner)) {
488
+ return null;
489
+ }
490
+ const table = inner.table;
491
+ const tableName = table ? this.getTableName(table) : undefined;
492
+ const meta = this.findOwnerMeta(tableName);
493
+ if (!meta) {
494
+ return null;
495
+ }
496
+ if (SelectAllNode.is(inner.column)) {
497
+ return this.expandStar(meta, table, tableName);
498
+ }
499
+ const fieldName = inner.column.column.name;
500
+ const prop = this.findProperty(meta, fieldName);
501
+ const ct = prop && this.fieldType(prop, fieldName);
502
+ return ct?.convertToJSValueSQL ? [this.wrapRead(ct, fieldName, tableName)] : null;
503
+ }
504
+ expandStar(meta, table, tableName) {
505
+ if (!meta || !meta.props.some(p => p.hasConvertToJSValueSQL)) {
506
+ return null;
507
+ }
508
+ const out = [];
509
+ for (const prop of meta.props) {
510
+ if (prop.persist === false || !prop.fieldNames?.length || !EXPANDABLE_KINDS.has(prop.kind)) {
511
+ continue;
512
+ }
513
+ for (const fieldName of prop.fieldNames) {
514
+ const ct = this.fieldType(prop, fieldName);
515
+ out.push(ct?.convertToJSValueSQL
516
+ ? this.wrapRead(ct, fieldName, tableName)
517
+ : SelectionNode.create(table ? ReferenceNode.create(ColumnNode.create(fieldName), table) : ColumnNode.create(fieldName)));
518
+ }
519
+ }
520
+ return out;
521
+ }
522
+ wrapRead(customType, fieldName, tableName) {
523
+ const key = this.#platform.quoteIdentifier(tableName ? `${tableName}.${fieldName}` : fieldName);
524
+ const sql = customType.convertToJSValueSQL(key, this.#platform);
525
+ return SelectionNode.create(AliasNode.create(RawNode.createWithSql(sql), IdentifierNode.create(fieldName)));
526
+ }
527
+ wrapWrite(prop, fieldName, valueNode) {
528
+ if (!prop?.hasConvertToDatabaseValueSQL || !fieldName || valueNode.value == null || isRaw(valueNode.value)) {
529
+ return valueNode;
530
+ }
531
+ const customType = this.fieldType(prop, fieldName);
532
+ if (!customType?.convertToDatabaseValueSQL) {
533
+ return valueNode;
534
+ }
535
+ const fragments = customType.convertToDatabaseValueSQL('?', this.#platform).split('?');
536
+ return RawNode.create(fragments, fragments.slice(0, -1).map(() => valueNode));
537
+ }
538
+ /** Resolve the customType for a specific field name (handles composite-PK FK fan-out via prop.customTypes[]). */
539
+ fieldType(prop, fieldName) {
540
+ return prop.customType ?? prop.customTypes?.[prop.fieldNames.indexOf(fieldName)];
541
+ }
542
+ findOwnerMeta(name) {
543
+ if (name) {
544
+ return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
545
+ }
546
+ let single;
547
+ for (const meta of this.#contextStack[this.#contextStack.length - 1].values()) {
548
+ if (!meta) {
549
+ continue;
550
+ }
551
+ if (single && single !== meta) {
552
+ return undefined;
553
+ }
554
+ single = meta;
555
+ }
556
+ return single;
557
+ }
446
558
  mapColumnsToProperties(columns, meta) {
447
559
  return columns.map(column => {
448
560
  const columnName = this.normalizeColumnName(column.column);
@@ -466,9 +578,6 @@ export class MikroTransformer extends OperationNodeTransformer {
466
578
  }
467
579
  return meta.props.find(prop => prop.fieldNames?.includes(columnName));
468
580
  }
469
- shouldConvertValues() {
470
- return !!this.#options.convertValues;
471
- }
472
581
  prepareInputValue(prop, value, enabled) {
473
582
  if (!enabled || !prop || value == null) {
474
583
  return value;
@@ -1347,6 +1347,7 @@ export class QueryBuilder {
1347
1347
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
1348
1348
  });
1349
1349
  const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
1350
+ const joinCountBefore = Object.keys(this.#state.joins).length;
1350
1351
  cond = criteriaNode.process(this, { ignoreBranching: true, alias });
1351
1352
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
1352
1353
  path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
@@ -1376,6 +1377,20 @@ export class QueryBuilder {
1376
1377
  this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1377
1378
  this.#state.joins[aliasedName].path ??= path;
1378
1379
  }
1380
+ // auto-joins added by cond processing that depend on the new alias would otherwise produce a
1381
+ // forward reference (the auto-join's ON refers to alias, while alias's ON refers back to it);
1382
+ // fold them into the new join so both aliases share scope in the outer ON clause (issue #7681)
1383
+ const condJoin = this.#state.joins[aliasedName];
1384
+ const joinKeys = Object.keys(this.#state.joins);
1385
+ for (let i = joinCountBefore; i < joinKeys.length; i++) {
1386
+ const j = this.#state.joins[joinKeys[i]];
1387
+ if (j === condJoin || j.ownerAlias !== alias) {
1388
+ continue;
1389
+ }
1390
+ const nested = (condJoin.nested ??= new Set());
1391
+ j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
1392
+ nested.add(j);
1393
+ }
1379
1394
  return { prop, key: aliasedName };
1380
1395
  }
1381
1396
  prepareFields(fields, type = 'where', schema) {