Spanner の PROTO BUNDLE に登録した内容を自前でヒューマンリーダブルに出力する
この記事は Spanner Advent Calendar 4 日目 & apstndb Advent Calendar 6 日目です。
2024年5月に追加された Spanner の Protocol Buffers サポート は使っていますか?
私は無職の技術愛好家なので現在 Spanner を業務では使っていませんが、
Spanner のドキュメントを一通り読んで挙動も試してそれなりに理解したので何度かに分けて紹介したいと思います。
この記事では、現在 Spanner に登録されている Protocol Buffers の型についての情報全てを読める形で確認する手段について説明します。
下準備
この記事では 公式ドキュメントのサンプルを前提とします。
syntax = "proto2";
package examples.shipping;
message Order {
optional string order_number = 1;
optional int64 date = 2;
message Address {
optional string street = 1;
optional string city = 2;
optional string state = 3;
optional string country = 4;
}
optional Address shipping_address = 3;
message Item {
optional string product_name = 1;
optional int32 quantity = 2;
}
repeated Item line_item = 4;
}
message OrderHistory {
optional string order_number = 1;
optional int64 date = 2;
}
この proto ファイルを Spanner で使うためには下記のように protoc
でコンパイルしてから使います。
$ protoc --include_imports --descriptor_set_out=order_descriptors.pb order_protos.proto
Protocol Buffers の方の登録は DDL であり、 gcloud spanner databases ddl update
コマンドで一通りのオペレーションは可能です。
しかし、試行錯誤がしづらいので対話型ツールの spanner-cli かフォークの spanner-mycli を使うのがおすすめです。(環境変数で指定できるので -p
, -i
, -d
は省略)
$ spanner-cli --proto-descriptor-file order_descriptors.pb
# or
$ spanner-mycli --proto-descriptor-file order_descriptors.pb
この記事ではこの中の2つの Protocol Buffers メッセージ型を使って CREATE PROTO BUNDLE
します。
spanner> CREATE PROTO BUNDLE (`examples.shipping.Order`,
`examples.shipping.Order.Address`);
ここまですると Spanner 上のさまざまな場所で登録した型を使うことができます。
例えば次のようなものです。
PROTO BUNDLE
の中身の取得
さて、今どのような Protocol Buffer 型が登録されていて使用可能かが確認したくなりませんか?
確認方法はあります。 Spanner は一通りの機能が REST と gRPC の2つの API からアクセスできるので、機能的に等価な下記の2つの API が提供されています。
-
projects.instances.databases.getDdl
REST API -
google.spanner.admin.database.v1.DatabaseAdmin.GetDatabaseDdl
gRPC API
この記事では REST API を使って説明します。
{
"statements": [
string
],
"protoDescriptors": string
}
protoDescriptors
フィールドは次のように定義されています。
string (bytes format)
Proto descriptors stored in the database. Contains a protobuf-serialized google.protobuf.FileDescriptorSet. For more details, see protobuffer self description.
A base64-encoded string.
プログラムを書かずに Protocol Buffers でシリアライズされたバイナリメッセージをテキストフォーマットに変換するため、 protoc
コマンドの --decode
フラグを使用します。
$ protoc --help
Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
...
--decode=MESSAGE_TYPE Read a binary message of the given type from
standard input and write it in text format
to standard output. The message type must
be defined in PROTO_FILES or their imports.
...
.protoDescriptor
フィールドに入った base64 された google.protobuf.FileDescriptorSet
をデコードすれば良いので、下記のように proto ファイルを用意して prototext 形式で出力することができます。
$ curl https://raw.githubusercontent.com/protocolbuffers/protobuf/refs/heads/main/src/google/protobuf/descriptor.proto -o descriptor.proto
$ jqurl -s --auth=google --raw-output \
"https://spanner.googleapis.com/v1/projects/${SPANNER_PROJECT}/instances/${SPANNER_INSTANCE}/databases/${SPANNER_DATABASE}/ddl" .protoDescriptors |
base64 -d > proto_bundle.pb
$ protoc --decode=google.protobuf.FileDescriptorSet descriptor.proto < proto_bundle.pb
この結果は次のような prototext
形式の出力となります。
file {
name: "package:examples.shipping"
package: "examples.shipping"
message_type {
name: "Order"
field {
name: "order_number"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "orderNumber"
}
field {
name: "date"
number: 2
label: LABEL_OPTIONAL
type: TYPE_INT64
json_name: "date"
}
field {
name: "shipping_address"
number: 3
label: LABEL_OPTIONAL
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Address"
json_name: "shippingAddress"
}
field {
name: "line_item"
number: 4
label: LABEL_REPEATED
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Item"
json_name: "lineItem"
}
nested_type {
name: "Address"
field {
name: "street"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "street"
}
field {
name: "city"
number: 2
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "city"
}
field {
name: "state"
number: 3
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "state"
}
field {
name: "country"
number: 4
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "country"
}
}
nested_type {
name: "Item"
options {
14004 {
1: 1
}
}
}
}
}
確かに登録した examples.shipping.Order
, examples.shipping.Order.Address
の2つの message type が入っていることが分かりますね。
14004
謎の ところで、先ほどの出力の中にこのような行がありました。
...
nested_type {
name: "Item"
options {
14004 {
1: 1
}
}
}
...
これは何でしょうか?
examples.shipping.Order.Item
は登録していないのでこのような行が出力されることが想定されます。
試しに examples.shipping.Order.Address
も削除してみてからまた取得した protoDescriptors
フィールドを出力すると、 Address
も同じ出力になります。
spanner> ALTER PROTO BUNDLE DELETE (`examples.shipping.Order.Address`);
file {
name: "package:examples.shipping"
package: "examples.shipping"
message_type {
name: "Order"
field {
name: "order_number"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "orderNumber"
}
field {
name: "date"
number: 2
label: LABEL_OPTIONAL
type: TYPE_INT64
json_name: "date"
}
field {
name: "shipping_address"
number: 3
label: LABEL_OPTIONAL
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Address"
json_name: "shippingAddress"
}
field {
name: "line_item"
number: 4
label: LABEL_REPEATED
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Item"
json_name: "lineItem"
}
nested_type {
name: "Address"
options {
14004 {
1: 1
}
}
}
nested_type {
name: "Item"
options {
14004 {
1: 1
}
}
}
}
}
どうやらネストされた型で未知の型、削除された型にこの options {14004 {1: 1}}
が指定されるようです。これは何なのでしょうか?
答えは 14004
を含む Protocol Buffer ファイルを検索する ことで見つけることができます。
zetasql.PlaceholderDescriptorProto
が答えです。
コメントによると、 Protocol Buffers にも未知の型をプレースホルダーする機能はあるものの、 Enum か Message かを判別するなどがうまくいかないため ZetaSQL ではこの zetasql.PlaceholderDescriptorProto
を使っているとのころです。
この option を表現するメッセージを protoc
は知らないためデコードすることができず、未解釈のまま出力されてしまっているわけです。
このファイルを使ってデコードしてみましょう。
$ curl -s https://raw.githubusercontent.com/google/zetasql/refs/heads/master/zetasql/proto/placeholder_descriptor.proto -o placeholder_descriptor.proto
$ jqurl -s --auth=google --raw-output \
"https://spanner.googleapis.com/v1/projects/${SPANNER_PROJECT}/instances/${SPANNER_INSTANCE}/databases/${SPANNER_DATABASE}/ddl" .protoDescriptors |
base64 -d | protoc --decode=google.protobuf.FileDescriptorSet placeholder_descriptor.proto
結果はこの通り。
file {
name: "package:examples.shipping"
package: "examples.shipping"
message_type {
name: "Order"
field {
name: "order_number"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "orderNumber"
}
field {
name: "date"
number: 2
label: LABEL_OPTIONAL
type: TYPE_INT64
json_name: "date"
}
field {
name: "shipping_address"
number: 3
label: LABEL_OPTIONAL
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Address"
json_name: "shippingAddress"
}
field {
name: "line_item"
number: 4
label: LABEL_REPEATED
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Item"
json_name: "lineItem"
}
nested_type {
name: "Address"
options {
[zetasql.PlaceholderDescriptorProto.placeholder_descriptor] {
is_placeholder: true
}
}
}
nested_type {
name: "Item"
options {
[zetasql.PlaceholderDescriptorProto.placeholder_descriptor] {
is_placeholder: true
}
}
}
}
}
先ほど出力できなかったものが prototext の Extension Fields として完全に出力されています。
これで完璧に PROTO BUNDLE
の中身をフォーマットすることができました。
ここまであえて触れていませんでしたが gcloud spanner databases ddl describe
コマンドでも PROTO BUNDLE
に登録した proto descriptors を prototext で出力することが可能です。
しかし、このコマンドは zetasql.PlaceholderDescriptorProto
を知りません。
$ gcloud spanner databases ddl describe ${SPANNER_DATABASE} --include-proto-descriptors
...
Proto Bundle Descriptors:
file {
name: "package:examples.shipping"
package: "examples.shipping"
message_type {
name: "Order"
field {
name: "order_number"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
json_name: "orderNumber"
}
field {
name: "date"
number: 2
label: LABEL_OPTIONAL
type: TYPE_INT64
json_name: "date"
}
field {
name: "shipping_address"
number: 3
label: LABEL_OPTIONAL
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Address"
json_name: "shippingAddress"
}
field {
name: "line_item"
number: 4
label: LABEL_REPEATED
type: TYPE_MESSAGE
type_name: ".examples.shipping.Order.Item"
json_name: "lineItem"
}
nested_type {
name: "Address"
options {
}
}
nested_type {
name: "Item"
options {
}
}
}
}
options
の中身を完全に無視すらしてしまっています。これでは本当の protoDescriptors
の中身を知ることができません。この記事で書いた方法の方が優れていますね。
まとめ
-
PROTO BUNDLE
には Protocol Buffers の型を登録することができる。 -
PROTO BUNDLE
で登録した内容は API から取得可能であり、google.protobuf.FileDescriptorSet
としてデコードできる。 - 未知の型、削除された型は
zetasql.PlaceholderDescriptorProto
オプションが指定された型名のみが残る。
実は他にも PROTO BUNDLE
の中身を取得する手段は存在します。後日これを紹介したいと思います。
Spanner Advent Calendar 5日目の記事として l-jiang-zdf (キョウ ライ) さんによる「BigQuery 連携クエリ・外部データセットが使う Spanner トランザクションについて」がすでに公開されています。Spanner Advent Calendar を引き続きお楽しみください。
おまけ: 何故ここまで調べたのか?
ここまで調べ切った理由は spanner-mycli に Spanner に登録している Protocol Bufffers の型を表示する機能を追加するためでした。
spanner> SHOW REMOTE PROTO;
+------------------------------+-------------------+
| full_name | package |
+------------------------------+-------------------+
| examples.shipping.Order | examples.shipping |
| examples.shipping.Order.Item | examples.shipping |
+------------------------------+-------------------+
未登録の型や削除済の型の区別がつかないのであればこの機能はどの型が登録されているのかを知るためには使えません。
私自身の経験から、この記事の内容は Spanner のツール作者には実用的だと言えます。
Discussion