Skip to content

Commit e9c1f9d

Browse files
authored
feat(Spanner): Proto Columns (#8108)
* feat(Spanner): Proto Columns * cs ignore generated files * adds phpstan template * fix new class copyright * add protoDescriptors phpdoc * add test for Database Proto Columns * add descriptor data * move things around, add Write test against emulator * ignore generated classes in cs
1 parent 40f4cf6 commit e9c1f9d

19 files changed

Lines changed: 951 additions & 12 deletions

File tree

Spanner/composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
},
3939
"autoload-dev": {
4040
"psr-4": {
41-
"Google\\Cloud\\Spanner\\Tests\\": "tests"
41+
"Google\\Cloud\\Spanner\\Tests\\": "tests",
42+
"Testing\\Data\\": "tests/data/generated/Testing/Data",
43+
"GPBMetadata\\Data\\": "tests/data/generated/GPBMetadata/Data"
4244
}
4345
}
4446
}

Spanner/src/ArrayType.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ class ArrayType
7878
* `Database::TYPE_BOOL`, `Database::TYPE_INT64`,
7979
* `Database::TYPE_FLOAT64`, `Database::TYPE_TIMESTAMP`,
8080
* `Database::TYPE_DATE`, `Database::TYPE_STRING`,
81-
* `Database::TYPE_NUMERIC`, `Database::TYPE_PG_NUMERIC` and
82-
* `Database::TYPE_BYTES`. Nested arrays are not supported in Cloud
83-
* Spanner, and attempts to use `Database::TYPE_ARRAY` will result in
84-
* an exception. If null is given,
81+
* `Database::TYPE_NUMERIC`, `Database::TYPE_PG_NUMERIC`
82+
* `Database::TYPE_BYTES`, and `Database::TYPE_PROTO`. Nested arrays
83+
* are not supported in Cloud Spanner, and attempts to use
84+
* `Database::TYPE_ARRAY` will result in an exception. If null is given,
8585
* Google Cloud PHP will attempt to infer the array type.
8686
* @throws \InvalidArgumentException If an invalid type is provided, or if
8787
* a struct is defined but the given type is not

Spanner/src/Database.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class Database
119119
const TYPE_ARRAY = TypeCode::PBARRAY;
120120
const TYPE_STRUCT = TypeCode::STRUCT;
121121
const TYPE_NUMERIC = TypeCode::NUMERIC;
122+
const TYPE_PROTO = TypeCode::PROTO;
122123
const TYPE_PG_NUMERIC = 'pgNumeric';
123124
const TYPE_PG_JSONB = 'pgJsonb';
124125
const TYPE_JSON = TypeCode::JSON;
@@ -427,6 +428,10 @@ public function exists(array $options = [])
427428
* Configuration Options
428429
*
429430
* @type string[] $statements Additional DDL statements.
431+
* @type \Google\Protobuf\FileDescriptorSet|string $protoDescriptors The file
432+
* descriptor set object to be used in the update, or alternatively, an absolute
433+
* path to the generated file descriptor set. The descriptor set is only used
434+
* during DDL statements, such as `CREATE PROTO BUNDLE`.
430435
* }
431436
* @return LongRunningOperation<Database>
432437
*/

Spanner/src/Instance.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,10 @@ public function delete(array $options = [])
445445
* Configuration Options
446446
*
447447
* @type array $statements Additional DDL statements.
448+
* @type \Google\Protobuf\FileDescriptorSet|string $protoDescriptors The file
449+
* descriptor set object to be used in the update, or alternatively, an absolute
450+
* path to the generated file descriptor set. The descriptor set is only used
451+
* during DDL statements, such as `CREATE PROTO BUNDLE`.
448452
* @type SessionPoolInterface $sessionPool A pool used to manage
449453
* sessions.
450454
* }

Spanner/src/Proto.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Google Inc. All Rights Reserved.
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+
use Google\Protobuf\Internal\DescriptorPool;
21+
use Google\Protobuf\Internal\Message;
22+
use RuntimeException;
23+
24+
/**
25+
* Represents a value with a data type of
26+
* [proto](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TypeCode).
27+
*
28+
* @phpstan-template T of Message
29+
*/
30+
class Proto implements ValueInterface
31+
{
32+
/**
33+
* @param string $value The proto data, base64-encoded.
34+
* @param string $protoTypeFqn The fully qualified name of the proto type.
35+
*/
36+
public function __construct(
37+
private string $value,
38+
private string $protoTypeFqn
39+
) {
40+
}
41+
42+
/**
43+
* Get the proto column as a protobuf Message.
44+
*
45+
* Example:
46+
* ```
47+
* $message = $proto->get();
48+
* var_dump($message->serializeToJsonString());
49+
* ```
50+
*
51+
* @return T
52+
* @throws RuntimeException If the proto type is not found.
53+
*/
54+
public function get(): Message
55+
{
56+
/** @var \Google\Protobuf\Internal\DescriptorPool $pool */
57+
$pool = DescriptorPool::getGeneratedPool();
58+
/** @var \Google\Protobuf\Internal\Descriptor|null $descriptor */
59+
$descriptor = $pool->getDescriptorByProtoName($this->protoTypeFqn);
60+
if (!$descriptor) {
61+
throw new RuntimeException(sprintf(
62+
'Unable to decode proto value. Descriptor not found for %s.',
63+
$this->protoTypeFqn
64+
));
65+
}
66+
/** @var Message $message */
67+
$message = new ($descriptor->getClass())();
68+
$message->mergeFromString(base64_decode($this->value));
69+
return $message;
70+
}
71+
72+
public function getValue(): string
73+
{
74+
return $this->value;
75+
}
76+
77+
public function getProtoTypeFqn(): string
78+
{
79+
return $this->protoTypeFqn;
80+
}
81+
82+
/**
83+
* Get the type.
84+
*
85+
* Example:
86+
* ```
87+
* echo $proto->type();
88+
* ```
89+
*
90+
* @return int
91+
*/
92+
public function type(): int
93+
{
94+
return Database::TYPE_PROTO;
95+
}
96+
97+
/**
98+
* Format the value as a string.
99+
*
100+
* Example:
101+
* ```
102+
* echo $proto->formatAsString();
103+
* ```
104+
*
105+
* @return string
106+
*/
107+
public function formatAsString(): string
108+
{
109+
return $this->value;
110+
}
111+
112+
/**
113+
* Format the value as a string.
114+
*
115+
* @return string
116+
* @access private
117+
*/
118+
public function __toString()
119+
{
120+
return $this->formatAsString();
121+
}
122+
}

Spanner/src/ValueMapper.php

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Google\Cloud\Core\TimeTrait;
2323
use Google\Cloud\Spanner\V1\TypeAnnotationCode;
2424
use Google\Cloud\Spanner\V1\TypeCode;
25+
use Google\Protobuf\Internal\DescriptorPool;
26+
use Google\Protobuf\Internal\Message;
2527

2628
/**
2729
* Manage value mappings between Google Cloud PHP and Cloud Spanner
@@ -43,6 +45,7 @@ class ValueMapper
4345
const TYPE_STRUCT = TypeCode::STRUCT;
4446
const TYPE_NUMERIC = TypeCode::NUMERIC;
4547
const TYPE_JSON = TypeCode::JSON;
48+
const TYPE_PROTO = TypeCode::PROTO;
4649
const TYPE_PG_NUMERIC = 'pgNumeric';
4750
const TYPE_PG_JSONB = 'pgJsonb';
4851
const TYPE_PG_OID = 'pgOid';
@@ -67,6 +70,7 @@ class ValueMapper
6770
self::TYPE_PG_JSONB,
6871
self::TYPE_PG_OID,
6972
self::TYPE_FLOAT32,
73+
self::TYPE_PROTO,
7074
];
7175

7276
/*
@@ -366,6 +370,9 @@ private function decodeValue($value, array $type)
366370
}
367371
}
368372

373+
break;
374+
case self::TYPE_PROTO:
375+
$value = new Proto($value, $type['protoTypeFqn']);
369376
break;
370377
}
371378

@@ -652,6 +659,7 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType =
652659

653660
// counts the diff data types used inside the array
654661
$uniqueTypes = [];
662+
$protoTypeFqn = null;
655663
$res = null;
656664
if ($value !== null) {
657665
$res = [];
@@ -682,6 +690,10 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType =
682690
$inferredType['typeAnnotation'] = $type[1]['typeAnnotation'];
683691
}
684692

693+
if (isset($type[1]['protoTypeFqn'])) {
694+
$protoTypeFqn = $type[1]['protoTypeFqn'];
695+
}
696+
685697
$inferredTypes[] = $inferredType;
686698
}
687699
}
@@ -722,6 +734,9 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType =
722734
$typeObject = $nestedDef[1];
723735
} else {
724736
$typeObject = $this->typeObject($typeCode, $typeAnnotationCode);
737+
if ($protoTypeFqn) {
738+
$typeObject['protoTypeFqn'] = $protoTypeFqn;
739+
}
725740
}
726741

727742
$type = $this->typeObject(
@@ -753,10 +768,13 @@ private function objectParam($value)
753768
? $value->typeAnnotation()
754769
: null;
755770

756-
return [
757-
$this->typeObject($value->type(), $typeAnnotation),
758-
$value->formatAsString()
759-
];
771+
$typeObject = $this->typeObject($value->type(), $typeAnnotation);
772+
773+
if ($value instanceof Proto) {
774+
$typeObject['protoTypeFqn'] = $value->getProtoTypeFqn();
775+
}
776+
777+
return [$typeObject, $value->formatAsString()];
760778
}
761779

762780
if ($value instanceof Int64) {
@@ -766,6 +784,20 @@ private function objectParam($value)
766784
];
767785
}
768786

787+
if ($value instanceof Message) {
788+
$fullName = DescriptorPool::getGeneratedPool()
789+
->getDescriptorByClassName(get_class($value))
790+
->getFullName();
791+
$typeObject = [
792+
'code' => self::TYPE_PROTO,
793+
'protoTypeFqn' => $fullName,
794+
];
795+
return [
796+
$typeObject,
797+
base64_encode($value->serializetoString())
798+
];
799+
}
800+
769801
throw new \InvalidArgumentException(sprintf(
770802
'Unrecognized value type %s. ' .
771803
'Please ensure you are using the latest version of google/cloud or google/cloud-spanner.',

Spanner/tests/System/AdminTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,45 @@ public function testDatabaseDropProtection()
175175
$this->assertFalse($db->exists());
176176
}
177177

178+
public function testDatabaseProtoColumns()
179+
{
180+
$instance = self::$instance;
181+
182+
$dbName = uniqid(self::TESTING_PREFIX);
183+
$op = $instance->createDatabase($dbName, [
184+
'statements' => [
185+
'CREATE PROTO BUNDLE (' .
186+
'testing.data.User,' .
187+
'testing.data.User.Address,' .
188+
'testing.data.Book' .
189+
')',
190+
'CREATE TABLE Users (' .
191+
'Id INT64,' .
192+
'User `testing.data.User`,' .
193+
'Books ARRAY<`testing.data.Book`>,' .
194+
') PRIMARY KEY (Id)'
195+
],
196+
'protoDescriptors' => file_get_contents(__DIR__ . '/../data/proto/user.pb'),
197+
]);
198+
199+
$this->assertInstanceOf(LongRunningOperation::class, $op);
200+
$db = $op->pollUntilComplete();
201+
$this->assertInstanceOf(Database::class, $db);
202+
203+
self::$deletionQueue->add(function () use ($db) {
204+
$db->drop();
205+
});
206+
207+
$databases = $instance->databases();
208+
$database = array_filter(iterator_to_array($databases), function ($db) use ($dbName) {
209+
return $this->parseDbName($db->name()) === $dbName;
210+
});
211+
212+
$this->assertInstanceOf(Database::class, current($database));
213+
$this->assertTrue($db->exists());
214+
$this->assertStringStartsWith('CREATE PROTO BUNDLE ', $db->ddl()[0]);
215+
}
216+
178217
public function testCreateCustomerManagedInstanceConfiguration()
179218
{
180219
$this->skipEmulatorTests();

0 commit comments

Comments
 (0)