Skip to content

Commit c329521

Browse files
committed
feat: Support UUID data type
1 parent 82de3f7 commit c329521

File tree

7 files changed

+243
-3
lines changed

7 files changed

+243
-3
lines changed

Spanner/src/Database.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class Database
115115
public const TYPE_JSON = TypeCode::JSON;
116116
public const TYPE_PG_OID = 'pgOid';
117117
public const TYPE_INTERVAL = TypeCode::INTERVAL;
118+
public const TYPE_UUID = TypeCode::UUID;
118119

119120
private Operation $operation;
120121
private IamManager|null $iam = null;

Spanner/src/SpannerClient.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,26 @@ public function pgOid(string|null $value): PgOid
873873
return new PgOid($value);
874874
}
875875

876+
/**
877+
* Create a UUID object.
878+
*
879+
* The string value must consists of 32 hexadecimal digits in five groups separated
880+
* by hyphens in the form 8-4-4-4-12. The hexadecimal digits represent 122
881+
* random bits and 6 fixed bits, in compliance with RFC 4122 section 4.4.
882+
*
883+
* Example:
884+
* ```
885+
* $uuid = $spanner->uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479');
886+
* ```
887+
*
888+
* @param string $value The UUID value.
889+
* @return Uuid
890+
*/
891+
public function uuid(string $value): Uuid
892+
{
893+
return new Uuid($value);
894+
}
895+
876896
/**
877897
* Create an Int64 object. This can be used to work with 64 bit integers as
878898
* a string value while on a 32 bit platform.

Spanner/src/Uuid.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\Cloud\Spanner;
19+
20+
/**
21+
* Represents a value with a data type of
22+
* [UUID](https://cloud.google.com/spanner/docs/reference/standard-sql/data-types#uuid_type).
23+
*
24+
* Example:
25+
* ```
26+
* use Google\Cloud\Spanner\SpannerClient;
27+
*
28+
* $spanner = new SpannerClient(['projectId' => 'my-project']);
29+
*
30+
* $uuid = $spanner->uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479');
31+
* ```
32+
*/
33+
class Uuid implements ValueInterface
34+
{
35+
/**
36+
* @var string
37+
*/
38+
private $value;
39+
40+
/**
41+
* @param string $value The UUID value.
42+
* @throws \InvalidArgumentException
43+
*/
44+
public function __construct(string $value)
45+
{
46+
// Canonical UUID format: 8-4-4-4-12 hex digits
47+
$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}$/';
48+
if (!preg_match($pattern, $value)) {
49+
throw new \InvalidArgumentException(
50+
'Invalid UUID format. Expected canonical format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
51+
);
52+
}
53+
$this->value = strtolower($value);
54+
}
55+
56+
/**
57+
* Get the underlying value.
58+
*
59+
* @return string
60+
*/
61+
public function get(): string
62+
{
63+
return $this->value;
64+
}
65+
66+
/**
67+
* Get the type.
68+
*
69+
* @return int
70+
*/
71+
public function type(): int
72+
{
73+
return Database::TYPE_UUID;
74+
}
75+
76+
/**
77+
* Format the value as a string.
78+
*
79+
* @return string
80+
*/
81+
public function formatAsString(): string
82+
{
83+
return $this->value;
84+
}
85+
86+
/**
87+
* Format the value as a string.
88+
*
89+
* @return string
90+
*/
91+
public function __toString()
92+
{
93+
return $this->value;
94+
}
95+
}

Spanner/src/ValueMapper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class ValueMapper
4848
const TYPE_JSON = TypeCode::JSON;
4949
const TYPE_PROTO = TypeCode::PROTO;
5050
const TYPE_INTERVAL = TypeCode::INTERVAL;
51+
const TYPE_UUID = TypeCode::UUID;
5152
const TYPE_PG_NUMERIC = 'pgNumeric';
5253
const TYPE_PG_JSONB = 'pgJsonb';
5354
const TYPE_PG_OID = 'pgOid';
@@ -74,6 +75,7 @@ class ValueMapper
7475
self::TYPE_FLOAT32,
7576
self::TYPE_PROTO,
7677
self::TYPE_INTERVAL,
78+
self::TYPE_UUID,
7779
];
7880

7981
/*
@@ -386,6 +388,9 @@ private function decodeValue(mixed $value, array $type): mixed
386388
}
387389
$value = Interval::parse($value);
388390
break;
391+
case self::TYPE_UUID:
392+
$value = new Uuid($value);
393+
break;
389394
}
390395

391396
return $value;

Spanner/tests/System/WriteTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public static function setUpTestFixtures(): void
8080
stringField STRING(MAX),
8181
timestampField TIMESTAMP,
8282
numericField NUMERIC,
83+
uuidField STRING(36),
84+
arrayUuidField ARRAY<STRING(36)>,
8385
protoField `testing.data.User`,
8486
) PRIMARY KEY (id)',
8587
'CREATE TABLE ' . self::COMMIT_TIMESTAMP_TABLE_NAME . ' (
@@ -108,7 +110,8 @@ public function fieldValueProvider()
108110
[$this->randId(), 'intField', 787878787],
109111
[$this->randId(), 'stringField', 'foo bar'],
110112
[$this->randId(), 'timestampField', new Timestamp(new \DateTime())],
111-
[$this->randId(), 'numericField', new Numeric('0.123456789')]
113+
[$this->randId(), 'numericField', new Numeric('0.123456789')],
114+
[$this->randId(), 'uuidField', new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479')]
112115
];
113116
}
114117

@@ -138,6 +141,8 @@ public function testWriteAndReadBackValue($id, $field, $value)
138141

139142
if ($value instanceof Timestamp) {
140143
$this->assertEquals($value->formatAsString(), $row[$field]->formatAsString());
144+
} elseif ($value instanceof Uuid) {
145+
$this->assertEquals($value->formatAsString(), $row[$field]->formatAsString());
141146
} else {
142147
$this->assertValues($value, $row[$field]);
143148
}
@@ -156,6 +161,8 @@ public function testWriteAndReadBackValue($id, $field, $value)
156161
$this->assertInstanceOf(Proto::class, $row[$field]);
157162
$this->assertEquals(base64_encode($value->serializeToString()), $row[$field]->getValue());
158163
$this->assertEquals($value, $row[$field]->get());
164+
} elseif ($value instanceof Uuid) {
165+
$this->assertEquals($value->formatAsString(), $row[$field]->formatAsString());
159166
} else {
160167
$this->assertValues($value, $row[$field]);
161168
}
@@ -305,6 +312,11 @@ public function arrayFieldValueProvider()
305312
new User(['name' => 'User 1']),
306313
new User(['name' => 'User 2']),
307314
]],
315+
[$this->randId(), 'arrayUuidField', [
316+
new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'),
317+
new Uuid('a47ac10b-58cc-4372-a567-0e02b2c3d479')
318+
]],
319+
[$this->randId(), 'arrayUuidField', null],
308320
];
309321
}
310322

Spanner/tests/Unit/UuidTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace Google\Cloud\Spanner\Tests\Unit;
19+
20+
use Google\Cloud\Spanner\Database;
21+
use Google\Cloud\Spanner\Uuid;
22+
use PHPUnit\Framework\TestCase;
23+
24+
/**
25+
* @group spanner
26+
*/
27+
class UuidTest extends TestCase
28+
{
29+
public function testUuid()
30+
{
31+
$val = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
32+
$uuid = new Uuid($val);
33+
34+
$this->assertEquals($val, $uuid->get());
35+
$this->assertEquals($val, (string) $uuid);
36+
$this->assertEquals($val, $uuid->formatAsString());
37+
$this->assertEquals(Database::TYPE_UUID, $uuid->type());
38+
}
39+
40+
public function testUuidLowercase()
41+
{
42+
$val = 'F47AC10B-58CC-4372-A567-0E02B2C3D479';
43+
$uuid = new Uuid($val);
44+
45+
$this->assertEquals(strtolower($val), $uuid->get());
46+
}
47+
48+
public function testInvalidUuid()
49+
{
50+
$this->expectException(\InvalidArgumentException::class);
51+
$this->expectExceptionMessage('Invalid UUID format');
52+
53+
new Uuid('invalid-uuid');
54+
}
55+
}

Spanner/tests/Unit/ValueMapperTest.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Google\Cloud\Spanner\V1\TypeAnnotationCode;
3535
use Google\Cloud\Spanner\V1\TypeCode;
3636
use Google\Cloud\Spanner\ValueMapper;
37+
use Google\Cloud\Spanner\Uuid;
3738
use PHPUnit\Framework\TestCase;
3839
use Testing\Data\Book;
3940
use Testing\Data\User;
@@ -73,7 +74,8 @@ public function simpleTypes()
7374
[1, Database::TYPE_INT64],
7475
['john', Database::TYPE_STRING],
7576
[3.1415, Database::TYPE_FLOAT64],
76-
[false, Database::TYPE_BOOL]
77+
[false, Database::TYPE_BOOL],
78+
[new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'), Database::TYPE_UUID]
7779
];
7880
}
7981

@@ -858,7 +860,8 @@ public function simpleTypeValues()
858860
[new Bytes('hello world'), base64_encode('hello world')],
859861
[new Proto('hello world', 'foo'), 'hello world'],
860862
[['foo', 'bar']],
861-
['{\"rating\":9,\"open\":true}']
863+
['{\"rating\":9,\"open\":true}'],
864+
[new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479'), 'f47ac10b-58cc-4372-a567-0e02b2c3d479']
862865
];
863866
}
864867

@@ -1379,4 +1382,53 @@ public function provideFloatTypes()
13791382
[Database::TYPE_FLOAT32]
13801383
];
13811384
}
1385+
1386+
public function testDecodeValuesUuid()
1387+
{
1388+
$uuid = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
1389+
$res = $this->mapper->decodeValues(
1390+
$this->createField(Database::TYPE_UUID),
1391+
$this->createRow($uuid),
1392+
Result::RETURN_ASSOCIATIVE
1393+
);
1394+
$this->assertInstanceOf(Uuid::class, $res['rowName']);
1395+
$this->assertEquals($uuid, $res['rowName']->get());
1396+
}
1397+
1398+
public function testFormatParamsForExecuteSqlArrayUuid()
1399+
{
1400+
$uuid = new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479');
1401+
$params = [
1402+
'array' => [$uuid]
1403+
];
1404+
1405+
$res = $this->mapper->formatParamsForExecuteSql($params);
1406+
1407+
$this->assertEquals($uuid->get(), $res['params']['array'][0]);
1408+
$this->assertEquals(Database::TYPE_ARRAY, $res['paramTypes']['array']['code']);
1409+
$this->assertEquals(Database::TYPE_UUID, $res['paramTypes']['array']['arrayElementType']['code']);
1410+
}
1411+
1412+
public function testFormatParamsForExecuteSqlStructUuid()
1413+
{
1414+
$uuid = new Uuid('f47ac10b-58cc-4372-a567-0e02b2c3d479');
1415+
$params = [
1416+
'struct' => [
1417+
'uuid' => $uuid
1418+
]
1419+
];
1420+
$types = [
1421+
'struct' => (new StructType())
1422+
->add('uuid', Database::TYPE_UUID)
1423+
];
1424+
1425+
$res = $this->mapper->formatParamsForExecuteSql($params, $types);
1426+
1427+
$this->assertEquals($uuid->get(), $res['params']['struct'][0]);
1428+
$this->assertEquals(Database::TYPE_STRUCT, $res['paramTypes']['struct']['code']);
1429+
$this->assertEquals(
1430+
Database::TYPE_UUID,
1431+
$res['paramTypes']['struct']['structType']['fields'][0]['type']['code']
1432+
);
1433+
}
13821434
}

0 commit comments

Comments
 (0)