Skip to content

Commit

Permalink
IBX-8566: Fixed postgres language limit (#454)
Browse files Browse the repository at this point in the history
For more details see https://issues.ibexa.co/browse/IBX-8566 and #454

Key changes:

* Implemented PostgresqlGateway

* Implemented SharedGateway\AbstractGateway to avoid code duplication

* [Tests] Added integration test coverage for the use case

* [Tests] Aligned tests with the changes

* Fixed PHP deprecated error for $exp overflow for the last language
  • Loading branch information
reithor authored Nov 25, 2024
1 parent c38bd01 commit eed0cea
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 41 deletions.
11 changes: 1 addition & 10 deletions src/lib/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -1357,16 +1357,7 @@ public function setName(int $contentId, int $version, string $name, string $lang
*/
private function getSetNameLanguageMaskSubQuery(): string
{
return <<<SQL
(SELECT
CASE
WHEN (initial_language_id = :language_id AND (language_mask & :language_id) <> 0 )
THEN (:language_id | 1)
ELSE :language_id
END
FROM ezcontentobject
WHERE id = :content_id)
SQL;
return $this->sharedGateway->getSetNameLanguageMaskSubQuery();
}

public function deleteContent(int $contentId): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ public function extractLanguageIdsFromMask($languageMask): array
$result = [];

// Decomposition of $languageMask into its binary components.
while ($exp <= $languageMask) {
// check if $exp has not overflown and became float (happens for the last possible language in the mask)
while (is_int($exp) && $exp <= $languageMask) {
if ($languageMask & $exp) {
$result[] = $exp;
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/Persistence/Legacy/Content/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,8 @@ private function extractLanguageCodesFromMask(int $languageMask, array $allLangu
$result = [];

// Decomposition of $languageMask into its binary components to extract language codes
while ($exp <= $languageMask) {
// check if $exp has not overflown and became float (happens for the last possible language in the mask)
while (is_int($exp) && $exp <= $languageMask) {
if ($languageMask & $exp) {
if (isset($allLanguages[$exp])) {
$result[] = $allLanguages[$exp]->languageCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform;

use Doctrine\DBAL\Connection;
use Ibexa\Core\Persistence\Legacy\SharedGateway\Gateway;

/**
* @internal
*/
abstract class AbstractGateway implements Gateway
{
protected Connection $connection;

public function __construct(Connection $connection)
{
$this->connection = $connection;
}

public function getColumnNextIntegerValue(
string $tableName,
string $columnName,
string $sequenceName
): ?int {
return null;
}

/**
* Return a language sub select query for setName.
*
* The query generates the proper language mask at the runtime of the INSERT/UPDATE query
* generated by setName.
*
* @see setName
*/
public function getSetNameLanguageMaskSubQuery(): string
{
return <<<SQL
(SELECT
CASE
WHEN (initial_language_id = :language_id AND (language_mask & :language_id) <> 0 )
THEN (:language_id | 1)
ELSE :language_id
END
FROM ezcontentobject
WHERE id = :content_id)
SQL;
}

public function getLastInsertedId(string $sequenceName): int
{
return (int)$this->connection->lastInsertId($sequenceName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,8 @@

namespace Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform;

use Doctrine\DBAL\Connection;
use Ibexa\Core\Persistence\Legacy\SharedGateway\Gateway;

final class FallbackGateway implements Gateway
final class FallbackGateway extends AbstractGateway
{
/** @var \Doctrine\DBAL\Connection */
private $connection;

public function __construct(Connection $connection)
{
$this->connection = $connection;
}

public function getColumnNextIntegerValue(
string $tableName,
string $columnName,
string $sequenceName
): ?int {
return null;
}

public function getLastInsertedId(string $sequenceName): int
{
return (int)$this->connection->lastInsertId($sequenceName);
}
}

class_alias(FallbackGateway::class, 'eZ\Publish\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\FallbackGateway');
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform;

final class PostgresqlGateway extends AbstractGateway
{
/**
* Return a language sub select query for setName.
*
* The query generates the proper language mask at the runtime of the INSERT/UPDATE query
* generated by setName.
*
* @see setName
*/
public function getSetNameLanguageMaskSubQuery(): string
{
return <<<SQL
(SELECT
CASE
WHEN (initial_language_id = :language_id AND (language_mask & :language_id) <> 0 )
THEN (cast(:language_id as BIGINT) | 1)
ELSE :language_id
END
FROM ezcontentobject
WHERE id = :content_id)
SQL;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
namespace Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform;

use Ibexa\Core\Base\Exceptions\DatabaseException;
use Ibexa\Core\Persistence\Legacy\SharedGateway\Gateway;

final class SqliteGateway implements Gateway
final class SqliteGateway extends AbstractGateway
{
/**
* Error code 7 for a fatal error - taken from an existing driver implementation.
Expand All @@ -21,7 +20,7 @@ final class SqliteGateway implements Gateway
private const DB_INT_MAX = 2147483647;

/** @var array<string, int> */
private $lastInsertedIds = [];
private array $lastInsertedIds = [];

public function getColumnNextIntegerValue(
string $tableName,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/Persistence/Legacy/SharedGateway/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public function getColumnNextIntegerValue(
* It returns integer as all the IDs in the Ibexa Legacy Storage are (big)integers
*/
public function getLastInsertedId(string $sequenceName): int;

/**
* Return a language sub select query for setName.
*/
public function getSetNameLanguageMaskSubQuery(): string;
}

class_alias(Gateway::class, 'eZ\Publish\Core\Persistence\Legacy\SharedGateway\Gateway');
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ services:
bind:
$connection: '@ibexa.persistence.connection'

Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\AbstractGateway: ~

Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\FallbackGateway: ~

Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\SqliteGateway:
tags:
- { name: ibexa.storage.legacy.gateway.shared, platform: sqlite }

Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\PostgresqlGateway:
tags:
- { name: ibexa.storage.legacy.gateway.shared, platform: postgresql }

Ibexa\Core\Persistence\Legacy\SharedGateway\GatewayFactory:
arguments:
$fallbackGateway: '@Ibexa\Core\Persistence\Legacy\SharedGateway\DatabasePlatform\FallbackGateway'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Core\Repository\ContentService;

use Ibexa\Tests\Integration\Core\RepositoryTestCase;
use Symfony\Component\Yaml\Yaml;

/**
* @covers \Ibexa\Contracts\Core\Repository\ContentService
*/
final class MaxLanguagesContentServiceTest extends RepositoryTestCase
{
/** @var list<array{languageCode: string, name: string }> */
private static array $languagesRawList = [];

public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();

self::$languagesRawList = Yaml::parseFile(dirname(__DIR__) . '/_fixtures/max_languages.yaml');
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
*/
protected function setUp(): void
{
parent::setUp();

$this->prepareMaxLanguages();
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testCreateContent(): void
{
$names = array_merge(...array_map(
static fn (array $languageData): array => [
$languageData['languageCode'] => $languageData['name'] . ' name',
],
self::$languagesRawList
));
$this->createFolder($names);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
*/
private function prepareMaxLanguages(): void
{
$languageService = self::getLanguageService();

foreach (self::$languagesRawList as $languageData) {
$languageCreateStruct = $languageService->newLanguageCreateStruct();
$languageCreateStruct->languageCode = $languageData['languageCode'];
$languageCreateStruct->name = $languageData['name'];
$languageService->createLanguage($languageCreateStruct);
}
}
}
62 changes: 62 additions & 0 deletions tests/integration/Core/Repository/_fixtures/max_languages.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
- { languageCode: alb-SQ, name: Albanian }
- { languageCode: ara-AR, name: Arabic }
- { languageCode: aze-AZ, name: Azerbaijani }
- { languageCode: bos-BS, name: Bosnian }
- { languageCode: cha-CH, name: Chamorro }
- { languageCode: chi-ZH, name: Chinese }
- { languageCode: cze-CS, name: Czech }
- { languageCode: dan-DA, name: Danish }
- { languageCode: dut-NL, name: Dutch (Flemish) }
#- { languageCode: eng-GB, name: English (United Kingdom) } # Pre-exists in the initial db fixture
#- { languageCode: eng-US, name: English (United States) } # Pre-exists in the initial db fixture
- { languageCode: eng-AU, name: English (Australia) }
- { languageCode: epo-EO, name: Esperanto }
- { languageCode: esp-ES, name: Spanish }
- { languageCode: esp-MX, name: Spanish (Mexico) }
- { languageCode: est-ET, name: Estonian }
- { languageCode: fas-FA, name: Persian (Farsi) }
- { languageCode: fin-FI, name: Finnish }
- { languageCode: fre-FR, name: French (France) }
- { languageCode: fre-BE, name: French (Belgium) }
- { languageCode: fre-CA, name: French (Canada) }
- { languageCode: fre-CH, name: French (Switzerland) }
- { languageCode: fre-LU, name: French (Luxembourg) }
- { languageCode: geo-KA, name: Georgian }
#- { languageCode: ger-DE, name: German (Germany) } # Pre-exists in the initial db fixture
- { languageCode: ger-AT, name: German (Austria) }
- { languageCode: ger-CH, name: German (Switzerland) }
- { languageCode: ger-LI, name: German (Liechtenstein) }
- { languageCode: ger-LU, name: German (Luxembourg) }
- { languageCode: gle-GA, name: Irish }
- { languageCode: gla-GD, name: Scottish (Gaelic) }
- { languageCode: gre-EL, name: Greek }
- { languageCode: hin-HI, name: Hebrew }
- { languageCode: heb-HE, name: Hebrew }
- { languageCode: hrv-HR, name: Croatian }
- { languageCode: hun-HU, name: Hungarian }
- { languageCode: ind-ID, name: Indonesian }
- { languageCode: isl-IS, name: Icelandic }
- { languageCode: ita-IT, name: Italian }
- { languageCode: jpn-JA, name: Japanese }
- { languageCode: kor-KO, name: Korean }
- { languageCode: lat-LA, name: Latin }
- { languageCode: lav-LV, name: Latvian }
- { languageCode: lit-LT, name: Lithuanian }
- { languageCode: mao-MI, name: Maori (New Zealand) }
- { languageCode: may-MS, name: Malay }
- { languageCode: nor-NO, name: Norwegian }
- { languageCode: pol-PL, name: Polish }
- { languageCode: por-PT, name: Portuguese (Portugal) }
- { languageCode: por-BR, name: Portuguese (Brazil) }
- { languageCode: rum-RO, name: Romanian }
- { languageCode: slo-SK, name: Slovak }
- { languageCode: swe-SV, name: Swedish }
- { languageCode: bul-BG, name: Bulgarian }
- { languageCode: swa-SW, name: Swahili (Swahili) }
- { languageCode: tha-TH, name: Thai }
- { languageCode: tib-BO, name: Tibetan }
- { languageCode: tlh-TL, name: Klingon }
- { languageCode: tur-TR, name: Turkish }
- { languageCode: ukr-UK, name: Ukrainian }
- { languageCode: wel-CY, name: Welsh (Swahili) }
- { languageCode: yid-YI, name: Yiddish }
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class GatewayFactoryTest extends TestCase
public function setUp(): void
{
$gateways = [
'sqlite' => new SqliteGateway(),
'sqlite' => new SqliteGateway($this->createMock(Connection::class)),
];

$this->factory = new GatewayFactory(
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/Persistence/Legacy/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ final public function getSharedGateway(): SharedGateway\Gateway
$factory = new SharedGateway\GatewayFactory(
new SharedGateway\DatabasePlatform\FallbackGateway($connection),
[
'sqlite' => new SharedGateway\DatabasePlatform\SqliteGateway(),
'sqlite' => new SharedGateway\DatabasePlatform\SqliteGateway($connection),
]
);

Expand Down

0 comments on commit eed0cea

Please sign in to comment.