- <?php
- declare(strict_types=1);
- namespace Doctrine\ORM\Persisters\Entity;
- use BackedEnum;
- use Doctrine\Common\Collections\Criteria;
- use Doctrine\Common\Collections\Expr\Comparison;
- use Doctrine\DBAL\Connection;
- use Doctrine\DBAL\LockMode;
- use Doctrine\DBAL\Platforms\AbstractPlatform;
- use Doctrine\DBAL\Result;
- use Doctrine\DBAL\Types\Type;
- use Doctrine\DBAL\Types\Types;
- use Doctrine\Deprecations\Deprecation;
- use Doctrine\ORM\EntityManagerInterface;
- use Doctrine\ORM\Internal\CriteriaOrderings;
- use Doctrine\ORM\Mapping\ClassMetadata;
- use Doctrine\ORM\Mapping\MappingException;
- use Doctrine\ORM\Mapping\QuoteStrategy;
- use Doctrine\ORM\OptimisticLockException;
- use Doctrine\ORM\PersistentCollection;
- use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
- use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
- use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
- use Doctrine\ORM\Persisters\SqlExpressionVisitor;
- use Doctrine\ORM\Persisters\SqlValueVisitor;
- use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
- use Doctrine\ORM\Query;
- use Doctrine\ORM\Query\QueryException;
- use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
- use Doctrine\ORM\UnitOfWork;
- use Doctrine\ORM\Utility\IdentifierFlattener;
- use Doctrine\ORM\Utility\LockSqlHelper;
- use Doctrine\ORM\Utility\PersisterHelper;
- use LengthException;
- use function array_combine;
- use function array_keys;
- use function array_map;
- use function array_merge;
- use function array_search;
- use function array_unique;
- use function array_values;
- use function assert;
- use function count;
- use function implode;
- use function is_array;
- use function is_object;
- use function reset;
- use function spl_object_id;
- use function sprintf;
- use function str_contains;
- use function strtoupper;
- use function trim;
- /**
-  * A BasicEntityPersister maps an entity to a single table in a relational database.
-  *
-  * A persister is always responsible for a single entity type.
-  *
-  * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
-  * state of entities onto a relational database when the UnitOfWork is committed,
-  * as well as for basic querying of entities and their associations (not DQL).
-  *
-  * The persisting operations that are invoked during a commit of a UnitOfWork to
-  * persist the persistent entity state are:
-  *
-  *   - {@link addInsert} : To schedule an entity for insertion.
-  *   - {@link executeInserts} : To execute all scheduled insertions.
-  *   - {@link update} : To update the persistent state of an entity.
-  *   - {@link delete} : To delete the persistent state of an entity.
-  *
-  * As can be seen from the above list, insertions are batched and executed all at once
-  * for increased efficiency.
-  *
-  * The querying operations invoked during a UnitOfWork, either through direct find
-  * requests or lazy-loading, are the following:
-  *
-  *   - {@link load} : Loads (the state of) a single, managed entity.
-  *   - {@link loadAll} : Loads multiple, managed entities.
-  *   - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
-  *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
-  *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
-  *
-  * The BasicEntityPersister implementation provides the default behavior for
-  * persisting and querying entities that are mapped to a single database table.
-  *
-  * Subclasses can be created to provide custom persisting and querying strategies,
-  * i.e. spanning multiple tables.
-  *
-  * @phpstan-import-type AssociationMapping from ClassMetadata
-  */
- class BasicEntityPersister implements EntityPersister
- {
-     use CriteriaOrderings;
-     use LockSqlHelper;
-     /** @var array<string,string> */
-     private static $comparisonMap = [
-         Comparison::EQ          => '= %s',
-         Comparison::NEQ         => '!= %s',
-         Comparison::GT          => '> %s',
-         Comparison::GTE         => '>= %s',
-         Comparison::LT          => '< %s',
-         Comparison::LTE         => '<= %s',
-         Comparison::IN          => 'IN (%s)',
-         Comparison::NIN         => 'NOT IN (%s)',
-         Comparison::CONTAINS    => 'LIKE %s',
-         Comparison::STARTS_WITH => 'LIKE %s',
-         Comparison::ENDS_WITH   => 'LIKE %s',
-     ];
-     /**
-      * Metadata object that describes the mapping of the mapped entity class.
-      *
-      * @var ClassMetadata
-      */
-     protected $class;
-     /**
-      * The underlying DBAL Connection of the used EntityManager.
-      *
-      * @var Connection $conn
-      */
-     protected $conn;
-     /**
-      * The database platform.
-      *
-      * @var AbstractPlatform
-      */
-     protected $platform;
-     /**
-      * The EntityManager instance.
-      *
-      * @var EntityManagerInterface
-      */
-     protected $em;
-     /**
-      * Queued inserts.
-      *
-      * @phpstan-var array<int, object>
-      */
-     protected $queuedInserts = [];
-     /**
-      * The map of column names to DBAL mapping types of all prepared columns used
-      * when INSERTing or UPDATEing an entity.
-      *
-      * @see prepareInsertData($entity)
-      * @see prepareUpdateData($entity)
-      *
-      * @var mixed[]
-      */
-     protected $columnTypes = [];
-     /**
-      * The map of quoted column names.
-      *
-      * @see prepareInsertData($entity)
-      * @see prepareUpdateData($entity)
-      *
-      * @var mixed[]
-      */
-     protected $quotedColumns = [];
-     /**
-      * The quote strategy.
-      *
-      * @var QuoteStrategy
-      */
-     protected $quoteStrategy;
-     /**
-      * The IdentifierFlattener used for manipulating identifiers
-      *
-      * @var IdentifierFlattener
-      */
-     protected $identifierFlattener;
-     /** @var CachedPersisterContext */
-     protected $currentPersisterContext;
-     /** @var CachedPersisterContext */
-     private $limitsHandlingContext;
-     /** @var CachedPersisterContext */
-     private $noLimitsContext;
-     /** @var ?string */
-     private $filterHash = null;
-     /**
-      * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
-      * and persists instances of the class described by the given ClassMetadata descriptor.
-      */
-     public function __construct(EntityManagerInterface $em, ClassMetadata $class)
-     {
-         $this->em                    = $em;
-         $this->class                 = $class;
-         $this->conn                  = $em->getConnection();
-         $this->platform              = $this->conn->getDatabasePlatform();
-         $this->quoteStrategy         = $em->getConfiguration()->getQuoteStrategy();
-         $this->identifierFlattener   = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
-         $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
-             $class,
-             new Query\ResultSetMapping(),
-             false
-         );
-         $this->limitsHandlingContext = new CachedPersisterContext(
-             $class,
-             new Query\ResultSetMapping(),
-             true
-         );
-     }
-     final protected function isFilterHashUpToDate(): bool
-     {
-         return $this->filterHash === $this->em->getFilters()->getHash();
-     }
-     final protected function updateFilterHash(): void
-     {
-         $this->filterHash = $this->em->getFilters()->getHash();
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getClassMetadata()
-     {
-         return $this->class;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getResultSetMapping()
-     {
-         return $this->currentPersisterContext->rsm;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function addInsert($entity)
-     {
-         $this->queuedInserts[spl_object_id($entity)] = $entity;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getInserts()
-     {
-         return $this->queuedInserts;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function executeInserts()
-     {
-         if (! $this->queuedInserts) {
-             return;
-         }
-         $uow            = $this->em->getUnitOfWork();
-         $idGenerator    = $this->class->idGenerator;
-         $isPostInsertId = $idGenerator->isPostInsertGenerator();
-         $stmt      = $this->conn->prepare($this->getInsertSQL());
-         $tableName = $this->class->getTableName();
-         foreach ($this->queuedInserts as $key => $entity) {
-             $insertData = $this->prepareInsertData($entity);
-             if (isset($insertData[$tableName])) {
-                 $paramIndex = 1;
-                 foreach ($insertData[$tableName] as $column => $value) {
-                     $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
-                 }
-             }
-             $stmt->executeStatement();
-             if ($isPostInsertId) {
-                 $generatedId = $idGenerator->generateId($this->em, $entity);
-                 $id          = [$this->class->identifier[0] => $generatedId];
-                 $uow->assignPostInsertId($entity, $generatedId);
-             } else {
-                 $id = $this->class->getIdentifierValues($entity);
-             }
-             if ($this->class->requiresFetchAfterChange) {
-                 $this->assignDefaultVersionAndUpsertableValues($entity, $id);
-             }
-             // Unset this queued insert, so that the prepareUpdateData() method (called via prepareInsertData() method)
-             // knows right away (for the next entity already) that the current entity has been written to the database
-             // and no extra updates need to be scheduled to refer to it.
-             //
-             // In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities
-             // from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they
-             // were given to our addInsert() method.
-             unset($this->queuedInserts[$key]);
-         }
-     }
-     /**
-      * Retrieves the default version value which was created
-      * by the preceding INSERT statement and assigns it back in to the
-      * entities version field if the given entity is versioned.
-      * Also retrieves values of columns marked as 'non insertable' and / or
-      * 'not updatable' and assigns them back to the entities corresponding fields.
-      *
-      * @param object  $entity
-      * @param mixed[] $id
-      *
-      * @return void
-      */
-     protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
-     {
-         $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);
-         foreach ($values as $field => $value) {
-             $value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);
-             $this->class->setFieldValue($entity, $field, $value);
-         }
-     }
-     /**
-      * Fetches the current version value of a versioned entity and / or the values of fields
-      * marked as 'not insertable' and / or 'not updatable'.
-      *
-      * @param ClassMetadata $versionedClass
-      * @param mixed[]       $id
-      *
-      * @return mixed
-      */
-     protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
-     {
-         $columnNames = [];
-         foreach ($this->class->fieldMappings as $key => $column) {
-             if (isset($column['generated']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {
-                 $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);
-             }
-         }
-         $tableName  = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
-         $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
-         // FIXME: Order with composite keys might not be correct
-         $sql = 'SELECT ' . implode(', ', $columnNames)
-             . ' FROM ' . $tableName
-             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
-         $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
-         $values = $this->conn->fetchNumeric(
-             $sql,
-             array_values($flatId),
-             $this->extractIdentifierTypes($id, $versionedClass)
-         );
-         if ($values === false) {
-             throw new LengthException('Unexpected empty result for database query.');
-         }
-         $values = array_combine(array_keys($columnNames), $values);
-         if (! $values) {
-             throw new LengthException('Unexpected number of database columns.');
-         }
-         return $values;
-     }
-     /**
-      * @param mixed[] $id
-      *
-      * @return int[]|null[]|string[]
-      * @phpstan-return list<int|string|null>
-      */
-     final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
-     {
-         $types = [];
-         foreach ($id as $field => $value) {
-             $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
-         }
-         return $types;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function update($entity)
-     {
-         $tableName  = $this->class->getTableName();
-         $updateData = $this->prepareUpdateData($entity);
-         if (! isset($updateData[$tableName])) {
-             return;
-         }
-         $data = $updateData[$tableName];
-         if (! $data) {
-             return;
-         }
-         $isVersioned     = $this->class->isVersioned;
-         $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
-         $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
-         if ($this->class->requiresFetchAfterChange) {
-             $id = $this->class->getIdentifierValues($entity);
-             $this->assignDefaultVersionAndUpsertableValues($entity, $id);
-         }
-     }
-     /**
-      * Performs an UPDATE statement for an entity on a specific table.
-      * The UPDATE can optionally be versioned, which requires the entity to have a version field.
-      *
-      * @param object  $entity          The entity object being updated.
-      * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
-      * @param mixed[] $updateData      The map of columns to update (column => value).
-      * @param bool    $versioned       Whether the UPDATE should be versioned.
-      *
-      * @throws UnrecognizedField
-      * @throws OptimisticLockException
-      */
-     final protected function updateTable(
-         $entity,
-         $quotedTableName,
-         array $updateData,
-         $versioned = false
-     ): void {
-         $set    = [];
-         $types  = [];
-         $params = [];
-         foreach ($updateData as $columnName => $value) {
-             $placeholder = '?';
-             $column      = $columnName;
-             switch (true) {
-                 case isset($this->class->fieldNames[$columnName]):
-                     $fieldName = $this->class->fieldNames[$columnName];
-                     $column    = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
-                     if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
-                         $type        = Type::getType($this->columnTypes[$columnName]);
-                         $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
-                     }
-                     break;
-                 case isset($this->quotedColumns[$columnName]):
-                     $column = $this->quotedColumns[$columnName];
-                     break;
-             }
-             $params[] = $value;
-             $set[]    = $column . ' = ' . $placeholder;
-             $types[]  = $this->columnTypes[$columnName];
-         }
-         $where      = [];
-         $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
-         foreach ($this->class->identifier as $idField) {
-             if (! isset($this->class->associationMappings[$idField])) {
-                 $params[] = $identifier[$idField];
-                 $types[]  = $this->class->fieldMappings[$idField]['type'];
-                 $where[]  = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
-                 continue;
-             }
-             $params[] = $identifier[$idField];
-             $where[]  = $this->quoteStrategy->getJoinColumnName(
-                 $this->class->associationMappings[$idField]['joinColumns'][0],
-                 $this->class,
-                 $this->platform
-             );
-             $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
-             $targetType    = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
-             if ($targetType === []) {
-                 throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]);
-             }
-             $types[] = reset($targetType);
-         }
-         if ($versioned) {
-             $versionField = $this->class->versionField;
-             assert($versionField !== null);
-             $versionFieldType = $this->class->fieldMappings[$versionField]['type'];
-             $versionColumn    = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
-             $where[]  = $versionColumn;
-             $types[]  = $this->class->fieldMappings[$versionField]['type'];
-             $params[] = $this->class->reflFields[$versionField]->getValue($entity);
-             switch ($versionFieldType) {
-                 case Types::SMALLINT:
-                 case Types::INTEGER:
-                 case Types::BIGINT:
-                     $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
-                     break;
-                 case Types::DATETIME_MUTABLE:
-                     $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
-                     break;
-             }
-         }
-         $sql = 'UPDATE ' . $quotedTableName
-              . ' SET ' . implode(', ', $set)
-              . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
-         $result = $this->conn->executeStatement($sql, $params, $types);
-         if ($versioned && ! $result) {
-             throw OptimisticLockException::lockFailed($entity);
-         }
-     }
-     /**
-      * @param array<mixed> $identifier
-      * @param string[]     $types
-      *
-      * @todo Add check for platform if it supports foreign keys/cascading.
-      */
-     protected function deleteJoinTableRecords(array $identifier, array $types): void
-     {
-         foreach ($this->class->associationMappings as $mapping) {
-             if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY || isset($mapping['isOnDeleteCascade'])) {
-                 continue;
-             }
-             // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
-             // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
-             $selfReferential = ($mapping['targetEntity'] === $mapping['sourceEntity']);
-             $class           = $this->class;
-             $association     = $mapping;
-             $otherColumns    = [];
-             $otherKeys       = [];
-             $keys            = [];
-             if (! $mapping['isOwningSide']) {
-                 $class       = $this->em->getClassMetadata($mapping['targetEntity']);
-                 $association = $class->associationMappings[$mapping['mappedBy']];
-             }
-             $joinColumns = $mapping['isOwningSide']
-                 ? $association['joinTable']['joinColumns']
-                 : $association['joinTable']['inverseJoinColumns'];
-             if ($selfReferential) {
-                 $otherColumns = ! $mapping['isOwningSide']
-                     ? $association['joinTable']['joinColumns']
-                     : $association['joinTable']['inverseJoinColumns'];
-             }
-             foreach ($joinColumns as $joinColumn) {
-                 $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
-             }
-             foreach ($otherColumns as $joinColumn) {
-                 $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
-             }
-             $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
-             $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
-             if ($selfReferential) {
-                 $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types);
-             }
-         }
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function delete($entity)
-     {
-         $class      = $this->class;
-         $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
-         $tableName  = $this->quoteStrategy->getTableName($class, $this->platform);
-         $idColumns  = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
-         $id         = array_combine($idColumns, $identifier);
-         $types      = $this->getClassIdentifiersTypes($class);
-         $this->deleteJoinTableRecords($identifier, $types);
-         return (bool) $this->conn->delete($tableName, $id, $types);
-     }
-     /**
-      * Prepares the changeset of an entity for database insertion (UPDATE).
-      *
-      * The changeset is obtained from the currently running UnitOfWork.
-      *
-      * During this preparation the array that is passed as the second parameter is filled with
-      * <columnName> => <value> pairs, grouped by table name.
-      *
-      * Example:
-      * <code>
-      * array(
-      *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
-      *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
-      *    ...
-      * )
-      * </code>
-      *
-      * @param object $entity   The entity for which to prepare the data.
-      * @param bool   $isInsert Whether the data to be prepared refers to an insert statement.
-      *
-      * @return mixed[][] The prepared data.
-      * @phpstan-return array<string, array<array-key, mixed|null>>
-      */
-     protected function prepareUpdateData($entity, bool $isInsert = false)
-     {
-         $versionField = null;
-         $result       = [];
-         $uow          = $this->em->getUnitOfWork();
-         $versioned = $this->class->isVersioned;
-         if ($versioned !== false) {
-             $versionField = $this->class->versionField;
-         }
-         foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
-             if (isset($versionField) && $versionField === $field) {
-                 continue;
-             }
-             if (isset($this->class->embeddedClasses[$field])) {
-                 continue;
-             }
-             $newVal = $change[1];
-             if (! isset($this->class->associationMappings[$field])) {
-                 $fieldMapping = $this->class->fieldMappings[$field];
-                 $columnName   = $fieldMapping['columnName'];
-                 if (! $isInsert && isset($fieldMapping['notUpdatable'])) {
-                     continue;
-                 }
-                 if ($isInsert && isset($fieldMapping['notInsertable'])) {
-                     continue;
-                 }
-                 $this->columnTypes[$columnName] = $fieldMapping['type'];
-                 $result[$this->getOwningTable($field)][$columnName] = $newVal;
-                 continue;
-             }
-             $assoc = $this->class->associationMappings[$field];
-             // Only owning side of x-1 associations can have a FK column.
-             if (! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
-                 continue;
-             }
-             if ($newVal !== null) {
-                 $oid = spl_object_id($newVal);
-                 // If the associated entity $newVal is not yet persisted and/or does not yet have
-                 // an ID assigned, we must set $newVal = null. This will insert a null value and
-                 // schedule an extra update on the UnitOfWork.
-                 //
-                 // This gives us extra time to a) possibly obtain a database-generated identifier
-                 // value for $newVal, and b) insert $newVal into the database before the foreign
-                 // key reference is being made.
-                 //
-                 // When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware
-                 // of the implementation details that our own executeInserts() method will remove
-                 // entities from the former as soon as the insert statement has been executed and
-                 // a post-insert ID has been assigned (if necessary), and that the UnitOfWork has
-                 // already removed entities from its own list at the time they were passed to our
-                 // addInsert() method.
-                 //
-                 // Then, there is one extra exception we can make: An entity that references back to itself
-                 // _and_ uses an application-provided ID (the "NONE" generator strategy) also does not
-                 // need the extra update, although it is still in the list of insertions itself.
-                 // This looks like a minor optimization at first, but is the capstone for being able to
-                 // use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs).
-                 if (
-                     (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal))
-                     && ! ($newVal === $entity && $this->class->isIdentifierNatural())
-                 ) {
-                     $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
-                     $newVal = null;
-                 }
-             }
-             $newValId = null;
-             if ($newVal !== null) {
-                 $newValId = $uow->getEntityIdentifier($newVal);
-             }
-             $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
-             $owningTable = $this->getOwningTable($field);
-             foreach ($assoc['joinColumns'] as $joinColumn) {
-                 $sourceColumn = $joinColumn['name'];
-                 $targetColumn = $joinColumn['referencedColumnName'];
-                 $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                 $this->quotedColumns[$sourceColumn]  = $quotedColumn;
-                 $this->columnTypes[$sourceColumn]    = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
-                 $result[$owningTable][$sourceColumn] = $newValId
-                     ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
-                     : null;
-             }
-         }
-         return $result;
-     }
-     /**
-      * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
-      * The changeset of the entity is obtained from the currently running UnitOfWork.
-      *
-      * The default insert data preparation is the same as for updates.
-      *
-      * @see prepareUpdateData
-      *
-      * @param object $entity The entity for which to prepare the data.
-      *
-      * @return mixed[][] The prepared data for the tables to update.
-      * @phpstan-return array<string, mixed[]>
-      */
-     protected function prepareInsertData($entity)
-     {
-         return $this->prepareUpdateData($entity, true);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getOwningTable($fieldName)
-     {
-         return $this->class->getTableName();
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = null)
-     {
-         $this->switchPersisterContext(null, $limit);
-         $sql              = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
-         [$params, $types] = $this->expandParameters($criteria);
-         $stmt             = $this->conn->executeQuery($sql, $params, $types);
-         if ($entity !== null) {
-             $hints[Query::HINT_REFRESH]        = true;
-             $hints[Query::HINT_REFRESH_ENTITY] = $entity;
-         }
-         $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
-         $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
-         return $entities ? $entities[0] : null;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadById(array $identifier, $entity = null)
-     {
-         return $this->load($identifier, $entity);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
-     {
-         $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity']);
-         if ($foundEntity !== false) {
-             return $foundEntity;
-         }
-         $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
-         if ($assoc['isOwningSide']) {
-             $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
-             // Mark inverse side as fetched in the hints, otherwise the UoW would
-             // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
-             $hints = [];
-             if ($isInverseSingleValued) {
-                 $hints['fetched']['r'][$assoc['inversedBy']] = true;
-             }
-             $targetEntity = $this->load($identifier, null, $assoc, $hints);
-             // Complete bidirectional association, if necessary
-             if ($targetEntity !== null && $isInverseSingleValued) {
-                 $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
-             }
-             return $targetEntity;
-         }
-         $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
-         $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
-         $computedIdentifier = [];
-         /** @var array<string,mixed>|null $sourceEntityData */
-         $sourceEntityData = null;
-         // TRICKY: since the association is specular source and target are flipped
-         foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
-             if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
-                 // The likely case here is that the column is a join column
-                 // in an association mapping. However, there is no guarantee
-                 // at this point that a corresponding (generally identifying)
-                 // association has been mapped in the source entity. To handle
-                 // this case we directly reference the column-keyed data used
-                 // to initialize the source entity before throwing an exception.
-                 $resolvedSourceData = false;
-                 if (! isset($sourceEntityData)) {
-                     $sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
-                 }
-                 if (isset($sourceEntityData[$sourceKeyColumn])) {
-                     $dataValue = $sourceEntityData[$sourceKeyColumn];
-                     if ($dataValue !== null) {
-                         $resolvedSourceData                                                    = true;
-                         $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
-                             $dataValue;
-                     }
-                 }
-                 if (! $resolvedSourceData) {
-                     throw MappingException::joinColumnMustPointToMappedField(
-                         $sourceClass->name,
-                         $sourceKeyColumn
-                     );
-                 }
-             } else {
-                 $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
-                     $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
-             }
-         }
-         $targetEntity = $this->load($computedIdentifier, null, $assoc);
-         if ($targetEntity !== null) {
-             $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
-         }
-         return $targetEntity;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function refresh(array $id, $entity, $lockMode = null)
-     {
-         $sql              = $this->getSelectSQL($id, null, $lockMode);
-         [$params, $types] = $this->expandParameters($id);
-         $stmt             = $this->conn->executeQuery($sql, $params, $types);
-         $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
-         $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function count($criteria = [])
-     {
-         $sql = $this->getCountSQL($criteria);
-         [$params, $types] = $criteria instanceof Criteria
-             ? $this->expandCriteriaParameters($criteria)
-             : $this->expandParameters($criteria);
-         return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadCriteria(Criteria $criteria)
-     {
-         $orderBy = self::getCriteriaOrderings($criteria);
-         $limit   = $criteria->getMaxResults();
-         $offset  = $criteria->getFirstResult();
-         $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
-         [$params, $types] = $this->expandCriteriaParameters($criteria);
-         $stmt     = $this->conn->executeQuery($query, $params, $types);
-         $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
-         return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function expandCriteriaParameters(Criteria $criteria)
-     {
-         $expression = $criteria->getWhereExpression();
-         $sqlParams  = [];
-         $sqlTypes   = [];
-         if ($expression === null) {
-             return [$sqlParams, $sqlTypes];
-         }
-         $valueVisitor = new SqlValueVisitor();
-         $valueVisitor->dispatch($expression);
-         [, $types] = $valueVisitor->getParamsAndTypes();
-         foreach ($types as $type) {
-             [$field, $value, $operator] = $type;
-             if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
-                 continue;
-             }
-             $sqlParams = array_merge($sqlParams, $this->getValues($value));
-             $sqlTypes  = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
-         }
-         return [$sqlParams, $sqlTypes];
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null)
-     {
-         $this->switchPersisterContext($offset, $limit);
-         $sql              = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
-         [$params, $types] = $this->expandParameters($criteria);
-         $stmt             = $this->conn->executeQuery($sql, $params, $types);
-         $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
-         return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
-     {
-         $this->switchPersisterContext($offset, $limit);
-         $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
-         return $this->loadArrayFromResult($assoc, $stmt);
-     }
-     /**
-      * Loads an array of entities from a given DBAL statement.
-      *
-      * @param mixed[] $assoc
-      *
-      * @return mixed[]
-      */
-     private function loadArrayFromResult(array $assoc, Result $stmt): array
-     {
-         $rsm   = $this->currentPersisterContext->rsm;
-         $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
-         if (isset($assoc['indexBy'])) {
-             $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
-             $rsm->addIndexBy('r', $assoc['indexBy']);
-         }
-         return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
-     }
-     /**
-      * Hydrates a collection from a given DBAL statement.
-      *
-      * @param mixed[] $assoc
-      *
-      * @return mixed[]
-      */
-     private function loadCollectionFromStatement(
-         array $assoc,
-         Result $stmt,
-         PersistentCollection $coll
-     ): array {
-         $rsm   = $this->currentPersisterContext->rsm;
-         $hints = [
-             UnitOfWork::HINT_DEFEREAGERLOAD => true,
-             'collection' => $coll,
-         ];
-         if (isset($assoc['indexBy'])) {
-             $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
-             $rsm->addIndexBy('r', $assoc['indexBy']);
-         }
-         return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
-     {
-         $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
-         return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
-     }
-     /**
-      * @param object $sourceEntity
-      * @phpstan-param array<string, mixed> $assoc
-      *
-      * @return Result
-      *
-      * @throws MappingException
-      */
-     private function getManyToManyStatement(
-         array $assoc,
-         $sourceEntity,
-         ?int $offset = null,
-         ?int $limit = null
-     ) {
-         $this->switchPersisterContext($offset, $limit);
-         $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
-         $class       = $sourceClass;
-         $association = $assoc;
-         $criteria    = [];
-         $parameters  = [];
-         if (! $assoc['isOwningSide']) {
-             $class       = $this->em->getClassMetadata($assoc['targetEntity']);
-             $association = $class->associationMappings[$assoc['mappedBy']];
-         }
-         $joinColumns = $assoc['isOwningSide']
-             ? $association['joinTable']['joinColumns']
-             : $association['joinTable']['inverseJoinColumns'];
-         $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
-         foreach ($joinColumns as $joinColumn) {
-             $sourceKeyColumn = $joinColumn['referencedColumnName'];
-             $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
-             switch (true) {
-                 case $sourceClass->containsForeignIdentifier:
-                     $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
-                     $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
-                     if (isset($sourceClass->associationMappings[$field])) {
-                         $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
-                         $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
-                     }
-                     break;
-                 case isset($sourceClass->fieldNames[$sourceKeyColumn]):
-                     $field = $sourceClass->fieldNames[$sourceKeyColumn];
-                     $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
-                     break;
-                 default:
-                     throw MappingException::joinColumnMustPointToMappedField(
-                         $sourceClass->name,
-                         $sourceKeyColumn
-                     );
-             }
-             $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
-             $parameters[]                                        = [
-                 'value' => $value,
-                 'field' => $field,
-                 'class' => $sourceClass,
-             ];
-         }
-         $sql              = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
-         [$params, $types] = $this->expandToManyParameters($parameters);
-         return $this->conn->executeQuery($sql, $params, $types);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null)
-     {
-         $this->switchPersisterContext($offset, $limit);
-         $lockSql    = '';
-         $joinSql    = '';
-         $orderBySql = '';
-         if ($assoc !== null && $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
-             $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
-         }
-         if (isset($assoc['orderBy'])) {
-             $orderBy = $assoc['orderBy'];
-         }
-         if ($orderBy) {
-             $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
-         }
-         $conditionSql = $criteria instanceof Criteria
-             ? $this->getSelectConditionCriteriaSQL($criteria)
-             : $this->getSelectConditionSQL($criteria, $assoc);
-         switch ($lockMode) {
-             case LockMode::PESSIMISTIC_READ:
-                 $lockSql = ' ' . $this->getReadLockSQL($this->platform);
-                 break;
-             case LockMode::PESSIMISTIC_WRITE:
-                 $lockSql = ' ' . $this->getWriteLockSQL($this->platform);
-                 break;
-         }
-         $columnList = $this->getSelectColumnsSQL();
-         $tableAlias = $this->getSQLTableAlias($this->class->name);
-         $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
-         $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
-         if ($filterSql !== '') {
-             $conditionSql = $conditionSql
-                 ? $conditionSql . ' AND ' . $filterSql
-                 : $filterSql;
-         }
-         $select = 'SELECT ' . $columnList;
-         $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
-         $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
-         $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
-         $lock   = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
-         $query  = $select
-             . $lock
-             . $join
-             . $where
-             . $orderBySql;
-         return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getCountSQL($criteria = [])
-     {
-         $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
-         $tableAlias = $this->getSQLTableAlias($this->class->name);
-         $conditionSql = $criteria instanceof Criteria
-             ? $this->getSelectConditionCriteriaSQL($criteria)
-             : $this->getSelectConditionSQL($criteria);
-         $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
-         if ($filterSql !== '') {
-             $conditionSql = $conditionSql
-                 ? $conditionSql . ' AND ' . $filterSql
-                 : $filterSql;
-         }
-         return 'SELECT COUNT(*) '
-             . 'FROM ' . $tableName . ' ' . $tableAlias
-             . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
-     }
-     /**
-      * Gets the ORDER BY SQL snippet for ordered collections.
-      *
-      * @phpstan-param array<string, string> $orderBy
-      *
-      * @throws InvalidOrientation
-      * @throws InvalidFindByCall
-      * @throws UnrecognizedField
-      */
-     final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string
-     {
-         $orderByList = [];
-         foreach ($orderBy as $fieldName => $orientation) {
-             $orientation = strtoupper(trim($orientation));
-             if ($orientation !== 'ASC' && $orientation !== 'DESC') {
-                 throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName);
-             }
-             if (isset($this->class->fieldMappings[$fieldName])) {
-                 $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
-                     ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
-                     : $baseTableAlias;
-                 $columnName    = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
-                 $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
-                 continue;
-             }
-             if (isset($this->class->associationMappings[$fieldName])) {
-                 if (! $this->class->associationMappings[$fieldName]['isOwningSide']) {
-                     throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName);
-                 }
-                 $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
-                     ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
-                     : $baseTableAlias;
-                 foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
-                     $columnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                     $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
-                 }
-                 continue;
-             }
-             throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName);
-         }
-         return ' ORDER BY ' . implode(', ', $orderByList);
-     }
-     /**
-      * Gets the SQL fragment with the list of columns to select when querying for
-      * an entity in this persister.
-      *
-      * Subclasses should override this method to alter or change the select column
-      * list SQL fragment. Note that in the implementation of BasicEntityPersister
-      * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
-      * Subclasses may or may not do the same.
-      *
-      * @return string The SQL fragment.
-      */
-     protected function getSelectColumnsSQL()
-     {
-         if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
-             return $this->currentPersisterContext->selectColumnListSql;
-         }
-         $columnList = [];
-         $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
-         // Add regular columns to select list
-         foreach ($this->class->fieldNames as $field) {
-             $columnList[] = $this->getSelectColumnSQL($field, $this->class);
-         }
-         $this->currentPersisterContext->selectJoinSql = '';
-         $eagerAliasCounter                            = 0;
-         foreach ($this->class->associationMappings as $assocField => $assoc) {
-             $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
-             if ($assocColumnSQL) {
-                 $columnList[] = $assocColumnSQL;
-             }
-             $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
-             $isAssocFromOneEager     = $assoc['type'] & ClassMetadata::TO_ONE && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
-             if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
-                 continue;
-             }
-             if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
-                 continue;
-             }
-             $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
-             if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
-                 continue; // now this is why you shouldn't use inheritance
-             }
-             $assocAlias = 'e' . ($eagerAliasCounter++);
-             $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
-             foreach ($eagerEntity->fieldNames as $field) {
-                 $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
-             }
-             foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
-                 $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
-                     $eagerAssocField,
-                     $eagerAssoc,
-                     $eagerEntity,
-                     $assocAlias
-                 );
-                 if ($eagerAssocColumnSQL) {
-                     $columnList[] = $eagerAssocColumnSQL;
-                 }
-             }
-             $association   = $assoc;
-             $joinCondition = [];
-             if (isset($assoc['indexBy'])) {
-                 $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
-             }
-             if (! $assoc['isOwningSide']) {
-                 $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
-                 $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
-             }
-             $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
-             $joinTableName  = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
-             if ($assoc['isOwningSide']) {
-                 $tableAlias                                    = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
-                 $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
-                 foreach ($association['joinColumns'] as $joinColumn) {
-                     $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                     $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
-                     $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
-                                         . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
-                 }
-                 // Add filter SQL
-                 $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias);
-                 if ($filterSql) {
-                     $joinCondition[] = $filterSql;
-                 }
-             } else {
-                 $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
-                 foreach ($association['joinColumns'] as $joinColumn) {
-                     $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                     $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
-                     $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
-                         . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
-                 }
-                 // Add filter SQL
-                 $filterSql = $this->generateFilterConditionSQL($eagerEntity, $joinTableAlias);
-                 if ($filterSql) {
-                     $joinCondition[] = $filterSql;
-                 }
-             }
-             $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
-             $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
-         }
-         $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
-         $this->updateFilterHash();
-         return $this->currentPersisterContext->selectColumnListSql;
-     }
-     /**
-      * Gets the SQL join fragment used when selecting entities from an association.
-      *
-      * @param string             $field
-      * @param AssociationMapping $assoc
-      * @param string             $alias
-      *
-      * @return string
-      */
-     protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
-     {
-         if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
-             return '';
-         }
-         $columnList    = [];
-         $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
-         $isIdentifier  = isset($assoc['id']) && $assoc['id'] === true;
-         $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias));
-         foreach ($assoc['joinColumns'] as $joinColumn) {
-             $quotedColumn     = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-             $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
-             $type             = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
-             $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
-             $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
-         }
-         return implode(', ', $columnList);
-     }
-     /**
-      * Gets the SQL join fragment used when selecting entities from a
-      * many-to-many association.
-      *
-      * @phpstan-param AssociationMapping $manyToMany
-      *
-      * @return string
-      */
-     protected function getSelectManyToManyJoinSQL(array $manyToMany)
-     {
-         $conditions       = [];
-         $association      = $manyToMany;
-         $sourceTableAlias = $this->getSQLTableAlias($this->class->name);
-         if (! $manyToMany['isOwningSide']) {
-             $targetEntity = $this->em->getClassMetadata($manyToMany['targetEntity']);
-             $association  = $targetEntity->associationMappings[$manyToMany['mappedBy']];
-         }
-         $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
-         $joinColumns   = $manyToMany['isOwningSide']
-             ? $association['joinTable']['inverseJoinColumns']
-             : $association['joinTable']['joinColumns'];
-         foreach ($joinColumns as $joinColumn) {
-             $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-             $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
-             $conditions[]       = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
-         }
-         return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getInsertSQL()
-     {
-         $columns   = $this->getInsertColumnList();
-         $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
-         if ($columns === []) {
-             $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
-             return $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
-         }
-         $placeholders = [];
-         $columns      = array_unique($columns);
-         foreach ($columns as $column) {
-             $placeholder = '?';
-             if (
-                 isset($this->class->fieldNames[$column])
-                 && isset($this->columnTypes[$this->class->fieldNames[$column]])
-                 && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])
-             ) {
-                 $type        = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
-                 $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
-             }
-             $placeholders[] = $placeholder;
-         }
-         $columns      = implode(', ', $columns);
-         $placeholders = implode(', ', $placeholders);
-         return sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $placeholders);
-     }
-     /**
-      * Gets the list of columns to put in the INSERT SQL statement.
-      *
-      * Subclasses should override this method to alter or change the list of
-      * columns placed in the INSERT statements used by the persister.
-      *
-      * @return string[] The list of columns.
-      * @phpstan-return list<string>
-      */
-     protected function getInsertColumnList()
-     {
-         $columns = [];
-         foreach ($this->class->reflFields as $name => $field) {
-             if ($this->class->isVersioned && $this->class->versionField === $name) {
-                 continue;
-             }
-             if (isset($this->class->embeddedClasses[$name])) {
-                 continue;
-             }
-             if (isset($this->class->associationMappings[$name])) {
-                 $assoc = $this->class->associationMappings[$name];
-                 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
-                     foreach ($assoc['joinColumns'] as $joinColumn) {
-                         $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                     }
-                 }
-                 continue;
-             }
-             if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {
-                 if (isset($this->class->fieldMappings[$name]['notInsertable'])) {
-                     continue;
-                 }
-                 $columns[]                = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
-                 $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
-             }
-         }
-         return $columns;
-     }
-     /**
-      * Gets the SQL snippet of a qualified column name for the given field name.
-      *
-      * @param string        $field The field name.
-      * @param ClassMetadata $class The class that declares this field. The table this class is
-      *                             mapped to must own the column for the given field.
-      * @param string        $alias
-      *
-      * @return string
-      */
-     protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
-     {
-         $root         = $alias === 'r' ? '' : $alias;
-         $tableAlias   = $this->getSQLTableAlias($class->name, $root);
-         $fieldMapping = $class->fieldMappings[$field];
-         $sql          = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
-         $columnAlias = null;
-         if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) {
-             $columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field);
-         }
-         if ($columnAlias === null) {
-             $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
-         }
-         $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
-         if (! empty($fieldMapping['enumType'])) {
-             $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping['enumType']);
-         }
-         if (isset($fieldMapping['requireSQLConversion'])) {
-             $type = Type::getType($fieldMapping['type']);
-             $sql  = $type->convertToPHPValueSQL($sql, $this->platform);
-         }
-         return $sql . ' AS ' . $columnAlias;
-     }
-     /**
-      * Gets the SQL table alias for the given class name.
-      *
-      * @param string $className
-      * @param string $assocName
-      *
-      * @return string The SQL table alias.
-      *
-      * @todo Reconsider. Binding table aliases to class names is not such a good idea.
-      */
-     protected function getSQLTableAlias($className, $assocName = '')
-     {
-         if ($assocName) {
-             $className .= '#' . $assocName;
-         }
-         if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
-             return $this->currentPersisterContext->sqlTableAliases[$className];
-         }
-         $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
-         $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
-         return $tableAlias;
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function lock(array $criteria, $lockMode)
-     {
-         $lockSql      = '';
-         $conditionSql = $this->getSelectConditionSQL($criteria);
-         switch ($lockMode) {
-             case LockMode::PESSIMISTIC_READ:
-                 $lockSql = $this->getReadLockSQL($this->platform);
-                 break;
-             case LockMode::PESSIMISTIC_WRITE:
-                 $lockSql = $this->getWriteLockSQL($this->platform);
-                 break;
-         }
-         $lock  = $this->getLockTablesSql($lockMode);
-         $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
-         $sql   = 'SELECT 1 '
-              . $lock
-              . $where
-              . $lockSql;
-         [$params, $types] = $this->expandParameters($criteria);
-         $this->conn->executeQuery($sql, $params, $types);
-     }
-     /**
-      * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
-      *
-      * @param int|null $lockMode One of the Doctrine\DBAL\LockMode::* constants.
-      * @phpstan-param LockMode::*|null $lockMode
-      *
-      * @return string
-      */
-     protected function getLockTablesSql($lockMode)
-     {
-         if ($lockMode === null) {
-             Deprecation::trigger(
-                 'doctrine/orm',
-                 'https://github.com/doctrine/orm/pull/9466',
-                 'Passing null as argument to %s is deprecated, pass LockMode::NONE instead.',
-                 __METHOD__
-             );
-             $lockMode = LockMode::NONE;
-         }
-         return $this->platform->appendLockHint(
-             'FROM '
-             . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
-             . $this->getSQLTableAlias($this->class->name),
-             $lockMode
-         );
-     }
-     /**
-      * Gets the Select Where Condition from a Criteria object.
-      *
-      * @return string
-      */
-     protected function getSelectConditionCriteriaSQL(Criteria $criteria)
-     {
-         $expression = $criteria->getWhereExpression();
-         if ($expression === null) {
-             return '';
-         }
-         $visitor = new SqlExpressionVisitor($this, $this->class);
-         return $visitor->dispatch($expression);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
-     {
-         $selectedColumns = [];
-         $columns         = $this->getSelectConditionStatementColumnSQL($field, $assoc);
-         if (count($columns) > 1 && $comparison === Comparison::IN) {
-             /*
-              *  @todo try to support multi-column IN expressions.
-              *  Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
-              */
-             throw CantUseInOperatorOnCompositeKeys::create();
-         }
-         foreach ($columns as $column) {
-             $placeholder = '?';
-             if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
-                 $type        = Type::getType($this->class->fieldMappings[$field]['type']);
-                 $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
-             }
-             if ($comparison !== null) {
-                 // special case null value handling
-                 if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
-                     $selectedColumns[] = $column . ' IS NULL';
-                     continue;
-                 }
-                 if ($comparison === Comparison::NEQ && $value === null) {
-                     $selectedColumns[] = $column . ' IS NOT NULL';
-                     continue;
-                 }
-                 $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
-                 continue;
-             }
-             if (is_array($value)) {
-                 $in = sprintf('%s IN (%s)', $column, $placeholder);
-                 if (array_search(null, $value, true) !== false) {
-                     $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
-                     continue;
-                 }
-                 $selectedColumns[] = $in;
-                 continue;
-             }
-             if ($value === null) {
-                 $selectedColumns[] = sprintf('%s IS NULL', $column);
-                 continue;
-             }
-             $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
-         }
-         return implode(' AND ', $selectedColumns);
-     }
-     /**
-      * Builds the left-hand-side of a where condition statement.
-      *
-      * @phpstan-param AssociationMapping|null $assoc
-      *
-      * @return string[]
-      * @phpstan-return list<string>
-      *
-      * @throws InvalidFindByCall
-      * @throws UnrecognizedField
-      */
-     private function getSelectConditionStatementColumnSQL(
-         string $field,
-         ?array $assoc = null
-     ): array {
-         if (isset($this->class->fieldMappings[$field])) {
-             $className = $this->class->fieldMappings[$field]['inherited'] ?? $this->class->name;
-             return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
-         }
-         if (isset($this->class->associationMappings[$field])) {
-             $association = $this->class->associationMappings[$field];
-             // Many-To-Many requires join table check for joinColumn
-             $columns = [];
-             $class   = $this->class;
-             if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
-                 if (! $association['isOwningSide']) {
-                     $association = $assoc;
-                 }
-                 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
-                 $joinColumns   = $assoc['isOwningSide']
-                     ? $association['joinTable']['joinColumns']
-                     : $association['joinTable']['inverseJoinColumns'];
-                 foreach ($joinColumns as $joinColumn) {
-                     $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
-                 }
-             } else {
-                 if (! $association['isOwningSide']) {
-                     throw InvalidFindByCall::fromInverseSideUsage(
-                         $this->class->name,
-                         $field
-                     );
-                 }
-                 $className = $association['inherited'] ?? $this->class->name;
-                 foreach ($association['joinColumns'] as $joinColumn) {
-                     $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
-                 }
-             }
-             return $columns;
-         }
-         if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) {
-             // very careless developers could potentially open up this normally hidden api for userland attacks,
-             // therefore checking for spaces and function calls which are not allowed.
-             // found a join column condition, not really a "field"
-             return [$field];
-         }
-         throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field);
-     }
-     /**
-      * Gets the conditional SQL fragment used in the WHERE clause when selecting
-      * entities in this persister.
-      *
-      * Subclasses are supposed to override this method if they intend to change
-      * or alter the criteria by which entities are selected.
-      *
-      * @param AssociationMapping|null $assoc
-      * @phpstan-param array<string, mixed> $criteria
-      * @phpstan-param array<string, mixed>|null $assoc
-      *
-      * @return string
-      */
-     protected function getSelectConditionSQL(array $criteria, $assoc = null)
-     {
-         $conditions = [];
-         foreach ($criteria as $field => $value) {
-             $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
-         }
-         return implode(' AND ', $conditions);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
-     {
-         $this->switchPersisterContext($offset, $limit);
-         $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
-         return $this->loadArrayFromResult($assoc, $stmt);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
-     {
-         $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
-         return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
-     }
-     /**
-      * Builds criteria and execute SQL statement to fetch the one to many entities from.
-      *
-      * @param object $sourceEntity
-      * @phpstan-param AssociationMapping $assoc
-      */
-     private function getOneToManyStatement(
-         array $assoc,
-         $sourceEntity,
-         ?int $offset = null,
-         ?int $limit = null
-     ): Result {
-         $this->switchPersisterContext($offset, $limit);
-         $criteria    = [];
-         $parameters  = [];
-         $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
-         $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
-         $tableAlias  = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
-         foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
-             if ($sourceClass->containsForeignIdentifier) {
-                 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
-                 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
-                 if (isset($sourceClass->associationMappings[$field])) {
-                     $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
-                     $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
-                 }
-                 $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
-                 $parameters[]                                   = [
-                     'value' => $value,
-                     'field' => $field,
-                     'class' => $sourceClass,
-                 ];
-                 continue;
-             }
-             $field = $sourceClass->fieldNames[$sourceKeyColumn];
-             $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
-             $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
-             $parameters[]                                   = [
-                 'value' => $value,
-                 'field' => $field,
-                 'class' => $sourceClass,
-             ];
-         }
-         $sql              = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
-         [$params, $types] = $this->expandToManyParameters($parameters);
-         return $this->conn->executeQuery($sql, $params, $types);
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function expandParameters($criteria)
-     {
-         $params = [];
-         $types  = [];
-         foreach ($criteria as $field => $value) {
-             if ($value === null) {
-                 continue; // skip null values.
-             }
-             $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
-             $params = array_merge($params, $this->getValues($value));
-         }
-         return [$params, $types];
-     }
-     /**
-      * Expands the parameters from the given criteria and use the correct binding types if found,
-      * specialized for OneToMany or ManyToMany associations.
-      *
-      * @param mixed[][] $criteria an array of arrays containing following:
-      *                             - field to which each criterion will be bound
-      *                             - value to be bound
-      *                             - class to which the field belongs to
-      *
-      * @return mixed[][]
-      * @phpstan-return array{0: array, 1: list<int|string|null>}
-      */
-     private function expandToManyParameters(array $criteria): array
-     {
-         $params = [];
-         $types  = [];
-         foreach ($criteria as $criterion) {
-             if ($criterion['value'] === null) {
-                 continue; // skip null values.
-             }
-             $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
-             $params = array_merge($params, $this->getValues($criterion['value']));
-         }
-         return [$params, $types];
-     }
-     /**
-      * Infers field types to be used by parameter type casting.
-      *
-      * @param mixed $value
-      *
-      * @return int[]|null[]|string[]
-      * @phpstan-return list<int|string|null>
-      *
-      * @throws QueryException
-      */
-     private function getTypes(string $field, $value, ClassMetadata $class): array
-     {
-         $types = [];
-         switch (true) {
-             case isset($class->fieldMappings[$field]):
-                 $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
-                 break;
-             case isset($class->associationMappings[$field]):
-                 $assoc = $class->associationMappings[$field];
-                 $class = $this->em->getClassMetadata($assoc['targetEntity']);
-                 if (! $assoc['isOwningSide']) {
-                     $assoc = $class->associationMappings[$assoc['mappedBy']];
-                     $class = $this->em->getClassMetadata($assoc['targetEntity']);
-                 }
-                 $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
-                     ? $assoc['relationToTargetKeyColumns']
-                     : $assoc['sourceToTargetKeyColumns'];
-                 foreach ($columns as $column) {
-                     $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
-                 }
-                 break;
-             default:
-                 $types[] = null;
-                 break;
-         }
-         if (is_array($value)) {
-             return array_map(static function ($type) {
-                 $type = Type::getType($type);
-                 return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
-             }, $types);
-         }
-         return $types;
-     }
-     /**
-      * Retrieves the parameters that identifies a value.
-      *
-      * @param mixed $value
-      *
-      * @return mixed[]
-      */
-     private function getValues($value): array
-     {
-         if (is_array($value)) {
-             $newValue = [];
-             foreach ($value as $itemValue) {
-                 $newValue = array_merge($newValue, $this->getValues($itemValue));
-             }
-             return [$newValue];
-         }
-         return $this->getIndividualValue($value);
-     }
-     /**
-      * Retrieves an individual parameter value.
-      *
-      * @param mixed $value
-      *
-      * @phpstan-return list<mixed>
-      */
-     private function getIndividualValue($value): array
-     {
-         if (! is_object($value)) {
-             return [$value];
-         }
-         if ($value instanceof BackedEnum) {
-             return [$value->value];
-         }
-         $valueClass = DefaultProxyClassNameResolver::getClass($value);
-         if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
-             return [$value];
-         }
-         $class = $this->em->getClassMetadata($valueClass);
-         if ($class->isIdentifierComposite) {
-             $newValue = [];
-             foreach ($class->getIdentifierValues($value) as $innerValue) {
-                 $newValue = array_merge($newValue, $this->getValues($innerValue));
-             }
-             return $newValue;
-         }
-         return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
-     }
-     /**
-      * {@inheritDoc}
-      */
-     public function exists($entity, ?Criteria $extraConditions = null)
-     {
-         $criteria = $this->class->getIdentifierValues($entity);
-         if (! $criteria) {
-             return false;
-         }
-         $alias = $this->getSQLTableAlias($this->class->name);
-         $sql = 'SELECT 1 '
-              . $this->getLockTablesSql(LockMode::NONE)
-              . ' WHERE ' . $this->getSelectConditionSQL($criteria);
-         [$params, $types] = $this->expandParameters($criteria);
-         if ($extraConditions !== null) {
-             $sql                             .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
-             [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);
-             $params = array_merge($params, $criteriaParams);
-             $types  = array_merge($types, $criteriaTypes);
-         }
-         $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
-         if ($filterSql) {
-             $sql .= ' AND ' . $filterSql;
-         }
-         return (bool) $this->conn->fetchOne($sql, $params, $types);
-     }
-     /**
-      * Generates the appropriate join SQL for the given join column.
-      *
-      * @param array[] $joinColumns The join columns definition of an association.
-      * @phpstan-param array<array<string, mixed>> $joinColumns
-      *
-      * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
-      */
-     protected function getJoinSQLForJoinColumns($joinColumns)
-     {
-         // if one of the join columns is nullable, return left join
-         foreach ($joinColumns as $joinColumn) {
-             if (! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
-                 return 'LEFT JOIN';
-             }
-         }
-         return 'INNER JOIN';
-     }
-     /**
-      * @param string $columnName
-      *
-      * @return string
-      */
-     public function getSQLColumnAlias($columnName)
-     {
-         return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
-     }
-     /**
-      * Generates the filter SQL for a given entity and table alias.
-      *
-      * @param ClassMetadata $targetEntity     Metadata of the target entity.
-      * @param string        $targetTableAlias The table alias of the joined/selected table.
-      *
-      * @return string The SQL query part to add to a query.
-      */
-     protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
-     {
-         $filterClauses = [];
-         foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
-             $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
-             if ($filterExpr !== '') {
-                 $filterClauses[] = '(' . $filterExpr . ')';
-             }
-         }
-         $sql = implode(' AND ', $filterClauses);
-         return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
-     }
-     /**
-      * Switches persister context according to current query offset/limits
-      *
-      * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
-      *
-      * @param int|null $offset
-      * @param int|null $limit
-      *
-      * @return void
-      */
-     protected function switchPersisterContext($offset, $limit)
-     {
-         if ($offset === null && $limit === null) {
-             $this->currentPersisterContext = $this->noLimitsContext;
-             return;
-         }
-         $this->currentPersisterContext = $this->limitsHandlingContext;
-     }
-     /**
-      * @return string[]
-      * @phpstan-return list<string>
-      */
-     protected function getClassIdentifiersTypes(ClassMetadata $class): array
-     {
-         $entityManager = $this->em;
-         return array_map(
-             static function ($fieldName) use ($class, $entityManager): string {
-                 $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager);
-                 assert(isset($types[0]));
-                 return $types[0];
-             },
-             $class->identifier
-         );
-     }
- }