Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 3 additions & 9 deletions src/AbstractTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
*/
public function tokenize(): SqlToken
{
$this->length = mb_strlen($this->sql, 'UTF-8');

Check warning on line 89 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "MBString": @@ @@ */ public function tokenize(): SqlToken { - $this->length = mb_strlen($this->sql, 'UTF-8'); + $this->length = strlen($this->sql); $this->offset = 0; $this->substrings = []; $this->buffer = '';
$this->offset = 0;
$this->substrings = [];
$this->buffer = '';
Expand Down Expand Up @@ -121,14 +121,14 @@
$this->advance(1);
}

$this->addTokenFromBuffer();

Check warning on line 124 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ $this->buffer .= $this->substring(1); $this->advance(1); } - $this->addTokenFromBuffer(); + if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { unset($token[-1]); }

if (
$token->getHasChildren()

Check warning on line 127 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "LogicalAndSingleSubExprNegation": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if (!$token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { unset($token[-1]); } return $token;

Check warning on line 127 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "LogicalAndAllSubExprNegation": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if (!$token->getHasChildren() && !$token[-1] instanceof SqlToken && $token[-1]->getHasChildren()) { unset($token[-1]); } return $token;

Check warning on line 127 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "LogicalAnd": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if (($token->getHasChildren() || $token[-1] instanceof SqlToken) && !$token[-1]->getHasChildren()) { unset($token[-1]); } return $token;
&& $token[-1] instanceof SqlToken

Check warning on line 128 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "InstanceOf_": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if ($token->getHasChildren() && !$token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { unset($token[-1]); } return $token;

Check warning on line 128 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if ($token->getHasChildren() && $token[-0] instanceof SqlToken && !$token[-1]->getHasChildren()) { unset($token[-1]); } return $token;

Check warning on line 128 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if ($token->getHasChildren() && $token[-2] instanceof SqlToken && !$token[-1]->getHasChildren()) { unset($token[-1]); } return $token;
&& !$token[-1]->getHasChildren()

Check warning on line 129 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $this->advance(1); } $this->addTokenFromBuffer(); - if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { + if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-0]->getHasChildren()) { unset($token[-1]); } return $token;
) {
unset($token[-1]);

Check warning on line 131 in src/AbstractTokenizer.php

View workflow job for this annotation

GitHub Actions / PHP 8.5-sqlite-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ } $this->addTokenFromBuffer(); if ($token->getHasChildren() && $token[-1] instanceof SqlToken && !$token[-1]->getHasChildren()) { - unset($token[-1]); + unset($token[-0]); } return $token; }
}

return $token;
Expand Down Expand Up @@ -265,19 +265,15 @@
*/
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 '';
}

$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');
Expand All @@ -296,9 +292,7 @@
*/
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;
Expand Down
37 changes: 37 additions & 0 deletions src/Builder/UuidValueBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Sqlite\Builder;

use Yiisoft\Db\Constant\DataType;
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<UuidValue>
*/
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,
);
}
}
3 changes: 3 additions & 0 deletions src/DQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -141,6 +143,7 @@ protected function defaultExpressionBuilders(): array
ArrayMerge::class => ArrayMergeBuilder::class,
Greatest::class => GreatestBuilder::class,
Least::class => LeastBuilder::class,
UuidValue::class => UuidValueBuilder::class,
];
}
}
4 changes: 1 addition & 3 deletions src/SqlToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,7 @@ private function tokensMatch(
continue;
}

if ($firstMatchIndex === null) {
$firstMatchIndex = $offset;
}
$firstMatchIndex ??= $offset;

$lastMatchIndex = $offset;
$wildcard = false;
Expand Down
79 changes: 79 additions & 0 deletions tests/Builder/UuidValueBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

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;
use Yiisoft\Db\Helper\DbUuidHelper;
use Yiisoft\Db\Sqlite\Builder\UuidValueBuilder;
use Yiisoft\Db\Sqlite\Tests\Support\IntegrationTestTrait;
use Yiisoft\Db\Tests\Support\IntegrationTestCase;

use function hex2bin;
use function strlen;

/**
* @group sqlite
*/
final class UuidValueBuilderTest extends IntegrationTestCase
{
use IntegrationTestTrait;

private const UUID = '738146be-87b1-49f2-9913-36142fb6fcbe';
private const HEX = '738146be87b149f2991336142fb6fcbe';

/**
* 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($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');
}
}