Skip to content

Commit

Permalink
UUID PK support (#595)
Browse files Browse the repository at this point in the history
* UUID PK support

* fix

* styleci

* move to dedicated class

* fixes

* fixes for mariadb

* test fix

* test fix

* add psalm fix

* mysql 5.7 fix

* add tests for UuidHelper

* styleci fix
  • Loading branch information
darkdef committed Mar 18, 2023
1 parent 6eb289c commit 58ccf6f
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 5 deletions.
57 changes: 57 additions & 0 deletions src/Helper/UuidHelper.php
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Helper;

use Yiisoft\Db\Exception\InvalidArgumentException;

use function bin2hex;
use function hex2bin;
use function preg_match;
use function str_replace;

final class UuidHelper
{
public static function toUuid(string $blobString): string
{
if (self::isValidUuid($blobString)) {
return $blobString;
}

if (strlen($blobString) === 16) {
$hex = bin2hex($blobString);
} elseif (strlen($blobString) === 32 && self::isValidHexUuid($blobString)) {
$hex = $blobString;
} else {
throw new InvalidArgumentException('Length of source data is should be 16 or 32 bytes.');
}

return
substr($hex, 0, 8) . '-' .
substr($hex, 8, 4) . '-' .
substr($hex, 12, 4) . '-' .
substr($hex, 16, 4) . '-' .
substr($hex, 20)
;
}

public static function isValidUuid(string $uuidString): bool
{
return (bool) preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuidString);
}

public static function isValidHexUuid(string $uuidString): bool
{
return (bool) preg_match('/^[0-9a-f]{32}$/i', $uuidString);
}

public static function uuidToBlob(string $uuidString): string
{
if (!self::isValidUuid($uuidString)) {
throw new InvalidArgumentException('Incorrect UUID.');
}

return (string) hex2bin(str_replace('-', '', $uuidString));
}
}
15 changes: 10 additions & 5 deletions src/Schema/Builder/AbstractColumn.php
Expand Up @@ -39,6 +39,8 @@ abstract class AbstractColumn implements ColumnInterface
public const CATEGORY_NUMERIC = 'numeric';
public const CATEGORY_TIME = 'time';
public const CATEGORY_OTHER = 'other';
public const CATEGORY_UUID = 'uuid';
public const CATEGORY_UUID_PK = 'uuid_pk';

protected bool|null $isNotNull = null;
protected bool $isUnique = false;
Expand Down Expand Up @@ -72,6 +74,8 @@ abstract class AbstractColumn implements ColumnInterface
SchemaInterface::TYPE_BINARY => self::CATEGORY_OTHER,
SchemaInterface::TYPE_BOOLEAN => self::CATEGORY_NUMERIC,
SchemaInterface::TYPE_MONEY => self::CATEGORY_NUMERIC,
SchemaInterface::TYPE_UUID => self::CATEGORY_UUID,
SchemaInterface::TYPE_UUID_PK => self::CATEGORY_UUID_PK,
];

/**
Expand Down Expand Up @@ -159,11 +163,12 @@ public function append(string $sql): static

public function asString(): string
{
if ($this->getTypeCategory() === self::CATEGORY_PK) {
$format = '{type}{check}{comment}{append}';
} else {
$format = $this->format;
}
$format = match ($this->getTypeCategory()) {
self::CATEGORY_PK => '{type}{check}{comment}{append}',
self::CATEGORY_UUID => '{type}{notnull}{unique}{default}{check}{comment}{append}',
self::CATEGORY_UUID_PK => '{type}{notnull}{default}{check}{comment}{append}',
default => $this->format,
};

return $this->buildCompleteString($format);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/SchemaInterface.php
Expand Up @@ -47,10 +47,14 @@ interface SchemaInterface extends ConstraintSchemaInterface
public const INDEX_NONCLUSTERED = 'NONCLUSTERED';
/* Oracle */
public const INDEX_BITMAP = 'BITMAP';
/* DB Types */
public const TYPE_PK = 'pk';
public const TYPE_UPK = 'upk';
public const TYPE_BIGPK = 'bigpk';
public const TYPE_UBIGPK = 'ubigpk';
public const TYPE_UUID_PK = 'uuid_pk';
public const TYPE_UUID_PK_SEQ = 'uuid_pk_seq';
public const TYPE_UUID = 'uuid';
public const TYPE_CHAR = 'char';
public const TYPE_STRING = 'string';
public const TYPE_TEXT = 'text';
Expand All @@ -70,6 +74,7 @@ interface SchemaInterface extends ConstraintSchemaInterface
public const TYPE_MONEY = 'money';
public const TYPE_JSON = 'json';
public const TYPE_JSONB = 'jsonb';
/* PHP Types */
public const PHP_TYPE_INTEGER = 'integer';
public const PHP_TYPE_STRING = 'string';
public const PHP_TYPE_BOOLEAN = 'boolean';
Expand Down
99 changes: 99 additions & 0 deletions tests/Common/CommonColumnSchemaBuilderTest.php
Expand Up @@ -5,6 +5,10 @@
namespace Yiisoft\Db\Tests\Common;

use PHPUnit\Framework\TestCase;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Helper\UuidHelper;
use Yiisoft\Db\Query\Query;
use Yiisoft\Db\Schema\SchemaInterface;
use Yiisoft\Db\Tests\Support\TestTrait;

use function array_shift;
Expand All @@ -22,6 +26,58 @@ public function testCustomTypes(string $expected, string $type, int|null $length
$this->checkBuildString($expected, $type, $length, $calls);
}

/**
* @dataProvider \Yiisoft\Db\Tests\Provider\ColumnSchemaBuilderProvider::createColumnTypes
*/
public function testCreateColumnTypes(string $expected, string $type, int|null $length, array $calls): void
{
$this->checkCreateColumn($expected, $type, $length, $calls);
}

public function testUuid(): void
{
$db = $this->getConnection();
$schema = $db->getSchema();

$tableName = '{{%column_schema_builder_types}}';
if ($db->getTableSchema($tableName, true)) {
$db->createCommand()->dropTable($tableName)->execute();
}

$db->createCommand()->createTable($tableName, [
'uuid_pk' => $schema->createColumn(SchemaInterface::TYPE_UUID_PK),
'int_col' => $schema->createColumn(SchemaInterface::TYPE_INTEGER),
])->execute();
$tableSchema = $db->getTableSchema($tableName, true);
$this->assertNotNull($tableSchema);

$uuidValue = $uuidSource = '738146be-87b1-49f2-9913-36142fb6fcbe';

if ($db->getName() === 'oci') {
$uuidValue = new Expression('HEXTORAW(REGEXP_REPLACE(:uuid, \'-\', \'\'))', [':uuid' => $uuidValue]);
} elseif ($db->getName() === 'mysql') {
$uuidValue = UuidHelper::uuidToBlob($uuidValue);
}

$db->createCommand()->insert($tableName, [
'int_col' => 1,
'uuid_pk' => $uuidValue,
])->execute();

$uuid = (new Query($db))
->select(['[[uuid_pk]]'])
->from($tableName)
->where(['int_col' => 1])
->scalar()
;

$uuidString = strtolower(UuidHelper::toUuid($uuid));

$this->assertEquals($uuidSource, $uuidString);

$db->close();
}

protected function checkBuildString(string $expected, string $type, int|null $length, array $calls): void
{
$db = $this->getConnection();
Expand All @@ -38,4 +94,47 @@ protected function checkBuildString(string $expected, string $type, int|null $le

$db->close();
}

protected function checkCreateColumn(string $expected, string $type, int|null $length, array $calls): void
{
$db = $this->getConnection();

if (str_contains($expected, 'UUID_TO_BIN')) {
$serverVersion = $db->getServerVersion();
if (str_contains($serverVersion, 'MariaDB')) {
$db->close();
$this->markTestSkipped('UUID_TO_BIN not supported MariaDB as defaultValue');
}
if (version_compare($serverVersion, '8', '<')) {
$db->close();
$this->markTestSkipped('UUID_TO_BIN not exists in MySQL 5.7');
}
}

$schema = $db->getSchema();
$builder = $schema->createColumn($type, $length);

foreach ($calls as $call) {
$method = array_shift($call);
call_user_func_array([$builder, $method], $call);
}

$tableName = '{{%column_schema_builder_types}}';
if ($db->getTableSchema($tableName, true)) {
$db->createCommand()->dropTable($tableName)->execute();
}

$command = $db->createCommand()->createTable($tableName, [
'column' => $builder,
]);

$this->assertStringContainsString("\t" . $expected . "\n", $command->getRawSql());

$command->execute();

$tableSchema = $db->getTableSchema($tableName, true);
$this->assertNotNull($tableSchema);

$db->close();
}
}
91 changes: 91 additions & 0 deletions tests/Db/Helper/UuidHelperTest.php
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Tests\Db\Helper;

use PHPUnit\Framework\TestCase;
use Yiisoft\Db\Exception\InvalidArgumentException;
use Yiisoft\Db\Helper\UuidHelper;

/**
* @group db
*/
final class UuidHelperTest extends TestCase
{
/**
* @dataProvider successUuids
*/
public function testConvert(string $uuid): void
{
$blobUuid = UuidHelper::uuidToBlob($uuid);
$this->assertEquals($uuid, UuidHelper::toUuid($blobUuid));
}

/**
* @dataProvider incorrectUuids
*/
public function testToBlobIncorrectUuid(string $uuid): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Incorrect UUID.');

$blobUuid = UuidHelper::uuidToBlob($uuid);
$this->assertEquals($uuid, UuidHelper::toUuid($blobUuid));
}

/**
* @dataProvider blobUuids
*/
public function testToUuid($blobUuid, $expected): void
{
$uuid = UuidHelper::toUuid($blobUuid);
$this->assertEquals($expected, $uuid);
}

/**
* @dataProvider incorrectBlobUuids
*/
public function testToUuidFailed($blobUuid, $expected): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Length of source data is should be 16 or 32 bytes.');

$uuid = UuidHelper::toUuid($blobUuid);
$this->assertEquals($expected, $uuid);
}

public function successUuids(): array
{
return [
['738146be-87b1-49f2-9913-36142fb6fcbe'],
];
}

public function incorrectUuids(): array
{
return [
['738146be-87b149f2-9913-36142fb6fcbe'],
['738146be-87b1-K9f2-9913-36142fb6fcbe'],
['738146be+87b1-K9f2-9913-36142fb6fcbe'],
];
}

public function blobUuids(): array
{
return [
['738146be-87b1-49f2-9913-36142fb6fcbe', '738146be-87b1-49f2-9913-36142fb6fcbe'],
['738146be87b149f2991336142fb6fcbe', '738146be-87b1-49f2-9913-36142fb6fcbe'],
[hex2bin('738146be87b149f2991336142fb6fcbe'), '738146be-87b1-49f2-9913-36142fb6fcbe'],
];
}

public function incorrectBlobUuids(): array
{
return [
['738146be-87b1-49f2-9913-36142fbfcbe', '738146be-87b1-49f2-9913-36142fb6fcbe'],
['738146be87b149f2991336142fb6fcb', '738146be-87b1-49f2-9913-36142fb6fcbe'],
[hex2bin('738146be87b149f291336142fb6fcb'), '738146be-87b1-49f2-9913-36142fb6fcbe'],
];
}
}
2 changes: 2 additions & 0 deletions tests/Db/Schema/ColumnSchemaBuilderTest.php
Expand Up @@ -157,6 +157,8 @@ public function testGetCategoryMap(): void
'binary' => 'other',
'boolean' => 'numeric',
'money' => 'numeric',
'uuid' => 'uuid',
'uuid_pk' => 'uuid_pk',
],
$column->getCategoryMap(),
);
Expand Down

0 comments on commit 58ccf6f

Please sign in to comment.