import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
import { Utils } from './utils/Utils.js';
import { Cursor } from './utils/Cursor.js';
import { QueryHelper } from './utils/QueryHelper.js';
import { TransactionContext } from './utils/TransactionContext.js';
import { isRaw, Raw } from './utils/RawQueryFragment.js';
import { EntityFactory } from './entity/EntityFactory.js';
import { EntityAssigner } from './entity/EntityAssigner.js';
import { validateEmptyWhere, validateParams, validatePrimaryKey, validateProperty } from './entity/validators.js';
import { EntityLoader } from './entity/EntityLoader.js';
import { Reference } from './entity/Reference.js';
import { helper } from './entity/wrap.js';
import { ChangeSet, ChangeSetType } from './unit-of-work/ChangeSet.js';
import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
import {
  EventType,
  FlushMode,
  LoadStrategy,
  LockMode,
  PopulateHint,
  PopulatePath,
  QueryFlag,
  ReferenceKind,
  SCALAR_TYPES,
} from './enums.js';
import { EventManager } from './events/EventManager.js';
import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js';
import { OptimisticLockError, ValidationError } from './errors.js';
import { applyPopulateHints, getLoadingStrategy } from './entity/utils.js';
import { TransactionManager } from './utils/TransactionManager.js';
/**
 * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
 * such as UnitOfWork, Query Language, and Repository API.
 * @template {IDatabaseDriver} Driver current driver type
 */
export class EntityManager {
  config;
  driver;
  metadata;
  eventManager;
  static #counter = 1;
  /** @internal */
  _id = EntityManager.#counter++;
  /** Whether this is the global (root) EntityManager instance. */
  global = false;
  /** The context name of this EntityManager, derived from the ORM configuration. */
  name;
  #loaders = {};
  #repositoryMap = new Map();
  #entityLoader;
  #comparator;
  #entityFactory;
  #unitOfWork;
  #resultCache;
  #filters = {};
  #filterParams = {};
  loggerContext;
  #transactionContext;
  #disableTransactions;
  #flushMode;
  #schema;
  #useContext;
  /**
   * @internal
   */
  constructor(config, driver, metadata, useContext = true, eventManager = new EventManager(config.get('subscribers'))) {
    this.config = config;
    this.driver = driver;
    this.metadata = metadata;
    this.eventManager = eventManager;
    this.#useContext = useContext;
    this.#entityLoader = new EntityLoader(this);
    this.name = this.config.get('contextName');
    this.#comparator = this.config.getComparator(this.metadata);
    this.#resultCache = this.config.getResultCacheAdapter();
    this.#disableTransactions = this.config.get('disableTransactions');
    this.#entityFactory = new EntityFactory(this);
    this.#unitOfWork = new UnitOfWork(this);
  }
  /**
   * Gets the Driver instance used by this EntityManager.
   * Driver is singleton, for one MikroORM instance, only one driver is created.
   */
  getDriver() {
    return this.driver;
  }
  /**
   * Gets the Connection instance, by default returns write connection
   */
  getConnection(type) {
    return this.driver.getConnection(type);
  }
  /**
   * Gets the platform instance. Just like the driver, platform is singleton, one for a MikroORM instance.
   */
  getPlatform() {
    return this.driver.getPlatform();
  }
  /**
   * Gets repository for given entity. You can pass either string name or entity class reference.
   */
  getRepository(entityName) {
    const meta = this.metadata.get(entityName);
    if (!this.#repositoryMap.has(meta)) {
      const RepositoryClass = this.config.getRepositoryClass(meta.repository);
      this.#repositoryMap.set(meta, new RepositoryClass(this, entityName));
    }
    return this.#repositoryMap.get(meta);
  }
  /**
   * Shortcut for `em.getRepository()`.
   */
  repo(entityName) {
    return this.getRepository(entityName);
  }
  /**
   * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
   */
  async find(entityName, where, options = {}) {
    if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
      const em = this.getContext(false);
      const fork = em.fork({ keepTransactionContext: true });
      const ret = await fork.find(entityName, where, { ...options, disableIdentityMap: false });
      fork.clear();
      return ret;
    }
    const em = this.getContext();
    em.prepareOptions(options);
    await em.tryFlush(entityName, options);
    where = await em.processWhere(entityName, where, options, 'read');
    validateParams(where);
    const meta = this.metadata.get(entityName);
    if (meta.orderBy) {
      options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
    } else {
      options.orderBy ??= {};
    }
    options.populate = await em.preparePopulate(entityName, options);
    const populate = options.populate;
    const cacheKey = em.cacheKey(entityName, options, 'em.find', where);
    const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
    if (cached?.data) {
      await em.#entityLoader.populate(entityName, cached.data, populate, {
        ...options,
        ...em.getPopulateWhere(where, options),
        ignoreLazyScalarProperties: true,
        lookup: false,
      });
      return cached.data;
    }
    options = { ...options };
    // save the original hint value so we know it was infer/all
    options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
    options.populateWhere = this.createPopulateWhere({ ...where }, options);
    options.populateFilter = await this.getJoinedFilters(meta, options);
    await em.processUnionWhere(entityName, options, 'read');
    const results = await em.driver.find(entityName, where, { ctx: em.#transactionContext, em, ...options });
    if (results.length === 0) {
      await em.storeCache(options.cache, cached, []);
      return [];
    }
    const ret = [];
    for (const data of results) {
      const entity = em.#entityFactory.create(entityName, data, {
        merge: true,
        refresh: options.refresh,
        schema: options.schema,
        convertCustomTypes: true,
      });
      ret.push(entity);
    }
    const unique = Utils.unique(ret);
    await em.#entityLoader.populate(entityName, unique, populate, {
      ...options,
      ...em.getPopulateWhere(where, options),
      ignoreLazyScalarProperties: true,
      lookup: false,
    });
    await em.#unitOfWork.dispatchOnLoadEvent();
    if (meta.virtual) {
      await em.storeCache(options.cache, cached, () => ret);
    } else {
      await em.storeCache(options.cache, cached, () => unique.map(e => helper(e).toPOJO()));
    }
    return unique;
  }
  /**
   * Finds all entities and returns an async iterable (async generator) that yields results one by one.
   * The results are merged and mapped to entity instances, without adding them to the identity map.
   * You can disable merging by passing the options `{ mergeResults: false }`.
   * With `mergeResults` disabled, to-many collections will contain at most one item, and you will get duplicate
   * root entities when there are multiple items in the populated collection.
   * This is useful for processing large datasets without loading everything into memory at once.
   *
   * ```ts
   * const stream = em.stream(Book, { populate: ['author'] });
   *
   * for await (const book of stream) {
   *   // book is an instance of Book entity
   *   console.log(book.title, book.author.name);
   * }
   * ```
   */
  async *stream(entityName, options = {}) {
    const em = this.getContext();
    em.prepareOptions(options);
    options.strategy = 'joined';
    await em.tryFlush(entityName, options);
    const where = await em.processWhere(entityName, options.where ?? {}, options, 'read');
    validateParams(where);
    options.orderBy = options.orderBy || {};
    options.populate = await em.preparePopulate(entityName, options);
    const meta = this.metadata.get(entityName);
    options = { ...options };
    // save the original hint value so we know it was infer/all
    options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
    options.populateWhere = this.createPopulateWhere({ ...where }, options);
    options.populateFilter = await this.getJoinedFilters(meta, options);
    const stream = em.driver.stream(entityName, where, {
      ctx: em.#transactionContext,
      mapResults: false,
      ...options,
    });
    for await (const data of stream) {
      const fork = em.fork();
      const entity = fork.#entityFactory.create(entityName, data, {
        refresh: options.refresh,
        schema: options.schema,
        convertCustomTypes: true,
      });
      helper(entity).setSerializationContext({
        populate: options.populate,
        fields: options.fields,
        exclude: options.exclude,
      });
      await fork.#unitOfWork.dispatchOnLoadEvent();
      fork.clear();
      yield entity;
    }
  }
  /**
   * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
   */
  async findAll(entityName, options) {
    return this.find(entityName, options?.where ?? {}, options);
  }
  getPopulateWhere(where, options) {
    if (options.populateWhere === undefined) {
      options.populateWhere = this.config.get('populateWhere');
    }
    if (options.populateWhere === PopulateHint.ALL) {
      return { where: {}, populateWhere: options.populateWhere };
    }
    /* v8 ignore next */
    if (options.populateWhere === PopulateHint.INFER) {
      return { where, populateWhere: options.populateWhere };
    }
    return { where: options.populateWhere };
  }
  /**
   * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
   */
  addFilter(options) {
    if (options.entity) {
      options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
    }
    options.default ??= true;
    this.getContext(false).#filters[options.name] = options;
  }
  /**
   * Sets filter parameter values globally inside context defined by this entity manager.
   * If you want to set shared value for all contexts, be sure to use the root entity manager.
   */
  setFilterParams(name, args) {
    this.getContext().#filterParams[name] = args;
  }
  /**
   * Returns filter parameters for given filter set in this context.
   */
  getFilterParams(name) {
    return this.getContext().#filterParams[name];
  }
  /**
   * Sets logger context for this entity manager.
   */
  setLoggerContext(context) {
    this.getContext().loggerContext = context;
  }
  /**
   * Gets logger context for this entity manager.
   */
  getLoggerContext(options) {
    const em = options?.disableContextResolution ? this : this.getContext();
    em.loggerContext ??= {};
    return em.loggerContext;
  }
  /** Sets the flush mode for this EntityManager. Pass `undefined` to reset to the global default. */
  setFlushMode(flushMode) {
    this.getContext(false).#flushMode = flushMode;
  }
  async processWhere(entityName, where, options, type) {
    where = QueryHelper.processWhere({
      where,
      entityName,
      metadata: this.metadata,
      platform: this.driver.getPlatform(),
      convertCustomTypes: options.convertCustomTypes,
      aliased: type === 'read',
    });
    where = await this.applyFilters(entityName, where, options.filters ?? {}, type, options);
    where = this.applyDiscriminatorCondition(entityName, where);
    return where;
  }
  async processUnionWhere(entityName, options, type) {
    if (options.unionWhere?.length) {
      if (!this.driver.getPlatform().supportsUnionWhere()) {
        throw new Error(`unionWhere is only supported on SQL drivers`);
      }
      options.unionWhere = await Promise.all(
        options.unionWhere.map(branch => this.processWhere(entityName, branch, options, type)),
      );
    }
  }
  // this method only handles the problem for mongo driver, SQL drivers have their implementation inside QueryBuilder
  applyDiscriminatorCondition(entityName, where) {
    const meta = this.metadata.find(entityName);
    if (meta?.root.inheritanceType !== 'sti' || !meta?.discriminatorValue) {
      return where;
    }
    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);
    /* v8 ignore next */
    where[meta.root.discriminatorColumn] =
      children.length > 0
        ? { $in: [meta.discriminatorValue, ...children.map(c => c.discriminatorValue)] }
        : meta.discriminatorValue;
    return where;
  }
  createPopulateWhere(cond, options) {
    const ret = {};
    const populateWhere = options.populateWhere ?? this.config.get('populateWhere');
    if (populateWhere === PopulateHint.INFER) {
      Utils.merge(ret, cond);
    } else if (typeof populateWhere === 'object') {
      Utils.merge(ret, populateWhere);
    }
    return ret;
  }
  async getJoinedFilters(meta, options) {
    // If user provided populateFilter, merge it with computed filters
    const userFilter = options.populateFilter;
    if (!this.config.get('filtersOnRelations') || !options.populate) {
      return userFilter;
    }
    const ret = {};
    for (const hint of options.populate) {
      const field = hint.field.split(':')[0];
      const prop = meta.properties[field];
      const strategy = getLoadingStrategy(
        prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'),
        prop.kind,
      );
      const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
      if (!joined && !hint.filter) {
        continue;
      }
      const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
      const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
        ...options,
        populate: hint.children,
      });
      const where2 = await this.getJoinedFilters(prop.targetMeta, {
        ...options,
        filters,
        populate: hint.children,
        populateWhere: PopulateHint.ALL,
      });
      if (Utils.hasObjectKeys(where)) {
        ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
      }
      if (where2 && Utils.hasObjectKeys(where2)) {
        if (ret[field]) {
          Utils.merge(ret[field], where2);
        } else {
          ret[field] = where2;
        }
      }
    }
    // Merge user-provided populateFilter with computed filters
    if (userFilter) {
      Utils.merge(ret, userFilter);
    }
    return Utils.hasObjectKeys(ret) ? ret : undefined;
  }
  /**
   * When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
   */
  async autoJoinRefsForFilters(meta, options, parent) {
    if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
      return;
    }
    const ret = options.populate;
    for (const prop of meta.relations) {
      if (
        prop.object ||
        ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
        !(
          (options.fields?.length ?? 0) === 0 ||
          options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`))
        ) ||
        (parent?.class === prop.targetMeta.root.class && parent.propName === prop.inversedBy)
      ) {
        continue;
      }
      options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
      const cond = await this.applyFilters(prop.targetMeta.class, {}, options.filters, 'read', options);
      if (!Utils.isEmpty(cond)) {
        const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
        let found = false;
        for (const hint of populated) {
          if (!hint.all) {
            hint.filter = true;
          }
          const strategy = getLoadingStrategy(
            prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'),
            prop.kind,
          );
          if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
            found = true;
          }
        }
        if (!found) {
          ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
        }
      }
    }
    for (const hint of ret) {
      const [field, ref] = hint.field.split(':');
      const prop = meta?.properties[field];
      if (prop && !ref) {
        hint.children ??= [];
        await this.autoJoinRefsForFilters(
          prop.targetMeta,
          { ...options, populate: hint.children },
          { class: meta.root.class, propName: prop.name },
        );
      }
    }
  }
  /**
   * @internal
   */
  async applyFilters(entityName, where, options, type, findOptions) {
    const meta = this.metadata.get(entityName);
    const filters = [];
    const ret = [];
    const active = new Set();
    const push = source => {
      const activeFilters = QueryHelper.getActiveFilters(meta, options, source).filter(f => !active.has(f.name));
      filters.push(...activeFilters);
      activeFilters.forEach(f => active.add(f.name));
    };
    push(this.config.get('filters'));
    push(this.#filters);
    push(meta.filters);
    if (filters.length === 0) {
      return where;
    }
    for (const filter of filters) {
      let cond;
      if (filter.cond instanceof Function) {
        // @ts-ignore
        // oxfmt-ignore
        const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().#filterParams[filter.name];
        if (!args && filter.cond.length > 0 && filter.args !== false) {
          throw new Error(`No arguments provided for filter '${filter.name}'`);
        }
        cond = await filter.cond(args, type, this, findOptions, Utils.className(entityName));
      } else {
        cond = filter.cond;
      }
      cond = QueryHelper.processWhere({
        where: cond,
        entityName,
        metadata: this.metadata,
        platform: this.driver.getPlatform(),
        aliased: type === 'read',
      });
      if (filter.strict) {
        Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
      }
      ret.push(cond);
    }
    const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
    return conds.length > 1 ? { $and: conds } : conds[0];
  }
  /**
   * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
   * where the first element is the array of entities, and the second is the count.
   */
  async findAndCount(entityName, where, options = {}) {
    const em = this.getContext(false);
    await em.tryFlush(entityName, options);
    options.flushMode = 'commit'; // do not try to auto flush again
    return Promise.all([em.find(entityName, where, options), em.count(entityName, where, options)]);
  }
  /**
   * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as {@apilink Cursor} object.
   * Supports `before`, `after`, `first` and `last` options while disallowing `limit` and `offset`. Explicit `orderBy` option
   * is required.
   *
   * Use `first` and `after` for forward pagination, or `last` and `before` for backward pagination.
   *
   * - `first` and `last` are numbers and serve as an alternative to `offset`, those options are mutually exclusive, use only one at a time
   * - `before` and `after` specify the previous cursor value, it can be one of the:
   *     - `Cursor` instance
   *     - opaque string provided by `startCursor/endCursor` properties
   *     - POJO/entity instance
   *
   * ```ts
   * const currentCursor = await em.findByCursor(User, {
   *   first: 10,
   *   after: previousCursor, // cursor instance
   *   orderBy: { id: 'desc' },
   * });
   *
   * // to fetch next page
   * const nextCursor = await em.findByCursor(User, {
   *   first: 10,
   *   after: currentCursor.endCursor, // opaque string
   *   orderBy: { id: 'desc' },
   * });
   *
   * // to fetch next page
   * const nextCursor2 = await em.findByCursor(User, {
   *   first: 10,
   *   after: { id: lastSeenId }, // entity-like POJO
   *   orderBy: { id: 'desc' },
   * });
   * ```
   *
   * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
   * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
   * of pages.
   *
   * The `Cursor` object provides the following interface:
   *
   * ```ts
   * Cursor<User> {
   *   items: [
   *     User { ... },
   *     User { ... },
   *     User { ... },
   *   ],
   *   totalCount: 50, // not included if `includeCount: false`
   *   startCursor: 'WzRd',
   *   endCursor: 'WzZd',
   *   hasPrevPage: true,
   *   hasNextPage: true,
   * }
   * ```
   */
  async findByCursor(entityName, options) {
    const em = this.getContext(false);
    options.overfetch ??= true;
    options.where ??= {};
    if (Utils.isEmpty(options.orderBy) && !Raw.hasObjectFragments(options.orderBy)) {
      throw new Error('Explicit `orderBy` option required');
    }
    const [entities, count] =
      options.includeCount !== false
        ? await em.findAndCount(entityName, options.where, options)
        : [await em.find(entityName, options.where, options)];
    return new Cursor(entities, count, options, this.metadata.get(entityName));
  }
  /**
   * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
   * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
   * in database, the method throws an error just like `em.findOneOrFail()` (and respects the same config options).
   */
  async refreshOrFail(entity, options = {}) {
    const ret = await this.refresh(entity, options);
    if (!ret) {
      options.failHandler ??= this.config.get('findOneOrFailHandler');
      const wrapped = helper(entity);
      const where = wrapped.getPrimaryKey();
      throw options.failHandler(wrapped.__meta.className, where);
    }
    return ret;
  }
  /**
   * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
   * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
   * in database, the method returns `null`.
   */
  async refresh(entity, options = {}) {
    const fork = this.fork({ keepTransactionContext: true });
    const wrapped = helper(entity);
    const reloaded = await fork.findOne(wrapped.__meta.class, entity, {
      schema: wrapped.__schema,
      ...options,
      flushMode: FlushMode.COMMIT,
    });
    const em = this.getContext();
    if (!reloaded) {
      em.#unitOfWork.unsetIdentity(entity);
      return null;
    }
    let found = false;
    for (const e of fork.#unitOfWork.getIdentityMap()) {
      const ref = em.getReference(e.constructor, helper(e).getPrimaryKey());
      const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true, convertCustomTypes: false });
      em.config
        .getHydrator(this.metadata)
        .hydrate(ref, helper(ref).__meta, data, em.#entityFactory, 'full', false, false);
      Utils.merge(helper(ref).__originalEntityData, this.#comparator.prepareEntity(e));
      found ||= ref === entity;
    }
    if (!found) {
      const data = helper(reloaded).serialize({
        ignoreSerializers: true,
        includeHidden: true,
        convertCustomTypes: true,
      });
      em.config
        .getHydrator(this.metadata)
        .hydrate(entity, wrapped.__meta, data, em.#entityFactory, 'full', false, true);
      Utils.merge(wrapped.__originalEntityData, this.#comparator.prepareEntity(reloaded));
    }
    return entity;
  }
  /**
   * Finds first entity matching your `where` query.
   */
  async findOne(entityName, where, options = {}) {
    if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
      const em = this.getContext(false);
      const fork = em.fork({ keepTransactionContext: true });
      const ret = await fork.findOne(entityName, where, { ...options, disableIdentityMap: false });
      fork.clear();
      return ret;
    }
    const em = this.getContext();
    em.prepareOptions(options);
    let entity = em.#unitOfWork.tryGetById(entityName, where, options.schema);
    // query for a not managed entity which is already in the identity map as it
    // was provided with a PK this entity does not exist in the db, there can't
    // be any relations to it, so no need to deal with the populate hint
    if (entity && !helper(entity).__managed) {
      return entity;
    }
    await em.tryFlush(entityName, options);
    const meta = em.metadata.get(entityName);
    where = await em.processWhere(entityName, where, options, 'read');
    validateEmptyWhere(where);
    em.checkLockRequirements(options.lockMode, meta);
    const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
    if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
      return em.lockAndPopulate(meta, entity, where, options);
    }
    validateParams(where);
    options.populate = await em.preparePopulate(entityName, options);
    const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
    const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
    if (cached?.data !== undefined) {
      if (cached.data) {
        await em.#entityLoader.populate(entityName, [cached.data], options.populate, {
          ...options,
          ...em.getPopulateWhere(where, options),
          ignoreLazyScalarProperties: true,
          lookup: false,
        });
      }
      return cached.data;
    }
    options = { ...options };
    // save the original hint value so we know it was infer/all
    options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
    options.populateWhere = this.createPopulateWhere({ ...where }, options);
    options.populateFilter = await this.getJoinedFilters(meta, options);
    await em.processUnionWhere(entityName, options, 'read');
    const data = await em.driver.findOne(entityName, where, {
      ctx: em.#transactionContext,
      em,
      ...options,
    });
    if (!data) {
      await em.storeCache(options.cache, cached, null);
      return null;
    }
    entity = em.#entityFactory.create(entityName, data, {
      merge: true,
      refresh: options.refresh,
      schema: options.schema,
      convertCustomTypes: true,
    });
    await em.lockAndPopulate(meta, entity, where, options);
    await em.#unitOfWork.dispatchOnLoadEvent();
    await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
    return entity;
  }
  /**
   * Finds first entity matching your `where` query. If nothing found, it will throw an error.
   * If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
   * You can override the factory for creating this method via `options.failHandler` locally
   * or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
   */
  async findOneOrFail(entityName, where, options = {}) {
    let entity;
    let isStrictViolation = false;
    if (options.strict) {
      const ret = await this.find(entityName, where, { ...options, limit: 2 });
      isStrictViolation = ret.length !== 1;
      entity = ret[0];
    } else {
      entity = await this.findOne(entityName, where, options);
    }
    if (!entity || isStrictViolation) {
      const key = options.strict ? 'findExactlyOneOrFailHandler' : 'findOneOrFailHandler';
      options.failHandler ??= this.config.get(key);
      const name = Utils.className(entityName);
      /* v8 ignore next */
      where = Utils.isEntity(where) ? helper(where).getPrimaryKey() : where;
      throw options.failHandler(name, where);
    }
    return entity;
  }
  /**
   * Creates or updates the entity, based on whether it is already present in the database.
   * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
   * entity instance. The method accepts either `entityName` together with the entity `data`, or just entity instance.
   *
   * ```ts
   * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41
   * const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 });
   * ```
   *
   * The entity data needs to contain either the primary key, or any other unique property. Let's consider the following example, where `Author.email` is a unique property:
   *
   * ```ts
   * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41
   * // select "id" from "author" where "email" = 'foo@bar.com'
   * const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 });
   * ```
   *
   * Depending on the driver support, this will either use a returning query, or a separate select query, to fetch the primary key if it's missing from the `data`.
   *
   * If the entity is already present in current context, there won't be any queries - instead, the entity data will be assigned and an explicit `flush` will be required for those changes to be persisted.
   */
  async upsert(entityNameOrEntity, data, options = {}) {
    if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
      const em = this.getContext(false);
      const fork = em.fork({ keepTransactionContext: true });
      const ret = await fork.upsert(entityNameOrEntity, data, { ...options, disableIdentityMap: false });
      fork.clear();
      return ret;
    }
    const em = this.getContext(false);
    em.prepareOptions(options);
    let entityName;
    let where;
    let entity = null;
    if (data === undefined) {
      entityName = entityNameOrEntity.constructor;
      data = entityNameOrEntity;
    } else {
      entityName = entityNameOrEntity;
    }
    const meta = this.metadata.get(entityName);
    const convertCustomTypes = !Utils.isEntity(data);
    if (Utils.isEntity(data)) {
      entity = data;
      if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
        em.#entityFactory.mergeData(meta, entity, data, { initialized: true });
        return entity;
      }
      where = helper(entity).getPrimaryKey();
      data = em.#comparator.prepareEntity(entity);
    } else {
      data = Utils.copy(QueryHelper.processParams(data));
      where = Utils.extractPK(data, meta);
      if (where && !this.config.get('upsertManaged')) {
        const exists = em.#unitOfWork.getById(entityName, where, options.schema);
        if (exists) {
          return em.assign(exists, data);
        }
      }
    }
    where = getWhereCondition(meta, options.onConflictFields, data, where).where;
    data = QueryHelper.processObjectParams(data);
    validateParams(data, 'insert data');
    if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
      await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta);
    }
    const ret = await em.driver.nativeUpdate(entityName, where, data, {
      ctx: em.#transactionContext,
      upsert: true,
      convertCustomTypes,
      ...options,
    });
    em.#unitOfWork.getChangeSetPersister().mapReturnedValues(entity, data, ret.row, meta, true);
    entity ??= em.#entityFactory.create(entityName, data, {
      refresh: true,
      initialized: true,
      schema: options.schema,
    });
    const uniqueFields =
      options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where) : meta.primaryKeys);
    const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
    if (
      options.onConflictAction === 'ignore' ||
      !helper(entity).hasPrimaryKey() ||
      (returning.length > 0 && !(this.getPlatform().usesReturningStatement() && ret.row))
    ) {
      const where = {};
      if (Array.isArray(uniqueFields)) {
        for (const prop of uniqueFields) {
          if (data[prop] != null) {
            where[prop] = data[prop];
          } else if (meta.primaryKeys.includes(prop) && ret.insertId != null) {
            where[prop] = ret.insertId;
          }
        }
      } else {
        Object.keys(data).forEach(prop => {
          where[prop] = data[prop];
        });
        if (meta.simplePK && ret.insertId != null) {
          where[meta.primaryKeys[0]] = ret.insertId;
        }
      }
      const data2 = await this.driver.findOne(meta.class, where, {
        fields: returning,
        ctx: em.#transactionContext,
        convertCustomTypes: true,
        connectionType: 'write',
        schema: options.schema,
      });
      em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
    }
    // recompute the data as there might be some values missing (e.g. those with db column defaults)
    const snapshot = this.#comparator.prepareEntity(entity);
    em.#unitOfWork.register(entity, snapshot, { refresh: true });
    if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
      await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
    }
    return entity;
  }
  /**
   * Creates or updates the entity, based on whether it is already present in the database.
   * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
   * entity instance. The method accepts either `entityName` together with the entity `data`, or just entity instance.
   *
   * ```ts
   * // insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 41
   * const authors = await em.upsertMany(Author, [{ email: 'foo@bar.com', age: 33 }, ...]);
   * ```
   *
   * The entity data needs to contain either the primary key, or any other unique property. Let's consider the following example, where `Author.email` is a unique property:
   *
   * ```ts
   * // insert into "author" ("age", "email") values (33, 'foo@bar.com'), (666, 'lol@lol.lol') on conflict ("email") do update set "age" = excluded."age"
   * // select "id" from "author" where "email" = 'foo@bar.com'
   * const author = await em.upsertMany(Author, [
   *   { email: 'foo@bar.com', age: 33 },
   *   { email: 'lol@lol.lol', age: 666 },
   * ]);
   * ```
   *
   * Depending on the driver support, this will either use a returning query, or a separate select query, to fetch the primary key if it's missing from the `data`.
   *
   * If the entity is already present in current context, there won't be any queries - instead, the entity data will be assigned and an explicit `flush` will be required for those changes to be persisted.
   */
  async upsertMany(entityNameOrEntity, data, options = {}) {
    if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
      const em = this.getContext(false);
      const fork = em.fork({ keepTransactionContext: true });
      const ret = await fork.upsertMany(entityNameOrEntity, data, { ...options, disableIdentityMap: false });
      fork.clear();
      return ret;
    }
    const em = this.getContext(false);
    em.prepareOptions(options);
    let entityName;
    let propIndex;
    if (data === undefined) {
      entityName = entityNameOrEntity[0].constructor;
      data = entityNameOrEntity;
    } else {
      entityName = entityNameOrEntity;
    }
    const batchSize = options.batchSize ?? this.config.get('batchSize');
    if (data.length > batchSize) {
      const ret = [];
      for (let i = 0; i < data.length; i += batchSize) {
        const chunk = data.slice(i, i + batchSize);
        ret.push(...(await this.upsertMany(entityName, chunk, options)));
      }
      return ret;
    }
    const meta = this.metadata.get(entityName);
    const convertCustomTypes = !Utils.isEntity(data[0]);
    const allData = [];
    const allWhere = [];
    const entities = new Map();
    const entitiesByData = new Map();
    for (let i = 0; i < data.length; i++) {
      let row = data[i];
      let where;
      if (Utils.isEntity(row)) {
        const entity = row;
        if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
          em.#entityFactory.mergeData(meta, entity, row, { initialized: true });
          entities.set(entity, row);
          entitiesByData.set(row, entity);
          continue;
        }
        where = helper(entity).getPrimaryKey();
        row = em.#comparator.prepareEntity(entity);
      } else {
        row = data[i] = Utils.copy(QueryHelper.processParams(row));
        where = Utils.extractPK(row, meta);
        if (where && !this.config.get('upsertManaged')) {
          const exists = em.#unitOfWork.getById(entityName, where, options.schema);
          if (exists) {
            em.assign(exists, row);
            entities.set(exists, row);
            entitiesByData.set(row, exists);
            continue;
          }
        }
      }
      const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
      propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
      const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
      propIndex = tmp.propIndex;
      where = QueryHelper.processWhere({
        where: tmp.where,
        entityName,
        metadata: this.metadata,
        platform: this.getPlatform(),
      });
      row = QueryHelper.processObjectParams(row);
      validateParams(row, 'insert data');
      allData.push(row);
      allWhere.push(where);
    }
    if (entities.size === data.length) {
      return [...entities.keys()];
    }
    if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
      for (const dto of data) {
        const entity = entitiesByData.get(dto) ?? dto;
        await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
      }
    }
    const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
      ctx: em.#transactionContext,
      upsert: true,
      convertCustomTypes,
      ...options,
    });
    entities.clear();
    entitiesByData.clear();
    const loadPK = new Map();
    allData.forEach((row, i) => {
      em.#unitOfWork
        .getChangeSetPersister()
        .mapReturnedValues(
          Utils.isEntity(data[i]) ? data[i] : null,
          Utils.isEntity(data[i]) ? {} : data[i],
          res.rows?.[i],
          meta,
          true,
        );
      const entity = Utils.isEntity(data[i])
        ? data[i]
        : em.#entityFactory.create(entityName, row, {
            refresh: true,
            initialized: true,
            schema: options.schema,
          });
      if (!helper(entity).hasPrimaryKey()) {
        loadPK.set(entity, allWhere[i]);
      }
      entities.set(entity, row);
      entitiesByData.set(row, entity);
    });
    // skip if we got the PKs via returning statement (`rows`)
    // oxfmt-ignore
    const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
    const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
    const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);
    if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
      const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
      const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []);
      for (const cond of loadPK.values()) {
        Utils.keys(cond).forEach(key => add.add(key));
      }
      const where = { $or: [] };
      data.forEach((item, idx) => {
        where.$or[idx] = {};
        const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
        props.forEach(prop => {
          where.$or[idx][prop] = item[prop];
        });
      });
      const data2 = await this.driver.find(meta.class, where, {
        fields: returning.concat(...add).concat(...(Array.isArray(uniqueFields) ? uniqueFields : [])),
        ctx: em.#transactionContext,
        convertCustomTypes: true,
        connectionType: 'write',
        schema: options.schema,
      });
      for (const [entity, cond] of loadPK.entries()) {
        const row = data2.find(row => {
          const tmp = {};
          add.forEach(k => {
            if (!meta.properties[k]?.primary) {
              tmp[k] = row[k];
            }
          });
          return this.#comparator.matching(entityName, cond, tmp);
        });
        /* v8 ignore next */
        if (!row) {
          throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
        }
        em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full', false, true);
      }
      if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
        for (let i = 0; i < allData.length; i++) {
          const data = allData[i];
          const cond = uniqueFields.reduce((a, b) => {
            // @ts-ignore
            a[b] = data[b];
            return a;
          }, {});
          const entity = entitiesByData.get(data);
          const row = data2.find(item => {
            const pk = uniqueFields.reduce((a, b) => {
              // @ts-ignore
              a[b] = item[b];
              return a;
            }, {});
            return this.#comparator.matching(entityName, cond, pk);
          });
          /* v8 ignore next */
          if (!row) {
            throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
          }
          em.getHydrator().hydrate(entity, meta, row, em.#entityFactory, 'full');
        }
      }
    }
    for (const [entity] of entities) {
      // recompute the data as there might be some values missing (e.g. those with db column defaults)
      const snapshot = this.#comparator.prepareEntity(entity);
      em.#unitOfWork.register(entity, snapshot, { refresh: true });
    }
    if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
      for (const [entity] of entities) {
        await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
      }
    }
    return [...entities.keys()];
  }
  /**
   * Runs your callback wrapped inside a database transaction.
   *
   * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
   * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
   * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
   * method automatically creates an async context for the transaction.
   *
   * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
   * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
   * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
   * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
   *
   * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
   * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
   * and then call this method on the fork.
   *
   * **Example:**
   * ```ts
   * await em.transactional(async (em) => {
   *   const author = new Author('Jon');
   *   em.persist(author);
   *   // flush is called automatically at the end of the callback
   * });
   * ```
   */
  async transactional(cb, options = {}) {
    const em = this.getContext(false);
    if (this.#disableTransactions || em.#disableTransactions) {
      return cb(em);
    }
    const manager = new TransactionManager(this);
    return manager.handle(cb, options);
  }
  /**
   * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
   */
  async begin(options = {}) {
    if (this.#disableTransactions) {
      return;
    }
    const em = this.getContext(false);
    em.#transactionContext = await em.getConnection('write').begin({
      ...options,
      eventBroadcaster: new TransactionEventBroadcaster(em, { topLevelTransaction: !options.ctx }),
    });
  }
  /**
   * Commits the transaction bound to this EntityManager. Flushes before doing the actual commit query.
   */
  async commit() {
    const em = this.getContext(false);
    if (this.#disableTransactions) {
      await em.flush();
      return;
    }
    if (!em.#transactionContext) {
      throw ValidationError.transactionRequired();
    }
    await em.flush();
    await em.getConnection('write').commit(em.#transactionContext, new TransactionEventBroadcaster(em));
    em.#transactionContext = undefined;
  }
  /**
   * Rollbacks the transaction bound to this EntityManager.
   */
  async rollback() {
    if (this.#disableTransactions) {
      return;
    }
    const em = this.getContext(false);
    if (!em.#transactionContext) {
      throw ValidationError.transactionRequired();
    }
    await em.getConnection('write').rollback(em.#transactionContext, new TransactionEventBroadcaster(em));
    em.#transactionContext = undefined;
    em.#unitOfWork.clearActionsQueue();
  }
  /**
   * Runs your callback wrapped inside a database transaction.
   */
  async lock(entity, lockMode, options = {}) {
    options = Utils.isPlainObject(options) ? options : { lockVersion: options };
    await this.getUnitOfWork().lock(entity, { lockMode, ...options });
  }
  /**
   * Fires native insert query. Calling this has no side effects on the context (identity map).
   */
  async insert(entityNameOrEntity, data, options = {}) {
    const em = this.getContext(false);
    em.prepareOptions(options);
    let entityName;
    if (data === undefined) {
      entityName = entityNameOrEntity.constructor;
      data = entityNameOrEntity;
    } else {
      entityName = entityNameOrEntity;
    }
    if (Utils.isEntity(data)) {
      if (options.schema && helper(data).getSchema() == null) {
        helper(data).setSchema(options.schema);
      }
      if (!helper(data).__managed) {
        // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
        em.#unitOfWork.getPersistStack().delete(data);
        // it can be also in the identity map if it had a PK value already
        em.#unitOfWork.unsetIdentity(data);
      }
      const meta = helper(data).__meta;
      const payload = em.#comparator.prepareEntity(data);
      const cs = new ChangeSet(data, ChangeSetType.CREATE, payload, meta);
      await em.#unitOfWork.getChangeSetPersister().executeInserts([cs], { ctx: em.#transactionContext, ...options });
      return cs.getPrimaryKey();
    }
    data = QueryHelper.processObjectParams(data);
    validateParams(data, 'insert data');
    const res = await em.driver.nativeInsert(entityName, data, {
      ctx: em.#transactionContext,
      ...options,
    });
    return res.insertId;
  }
  /**
   * Fires native multi-insert query. Calling this has no side effects on the context (identity map).
   */
  async insertMany(entityNameOrEntities, data, options = {}) {
    const em = this.getContext(false);
    em.prepareOptions(options);
    let entityName;
    if (data === undefined) {
      entityName = entityNameOrEntities[0].constructor;
      data = entityNameOrEntities;
    } else {
      entityName = entityNameOrEntities;
    }
    if (data.length === 0) {
      return [];
    }
    if (Utils.isEntity(data[0])) {
      const meta = helper(data[0]).__meta;
      const css = data.map(row => {
        if (options.schema && helper(row).getSchema() == null) {
          helper(row).setSchema(options.schema);
        }
        if (!helper(row).__managed) {
          // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
          em.#unitOfWork.getPersistStack().delete(row);
          // it can be also in the identity map if it had a PK value already
          em.#unitOfWork.unsetIdentity(row);
        }
        const payload = em.#comparator.prepareEntity(row);
        return new ChangeSet(row, ChangeSetType.CREATE, payload, meta);
      });
      await em.#unitOfWork.getChangeSetPersister().executeInserts(css, { ctx: em.#transactionContext, ...options });
      return css.map(cs => cs.getPrimaryKey());
    }
    data = data.map(row => QueryHelper.processObjectParams(row));
    data.forEach(row => validateParams(row, 'insert data'));
    const res = await em.driver.nativeInsertMany(entityName, data, {
      ctx: em.#transactionContext,
      ...options,
    });
    if (res.insertedIds) {
      return res.insertedIds;
    }
    return [res.insertId];
  }
  /**
   * Fires native update query. Calling this has no side effects on the context (identity map).
   */
  async nativeUpdate(entityName, where, data, options = {}) {
    const em = this.getContext(false);
    em.prepareOptions(options);
    await em.processUnionWhere(entityName, options, 'update');
    data = QueryHelper.processObjectParams(data);
    where = await em.processWhere(entityName, where, { ...options, convertCustomTypes: false }, 'update');
    validateParams(data, 'update data');
    validateParams(where, 'update condition');
    const res = await em.driver.nativeUpdate(entityName, where, data, {
      ctx: em.#transactionContext,
      em,
      ...options,
    });
    return res.affectedRows;
  }
  /**
   * Fires native delete query. Calling this has no side effects on the context (identity map).
   */
  async nativeDelete(entityName, where, options = {}) {
    const em = this.getContext(false);
    em.prepareOptions(options);
    await em.processUnionWhere(entityName, options, 'delete');
    where = await em.processWhere(entityName, where, options, 'delete');
    validateParams(where, 'delete condition');
    const res = await em.driver.nativeDelete(entityName, where, {
      ctx: em.#transactionContext,
      em,
      ...options,
    });
    return res.affectedRows;
  }
  /**
   * Maps raw database result to an entity and merges it to this EntityManager.
   */
  map(entityName, result, options = {}) {
    const meta = this.metadata.get(entityName);
    const data = this.driver.mapResult(result, meta);
    for (const k of Object.keys(data)) {
      const prop = meta.properties[k];
      if (
        prop?.kind === ReferenceKind.SCALAR &&
        SCALAR_TYPES.has(prop.runtimeType) &&
        !prop.customType &&
        (prop.setter || !prop.getter)
      ) {
        validateProperty(prop, data[k], data);
      }
    }
    return this.merge(entityName, data, {
      convertCustomTypes: true,
      refresh: true,
      validate: false,
      ...options,
    });
  }
  /**
   * Merges given entity to this EntityManager so it becomes managed. You can force refreshing of existing entities
   * via second parameter. By default, it will return already loaded entities without modifying them.
   */
  merge(entityName, data, options = {}) {
    if (Utils.isEntity(entityName)) {
      return this.merge(entityName.constructor, entityName, data);
    }
    const em = options.disableContextResolution ? this : this.getContext();
    options.schema ??= em.#schema;
    options.validate ??= true;
    options.cascade ??= true;
    validatePrimaryKey(data, em.metadata.get(entityName));
    let entity = em.#unitOfWork.tryGetById(entityName, data, options.schema, false);
    if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
      return entity;
    }
    const dataIsEntity = Utils.isEntity(data);
    entity = dataIsEntity ? data : em.#entityFactory.create(entityName, data, { merge: true, ...options });
    const visited = options.cascade ? undefined : new Set([entity]);
    em.#unitOfWork.merge(entity, visited);
    return entity;
  }
  /**
   * Creates new instance of given entity and populates it with given data.
   * The entity constructor will be used unless you provide `{ managed: true }` in the `options` parameter.
   * The constructor will be given parameters based on the defined constructor of the entity. If the constructor
   * parameter matches a property name, its value will be extracted from `data`. If no matching property exists,
   * the whole `data` parameter will be passed. This means we can also define `constructor(data: Partial<T>)` and
   * `em.create()` will pass the data into it (unless we have a property named `data` too).
   *
   * The parameters are strictly checked, you need to provide all required properties. You can use `OptionalProps`
   * symbol to omit some properties from this check without making them optional. Alternatively, use `partial: true`
   * in the options to disable the strict checks for required properties. This option has no effect on runtime.
   *
   * The newly created entity will be automatically marked for persistence via `em.persist` unless you disable this
   * behavior, either locally via `persist: false` option, or globally via `persistOnCreate` ORM config option.
   */
  create(entityName, data, options = {}) {
    const em = this.getContext();
    options.schema ??= em.#schema;
    const entity = em.#entityFactory.create(entityName, data, {
      ...options,
      newEntity: !options.managed,
      merge: options.managed,
      normalizeAccessors: true,
    });
    options.persist ??= em.config.get('persistOnCreate');
    if (options.persist && !this.getMetadata(entityName).embeddable) {
      em.persist(entity);
    }
    return entity;
  }
  /**
   * Shortcut for `wrap(entity).assign(data, { em })`
   */
  assign(entity, data, options = {}) {
    return EntityAssigner.assign(entity, data, { em: this.getContext(), ...options });
  }
  /**
   * Gets a reference to the entity identified by the given type and identifier without actually loading it, if the entity is not yet loaded
   */
  getReference(entityName, id, options = {}) {
    options.schema ??= this.schema;
    options.convertCustomTypes ??= false;
    const meta = this.metadata.get(entityName);
    if (Utils.isPrimaryKey(id)) {
      if (meta.compositePK) {
        throw ValidationError.invalidCompositeIdentifier(meta);
      }
      id = [id];
    }
    const entity = this.getEntityFactory().createReference(entityName, id, { merge: true, ...options });
    if (options.wrapped) {
      return Reference.create(entity);
    }
    return entity;
  }
  /**
   * Returns total number of entities matching your `where` query.
   */
  async count(entityName, where = {}, options = {}) {
    const em = this.getContext(false);
    // Shallow copy options since the object will be modified when deleting orderBy
    options = { ...options };
    em.prepareOptions(options);
    await em.tryFlush(entityName, options);
    where = await em.processWhere(entityName, where, options, 'read');
    options.populate = await em.preparePopulate(entityName, options);
    options = { ...options };
    // save the original hint value so we know it was infer/all
    const meta = em.metadata.find(entityName);
    options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
    options.populateWhere = this.createPopulateWhere({ ...where }, options);
    options.populateFilter = await this.getJoinedFilters(meta, options);
    validateParams(where);
    delete options.orderBy;
    await em.processUnionWhere(entityName, options, 'read');
    const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
    const cached = await em.tryCache(entityName, options.cache, cacheKey);
    if (cached?.data !== undefined) {
      return cached.data;
    }
    const count = await em.driver.count(entityName, where, { ctx: em.#transactionContext, em, ...options });
    await em.storeCache(options.cache, cached, () => +count);
    return +count;
  }
  /**
   * Tells the EntityManager to make an instance managed and persistent.
   * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
   */
  persist(entity) {
    const em = this.getContext();
    if (Utils.isEntity(entity)) {
      // do not cascade just yet, cascading of entities in persist stack is done when flushing
      em.#unitOfWork.persist(entity, undefined, { cascade: false });
      return em;
    }
    const entities = Utils.asArray(entity);
    for (const ent of entities) {
      if (!Utils.isEntity(ent, true)) {
        /* v8 ignore next */
        const meta = typeof ent === 'object' ? em.metadata.find(ent.constructor) : undefined;
        throw ValidationError.notDiscoveredEntity(ent, meta);
      }
      // do not cascade just yet, cascading of entities in persist stack is done when flushing
      em.#unitOfWork.persist(Reference.unwrapReference(ent), undefined, { cascade: false });
    }
    return this;
  }
  /**
   * Marks entity for removal.
   * A removed entity will be removed from the database at or before transaction commit or as a result of the flush operation.
   *
   * To remove entities by condition, use `em.nativeDelete()`.
   */
  remove(entity) {
    const em = this.getContext();
    if (Utils.isEntity(entity)) {
      // do not cascade just yet, cascading of entities in persist stack is done when flushing
      em.#unitOfWork.remove(entity, undefined, { cascade: false });
      return em;
    }
    const entities = Utils.asArray(entity, true);
    for (const ent of entities) {
      if (!Utils.isEntity(ent, true)) {
        throw new Error(
          `You need to pass entity instance or reference to 'em.remove()'. To remove entities by condition, use 'em.nativeDelete()'.`,
        );
      }
      // do not cascade just yet, cascading of entities in remove stack is done when flushing
      em.#unitOfWork.remove(Reference.unwrapReference(ent), undefined, { cascade: false });
    }
    return em;
  }
  /**
   * Flushes all changes to objects that have been queued up to now to the database.
   * This effectively synchronizes the in-memory state of managed objects with the database.
   */
  async flush() {
    await this.getUnitOfWork().commit();
  }
  /**
   * @internal
   */
  async tryFlush(entityName, options) {
    const em = this.getContext();
    const flushMode = options.flushMode ?? em.#flushMode ?? em.config.get('flushMode');
    const meta = em.metadata.get(entityName);
    if (flushMode === FlushMode.COMMIT) {
      return;
    }
    if (flushMode === FlushMode.ALWAYS || em.getUnitOfWork().shouldAutoFlush(meta)) {
      await em.flush();
    }
  }
  /**
   * Clears the EntityManager. All entities that are currently managed by this EntityManager become detached.
   */
  clear() {
    this.getContext().#unitOfWork.clear();
  }
  /**
   * Checks whether given property can be populated on the entity.
   */
  canPopulate(entityName, property) {
    // eslint-disable-next-line prefer-const
    let [p, ...parts] = property.split('.');
    const meta = this.metadata.find(entityName);
    if (!meta) {
      return true;
    }
    if (p.includes(':')) {
      p = p.split(':', 2)[0];
    }
    // For TPT inheritance, check the entity's own properties, not just the root's
    // For STI, meta.properties includes all properties anyway
    const ret = p in meta.properties;
    if (parts.length > 0) {
      return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
    }
    return ret;
  }
  /**
   * Loads specified relations in batch. This will execute one query for each relation, that will populate it on all the specified entities.
   */
  async populate(entities, populate, options = {}) {
    const arr = Utils.asArray(entities);
    if (arr.length === 0) {
      return entities;
    }
    const em = this.getContext();
    em.prepareOptions(options);
    const entityName = arr[0].constructor;
    const preparedPopulate = await em.preparePopulate(
      entityName,
      { populate: populate, filters: options.filters },
      options.validate,
    );
    await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
    return entities;
  }
  /**
   * Returns new EntityManager instance with its own identity map
   */
  fork(options = {}) {
    const em = options.disableContextResolution ? this : this.getContext(false);
    options.clear ??= true;
    options.useContext ??= false;
    options.freshEventManager ??= false;
    options.cloneEventManager ??= false;
    const eventManager = options.freshEventManager
      ? new EventManager(em.config.get('subscribers'))
      : options.cloneEventManager
        ? em.eventManager.clone()
        : em.eventManager;
    // we need to allow global context here as forking from global EM is fine
    const allowGlobalContext = em.config.get('allowGlobalContext');
    em.config.set('allowGlobalContext', true);
    const fork = new em.constructor(em.config, em.driver, em.metadata, options.useContext, eventManager);
    fork.setFlushMode(options.flushMode ?? em.#flushMode);
    fork.#disableTransactions =
      options.disableTransactions ?? this.#disableTransactions ?? this.config.get('disableTransactions');
    em.config.set('allowGlobalContext', allowGlobalContext);
    if (options.keepTransactionContext) {
      fork.#transactionContext = em.#transactionContext;
    }
    fork.#filters = { ...em.#filters };
    fork.#filterParams = Utils.copy(em.#filterParams);
    fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
    fork.#schema = options.schema ?? em.#schema;
    if (!options.clear) {
      for (const entity of em.#unitOfWork.getIdentityMap()) {
        fork.#unitOfWork.register(entity);
      }
      for (const entity of em.#unitOfWork.getPersistStack()) {
        fork.#unitOfWork.persist(entity);
      }
      for (const entity of em.#unitOfWork.getOrphanRemoveStack()) {
        fork.#unitOfWork.getOrphanRemoveStack().add(entity);
      }
    }
    return fork;
  }
  /**
   * Gets the UnitOfWork used by the EntityManager to coordinate operations.
   */
  getUnitOfWork(useContext = true) {
    if (!useContext) {
      return this.#unitOfWork;
    }
    return this.getContext().#unitOfWork;
  }
  /**
   * Gets the EntityFactory used by the EntityManager.
   */
  getEntityFactory() {
    return this.getContext().#entityFactory;
  }
  /**
   * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
   */
  getEntityLoader() {
    return this.getContext().#entityLoader;
  }
  /**
   * Gets the Hydrator used by the EntityManager.
   */
  getHydrator() {
    return this.config.getHydrator(this.getMetadata());
  }
  /**
   * Gets the EntityManager based on current transaction/request context.
   * @internal
   */
  getContext(validate = true) {
    if (!this.#useContext) {
      return this;
    }
    let em = TransactionContext.getEntityManager(this.name); // prefer the tx context
    if (em) {
      return em;
    }
    // no explicit tx started
    em = this.config.get('context')(this.name) ?? this;
    if (validate && !this.config.get('allowGlobalContext') && em.global) {
      throw ValidationError.cannotUseGlobalContext();
    }
    return em;
  }
  /** Gets the EventManager instance used by this EntityManager. */
  getEventManager() {
    return this.eventManager;
  }
  /**
   * Checks whether this EntityManager is currently operating inside a database transaction.
   */
  isInTransaction() {
    return !!this.getContext(false).#transactionContext;
  }
  /**
   * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
   */
  getTransactionContext() {
    return this.getContext(false).#transactionContext;
  }
  /**
   * Sets the transaction context.
   */
  setTransactionContext(ctx) {
    if (!ctx) {
      this.resetTransactionContext();
    } else {
      this.getContext(false).#transactionContext = ctx;
    }
  }
  /**
   * Resets the transaction context.
   */
  resetTransactionContext() {
    this.getContext(false).#transactionContext = undefined;
  }
  /**
   * Gets the `MetadataStorage` (without parameters) or `EntityMetadata` instance when provided with the `entityName` parameter.
   */
  getMetadata(entityName) {
    if (entityName) {
      return this.metadata.get(entityName);
    }
    return this.metadata;
  }
  /**
   * Gets the EntityComparator.
   */
  getComparator() {
    return this.#comparator;
  }
  checkLockRequirements(mode, meta) {
    if (!mode) {
      return;
    }
    if (mode === LockMode.OPTIMISTIC && !meta.versionProperty) {
      throw OptimisticLockError.notVersioned(meta);
    }
    if ([LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(mode) && !this.isInTransaction()) {
      throw ValidationError.transactionRequired();
    }
  }
  async lockAndPopulate(meta, entity, where, options) {
    if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
      await this.lock(entity, options.lockMode, {
        lockVersion: options.lockVersion,
        lockTableAliases: options.lockTableAliases,
      });
    }
    const preparedPopulate = await this.preparePopulate(meta.class, options);
    await this.#entityLoader.populate(meta.class, [entity], preparedPopulate, {
      ...options,
      ...this.getPopulateWhere(where, options),
      orderBy: options.populateOrderBy ?? options.orderBy,
      ignoreLazyScalarProperties: true,
      lookup: false,
    });
    return entity;
  }
  buildFields(fields) {
    return fields.reduce((ret, f) => {
      if (Utils.isPlainObject(f)) {
        Utils.keys(f).forEach(ff => ret.push(...this.buildFields(f[ff]).map(field => `${ff}.${field}`)));
      } else {
        ret.push(f);
      }
      return ret;
    }, []);
  }
  /** @internal */
  async preparePopulate(entityName, options, validate = true) {
    if (options.populate === false) {
      return [];
    }
    const meta = this.metadata.find(entityName);
    // infer populate hint if only `fields` are available
    if (!options.populate && options.fields) {
      // we need to prune the `populate` hint from to-one relations, as partially loading them does not require their population, we want just the FK
      const pruneToOneRelations = (meta, fields) => {
        const ret = [];
        for (let field of fields) {
          if (field === PopulatePath.ALL || field.startsWith(`${PopulatePath.ALL}.`)) {
            ret.push(
              ...meta.props
                .filter(prop => prop.lazy || [ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind))
                .map(prop => prop.name),
            );
            continue;
          }
          field = field.split(':')[0];
          if (
            !field.includes('.') &&
            ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(meta.properties[field].kind)
          ) {
            ret.push(field);
            continue;
          }
          const parts = field.split('.');
          const key = parts.shift();
          if (parts.length === 0) {
            continue;
          }
          const prop = meta.properties[key];
          if (!prop.targetMeta) {
            ret.push(key);
            continue;
          }
          const inner = pruneToOneRelations(prop.targetMeta, [parts.join('.')]);
          if (inner.length > 0) {
            ret.push(...inner.map(c => `${key}.${c}`));
          }
        }
        return Utils.unique(ret);
      };
      options.populate = pruneToOneRelations(meta, this.buildFields(options.fields));
    }
    if (!options.populate) {
      const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
      await this.autoJoinRefsForFilters(meta, { ...options, populate });
      return populate;
    }
    if (typeof options.populate !== 'boolean') {
      options.populate = Utils.asArray(options.populate)
        .map(field => {
          /* v8 ignore next */
          if (typeof field === 'boolean' || field === PopulatePath.ALL) {
            return [{ field: meta.primaryKeys[0], strategy: options.strategy, all: !!field }]; //
          }
          // will be handled in QueryBuilder when processing the where condition via CriteriaNode
          if (field === PopulatePath.INFER) {
            options.flags ??= [];
            options.flags.push(QueryFlag.INFER_POPULATE);
            return [];
          }
          if (typeof field === 'string') {
            return [{ field, strategy: options.strategy }];
          }
          return [field];
        })
        .flat();
    }
    const populate = this.#entityLoader.normalizePopulate(
      entityName,
      options.populate,
      options.strategy,
      true,
      options.exclude,
    );
    const invalid = populate.find(({ field }) => !this.canPopulate(entityName, field));
    if (validate && invalid) {
      throw ValidationError.invalidPropertyName(entityName, invalid.field);
    }
    await this.autoJoinRefsForFilters(meta, { ...options, populate });
    for (const field of populate) {
      // force select-in strategy when populating all relations as otherwise we could cause infinite loops when self-referencing
      const all = field.all ?? (Array.isArray(options.populate) && options.populate.includes('*'));
      field.strategy = all ? LoadStrategy.SELECT_IN : (options.strategy ?? field.strategy);
    }
    if (options.populateHints) {
      applyPopulateHints(populate, options.populateHints);
    }
    return populate;
  }
  /**
   * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
   * some additional lazy properties, if so, we reload and merge the data from database
   */
  shouldRefresh(meta, entity, options) {
    if (!helper(entity).__initialized || options.refresh) {
      return true;
    }
    let autoRefresh;
    if (options.fields) {
      autoRefresh = options.fields.some(field => !helper(entity).__loadedProperties.has(field));
    } else {
      autoRefresh = meta.comparableProps.some(prop => {
        const inlineEmbedded = prop.kind === ReferenceKind.EMBEDDED && !prop.object;
        return !inlineEmbedded && !prop.lazy && !helper(entity).__loadedProperties.has(prop.name);
      });
    }
    if (autoRefresh || options.filters) {
      return true;
    }
    if (Array.isArray(options.populate)) {
      return options.populate.some(field => !helper(entity).__loadedProperties.has(field));
    }
    return !!options.populate;
  }
  prepareOptions(options) {
    if (!Utils.isEmpty(options.fields) && !Utils.isEmpty(options.exclude)) {
      throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
    }
    options.schema ??= this.#schema;
    options.logging = options.loggerContext = Utils.merge(
      { id: this.id },
      this.loggerContext,
      options.loggerContext,
      options.logging,
    );
  }
  /**
   * @internal
   */
  cacheKey(entityName, options, method, where) {
    const { ...opts } = options;
    // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
    for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) {
      delete opts[k];
    }
    return [Utils.className(entityName), method, opts, where];
  }
  /**
   * @internal
   */
  async tryCache(entityName, config, key, refresh, merge) {
    config ??= this.config.get('resultCache').global;
    if (!config) {
      return undefined;
    }
    const em = this.getContext();
    const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
    const cached = await em.#resultCache.get(cacheKey);
    if (!cached) {
      return { key: cacheKey, data: cached };
    }
    let data;
    const createOptions = {
      merge: true,
      convertCustomTypes: false,
      refresh,
      recomputeSnapshot: true,
    };
    if (Array.isArray(cached) && merge) {
      data = cached.map(item => em.#entityFactory.create(entityName, item, createOptions));
    } else if (Utils.isObject(cached) && merge) {
      data = em.#entityFactory.create(entityName, cached, createOptions);
    } else {
      data = cached;
    }
    await em.#unitOfWork.dispatchOnLoadEvent();
    return { key: cacheKey, data };
  }
  /**
   * @internal
   */
  async storeCache(config, key, data) {
    config ??= this.config.get('resultCache').global;
    if (config) {
      const em = this.getContext();
      const expiration = Array.isArray(config) ? config[1] : typeof config === 'number' ? config : undefined;
      await em.#resultCache.set(key.key, data instanceof Function ? data() : data, '', expiration);
    }
  }
  /**
   * Clears result cache for given cache key. If we want to be able to call this method,
   * we need to set the cache key explicitly when storing the cache.
   *
   * ```ts
   * // set the cache key to 'book-cache-key', with expiration of 60s
   * const res = await em.find(Book, { ... }, { cache: ['book-cache-key', 60_000] });
   *
   * // clear the cache key by name
   * await em.clearCache('book-cache-key');
   * ```
   */
  async clearCache(cacheKey) {
    await this.getContext().#resultCache.remove(cacheKey);
  }
  /**
   * Returns the default schema of this EntityManager. Respects the context, so global EM will give you the contextual schema
   * if executed inside request context handler.
   */
  get schema() {
    return this.getContext(false).#schema;
  }
  /**
   * Sets the default schema of this EntityManager. Respects the context, so global EM will set the contextual schema
   * if executed inside request context handler.
   */
  set schema(schema) {
    this.getContext(false).#schema = schema ?? undefined;
  }
  /** @internal */
  async getDataLoader(type) {
    const em = this.getContext();
    if (em.#loaders[type]) {
      return em.#loaders[type];
    }
    const { DataloaderUtils } = await import('@mikro-orm/core/dataloader');
    const DataLoader = await DataloaderUtils.getDataLoader();
    switch (type) {
      case 'ref':
        return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getRefBatchLoadFn(em)));
      case '1:m':
        return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
      case 'm:n':
        return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
    }
  }
  /**
   * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
   * if executed inside request context handler.
   */
  get id() {
    return this.getContext(false)._id;
  }
  /** @ignore */
  [Symbol.for('nodejs.util.inspect.custom')]() {
    return `[EntityManager<${this.id}>]`;
  }
}
