Browse Source

Merge pull request #11297 from AntonNguyen/add-binary-uuid-type

[RFC] Added database support for the binary-uuid type
Mark Story 8 years ago
parent
commit
9fa798e39a

+ 4 - 0
src/Database/Schema/MysqlSchema.php

@@ -134,6 +134,9 @@ class MysqlSchema extends BaseSchema
 
             return ['type' => TableSchema::TYPE_TEXT, 'length' => $length];
         }
+        if ($col === 'binary' && $length === 16) {
+            return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
+        }
         if (strpos($col, 'blob') !== false || $col === 'binary') {
             $lengthName = substr($col, 0, -4);
             $length = isset(Table::$columnLengths[$lengthName]) ? Table::$columnLengths[$lengthName] : null;
@@ -314,6 +317,7 @@ class MysqlSchema extends BaseSchema
             TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
             TableSchema::TYPE_INTEGER => ' INTEGER',
             TableSchema::TYPE_BIGINTEGER => ' BIGINT',
+            TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
             TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
             TableSchema::TYPE_FLOAT => ' FLOAT',
             TableSchema::TYPE_DECIMAL => ' DECIMAL',

+ 1 - 0
src/Database/Schema/PostgresSchema.php

@@ -354,6 +354,7 @@ class PostgresSchema extends BaseSchema
         $typeMap = [
             TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
             TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
+            TableSchema::TYPE_BINARY_UUID => ' UUID',
             TableSchema::TYPE_BINARY => ' BYTEA',
             TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
             TableSchema::TYPE_FLOAT => ' FLOAT',

+ 4 - 0
src/Database/Schema/SqliteSchema.php

@@ -99,6 +99,9 @@ class SqliteSchema extends BaseSchema
             return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
         }
 
+        if ($col === 'binary' && $length === 16) {
+            return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
+        }
         if (in_array($col, ['blob', 'clob'])) {
             return ['type' => TableSchema::TYPE_BINARY, 'length' => null];
         }
@@ -284,6 +287,7 @@ class SqliteSchema extends BaseSchema
     {
         $data = $schema->getColumn($name);
         $typeMap = [
+            TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
             TableSchema::TYPE_UUID => ' CHAR(36)',
             TableSchema::TYPE_TINYINTEGER => ' TINYINT',
             TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',

+ 1 - 0
src/Database/Schema/SqlserverSchema.php

@@ -335,6 +335,7 @@ class SqlserverSchema extends BaseSchema
             TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
             TableSchema::TYPE_INTEGER => ' INTEGER',
             TableSchema::TYPE_BIGINTEGER => ' BIGINT',
+            TableSchema::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
             TableSchema::TYPE_BOOLEAN => ' BIT',
             TableSchema::TYPE_FLOAT => ' FLOAT',
             TableSchema::TYPE_DECIMAL => ' DECIMAL',

+ 7 - 0
src/Database/Schema/TableSchemaInterface.php

@@ -30,6 +30,13 @@ interface TableSchemaInterface extends SchemaInterface
     const TYPE_BINARY = 'binary';
 
     /**
+     * Binary UUID column type
+     *
+     * @var string
+     */
+    const TYPE_BINARY_UUID = 'binaryuuid';
+
+    /**
      * Date column type
      *
      * @var string

+ 1 - 0
src/Database/Type.php

@@ -37,6 +37,7 @@ class Type implements TypeInterface
         'integer' => 'Cake\Database\Type\IntegerType',
         'biginteger' => 'Cake\Database\Type\IntegerType',
         'binary' => 'Cake\Database\Type\BinaryType',
+        'binaryuuid' => 'Cake\Database\Type\BinaryUuidType',
         'boolean' => 'Cake\Database\Type\BoolType',
         'date' => 'Cake\Database\Type\DateType',
         'datetime' => 'Cake\Database\Type\DateTimeType',

+ 169 - 0
src/Database/Type/BinaryUuidType.php

@@ -0,0 +1,169 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.6.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Type;
+
+use Cake\Core\Exception\Exception;
+use Cake\Database\Driver;
+use Cake\Database\Driver\Sqlserver;
+use Cake\Database\Type;
+use Cake\Database\TypeInterface;
+use Cake\Utility\Text;
+use PDO;
+
+/**
+ * Binary UUID type converter.
+ *
+ * Use to convert binary uuid data between PHP and the database types.
+ */
+class BinaryUuidType extends Type implements TypeInterface
+{
+    /**
+     * Identifier name for this type.
+     *
+     * (This property is declared here again so that the inheritance from
+     * Cake\Database\Type can be removed in the future.)
+     *
+     * @var string|null
+     */
+    protected $_name;
+
+    /**
+     * Constructor.
+     *
+     * (This method is declared here again so that the inheritance from
+     * Cake\Database\Type can be removed in the future.)
+     *
+     * @param string|null $name The name identifying this type
+     */
+    public function __construct($name = null)
+    {
+        $this->_name = $name;
+    }
+
+    /**
+     * Convert binary uuid data into the database format.
+     *
+     * Binary data is not altered before being inserted into the database.
+     * As PDO will handle reading file handles.
+     *
+     * @param string|resource $value The value to convert.
+     * @param \Cake\Database\Driver $driver The driver instance to convert with.
+     * @return string|resource
+     */
+    public function toDatabase($value, Driver $driver)
+    {
+        if (is_string($value)) {
+            return $this->convertStringToBinaryUuid($value);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Generate a new binary UUID
+     *
+     * @return string A new primary key value.
+     */
+    public function newId()
+    {
+        return Text::uuid();
+    }
+
+    /**
+     * Convert binary uuid into resource handles
+     *
+     * @param null|string|resource $value The value to convert.
+     * @param \Cake\Database\Driver $driver The driver instance to convert with.
+     * @return resource|null
+     * @throws \Cake\Core\Exception\Exception
+     */
+    public function toPHP($value, Driver $driver)
+    {
+        if ($value === null) {
+            return null;
+        }
+        if (is_string($value)) {
+            return $this->convertBinaryUuidToString($value);
+        }
+        if (is_resource($value)) {
+            return $value;
+        }
+
+        throw new Exception(sprintf('Unable to convert %s into binary uuid.', gettype($value)));
+    }
+
+    /**
+     * Get the correct PDO binding type for Binary data.
+     *
+     * @param mixed $value The value being bound.
+     * @param \Cake\Database\Driver $driver The driver.
+     * @return int
+     */
+    public function toStatement($value, Driver $driver)
+    {
+        return PDO::PARAM_LOB;
+    }
+
+    /**
+     * Marshalls flat data into PHP objects.
+     *
+     * Most useful for converting request data into PHP objects
+     * that make sense for the rest of the ORM/Database layers.
+     *
+     * @param mixed $value The value to convert.
+     *
+     * @return mixed Converted value.
+     */
+    public function marshal($value)
+    {
+        return $value;
+    }
+
+    /**
+     * Converts a binary uuid to a string representation
+     *
+     *
+     * @param mixed $binary The value to convert.
+     *
+     * @return string Converted value.
+     */
+    protected function convertBinaryUuidToString($binary)
+    {
+        $string = unpack("H*", $binary);
+
+        $string = preg_replace(
+            "/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/",
+            "$1-$2-$3-$4-$5",
+            $string
+        );
+
+        return $string[1];
+    }
+
+    /**
+     * Converts a string uuid to a binary representation
+     *
+     *
+     * @param mixed $string The value to convert.
+     *
+     * @return mixed Converted value.
+     */
+    protected function convertStringToBinaryUuid($string)
+    {
+        $string = str_replace('-', '', $string);
+
+        return pack("H*", $string);
+    }
+}

+ 47 - 0
tests/Fixture/BinaryUuiditemsFixture.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         1.2.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+/**
+ * BinaryUuiditemsFixture
+ */
+class BinaryUuiditemsFixture extends TestFixture
+{
+
+    /**
+     * fields property
+     *
+     * @var array
+     */
+    public $fields = [
+        'id' => ['type' => 'binaryuuid'],
+        'name' => ['type' => 'string', 'null' => false],
+        'published' => ['type' => 'boolean', 'null' => false],
+        '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
+    ];
+
+    /**
+     * records property
+     *
+     * @var array
+     */
+    public $records = [
+        ['id' => '481fc6d0-b920-43e0-a40d-6d1740cf8569', 'published' => true, 'name' => 'Item 1'],
+        ['id' => '48298a29-81c0-4c26-a7fb-413140cf8569', 'published' => false, 'name' => 'Item 2'],
+        ['id' => '482b7756-8da0-419a-b21f-27da40cf8569', 'published' => true, 'name' => 'Item 3'],
+    ];
+}

+ 9 - 0
tests/TestCase/Database/Schema/MysqlSchemaTest.php

@@ -120,6 +120,10 @@ class MysqlSchemaTest extends TestCase
                 ['type' => 'uuid', 'length' => null]
             ],
             [
+                'BINARY(16)',
+                ['type' => 'binaryuuid', 'length' => null]
+            ],
+            [
                 'TEXT',
                 ['type' => 'text', 'length' => null]
             ],
@@ -531,6 +535,11 @@ SQL;
                 '`id` CHAR(36)'
             ],
             [
+                'id',
+                ['type' => 'binaryuuid'],
+                '`id` BINARY(16)'
+            ],
+            [
                 'title',
                 ['type' => 'string', 'length' => 255, 'null' => false, 'collate' => 'utf8_unicode_ci'],
                 '`title` VARCHAR(255) COLLATE utf8_unicode_ci NOT NULL'

+ 5 - 0
tests/TestCase/Database/Schema/PostgresSchemaTest.php

@@ -665,6 +665,11 @@ SQL;
                 '"id" UUID NOT NULL'
             ],
             [
+                'id',
+                ['type' => 'binaryuuid', 'length' => null, 'null' => false],
+                '"id" UUID NOT NULL'
+            ],
+            [
                 'role',
                 ['type' => 'string', 'length' => 10, 'null' => false, 'default' => 'admin'],
                 '"role" VARCHAR(10) NOT NULL DEFAULT \'admin\''

+ 9 - 0
tests/TestCase/Database/Schema/SqliteSchemaTest.php

@@ -84,6 +84,10 @@ class SqliteSchemaTest extends TestCase
                 ['type' => 'uuid', 'length' => null]
             ],
             [
+                'BINARY(16)',
+                ['type' => 'binaryuuid', 'length' => null]
+            ],
+            [
                 'BLOB',
                 ['type' => 'binary', 'length' => null]
             ],
@@ -487,6 +491,11 @@ SQL;
                 ['type' => 'uuid'],
                 '"id" CHAR(36)'
             ],
+            [
+                'id',
+                ['type' => 'binaryuuid'],
+                '"id" BINARY(16)'
+            ],
             // Text
             [
                 'body',

+ 5 - 0
tests/TestCase/Database/Schema/SqlserverSchemaTest.php

@@ -532,6 +532,11 @@ SQL;
                 '[id] UNIQUEIDENTIFIER NOT NULL'
             ],
             [
+                'id',
+                ['type' => 'binaryuuid', 'null' => false],
+                '[id] UNIQUEIDENTIFIER NOT NULL'
+            ],
+            [
                 'role',
                 ['type' => 'string', 'length' => 10, 'null' => false, 'default' => 'admin'],
                 "[role] NVARCHAR(10) NOT NULL DEFAULT 'admin'"

+ 113 - 0
tests/TestCase/Database/Type/BinaryUuidTypeTest.php

@@ -0,0 +1,113 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.6.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Database\Type;
+
+use Cake\Database\Type;
+use Cake\Database\Type\BinaryUuidType;
+use Cake\TestSuite\TestCase;
+use Cake\Utility\Text;
+use PDO;
+
+/**
+ * Test for the Binary uuid type.
+ */
+class BinaryUuidTypeTest extends TestCase
+{
+    /**
+     * @var \Cake\Database\Type\BinaryUuidType
+     */
+    public $type;
+
+    /**
+     * @var \Cake\Database\Driver
+     */
+    public $driver;
+
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $this->type = new BinaryUuidType();
+        $this->driver = $this->getMockBuilder('Cake\Database\Driver')->getMock();
+    }
+
+    /**
+     * Test toPHP
+     *
+     * @return void
+     */
+    public function testToPHP()
+    {
+        $this->assertNull($this->type->toPHP(null, $this->driver));
+
+        $result = $this->type->toPHP(Text::uuid(), $this->driver);
+        $uuidRegex = '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/';
+        preg_match_all(
+            $uuidRegex,
+            $result,
+            $matches
+        );
+
+        $result = $matches[0];
+        $this->assertSame(count($result), 2);
+
+        $fh = fopen(__FILE__, 'r');
+        $result = $this->type->toPHP($fh, $this->driver);
+        $this->assertSame($fh, $result);
+        $this->assertInternalType('resource', $result);
+        fclose($fh);
+    }
+
+    /**
+     * Test exceptions on invalid data.
+     *
+     * @expectedException \Cake\Core\Exception\Exception
+     * @expectedExceptionMessage Unable to convert array into binary uuid.
+     */
+    public function testToPHPFailure()
+    {
+        $this->type->toPHP([], $this->driver);
+    }
+
+    /**
+     * Test converting to database format
+     *
+     * @return void
+     */
+    public function testToDatabase()
+    {
+        $fh = fopen(__FILE__, 'r');
+        $result = $this->type->toDatabase($fh, $this->driver);
+        $this->assertSame($fh, $result);
+
+        $value = Text::uuid();
+        $result = $this->type->toDatabase($value, $this->driver);
+        $this->assertSame(str_replace('-', '', $value), unpack('H*', $result)[1]);
+    }
+
+    /**
+     * Test that the PDO binding type is correct.
+     *
+     * @return void
+     */
+    public function testToStatement()
+    {
+        $this->assertEquals(PDO::PARAM_LOB, $this->type->toStatement('', $this->driver));
+    }
+}

+ 45 - 15
tests/TestCase/ORM/TableUuidTest.php

@@ -32,7 +32,8 @@ class TableUuidTest extends TestCase
      * @var array
      */
     public $fixtures = [
-        'core.uuiditems', 'core.uuidportfolios'
+        'core.binary_uuiditems',
+        'core.uuiditems',
     ];
 
     /**
@@ -59,17 +60,28 @@ class TableUuidTest extends TestCase
     }
 
     /**
+     * Provider for testing that string and binary uuids work the same
+     *
+     * @return array
+     */
+    public function uuidTableProvider()
+    {
+        return [['uuiditems'], ['binary_uuiditems']];
+    }
+
+    /**
      * Test saving new records sets uuids
      *
+     * @dataProvider uuidTableProvider
      * @return void
      */
-    public function testSaveNew()
+    public function testSaveNew($tableName)
     {
         $entity = new Entity([
             'name' => 'shiny new',
             'published' => true,
         ]);
-        $table = TableRegistry::get('uuiditems');
+        $table = TableRegistry::get($tableName);
         $this->assertSame($entity, $table->save($entity));
         $this->assertRegExp('/^[a-f0-9-]{36}$/', $entity->id, 'Should be 36 characters');
 
@@ -81,9 +93,10 @@ class TableUuidTest extends TestCase
     /**
      * Test saving new records allows manual uuids
      *
+     * @dataProvider uuidTableProvider
      * @return void
      */
-    public function testSaveNewSpecificId()
+    public function testSaveNewSpecificId($tableName)
     {
         $id = Text::uuid();
         $entity = new Entity([
@@ -91,7 +104,7 @@ class TableUuidTest extends TestCase
             'name' => 'shiny and new',
             'published' => true,
         ]);
-        $table = TableRegistry::get('uuiditems');
+        $table = TableRegistry::get($tableName);
         $this->assertSame($entity, $table->save($entity));
         $this->assertSame($id, $entity->id);
 
@@ -104,9 +117,10 @@ class TableUuidTest extends TestCase
     /**
      * Test saving existing records works
      *
+     * @dataProvider uuidTableProvider
      * @return void
      */
-    public function testSaveUpdate()
+    public function testSaveUpdate($tableName)
     {
         $id = '481fc6d0-b920-43e0-a40d-6d1740cf8569';
         $entity = new Entity([
@@ -115,7 +129,7 @@ class TableUuidTest extends TestCase
             'published' => true,
         ]);
 
-        $table = TableRegistry::get('uuiditems');
+        $table = TableRegistry::get($tableName);
         $this->assertSame($entity, $table->save($entity));
         $this->assertEquals($id, $entity->id, 'Should be 36 characters');
 
@@ -127,28 +141,44 @@ class TableUuidTest extends TestCase
     /**
      * Test delete with string pk.
      *
+     * @dataProvider uuidTableProvider
      * @return void
      */
-    public function testDelete()
+    public function testGetById($tableName)
     {
-        $id = '481fc6d0-b920-43e0-a40d-6d1740cf8569';
-        $table = TableRegistry::get('uuiditems');
-        $entity = $table->find('all')->where(['id' => $id])->first();
+        $table = TableRegistry::get($tableName);
+        $entity = $table->find('all')->firstOrFail();
+
+        $result = $table->get($entity->id);
+        $this->assertSame($result->id, $entity->id);
+    }
+
+    /**
+     * Test delete with string pk.
+     *
+     * @dataProvider uuidTableProvider
+     * @return void
+     */
+    public function testDelete($tableName)
+    {
+        $table = TableRegistry::get($tableName);
+        $entity = $table->find('all')->firstOrFail();
 
         $this->assertTrue($table->delete($entity));
-        $query = $table->find('all')->where(['id' => $id]);
-        $this->assertCount(0, $query->execute(), 'No rows left');
+        $query = $table->find('all')->where(['id' => $entity->id]);
+        $this->assertEmpty($query->first(), 'No row left');
     }
 
     /**
      * Tests that sql server does not error when an empty uuid is bound
      *
+     * @dataProvider uuidTableProvider
      * @return void
      */
-    public function testEmptyUuid()
+    public function testEmptyUuid($tableName)
     {
         $id = '';
-        $table = TableRegistry::get('uuiditems');
+        $table = TableRegistry::get($tableName);
         $entity = $table->find('all')
             ->where(['id' => $id])
             ->first();