import { EntityMetadata } from '../typings.js';
import { compareArrays, Utils } from '../utils/Utils.js';
import { QueryHelper } from '../utils/QueryHelper.js';
import { MetadataValidator } from './MetadataValidator.js';
import { MetadataProvider } from './MetadataProvider.js';
import { MetadataStorage } from './MetadataStorage.js';
import { EntitySchema } from './EntitySchema.js';
import { Cascade, ReferenceKind } from '../enums.js';
import { MetadataError } from '../errors.js';
import { t, Type } from '../types/index.js';
import { colors } from '../logging/colors.js';
import { raw, Raw } from '../utils/RawQueryFragment.js';
import { BaseEntity } from '../entity/BaseEntity.js';
/** Discovers, validates, and processes entity metadata from configured sources. */
export class MetadataDiscovery {
  #namingStrategy;
  #metadataProvider;
  #logger;
  #schemaHelper;
  #validator = new MetadataValidator();
  #discovered = [];
  #metadata;
  #platform;
  #config;
  constructor(metadata, platform, config) {
    this.#metadata = metadata;
    this.#platform = platform;
    this.#config = config;
    this.#namingStrategy = this.#config.getNamingStrategy();
    this.#metadataProvider = this.#config.getMetadataProvider();
    this.#logger = this.#config.getLogger();
    this.#schemaHelper = this.#platform.getSchemaHelper();
  }
  /** Discovers all entities asynchronously and returns the populated MetadataStorage. */
  async discover(preferTs = true) {
    this.#discovered.length = 0;
    const startTime = Date.now();
    const suffix =
      this.#metadataProvider.constructor === MetadataProvider
        ? ''
        : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
    this.#logger.log('discovery', `ORM entity discovery started${suffix}`);
    await this.findEntities(preferTs);
    for (const meta of this.#discovered) {
      /* v8 ignore next */
      await this.#config.get('discovery').onMetadata?.(meta, this.#platform);
    }
    this.processDiscoveredEntities(this.#discovered);
    const diff = Date.now() - startTime;
    this.#logger.log(
      'discovery',
      `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`,
    );
    const storage = this.mapDiscoveredEntities();
    /* v8 ignore next */
    await this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
    return storage;
  }
  /** Discovers all entities synchronously and returns the populated MetadataStorage. */
  discoverSync() {
    this.#discovered.length = 0;
    const startTime = Date.now();
    const suffix =
      this.#metadataProvider.constructor === MetadataProvider
        ? ''
        : `, using ${colors.cyan(this.#metadataProvider.constructor.name)}`;
    this.#logger.log('discovery', `ORM entity discovery started${suffix} in sync mode`);
    const refs = this.#config.get('entities');
    this.discoverReferences(refs);
    for (const meta of this.#discovered) {
      /* v8 ignore next */
      void this.#config.get('discovery').onMetadata?.(meta, this.#platform);
    }
    this.processDiscoveredEntities(this.#discovered);
    const diff = Date.now() - startTime;
    this.#logger.log(
      'discovery',
      `- entity discovery finished, found ${colors.green('' + this.#discovered.length)} entities, took ${colors.green(`${diff} ms`)}`,
    );
    const storage = this.mapDiscoveredEntities();
    /* v8 ignore next */
    void this.#config.get('discovery').afterDiscovered?.(storage, this.#platform);
    return storage;
  }
  mapDiscoveredEntities() {
    const discovered = new MetadataStorage();
    this.#discovered
      .filter(meta => meta.root.name)
      .sort((a, b) => b.root.name.localeCompare(a.root.name))
      .forEach(meta => {
        this.#platform.validateMetadata(meta);
        discovered.set(meta.class, meta);
      });
    for (const meta of discovered) {
      meta.root = discovered.get(meta.root.class);
      if (meta.inheritanceType === 'tpt') {
        this.computeTPTOwnProps(meta);
      }
    }
    return discovered;
  }
  initAccessors(meta) {
    for (const prop of Object.values(meta.properties)) {
      if (!prop.accessor || meta.properties[prop.accessor]) {
        continue;
      }
      const desc = Object.getOwnPropertyDescriptor(meta.prototype, prop.name);
      if (desc?.get || desc?.set) {
        this.initRelation(prop);
        this.initFieldName(prop);
        const accessor = prop.name;
        prop.name = typeof prop.accessor === 'string' ? prop.accessor : prop.name;
        if (prop.accessor === true) {
          prop.getter = prop.setter = true;
        } else {
          prop.getter = prop.setter = false;
        }
        prop.accessor = accessor;
        prop.serializedName ??= accessor;
        Utils.renameKey(meta.properties, accessor, prop.name);
      } else {
        const name = prop.name;
        if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
          prop.name = prop.accessor;
        }
        this.initRelation(prop);
        this.initFieldName(prop);
        prop.serializedName ??= prop.accessor;
        prop.name = name;
      }
    }
  }
  /** Processes discovered entities: initializes relations, embeddables, indexes, and inheritance. */
  processDiscoveredEntities(discovered) {
    for (const meta of discovered) {
      let i = 1;
      Object.values(meta.properties).forEach(prop => meta.propertyOrder.set(prop.name, i++));
      Object.values(meta.properties).forEach(prop => this.initPolyEmbeddables(prop, discovered));
      this.initAccessors(meta);
    }
    // ignore base entities (not annotated with @Entity)
    const filtered = discovered.filter(meta => meta.root.name);
    // sort so we discover entities first to get around issues with nested embeddables
    filtered.sort((a, b) => (!a.embeddable === !b.embeddable ? 0 : a.embeddable ? 1 : -1));
    filtered.forEach(meta => this.initSingleTableInheritance(meta, filtered));
    filtered.forEach(meta => this.initTPTRelationships(meta, filtered));
    filtered.forEach(meta => this.defineBaseEntityProperties(meta));
    filtered.forEach(meta => {
      const newMeta = EntitySchema.fromMetadata(meta).init().meta;
      return this.#metadata.set(newMeta.class, newMeta);
    });
    filtered.forEach(meta => this.initAutoincrement(meta));
    const forEachProp = cb => {
      filtered.forEach(meta => Object.values(meta.properties).forEach(prop => cb(meta, prop)));
    };
    forEachProp((m, p) => this.initFactoryField(m, p));
    forEachProp((m, p) => this.initPolymorphicRelation(m, p, filtered));
    forEachProp((_m, p) => this.initRelation(p));
    forEachProp((m, p) => this.initEmbeddables(m, p));
    forEachProp((_m, p) => this.initFieldName(p));
    filtered.forEach(meta => this.finalizeTPTInheritance(meta, filtered));
    filtered.forEach(meta => this.computeTPTOwnProps(meta));
    forEachProp((m, p) => this.initVersionProperty(m, p));
    forEachProp((m, p) => this.initCustomType(m, p));
    forEachProp((m, p) => this.initGeneratedColumn(m, p));
    filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
    filtered.forEach(meta => this.initCheckConstraints(meta));
    forEachProp((_m, p) => {
      this.initDefaultValue(p);
      this.inferTypeFromDefault(p);
      this.initRelation(p);
      this.initColumnType(p);
    });
    forEachProp((m, p) => this.initIndexes(m, p));
    filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
    for (const meta of filtered) {
      discovered.push(...this.processEntity(meta));
    }
    discovered.forEach(meta => meta.sync(true));
    this.#metadataProvider.combineCache();
    return discovered.map(meta => {
      meta = this.#metadata.get(meta.class);
      meta.sync(true);
      this.findReferencingProperties(meta, filtered);
      if (meta.inheritanceType === 'tpt') {
        this.computeTPTOwnProps(meta);
      }
      return meta;
    });
  }
  async findEntities(preferTs) {
    const { entities, entitiesTs, baseDir } = this.#config.getAll();
    const targets = preferTs && entitiesTs.length > 0 ? entitiesTs : entities;
    const processed = [];
    const paths = [];
    for (const entity of targets) {
      if (typeof entity === 'string') {
        paths.push(entity);
      } else {
        processed.push(entity);
      }
    }
    if (paths.length > 0) {
      const { discoverEntities } = await import('@mikro-orm/core/file-discovery');
      processed.push(...(await discoverEntities(paths, { baseDir })));
    }
    return this.discoverReferences(processed);
  }
  discoverMissingTargets() {
    const unwrap = type =>
      type
        .replace(/Array<(.*)>/, '$1') // unwrap array
        .replace(/\[]$/, '') // remove array suffix
        .replace(/\((.*)\)/, '$1'); // unwrap union types
    const missing = [];
    this.#discovered.forEach(meta =>
      Object.values(meta.properties).forEach(prop => {
        if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.pivotEntity) {
          const pivotEntity = prop.pivotEntity;
          const target = typeof pivotEntity === 'function' && !pivotEntity.prototype ? pivotEntity() : pivotEntity;
          if (!this.#discovered.find(m => m.className === Utils.className(target))) {
            missing.push(target);
          }
        }
        if (prop.kind !== ReferenceKind.SCALAR) {
          const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
          if (
            !unwrap(prop.type)
              .split(/ ?\| ?/)
              .every(type => this.#discovered.find(m => m.className === type))
          ) {
            missing.push(...Utils.asArray(target));
          }
        }
      }),
    );
    if (missing.length > 0) {
      this.tryDiscoverTargets(missing);
    }
  }
  tryDiscoverTargets(targets) {
    for (const target of targets) {
      const schema = EntitySchema.is(target) ? target : undefined;
      const isDiscoverable = typeof target === 'function' || schema;
      if (isDiscoverable && target.name) {
        // Get the actual class for EntitySchema, or use target directly for classes
        const targetClass = schema ? schema.meta.class : target;
        if (!this.#metadata.has(targetClass)) {
          this.discoverReferences([target], false);
          this.discoverMissingTargets();
        }
      }
    }
  }
  discoverReferences(refs, validate = true) {
    const found = [];
    for (const entity of refs) {
      if (typeof entity === 'string') {
        throw new Error('Folder based discovery requires the async `MikroORM.init()` method.');
      }
      const schema = this.getSchema(entity);
      const meta = schema.init().meta;
      this.#metadata.set(meta.class, meta);
      found.push(schema);
    }
    // discover parents (base entities) automatically
    for (const meta of this.#metadata) {
      let parent = meta.extends;
      if (EntitySchema.is(parent) && !this.#metadata.has(parent.init().meta.class)) {
        this.discoverReferences([parent], false);
      }
      if (typeof parent === 'function' && parent.name && !this.#metadata.has(parent)) {
        this.discoverReferences([parent], false);
      }
      /* v8 ignore next */
      if (!meta.class) {
        continue;
      }
      parent = Object.getPrototypeOf(meta.class);
      // Skip if parent is the auto-generated base class for the same entity (from setClass usage)
      if (
        parent.name !== '' &&
        parent.name !== meta.className &&
        !this.#metadata.has(parent) &&
        parent !== BaseEntity
      ) {
        this.discoverReferences([parent], false);
      }
    }
    for (const schema of found) {
      this.discoverEntity(schema);
    }
    this.discoverMissingTargets();
    if (validate) {
      this.#validator.validateDiscovered(this.#discovered, this.#config.get('discovery'));
    }
    return this.#discovered.filter(meta => found.find(m => m.name === meta.className));
  }
  reset(entityName) {
    const exists = this.#discovered.findIndex(
      m => m.class === entityName || m.className === Utils.className(entityName),
    );
    if (exists !== -1) {
      this.#metadata.reset(this.#discovered[exists].class);
      this.#discovered.splice(exists, 1);
    }
  }
  getSchema(entity) {
    if (EntitySchema.REGISTRY.has(entity)) {
      entity = EntitySchema.REGISTRY.get(entity);
    }
    if (EntitySchema.is(entity)) {
      const meta = Utils.copy(entity.meta, false);
      return EntitySchema.fromMetadata(meta);
    }
    // After the EntitySchema check, entity must be an EntityClass
    const cls = entity;
    const path = cls[MetadataStorage.PATH_SYMBOL];
    if (path) {
      const meta = Utils.copy(MetadataStorage.getMetadata(cls.name, path), false);
      meta.path = path;
      this.#metadata.set(cls, meta);
    }
    const exists = this.#metadata.has(cls);
    const meta = this.#metadata.get(cls, true);
    meta.abstract ??= !(exists && meta.name);
    const schema = EntitySchema.fromMetadata(meta);
    schema.setClass(cls);
    return schema;
  }
  getRootEntity(meta) {
    const base = meta.extends && this.#metadata.find(meta.extends);
    if (!base || base === meta) {
      // make sure we do not fall into infinite loop
      return meta;
    }
    const root = this.getRootEntity(base);
    // For STI or TPT, use the root entity.
    // Check both `inheritanceType` (set during discovery) and raw `inheritance` option (set before discovery).
    if (root.discriminatorColumn || root.inheritanceType || root.inheritance === 'tpt') {
      return root;
    }
    return meta;
  }
  discoverEntity(schema) {
    const meta = schema.meta;
    const path = meta.path;
    this.#logger.log(
      'discovery',
      `- processing entity ${colors.cyan(meta.className)}${colors.grey(path ? ` (${path})` : '')}`,
    );
    const root = this.getRootEntity(meta);
    schema.meta.path = meta.path;
    const cache = this.#metadataProvider.getCachedMetadata(meta, root);
    if (cache) {
      this.#logger.log('discovery', `- using cached metadata for entity ${colors.cyan(meta.className)}`);
      this.#discovered.push(meta);
      return;
    }
    // infer default value from property initializer early, as the metadata provider might use some defaults, e.g. string for reflect-metadata
    for (const prop of meta.props) {
      this.inferDefaultValue(meta, prop);
    }
    // if the definition is using EntitySchema we still want it to go through the metadata provider to validate no types are missing
    this.#metadataProvider.loadEntityMetadata(meta);
    if (!meta.tableName && meta.name) {
      const entityName = root.discriminatorColumn ? root.name : meta.name;
      meta.tableName = this.#namingStrategy.classToTableName(entityName);
    }
    this.#metadataProvider.saveToCache(meta);
    meta.root = root;
    this.#discovered.push(meta);
  }
  initNullability(prop) {
    if (prop.kind === ReferenceKind.ONE_TO_ONE) {
      return Utils.defaultValue(prop, 'nullable', prop.optional || !prop.owner);
    }
    return Utils.defaultValue(prop, 'nullable', prop.optional);
  }
  applyNamingStrategy(meta, prop) {
    if (!prop.fieldNames) {
      this.initFieldName(prop);
    }
    if (prop.kind === ReferenceKind.MANY_TO_MANY) {
      this.initManyToManyFields(meta, prop);
    }
    if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
      this.initManyToOneFields(prop);
    }
    if (prop.kind === ReferenceKind.ONE_TO_MANY) {
      this.initOneToManyFields(prop);
    }
  }
  initOwnColumns(meta) {
    meta.sync();
    for (const prop of meta.props) {
      if (
        !prop.joinColumns ||
        !prop.columnTypes ||
        prop.ownColumns ||
        ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
      ) {
        continue;
      }
      // For polymorphic relations, ownColumns should include all fieldNames
      // (discriminator + ID columns) since they are all owned by this relation
      if (prop.polymorphic) {
        prop.ownColumns = prop.fieldNames;
        continue;
      }
      if (prop.joinColumns.length > 1) {
        prop.ownColumns = prop.joinColumns.filter(col => {
          return !meta.props.find(p => p.name !== prop.name && (!p.fieldNames || p.fieldNames.includes(col)));
        });
      }
      if (!prop.ownColumns || prop.ownColumns.length === 0) {
        prop.ownColumns = prop.joinColumns;
      }
      if (prop.joinColumns.length !== prop.columnTypes.length) {
        prop.columnTypes = prop.joinColumns.flatMap(field => {
          const matched = meta.props.find(p => p.fieldNames?.includes(field));
          /* v8 ignore next */
          if (!matched) {
            throw MetadataError.fromWrongForeignKey(meta, prop, 'columnTypes');
          }
          return matched.columnTypes;
        });
      }
      if (prop.joinColumns.length !== prop.referencedColumnNames.length) {
        throw MetadataError.fromWrongForeignKey(meta, prop, 'referencedColumnNames');
      }
    }
  }
  initFieldName(prop, object = false) {
    if (prop.fieldNames && prop.fieldNames.length > 0) {
      return;
    }
    if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
      prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
    } else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
      prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
    } else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
      prop.fieldNames = this.initManyToManyFieldName(prop, prop.name);
    }
  }
  initManyToOneFieldName(prop, name, tableName) {
    const meta2 = prop.targetMeta;
    const ret = [];
    for (const primaryKey of meta2.primaryKeys) {
      this.initFieldName(meta2.properties[primaryKey]);
      for (const fieldName of meta2.properties[primaryKey].fieldNames) {
        ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, meta2.compositePK, tableName));
      }
    }
    return ret;
  }
  initManyToManyFieldName(prop, name) {
    const meta2 = prop.targetMeta;
    return meta2.primaryKeys.map(() => this.#namingStrategy.propertyToColumnName(name));
  }
  initManyToManyFields(meta, prop) {
    const meta2 = prop.targetMeta;
    Utils.defaultValue(prop, 'fixedOrder', !!prop.fixedOrderColumn);
    const pivotMeta = this.#metadata.find(prop.pivotEntity);
    const props = Object.values(pivotMeta?.properties ?? {});
    const pks = props.filter(p => p.primary);
    const fks = props.filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
    if (pivotMeta) {
      pivotMeta.pivotTable = true;
      prop.pivotTable = pivotMeta.tableName;
      if (pks.length === 1) {
        prop.fixedOrder = true;
        prop.fixedOrderColumn = pks[0].name;
      }
    }
    if (pivotMeta && (pks.length === 2 || fks.length >= 2)) {
      const owner = prop.mappedBy ? meta2.properties[prop.mappedBy] : prop;
      const [first, second] = this.ensureCorrectFKOrderInPivotEntity(pivotMeta, owner);
      prop.joinColumns ??= first.fieldNames;
      prop.inverseJoinColumns ??= second.fieldNames;
    }
    if (!prop.pivotTable && prop.owner && this.#platform.usesPivotTable()) {
      prop.pivotTable = this.#namingStrategy.joinTableName(meta.className, meta2.tableName, prop.name, meta.tableName);
    }
    if (prop.mappedBy) {
      const prop2 = meta2.properties[prop.mappedBy];
      this.initManyToManyFields(meta2, prop2);
      prop.pivotTable = prop2.pivotTable;
      prop.pivotEntity = prop2.pivotEntity;
      prop.fixedOrder = prop2.fixedOrder;
      prop.fixedOrderColumn = prop2.fixedOrderColumn;
      prop.joinColumns = prop2.inverseJoinColumns;
      prop.inverseJoinColumns = prop2.joinColumns;
      prop.polymorphic = prop2.polymorphic;
      prop.discriminator = prop2.discriminator;
      prop.discriminatorColumn = prop2.discriminatorColumn;
      prop.discriminatorValue = prop2.discriminatorValue;
    }
    prop.referencedColumnNames ??= Utils.flatten(
      meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames),
    );
    // For polymorphic M:N, use discriminator base name for FK column (e.g., taggable_id instead of post_id)
    if (prop.polymorphic && prop.discriminator) {
      prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName =>
        this.#namingStrategy.joinKeyColumnName(
          prop.discriminator,
          referencedColumnName,
          prop.referencedColumnNames.length > 1,
        ),
      );
    } else {
      const ownerTableName = this.isExplicitTableName(meta.root) ? meta.root.tableName : undefined;
      prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName =>
        this.#namingStrategy.joinKeyColumnName(
          meta.root.className,
          referencedColumnName,
          meta.compositePK,
          ownerTableName,
        ),
      );
    }
    const inverseTableName = this.isExplicitTableName(meta2.root) ? meta2.root.tableName : undefined;
    prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className, inverseTableName);
  }
  isExplicitTableName(meta) {
    return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
  }
  initManyToOneFields(prop) {
    if (prop.polymorphic && prop.polymorphTargets) {
      const fieldNames1 = prop.targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
      const idColumns = fieldNames1.map(fieldName =>
        this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, fieldNames1.length > 1),
      );
      prop.fieldNames ??= [prop.discriminatorColumn, ...idColumns];
      prop.joinColumns ??= idColumns;
      prop.referencedColumnNames ??= fieldNames1;
      prop.referencedTableName ??= prop.targetMeta.tableName;
      return;
    }
    const meta2 = prop.targetMeta;
    let fieldNames;
    // If targetKey is specified, use that property's field names instead of PKs
    if (prop.targetKey) {
      const targetProp = meta2.properties[prop.targetKey];
      fieldNames = targetProp.fieldNames;
    } else {
      fieldNames = Utils.flatten(meta2.primaryKeys.map(primaryKey => meta2.properties[primaryKey].fieldNames));
    }
    Utils.defaultValue(prop, 'referencedTableName', meta2.tableName);
    if (!prop.joinColumns) {
      prop.joinColumns = fieldNames.map(fieldName =>
        this.#namingStrategy.joinKeyColumnName(prop.name, fieldName, fieldNames.length > 1),
      );
    }
    if (!prop.referencedColumnNames) {
      prop.referencedColumnNames = fieldNames;
    }
    // Relations to composite PK targets need cascade update by default,
    // since composite PKs are more likely to have mutable components
    if (meta2.compositePK) {
      prop.updateRule ??= 'cascade';
    }
    // Nullable relations default to 'set null' on delete - when the referenced
    // entity is deleted, set the FK to null rather than failing
    if (prop.nullable) {
      prop.deleteRule ??= 'set null';
    }
  }
  initOneToManyFields(prop) {
    const meta2 = prop.targetMeta;
    if (!prop.joinColumns) {
      prop.joinColumns = [this.#namingStrategy.joinColumnName(prop.name)];
    }
    if (!prop.referencedColumnNames) {
      meta2.getPrimaryProps().forEach(pk => this.applyNamingStrategy(meta2, pk));
      prop.referencedColumnNames = Utils.flatten(meta2.getPrimaryProps().map(pk => pk.fieldNames));
    }
  }
  processEntity(meta) {
    const pks = Object.values(meta.properties).filter(prop => prop.primary);
    meta.primaryKeys = pks.map(prop => prop.name);
    meta.compositePK = pks.length > 1;
    // FK used as PK, we need to cascade - applies to both single FK-as-PK
    // and composite PKs where all PKs are FKs (e.g., pivot-like entities)
    const fkPks = pks.filter(pk => pk.kind !== ReferenceKind.SCALAR);
    if (fkPks.length > 0 && fkPks.length === pks.length) {
      for (const pk of fkPks) {
        pk.deleteRule ??= 'cascade';
        pk.updateRule ??= 'cascade';
      }
    }
    meta.forceConstructor ??= this.shouldForceConstructorUsage(meta);
    this.#validator.validateEntityDefinition(this.#metadata, meta.class, this.#config.get('discovery'));
    for (const prop of Object.values(meta.properties)) {
      this.initNullability(prop);
      this.applyNamingStrategy(meta, prop);
      this.initDefaultValue(prop);
      this.inferTypeFromDefault(prop);
      this.initVersionProperty(meta, prop);
      this.initCustomType(meta, prop);
      this.initColumnType(prop);
      this.initRelation(prop);
    }
    this.initOwnColumns(meta);
    meta.simplePK =
      pks.length === 1 && pks[0].kind === ReferenceKind.SCALAR && !pks[0].customType && pks[0].runtimeType !== 'Date';
    meta.serializedPrimaryKey ??= meta.props.find(prop => prop.serializedPrimaryKey)?.name;
    if (meta.serializedPrimaryKey && meta.serializedPrimaryKey !== meta.primaryKeys[0]) {
      meta.properties[meta.serializedPrimaryKey].persist ??= false;
    }
    if (this.#platform.usesPivotTable()) {
      return Object.values(meta.properties)
        .filter(prop => prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner && prop.pivotTable)
        .map(prop => {
          const pivotMeta = this.definePivotTableEntity(meta, prop);
          prop.pivotEntity = pivotMeta.class;
          if (prop.inversedBy) {
            prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
          }
          return pivotMeta;
        });
    }
    return [];
  }
  findReferencingProperties(meta, metadata) {
    for (const meta2 of metadata) {
      for (const prop2 of meta2.relations) {
        if (prop2.kind !== ReferenceKind.SCALAR && prop2.type === meta.className) {
          meta.referencingProperties.push({ meta: meta2, prop: prop2 });
        }
      }
    }
  }
  initFactoryField(meta, prop) {
    ['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
      const value = prop[type];
      if (value instanceof Function) {
        const meta2 = prop.targetMeta ?? this.#metadata.get(prop.target);
        prop[type] = value(meta2.properties)?.name;
        if (type === 'pivotEntity' && value) {
          prop[type] = value(meta2.properties);
        }
        if (prop[type] == null) {
          throw MetadataError.fromWrongReference(meta, prop, type);
        }
      }
    });
  }
  ensureCorrectFKOrderInPivotEntity(meta, owner) {
    const pks = Object.values(meta.properties).filter(p => p.primary);
    const fks = Object.values(meta.properties).filter(p => p.kind === ReferenceKind.MANY_TO_ONE);
    let first, second;
    if (pks.length === 2) {
      [first, second] = pks;
    } else if (fks.length >= 2) {
      [first, second] = fks;
    } else {
      /* v8 ignore next */
      return [];
    }
    // wrong FK order, first FK needs to point to the owning side
    // (note that we can detect this only if the FKs target different types)
    if (owner.type === first.type && first.type !== second.type) {
      delete meta.properties[first.name];
      meta.removeProperty(first.name, false);
      meta.addProperty(first);
      [first, second] = [second, first];
    }
    return [first, second];
  }
  definePivotTableEntity(meta, prop) {
    const pivotMeta = prop.pivotEntity
      ? this.#metadata.find(prop.pivotEntity)
      : this.#metadata.getByClassName(prop.pivotTable, false);
    // ensure inverse side exists so we can join it when populating via pivot tables
    if (!prop.inversedBy && prop.targetMeta) {
      const inverseName = `${meta.className}_${prop.name}__inverse`;
      prop.inversedBy = inverseName;
      const inverseProp = {
        name: inverseName,
        kind: ReferenceKind.MANY_TO_MANY,
        type: meta.className,
        target: meta.class,
        targetMeta: meta,
        mappedBy: prop.name,
        pivotEntity: prop.pivotEntity,
        pivotTable: prop.pivotTable,
        persist: false,
        hydrate: false,
      };
      this.applyNamingStrategy(prop.targetMeta, inverseProp);
      this.initCustomType(prop.targetMeta, inverseProp);
      prop.targetMeta.properties[inverseName] = inverseProp;
    }
    if (pivotMeta) {
      prop.pivotEntity = pivotMeta.class;
      this.ensureCorrectFKOrderInPivotEntity(pivotMeta, prop);
      if (prop.polymorphic && prop.discriminatorValue) {
        pivotMeta.polymorphicDiscriminatorMap ??= {};
        pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
        // For composite PK entities sharing a polymorphic pivot table,
        // we need to add columns for each entity type's PKs
        this.addPolymorphicPivotColumns(pivotMeta, meta, prop);
        // Add virtual M:1 relation for this polymorphic owner (for join loading)
        const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
        if (!pivotMeta.properties[ownerRelationName]) {
          pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
        }
      }
      return pivotMeta;
    }
    let tableName = prop.pivotTable;
    let schemaName;
    if (prop.pivotTable.includes('.')) {
      [schemaName, tableName] = prop.pivotTable.split('.');
    }
    schemaName ??= meta.schema;
    const targetMeta = prop.targetMeta;
    const targetType = targetMeta.className;
    const pivotMeta2 = new EntityMetadata({
      name: prop.pivotTable,
      className: prop.pivotTable,
      collection: tableName,
      schema: schemaName,
      pivotTable: true,
    });
    prop.pivotEntity = pivotMeta2.class;
    if (prop.fixedOrder) {
      const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
      pivotMeta2.properties[primaryProp.name] = primaryProp;
    } else {
      pivotMeta2.compositePK = true;
    }
    // handle self-referenced m:n with same default field names
    if (
      meta.className === targetType &&
      prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])
    ) {
      // use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
      const baseName = this.isExplicitTableName(meta) ? meta.tableName : meta.className;
      prop.joinColumns = prop.referencedColumnNames.map(name =>
        this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK),
      );
      prop.inverseJoinColumns = prop.referencedColumnNames.map(name =>
        this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK),
      );
      if (prop.inversedBy) {
        const prop2 = targetMeta.properties[prop.inversedBy];
        prop2.inverseJoinColumns = prop.joinColumns;
        prop2.joinColumns = prop.inverseJoinColumns;
      }
      // propagate updated joinColumns to all child entities that inherit this property (STI)
      for (const childMeta of this.#discovered.filter(m => m.root === meta && m !== meta)) {
        const childProp = childMeta.properties[prop.name];
        if (childProp) {
          childProp.joinColumns = prop.joinColumns;
          childProp.inverseJoinColumns = prop.inverseJoinColumns;
        }
      }
    }
    // For polymorphic M:N, create discriminator column and polymorphic FK
    if (prop.polymorphic && prop.discriminatorColumn) {
      this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
    } else {
      pivotMeta2.properties[meta.name + '_owner'] = this.definePivotProperty(
        prop,
        meta.name + '_owner',
        meta.class,
        targetType + '_inverse',
        true,
        meta.className === targetType,
      );
      pivotMeta2.properties[targetType + '_inverse'] = this.definePivotProperty(
        prop,
        targetType + '_inverse',
        targetMeta.class,
        meta.name + '_owner',
        false,
        meta.className === targetType,
      );
    }
    return this.#metadata.set(pivotMeta2.class, EntitySchema.fromMetadata(pivotMeta2).init().meta);
  }
  /**
   * Create a scalar property for a pivot table column.
   */
  createPivotScalarProperty(name, columnTypes, fieldNames = [name], options = {}) {
    return {
      name,
      fieldNames,
      columnTypes,
      type: options.type ?? 'number',
      kind: ReferenceKind.SCALAR,
      primary: options.primary ?? false,
      nullable: options.nullable ?? true,
      ...(options.persist !== undefined && { persist: options.persist }),
    };
  }
  /**
   * Get column types for an entity's primary keys, initializing them if needed.
   */
  getPrimaryKeyColumnTypes(meta) {
    const columnTypes = [];
    for (const pk of meta.primaryKeys) {
      const pkProp = meta.properties[pk];
      this.initCustomType(meta, pkProp);
      this.initColumnType(pkProp);
      columnTypes.push(...pkProp.columnTypes);
    }
    return columnTypes;
  }
  /**
   * Add missing FK columns for a polymorphic entity to an existing pivot table.
   */
  addPolymorphicPivotColumns(pivotMeta, meta, prop) {
    const existingFieldNames = new Set(Object.values(pivotMeta.properties).flatMap(p => p.fieldNames ?? []));
    const columnTypes = this.getPrimaryKeyColumnTypes(meta);
    for (let i = 0; i < prop.joinColumns.length; i++) {
      const joinColumn = prop.joinColumns[i];
      if (!existingFieldNames.has(joinColumn)) {
        pivotMeta.properties[joinColumn] = this.createPivotScalarProperty(joinColumn, [columnTypes[i]]);
      }
    }
  }
  /**
   * Define properties for a polymorphic pivot table.
   */
  definePolymorphicPivotProperties(pivotMeta, meta, prop, targetMeta) {
    const discriminatorColumn = prop.discriminatorColumn;
    const isCompositePK = meta.compositePK;
    // For composite PK polymorphic M:N, we need fixedOrder (auto-increment PK)
    if (isCompositePK && !prop.fixedOrder) {
      prop.fixedOrder = true;
      const primaryProp = this.defineFixedOrderProperty(prop, targetMeta);
      pivotMeta.properties[primaryProp.name] = primaryProp;
      pivotMeta.compositePK = false;
    }
    const discriminatorProp = this.createPivotScalarProperty(
      discriminatorColumn,
      [this.#platform.getVarcharTypeDeclarationSQL(prop)],
      [discriminatorColumn],
      { type: 'string', primary: !isCompositePK, nullable: false },
    );
    this.initFieldName(discriminatorProp);
    pivotMeta.properties[discriminatorColumn] = discriminatorProp;
    const columnTypes = this.getPrimaryKeyColumnTypes(meta);
    if (isCompositePK) {
      // Create separate properties for each PK column (nullable for other entity types)
      for (let i = 0; i < prop.joinColumns.length; i++) {
        pivotMeta.properties[prop.joinColumns[i]] = this.createPivotScalarProperty(prop.joinColumns[i], [
          columnTypes[i],
        ]);
      }
      // Virtual property combining all columns (for compatibility)
      pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(
        prop.discriminator,
        columnTypes,
        [...prop.joinColumns],
        { type: meta.className, persist: false },
      );
    } else {
      pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(
        prop.discriminator,
        columnTypes,
        [...prop.joinColumns],
        { type: meta.className, primary: true, nullable: false },
      );
    }
    pivotMeta.properties[targetMeta.className + '_inverse'] = this.definePivotProperty(
      prop,
      targetMeta.className + '_inverse',
      targetMeta.class,
      prop.discriminator,
      false,
      false,
    );
    // Create virtual M:1 relation to the polymorphic owner for single-query join loading
    const ownerRelationName = `${prop.discriminator}_${meta.tableName}`;
    pivotMeta.properties[ownerRelationName] = this.definePolymorphicOwnerRelation(prop, ownerRelationName, meta);
    pivotMeta.polymorphicDiscriminatorMap ??= {};
    pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
  }
  /**
   * Create a virtual M:1 relation from pivot to a polymorphic owner entity.
   * This enables single-query join loading for inverse-side polymorphic M:N.
   */
  definePolymorphicOwnerRelation(prop, name, targetMeta) {
    const ret = {
      name,
      type: targetMeta.className,
      target: targetMeta.class,
      kind: ReferenceKind.MANY_TO_ONE,
      nullable: true,
      owner: true,
      primary: false,
      createForeignKeyConstraint: false,
      persist: false,
      index: false,
    };
    ret.targetMeta = targetMeta;
    ret.fieldNames = ret.joinColumns = ret.ownColumns = [...prop.joinColumns];
    ret.referencedColumnNames = [];
    ret.inverseJoinColumns = [];
    for (const primaryKey of targetMeta.primaryKeys) {
      const pkProp = targetMeta.properties[primaryKey];
      ret.referencedColumnNames.push(...pkProp.fieldNames);
      ret.inverseJoinColumns.push(...pkProp.fieldNames);
      ret.length = pkProp.length;
      ret.precision = pkProp.precision;
      ret.scale = pkProp.scale;
    }
    const schema = targetMeta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
    ret.referencedTableName = schema && schema !== '*' ? schema + '.' + targetMeta.tableName : targetMeta.tableName;
    this.initColumnType(ret);
    this.initRelation(ret);
    return ret;
  }
  defineFixedOrderProperty(prop, targetMeta) {
    const pk = prop.fixedOrderColumn || this.#namingStrategy.referenceColumnName();
    const primaryProp = {
      name: pk,
      type: 'number',
      runtimeType: 'number',
      kind: ReferenceKind.SCALAR,
      primary: true,
      autoincrement: true,
      unsigned: this.#platform.supportsUnsigned(),
    };
    this.initFieldName(primaryProp);
    this.initColumnType(primaryProp);
    prop.fixedOrderColumn = pk;
    if (prop.inversedBy) {
      const prop2 = targetMeta.properties[prop.inversedBy];
      prop2.fixedOrder = true;
      prop2.fixedOrderColumn = pk;
    }
    return primaryProp;
  }
  definePivotProperty(prop, name, type, inverse, owner, selfReferencing) {
    const ret = {
      name,
      type: Utils.className(type),
      target: type,
      kind: ReferenceKind.MANY_TO_ONE,
      cascade: [Cascade.ALL],
      fixedOrder: prop.fixedOrder,
      fixedOrderColumn: prop.fixedOrderColumn,
      index: this.#platform.indexForeignKeys(),
      primary: !prop.fixedOrder,
      autoincrement: false,
      updateRule: prop.updateRule,
      deleteRule: prop.deleteRule,
      createForeignKeyConstraint: prop.createForeignKeyConstraint,
    };
    const defaultRule = selfReferencing && !this.#platform.supportsMultipleCascadePaths() ? 'no action' : 'cascade';
    ret.updateRule ??= defaultRule;
    ret.deleteRule ??= defaultRule;
    const meta = this.#metadata.get(type);
    ret.targetMeta = meta;
    ret.joinColumns = [];
    ret.inverseJoinColumns = [];
    const schema = meta.schema ?? this.#config.get('schema') ?? this.#platform.getDefaultSchemaName();
    ret.referencedTableName = schema && schema !== '*' ? schema + '.' + meta.tableName : meta.tableName;
    if (owner) {
      ret.owner = true;
      ret.inversedBy = inverse;
      ret.referencedColumnNames = prop.referencedColumnNames;
      ret.fieldNames = ret.joinColumns = prop.joinColumns;
      ret.inverseJoinColumns = prop.referencedColumnNames;
      meta.primaryKeys.forEach(primaryKey => {
        const prop2 = meta.properties[primaryKey];
        ret.length = prop2.length;
        ret.precision = prop2.precision;
        ret.scale = prop2.scale;
      });
    } else {
      ret.owner = false;
      ret.mappedBy = inverse;
      ret.fieldNames = ret.joinColumns = prop.inverseJoinColumns;
      ret.referencedColumnNames = [];
      ret.inverseJoinColumns = [];
      meta.primaryKeys.forEach(primaryKey => {
        const prop2 = meta.properties[primaryKey];
        ret.referencedColumnNames.push(...prop2.fieldNames);
        ret.inverseJoinColumns.push(...prop2.fieldNames);
        ret.length = prop2.length;
        ret.precision = prop2.precision;
        ret.scale = prop2.scale;
      });
    }
    this.initColumnType(ret);
    this.initRelation(ret);
    return ret;
  }
  autoWireBidirectionalProperties(meta) {
    Object.values(meta.properties)
      .filter(prop => prop.kind !== ReferenceKind.SCALAR && !prop.owner && prop.mappedBy)
      .forEach(prop => {
        const meta2 = prop.targetMeta;
        const prop2 = meta2.properties[prop.mappedBy];
        if (prop2 && !prop2.inversedBy) {
          prop2.inversedBy = prop.name;
        }
      });
  }
  defineBaseEntityProperties(meta) {
    const base = meta.extends && this.#metadata.get(meta.extends);
    if (!base || base === meta) {
      // make sure we do not fall into infinite loop
      return 0;
    }
    let order = this.defineBaseEntityProperties(base);
    const ownProps = Object.values(meta.properties);
    const old = ownProps.map(x => x.name);
    meta.properties = {};
    Object.values(base.properties).forEach(prop => {
      if (!prop.inherited) {
        meta.properties[prop.name] = prop;
      }
    });
    ownProps.forEach(prop => (meta.properties[prop.name] = prop));
    meta.filters = { ...base.filters, ...meta.filters };
    if (!meta.discriminatorValue) {
      Object.values(base.properties)
        .filter(prop => !old.includes(prop.name))
        .forEach(prop => {
          meta.properties[prop.name] = { ...prop };
          meta.propertyOrder.set(prop.name, (order += 0.01));
        });
    }
    meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
    meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
    meta.checks = Utils.unique([...base.checks, ...meta.checks]);
    const pks = Object.values(meta.properties)
      .filter(p => p.primary)
      .map(p => p.name);
    if (pks.length > 0 && meta.primaryKeys.length === 0) {
      meta.primaryKeys = pks;
    }
    Utils.keys(base.hooks).forEach(type => {
      meta.hooks[type] = Utils.unique([...base.hooks[type], ...(meta.hooks[type] || [])]);
    });
    if ((meta.constructorParams?.length ?? 0) === 0 && (base.constructorParams?.length ?? 0) > 0) {
      meta.constructorParams = [...base.constructorParams];
    }
    return order;
  }
  initPolyEmbeddables(embeddedProp, discovered, visited = new Set()) {
    if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
      return;
    }
    visited.add(embeddedProp);
    const types = embeddedProp.type.split(/ ?\| ?/);
    let embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
    const polymorphs = this.#discovered.filter(m => types.includes(m.name));
    // create virtual polymorphic entity
    if (!embeddable && polymorphs.length > 0) {
      const properties = {};
      let discriminatorColumn;
      const inlineProperties = meta => {
        Object.values(meta.properties).forEach(prop => {
          // defaults on db level would mess up with change tracking
          delete prop.default;
          if (properties[prop.name] && properties[prop.name].type !== prop.type) {
            properties[prop.name].type = `${properties[prop.name].type} | ${prop.type}`;
            properties[prop.name].runtimeType = 'any';
            properties[prop.name].stiMerged = true;
            return properties[prop.name];
          }
          // Deep copy to prevent mutating the original entity's property —
          // both from the merge path above (GH #6522/#6523) and from
          // downstream code that mutates nested arrays like fieldNames.
          return (properties[prop.name] = Utils.copy(prop));
        });
      };
      const processExtensions = meta => {
        const parent = this.#discovered.find(m => {
          return meta.extends && Utils.className(meta.extends) === m.className;
        });
        if (!parent) {
          return;
        }
        discriminatorColumn ??= parent.discriminatorColumn;
        inlineProperties(parent);
        processExtensions(parent);
      };
      polymorphs.forEach(meta => {
        inlineProperties(meta);
        processExtensions(meta);
      });
      const name = polymorphs
        .map(t => t.className)
        .sort()
        .join(' | ');
      embeddable = new EntityMetadata({
        name,
        className: name,
        embeddable: true,
        abstract: true,
        properties,
        polymorphs,
        discriminatorColumn,
      });
      embeddable.sync();
      discovered.push(embeddable);
      polymorphs.forEach(meta => (meta.root = embeddable));
    }
  }
  initPolymorphicRelation(meta, prop, discovered) {
    if (!prop.discriminator && !prop.discriminatorColumn && !prop.discriminatorMap && !Array.isArray(prop.target)) {
      return;
    }
    prop.polymorphic = true;
    prop.discriminator ??= prop.name;
    prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
    prop.createForeignKeyConstraint = false;
    const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
    if (isToOne) {
      const types = prop.type.split(/ ?\| ?/);
      prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
      prop.targetMeta = prop.polymorphTargets[0];
      prop.referencedPKs = prop.targetMeta?.primaryKeys;
    }
    if (prop.discriminatorMap) {
      const normalizedMap = {};
      for (const [key, value] of Object.entries(prop.discriminatorMap)) {
        const targetMeta = this.#metadata.getByClassName(value, false);
        if (!targetMeta) {
          throw MetadataError.fromUnknownEntity(value, `${meta.className}.${prop.name} discriminatorMap`);
        }
        normalizedMap[key] = targetMeta.class;
        if (targetMeta.class === meta.class) {
          prop.discriminatorValue = key;
        }
      }
      prop.discriminatorMap = normalizedMap;
    } else if (isToOne) {
      prop.discriminatorMap = {};
      const tableNameToTarget = new Map();
      for (const target of prop.polymorphTargets) {
        const existing = tableNameToTarget.get(target.tableName);
        if (existing) {
          throw MetadataError.incompatiblePolymorphicTargets(
            meta,
            prop,
            existing,
            target,
            `both use table '${target.tableName}'. Use separate properties instead of a single polymorphic relation.`,
          );
        }
        tableNameToTarget.set(target.tableName, target);
        prop.discriminatorMap[target.tableName] = target.class;
      }
    } else {
      prop.discriminatorValue ??= meta.tableName;
      if (!prop.discriminatorMap) {
        prop.discriminatorMap = { [prop.discriminatorValue]: meta.class };
      }
    }
  }
  initEmbeddables(meta, embeddedProp, visited = new Set()) {
    if (embeddedProp.kind !== ReferenceKind.EMBEDDED || visited.has(embeddedProp)) {
      return;
    }
    visited.add(embeddedProp);
    const embeddable = this.#discovered.find(m => m.name === embeddedProp.type);
    if (!embeddable) {
      throw MetadataError.fromUnknownEntity(embeddedProp.type, `${meta.className}.${embeddedProp.name}`);
    }
    embeddedProp.embeddable = embeddable.class;
    embeddedProp.embeddedProps = {};
    let order = meta.propertyOrder.get(embeddedProp.name);
    const getRootProperty = prop => (prop.embedded ? getRootProperty(meta.properties[prop.embedded[0]]) : prop);
    const isParentObject = prop => {
      if (prop.object || prop.array) {
        return true;
      }
      return prop.embedded ? isParentObject(meta.properties[prop.embedded[0]]) : false;
    };
    const isParentArray = prop => {
      if (prop.array) {
        return true;
      }
      return prop.embedded ? isParentArray(meta.properties[prop.embedded[0]]) : false;
    };
    const rootProperty = getRootProperty(embeddedProp);
    const parentProperty = meta.properties[embeddedProp.embedded?.[0] ?? ''];
    const object = isParentObject(embeddedProp);
    const array = isParentArray(embeddedProp);
    this.initFieldName(embeddedProp, rootProperty !== embeddedProp && object);
    // the prefix of the parent cannot be a boolean; it already passed here
    const prefix = this.getPrefix(embeddedProp, parentProperty);
    const glue = object ? '~' : '_';
    for (const prop of Object.values(embeddable.properties)) {
      const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) + prop.name;
      meta.properties[name] = Utils.copy(prop);
      meta.properties[name].name = name;
      meta.properties[name].embedded = [embeddedProp.name, prop.name];
      meta.propertyOrder.set(name, (order += 0.01));
      embeddedProp.embeddedProps[prop.name] = meta.properties[name];
      meta.properties[name].persist ??= embeddedProp.persist;
      const refInArray =
        array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
      if (embeddedProp.nullable || refInArray) {
        meta.properties[name].nullable = true;
      }
      if (meta.properties[name].fieldNames) {
        meta.properties[name].fieldNames[0] = prefix + meta.properties[name].fieldNames[0];
      } else {
        const name2 = meta.properties[name].name;
        meta.properties[name].name = prefix + prop.name;
        this.initFieldName(meta.properties[name]);
        meta.properties[name].name = name2;
      }
      if (object) {
        embeddedProp.object = true;
        let path = [];
        let tmp = embeddedProp;
        while (tmp.embedded && tmp.object) {
          path.unshift(tmp.embedded[1]);
          tmp = meta.properties[tmp.embedded[0]];
        }
        if (tmp === rootProperty) {
          path.unshift(rootProperty.fieldNames[0]);
        } else if (embeddedProp.embeddedPath) {
          path = [...embeddedProp.embeddedPath];
        } else {
          path = [embeddedProp.fieldNames[0]];
        }
        this.initFieldName(prop, true);
        this.initRelation(prop);
        path.push(prop.fieldNames[0]);
        meta.properties[name].fieldNames = prop.fieldNames;
        meta.properties[name].embeddedPath = path;
        const targetProp = prop.targetMeta?.getPrimaryProp() ?? prop;
        const fieldName = raw(
          this.#platform.getSearchJsonPropertySQL(path.join('->'), targetProp.runtimeType ?? targetProp.type, true),
        );
        meta.properties[name].fieldNameRaw = fieldName.sql; // for querying in SQL drivers
        meta.properties[name].persist = false; // only virtual as we store the whole object
        meta.properties[name].userDefined = false; // mark this as a generated/internal property, so we can distinguish from user-defined non-persist properties
        meta.properties[name].object = true;
        this.initCustomType(meta, meta.properties[name], false, true);
      }
      this.initEmbeddables(meta, meta.properties[name], visited);
    }
    for (const index of embeddable.indexes) {
      meta.indexes.push({
        ...index,
        properties: Utils.asArray(index.properties).map(p => {
          return embeddedProp.embeddedProps[p].name;
        }),
      });
    }
    for (const unique of embeddable.uniques) {
      meta.uniques.push({
        ...unique,
        properties: Utils.asArray(unique.properties).map(p => {
          return embeddedProp.embeddedProps[p].name;
        }),
      });
    }
  }
  initSingleTableInheritance(meta, metadata) {
    if (meta.root !== meta && !meta.__processed) {
      meta.root = metadata.find(m => m.class === meta.root.class);
      meta.root.__processed = true;
    } else {
      delete meta.root.__processed;
    }
    if (!meta.root.discriminatorColumn) {
      return;
    }
    meta.root.inheritanceType = 'sti';
    if (meta.root.discriminatorMap) {
      const map = meta.root.discriminatorMap;
      Object.keys(map)
        .filter(key => typeof map[key] === 'string')
        .forEach(key => (map[key] = this.#metadata.getByClassName(map[key]).class));
    } else {
      meta.root.discriminatorMap = {};
      const children = metadata
        .filter(m => m.root.class === meta.root.class && !m.abstract)
        .sort((a, b) => a.className.localeCompare(b.className));
      for (const m of children) {
        const name = m.discriminatorValue ?? this.#namingStrategy.classToTableName(m.className);
        meta.root.discriminatorMap[name] = m.class;
      }
    }
    meta.discriminatorValue = QueryHelper.findDiscriminatorValue(meta.root.discriminatorMap, meta.class);
    if (!meta.root.properties[meta.root.discriminatorColumn]) {
      this.createDiscriminatorProperty(meta.root);
    }
    Utils.defaultValue(
      meta.root.properties[meta.root.discriminatorColumn],
      'items',
      Object.keys(meta.root.discriminatorMap),
    );
    Utils.defaultValue(meta.root.properties[meta.root.discriminatorColumn], 'index', true);
    if (meta.root === meta) {
      return;
    }
    Object.values(meta.properties).forEach(prop => {
      const newProp = { ...prop };
      const rootProp = meta.root.properties[prop.name];
      // stiMerged is set during inlineProperties when a property was merged
      // from multiple polymorphic variants with different types. The flag is
      // cleared implicitly when the first child claims the root property via
      // addProperty below, so subsequent children correctly trigger renaming.
      const typesMatch = rootProp?.type === prop.type || rootProp?.stiMerged === true;
      if (
        rootProp &&
        (!typesMatch ||
          (rootProp.fieldNames && prop.fieldNames && !compareArrays(rootProp.fieldNames, prop.fieldNames)))
      ) {
        const name = newProp.name;
        this.initFieldName(newProp, newProp.object);
        newProp.renamedFrom = name;
        newProp.name = `${name}_${meta._id}`;
        meta.root.addProperty(newProp);
        this.initFieldName(prop, prop.object);
        // Track all field variants and map discriminator values to field names
        if (!rootProp.stiFieldNames) {
          this.initFieldName(rootProp, rootProp.object);
          rootProp.stiFieldNames = [...rootProp.fieldNames];
          rootProp.stiFieldNameMap = {};
          // Find which discriminator owns the original fieldNames
          for (const [discValue, childClass] of Object.entries(meta.root.discriminatorMap)) {
            const childMeta = this.#metadata.find(childClass);
            if (
              childMeta?.properties[prop.name]?.fieldNames &&
              compareArrays(childMeta.properties[prop.name].fieldNames, rootProp.fieldNames)
            ) {
              rootProp.stiFieldNameMap[discValue] = rootProp.fieldNames[0];
              break;
            }
          }
        }
        rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
        rootProp.stiFieldNames.push(...prop.fieldNames);
        newProp.nullable = true;
        newProp.name = name;
        newProp.hydrate = false;
        newProp.inherited = true;
        return;
      }
      if (prop.enum && prop.items && rootProp?.items) {
        newProp.items = Utils.unique([...rootProp.items, ...prop.items]);
      }
      newProp.nullable = true;
      newProp.inherited = !rootProp;
      meta.root.addProperty(newProp);
    });
    meta.tableName = meta.root.tableName;
    meta.root.indexes = Utils.unique([...meta.root.indexes, ...meta.indexes]);
    meta.root.uniques = Utils.unique([...meta.root.uniques, ...meta.uniques]);
    meta.root.checks = Utils.unique([...meta.root.checks, ...meta.checks]);
  }
  /**
   * First pass of TPT initialization: sets up hierarchy relationships
   * (inheritanceType, tptParent, tptChildren) before properties have fieldNames.
   */
  initTPTRelationships(meta, metadata) {
    if (meta.root !== meta) {
      meta.root = metadata.find(m => m.class === meta.root.class);
    }
    const inheritance = meta.inheritance;
    if (inheritance === 'tpt' && meta.root === meta) {
      meta.inheritanceType = 'tpt';
      meta.tptChildren = [];
    }
    if (meta.root.inheritanceType !== 'tpt') {
      return;
    }
    const parent = this.getTPTParent(meta, metadata);
    if (parent) {
      meta.tptParent = parent;
      meta.inheritanceType = 'tpt';
      parent.tptChildren ??= [];
      if (!parent.tptChildren.includes(meta)) {
        parent.tptChildren.push(meta);
      }
    }
  }
  /**
   * Second pass of TPT initialization: re-resolves metadata references after fieldNames
   * are set, syncs to registry metadata, and sets up discriminators.
   */
  finalizeTPTInheritance(meta, metadata) {
    if (meta.inheritanceType !== 'tpt') {
      return;
    }
    if (meta.tptParent) {
      meta.tptParent = metadata.find(m => m.class === meta.tptParent.class) ?? meta.tptParent;
    }
    if (meta.tptChildren) {
      meta.tptChildren = meta.tptChildren.map(child => metadata.find(m => m.class === child.class) ?? child);
    }
    const registryMeta = this.#metadata.get(meta.class);
    if (registryMeta && registryMeta !== meta) {
      registryMeta.inheritanceType = meta.inheritanceType;
      registryMeta.tptParent = meta.tptParent ? this.#metadata.get(meta.tptParent.class) : undefined;
      registryMeta.tptChildren = meta.tptChildren?.map(child => this.#metadata.get(child.class));
    }
    this.initTPTDiscriminator(meta, metadata);
  }
  /**
   * Initialize TPT discriminator map and virtual discriminator property.
   * Unlike STI where the discriminator is a persisted column, TPT discriminator is computed
   * at query time using CASE WHEN expressions based on which child table has data.
   */
  initTPTDiscriminator(meta, metadata) {
    const allDescendants = this.collectAllTPTDescendants(meta, metadata);
    allDescendants.sort((a, b) => this.getTPTDepth(b) - this.getTPTDepth(a));
    meta.allTPTDescendants = allDescendants;
    if (meta.root !== meta) {
      return;
    }
    meta.root.discriminatorMap = {};
    for (const m of allDescendants) {
      const name = this.#namingStrategy.classToTableName(m.className);
      meta.root.discriminatorMap[name] = m.class;
      m.discriminatorValue = name;
    }
    if (!meta.abstract) {
      const name = this.#namingStrategy.classToTableName(meta.className);
      meta.root.discriminatorMap[name] = meta.class;
      meta.discriminatorValue = name;
    }
    // Virtual discriminator property - computed at query time via CASE WHEN, not persisted
    const discriminatorColumn = '__tpt_type';
    if (!meta.root.properties[discriminatorColumn]) {
      meta.root.addProperty({
        name: discriminatorColumn,
        type: 'string',
        kind: ReferenceKind.SCALAR,
        persist: false,
        userDefined: false,
        hidden: true,
      });
    }
    meta.root.tptDiscriminatorColumn = discriminatorColumn;
  }
  /**
   * Recursively collect all TPT descendants (children, grandchildren, etc.)
   */
  collectAllTPTDescendants(meta, metadata) {
    const descendants = [];
    const collect = parent => {
      for (const child of parent.tptChildren ?? []) {
        const resolved = metadata.find(m => m.class === child.class) ?? child;
        if (!resolved.abstract) {
          descendants.push(resolved);
        }
        collect(resolved);
      }
    };
    collect(meta);
    return descendants;
  }
  /**
   * Computes ownProps for TPT entities - only properties defined in THIS entity,
   * not inherited from parent. Also creates synthetic join properties for parent/child relationships.
   *
   * Called multiple times during discovery as metadata is progressively built.
   * Each pass overwrites earlier results to reflect the final state of properties.
   */
  computeTPTOwnProps(meta) {
    if (meta.inheritanceType !== 'tpt') {
      return;
    }
    const belongsToTable = prop => prop.persist !== false || prop.primary;
    // Use meta.properties (object) since meta.props (array) may not be populated yet
    const allProps = Object.values(meta.properties);
    if (!meta.tptParent) {
      meta.ownProps = allProps.filter(belongsToTable);
      return;
    }
    const parentPropNames = new Set(Object.values(meta.tptParent.properties).map(p => p.name));
    meta.ownProps = allProps.filter(prop => !parentPropNames.has(prop.name) && belongsToTable(prop));
    // Create synthetic join properties for the parent-child relationship
    const childFieldNames = meta.getPrimaryProps().flatMap(p => p.fieldNames);
    const parentFieldNames = meta.tptParent.getPrimaryProps().flatMap(p => p.fieldNames);
    meta.tptParentProp = {
      name: '[tpt:parent]',
      kind: ReferenceKind.MANY_TO_ONE,
      targetMeta: meta.tptParent,
      fieldNames: childFieldNames,
      referencedColumnNames: parentFieldNames,
      persist: false,
    };
    meta.tptInverseProp = {
      name: '[tpt:child]',
      kind: ReferenceKind.ONE_TO_ONE,
      owner: true,
      targetMeta: meta,
      fieldNames: parentFieldNames,
      joinColumns: parentFieldNames,
      referencedColumnNames: childFieldNames,
      persist: false,
    };
  }
  /** Returns the depth of a TPT entity in its hierarchy (0 for root). */
  getTPTDepth(meta) {
    let depth = 0;
    let current = meta;
    while (current.tptParent) {
      depth++;
      current = current.tptParent;
    }
    return depth;
  }
  /**
   * Find the direct TPT parent entity for the given entity.
   */
  getTPTParent(meta, metadata) {
    if (meta.root === meta) {
      return undefined;
    }
    return metadata.find(m => {
      const ext = meta.extends;
      if (EntitySchema.is(ext)) {
        return m.class === ext.meta.class || m.className === ext.meta.className;
      }
      return m.class === ext || m.className === Utils.className(ext);
    });
  }
  createDiscriminatorProperty(meta) {
    meta.addProperty({
      name: meta.discriminatorColumn,
      type: 'string',
      enum: true,
      kind: ReferenceKind.SCALAR,
      userDefined: false,
    });
  }
  initAutoincrement(meta) {
    const pks = meta.getPrimaryProps();
    if (pks.length === 1 && this.#platform.isNumericProperty(pks[0])) {
      /* v8 ignore next */
      pks[0].autoincrement ??= true;
    }
  }
  createSchemaTable(meta) {
    const qualifiedName = meta.schema ? `${meta.schema}.${meta.tableName}` : meta.tableName;
    return {
      name: meta.tableName,
      schema: meta.schema,
      qualifiedName,
      toString: () => qualifiedName,
    };
  }
  initCheckConstraints(meta) {
    const columns = meta.createSchemaColumnMappingObject();
    const table = this.createSchemaTable(meta);
    for (const check of meta.checks) {
      const fieldNames = check.property ? meta.properties[check.property].fieldNames : [];
      check.name ??= this.#namingStrategy.indexName(meta.tableName, fieldNames, 'check');
      if (check.expression instanceof Function) {
        check.expression = check.expression(columns, table);
      }
    }
    if (this.#platform.usesEnumCheckConstraints() && !meta.embeddable) {
      for (const prop of meta.props) {
        if (
          prop.enum &&
          prop.persist !== false &&
          !prop.nativeEnumName &&
          prop.items?.every(item => typeof item === 'string')
        ) {
          this.initFieldName(prop);
          meta.checks.push({
            name: this.#namingStrategy.indexName(meta.tableName, prop.fieldNames, 'check'),
            property: prop.name,
            expression: `${this.#platform.quoteIdentifier(prop.fieldNames[0])} in ('${prop.items.join("', '")}')`,
          });
        }
      }
    }
  }
  initGeneratedColumn(meta, prop) {
    if (!prop.generated && prop.columnTypes) {
      const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
      if (match) {
        prop.columnTypes[0] = match[1];
        prop.generated = match[2];
        return;
      }
      const match2 = /^as (.*)/i.exec(prop.columnTypes[0]?.trim());
      if (match2) {
        prop.generated = match2[1];
      }
      return;
    }
    if (prop.generated instanceof Function) {
      const columns = meta.createSchemaColumnMappingObject();
      prop.generated = prop.generated(columns, this.createSchemaTable(meta));
    }
  }
  getDefaultVersionValue(meta, prop) {
    if (typeof prop.defaultRaw !== 'undefined') {
      return prop.defaultRaw;
    }
    /* v8 ignore next */
    if (prop.default != null) {
      return '' + this.#platform.convertVersionValue(prop.default, prop);
    }
    this.initCustomType(meta, prop, true);
    const type = prop.customType?.runtimeType ?? prop.runtimeType ?? prop.type;
    if (type === 'Date') {
      prop.length ??= this.#platform.getDefaultVersionLength();
      return this.#platform.getCurrentTimestampSQL(prop.length);
    }
    return '1';
  }
  inferDefaultValue(meta, prop) {
    try {
      // try to create two entity instances to detect the value is stable
      const now = Date.now();
      const entity1 = new meta.class();
      const entity2 = new meta.class();
      // we compare the two values by reference, this will discard things like `new Date()` or `Date.now()`
      if (
        this.#config.get('discovery').inferDefaultValues &&
        prop.default === undefined &&
        entity1[prop.name] != null &&
        entity1[prop.name] === entity2[prop.name] &&
        entity1[prop.name] !== now
      ) {
        prop.default ??= entity1[prop.name];
      }
      // if the default value is null, infer nullability
      if (entity1[prop.name] === null) {
        prop.nullable ??= true;
      }
      // but still use object values for type inference if not explicitly set, e.g. `createdAt = new Date()`
      if (prop.kind === ReferenceKind.SCALAR && prop.type == null && entity1[prop.name] != null) {
        prop.type = prop.runtimeType = Utils.getObjectType(entity1[prop.name]);
      }
    } catch {
      // ignore
    }
  }
  initDefaultValue(prop) {
    if (prop.defaultRaw || !('default' in prop)) {
      return;
    }
    let val = prop.default;
    const raw = Raw.getKnownFragment(val);
    if (raw) {
      prop.defaultRaw = this.#platform.formatQuery(raw.sql, raw.params);
      return;
    }
    if (Array.isArray(prop.default) && prop.customType) {
      val = prop.customType.convertToDatabaseValue(prop.default, this.#platform);
    }
    prop.defaultRaw = typeof val === 'string' ? `'${val}'` : '' + val;
  }
  inferTypeFromDefault(prop) {
    if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') {
      return;
    }
    switch (typeof prop.default) {
      case 'string':
        prop.type = prop.runtimeType = 'string';
        break;
      case 'number':
        prop.type = prop.runtimeType = 'number';
        break;
      case 'boolean':
        prop.type = prop.runtimeType = 'boolean';
        break;
    }
    if (prop.defaultRaw?.startsWith('current_timestamp')) {
      prop.type = prop.runtimeType = 'Date';
    }
  }
  initVersionProperty(meta, prop) {
    if (prop.version) {
      this.initDefaultValue(prop);
      meta.versionProperty = prop.name;
      prop.defaultRaw = this.getDefaultVersionValue(meta, prop);
    }
    if (prop.concurrencyCheck && !prop.primary) {
      meta.concurrencyCheckKeys.add(prop.name);
    }
  }
  initCustomType(meta, prop, simple = false, objectEmbeddable = false) {
    // `prop.type` might be actually instance of custom type class
    if (Type.isMappedType(prop.type) && !prop.customType) {
      prop.customType = prop.type;
      prop.type = prop.customType.constructor.name;
    }
    // `prop.type` might also be custom type class (not instance), so `typeof MyType` will give us `function`, not `object`
    if (typeof prop.type === 'function' && Type.isMappedType(prop.type.prototype) && !prop.customType) {
      // if the type is an ORM defined mapped type without `ensureComparable: true`,
      // we use just the type name, to have more performant hydration code
      const brand = Type.getOrmType(prop.type);
      const type = Utils.keys(t).find(type => {
        return !Type.getType(t[type]).ensureComparable(meta, prop) && (prop.type === t[type] || brand === type);
      });
      if (type) {
        prop.type = type === 'datetime' ? 'Date' : type;
      } else {
        prop.customType = new prop.type();
        prop.type = prop.customType.constructor.name;
      }
    }
    if (simple) {
      return;
    }
    if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
      prop.customType = new t.json();
    }
    if (
      prop.kind === ReferenceKind.SCALAR &&
      !prop.customType &&
      prop.columnTypes &&
      ['json', 'jsonb'].includes(prop.columnTypes[0])
    ) {
      prop.customType = new t.json();
    }
    if (prop.kind === ReferenceKind.EMBEDDED && !prop.customType && (prop.object || prop.array)) {
      prop.customType = new t.json();
    }
    if (!prop.customType && prop.array && prop.items) {
      prop.customType = new t.enumArray(`${meta.className}.${prop.name}`, prop.items);
    }
    const isArray = prop.type?.toLowerCase() === 'array' || prop.type?.toString().endsWith('[]');
    if (objectEmbeddable && !prop.customType && isArray) {
      prop.customType = new t.json();
    }
    // for number arrays we make sure to convert the items to numbers
    if (!prop.customType && prop.type === 'number[]') {
      prop.customType = new t.array(i => +i);
    }
    // `string[]` can be returned via ts-morph, while reflect metadata will give us just `array`
    if (!prop.customType && isArray) {
      prop.customType = new t.array();
    }
    if (!prop.customType && prop.type?.toLowerCase() === 'buffer') {
      prop.customType = new t.blob();
    }
    if (!prop.customType && prop.type?.toLowerCase() === 'uint8array') {
      prop.customType = new t.uint8array();
    }
    const mappedType = this.getMappedType(prop);
    if (prop.fieldNames?.length === 1 && !prop.customType) {
      [t.bigint, t.double, t.decimal, t.interval, t.date]
        .filter(type => mappedType instanceof type)
        .forEach(type => (prop.customType = new type()));
    }
    if (prop.customType && !prop.columnTypes) {
      const mappedType = this.getMappedType({
        columnTypes: [prop.customType.getColumnType(prop, this.#platform)],
      });
      if (prop.customType.compareAsType() === 'any' && ![t.json].some(t => prop.customType instanceof t)) {
        prop.runtimeType ??= mappedType.runtimeType;
      } else {
        prop.runtimeType ??= prop.customType.runtimeType;
      }
    } else if (prop.runtimeType === 'object') {
      prop.runtimeType = mappedType.runtimeType;
    } else {
      prop.runtimeType ??= mappedType.runtimeType;
    }
    if (prop.customType) {
      prop.customType.platform = this.#platform;
      prop.customType.meta = meta;
      prop.customType.prop = prop;
      prop.columnTypes ??= [prop.customType.getColumnType(prop, this.#platform)];
      prop.hasConvertToJSValueSQL =
        !!prop.customType.convertToJSValueSQL && prop.customType.convertToJSValueSQL('', this.#platform) !== '';
      prop.hasConvertToDatabaseValueSQL =
        !!prop.customType.convertToDatabaseValueSQL &&
        prop.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
      if (
        prop.customType instanceof t.bigint &&
        ['string', 'bigint', 'number'].includes(prop.runtimeType.toLowerCase())
      ) {
        prop.customType.mode = prop.runtimeType.toLowerCase();
      }
    }
    if (Type.isMappedType(prop.customType) && prop.kind === ReferenceKind.SCALAR && !isArray) {
      prop.type = prop.customType.name;
    }
    if (
      !prop.customType &&
      [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
      !prop.polymorphic &&
      prop.targetMeta.compositePK
    ) {
      prop.customTypes = [];
      for (const pk of prop.targetMeta.getPrimaryProps()) {
        if (pk.customType) {
          prop.customTypes.push(pk.customType);
          prop.hasConvertToJSValueSQL ||=
            !!pk.customType.convertToJSValueSQL && pk.customType.convertToJSValueSQL('', this.#platform) !== '';
          /* v8 ignore next */
          prop.hasConvertToDatabaseValueSQL ||=
            !!pk.customType.convertToDatabaseValueSQL &&
            pk.customType.convertToDatabaseValueSQL('', this.#platform) !== '';
        } else {
          prop.customTypes.push(undefined);
        }
      }
    }
    if (prop.kind === ReferenceKind.SCALAR && !(mappedType instanceof t.unknown)) {
      if (
        !prop.columnTypes &&
        prop.nativeEnumName &&
        meta.schema !== this.#platform.getDefaultSchemaName() &&
        meta.schema &&
        !prop.nativeEnumName.includes('.')
      ) {
        prop.columnTypes = [`${meta.schema}.${prop.nativeEnumName}`];
      } else {
        prop.columnTypes ??= [mappedType.getColumnType(prop, this.#platform)];
      }
      // use only custom types provided by user, we don't need to use the ones provided by ORM,
      // with exception for ArrayType and JsonType, those two are handled in
      if (!Object.values(t).some(type => type === mappedType.constructor)) {
        prop.customType ??= mappedType;
      }
    }
  }
  initRelation(prop) {
    if (prop.kind === ReferenceKind.SCALAR || prop.polymorphTargets) {
      return;
    }
    // when the target is a polymorphic embedded entity, `prop.target` is an array of classes, we need to get the metadata by the type name instead
    const meta2 = this.#metadata.find(prop.target) ?? this.#metadata.getByClassName(prop.type);
    // If targetKey is specified, use that property instead of PKs for referencedPKs
    prop.referencedPKs = prop.targetKey ? [prop.targetKey] : meta2.primaryKeys;
    prop.targetMeta = meta2;
    if (meta2.view) {
      prop.createForeignKeyConstraint = false;
    }
    // Auto-generate formula for persist: false relations, but only for single-column FKs
    // Composite FK relations need standard JOIN conditions, not formula-based
    if (
      !prop.formula &&
      prop.persist === false &&
      [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
      !prop.embedded
    ) {
      this.initFieldName(prop);
      if (prop.fieldNames?.length === 1) {
        prop.formula = table => `${table}.${this.#platform.quoteIdentifier(prop.fieldNames[0])}`;
      }
    }
  }
  initColumnType(prop) {
    this.initUnsigned(prop);
    // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
    const targetProps = prop.targetMeta
      ? prop.targetKey
        ? [prop.targetMeta.properties[prop.targetKey]]
        : prop.targetMeta.getPrimaryProps()
      : [];
    targetProps.map(targetProp => {
      prop.length ??= targetProp.length;
      prop.precision ??= targetProp.precision;
      prop.scale ??= targetProp.scale;
    });
    if (prop.kind === ReferenceKind.SCALAR && (prop.type == null || prop.type === 'object') && prop.columnTypes?.[0]) {
      delete prop.type;
      const mappedType = this.getMappedType(prop);
      prop.type = mappedType.compareAsType();
    }
    if (prop.columnTypes || !this.#schemaHelper) {
      return;
    }
    if (prop.kind === ReferenceKind.SCALAR) {
      const mappedType = this.getMappedType(prop);
      const SCALAR_TYPES = ['string', 'number', 'boolean', 'bigint', 'Date', 'Buffer', 'RegExp', 'any', 'unknown'];
      if (
        mappedType instanceof t.unknown &&
        // it could be a runtime type from reflect-metadata
        !SCALAR_TYPES.includes(prop.type) &&
        // or it might be inferred via ts-morph to some generic type alias
        !/[<>:"';{}]/.exec(prop.type)
      ) {
        const type =
          prop.length != null && !prop.type.endsWith(`(${prop.length})`) ? `${prop.type}(${prop.length})` : prop.type;
        prop.columnTypes = [type];
      } else {
        prop.columnTypes = [mappedType.getColumnType(prop, this.#platform)];
      }
      return;
    }
    /* v8 ignore next */
    if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
      prop.columnTypes = [this.#platform.getJsonDeclarationSQL()];
      return;
    }
    const targetMeta = prop.targetMeta;
    prop.columnTypes = [];
    // Use targetKey property if specified, otherwise use primary key properties
    const referencedProps = prop.targetKey ? [targetMeta.properties[prop.targetKey]] : targetMeta.getPrimaryProps();
    if (prop.polymorphic && prop.polymorphTargets) {
      prop.columnTypes.push(this.#platform.getVarcharTypeDeclarationSQL(prop));
    }
    for (const referencedProp of referencedProps) {
      this.initCustomType(targetMeta, referencedProp);
      this.initColumnType(referencedProp);
      const mappedType = this.getMappedType(referencedProp);
      let columnTypes = referencedProp.columnTypes;
      if (referencedProp.autoincrement) {
        columnTypes = [mappedType.getColumnType({ ...referencedProp, autoincrement: false }, this.#platform)];
      }
      prop.columnTypes.push(...columnTypes);
      if (!targetMeta.compositePK || prop.targetKey) {
        prop.customType = referencedProp.customType;
      }
    }
  }
  getMappedType(prop) {
    if (prop.customType) {
      return prop.customType;
    }
    /* v8 ignore next */
    let t = prop.columnTypes?.[0] ?? prop.type ?? '';
    if (prop.nativeEnumName) {
      t = 'enum';
    } else if (prop.enum) {
      t = prop.items?.every(item => typeof item === 'string') ? 'enum' : 'tinyint';
    }
    if (t === 'Date') {
      t = 'datetime';
    }
    return this.#platform.getMappedType(t);
  }
  getPrefix(prop, parent) {
    const { embeddedPath = [], fieldNames, prefix = true, prefixMode } = prop;
    if (prefix === true) {
      return (embeddedPath.length ? embeddedPath.join('_') : fieldNames[0]) + '_';
    }
    const prefixParent = parent ? this.getPrefix(parent, null) : '';
    if (prefix === false) {
      return prefixParent;
    }
    const mode = prefixMode ?? this.#config.get('embeddables').prefixMode;
    return mode === 'absolute' ? prefix : prefixParent + prefix;
  }
  initUnsigned(prop) {
    if (prop.unsigned != null) {
      return;
    }
    if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
      prop.unsigned = prop.targetMeta.getPrimaryProps().some(pk => {
        this.initUnsigned(pk);
        return pk.unsigned;
      });
      return;
    }
    prop.unsigned ??=
      (prop.primary || prop.unsigned) && this.#platform.isNumericProperty(prop) && this.#platform.supportsUnsigned();
  }
  initIndexes(meta, prop) {
    const hasIndex = meta.indexes.some(idx => idx.properties?.length === 1 && idx.properties[0] === prop.name);
    if (prop.kind === ReferenceKind.MANY_TO_ONE && this.#platform.indexForeignKeys() && !hasIndex) {
      prop.index ??= true;
    }
  }
  shouldForceConstructorUsage(meta) {
    const forceConstructor = this.#config.get('forceEntityConstructor');
    if (Array.isArray(forceConstructor)) {
      return forceConstructor.some(cls => Utils.className(cls) === meta.className);
    }
    return forceConstructor;
  }
}
