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/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/Builder/UuidValueBuilder.php b/src/Builder/UuidValueBuilder.php new file mode 100644 index 00000000..d43765aa --- /dev/null +++ b/src/Builder/UuidValueBuilder.php @@ -0,0 +1,37 @@ + + */ +final class UuidValueBuilder implements ExpressionBuilderInterface +{ + public function __construct( + private readonly QueryBuilderInterface $queryBuilder, + ) {} + + public function build(ExpressionInterface $expression, array &$params = []): string + { + /** @var UuidValue $expression */ + return $this->queryBuilder->bindParam( + new Param(DbUuidHelper::uuidToBlob($expression->value), DataType::LOB), + $params, + ); + } +} 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/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; diff --git a/tests/Builder/UuidValueBuilderTest.php b/tests/Builder/UuidValueBuilderTest.php new file mode 100644 index 00000000..d7d4db83 --- /dev/null +++ b/tests/Builder/UuidValueBuilderTest.php @@ -0,0 +1,79 @@ + [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($value), $params); + + $this->assertSame(':qp0', $result); + $this->assertEquals( + [':qp0' => new Param(DbUuidHelper::uuidToBlob(self::UUID), DataType::LOB)], + $params, + ); + } + + #[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($value)])->execute(); + + $bytes = $db->createCommand('SELECT [[id]] FROM [[uuid_value]]')->queryScalar(); + + $this->assertIsString($bytes); + $this->assertSame(16, strlen($bytes)); + $this->assertSame(self::UUID, DbUuidHelper::toUuid($bytes)); + + $this->dropTable('uuid_value'); + } +}