var _a;
import {
  EntityMetadata,
  helper,
  inspect,
  isRaw,
  LoadStrategy,
  LockMode,
  PopulateHint,
  QueryFlag,
  QueryHelper,
  raw,
  RawQueryFragment,
  Reference,
  ReferenceKind,
  serialize,
  Utils,
  ValidationError,
} from '@mikro-orm/core';
import { JoinType, QueryType } from './enums.js';
import { QueryBuilderHelper } from './QueryBuilderHelper.js';
import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
import { NativeQueryBuilder } from './NativeQueryBuilder.js';
/** Matches 'path as alias' — safe because ORM property names are JS identifiers (no spaces). */
const FIELD_ALIAS_RE = /^(.+?)\s+as\s+(\w+)$/i;
/**
 * SQL query builder with fluent interface.
 *
 * ```ts
 * const qb = orm.em.createQueryBuilder(Publisher);
 * qb.select('*')
 *   .where({
 *     name: 'test 123',
 *     type: PublisherType.GLOBAL,
 *   })
 *   .orderBy({
 *     name: QueryOrder.DESC,
 *     type: QueryOrder.ASC,
 *   })
 *   .limit(2, 1);
 *
 * const publisher = await qb.getSingleResult();
 * ```
 */
export class QueryBuilder {
  metadata;
  driver;
  context;
  connectionType;
  em;
  loggerContext;
  #state = _a.createDefaultState();
  #helper;
  #query;
  /** @internal */
  static createDefaultState() {
    return {
      aliasCounter: 0,
      explicitAlias: false,
      populateHintFinalized: false,
      joins: {},
      cond: {},
      orderBy: [],
      groupBy: [],
      having: {},
      comments: [],
      hintComments: [],
      subQueries: {},
      aliases: {},
      tptAlias: {},
      ctes: [],
      tptJoinsApplied: false,
      autoJoinedPaths: [],
      populate: [],
      populateMap: {},
      flags: new Set([QueryFlag.CONVERT_CUSTOM_TYPES]),
      finalized: false,
      joinedProps: new Map(),
    };
  }
  get mainAlias() {
    this.ensureFromClause();
    return this.#state.mainAlias;
  }
  get alias() {
    return this.mainAlias.aliasName;
  }
  get helper() {
    this.ensureFromClause();
    return this.#helper;
  }
  get type() {
    return this.#state.type ?? QueryType.SELECT;
  }
  /** @internal */
  get state() {
    return this.#state;
  }
  platform;
  /**
   * @internal
   */
  constructor(entityName, metadata, driver, context, alias, connectionType, em, loggerContext) {
    this.metadata = metadata;
    this.driver = driver;
    this.context = context;
    this.connectionType = connectionType;
    this.em = em;
    this.loggerContext = loggerContext;
    this.platform = this.driver.getPlatform();
    if (alias) {
      this.#state.aliasCounter++;
      this.#state.explicitAlias = true;
    }
    // @ts-expect-error union type does not match the overloaded method signature
    this.from(entityName, alias);
  }
  select(fields, distinct = false) {
    this.ensureNotFinalized();
    this.#state.fields = Utils.asArray(fields).flatMap(f => {
      if (typeof f !== 'string') {
        // Normalize sql.ref('prop') and sql.ref('prop').as('alias') to string form
        if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
          return this.resolveNestedPath(String(f.params[0]));
        }
        if (isRaw(f) && f.sql === '?? as ??' && f.params.length === 2) {
          return `${this.resolveNestedPath(String(f.params[0]))} as ${String(f.params[1])}`;
        }
        return f;
      }
      const asMatch = FIELD_ALIAS_RE.exec(f);
      if (asMatch) {
        return `${this.resolveNestedPath(asMatch[1].trim())} as ${asMatch[2]}`;
      }
      return this.resolveNestedPath(f);
    });
    if (distinct) {
      this.#state.flags.add(QueryFlag.DISTINCT);
    }
    return this.init(QueryType.SELECT);
  }
  /**
   * Adds fields to an existing SELECT query.
   */
  addSelect(fields) {
    this.ensureNotFinalized();
    if (this.#state.type && this.#state.type !== QueryType.SELECT) {
      return this;
    }
    return this.select([...Utils.asArray(this.#state.fields), ...Utils.asArray(fields)]);
  }
  distinct() {
    this.ensureNotFinalized();
    return this.setFlag(QueryFlag.DISTINCT);
  }
  distinctOn(fields) {
    this.ensureNotFinalized();
    this.#state.distinctOn = Utils.asArray(fields);
    return this;
  }
  /**
   * Creates an INSERT query with the given data.
   *
   * @example
   * ```ts
   * await em.createQueryBuilder(User)
   *   .insert({ name: 'John', email: 'john@example.com' })
   *   .execute();
   *
   * // Bulk insert
   * await em.createQueryBuilder(User)
   *   .insert([{ name: 'John' }, { name: 'Jane' }])
   *   .execute();
   * ```
   */
  insert(data) {
    return this.init(QueryType.INSERT, data);
  }
  /**
   * Creates an UPDATE query with the given data.
   * Use `where()` to specify which rows to update.
   *
   * @example
   * ```ts
   * await em.createQueryBuilder(User)
   *   .update({ name: 'John Doe' })
   *   .where({ id: 1 })
   *   .execute();
   * ```
   */
  update(data) {
    return this.init(QueryType.UPDATE, data);
  }
  /**
   * Creates a DELETE query.
   * Use `where()` to specify which rows to delete.
   *
   * @example
   * ```ts
   * await em.createQueryBuilder(User)
   *   .delete()
   *   .where({ id: 1 })
   *   .execute();
   *
   * // Or pass the condition directly
   * await em.createQueryBuilder(User)
   *   .delete({ isActive: false })
   *   .execute();
   * ```
   */
  delete(cond) {
    return this.init(QueryType.DELETE, undefined, cond);
  }
  /**
   * Creates a TRUNCATE query to remove all rows from the table.
   */
  truncate() {
    return this.init(QueryType.TRUNCATE);
  }
  /**
   * Creates a COUNT query to count matching rows.
   *
   * @example
   * ```ts
   * const count = await em.createQueryBuilder(User)
   *   .count()
   *   .where({ isActive: true })
   *   .execute('get');
   * ```
   */
  count(field, distinct = false) {
    if (field) {
      this.#state.fields = Utils.asArray(field);
    } else if (distinct || this.hasToManyJoins()) {
      this.#state.fields = this.mainAlias.meta.primaryKeys;
    } else {
      this.#state.fields = [raw('*')];
    }
    if (distinct) {
      this.#state.flags.add(QueryFlag.DISTINCT);
    }
    return this.init(QueryType.COUNT);
  }
  join(field, alias, cond = {}, type = JoinType.innerJoin, path, schema) {
    this.joinReference(field, alias, cond, type, path, schema);
    return this;
  }
  innerJoin(field, alias, cond = {}, schema) {
    this.join(field, alias, cond, JoinType.innerJoin, undefined, schema);
    return this;
  }
  innerJoinLateral(field, alias, cond = {}, schema) {
    return this.join(field, alias, cond, JoinType.innerJoinLateral, undefined, schema);
  }
  leftJoin(field, alias, cond = {}, schema) {
    return this.join(field, alias, cond, JoinType.leftJoin, undefined, schema);
  }
  leftJoinLateral(field, alias, cond = {}, schema) {
    return this.join(field, alias, cond, JoinType.leftJoinLateral, undefined, schema);
  }
  /**
   * Adds a JOIN clause and automatically selects the joined entity's fields.
   * This is useful for eager loading related entities.
   *
   * @example
   * ```ts
   * const qb = em.createQueryBuilder(Book, 'b');
   * qb.select('*')
   *   .joinAndSelect('b.author', 'a')
   *   .where({ 'a.name': 'John' });
   * ```
   */
  joinAndSelect(field, alias, cond = {}, type = JoinType.innerJoin, path, fields, schema) {
    if (!this.#state.type) {
      this.select('*');
    }
    let subquery;
    if (Array.isArray(field)) {
      const rawFragment = field[1] instanceof _a ? field[1].toRaw() : field[1];
      subquery = this.platform.formatQuery(rawFragment.sql, rawFragment.params);
      field = field[0];
    }
    const { prop, key } = this.joinReference(field, alias, cond, type, path, schema, subquery);
    const [fromAlias] = this.helper.splitField(field);
    if (subquery) {
      this.#state.joins[key].subquery = subquery;
    }
    const populate = this.#state.joinedProps.get(fromAlias);
    const item = { field: prop.name, strategy: LoadStrategy.JOINED, children: [] };
    if (populate) {
      populate.children.push(item);
    } else {
      // root entity
      this.#state.populate.push(item);
    }
    this.#state.joinedProps.set(alias, item);
    this.addSelect(this.getFieldsForJoinedLoad(prop, alias, fields));
    return this;
  }
  leftJoinAndSelect(field, alias, cond = {}, fields, schema) {
    return this.joinAndSelect(field, alias, cond, JoinType.leftJoin, undefined, fields, schema);
  }
  leftJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
    this.joinAndSelect(field, alias, cond, JoinType.leftJoinLateral, undefined, fields, schema);
    return this;
  }
  innerJoinAndSelect(field, alias, cond = {}, fields, schema) {
    return this.joinAndSelect(field, alias, cond, JoinType.innerJoin, undefined, fields, schema);
  }
  innerJoinLateralAndSelect(field, alias, cond = {}, fields, schema) {
    this.joinAndSelect(field, alias, cond, JoinType.innerJoinLateral, undefined, fields, schema);
    return this;
  }
  getFieldsForJoinedLoad(prop, alias, explicitFields) {
    const fields = [];
    const populate = [];
    const joinKey = Object.keys(this.#state.joins).find(join => join.endsWith(`#${alias}`));
    const targetMeta = prop.targetMeta;
    const schema = this.#state.schema ?? (targetMeta.schema !== '*' ? targetMeta.schema : undefined);
    if (joinKey) {
      const path = this.#state.joins[joinKey].path.split('.').slice(1);
      let children = this.#state.populate;
      for (let i = 0; i < path.length; i++) {
        const child = children.filter(hint => {
          const [propName] = hint.field.split(':', 2);
          return propName === path[i];
        });
        children = child.flatMap(c => c.children);
      }
      populate.push(...children);
    }
    for (const p of targetMeta.getPrimaryProps()) {
      fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
    }
    if (explicitFields && explicitFields.length > 0) {
      for (const field of explicitFields) {
        const [a, f] = this.helper.splitField(field);
        const p = targetMeta.properties[f];
        if (p) {
          fields.push(...this.driver.mapPropToFieldNames(this, p, alias, targetMeta, schema));
        } else {
          fields.push(`${a}.${f} as ${a}__${f}`);
        }
      }
    }
    targetMeta.props
      .filter(prop => {
        if (!explicitFields || explicitFields.length === 0) {
          return this.platform.shouldHaveColumn(prop, populate);
        }
        return prop.primary && !explicitFields.includes(prop.name) && !explicitFields.includes(`${alias}.${prop.name}`);
      })
      .forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias, targetMeta, schema)));
    return fields;
  }
  /**
   * Apply filters to the QB where condition.
   */
  async applyFilters(filterOptions = {}) {
    /* v8 ignore next */
    if (!this.em) {
      throw new Error('Cannot apply filters, this QueryBuilder is not attached to an EntityManager');
    }
    const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
    this.andWhere(cond);
  }
  /**
   * @internal
   */
  scheduleFilterCheck(path) {
    this.#state.autoJoinedPaths.push(path);
  }
  /**
   * @internal
   */
  async applyJoinedFilters(em, filterOptions) {
    for (const path of this.#state.autoJoinedPaths) {
      const join = this.getJoinForPath(path);
      if (join.type === JoinType.pivotJoin) {
        continue;
      }
      filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
      let cond = await em.applyFilters(join.prop.targetMeta.class, join.cond, filterOptions, 'read');
      const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, join.prop.targetMeta.class, cond);
      cond = criteriaNode.process(this, {
        matchPopulateJoins: true,
        filter: true,
        alias: join.alias,
        ignoreBranching: true,
        parentPath: join.path,
      });
      if (Utils.hasObjectKeys(cond) || RawQueryFragment.hasObjectFragments(cond)) {
        // remove nested filters, we only care about scalars here, nesting would require another join branch
        for (const key of Object.keys(cond)) {
          if (
            Utils.isPlainObject(cond[key]) &&
            Object.keys(cond[key]).every(
              k => !(Utils.isOperator(k) && !['$some', '$none', '$every', '$size'].includes(k)),
            )
          ) {
            delete cond[key];
          }
        }
        if (Utils.hasObjectKeys(join.cond) || RawQueryFragment.hasObjectFragments(join.cond)) {
          /* v8 ignore next */
          join.cond = { $and: [join.cond, cond] };
        } else {
          join.cond = { ...cond };
        }
      }
    }
  }
  withSubQuery(subQuery, alias) {
    this.ensureNotFinalized();
    if (isRaw(subQuery)) {
      this.#state.subQueries[alias] = this.platform.formatQuery(subQuery.sql, subQuery.params);
    } else {
      this.#state.subQueries[alias] = subQuery.toString();
    }
    return this;
  }
  where(cond, params, operator) {
    this.ensureNotFinalized();
    let processedCond;
    if (isRaw(cond)) {
      const sql = this.platform.formatQuery(cond.sql, cond.params);
      processedCond = { [raw(`(${sql})`)]: Utils.asArray(params) };
      operator ??= '$and';
    } else if (typeof cond === 'string') {
      processedCond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
      operator ??= '$and';
    } else {
      processedCond = QueryHelper.processWhere({
        where: cond,
        entityName: this.mainAlias.entityName,
        metadata: this.metadata,
        platform: this.platform,
        aliasMap: this.getAliasMap(),
        aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
        convertCustomTypes: this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES),
      });
    }
    const op = operator || params;
    const topLevel =
      !op || !(Utils.hasObjectKeys(this.#state.cond) || RawQueryFragment.hasObjectFragments(this.#state.cond));
    const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processedCond);
    const ignoreBranching = this.#state.resolvedPopulateWhere === 'infer';
    if (
      [QueryType.UPDATE, QueryType.DELETE].includes(this.type) &&
      criteriaNode.willAutoJoin(this, undefined, { ignoreBranching })
    ) {
      // use sub-query to support joining
      this.setFlag(this.type === QueryType.UPDATE ? QueryFlag.UPDATE_SUB_QUERY : QueryFlag.DELETE_SUB_QUERY);
      this.select(this.mainAlias.meta.primaryKeys, true);
    }
    if (topLevel) {
      this.#state.cond = criteriaNode.process(this, { ignoreBranching });
    } else if (Array.isArray(this.#state.cond[op])) {
      this.#state.cond[op].push(criteriaNode.process(this, { ignoreBranching }));
    } else {
      const cond1 = [this.#state.cond, criteriaNode.process(this, { ignoreBranching })];
      this.#state.cond = { [op]: cond1 };
    }
    if (this.#state.onConflict) {
      this.#state.onConflict[this.#state.onConflict.length - 1].where = this.helper.processOnConflictCondition(
        this.#state.cond,
        this.#state.schema,
      );
      this.#state.cond = {};
    }
    return this;
  }
  andWhere(cond, params) {
    return this.where(cond, params, '$and');
  }
  orWhere(cond, params) {
    return this.where(cond, params, '$or');
  }
  orderBy(orderBy) {
    return this.processOrderBy(orderBy, true);
  }
  andOrderBy(orderBy) {
    return this.processOrderBy(orderBy, false);
  }
  processOrderBy(orderBy, reset = true) {
    this.ensureNotFinalized();
    if (reset) {
      this.#state.orderBy = [];
    }
    const selectAliases = this.getSelectAliases();
    Utils.asArray(orderBy).forEach(orig => {
      // Shallow clone to avoid mutating the caller's object — safe because the clone
      // is only used within this loop iteration and `orig` is not referenced afterward.
      const o = { ...orig };
      // Wrap known select aliases in raw() so they bypass property validation and alias prefixing
      for (const key of Object.keys(o)) {
        if (selectAliases.has(key)) {
          o[raw('??', [key])] = o[key];
          delete o[key];
        }
      }
      this.helper.validateQueryOrder(o);
      const processed = QueryHelper.processWhere({
        where: o,
        entityName: this.mainAlias.entityName,
        metadata: this.metadata,
        platform: this.platform,
        aliasMap: this.getAliasMap(),
        aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
        convertCustomTypes: false,
        type: 'orderBy',
      });
      this.#state.orderBy.push(
        CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, {
          matchPopulateJoins: true,
          type: 'orderBy',
        }),
      );
    });
    return this;
  }
  /** Collect custom aliases from select fields (stored as 'resolved as alias' strings by select()). */
  getSelectAliases() {
    const aliases = new Set();
    for (const field of this.#state.fields ?? []) {
      if (typeof field === 'string') {
        const m = FIELD_ALIAS_RE.exec(field);
        if (m) {
          aliases.add(m[2]);
        }
      }
    }
    return aliases;
  }
  groupBy(fields) {
    this.ensureNotFinalized();
    this.#state.groupBy = Utils.asArray(fields).flatMap(f => {
      if (typeof f !== 'string') {
        // Normalize sql.ref('prop') to string for proper formula resolution
        if (isRaw(f) && f.sql === '??' && f.params.length === 1) {
          return this.resolveNestedPath(String(f.params[0]));
        }
        return f;
      }
      return this.resolveNestedPath(f);
    });
    return this;
  }
  /**
   * Adds a HAVING clause to the query, typically used with GROUP BY.
   *
   * @example
   * ```ts
   * qb.select([raw('count(*) as count'), 'status'])
   *   .groupBy('status')
   *   .having({ count: { $gt: 5 } });
   * ```
   */
  having(cond = {}, params, operator) {
    this.ensureNotFinalized();
    if (typeof cond === 'string') {
      cond = { [raw(`(${cond})`, params)]: [] };
    }
    const processed = CriteriaNodeFactory.createNode(
      this.metadata,
      this.mainAlias.entityName,
      cond,
      undefined,
      undefined,
      false,
    ).process(this, { type: 'having' });
    if (!this.#state.having || !operator) {
      this.#state.having = processed;
    } else {
      const cond1 = [this.#state.having, processed];
      this.#state.having = { [operator]: cond1 };
    }
    return this;
  }
  andHaving(cond, params) {
    return this.having(cond, params, '$and');
  }
  orHaving(cond, params) {
    return this.having(cond, params, '$or');
  }
  onConflict(fields = []) {
    const meta = this.mainAlias.meta;
    this.ensureNotFinalized();
    this.#state.onConflict ??= [];
    this.#state.onConflict.push({
      fields: isRaw(fields)
        ? fields
        : Utils.asArray(fields).flatMap(f => {
            const key = f.toString();
            /* v8 ignore next */
            return meta.properties[key]?.fieldNames ?? [key];
          }),
    });
    return this;
  }
  ignore() {
    if (!this.#state.onConflict) {
      throw new Error('You need to call `qb.onConflict()` first to use `qb.ignore()`');
    }
    this.#state.onConflict[this.#state.onConflict.length - 1].ignore = true;
    return this;
  }
  merge(data) {
    if (!this.#state.onConflict) {
      throw new Error('You need to call `qb.onConflict()` first to use `qb.merge()`');
    }
    if (Array.isArray(data) && data.length === 0) {
      return this.ignore();
    }
    this.#state.onConflict[this.#state.onConflict.length - 1].merge = data;
    return this;
  }
  returning(fields) {
    this.#state.returning = Utils.asArray(fields);
    return this;
  }
  /**
   * @internal
   */
  populate(populate, populateWhere, populateFilter) {
    this.ensureNotFinalized();
    this.#state.populate = populate;
    this.#state.populateWhere = populateWhere;
    this.#state.populateFilter = populateFilter;
    return this;
  }
  /**
   * Sets a LIMIT clause to restrict the number of results.
   *
   * @example
   * ```ts
   * qb.select('*').limit(10);        // First 10 results
   * qb.select('*').limit(10, 20);    // 10 results starting from offset 20
   * ```
   */
  limit(limit, offset = 0) {
    this.ensureNotFinalized();
    this.#state.limit = limit;
    if (offset) {
      this.offset(offset);
    }
    return this;
  }
  /**
   * Sets an OFFSET clause to skip a number of results.
   *
   * @example
   * ```ts
   * qb.select('*').limit(10).offset(20);  // Results 21-30
   * ```
   */
  offset(offset) {
    this.ensureNotFinalized();
    this.#state.offset = offset;
    return this;
  }
  withSchema(schema) {
    this.ensureNotFinalized();
    this.#state.schema = schema;
    return this;
  }
  setLockMode(mode, tables) {
    this.ensureNotFinalized();
    if (mode != null && ![LockMode.OPTIMISTIC, LockMode.NONE].includes(mode) && !this.context) {
      throw ValidationError.transactionRequired();
    }
    this.#state.lockMode = mode;
    this.#state.lockTables = tables;
    return this;
  }
  setFlushMode(flushMode) {
    this.ensureNotFinalized();
    this.#state.flushMode = flushMode;
    return this;
  }
  setFlag(flag) {
    this.ensureNotFinalized();
    this.#state.flags.add(flag);
    return this;
  }
  unsetFlag(flag) {
    this.ensureNotFinalized();
    this.#state.flags.delete(flag);
    return this;
  }
  hasFlag(flag) {
    return this.#state.flags.has(flag);
  }
  cache(config = true) {
    this.ensureNotFinalized();
    this.#state.cache = config;
    return this;
  }
  /**
   * Adds index hint to the FROM clause.
   */
  indexHint(sql) {
    this.ensureNotFinalized();
    this.#state.indexHint = sql;
    return this;
  }
  /**
   * Adds COLLATE clause to ORDER BY expressions.
   */
  collation(collation) {
    this.ensureNotFinalized();
    this.#state.collation = collation;
    return this;
  }
  /**
   * Prepend comment to the sql query using the syntax `/* ... *&#8205;/`. Some characters are forbidden such as `/*, *&#8205;/` and `?`.
   */
  comment(comment) {
    this.ensureNotFinalized();
    this.#state.comments.push(...Utils.asArray(comment));
    return this;
  }
  /**
   * Add hints to the query using comment-like syntax `/*+ ... *&#8205;/`. MySQL and Oracle use this syntax for optimizer hints.
   * Also various DB proxies and routers use this syntax to pass hints to alter their behavior. In other dialects the hints
   * are ignored as simple comments.
   */
  hintComment(comment) {
    this.ensureNotFinalized();
    this.#state.hintComments.push(...Utils.asArray(comment));
    return this;
  }
  from(target, aliasName) {
    this.ensureNotFinalized();
    if (target instanceof _a) {
      this.fromSubQuery(target, aliasName);
    } else if (typeof target === 'string' && !this.metadata.find(target)) {
      this.fromRawTable(target, aliasName);
    } else {
      if (aliasName && this.#state.mainAlias && Utils.className(target) !== this.#state.mainAlias.aliasName) {
        throw new Error(
          `Cannot override the alias to '${aliasName}' since a query already contains references to '${this.#state.mainAlias.aliasName}'`,
        );
      }
      this.fromEntityName(target, aliasName);
    }
    return this;
  }
  getNativeQuery(processVirtualEntity = true) {
    if (this.#state.unionQuery) {
      if (!this.#query?.qb) {
        this.#query = {};
        const nqb = this.platform.createNativeQueryBuilder();
        nqb.select('*');
        nqb.from(raw(`(${this.#state.unionQuery.sql})`, this.#state.unionQuery.params));
        this.#query.qb = nqb;
      }
      return this.#query.qb;
    }
    if (this.#query?.qb) {
      return this.#query.qb;
    }
    this.#query = {};
    this.finalize();
    const qb = this.getQueryBase(processVirtualEntity);
    for (const cte of this.#state.ctes) {
      const query = cte.query;
      const opts = { columns: cte.columns, materialized: cte.materialized };
      if (cte.recursive) {
        qb.withRecursive(cte.name, query, opts);
      } else {
        qb.with(cte.name, query, opts);
      }
    }
    const schema = this.getSchema(this.mainAlias);
    const isNotEmptyObject = obj => Utils.hasObjectKeys(obj) || RawQueryFragment.hasObjectFragments(obj);
    Utils.runIfNotEmpty(
      () => this.helper.appendQueryCondition(this.type, this.#state.cond, qb),
      this.#state.cond && !this.#state.onConflict,
    );
    Utils.runIfNotEmpty(
      () => qb.groupBy(this.prepareFields(this.#state.groupBy, 'groupBy', schema)),
      isNotEmptyObject(this.#state.groupBy),
    );
    Utils.runIfNotEmpty(
      () => this.helper.appendQueryCondition(this.type, this.#state.having, qb, undefined, 'having'),
      isNotEmptyObject(this.#state.having),
    );
    Utils.runIfNotEmpty(() => {
      const queryOrder = this.helper.getQueryOrder(
        this.type,
        this.#state.orderBy,
        this.#state.populateMap,
        this.#state.collation,
      );
      if (queryOrder.length > 0) {
        const sql = Utils.unique(queryOrder).join(', ');
        qb.orderBy(sql);
        return;
      }
    }, isNotEmptyObject(this.#state.orderBy));
    Utils.runIfNotEmpty(() => qb.limit(this.#state.limit), this.#state.limit != null);
    Utils.runIfNotEmpty(() => qb.offset(this.#state.offset), this.#state.offset);
    Utils.runIfNotEmpty(() => qb.comment(this.#state.comments), this.#state.comments);
    Utils.runIfNotEmpty(() => qb.hintComment(this.#state.hintComments), this.#state.hintComments);
    Utils.runIfNotEmpty(
      () => this.helper.appendOnConflictClause(QueryType.UPSERT, this.#state.onConflict, qb),
      this.#state.onConflict,
    );
    if (this.#state.lockMode) {
      this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
    }
    this.processReturningStatement(qb, this.mainAlias.meta, this.#state.data, this.#state.returning);
    return (this.#query.qb = qb);
  }
  processReturningStatement(qb, meta, data, returning) {
    const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
    if (!meta || !data || !usesReturningStatement) {
      return;
    }
    // always respect explicit returning hint
    if (returning && returning.length > 0) {
      qb.returning(returning.map(field => this.helper.mapper(field, this.type)));
      return;
    }
    if (this.type === QueryType.INSERT) {
      const returningProps = meta.hydrateProps
        .filter(
          prop =>
            prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)),
        )
        .filter(prop => !(prop.name in data));
      if (returningProps.length > 0) {
        qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
      }
      return;
    }
    if (this.type === QueryType.UPDATE) {
      const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
      if (returningProps.length > 0) {
        qb.returning(
          returningProps.flatMap(prop => {
            if (prop.hasConvertToJSValueSQL) {
              const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
              const sql =
                prop.customType.convertToJSValueSQL(aliased, this.platform) +
                ' as ' +
                this.platform.quoteIdentifier(prop.fieldNames[0]);
              return [raw(sql)];
            }
            return prop.fieldNames;
          }),
        );
      }
    }
  }
  /**
   * Returns the query with parameters as wildcards.
   */
  getQuery() {
    return this.toQuery().sql;
  }
  /**
   * Returns raw fragment representation of this QueryBuilder.
   */
  toRaw() {
    const { sql, params } = this.toQuery();
    return raw(sql, params);
  }
  toQuery() {
    if (this.#state.unionQuery) {
      return this.#state.unionQuery;
    }
    if (this.#query?.sql) {
      return { sql: this.#query.sql, params: this.#query.params };
    }
    const query = this.getNativeQuery().compile();
    this.#query.sql = query.sql;
    this.#query.params = query.params;
    return { sql: this.#query.sql, params: this.#query.params };
  }
  /**
   * Returns the list of all parameters for this query.
   */
  getParams() {
    return this.toQuery().params;
  }
  /**
   * Returns raw interpolated query string with all the parameters inlined.
   */
  getFormattedQuery() {
    const query = this.toQuery();
    return this.platform.formatQuery(query.sql, query.params);
  }
  /**
   * @internal
   */
  getAliasForJoinPath(path, options) {
    if (!path || path === Utils.className(this.mainAlias.entityName)) {
      return this.mainAlias.aliasName;
    }
    const join = typeof path === 'string' ? this.getJoinForPath(path, options) : path;
    if (join?.path?.endsWith('[pivot]')) {
      return join.alias;
    }
    return join?.inverseAlias || join?.alias;
  }
  /**
   * @internal
   */
  getJoinForPath(path, options) {
    const joins = Object.values(this.#state.joins);
    if (joins.length === 0) {
      return undefined;
    }
    let join = joins.find(j => j.path === path);
    if (options?.preferNoBranch) {
      join = joins.find(j => {
        return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '');
      });
    }
    if (!join && options?.ignoreBranching) {
      join = joins.find(j => {
        return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '');
      });
    }
    if (!join && options?.matchPopulateJoins && options?.ignoreBranching) {
      join = joins.find(j => {
        return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '');
      });
    }
    if (!join && options?.matchPopulateJoins) {
      join = joins.find(j => {
        return j.path?.replace(/\[populate]/g, '') === path.replace(/\[populate]/g, '');
      });
    }
    return join;
  }
  /**
   * @internal
   */
  getNextAlias(entityName = 'e') {
    entityName = Utils.className(entityName);
    return this.driver.config.getNamingStrategy().aliasName(entityName, this.#state.aliasCounter++);
  }
  /**
   * Registers a join for a specific polymorphic target type.
   * Used by the driver to create per-target LEFT JOINs for JOINED loading.
   * @internal
   */
  addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
    // Override referencedColumnNames to use the specific target's PK columns
    // (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
    const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
    const targetProp = { ...prop, targetMeta, referencedColumnNames };
    const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
    this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(
      targetProp,
      ownerAlias,
      alias,
      type,
      {},
      schema,
    );
    this.#state.joins[aliasedName].path = path;
    this.createAlias(targetMeta.class, alias);
  }
  /**
   * @internal
   */
  getAliasMap() {
    return Object.fromEntries(Object.entries(this.#state.aliases).map(([key, value]) => [key, value.entityName]));
  }
  /**
   * Executes this QB and returns the raw results, mapped to the property names (unless disabled via last parameter).
   * Use `method` to specify what kind of result you want to get (array/single/meta).
   */
  async execute(method, options) {
    options = typeof options === 'boolean' ? { mapResults: options } : (options ?? {});
    options.mergeResults ??= true;
    options.mapResults ??= true;
    const isRunType = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE, QueryType.TRUNCATE].includes(this.type);
    method ??= isRunType ? 'run' : 'all';
    if (!this.connectionType && (isRunType || this.context)) {
      this.connectionType = 'write';
    }
    if (!this.#state.finalized && method === 'get' && this.type === QueryType.SELECT) {
      this.limit(1);
    }
    const query = this.toQuery();
    const cached = await this.em?.tryCache(this.mainAlias.entityName, this.#state.cache, [
      'qb.execute',
      query.sql,
      query.params,
      method,
    ]);
    if (cached?.data !== undefined) {
      return cached.data;
    }
    const loggerContext = { id: this.em?.id, ...this.loggerContext };
    const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
    const meta = this.mainAlias.meta;
    if (!options.mapResults || !meta) {
      await this.em?.storeCache(this.#state.cache, cached, res);
      return res;
    }
    if (method === 'run') {
      return res;
    }
    const joinedProps = this.driver.joinedProps(meta, this.#state.populate);
    let mapped;
    if (Array.isArray(res)) {
      const map = {};
      mapped = res.map(r => this.driver.mapResult(r, meta, this.#state.populate, this, map));
      if (options.mergeResults && joinedProps.length > 0) {
        mapped = this.driver.mergeJoinedResult(mapped, this.mainAlias.meta, joinedProps);
      }
    } else {
      mapped = [this.driver.mapResult(res, meta, joinedProps, this)];
    }
    if (method === 'get') {
      await this.em?.storeCache(this.#state.cache, cached, mapped[0]);
      return mapped[0];
    }
    await this.em?.storeCache(this.#state.cache, cached, mapped);
    return mapped;
  }
  getConnection() {
    const write = !this.platform.getConfig().get('preferReadReplicas');
    const type = this.connectionType || (write ? 'write' : 'read');
    return this.driver.getConnection(type);
  }
  /**
   * Executes the query and returns an async iterable (async generator) that yields results one by one.
   * By default, the results are merged and mapped to entity instances, without adding them to the identity map.
   * You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`.
   * This is useful for processing large datasets without loading everything into memory at once.
   *
   * ```ts
   * const qb = em.createQueryBuilder(Book, 'b');
   * qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a');
   *
   * for await (const book of qb.stream()) {
   *   // book is an instance of Book entity
   *   console.log(book.title, book.author.name);
   * }
   * ```
   */
  async *stream(options) {
    options ??= {};
    options.mergeResults ??= true;
    options.mapResults ??= true;
    const query = this.toQuery();
    const loggerContext = { id: this.em?.id, ...this.loggerContext };
    const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
    const meta = this.mainAlias.meta;
    if (options.rawResults || !meta) {
      yield* res;
      return;
    }
    const joinedProps = this.driver.joinedProps(meta, this.#state.populate);
    const stack = [];
    const hash = data => {
      return Utils.getPrimaryKeyHash(meta.primaryKeys.map(pk => data[pk]));
    };
    for await (const row of res) {
      const mapped = this.driver.mapResult(row, meta, this.#state.populate, this);
      if (!options.mergeResults || joinedProps.length === 0) {
        yield this.mapResult(mapped, options.mapResults);
        continue;
      }
      if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
        const res = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
        for (const row of res) {
          yield this.mapResult(row, options.mapResults);
        }
        stack.length = 0;
      }
      stack.push(mapped);
    }
    if (stack.length > 0) {
      const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
      yield this.mapResult(merged[0], options.mapResults);
    }
  }
  /**
   * Alias for `qb.getResultList()`
   */
  async getResult() {
    return this.getResultList();
  }
  /**
   * Executes the query, returning array of results mapped to entity instances.
   */
  async getResultList(limit) {
    await this.em.tryFlush(this.mainAlias.entityName, { flushMode: this.#state.flushMode });
    const res = await this.execute('all', true);
    return this.mapResults(res, limit);
  }
  propagatePopulateHint(entity, hint) {
    helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
    hint.forEach(hint => {
      const [propName] = hint.field.split(':', 2);
      const value = Reference.unwrapReference(entity[propName]);
      if (Utils.isEntity(value)) {
        this.propagatePopulateHint(value, hint.children ?? []);
      } else if (Utils.isCollection(value)) {
        value.populated();
        value.getItems(false).forEach(item => this.propagatePopulateHint(item, hint.children ?? []));
      }
    });
  }
  mapResult(row, map = true) {
    if (!map) {
      return row;
    }
    const entity = this.em.map(this.mainAlias.entityName, row, { schema: this.#state.schema });
    this.propagatePopulateHint(entity, this.#state.populate);
    return entity;
  }
  mapResults(res, limit) {
    const entities = [];
    for (const row of res) {
      const entity = this.mapResult(row);
      this.propagatePopulateHint(entity, this.#state.populate);
      entities.push(entity);
      if (limit != null && --limit === 0) {
        break;
      }
    }
    return Utils.unique(entities);
  }
  /**
   * Executes the query, returning the first result or null
   */
  async getSingleResult() {
    if (!this.#state.finalized) {
      this.limit(1);
    }
    const [res] = await this.getResultList(1);
    return res || null;
  }
  async getCount(field, distinct) {
    let res;
    if (this.type === QueryType.COUNT) {
      res = await this.execute('get', false);
    } else {
      const qb = this.#state.type === undefined ? this : this.clone();
      qb.processPopulateHint(); // needs to happen sooner so `qb.hasToManyJoins()` reports correctly
      qb.count(field, distinct ?? qb.hasToManyJoins())
        .limit(undefined)
        .offset(undefined)
        .orderBy([]);
      res = await qb.execute('get', false);
    }
    return res ? +res.count : 0;
  }
  /**
   * Executes the query, returning both array of results and total count query (without offset and limit).
   */
  async getResultAndCount() {
    return [await this.clone().getResultList(), await this.clone().getCount()];
  }
  as(aliasOrTargetEntity, alias) {
    const qb = this.getNativeQuery();
    let finalAlias = aliasOrTargetEntity;
    /* v8 ignore next */
    if (typeof aliasOrTargetEntity === 'string' && aliasOrTargetEntity.includes('.')) {
      throw new Error(
        'qb.as(alias) no longer supports target entity name prefix, use qb.as(TargetEntity, key) signature instead',
      );
    }
    if (alias) {
      const meta = this.metadata.get(aliasOrTargetEntity);
      /* v8 ignore next */
      finalAlias = meta.properties[alias]?.fieldNames[0] ?? alias;
    }
    qb.as(finalAlias);
    // tag the instance, so it is possible to detect it easily
    Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
    return qb;
  }
  /**
   * Combines the current query with one or more other queries using `UNION ALL`.
   * All queries must select the same columns. Returns a `QueryBuilder` that
   * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
   * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
   *
   * ```ts
   * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
   * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
   * const qb3 = em.createQueryBuilder(Employee).select('id').where(condition3);
   * const subquery = qb1.unionAll(qb2, qb3);
   *
   * const results = await em.find(Employee, { id: { $in: subquery } });
   * ```
   */
  unionAll(...others) {
    return this.buildUnionQuery('union all', others);
  }
  /**
   * Combines the current query with one or more other queries using `UNION` (with deduplication).
   * All queries must select the same columns. Returns a `QueryBuilder` that
   * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
   * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
   *
   * ```ts
   * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
   * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
   * const subquery = qb1.union(qb2);
   *
   * const results = await em.find(Employee, { id: { $in: subquery } });
   * ```
   */
  union(...others) {
    return this.buildUnionQuery('union', others);
  }
  buildUnionQuery(separator, others) {
    const all = [this, ...others];
    const parts = [];
    const params = [];
    for (const qb of all) {
      const compiled = qb instanceof _a ? qb.toQuery() : qb.compile();
      parts.push(`(${compiled.sql})`);
      params.push(...compiled.params);
    }
    const result = this.clone(true);
    result.#state.unionQuery = { sql: parts.join(` ${separator} `), params };
    return result;
  }
  with(name, query, options) {
    return this.addCte(name, query, options);
  }
  withRecursive(name, query, options) {
    return this.addCte(name, query, options, true);
  }
  addCte(name, query, options, recursive) {
    this.ensureNotFinalized();
    if (this.#state.ctes.some(cte => cte.name === name)) {
      throw new Error(`CTE with name '${name}' already exists`);
    }
    // Eagerly compile QueryBuilder to RawQueryFragment — later mutations to the sub-query won't be reflected
    const compiled = query instanceof _a ? query.toRaw() : query;
    this.#state.ctes.push({
      name,
      query: compiled,
      recursive,
      columns: options?.columns,
      materialized: options?.materialized,
    });
    return this;
  }
  clone(reset, preserve) {
    const qb = new _a(
      this.#state.mainAlias.entityName,
      this.metadata,
      this.driver,
      this.context,
      this.#state.mainAlias.aliasName,
      this.connectionType,
      this.em,
    );
    if (reset !== true) {
      qb.#state = Utils.copy(this.#state);
      // CTEs contain NativeQueryBuilder instances that should not be deep-cloned
      qb.#state.ctes = this.#state.ctes.map(cte => ({ ...cte }));
      if (Array.isArray(reset)) {
        const fresh = _a.createDefaultState();
        for (const key of reset) {
          qb.#state[key] = fresh[key];
        }
      }
    } else if (preserve) {
      for (const key of preserve) {
        qb.#state[key] = Utils.copy(this.#state[key]);
      }
    }
    qb.#state.finalized = false;
    qb.#query = undefined;
    qb.#helper = qb.createQueryBuilderHelper();
    return qb;
  }
  /**
   * Sets logger context for this query builder.
   */
  setLoggerContext(context) {
    this.loggerContext = context;
  }
  /**
   * Gets logger context for this query builder.
   */
  getLoggerContext() {
    this.loggerContext ??= {};
    return this.loggerContext;
  }
  fromVirtual(meta) {
    if (typeof meta.expression === 'string') {
      return `(${meta.expression}) as ${this.platform.quoteIdentifier(this.alias)}`;
    }
    const res = meta.expression(this.em, this.#state.cond, {});
    if (typeof res === 'string') {
      return `(${res}) as ${this.platform.quoteIdentifier(this.alias)}`;
    }
    if (res instanceof _a) {
      return `(${res.getFormattedQuery()}) as ${this.platform.quoteIdentifier(this.alias)}`;
    }
    if (isRaw(res)) {
      const query = this.platform.formatQuery(res.sql, res.params);
      return `(${query}) as ${this.platform.quoteIdentifier(this.alias)}`;
    }
    /* v8 ignore next */
    return res;
  }
  /**
   * Adds a join from a property object. Used internally for TPT joins where the property
   * is synthetic (not in entity.properties) but defined on metadata (e.g., tptParentProp).
   * The caller must create the alias first via createAlias().
   * @internal
   */
  addPropertyJoin(prop, ownerAlias, alias, type, path, schema) {
    schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
    const key = `[tpt]${ownerAlias}#${alias}`;
    this.#state.joins[key] =
      prop.kind === ReferenceKind.MANY_TO_ONE
        ? this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, {}, schema)
        : this.helper.joinOneToReference(prop, ownerAlias, alias, type, {}, schema);
    this.#state.joins[key].path = path;
    return key;
  }
  joinReference(field, alias, cond, type, path, schema, subquery) {
    this.ensureNotFinalized();
    if (typeof field === 'object') {
      const prop = {
        name: '__subquery__',
        kind: ReferenceKind.MANY_TO_ONE,
      };
      if (field instanceof _a) {
        prop.type = Utils.className(field.mainAlias.entityName);
        prop.targetMeta = field.mainAlias.meta;
        field = field.getNativeQuery();
      }
      if (isRaw(field)) {
        field = this.platform.formatQuery(field.sql, field.params);
      }
      const key = `${this.alias}.${prop.name}#${alias}`;
      this.#state.joins[key] = {
        prop,
        alias,
        type,
        cond,
        schema,
        subquery: field.toString(),
        ownerAlias: this.alias,
      };
      return { prop, key };
    }
    if (!subquery && type.includes('lateral')) {
      throw new Error(`Lateral join can be used only with a sub-query.`);
    }
    const [fromAlias, fromField] = this.helper.splitField(field);
    const q = str => `'${str}'`;
    if (!this.#state.aliases[fromAlias]) {
      throw new Error(
        `Trying to join ${q(fromField)} with alias ${q(fromAlias)}, but ${q(fromAlias)} is not a known alias. Available aliases are: ${Object.keys(this.#state.aliases).map(q).join(', ')}.`,
      );
    }
    const entityName = this.#state.aliases[fromAlias].entityName;
    const meta = this.metadata.get(entityName);
    const prop = meta.properties[fromField];
    if (!prop) {
      throw new Error(
        `Trying to join ${q(field)}, but ${q(fromField)} is not a defined relation on ${meta.className}.`,
      );
    }
    // For TPT inheritance, owning relations (M:1 and owning 1:1) may have FK columns in a parent table
    // Resolve the correct alias for the table that owns the FK column
    const ownerAlias =
      prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner)
        ? this.helper.getTPTAliasForProperty(fromField, fromAlias)
        : fromAlias;
    this.createAlias(prop.targetMeta.class, alias);
    cond = QueryHelper.processWhere({
      where: cond,
      entityName: this.mainAlias.entityName,
      metadata: this.metadata,
      platform: this.platform,
      aliasMap: this.getAliasMap(),
      aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
    });
    const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
    cond = criteriaNode.process(this, { ignoreBranching: true, alias });
    let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
    path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
    if (prop.kind === ReferenceKind.ONE_TO_MANY) {
      this.#state.joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
      this.#state.joins[aliasedName].path ??= path;
    } else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
      let pivotAlias = alias;
      if (type !== JoinType.pivotJoin) {
        const oldPivotAlias = this.getAliasForJoinPath(path + '[pivot]');
        pivotAlias = oldPivotAlias ?? this.getNextAlias(prop.pivotEntity);
        aliasedName = `${fromAlias}.${prop.name}#${pivotAlias}`;
      }
      const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
      Object.assign(this.#state.joins, joins);
      this.createAlias(prop.pivotEntity, pivotAlias);
      this.#state.joins[aliasedName].path ??= path;
      aliasedName = Object.keys(joins)[1];
    } else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
      this.#state.joins[aliasedName] = this.helper.joinOneToReference(prop, ownerAlias, alias, type, cond, schema);
      this.#state.joins[aliasedName].path ??= path;
    } else {
      // MANY_TO_ONE
      this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
      this.#state.joins[aliasedName].path ??= path;
    }
    return { prop, key: aliasedName };
  }
  prepareFields(fields, type = 'where', schema) {
    const ret = [];
    const getFieldName = (name, customAlias) => {
      const alias = customAlias ?? (type === 'groupBy' ? null : undefined);
      return this.helper.mapper(name, this.type, undefined, alias, schema);
    };
    fields.forEach(originalField => {
      if (typeof originalField !== 'string') {
        ret.push(originalField);
        return;
      }
      // Strip 'as alias' suffix if present — the alias is passed to mapper at the end
      let field = originalField;
      let customAlias;
      const asMatch = FIELD_ALIAS_RE.exec(originalField);
      if (asMatch) {
        field = asMatch[1].trim();
        customAlias = asMatch[2];
      }
      const join = Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
      if (join && type === 'where') {
        ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[join]));
        return;
      }
      const [a, f] = this.helper.splitField(field);
      const prop = this.helper.getProperty(f, a);
      /* v8 ignore next */
      if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
        return;
      }
      if (prop?.persist === false && !prop.embedded && !prop.formula && type === 'where') {
        return;
      }
      if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
        const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
        const aliased = this.#state.aliases[a] ? `${a}.${name}` : name;
        ret.push(getFieldName(aliased, customAlias));
        return;
      }
      if (prop?.kind === ReferenceKind.EMBEDDED) {
        if (customAlias) {
          throw new Error(
            `Cannot use 'as ${customAlias}' alias on embedded property '${field}' because it expands to multiple columns. Alias individual fields instead (e.g. '${field}.propertyName as ${customAlias}').`,
          );
        }
        const nest = prop => {
          for (const childProp of Object.values(prop.embeddedProps)) {
            if (
              childProp.fieldNames &&
              (childProp.kind !== ReferenceKind.EMBEDDED || childProp.object) &&
              childProp.persist !== false
            ) {
              ret.push(getFieldName(childProp.fieldNames[0]));
            } else {
              nest(childProp);
            }
          }
        };
        nest(prop);
        return;
      }
      if (prop && prop.fieldNames.length > 1 && !prop.fieldNames.includes(f)) {
        if (customAlias) {
          throw new Error(
            `Cannot use 'as ${customAlias}' alias on '${field}' because it expands to multiple columns (${prop.fieldNames.join(', ')}).`,
          );
        }
        ret.push(...prop.fieldNames.map(f => getFieldName(f)));
        return;
      }
      ret.push(getFieldName(field, customAlias));
    });
    const requiresSQLConversion = this.mainAlias.meta.props.filter(
      p => p.hasConvertToJSValueSQL && p.persist !== false,
    );
    if (
      this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES) &&
      (fields.includes('*') || fields.includes(`${this.mainAlias.aliasName}.*`)) &&
      requiresSQLConversion.length > 0
    ) {
      for (const p of requiresSQLConversion) {
        ret.push(this.helper.mapper(p.name, this.type));
      }
    }
    for (const f of Object.keys(this.#state.populateMap)) {
      if (type === 'where' && this.#state.joins[f]) {
        ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[f]));
      }
    }
    return Utils.unique(ret);
  }
  /**
   * Resolves nested paths like `a.books.title` to their actual field references.
   * Auto-joins relations as needed and returns `{alias}.{field}`.
   * For embeddeds: navigates into flattened embeddeds to return the correct field name.
   */
  resolveNestedPath(field) {
    if (typeof field !== 'string' || !field.includes('.')) {
      return field;
    }
    const parts = field.split('.');
    // Simple alias.property case - let prepareFields handle it
    if (parts.length === 2 && this.#state.aliases[parts[0]]) {
      return field;
    }
    // Start with root alias
    let currentAlias = parts[0];
    let currentMeta = this.#state.aliases[currentAlias]
      ? this.metadata.get(this.#state.aliases[currentAlias].entityName)
      : this.mainAlias.meta;
    // If first part is not an alias, it's a property of the main entity
    if (!this.#state.aliases[currentAlias]) {
      currentAlias = this.mainAlias.aliasName;
      parts.unshift(currentAlias);
    }
    // Walk through the path parts (skip the alias)
    for (let i = 1; i < parts.length; i++) {
      const propName = parts[i];
      const prop = currentMeta.properties[propName];
      if (!prop) {
        return field; // Unknown property, return as-is for raw SQL support
      }
      const isLastPart = i === parts.length - 1;
      // Handle embedded properties - navigate into flattened embeddeds
      if (prop.kind === ReferenceKind.EMBEDDED) {
        if (prop.object) {
          return `${currentAlias}.${propName}`;
        }
        // Navigate through remaining path to find the leaf property
        const remainingPath = parts.slice(i + 1);
        let embeddedProp = prop;
        for (const part of remainingPath) {
          embeddedProp = embeddedProp?.embeddedProps?.[part];
          if (embeddedProp?.object && embeddedProp.fieldNames?.[0]) {
            return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
          }
        }
        return `${currentAlias}.${embeddedProp?.fieldNames?.[0] ?? propName}`;
      }
      // Handle relations - auto-join if not the last part
      if (
        prop.kind === ReferenceKind.MANY_TO_ONE ||
        prop.kind === ReferenceKind.ONE_TO_ONE ||
        prop.kind === ReferenceKind.ONE_TO_MANY ||
        prop.kind === ReferenceKind.MANY_TO_MANY
      ) {
        if (isLastPart) {
          return `${currentAlias}.${propName}`;
        }
        // Find existing join or create new one
        const joinPath = parts.slice(0, i + 1).join('.');
        const existingJoinKey = Object.keys(this.#state.joins).find(k => {
          const join = this.#state.joins[k];
          // Check by path or by key prefix (key format is `alias.field#joinAlias`)
          return join.path === joinPath || k.startsWith(`${currentAlias}.${propName}#`);
        });
        let joinAlias;
        if (existingJoinKey) {
          joinAlias = this.#state.joins[existingJoinKey].alias;
        } else {
          joinAlias = this.getNextAlias(prop.targetMeta?.className ?? propName);
          this.join(`${currentAlias}.${propName}`, joinAlias, {}, JoinType.leftJoin);
        }
        currentAlias = joinAlias;
        currentMeta = prop.targetMeta;
        continue;
      }
      // Scalar property - return it (if not last part, it's an invalid path but let SQL handle it)
      return `${currentAlias}.${propName}`;
    }
    return field;
  }
  init(type, data, cond) {
    this.ensureNotFinalized();
    this.#state.type = type;
    if ([QueryType.UPDATE, QueryType.DELETE].includes(type) && Utils.hasObjectKeys(this.#state.cond)) {
      throw new Error(
        `You are trying to call \`qb.where().${type.toLowerCase()}()\`. Calling \`qb.${type.toLowerCase()}()\` before \`qb.where()\` is required.`,
      );
    }
    if (!this.helper.isTableNameAliasRequired(type)) {
      this.#state.fields = undefined;
    }
    if (data) {
      if (Utils.isEntity(data)) {
        data = this.em?.getComparator().prepareEntity(data) ?? serialize(data);
      }
      this.#state.data = this.helper.processData(data, this.#state.flags.has(QueryFlag.CONVERT_CUSTOM_TYPES), false);
    }
    if (cond) {
      this.where(cond);
    }
    return this;
  }
  getQueryBase(processVirtualEntity) {
    const qb = this.platform.createNativeQueryBuilder().setFlags(this.#state.flags);
    const { subQuery, aliasName, entityName, meta, rawTableName } = this.mainAlias;
    const requiresAlias =
      this.#state.finalized && (this.#state.explicitAlias || this.helper.isTableNameAliasRequired(this.type));
    const alias = requiresAlias ? aliasName : undefined;
    const schema = this.getSchema(this.mainAlias);
    const tableName = rawTableName
      ? rawTableName
      : subQuery instanceof NativeQueryBuilder
        ? subQuery.as(aliasName)
        : subQuery
          ? raw(`(${subQuery.sql}) as ${this.platform.quoteIdentifier(aliasName)}`, subQuery.params)
          : this.helper.getTableName(entityName);
    const joinSchema = this.#state.schema ?? this.em?.schema ?? schema;
    const schemaOverride = this.#state.schema ?? this.em?.schema;
    if (meta.virtual && processVirtualEntity) {
      qb.from(raw(this.fromVirtual(meta)), { indexHint: this.#state.indexHint });
    } else {
      qb.from(tableName, {
        schema: rawTableName ? undefined : schema,
        alias,
        indexHint: this.#state.indexHint,
      });
    }
    switch (this.type) {
      case QueryType.SELECT:
        qb.select(this.prepareFields(this.#state.fields, 'where', schema));
        if (this.#state.distinctOn) {
          qb.distinctOn(this.prepareFields(this.#state.distinctOn, 'where', schema));
        } else if (this.#state.flags.has(QueryFlag.DISTINCT)) {
          qb.distinct();
        }
        this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
        break;
      case QueryType.COUNT: {
        const fields = this.#state.fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
        qb.count(fields, this.#state.flags.has(QueryFlag.DISTINCT));
        this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
        break;
      }
      case QueryType.INSERT:
        qb.insert(this.#state.data);
        break;
      case QueryType.UPDATE:
        qb.update(this.#state.data);
        this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
        this.helper.updateVersionProperty(qb, this.#state.data);
        break;
      case QueryType.DELETE:
        qb.delete();
        break;
      case QueryType.TRUNCATE:
        qb.truncate();
        break;
    }
    return qb;
  }
  applyDiscriminatorCondition() {
    const meta = this.mainAlias.meta;
    if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
      return;
    }
    const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
    const children = [];
    const lookUpChildren = (ret, type) => {
      const children = types.filter(meta2 => meta2.extends === type);
      children.forEach(m => lookUpChildren(ret, m.class));
      ret.push(...children.filter(c => c.discriminatorValue));
      return children;
    };
    lookUpChildren(children, meta.class);
    this.andWhere({
      [meta.root.discriminatorColumn]:
        children.length > 0
          ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] }
          : meta.discriminatorValue,
    });
  }
  /**
   * Ensures TPT joins are applied. Can be called early before finalize() to populate
   * the _tptAlias map for use in join resolution. Safe to call multiple times.
   * @internal
   */
  ensureTPTJoins() {
    this.applyTPTJoins();
  }
  /**
   * For TPT (Table-Per-Type) inheritance: INNER JOINs parent tables.
   * When querying a child entity, we need to join all parent tables.
   * Field selection is handled separately in addTPTParentFields().
   */
  applyTPTJoins() {
    const meta = this.mainAlias.meta;
    if (
      meta?.inheritanceType !== 'tpt' ||
      !meta.tptParent ||
      ![QueryType.SELECT, QueryType.COUNT].includes(this.type)
    ) {
      return;
    }
    if (this.#state.tptJoinsApplied) {
      return;
    }
    this.#state.tptJoinsApplied = true;
    let childMeta = meta;
    let childAlias = this.mainAlias.aliasName;
    while (childMeta.tptParent) {
      const parentMeta = childMeta.tptParent;
      const parentAlias = this.getNextAlias(parentMeta.className);
      this.createAlias(parentMeta.class, parentAlias);
      this.#state.tptAlias[parentMeta.className] = parentAlias;
      this.addPropertyJoin(
        childMeta.tptParentProp,
        childAlias,
        parentAlias,
        JoinType.innerJoin,
        `[tpt]${childMeta.className}`,
      );
      childMeta = parentMeta;
      childAlias = parentAlias;
    }
  }
  /**
   * For TPT inheritance: adds field selections from parent tables.
   */
  addTPTParentFields() {
    const meta = this.mainAlias.meta;
    if (
      meta?.inheritanceType !== 'tpt' ||
      !meta.tptParent ||
      ![QueryType.SELECT, QueryType.COUNT].includes(this.type)
    ) {
      return;
    }
    if (!this.#state.fields?.includes('*') && !this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`)) {
      return;
    }
    let parentMeta = meta.tptParent;
    while (parentMeta) {
      const parentAlias = this.#state.tptAlias[parentMeta.className];
      if (parentAlias) {
        const schema = parentMeta.schema === '*' ? '*' : this.driver.getSchemaName(parentMeta);
        parentMeta.ownProps
          .filter(prop => this.platform.shouldHaveColumn(prop, []))
          .forEach(prop =>
            this.#state.fields.push(...this.driver.mapPropToFieldNames(this, prop, parentAlias, parentMeta, schema)),
          );
      }
      parentMeta = parentMeta.tptParent;
    }
  }
  /**
   * For TPT polymorphic queries: LEFT JOINs all child tables when querying a TPT base class.
   * Adds discriminator and child fields to determine and load the concrete type.
   */
  applyTPTPolymorphicJoins() {
    const meta = this.mainAlias.meta;
    const descendants = meta?.allTPTDescendants;
    if (!descendants?.length || ![QueryType.SELECT, QueryType.COUNT].includes(this.type)) {
      return;
    }
    if (!this.#state.fields?.includes('*') && !this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`)) {
      return;
    }
    // LEFT JOIN each descendant table and add their fields
    for (const childMeta of descendants) {
      const childAlias = this.getNextAlias(childMeta.className);
      this.createAlias(childMeta.class, childAlias);
      this.#state.tptAlias[childMeta.className] = childAlias;
      this.addPropertyJoin(
        childMeta.tptInverseProp,
        this.mainAlias.aliasName,
        childAlias,
        JoinType.leftJoin,
        `[tpt]${meta.className}`,
      );
      // Add child fields
      const schema = childMeta.schema === '*' ? '*' : this.driver.getSchemaName(childMeta);
      childMeta.ownProps
        .filter(prop => !prop.primary && this.platform.shouldHaveColumn(prop, []))
        .forEach(prop =>
          this.#state.fields.push(...this.driver.mapPropToFieldNames(this, prop, childAlias, childMeta, schema)),
        );
    }
    // Add computed discriminator (CASE WHEN to determine concrete type)
    // descendants is pre-sorted by depth (deepest first) during discovery
    if (meta.tptDiscriminatorColumn) {
      this.#state.fields.push(
        this.driver.buildTPTDiscriminatorExpression(meta, descendants, this.#state.tptAlias, this.mainAlias.aliasName),
      );
    }
  }
  finalize() {
    if (this.#state.finalized) {
      return;
    }
    if (!this.#state.type) {
      this.select('*');
    }
    const meta = this.mainAlias.meta;
    this.applyDiscriminatorCondition();
    this.applyTPTJoins();
    this.addTPTParentFields();
    this.applyTPTPolymorphicJoins();
    this.processPopulateHint();
    this.processNestedJoins();
    if (meta && (this.#state.fields?.includes('*') || this.#state.fields?.includes(`${this.mainAlias.aliasName}.*`))) {
      const schema = this.getSchema(this.mainAlias);
      // Create a column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
      // For TPT, use helper to resolve correct alias per property (inherited props use parent alias)
      const quotedMainAlias = this.platform.quoteIdentifier(this.mainAlias.aliasName).toString();
      const columns = meta.createColumnMappingObject(
        prop => this.helper.getTPTAliasForProperty(prop.name, this.mainAlias.aliasName),
        quotedMainAlias,
      );
      meta.props
        .filter(prop => prop.formula && (!prop.lazy || this.#state.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
        .map(prop => {
          const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
          const table = this.helper.createFormulaTable(quotedMainAlias, meta, schema);
          return `${this.driver.evaluateFormula(prop.formula, columns, table)} as ${aliased}`;
        })
        .filter(
          field =>
            !this.#state.fields.some(f => {
              if (isRaw(f)) {
                return f.sql === field && f.params.length === 0;
              }
              return f === field;
            }),
        )
        .forEach(field => this.#state.fields.push(raw(field)));
    }
    QueryHelper.processObjectParams(this.#state.data);
    QueryHelper.processObjectParams(this.#state.cond);
    QueryHelper.processObjectParams(this.#state.having);
    // automatically enable paginate flag when we detect to-many joins, but only if there is no `group by` clause
    if (
      !this.#state.flags.has(QueryFlag.DISABLE_PAGINATE) &&
      this.#state.groupBy.length === 0 &&
      this.hasToManyJoins()
    ) {
      this.#state.flags.add(QueryFlag.PAGINATE);
    }
    if (
      meta &&
      !meta.virtual &&
      this.#state.flags.has(QueryFlag.PAGINATE) &&
      !this.#state.flags.has(QueryFlag.DISABLE_PAGINATE) &&
      (this.#state.limit > 0 || this.#state.offset > 0)
    ) {
      this.wrapPaginateSubQuery(meta);
    }
    if (
      meta &&
      (this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.#state.flags.has(QueryFlag.DELETE_SUB_QUERY))
    ) {
      this.wrapModifySubQuery(meta);
    }
    this.#state.finalized = true;
  }
  /** @internal */
  processPopulateHint() {
    if (this.#state.populateHintFinalized) {
      return;
    }
    const meta = this.mainAlias.meta;
    if (meta && this.#state.flags.has(QueryFlag.AUTO_JOIN_ONE_TO_ONE_OWNER)) {
      const relationsToPopulate = this.#state.populate.map(({ field }) => field);
      meta.relations
        .filter(
          prop =>
            prop.kind === ReferenceKind.ONE_TO_ONE &&
            !prop.owner &&
            !relationsToPopulate.includes(prop.name) &&
            !relationsToPopulate.includes(`${prop.name}:ref`),
        )
        .map(prop => ({ field: `${prop.name}:ref` }))
        .forEach(item => this.#state.populate.push(item));
    }
    this.#state.populate.forEach(({ field }) => {
      const [fromAlias, fromField] = this.helper.splitField(field);
      const aliasedField = `${fromAlias}.${fromField}`;
      const join = Object.keys(this.#state.joins).find(k => `${aliasedField}#${this.#state.joins[k].alias}` === k);
      if (join && this.#state.joins[join] && this.helper.isOneToOneInverse(fromField)) {
        this.#state.populateMap[join] = this.#state.joins[join].alias;
        return;
      }
      if (meta && this.helper.isOneToOneInverse(fromField)) {
        const prop = meta.properties[fromField];
        const alias = this.getNextAlias(prop.pivotEntity ?? prop.targetMeta.class);
        const aliasedName = `${fromAlias}.${prop.name}#${alias}`;
        this.#state.joins[aliasedName] = this.helper.joinOneToReference(
          prop,
          this.mainAlias.aliasName,
          alias,
          JoinType.leftJoin,
        );
        this.#state.joins[aliasedName].path =
          `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? meta.className}.${prop.name}`;
        this.#state.populateMap[aliasedName] = this.#state.joins[aliasedName].alias;
        this.createAlias(prop.targetMeta.class, alias);
      }
    });
    this.processPopulateWhere(false);
    this.processPopulateWhere(true);
    this.#state.populateHintFinalized = true;
  }
  processPopulateWhere(filter) {
    const value = filter ? this.#state.populateFilter : this.#state.populateWhere;
    if (value == null || value === PopulateHint.ALL) {
      return;
    }
    let joins = Object.values(this.#state.joins);
    for (const join of joins) {
      join.cond_ ??= join.cond;
      join.cond = { ...join.cond };
    }
    if (typeof value === 'object') {
      const cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, value).process(this, {
        matchPopulateJoins: true,
        ignoreBranching: true,
        preferNoBranch: true,
        filter,
      });
      // there might be new joins created by processing the `populateWhere` object
      joins = Object.values(this.#state.joins);
      this.mergeOnConditions(joins, cond, filter);
    }
  }
  mergeOnConditions(joins, cond, filter, op) {
    for (const k of Object.keys(cond)) {
      if (Utils.isOperator(k)) {
        if (Array.isArray(cond[k])) {
          cond[k].forEach(c => this.mergeOnConditions(joins, c, filter, k));
        }
        /* v8 ignore next */
        this.mergeOnConditions(joins, cond[k], filter, k);
      }
      const [alias] = this.helper.splitField(k);
      const join = joins.find(j => j.alias === alias);
      if (join) {
        const parentJoin = joins.find(j => j.alias === join.ownerAlias);
        // https://stackoverflow.com/a/56815807/3665878
        if (parentJoin && !filter) {
          const nested = (parentJoin.nested ??= new Set());
          join.type =
            join.type === JoinType.innerJoin ||
            [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(parentJoin.prop.kind)
              ? JoinType.nestedInnerJoin
              : JoinType.nestedLeftJoin;
          nested.add(join);
        }
        if (join.cond[k]) {
          /* v8 ignore next */
          join.cond = { [op ?? '$and']: [join.cond, { [k]: cond[k] }] };
        } else if (op === '$or') {
          join.cond.$or ??= [];
          join.cond.$or.push({ [k]: cond[k] });
        } else {
          join.cond = { ...join.cond, [k]: cond[k] };
        }
      }
    }
  }
  /**
   * When adding an inner join on a left joined relation, we need to nest them,
   * otherwise the inner join could discard rows of the root table.
   */
  processNestedJoins() {
    if (this.#state.flags.has(QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
      return;
    }
    const joins = Object.values(this.#state.joins);
    const lookupParentGroup = j => {
      return j.nested ?? (j.parent ? lookupParentGroup(j.parent) : undefined);
    };
    for (const join of joins) {
      if (join.type === JoinType.innerJoin) {
        join.parent = joins.find(j => j.alias === join.ownerAlias);
        // https://stackoverflow.com/a/56815807/3665878
        if (join.parent?.type === JoinType.leftJoin || join.parent?.type === JoinType.nestedLeftJoin) {
          const nested = (join.parent.nested ??= new Set());
          join.type = join.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
          nested.add(join);
        } else if (join.parent?.type === JoinType.nestedInnerJoin) {
          const group = lookupParentGroup(join.parent);
          const nested = group ?? (join.parent.nested ??= new Set());
          join.type = join.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
          nested.add(join);
        }
      }
    }
  }
  hasToManyJoins() {
    return Object.values(this.#state.joins).some(join => {
      return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
    });
  }
  wrapPaginateSubQuery(meta) {
    const schema = this.getSchema(this.mainAlias);
    const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
    const subQuery = this.clone(['orderBy', 'fields', 'lockMode', 'lockTables'])
      .select(pks)
      .groupBy(pks)
      .limit(this.#state.limit);
    // revert the on conditions added via populateWhere, we want to apply those only once
    for (const join of Object.values(subQuery.#state.joins)) {
      if (join.cond_) {
        join.cond = join.cond_;
      }
    }
    if (this.#state.offset) {
      subQuery.offset(this.#state.offset);
    }
    const addToSelect = [];
    if (this.#state.orderBy.length > 0) {
      const orderBy = [];
      for (const orderMap of this.#state.orderBy) {
        for (const field of Utils.getObjectQueryKeys(orderMap)) {
          const direction = orderMap[field];
          if (RawQueryFragment.isKnownFragmentSymbol(field)) {
            orderBy.push({ [field]: direction });
            continue;
          }
          const [a, f] = this.helper.splitField(field);
          const prop = this.helper.getProperty(f, a);
          const type = this.platform.castColumn(prop);
          const fieldName = this.helper.mapper(field, this.type, undefined, null);
          if (!prop?.persist && !prop?.formula && !prop?.hasConvertToJSValueSQL && !pks.includes(fieldName)) {
            addToSelect.push(fieldName);
          }
          const quoted = this.platform.quoteIdentifier(fieldName);
          const key = raw(`min(${quoted}${type})`);
          orderBy.push({ [key]: direction });
        }
      }
      subQuery.orderBy(orderBy);
    }
    subQuery.#state.finalized = true;
    const innerQuery = subQuery.as(this.mainAlias.aliasName).clear('select').select(pks);
    if (addToSelect.length > 0) {
      addToSelect.forEach(prop => {
        const field = this.#state.fields.find(field => {
          if (typeof field === 'object' && field && '__as' in field) {
            return field.__as === prop;
          }
          if (isRaw(field)) {
            // not perfect, but should work most of the time, ideally we should check only the alias (`... as alias`)
            return field.sql.includes(prop);
          }
          return false;
        });
        /* v8 ignore next */
        if (isRaw(field)) {
          innerQuery.select(field);
        } else if (field instanceof NativeQueryBuilder) {
          innerQuery.select(field.toRaw());
        } else if (field) {
          innerQuery.select(field);
        }
      });
    }
    // multiple sub-queries are needed to get around mysql limitations with order by + limit + where in + group by (o.O)
    // https://stackoverflow.com/questions/17892762/mysql-this-version-of-mysql-doesnt-yet-support-limit-in-all-any-some-subqu
    const subSubQuery = this.platform.createNativeQueryBuilder();
    subSubQuery.select(pks).from(innerQuery);
    this.#state.limit = undefined;
    this.#state.offset = undefined;
    // Save the original WHERE conditions before pruning joins
    const originalCond = this.#state.cond;
    const populatePaths = this.getPopulatePaths();
    if (!this.#state.fields.some(field => isRaw(field))) {
      this.pruneJoinsForPagination(meta, populatePaths);
    }
    // Transfer WHERE conditions to ORDER BY joins (GH #6160)
    this.transferConditionsForOrderByJoins(meta, originalCond, populatePaths);
    const { sql, params } = subSubQuery.compile();
    this.select(this.#state.fields).where({
      [Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
    });
  }
  /**
   * Computes the set of populate paths from the _populate hints.
   */
  getPopulatePaths() {
    const paths = new Set();
    function addPath(hints, prefix = '') {
      for (const hint of hints) {
        const field = hint.field.split(':')[0];
        const fullPath = prefix ? prefix + '.' + field : field;
        paths.add(fullPath);
        if (hint.children) {
          addPath(hint.children, fullPath);
        }
      }
    }
    addPath(this.#state.populate);
    return paths;
  }
  normalizeJoinPath(join, meta) {
    return join.path?.replace(/\[populate]|\[pivot]|:ref/g, '').replace(new RegExp(`^${meta.className}.`), '') ?? '';
  }
  /**
   * Transfers WHERE conditions to ORDER BY joins that are not used for population.
   * This ensures the outer query's ORDER BY uses the same filtered rows as the subquery.
   * GH #6160
   */
  transferConditionsForOrderByJoins(meta, cond, populatePaths) {
    if (!cond || this.#state.orderBy.length === 0) {
      return;
    }
    const orderByAliases = new Set(
      this.#state.orderBy
        .flatMap(hint => Object.keys(hint))
        .filter(k => !RawQueryFragment.isKnownFragmentSymbol(k))
        .map(k => k.split('.')[0]),
    );
    for (const join of Object.values(this.#state.joins)) {
      const joinPath = this.normalizeJoinPath(join, meta);
      const isPopulateJoin = populatePaths.has(joinPath);
      // Only transfer conditions for joins used for ORDER BY but not for population
      if (orderByAliases.has(join.alias) && !isPopulateJoin) {
        this.transferConditionsToJoin(cond, join);
      }
    }
  }
  /**
   * Removes joins that are not used for population or ordering to improve performance.
   */
  pruneJoinsForPagination(meta, populatePaths) {
    const orderByAliases = this.#state.orderBy.flatMap(hint => Object.keys(hint)).map(k => k.split('.')[0]);
    const joins = Object.entries(this.#state.joins);
    const rootAlias = this.alias;
    function addParentAlias(alias) {
      const join = joins.find(j => j[1].alias === alias);
      if (join && join[1].ownerAlias !== rootAlias) {
        orderByAliases.push(join[1].ownerAlias);
        addParentAlias(join[1].ownerAlias);
      }
    }
    for (const orderByAlias of orderByAliases) {
      addParentAlias(orderByAlias);
    }
    for (const [key, join] of joins) {
      const path = this.normalizeJoinPath(join, meta);
      if (!populatePaths.has(path) && !orderByAliases.includes(join.alias)) {
        delete this.#state.joins[key];
      }
    }
  }
  /**
   * Transfers WHERE conditions that reference a join alias to the join's ON clause.
   * This is needed when a join is kept for ORDER BY after pagination wrapping,
   * so the outer query orders by the same filtered rows as the subquery.
   * @internal
   */
  transferConditionsToJoin(cond, join, path = '') {
    const aliasPrefix = join.alias + '.';
    for (const key of Object.keys(cond)) {
      const value = cond[key];
      // Handle $and/$or operators - recurse into nested conditions
      if (key === '$and' || key === '$or') {
        if (Array.isArray(value)) {
          for (const item of value) {
            this.transferConditionsToJoin(item, join, path);
          }
        }
        continue;
      }
      // Check if this condition references the join alias
      if (key.startsWith(aliasPrefix)) {
        // Add condition to the join's ON clause
        join.cond[key] = value;
      }
    }
  }
  wrapModifySubQuery(meta) {
    const subQuery = this.clone();
    subQuery.#state.finalized = true;
    // wrap one more time to get around MySQL limitations
    // https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause
    const subSubQuery = this.platform.createNativeQueryBuilder();
    const method = this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) ? 'update' : 'delete';
    const schema = this.getSchema(this.mainAlias);
    const pks = this.prepareFields(meta.primaryKeys, 'sub-query', schema);
    this.#state.cond = {}; // otherwise we would trigger validation error
    this.#state.joins = {}; // included in the subquery
    subSubQuery.select(pks).from(subQuery.as(this.mainAlias.aliasName));
    const { sql, params } = subSubQuery.compile();
    this[method](this.#state.data).where({
      [Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
    });
  }
  getSchema(alias) {
    const { meta } = alias;
    const metaSchema = meta.schema && meta.schema !== '*' ? meta.schema : undefined;
    const schema = this.#state.schema ?? metaSchema ?? this.em?.schema ?? this.em?.config.getSchema(true);
    if (schema === this.platform.getDefaultSchemaName()) {
      return undefined;
    }
    return schema;
  }
  /** @internal */
  createAlias(entityName, aliasName, subQuery) {
    const meta = this.metadata.find(entityName);
    const alias = { aliasName, entityName, meta, subQuery };
    this.#state.aliases[aliasName] = alias;
    return alias;
  }
  createMainAlias(entityName, aliasName, subQuery) {
    this.#state.mainAlias = this.createAlias(entityName, aliasName, subQuery);
    this.#helper = this.createQueryBuilderHelper();
    return this.#state.mainAlias;
  }
  fromSubQuery(target, aliasName) {
    const { entityName } = target.mainAlias;
    aliasName ??= this.getNextAlias(entityName);
    const subQuery = target.#state.unionQuery ? target.toRaw() : target.getNativeQuery();
    this.createMainAlias(entityName, aliasName, subQuery);
  }
  fromEntityName(entityName, aliasName) {
    aliasName ??= this.#state.mainAlias?.aliasName ?? this.getNextAlias(entityName);
    this.createMainAlias(entityName, aliasName);
  }
  fromRawTable(tableName, aliasName) {
    aliasName ??= this.#state.mainAlias?.aliasName ?? this.getNextAlias(tableName);
    const meta = new EntityMetadata({
      className: tableName,
      collection: tableName,
    });
    meta.root = meta;
    this.#state.mainAlias = {
      aliasName,
      entityName: tableName,
      meta,
      rawTableName: tableName,
    };
    this.#state.aliases[aliasName] = this.#state.mainAlias;
    this.#helper = this.createQueryBuilderHelper();
  }
  createQueryBuilderHelper() {
    return new QueryBuilderHelper(
      this.mainAlias.entityName,
      this.mainAlias.aliasName,
      this.#state.aliases,
      this.#state.subQueries,
      this.driver,
      this.#state.tptAlias,
    );
  }
  ensureFromClause() {
    /* v8 ignore next */
    if (!this.#state.mainAlias) {
      throw new Error(`Cannot proceed to build a query because the main alias is not set.`);
    }
  }
  ensureNotFinalized() {
    if (this.#state.finalized) {
      throw new Error('This QueryBuilder instance is already finalized, clone it first if you want to modify it.');
    }
  }
  /** @ignore */
  /* v8 ignore next */
  [Symbol.for('nodejs.util.inspect.custom')](depth = 2) {
    const object = { ...this };
    const hidden = ['metadata', 'driver', 'context', 'platform', 'type'];
    Object.keys(object)
      .filter(k => k.startsWith('_'))
      .forEach(k => delete object[k]);
    Object.keys(object)
      .filter(k => object[k] == null)
      .forEach(k => delete object[k]);
    hidden.forEach(k => delete object[k]);
    let prefix = this.type ? this.type.substring(0, 1) + this.type.toLowerCase().substring(1) : '';
    if (this.#state.data) {
      object.data = this.#state.data;
    }
    if (this.#state.schema) {
      object.schema = this.#state.schema;
    }
    if (!Utils.isEmpty(this.#state.cond)) {
      object.where = this.#state.cond;
    }
    if (this.#state.onConflict?.[0]) {
      prefix = 'Upsert';
      object.onConflict = this.#state.onConflict[0];
    }
    if (!Utils.isEmpty(this.#state.orderBy)) {
      object.orderBy = this.#state.orderBy;
    }
    const name = this.#state.mainAlias
      ? `${prefix}QueryBuilder<${Utils.className(this.#state.mainAlias?.entityName)}>`
      : 'QueryBuilder';
    const ret = inspect(object, { depth });
    return ret === '[Object]' ? `[${name}]` : name + ' ' + ret;
  }
}
_a = QueryBuilder;
