From ce40e3abe4043f15579edffdcff38b97c63feabc Mon Sep 17 00:00:00 2001 From: Dawid Parafinski Date: Fri, 7 Aug 2026 12:17:31 +0200 Subject: [PATCH 1/2] IBX-12043: Upgraded to Doctrine DBAL 4 Binding types became enums, so Connection::ARRAY_PARAM_OFFSET arithmetic is replaced by a mapping onto ArrayParameterType and getBindingTypeForColumn() returns ParameterType. QueryBuilder::getQueryPart() is gone, so already-joined tables are tracked per QueryBuilder via core's JoinedTablesTracker and an uninitialised sub-select is recognised from the builder itself. The PostgreSQL CI job passed the server version as "server_version", which DoctrineBundle never reads; it hands DBAL 4 an empty string instead and the PostgreSQL driver rejects it. The query parameter is "serverVersion", and its value now matches the postgres service image. DBAL 4 makes lastInsertId() throw NoIdentityValue instead of returning "0" when the statement generated none, which breaks doInsert() for tables that have no auto-increment column, such as the child table of a joined inheritance hierarchy where the caller supplies the identifier. Those call sites now use doInsertWithoutIdentity() and never ask for a value the database was never going to produce. --- .github/workflows/backend-ci.yaml | 2 +- .../Gateway/AbstractDoctrineDatabase.php | 31 +++++++- .../Gateway/DoctrineSchemaMetadata.php | 19 ++++- .../DoctrineSchemaMetadataInterface.php | 8 +- src/lib/Gateway/ExpressionVisitor.php | 22 +++--- .../JoinedRelationshipTypeStrategy.php | 29 +++---- src/lib/Gateway/Parameter.php | 9 ++- .../SubSelectRelationshipTypeStrategy.php | 22 +++++- .../bundle/Gateway/ExpressionVisitorTest.php | 48 +++++++++--- .../BaseRelationshipTypeStrategyTestCase.php | 2 +- .../JoinedRelationshipTypeStrategyTest.php | 41 +++++----- .../SubSelectRelationshipTypeStrategyTest.php | 78 +++++++------------ 12 files changed, 182 insertions(+), 129 deletions(-) diff --git a/.github/workflows/backend-ci.yaml b/.github/workflows/backend-ci.yaml index f7bc485..c04de59 100644 --- a/.github/workflows/backend-ci.yaml +++ b/.github/workflows/backend-ci.yaml @@ -111,7 +111,7 @@ jobs: run: composer run-script --timeout=600 test-integration env: SEARCH_ENGINE: legacy - DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?server_version=10" + DATABASE_URL: "pgsql://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/testdb?serverVersion=11" integration-tests-mysql: name: MySQL integration tests diff --git a/src/contracts/Gateway/AbstractDoctrineDatabase.php b/src/contracts/Gateway/AbstractDoctrineDatabase.php index c47e3ac..4a00ffa 100644 --- a/src/contracts/Gateway/AbstractDoctrineDatabase.php +++ b/src/contracts/Gateway/AbstractDoctrineDatabase.php @@ -66,14 +66,37 @@ public function getMetadata(): DoctrineSchemaMetadataInterface * @throws \Doctrine\DBAL\Exception */ protected function doInsert(array $data): int + { + $this->executeInsert($data); + + return (int)$this->connection->lastInsertId(); + } + + /** + * Inserts a row into a table that generates no identity value, such as the child table of a + * joined inheritance hierarchy, where the identifier is supplied by the caller. + * + * @param array $data + * + * @throws \Doctrine\DBAL\Exception + */ + protected function doInsertWithoutIdentity(array $data): void + { + $this->executeInsert($data); + } + + /** + * @param array $data + * + * @throws \Doctrine\DBAL\Exception + */ + private function executeInsert(array $data): void { $metadata = $this->getMetadata(); $data = $metadata->convertToDatabaseValues($data); $types = $metadata->getBindingTypesForData($data); $this->connection->insert($metadata->getTableName(), $data, $types); - - return (int)$this->connection->lastInsertId(); } /** @@ -358,7 +381,7 @@ private function buildCondition(QueryBuilder $qb, string $column, $value): strin } elseif (is_array($value)) { $parameter = $qb->createPositionalParameter( $value, - $columnBinding + Connection::ARRAY_PARAM_OFFSET + $metadata->getArrayBindingTypeForColumn($column) ); $subquery->andWhere($qb->expr()->in($fullColumnName, $parameter)); @@ -390,7 +413,7 @@ private function buildCondition(QueryBuilder $qb, string $column, $value): strin if (is_array($value)) { $parameter = $qb->createPositionalParameter( $value, - $columnBinding + Connection::ARRAY_PARAM_OFFSET + $metadata->getArrayBindingTypeForColumn($column) ); return $qb->expr()->in($fullColumnName, $parameter); diff --git a/src/contracts/Gateway/DoctrineSchemaMetadata.php b/src/contracts/Gateway/DoctrineSchemaMetadata.php index 09fe4a5..f614df1 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadata.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadata.php @@ -8,7 +8,9 @@ namespace Ibexa\Contracts\CorePersistence\Gateway; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Types\Type; use Ibexa\Contracts\CorePersistence\Exception\MappingException; use Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingException; @@ -250,7 +252,7 @@ public function convertToDatabaseValues(array $data): array /** * @param array $data * - * @return array + * @return array * * @throws \Doctrine\DBAL\Exception */ @@ -267,11 +269,24 @@ public function getBindingTypesForData(array $data): array /** * @throws \Doctrine\DBAL\Exception */ - public function getBindingTypeForColumn(string $columnName): int + public function getBindingTypeForColumn(string $columnName): ParameterType { return $this->getColumnType($columnName)->getBindingType(); } + /** + * @throws \Doctrine\DBAL\Exception + */ + public function getArrayBindingTypeForColumn(string $columnName): ArrayParameterType + { + return match ($this->getBindingTypeForColumn($columnName)) { + ParameterType::INTEGER => ArrayParameterType::INTEGER, + ParameterType::ASCII => ArrayParameterType::ASCII, + ParameterType::BINARY => ArrayParameterType::BINARY, + default => ArrayParameterType::STRING, + }; + } + public function setTranslationSchemaMetadata(TranslationDoctrineSchemaMetadataInterface $translationMetadata): void { $this->translationMetadata = $translationMetadata; diff --git a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php index 18e4b1e..c3caab1 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php @@ -8,7 +8,9 @@ namespace Ibexa\Contracts\CorePersistence\Gateway; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Types\Type; /** @@ -84,7 +86,7 @@ public function convertToDatabaseValues(array $data): array; /** * @param array $data * - * @return array + * @return array * * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ @@ -99,7 +101,9 @@ public function getIdentifierColumn(): string; * @throws \Doctrine\DBAL\Exception * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ - public function getBindingTypeForColumn(string $columnName): int; + public function getBindingTypeForColumn(string $columnName): ParameterType; + + public function getArrayBindingTypeForColumn(string $columnName): ArrayParameterType; /** * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingExceptionInterface diff --git a/src/lib/Gateway/ExpressionVisitor.php b/src/lib/Gateway/ExpressionVisitor.php index a9036f7..e731831 100644 --- a/src/lib/Gateway/ExpressionVisitor.php +++ b/src/lib/Gateway/ExpressionVisitor.php @@ -112,10 +112,9 @@ public function walkComparison(Comparison $comparison) $parameterName = $column . '_' . count($this->parameters); $placeholder = $this->getPlaceholder($parameterName); $value = $this->walkValue($comparison->getValue()); - $type = $this->schemaMetadata->getBindingTypeForColumn($column); - if (is_array($value)) { - $type += Connection::ARRAY_PARAM_OFFSET; - } + $type = is_array($value) + ? $this->schemaMetadata->getArrayBindingTypeForColumn($column) + : $this->schemaMetadata->getBindingTypeForColumn($column); if ($this->isInheritedColumn($column)) { $inheritanceMetadata = $this->schemaMetadata->getInheritanceMetadataWithColumn($column); @@ -288,11 +287,9 @@ private function handleJoinQuery( QueryBuilder $relationshipQuery ): string { $value = $this->walkValue($comparison->getValue()); - $type = $relationshipMetadata->getBindingTypeForColumn($field); - - if (is_array($value)) { - $type += Connection::ARRAY_PARAM_OFFSET; - } + $type = is_array($value) + ? $relationshipMetadata->getArrayBindingTypeForColumn($field) + : $relationshipMetadata->getBindingTypeForColumn($field); $parameter = new Parameter($parameterName, $value, $type); $placeholder = $this->getPlaceholder($parameterName); @@ -323,10 +320,9 @@ private function handleSubSelectQuery( QueryBuilder $relationshipQuery ): string { $value = $this->walkValue($comparison->getValue()); - $type = $relationshipMetadata->getBindingTypeForColumn($field); - if (is_array($value)) { - $type += Connection::ARRAY_PARAM_OFFSET; - } + $type = is_array($value) + ? $relationshipMetadata->getArrayBindingTypeForColumn($field) + : $relationshipMetadata->getBindingTypeForColumn($field); $this->parameters[] = new Parameter($parameterName, $value, $type); diff --git a/src/lib/Gateway/JoinedRelationshipTypeStrategy.php b/src/lib/Gateway/JoinedRelationshipTypeStrategy.php index 053fe07..969746e 100644 --- a/src/lib/Gateway/JoinedRelationshipTypeStrategy.php +++ b/src/lib/Gateway/JoinedRelationshipTypeStrategy.php @@ -10,12 +10,20 @@ use Doctrine\DBAL\Query\QueryBuilder; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface; +use Ibexa\Core\Persistence\Doctrine\JoinedTablesTracker; /** * @internal */ final class JoinedRelationshipTypeStrategy implements RelationshipTypeStrategyInterface { + private JoinedTablesTracker $joinedTablesTracker; + + public function __construct() + { + $this->joinedTablesTracker = new JoinedTablesTracker(); + } + public function handleRelationshipType( QueryBuilder $queryBuilder, DoctrineRelationshipInterface $relationship, @@ -23,7 +31,7 @@ public function handleRelationshipType( string $fromTable, string $toTable ): void { - if ($this->isTableAlreadyJoined($queryBuilder, $toTable)) { + if (!$this->joinedTablesTracker->markTableAsJoined($queryBuilder, $toTable)) { return; } @@ -45,23 +53,4 @@ public function handleRelationshipTypeQuery( ): QueryBuilder { return $queryBuilder; } - - private function isTableAlreadyJoined( - QueryBuilder $queryBuilder, - string $tableToJoin - ): bool { - $joinQueryPart = $queryBuilder->getQueryPart('join'); - - foreach ($joinQueryPart as $joins) { - foreach ($joins as $join) { - $joinAlias = $join['joinAlias'] ?? $join['joinTable']; - - if ($joinAlias === $tableToJoin) { - return true; - } - } - } - - return false; - } } diff --git a/src/lib/Gateway/Parameter.php b/src/lib/Gateway/Parameter.php index 0e3fe6e..fc197a4 100644 --- a/src/lib/Gateway/Parameter.php +++ b/src/lib/Gateway/Parameter.php @@ -8,6 +8,9 @@ namespace Ibexa\CorePersistence\Gateway; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\ParameterType; + /** * @internal */ @@ -15,7 +18,7 @@ final class Parameter { private string $name; - private int $type; + private ArrayParameterType|ParameterType $type; /** @var mixed */ private $value; @@ -23,7 +26,7 @@ final class Parameter /** * @param mixed $value */ - public function __construct(string $name, $value, int $type) + public function __construct(string $name, $value, ArrayParameterType|ParameterType $type) { $this->name = $name; $this->value = $value; @@ -51,7 +54,7 @@ public function getValue() return $this->value; } - public function getType(): int + public function getType(): ArrayParameterType|ParameterType { return $this->type; } diff --git a/src/lib/Gateway/SubSelectRelationshipTypeStrategy.php b/src/lib/Gateway/SubSelectRelationshipTypeStrategy.php index 74981c0..63ec18b 100644 --- a/src/lib/Gateway/SubSelectRelationshipTypeStrategy.php +++ b/src/lib/Gateway/SubSelectRelationshipTypeStrategy.php @@ -8,7 +8,10 @@ namespace Ibexa\CorePersistence\Gateway; -use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Query\Exception\NonUniqueAlias; +use Doctrine\DBAL\Query\Exception\UnknownAlias; +use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Query\QueryException; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface; use LogicException; @@ -17,6 +20,19 @@ */ final class SubSelectRelationshipTypeStrategy implements RelationshipTypeStrategyInterface { + private function isQueryInitialised(QueryBuilder $queryBuilder): bool + { + try { + $queryBuilder->getSQL(); + + return true; + } catch (UnknownAlias | NonUniqueAlias) { + return true; + } catch (QueryException) { + return false; + } + } + public function handleRelationshipType( QueryBuilder $queryBuilder, DoctrineRelationshipInterface $relationship, @@ -24,7 +40,7 @@ public function handleRelationshipType( string $fromTable, string $toTable ): void { - if (empty($queryBuilder->getQueryPart('select'))) { + if (!$this->isQueryInitialised($queryBuilder)) { $queryBuilder ->select($toTable . '.' . $relationship->getRelatedClassIdColumn()) ->from($toTable); @@ -48,7 +64,7 @@ public function handleRelationshipTypeQuery( string $fullColumnName, string $placeholder ): QueryBuilder { - if (empty($queryBuilder->getQueryPart('select'))) { + if (!$this->isQueryInitialised($queryBuilder)) { throw new LogicException( 'Query is not initialized.', ); diff --git a/tests/bundle/Gateway/ExpressionVisitorTest.php b/tests/bundle/Gateway/ExpressionVisitorTest.php index 845eb90..af71cba 100644 --- a/tests/bundle/Gateway/ExpressionVisitorTest.php +++ b/tests/bundle/Gateway/ExpressionVisitorTest.php @@ -10,7 +10,9 @@ use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\Common\Collections\Expr\CompositeExpression; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Query\QueryBuilder; @@ -45,7 +47,7 @@ protected function setUp(): void { $this->connection = $this->createMock(Connection::class); - $this->connection->method('getExpressionBuilder') + $this->connection->method('createExpressionBuilder') ->willReturn(new ExpressionBuilder($this->connection)); $platform = $this->getMockBuilder(AbstractPlatform::class) @@ -56,6 +58,14 @@ protected function setUp(): void $this->schemaMetadata = $this->createMock(DoctrineSchemaMetadataInterface::class); + $this->schemaMetadata + ->method('getBindingTypeForColumn') + ->willReturn(ParameterType::STRING); + + $this->schemaMetadata + ->method('getArrayBindingTypeForColumn') + ->willReturn(ArrayParameterType::STRING); + $this->registry = $this->createMock(DoctrineSchemaMetadataRegistryInterface::class); $this->registry->method('getMetadataForTable') ->with('table_name') @@ -83,7 +93,7 @@ public function testWalkComparison(): void new Parameter( 'field_0', 'value', - 0, + ParameterType::STRING, ), ], $this->expressionVisitor->getParameters()); } @@ -101,7 +111,7 @@ public function testLogicalNot(): void new Parameter( 'field_0', 'value', - 0, + ParameterType::STRING, ), ], $this->expressionVisitor->getParameters()); } @@ -126,12 +136,12 @@ public function testLogicalAnd(): void new Parameter( 'field_0', 'value', - 0, + ParameterType::STRING, ), new Parameter( 'field_2_1', 'value_2', - 0, + ParameterType::STRING, ), ], $this->expressionVisitor->getParameters()); } @@ -278,37 +288,37 @@ public static function provideForFieldFromInheritedRelationship(): iterable yield [ new Comparison('relationship_1.field', '=', 'value'), 'relationship_table_name.field = :field_0', - [new Parameter('field_0', 'value', 0)], + [new Parameter('field_0', 'value', ParameterType::STRING)], ]; yield [ new Comparison('relationship_1.field', 'IN', ['value', 'value_2']), 'relationship_table_name.field IN (:field_0)', - [new Parameter('field_0', ['value', 'value_2'], 100)], + [new Parameter('field_0', ['value', 'value_2'], ArrayParameterType::STRING)], ]; yield [ new Comparison('relationship_1.field', '=', ['value', 'value_2']), 'relationship_table_name.field IN (:field_0)', - [new Parameter('field_0', ['value', 'value_2'], 100)], + [new Parameter('field_0', ['value', 'value_2'], ArrayParameterType::STRING)], ]; yield [ new Comparison('relationship_1.field', 'STARTS_WITH', 'value'), 'relationship_table_name.field LIKE :field_0', - [new Parameter('field_0', 'value%', 0)], + [new Parameter('field_0', 'value%', ParameterType::STRING)], ]; yield [ new Comparison('relationship_1.field', 'ENDS_WITH', 'value'), 'relationship_table_name.field LIKE :field_0', - [new Parameter('field_0', '%value', 0)], + [new Parameter('field_0', '%value', ParameterType::STRING)], ]; yield [ new Comparison('relationship_1.field', 'CONTAINS', 'value'), 'relationship_table_name.field LIKE :field_0', - [new Parameter('field_0', '%value%', 0)], + [new Parameter('field_0', '%value%', ParameterType::STRING)], ]; } @@ -393,6 +403,14 @@ public function testFieldFromSubclass(): void ->with('inherited_field') ->willReturn($inheritanceMetadata); + $inheritanceMetadata + ->method('getBindingTypeForColumn') + ->willReturn(ParameterType::STRING); + + $inheritanceMetadata + ->method('getArrayBindingTypeForColumn') + ->willReturn(ArrayParameterType::STRING); + $inheritanceMetadata ->expects(self::once()) ->method('getTableName') @@ -424,6 +442,14 @@ private function createRelationshipSchemaMetadata(string $tableName = 'relations ->method('getIdentifierColumn') ->willReturn('id'); + $relationshipMetadata + ->method('getBindingTypeForColumn') + ->willReturn(ParameterType::STRING); + + $relationshipMetadata + ->method('getArrayBindingTypeForColumn') + ->willReturn(ArrayParameterType::STRING); + return $relationshipMetadata; } diff --git a/tests/lib/Gateway/BaseRelationshipTypeStrategyTestCase.php b/tests/lib/Gateway/BaseRelationshipTypeStrategyTestCase.php index 50afc19..53b90ff 100644 --- a/tests/lib/Gateway/BaseRelationshipTypeStrategyTestCase.php +++ b/tests/lib/Gateway/BaseRelationshipTypeStrategyTestCase.php @@ -25,7 +25,7 @@ protected function setUp(): void { $this->connection = $this->createMock(Connection::class); $this->connection - ->method('getExpressionBuilder') + ->method('createExpressionBuilder') ->willReturn(new ExpressionBuilder($this->connection)); $platform = $this->getMockBuilder(AbstractPlatform::class) diff --git a/tests/lib/Gateway/JoinedRelationshipTypeStrategyTest.php b/tests/lib/Gateway/JoinedRelationshipTypeStrategyTest.php index 6537e17..769a4dc 100644 --- a/tests/lib/Gateway/JoinedRelationshipTypeStrategyTest.php +++ b/tests/lib/Gateway/JoinedRelationshipTypeStrategyTest.php @@ -29,6 +29,7 @@ protected function setUp(): void public function testHandleRelationshipType(): void { $queryBuilder = new QueryBuilder($this->connection); + $queryBuilder->select('from_table.id')->from('from_table'); $this->strategy->handleRelationshipType( $queryBuilder, @@ -38,36 +39,38 @@ public function testHandleRelationshipType(): void 'to_table' ); - self::assertEmpty($queryBuilder->getQueryPart('select')); - self::assertEmpty($queryBuilder->getQueryPart('from')); - self::assertEmpty($queryBuilder->getQueryPart('where')); self::assertSame( - [ - 'from_table' => [ - [ - 'joinType' => 'left', - 'joinTable' => 'to_table', - 'joinAlias' => 'to_table', - 'joinCondition' => 'from_table.foreign_key_column = to_table.related_class_id_column', - ], - ], - ], - $queryBuilder->getQueryPart('join') + 'SELECT from_table.id FROM from_table' + . ' LEFT JOIN to_table to_table' + . ' ON from_table.foreign_key_column = to_table.related_class_id_column', + $queryBuilder->getSQL() ); } - public function testHandleRelationshipTypeQuery(): void + public function testHandleRelationshipTypeIsIdempotentForTheSameTable(): void { $queryBuilder = new QueryBuilder($this->connection); + $queryBuilder->select('from_table.id')->from('from_table'); + + $relationship = $this->createDoctrineRelationship(DoctrineRelationship::JOIN_TYPE_JOINED); + $this->strategy->handleRelationshipType($queryBuilder, $relationship, 'root_alias', 'from_table', 'to_table'); + $sqlAfterFirstJoin = $queryBuilder->getSQL(); + $this->strategy->handleRelationshipType($queryBuilder, $relationship, 'root_alias', 'from_table', 'to_table'); + + self::assertSame($sqlAfterFirstJoin, $queryBuilder->getSQL()); + } + + public function testHandleRelationshipTypeQueryLeavesTheQueryUntouched(): void + { + $queryBuilder = new QueryBuilder($this->connection); + $queryBuilder->select('from_table.id')->from('from_table'); + $relationshipQuery = $this->strategy->handleRelationshipTypeQuery( $queryBuilder, 'to_table.related_class_id_column_0', ':related_class_id_column_0' ); - self::assertEmpty($relationshipQuery->getQueryPart('select')); - self::assertEmpty($relationshipQuery->getQueryPart('from')); - self::assertEmpty($relationshipQuery->getQueryPart('where')); - self::assertEmpty($relationshipQuery->getQueryPart('join')); + self::assertSame('SELECT from_table.id FROM from_table', $relationshipQuery->getSQL()); } } diff --git a/tests/lib/Gateway/SubSelectRelationshipTypeStrategyTest.php b/tests/lib/Gateway/SubSelectRelationshipTypeStrategyTest.php index 394aacf..073fd05 100644 --- a/tests/lib/Gateway/SubSelectRelationshipTypeStrategyTest.php +++ b/tests/lib/Gateway/SubSelectRelationshipTypeStrategyTest.php @@ -8,7 +8,6 @@ namespace Ibexa\Tests\CorePersistence\Gateway; -use Doctrine\DBAL\Query\Expression\CompositeExpression; use Doctrine\DBAL\Query\QueryBuilder; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship; use Ibexa\CorePersistence\Gateway\SubSelectRelationshipTypeStrategy; @@ -28,54 +27,52 @@ protected function setUp(): void $this->strategy = new SubSelectRelationshipTypeStrategy(); } - public function testHandleRelationshipType(): void + public function testHandleRelationshipTypeInitialisesAnEmptyQuery(): void { $queryBuilder = new QueryBuilder($this->connection); $this->strategy->handleRelationshipType( $queryBuilder, $this->createDoctrineRelationship(DoctrineRelationship::JOIN_TYPE_SUB_SELECT), - 'root_alias', - 'from_table', + 'to_table', + 'to_table', 'to_table' ); self::assertSame( - ['to_table.related_class_id_column'], - $queryBuilder->getQueryPart('select') + 'SELECT to_table.related_class_id_column FROM to_table', + $queryBuilder->getSQL() ); - self::assertSame( - [ - [ - 'table' => 'to_table', - 'alias' => null, - ], - ], - $queryBuilder->getQueryPart('from') + } + + public function testHandleRelationshipTypeJoinsWhenFromTableIsNotTheRootAlias(): void + { + $queryBuilder = new QueryBuilder($this->connection); + $queryBuilder->select('from_table.id')->from('from_table'); + + $this->strategy->handleRelationshipType( + $queryBuilder, + $this->createDoctrineRelationship(DoctrineRelationship::JOIN_TYPE_SUB_SELECT), + 'root_alias', + 'from_table', + 'to_table' ); - self::assertEmpty($queryBuilder->getQueryPart('where')); + self::assertSame( - [ - 'from_table' => [ - [ - 'joinType' => 'inner', - 'joinTable' => 'to_table', - 'joinAlias' => 'to_table', - 'joinCondition' => 'from_table.foreign_key_column = to_table.related_class_id_column', - ], - ], - ], - $queryBuilder->getQueryPart('join') + 'SELECT from_table.id FROM from_table' + . ' INNER JOIN to_table to_table' + . ' ON from_table.foreign_key_column = to_table.related_class_id_column', + $queryBuilder->getSQL() ); } - public function testHandleRelationshipTypeQueryThrowsRuntimeMappingException(): void + public function testHandleRelationshipTypeQueryThrowsForAnUninitialisedQuery(): void { $this->expectException(LogicException::class); $this->expectExceptionMessage('Query is not initialized.'); $this->strategy->handleRelationshipTypeQuery( - $this->createMock(QueryBuilder::class), + new QueryBuilder($this->connection), 'alias.related_class_id_column', ':alias.related_class_id_column_0' ); @@ -95,28 +92,9 @@ public function testHandleRelationshipTypeQuery(): void ); self::assertSame( - ['test_alias.related_class_id_column'], - $relationshipQuery->getQueryPart('select') + 'SELECT test_alias.related_class_id_column FROM test_table test_alias' + . ' WHERE test_alias.related_class_id_column IN (:related_class_id_column_0)', + $relationshipQuery->getSQL() ); - self::assertSame( - [ - [ - 'table' => 'test_table', - 'alias' => 'test_alias', - ], - ], - $relationshipQuery->getQueryPart('from') - ); - self::assertEquals( - new CompositeExpression( - CompositeExpression::TYPE_AND, - [ - 'test_alias.related_class_id_column IN (:related_class_id_column_0)', - ] - ), - $relationshipQuery->getQueryPart('where') - ); - - self::assertEmpty($relationshipQuery->getQueryPart('join')); } } From 1f91bb59df3ad76e9255002cbc622069e813d8e8 Mon Sep 17 00:00:00 2001 From: Dawid Parafinski Date: Fri, 7 Aug 2026 12:17:31 +0200 Subject: [PATCH 2/2] [TMP] IBX-12043: Pinned Ibexa dependencies to their DBAL 4 branches Points ibexa/core, ibexa/doctrine-schema, ibexa/test-core at their dbal-4-upgrade branches so this one can resolve before they are merged. Revert this commit once they are. --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 20bce1a..8393c42 100644 --- a/composer.json +++ b/composer.json @@ -7,8 +7,8 @@ ], "require": { "php": " >=8.3", - "ibexa/core": "~6.0.x-dev", - "ibexa/doctrine-schema": "~6.0.x-dev", + "ibexa/core": "dev-dbal-4-upgrade as 6.0.x-dev", + "ibexa/doctrine-schema": "dev-dbal-4-upgrade as 6.0.x-dev", "symfony/config": "^7.4", "symfony/dependency-injection": "^7.4", "symfony/event-dispatcher": "^7.4", @@ -20,7 +20,7 @@ "dama/doctrine-test-bundle": "^8.2", "ibexa/code-style": "~2.0.0", "ibexa/rector": "~6.0.x-dev", - "ibexa/test-core": "~6.0.x-dev", + "ibexa/test-core": "dev-dbal-4-upgrade as 6.0.x-dev", "phpstan/phpstan": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.0",