From ee901ce92c1d41cb55d6d3114dabd8f3567fb312 Mon Sep 17 00:00:00 2001 From: zoran Date: Tue, 8 Sep 2026 07:16:20 +0200 Subject: [PATCH 1/6] New: Add SQLite implementation of `UuidValue` expression SQLite stores a UUID as 16 raw bytes in a `blob(16)` column, so `prepareValue()` converts the canonical form with `DbUuidHelper::uuidToBlob()` and binds it as `DataType::LOB`. Requires yiisoft/db#1199. --- CHANGELOG.md | 1 + src/Builder/UuidValueBuilder.php | 25 +++++++++++ src/DQLQueryBuilder.php | 3 ++ tests/Builder/UuidValueBuilderTest.php | 57 ++++++++++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 src/Builder/UuidValueBuilder.php create mode 100644 tests/Builder/UuidValueBuilderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d6ee9ea..d78724c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enh #414: Explicitly import classes and constants in "use" section (@mspirkov) - Enh #415: Remove unnecessary files from Composer package (@mspirkov) - Enh #416: Add `ext-pdo_sqlite` to `require` section of `composer.json` (@Tigrov) +- New #432: Add SQLite implementation of `UuidValue` expression (@KalimeroMK) ## 2.0.0 December 05, 2025 diff --git a/src/Builder/UuidValueBuilder.php b/src/Builder/UuidValueBuilder.php new file mode 100644 index 00000000..e3e8c690 --- /dev/null +++ b/src/Builder/UuidValueBuilder.php @@ -0,0 +1,25 @@ +value), DataType::LOB); + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 808b3a9c..f68d0972 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Expression\Function\ArrayMerge; use Yiisoft\Db\Expression\Function\Greatest; use Yiisoft\Db\Expression\Function\Least; +use Yiisoft\Db\Expression\Value\UuidValue; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; @@ -23,6 +24,7 @@ use Yiisoft\Db\Sqlite\Builder\JsonOverlapsBuilder; use Yiisoft\Db\Sqlite\Builder\LeastBuilder; use Yiisoft\Db\Sqlite\Builder\LikeBuilder; +use Yiisoft\Db\Sqlite\Builder\UuidValueBuilder; use function array_filter; use function array_merge; @@ -141,6 +143,7 @@ protected function defaultExpressionBuilders(): array ArrayMerge::class => ArrayMergeBuilder::class, Greatest::class => GreatestBuilder::class, Least::class => LeastBuilder::class, + UuidValue::class => UuidValueBuilder::class, ]; } } diff --git a/tests/Builder/UuidValueBuilderTest.php b/tests/Builder/UuidValueBuilderTest.php new file mode 100644 index 00000000..72cbc83a --- /dev/null +++ b/tests/Builder/UuidValueBuilderTest.php @@ -0,0 +1,57 @@ +getSharedConnection(); + $builder = new UuidValueBuilder($db->getQueryBuilder()); + + $params = []; + $result = $builder->build(new UuidValue(self::UUID), $params); + + $this->assertSame(':qp0', $result); + $this->assertEquals( + [':qp0' => new Param(DbUuidHelper::uuidToBlob(self::UUID), DataType::LOB)], + $params, + ); + } + + public function testInsertAndSelectUuid(): void + { + $db = $this->getSharedConnection(); + + $this->dropTable('uuid_value'); + $this->executeStatements('CREATE TABLE [[uuid_value]] ([[id]] blob(16) NOT NULL)'); + + $db->createCommand()->insert('uuid_value', ['id' => new UuidValue(self::UUID)])->execute(); + + $bytes = $db->createCommand('SELECT [[id]] FROM [[uuid_value]]')->queryScalar(); + + $this->assertSame(16, strlen($bytes)); + $this->assertSame(self::UUID, DbUuidHelper::toUuid($bytes)); + + $this->dropTable('uuid_value'); + } +} From 573d018d7cb6ac1746b684bfb8a7041bcd086d7d Mon Sep 17 00:00:00 2001 From: zoran Date: Tue, 8 Sep 2026 08:33:22 +0200 Subject: [PATCH 2/6] Rename the base-class alias and guard the read-back type in the test --- src/Builder/UuidValueBuilder.php | 4 ++-- tests/Builder/UuidValueBuilderTest.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Builder/UuidValueBuilder.php b/src/Builder/UuidValueBuilder.php index e3e8c690..9878d2f4 100644 --- a/src/Builder/UuidValueBuilder.php +++ b/src/Builder/UuidValueBuilder.php @@ -5,7 +5,7 @@ namespace Yiisoft\Db\Sqlite\Builder; use Yiisoft\Db\Constant\DataType; -use Yiisoft\Db\Expression\Value\Builder\UuidValueBuilder as AbstractUuidValueBuilder; +use Yiisoft\Db\Expression\Value\Builder\UuidValueBuilder as BaseUuidValueBuilder; use Yiisoft\Db\Expression\Value\Param; use Yiisoft\Db\Expression\Value\UuidValue; use Yiisoft\Db\Helper\DbUuidHelper; @@ -16,7 +16,7 @@ * SQLite stores a UUID as 16 raw bytes in a `blob(16)` column, so the canonical string form is converted to bytes and * bound as {@see DataType::LOB}. */ -final class UuidValueBuilder extends AbstractUuidValueBuilder +final class UuidValueBuilder extends BaseUuidValueBuilder { protected function prepareValue(UuidValue $expression): Param { diff --git a/tests/Builder/UuidValueBuilderTest.php b/tests/Builder/UuidValueBuilderTest.php index 72cbc83a..92a90ded 100644 --- a/tests/Builder/UuidValueBuilderTest.php +++ b/tests/Builder/UuidValueBuilderTest.php @@ -49,6 +49,7 @@ public function testInsertAndSelectUuid(): void $bytes = $db->createCommand('SELECT [[id]] FROM [[uuid_value]]')->queryScalar(); + $this->assertIsString($bytes); $this->assertSame(16, strlen($bytes)); $this->assertSame(self::UUID, DbUuidHelper::toUuid($bytes)); From 7fdf83bc9b804a1bca1bc52610107d114d75410c Mon Sep 17 00:00:00 2001 From: zoran Date: Fri, 11 Sep 2026 14:24:54 +0200 Subject: [PATCH 3/6] Cover every accepted UUID input form in the builder test --- tests/Builder/UuidValueBuilderTest.php | 29 ++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/Builder/UuidValueBuilderTest.php b/tests/Builder/UuidValueBuilderTest.php index 92a90ded..d7d4db83 100644 --- a/tests/Builder/UuidValueBuilderTest.php +++ b/tests/Builder/UuidValueBuilderTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Sqlite\Tests\Builder; +use PHPUnit\Framework\Attributes\DataProvider; use Yiisoft\Db\Constant\DataType; use Yiisoft\Db\Expression\Value\Param; use Yiisoft\Db\Expression\Value\UuidValue; @@ -12,6 +13,7 @@ use Yiisoft\Db\Sqlite\Tests\Support\IntegrationTestTrait; use Yiisoft\Db\Tests\Support\IntegrationTestCase; +use function hex2bin; use function strlen; /** @@ -22,14 +24,32 @@ final class UuidValueBuilderTest extends IntegrationTestCase use IntegrationTestTrait; private const UUID = '738146be-87b1-49f2-9913-36142fb6fcbe'; + private const HEX = '738146be87b149f2991336142fb6fcbe'; - public function testBuildBindsRawBytesAsLob(): void + /** + * Every form {@see UuidValue} accepts, all denoting the same UUID. + */ + public static function values(): iterable + { + yield 'canonical' => [self::UUID]; + yield 'canonical in upper case' => ['738146BE-87B1-49F2-9913-36142FB6FCBE']; + yield 'hexadecimal' => [self::HEX]; + yield 'hexadecimal in upper case' => ['738146BE87B149F2991336142FB6FCBE']; + yield 'bytes' => [hex2bin(self::HEX)]; + } + + /** + * `UuidValue` normalizes the value to the canonical form on construction, so the builder binds the same 16 bytes + * whichever form it was created from. + */ + #[DataProvider('values')] + public function testBuildBindsRawBytesAsLob(string $value): void { $db = $this->getSharedConnection(); $builder = new UuidValueBuilder($db->getQueryBuilder()); $params = []; - $result = $builder->build(new UuidValue(self::UUID), $params); + $result = $builder->build(new UuidValue($value), $params); $this->assertSame(':qp0', $result); $this->assertEquals( @@ -38,14 +58,15 @@ public function testBuildBindsRawBytesAsLob(): void ); } - public function testInsertAndSelectUuid(): void + #[DataProvider('values')] + public function testInsertAndSelectUuid(string $value): void { $db = $this->getSharedConnection(); $this->dropTable('uuid_value'); $this->executeStatements('CREATE TABLE [[uuid_value]] ([[id]] blob(16) NOT NULL)'); - $db->createCommand()->insert('uuid_value', ['id' => new UuidValue(self::UUID)])->execute(); + $db->createCommand()->insert('uuid_value', ['id' => new UuidValue($value)])->execute(); $bytes = $db->createCommand('SELECT [[id]] FROM [[uuid_value]]')->queryScalar(); From b9840fa4280650884ecb6baf7d3407741966834d Mon Sep 17 00:00:00 2001 From: zoran Date: Sun, 13 Sep 2026 07:46:41 +0200 Subject: [PATCH 4/6] Implement ExpressionBuilderInterface directly in UuidValueBuilder The core UuidValueBuilder is final now, so this builder no longer extends it. Dropping the inheritance keeps the coupling with the core package at the interface. --- src/Builder/UuidValueBuilder.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Builder/UuidValueBuilder.php b/src/Builder/UuidValueBuilder.php index 9878d2f4..317bbf8a 100644 --- a/src/Builder/UuidValueBuilder.php +++ b/src/Builder/UuidValueBuilder.php @@ -5,21 +5,33 @@ namespace Yiisoft\Db\Sqlite\Builder; use Yiisoft\Db\Constant\DataType; -use Yiisoft\Db\Expression\Value\Builder\UuidValueBuilder as BaseUuidValueBuilder; +use Yiisoft\Db\Expression\ExpressionBuilderInterface; +use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\Value\Param; use Yiisoft\Db\Expression\Value\UuidValue; use Yiisoft\Db\Helper\DbUuidHelper; +use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; /** * Builds a {@see UuidValue} expression for SQLite. * * SQLite stores a UUID as 16 raw bytes in a `blob(16)` column, so the canonical string form is converted to bytes and * bound as {@see DataType::LOB}. + * + * @implements ExpressionBuilderInterface */ -final class UuidValueBuilder extends BaseUuidValueBuilder +final class UuidValueBuilder implements ExpressionBuilderInterface { - protected function prepareValue(UuidValue $expression): Param + public function __construct( + private readonly QueryBuilderInterface $queryBuilder, + ) {} + + public function build(ExpressionInterface $expression, array &$params = []): string { - return new Param(DbUuidHelper::uuidToBlob($expression->value), DataType::LOB); + /** @var UuidValue $expression */ + return $this->queryBuilder->buildValue( + new Param(DbUuidHelper::uuidToBlob($expression->value), DataType::LOB), + $params, + ); } } From 8342ce1d6be0399343d6ad3890df02f1f7b25a94 Mon Sep 17 00:00:00 2001 From: zoran Date: Sun, 13 Sep 2026 11:04:55 +0200 Subject: [PATCH 5/6] Apply Rector: use null coalescing assignment --- src/AbstractTokenizer.php | 12 +++--------- src/SqlToken.php | 4 +--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/AbstractTokenizer.php b/src/AbstractTokenizer.php index 08a5cf8d..346295f7 100644 --- a/src/AbstractTokenizer.php +++ b/src/AbstractTokenizer.php @@ -265,9 +265,7 @@ protected function startsWithAnyLongest( */ protected function substring(int $length, bool $caseSensitive = true, ?int $offset = null): string { - if ($offset === null) { - $offset = $this->offset; - } + $offset ??= $this->offset; if ($offset + $length > $this->length) { return ''; @@ -275,9 +273,7 @@ protected function substring(int $length, bool $caseSensitive = true, ?int $offs $cacheKey = $offset . ',' . $length; - if (!isset($this->substrings[$cacheKey . ',1'])) { - $this->substrings[$cacheKey . ',1'] = mb_substr($this->sql, $offset, $length, 'UTF-8'); - } + $this->substrings[$cacheKey . ',1'] ??= mb_substr($this->sql, $offset, $length, 'UTF-8'); if (!$caseSensitive && !isset($this->substrings[$cacheKey . ',0'])) { $this->substrings[$cacheKey . ',0'] = mb_strtoupper($this->substrings[$cacheKey . ',1'], 'UTF-8'); @@ -296,9 +292,7 @@ protected function substring(int $length, bool $caseSensitive = true, ?int $offs */ protected function indexAfter(string $string, ?int $offset = null): int { - if ($offset === null) { - $offset = $this->offset; - } + $offset ??= $this->offset; if ($offset + mb_strlen($string, 'UTF-8') > $this->length) { return $this->length; diff --git a/src/SqlToken.php b/src/SqlToken.php index e509cadc..ce28a8b6 100644 --- a/src/SqlToken.php +++ b/src/SqlToken.php @@ -374,9 +374,7 @@ private function tokensMatch( continue; } - if ($firstMatchIndex === null) { - $firstMatchIndex = $offset; - } + $firstMatchIndex ??= $offset; $lastMatchIndex = $offset; $wildcard = false; From 309416bb40600e3b5353858b69519d58dd81a3ee Mon Sep 17 00:00:00 2001 From: zoran Date: Sun, 13 Sep 2026 20:36:26 +0200 Subject: [PATCH 6/6] Use bindParam() instead of buildValue() in UuidValueBuilder --- src/Builder/UuidValueBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Builder/UuidValueBuilder.php b/src/Builder/UuidValueBuilder.php index 317bbf8a..d43765aa 100644 --- a/src/Builder/UuidValueBuilder.php +++ b/src/Builder/UuidValueBuilder.php @@ -29,7 +29,7 @@ public function __construct( public function build(ExpressionInterface $expression, array &$params = []): string { /** @var UuidValue $expression */ - return $this->queryBuilder->buildValue( + return $this->queryBuilder->bindParam( new Param(DbUuidHelper::uuidToBlob($expression->value), DataType::LOB), $params, );