🍣

Spanner の PROTO BUNDLE に登録した内容を自前でヒューマンリーダブルに出力する

2024/12/07に公開

この記事は Spanner Advent Calendar 4 日目 & apstndb Advent Calendar 6 日目です。

2024年5月に追加された Spanner の Protocol Buffers サポート は使っていますか?

私は無職の技術愛好家なので現在 Spanner を業務では使っていませんが、
Spanner のドキュメントを一通り読んで挙動も試してそれなりに理解したので何度かに分けて紹介したいと思います。

この記事では、現在 Spanner に登録されている Protocol Buffers の型についての情報全てを読める形で確認する手段について説明します。

下準備

この記事では 公式ドキュメントのサンプルを前提とします。

order_protos.proto
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 が提供されています。

この記事では 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 が答えです。

https://github.com/google/zetasql/blob/2024.11.1/zetasql/proto/placeholder_descriptor.proto#L26-L42

コメントによると、 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