diff --git a/Spanner/src/Database.php b/Spanner/src/Database.php index 477c4d65429b..94f864c3672f 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -115,6 +115,7 @@ class Database public const TYPE_JSON = TypeCode::JSON; public const TYPE_PG_OID = 'pgOid'; public const TYPE_INTERVAL = TypeCode::INTERVAL; + public const TYPE_UUID = TypeCode::UUID; private Operation $operation; private IamManager|null $iam = null; diff --git a/Spanner/src/SpannerClient.php b/Spanner/src/SpannerClient.php index 5d3e6a0c5349..b751f01179fa 100644 --- a/Spanner/src/SpannerClient.php +++ b/Spanner/src/SpannerClient.php @@ -873,6 +873,26 @@ public function pgOid(string|null $value): PgOid return new PgOid($value); } + /** + * Create a UUID object. + * + * The string value must consists of 32 hexadecimal digits in five groups separated + * by hyphens in the form 8-4-4-4-12. The hexadecimal digits represent 122 + * random bits and 6 fixed bits, in compliance with RFC 4122 section 4.4. + * + * Example: + * ``` + * $uuid = $spanner->uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'); + * ``` + * + * @param string $value The UUID value. + * @return Uuid + */ + public function uuid(string $value): Uuid + { + return new Uuid($value); + } + /** * Create an Int64 object. This can be used to work with 64 bit integers as * a string value while on a 32 bit platform. diff --git a/Spanner/src/Uuid.php b/Spanner/src/Uuid.php new file mode 100644 index 000000000000..0a7cd038198a --- /dev/null +++ b/Spanner/src/Uuid.php @@ -0,0 +1,95 @@ + 'my-project']); + * + * $uuid = $spanner->uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'); + * ``` + */ +class Uuid implements ValueInterface +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value The UUID value. + * @throws \InvalidArgumentException + */ + public function __construct(string $value) + { + // Canonical UUID format: 8-4-4-4-12 hex digits + $pattern = '/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/'; + if (!preg_match($pattern, $value)) { + throw new \InvalidArgumentException( + 'Invalid UUID format. Expected canonical hexadecimal format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + } + $this->value = strtolower($value); + } + + /** + * Get the underlying value. + * + * @return string + */ + public function get(): string + { + return $this->value; + } + + /** + * Get the type. + * + * @return int + */ + public function type(): int + { + return Database::TYPE_UUID; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function formatAsString(): string + { + return $this->value; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function __toString() + { + return $this->value; + } +} diff --git a/Spanner/src/ValueMapper.php b/Spanner/src/ValueMapper.php index 16cad27973a5..83e9ba965bce 100644 --- a/Spanner/src/ValueMapper.php +++ b/Spanner/src/ValueMapper.php @@ -48,6 +48,7 @@ class ValueMapper const TYPE_JSON = TypeCode::JSON; const TYPE_PROTO = TypeCode::PROTO; const TYPE_INTERVAL = TypeCode::INTERVAL; + const TYPE_UUID = TypeCode::UUID; const TYPE_PG_NUMERIC = 'pgNumeric'; const TYPE_PG_JSONB = 'pgJsonb'; const TYPE_PG_OID = 'pgOid'; @@ -74,6 +75,7 @@ class ValueMapper self::TYPE_FLOAT32, self::TYPE_PROTO, self::TYPE_INTERVAL, + self::TYPE_UUID, ]; /* @@ -386,6 +388,9 @@ private function decodeValue(mixed $value, array $type): mixed } $value = Interval::parse($value); break; + case self::TYPE_UUID: + $value = new Uuid($value); + break; } return $value; diff --git a/Spanner/tests/System/WriteTest.php b/Spanner/tests/System/WriteTest.php index ef0d17979fa9..5efd7b02d3c5 100644 --- a/Spanner/tests/System/WriteTest.php +++ b/Spanner/tests/System/WriteTest.php @@ -80,6 +80,8 @@ public static function setUpTestFixtures(): void stringField STRING(MAX), timestampField TIMESTAMP, numericField NUMERIC, + uuidField STRING(36), + arrayUuidField ARRAY, protoField `testing.data.User`, ) PRIMARY KEY (id)', 'CREATE TABLE ' . self::COMMIT_TIMESTAMP_TABLE_NAME . ' ( @@ -108,7 +110,8 @@ public function fieldValueProvider() [$this->randId(), 'intField', 787878787], [$this->randId(), 'stringField', 'foo bar'], [$this->randId(), 'timestampField', new Timestamp(new \DateTime())], - [$this->randId(), 'numericField', new Numeric('0.123456789')] + [$this->randId(), 'numericField', new Numeric('0.123456789')], + [$this->randId(), 'uuidField', new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479')] ]; } @@ -136,7 +139,7 @@ public function testWriteAndReadBackValue($id, $field, $value) $read = $db->read(self::TABLE_NAME, $keyset, [$field]); $row = $read->rows()->current(); - if ($value instanceof Timestamp) { + if ($value instanceof Timestamp || $value instanceof Uuid) { $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); } else { $this->assertValues($value, $row[$field]); @@ -150,7 +153,7 @@ public function testWriteAndReadBackValue($id, $field, $value) ]); $row = $exec->rows()->current(); - if ($value instanceof Timestamp) { + if ($value instanceof Timestamp || $value instanceof Uuid) { $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); } elseif ($value instanceof Message) { $this->assertInstanceOf(Proto::class, $row[$field]); @@ -305,6 +308,11 @@ public function arrayFieldValueProvider() new User(['name' => 'User 1']), new User(['name' => 'User 2']), ]], + [$this->randId(), 'arrayUuidField', [ + new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'), + new Uuid('a47ac10b-58cc-4372-a567-0e02b2c3d479') + ]], + [$this->randId(), 'arrayUuidField', null], ]; } diff --git a/Spanner/tests/Unit/UuidTest.php b/Spanner/tests/Unit/UuidTest.php new file mode 100644 index 000000000000..6a0a363f1dfc --- /dev/null +++ b/Spanner/tests/Unit/UuidTest.php @@ -0,0 +1,55 @@ +assertEquals($val, $uuid->get()); + $this->assertEquals($val, (string) $uuid); + $this->assertEquals($val, $uuid->formatAsString()); + $this->assertEquals(Database::TYPE_UUID, $uuid->type()); + } + + public function testUuidLowercase() + { + $val = 'F47AC10B-58CC-4372-A567-0E02B2C3D479'; + $uuid = new Uuid($val); + + $this->assertEquals(strtolower($val), $uuid->get()); + } + + public function testInvalidUuid() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid UUID format'); + + new Uuid('invalid-uuid'); + } +} diff --git a/Spanner/tests/Unit/ValueMapperTest.php b/Spanner/tests/Unit/ValueMapperTest.php index b2cf74c87213..fe4247c42fd1 100644 --- a/Spanner/tests/Unit/ValueMapperTest.php +++ b/Spanner/tests/Unit/ValueMapperTest.php @@ -34,6 +34,7 @@ use Google\Cloud\Spanner\V1\TypeAnnotationCode; use Google\Cloud\Spanner\V1\TypeCode; use Google\Cloud\Spanner\ValueMapper; +use Google\Cloud\Spanner\Uuid; use PHPUnit\Framework\TestCase; use Testing\Data\Book; use Testing\Data\User; @@ -73,7 +74,8 @@ public function simpleTypes() [1, Database::TYPE_INT64], ['john', Database::TYPE_STRING], [3.1415, Database::TYPE_FLOAT64], - [false, Database::TYPE_BOOL] + [false, Database::TYPE_BOOL], + [new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'), Database::TYPE_UUID] ]; } @@ -858,7 +860,8 @@ public function simpleTypeValues() [new Bytes('hello world'), base64_encode('hello world')], [new Proto('hello world', 'foo'), 'hello world'], [['foo', 'bar']], - ['{\"rating\":9,\"open\":true}'] + ['{\"rating\":9,\"open\":true}'], + [new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'), 'f47ac10b-58cc-4372-a567-0e02b2c3d479'] ]; } @@ -1379,4 +1382,53 @@ public function provideFloatTypes() [Database::TYPE_FLOAT32] ]; } + + public function testDecodeValuesUuid() + { + $uuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'; + $res = $this->mapper->decodeValues( + $this->createField(Database::TYPE_UUID), + $this->createRow($uuid), + Result::RETURN_ASSOCIATIVE + ); + $this->assertInstanceOf(Uuid::class, $res['rowName']); + $this->assertEquals($uuid, $res['rowName']->get()); + } + + public function testFormatParamsForExecuteSqlArrayUuid() + { + $uuid = new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'); + $params = [ + 'array' => [$uuid] + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($uuid->get(), $res['params']['array'][0]); + $this->assertEquals(Database::TYPE_ARRAY, $res['paramTypes']['array']['code']); + $this->assertEquals(Database::TYPE_UUID, $res['paramTypes']['array']['arrayElementType']['code']); + } + + public function testFormatParamsForExecuteSqlStructUuid() + { + $uuid = new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'); + $params = [ + 'struct' => [ + 'uuid' => $uuid + ] + ]; + $types = [ + 'struct' => (new StructType()) + ->add('uuid', Database::TYPE_UUID) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params, $types); + + $this->assertEquals($uuid->get(), $res['params']['struct'][0]); + $this->assertEquals(Database::TYPE_STRUCT, $res['paramTypes']['struct']['code']); + $this->assertEquals( + Database::TYPE_UUID, + $res['paramTypes']['struct']['structType']['fields'][0]['type']['code'] + ); + } }