@mikro-orm/sql 7.1.13-dev.9 → 7.1.14-dev.0

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.
@@ -81,7 +81,9 @@ export class SqlEntityManager extends EntityManager {
81
81
  const { where: rawWhere, ...countOptions } = options;
82
82
  await em.tryFlush(entityName, options);
83
83
  const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
84
- const qb = em.createQueryBuilder(meta.class);
84
+ // match `em.count()` semantics: an active transaction always wins over the requested connection type
85
+ const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
86
+ const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
85
87
  qb
86
88
  .select([...fields, raw('count(*) as cnt')])
87
89
  .where(where)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.13-dev.9",
3
+ "version": "7.1.14-dev.0",
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",
@@ -50,10 +50,10 @@
50
50
  "kysely": "0.29.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.12"
53
+ "@mikro-orm/core": "^7.1.13"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.13-dev.9"
56
+ "@mikro-orm/core": "7.1.14-dev.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -1,5 +1,5 @@
1
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';
2
+ import { type BinaryOperationNode, 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,6 +28,12 @@ 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
+ transformBinaryOperation(node: BinaryOperationNode, queryId: QueryId): BinaryOperationNode;
32
+ /** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
33
+ resolveOperandProperty(operand: OperationNode): {
34
+ prop: EntityProperty;
35
+ fieldName: string;
36
+ } | undefined;
31
37
  processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
32
38
  expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
33
39
  expandSelection(sel: SelectionNode): SelectionNode[] | null;
@@ -455,6 +455,60 @@ export class MikroTransformer extends OperationNodeTransformer {
455
455
  updates,
456
456
  };
457
457
  }
458
+ transformBinaryOperation(node, queryId) {
459
+ const transformed = super.transformBinaryOperation(node, queryId);
460
+ if (!this.#options.convertValues) {
461
+ return transformed;
462
+ }
463
+ const resolved = this.resolveOperandProperty(transformed.leftOperand);
464
+ if (!resolved) {
465
+ return transformed;
466
+ }
467
+ const { prop, fieldName } = resolved;
468
+ const right = transformed.rightOperand;
469
+ if (ValueNode.is(right)) {
470
+ const converted = this.processInputValueNode(prop, fieldName, right);
471
+ return converted === right ? transformed : { ...transformed, rightOperand: converted };
472
+ }
473
+ if (PrimitiveValueListNode.is(right)) {
474
+ // upgrade to ValueListNode when the type needs SQL-side wrapping, since
475
+ // PrimitiveValueListNode can only hold primitives
476
+ if (prop.hasConvertToDatabaseValueSQL) {
477
+ const values = right.values.map(value => this.processInputValueNode(prop, fieldName, ValueNode.create(value)));
478
+ return { ...transformed, rightOperand: ValueListNode.create(values) };
479
+ }
480
+ const values = right.values.map(value => this.prepareInputValue(prop, value, true));
481
+ return values.every((value, idx) => value === right.values[idx])
482
+ ? transformed
483
+ : { ...transformed, rightOperand: PrimitiveValueListNode.create(values) };
484
+ }
485
+ if (ValueListNode.is(right)) {
486
+ let changed = false;
487
+ const values = right.values.map(valueNode => {
488
+ if (!ValueNode.is(valueNode)) {
489
+ return valueNode;
490
+ }
491
+ const converted = this.processInputValueNode(prop, fieldName, valueNode);
492
+ if (converted !== valueNode) {
493
+ changed = true;
494
+ }
495
+ return converted;
496
+ });
497
+ return changed ? { ...transformed, rightOperand: ValueListNode.create(values) } : transformed;
498
+ }
499
+ return transformed;
500
+ }
501
+ /** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
502
+ resolveOperandProperty(operand) {
503
+ if (!ReferenceNode.is(operand) || !ColumnNode.is(operand.column)) {
504
+ return undefined;
505
+ }
506
+ const tableName = operand.table ? this.getTableName(operand.table) : undefined;
507
+ const meta = this.findOwnerMeta(tableName);
508
+ const fieldName = this.normalizeColumnName(operand.column.column);
509
+ const prop = this.findProperty(meta, fieldName);
510
+ return prop ? { prop, fieldName } : undefined;
511
+ }
458
512
  processInputValueNode(prop, fieldName, valueNode) {
459
513
  const converted = this.prepareInputValue(prop, valueNode.value, true);
460
514
  const newValueNode = converted === valueNode.value
@@ -543,8 +597,13 @@ export class MikroTransformer extends OperationNodeTransformer {
543
597
  if (name) {
544
598
  return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
545
599
  }
600
+ // the stack can be empty when transforming a raw root node with embedded expressions
601
+ const context = this.#contextStack[this.#contextStack.length - 1];
602
+ if (!context) {
603
+ return undefined;
604
+ }
546
605
  let single;
547
- for (const meta of this.#contextStack[this.#contextStack.length - 1].values()) {
606
+ for (const meta of context.values()) {
548
607
  if (!meta) {
549
608
  continue;
550
609
  }
@@ -1001,6 +1001,13 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
1001
1001
  * would flatten them into `and` conjuncts.
1002
1002
  */
1003
1003
  private canDistributeOrBranches;
1004
+ private getOrBranchAliases;
1005
+ /**
1006
+ * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
1007
+ * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
1008
+ * can reference their aliases.
1009
+ */
1010
+ private mergeFilterOrCondition;
1004
1011
  /**
1005
1012
  * When adding an inner join on a left joined relation, we need to nest them,
1006
1013
  * otherwise the inner join could discard rows of the root table.
@@ -2038,8 +2038,11 @@ export class QueryBuilder {
2038
2038
  for (const k of Object.keys(cond)) {
2039
2039
  if (Utils.isOperator(k)) {
2040
2040
  if (Array.isArray(cond[k])) {
2041
- // a partial `$or` on a join drops rows matching a sibling branch, but entity filters have no other sink, so they must pass
2042
- if (k === '$or' && !filter && !this.canDistributeOrBranches(cond[k], joins)) {
2041
+ if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
2042
+ // entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
2043
+ if (filter) {
2044
+ this.mergeFilterOrCondition(cond[k], joins);
2045
+ }
2043
2046
  continue;
2044
2047
  }
2045
2048
  cond[k].forEach((c) => this.mergeOnConditions(joins, c, filter, k));
@@ -2082,6 +2085,12 @@ export class QueryBuilder {
2082
2085
  * would flatten them into `and` conjuncts.
2083
2086
  */
2084
2087
  canDistributeOrBranches(branches, joins) {
2088
+ const aliases = this.getOrBranchAliases(branches);
2089
+ const targeted = joins.filter(j => aliases.has(j.alias));
2090
+ const flat = branches.every(branch => Object.keys(branch).every(k => !Utils.isOperator(k)));
2091
+ return aliases.size === 1 && targeted.length === 1 && flat;
2092
+ }
2093
+ getOrBranchAliases(branches) {
2085
2094
  const aliases = new Set();
2086
2095
  const collectAliases = (cond) => {
2087
2096
  for (const k of Object.keys(cond)) {
@@ -2094,9 +2103,37 @@ export class QueryBuilder {
2094
2103
  }
2095
2104
  };
2096
2105
  branches.forEach(collectAliases);
2097
- const targeted = joins.filter(j => aliases.has(j.alias));
2098
- const flat = branches.every(branch => Object.keys(branch).every(k => !Utils.isOperator(k)));
2099
- return aliases.size === 1 && targeted.length === 1 && flat;
2106
+ return aliases;
2107
+ }
2108
+ /**
2109
+ * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
2110
+ * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
2111
+ * can reference their aliases.
2112
+ */
2113
+ mergeFilterOrCondition(branches, joins) {
2114
+ const aliases = this.getOrBranchAliases(branches);
2115
+ const chainOf = (join) => {
2116
+ const parent = joins.find(j => j.alias === join.ownerAlias);
2117
+ return parent ? [join, ...chainOf(parent)] : [join];
2118
+ };
2119
+ // chains go from each targeted join up to its root, so the first join present in all of them is the outermost common one
2120
+ const chains = joins.filter(j => aliases.has(j.alias)).map(chainOf);
2121
+ const anchor = chains[0]?.find(a => chains.every(chain => chain.includes(a)));
2122
+ /* v8 ignore next 3 */
2123
+ if (!anchor) {
2124
+ return;
2125
+ }
2126
+ for (const chain of chains) {
2127
+ // nest the chain below the anchor, so the anchor's `on` clause can reference the nested aliases
2128
+ for (let i = 0; chain[i] !== anchor; i++) {
2129
+ const nested = (chain[i + 1].nested ??= new Set());
2130
+ if (!nested.has(chain[i])) {
2131
+ chain[i].type = chain[i].type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
2132
+ nested.add(chain[i]);
2133
+ }
2134
+ }
2135
+ }
2136
+ anchor.cond = anchor.cond.$or ? { $and: [anchor.cond, { $or: branches }] } : { ...anchor.cond, $or: branches };
2100
2137
  }
2101
2138
  /**
2102
2139
  * When adding an inner join on a left joined relation, we need to nest them,