PostgreSQLクエリプロトコル概観

コロナのせいにして引きこもってPostgreSQLと遊んでいたので、ブログにまとめてみる。

年末のPostgreSQLアドベントカレンダーで、PostgreSQLのプロトコルについてちょっと書いた。同僚や社外の人間など、話題に出して貰う機会があったが、「そもそも、簡易プロトコルと拡張プロトコルの2種類があることを知らなかった」という話を複数回聞いた。たしかに、普通にアプリケーションを開発する場合、プロトコルはクライアントインターフェイスに隠蔽されていて、意識することはない。せっかくなので、そのあたりもう少し雰囲気がわかるように書いてみようかなという気持ちになった。

背景

主題であるPostgreSQLのプロトコルを覗くために、前提となる知識を紹介する。PostgreSQLはフロントエンド(=クライアント)とバックエンド(=サーバ)の通信に、メッセージという単位をやりとりする。一部の例外を除いて、ほとんどはフロントエンドがバックエンドにメッセージを送信し、それを受信したバックエンドがフロントエンドにメッセージを返す。メッセージの先頭バイトはメッセージの種類を識別する値、次の4バイトはメッセージの残りの長さを指定し、残りはペイロード(メッセージの種類によって異なる)となる。

f:id:kyabatalian:20200308221222p:plain
メッセージフォーマット

メッセージごとのフォーマットはドキュメントに明示されており、各メッセージの仕様が十分に説明されている。それを読んでわかるひとはここで解散。

課題

メッセージごとの仕様をふまえて、現実的にはそれらを組み合わせて実際の処理を行う必要がある。任意の処理をPostgreSQLにさせたいとき、どういうメッセージをどういう順序で送信し、どういうメッセージが返ってくるのか、といった具体的なユースケースに基づく説明は、公式にはされていない。これを理解する方法として、既存のクライアントインターフェイスを参考にすることはできる。TCPパケットをキャプチャしているストリームでやりとりされるメッセージをみることは比較的簡単にできる。

17:57:57.965463 IP 127.0.0.1.33174 > 127.0.0.1.5432: Flags [P.], seq 2521464336:2521464403, ack 414052419, win 1418, options [nop,nop,TS val 6835604 ecr 6823158], length 67
    0x0000:  4500 0077 a8d0 4000 4006 93ae 7f00 0001  E..w..@.@.......
    0x0010:  7f00 0001 8196 1538 964a 7e10 18ad f043  .......8.J~....C
    0x0020:  8018 058a fe6b 0000 0101 080a 0068 4d94  .....k.......hM.
    0x0030:  0068 1cf6 5100 0000 4273 656c 6563 7420  .h..Q...Bselect.
    0x0040:  7265 6c61 636c 2066 726f 6d20 7067 5f63  relacl.from.pg_c
    0x0050:  6c61 7373 2077 6865 7265 2072 656c 6163  lass.where.relac
    0x0060:  6c20 6973 206e 6f74 206e 756c 6c20 6c69  l.is.not.null.li
    0x0070:  6d69 7420 313b 00                        mit.1;.

tcpdump PostgreSQL

かろうじてASCII文字として認識できる部分もあるが、バイナリで解釈して都度ドキュメントと照合するのは大変すぎる。また、メッセージは非同期でやりとりされるし、クライアントインターフェイスが内部的にメッセージを送信するため、任意の処理に紐づくメッセージとその順序を認識することは難しい。また、既存のクライアントインターフェイスのコードから読み解く方法もあるが、その絶対数が少ない上に実用的になればなるほど高度に最適化されており、プロトコルを形成するメッセージ群を識別することは難しい。と思わないひとはここで解散。

TCPパケットからPostgreSQLのプロトコルを解析として以下の事例はあるが、これはクエリまで書き下しており、メッセージの単位でみることはできない。

k1low.hatenablog.com

このように、「PostgreSQLはクエリを実行するとき、どのようなメッセージをやりとりしているか」を把握することが難しい状況にある。そこで、一般的な処理を実行するときにメッセージがやりとりされるようすを紹介してみる。

環境

今回は、PostgreSQLのプロトコルの実体であるメッセージを単位として、通信を覗きたい。というわけで、雑なシリアライザと、それをつかった雑なコマンドラインツールをつくった。

github.com

今回の検証用に作っただけなので、コードはひどいし実用性とかはまったくないけど、各メッセージのプロパティをみればどんなデータを送受信しているかはなんとなくみえるはず。

public class ParseMessage : FrontendMessage
{
    public static byte MessageTypeId = (byte)'P';

    public string PreparedStatementName { get; set; } = string.Empty;

    public string Query { get; set; } = string.Empty;

    public short ParameterDataTypeOidsCount { get { return (short)ParameterDataTypeOids.Count; } }

    public IList<int> ParameterDataTypeOids { get; set; } = new List<int>();

    public override byte[] Serialize()
    {
        // 略
    }
}

コマンドラインツールは、送信するメッセージをJSON形式で指定すればパースしてメッセージをバックエンドに送信し、バックエンドから返ってきたメッセージを出力するインターフェイスになっている。例えば、簡易クエリを使ったDELETE文の実行は以下のようになる。

pg_msg =# {"messages":[{"name":"Query","body":{"Query":"DELETE FROM table1"}}]}
<--    Query {"Query":"DELETE FROM table1"}
   --> CommandComplete {"CommandTag":"DELETE 0"}
   --> ReadyForQuery {"TransactionStatus":73}

(送受信の方向) (メッセージ名) (メッセージのペイロード) という構造で出力している。 <-- Query {"Query":"delete from table1"} は、「QueryメッセージのQueryフィールドに DELETE FROM table1 を指定して、バックエンド(=PostgreSQL)に送信したことを意味する。

検証

上記のコマンドラインツールを使って、いくつかのユースケースでPostgreSQLのプロトコルを介したメッセージのやりとりを検証してみる。なお、事前に CREATE TABLE table1 (integer test_column PRIMARY KEY) でテーブルを作成しておく。また、jsonを指定する行は省略して、送信と受信の出力結果のみをみる。

認証

まず、PostgreSQLに対して認証する必要がある。ユーザ名とデータベース名を付与して送信する。

<--    Startup {"ProtocolVersion":196608,"Parameters":[{"Name":"user","Value":"kabata"},{"Name":"database","Value":"postgres"}],"EndMessage":0}
   --> Authentication {"AuthResult":0}
   --> ParameterStatus {"Name":"application_name","Value":""}
   --> ParameterStatus {"Name":"client_encoding","Value":"UTF8"}
   --> ParameterStatus {"Name":"DateStyle","Value":"ISO, MDY"}
   --> ParameterStatus {"Name":"integer_datetimes","Value":"on"}
   --> ParameterStatus {"Name":"IntervalStyle","Value":"postgres"}
   --> ParameterStatus {"Name":"is_superuser","Value":"on"}
   --> ParameterStatus {"Name":"server_encoding","Value":"UTF8"}
   --> ParameterStatus {"Name":"server_version","Value":"12.1"}
   --> ParameterStatus {"Name":"session_authorization","Value":"kabata"}
   --> ParameterStatus {"Name":"standard_conforming_strings","Value":"on"}
   --> ParameterStatus {"Name":"TimeZone","Value":"Asia/Tokyo"}
   --> BackendKeyData {"ProcessId":95386,"SecretKey":738316716}
   --> ReadyForQuery {"TransactionStatus":73}

サーバに設定されている認証方式に応じて、レスポンスのメッセージは変わる。例えばパスワード認証を要求する場合はAuthenticationCleartextPasswordメッセージやAuthenticationMD5Passwordメッセージがかえる。今回はパスワードを要求せず、AuthenticationOkメッセージを返すようにしている*1。その後、ParameterStatusメッセージによって、バックエンドの設定値がかえるので、クライアントインターフェイスでこれらを利用することができる。

また、BackendKeyDataメッセージに含まれる情報は、クライアントインターフェイスからバックエンドのプロセスを操作するために必要になる。例えば、クエリをキャンセルする際には、この情報を指定してバックエンドに送信する。最後のReadyForQueryメッセージは、次のクエリを受け付ける状態になったことを意味する。TransactionStatusの73は、ASCIIコードでの文字 I であり、トランザクションがIdle状態であることを意味している。

簡易クエリ

PostgreSQLに対してクエリを実行させるときの方法として、大きく簡易プロトコルと拡張プロトコルの2種類がある。まず簡易クエリを実行してみる。その名のとおりQueryメッセージを送信し、結果を受信する。

<--    Query {"Query":"INSERT INTO table1 VALUES (1)"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":73}

Queryメッセージでクエリを送信すると、PostgreSQLがそれを実行する。成功したらCommandCompleteメッセージとReadyForQueryメッセージが返ってくる。シンプル。CommandCompleteメッセージでは、コマンドタグというフィールドを含んでいる。コマンドタグ自体の仕様はPostgreSQLのドキュメントには明示されていない。 INSERTに関しては、 INSERT oid count というフォーマットになっている*2。対象のテーブルに WITH OIDS がついていればoidが振られるが、今回はそれがないので0になっている。countは挿入した行数。

パイプライン

メッセージなので当然非同期に処理される。そのため、都度ReadyForQueryメッセージを待つ必要はない。

<--    Query {"Query":"INSERT INTO table1 VALUES (2)"}
<--    Query {"Query":"INSERT INTO table1 VALUES (3)"}
<--    Query {"Query":"INSERT INTO table1 VALUES (4)"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":73}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":73}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":73}

Queryメッセージを3つ送信して、そのあとそれらの結果を受信している。後述する拡張クエリのようにひとつの処理を複数のメッセージで処理する場合をのぞいて、基本的にメッセージは互いに独立して実行できる。

複数コマンド

複数のコマンドを ; でつなげて、ひとつのQueryメッセージで送信することもできる。

<--    Query {"Query":"INSERT INTO table1 VALUES (5);INSERT INTO table1 VALUES (6);INSERT INTO table1 VALUES (7);"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":73}

このとき、CommandCompleteメッセージはコマンドごとに発行される。ただしパイプラインのときと違い、ReadyForQueryメッセージはひとつだけ返ってくる。

暗黙的トランザクション(簡易クエリ)

トランザクションを明示しない場合に、失敗するコマンドを実行してみる。

<--    Query {"Query":"INSERT INTO table1 VALUES (8);INSERT INTO table1 VALUES (8);INSERT INTO table1 VALUES (9);"}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ErrorResponse {"Fields":[{"Id":83,"Value":"ERROR"},{"Id":86,"Value":"ERROR"},{"Id":67,"Value":"23505"},{"Id":77,"Value":"duplicate key value violates unique constraint \u0022table1_pkey\u0022"},{"Id":68,"Value":"Key (id)=(8) already exists."},{"Id":115,"Value":"public"},{"Id":116,"Value":"table1"},{"Id":110,"Value":"table1_pkey"},{"Id":70,"Value":"nbtinsert.c"},{"Id":76,"Value":"570"},{"Id":82,"Value":"_bt_check_unique"}]}
   --> ReadyForQuery {"TransactionStatus":73}

ErrorResponseメッセージが返ってくる。このメッセージはエラーの情報をもっている。今回の例だと、重複キーエラーが発生したことがわかる。各フィールドにはidがふられており、それらの詳細はドキュメントに記述されている。

明示的トランザクション(簡易クエリ)

トランザクションを明示した場合に、失敗するコマンドを実行してみる。トランザクションは、QueryメッセージにBEGINコマンドを指定して送信する。

<--    Query {"Query":"BEGIN"}
<--    Query {"Query":"INSERT INTO table1 VALUES (10)"}
<--    Query {"Query":"INSERT INTO table1 VALUES (10)"}
<--    Query {"Query":"INSERT INTO table1 VALUES (11)"}
<--    Query {"Query":"END"}
   --> CommandComplete {"CommandTag":"BEGIN"}
   --> ReadyForQuery {"TransactionStatus":84}
   --> CommandComplete {"CommandTag":"INSERT 0 1"}
   --> ReadyForQuery {"TransactionStatus":84}
   --> ErrorResponse {"Fields":[{"Id":83,"Value":"ERROR"},{"Id":86,"Value":"ERROR"},{"Id":67,"Value":"23505"},{"Id":77,"Value":"duplicate key value violates unique constraint \u0022table1_pkey\u0022"},{"Id":68,"Value":"Key (id)=(10) already exists."},{"Id":115,"Value":"public"},{"Id":116,"Value":"table1"},{"Id":110,"Value":"table1_pkey"},{"Id":70,"Value":"nbtinsert.c"},{"Id":76,"Value":"570"},{"Id":82,"Value":"_bt_check_unique"}]}
   --> ReadyForQuery {"TransactionStatus":69}
   --> ErrorResponse {"Fields":[{"Id":83,"Value":"ERROR"},{"Id":86,"Value":"ERROR"},{"Id":67,"Value":"25P02"},{"Id":77,"Value":"current transaction is aborted, commands ignored until end of transaction block"},{"Id":70,"Value":"postgres.c"},{"Id":76,"Value":"1105"},{"Id":82,"Value":"exec_simple_query"}]}
   --> ReadyForQuery {"TransactionStatus":69}
   --> CommandComplete {"CommandTag":"ROLLBACK"}
   --> ReadyForQuery {"TransactionStatus":73}

BEGINコマンドを実行したらReadyForQueryメッセージが返るが、このときTransactionStatusが84(= T )になっている。これは、トランザクションブロック内であることを意味する。そのあと、明示しない場合と同じくキー重複のErrorResponseメッセージが返るが、このときTransactionStatusが69(= E )になっている。これは失敗したトランザクションブロックであることを意味する。この時点でトランザクションはロールバック以外の問い合わせを拒絶するようになるため、その次のINSERTコマンドもErrorResponseメッセージを返している。最後のENDコマンドによりトランザクションがロールバックされ、TransactionStatusが73(= I)になっている。

クエリ結果

SELECTコマンドにより結果を取得してみる。

<--    Query {"Query":"SELECT * FROM table1"}
   --> RowDescription {"FieldsCount":1,"RowFieldDescriptions":[{"FieldName":"id","TableOid":16397,"RowAttributeId":1,"FieldTypeOid":23,"DataTypeSize":4,"TypeModifier":-1,"FormatCode":0}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[49]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[50]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[51]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[52]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[53]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[54]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[55]}]}
   --> CommandComplete {"CommandTag":"SELECT 7"}
   --> ReadyForQuery {"TransactionStatus":73}

RowDescriptionメッセージは後続で送信されるデータの各属性の情報を持っている。今回は1つだけをもっており、"FieldTypeOid":23 はinteger型であること、 "DataTypeSize":4 はその型の長さが4バイトであることを意味する。 "FormatCode":0 はそのフィールドに使用される書式コードを意味し、0(テキスト)または1(バイナリ)のいずれかになる。

DataRowメッセージでは、クエリの結果を受信している。ここまでの検証でINSERTしてきたデータが取得できている。Valueはバイト列である。ここでは文字列であるため、例えば Valueに51とあるのは、文字列の "3" に対応している。

拡張クエリ

簡易クエリは単一のクエリがひとつのメッセージと1:1に対応していたが、拡張クエリでは、複数のメッセージを組み合わせで実行する。メッセージ間で利用される情報は、プリペアド文とポータルという2種類のオブジェクトで表現される。プリペアド文はSQLの構文解析、意味解析を行い、ポータルはパラメータを設定して実行可能な状態にする。簡易クエリよりもタスクが分割されて処理が最適化されるし、パラメータを型指定で埋め込むため、セキュリティ的にも安全になる。

<--    Parse {"PreparedStatementName":"stmt1","Query":"SELECT * FROM table1","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"portal1","PreparedStatementName":"stmt1","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":0,"ParameterValues":[],"RowFormatCode":0}
<--    Describe {"TargetType":80,"TargetName":"portal1"}
<--    Execute {"PortalName":"portal1","Limit":0}
<--    Sync {}
   --> ParseComplete {}
   --> BindComplete {}
   --> RowDescription {"FieldsCount":1,"RowFieldDescriptions":[{"FieldName":"id","TableOid":16397,"RowAttributeId":1,"FieldTypeOid":23,"DataTypeSize":4,"TypeModifier":-1,"FormatCode":0}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[49]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[50]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[51]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[52]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[53]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[54]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[55]}]}
   --> CommandComplete {"CommandTag":"SELECT 7"}
   --> ReadyForQuery {"TransactionStatus":73}

Parseメッセージは、 SQLからプリペアド文を作成する。次のBindメッセージでは、プリペアド文にパラメータを埋め込み、ポータルを作成する。Describeメッセージは、対象であるプリペアド文( S )またはポータル( P )に関する状態を取得する。ここでは、TargetTypeに80(=P)を指定しており、RowDescriptionメッセージが返ってきている。これにより、ポータルが保持している、行数や型情報などが取得できる。その上で、Executeメッセージを実行することにより、実行でき、あとは簡易クエリと同じ結果が取得できている。簡易クエリと異なり、BindメッセージでRowFormatCodeを指定することもできる。

Syncメッセージは

暗黙的トランザクション(拡張クエリ)

拡張クエリにおけるトランザクションの挙動をみてみる。

<--    Parse {"PreparedStatementName":"stmt2","Query":"SELECT 1/0","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"portal2","PreparedStatementName":"stmt2","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":0,"ParameterValues":[],"RowFormatCode":0}
<--    Execute {"PortalName":"portal2","Limit":0}
<--    Sync {}
   --> ParseComplete {}
   --> ErrorResponse {"Fields":[{"Id":83,"Value":"ERROR"},{"Id":86,"Value":"ERROR"},{"Id":67,"Value":"22012"},{"Id":77,"Value":"division by zero"},{"Id":70,"Value":"int.c"},{"Id":76,"Value":"824"},{"Id":82,"Value":"int4div"}]}
   --> ReadyForQuery {"TransactionStatus":73}

Parseメッセージはコマンドの解析しかしないので成功している。Bindメッセージで失敗し、ErrorResponseが返っている。

暗黙的トランザクションのロールバック

拡張クエリにおいて、トランザクションを張らずにロールバックしてみる。

<--    Parse {"PreparedStatementName":"stmt3","Query":"SELECT now()","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Parse {"PreparedStatementName":"stmt4","Query":"ROLLBACK","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"portal4","PreparedStatementName":"stmt4","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":0,"ParameterValues":[],"RowFormatCode":0}
<--    Execute {"PortalName":"portal4","Limit":0}
<--    Sync {}
   --> ParseComplete {}
   --> ParseComplete {}
   --> BindComplete {}
   --> NoticeResponse {"Fields":[{"Id":83,"Value":"WARNING"},{"Id":86,"Value":"WARNING"},{"Id":67,"Value":"25P01"},{"Id":77,"Value":"there is no transaction in progress"},{"Id":70,"Value":"xact.c"},{"Id":76,"Value":"3940"},{"Id":82,"Value":"UserAbortTransactionBlock"}]}
   --> CommandComplete {"CommandTag":"ROLLBACK"}
   --> ReadyForQuery {"TransactionStatus":73}

2つ目のParseメッセージでROLLBACKコマンドを実行しているが、トランザクションブロックが存在しないため、NoticeResponseメッセージが返っている。コマンド自体は成功し、続けてCommandCompleteメッセージが返っている。この時点で1つ目のParseメッセージで生成されたプリペアド文はまだバックエンドに残っている。

パラメータ

拡張クエリでパラメータを指定してみる。パラメータの指定には位置パラメータ参照を利用する。

<--    Parse {"PreparedStatementName":"stmt5","Query":"SELECT * FROM table1 WHERE id = $1","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"portal5","PreparedStatementName":"stmt5","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":1,"ParameterValues":[{"Length":1,"Value":"Mg=="}],"RowFormatCode":0}
<--    Execute {"PortalName":"portal5","Limit":0}
<--    Bind {"PortalName":"portal6","PreparedStatementName":"stmt5","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":1,"ParameterValues":[{"Length":1,"Value":"NA=="}],"RowFormatCode":0}
<--    Execute {"PortalName":"portal6","Limit":0}
<--    Sync {}
   --> ParseComplete {}
   --> BindComplete {}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[50]}]}
   --> CommandComplete {"CommandTag":"SELECT 1"}
   --> BindComplete {}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[52]}]}
   --> CommandComplete {"CommandTag":"SELECT 1"}
   --> ReadyForQuery {"TransactionStatus":73}

Parseメッセージで SELECT * FROM table1 WHERE id = $1 として位置パラメータ参照を指定し、後続のBindメッセージでパラメータの値を渡す。ここでは "ParameterValues":[{"Length":1,"Value":"Mg=="}] と出力されているが、これは 2 の文字がbase64でエンコードした値を指定している。これにより、バックエンドでパラメータを埋め込んで SELECT * FROM table1 WHERE id = 2 として実行され、結果がDataRowメッセージで "Value":[50] として返っている((50はASCIIコードで 2))。

注目すべきは、1つのParseメッセージに対して、2つのBindメッセージ/Executeメッセージを実行している。パラメータが2のときと4のときで、プリペアド文を使いまわしている。このように、「同じコマンドだけどパラメータの値が違う」コマンドが大量に実行されるケースで、簡易クエリよりも拡張クエリのほうが性能面で優位になることがわかる。

カーソル

拡張クエリでカーソルを使って段階的にデータを取得してみる。

--    Parse {"PreparedStatementName":"","Query":"SELECT * FROM table1 ORDER BY id","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"cur1","PreparedStatementName":"","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":0,"ParameterValues":[],"RowFormatCode":0}
<--    Execute {"PortalName":"cur1","Limit":1}
<--    Execute {"PortalName":"cur1","Limit":1}
<--    Parse {"PreparedStatementName":"","Query":"MOVE 2 cur1","ParameterDataTypeOidsCount":0,"ParameterDataTypeOids":[]}
<--    Bind {"PortalName":"","PreparedStatementName":"","ParameterFormatCodeCount":0,"ParameterFormatCodes":[],"ParameterValueCount":0,"ParameterValues":[],"RowFormatCode":0}
<--    Execute {"PortalName":"","Limit":0}
<--    Execute {"PortalName":"cur1","Limit":0}
<--    Sync {}
   --> ParseComplete {}
   --> BindComplete {}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[49]}]}
   --> PortalSuspended {}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[50]}]}
   --> PortalSuspended {}
   --> ParseComplete {}
   --> BindComplete {}
   --> CommandComplete {"CommandTag":"MOVE 2"}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[53]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[54]}]}
   --> DataRow {"ColumnCount":1,"Rows":[{"Length":1,"Value":[55]}]}
   --> CommandComplete {"CommandTag":"SELECT 3"}
   --> ReadyForQuery {"TransactionStatus":73}

Parseメッセージで指定したコマンドは、すでに検証したとおり7件の結果が返るが、Executeメッセージで "Limit":1 を指定することにより、1件だけ取得している。また、 MOVE 2 cur1 でカーソル位置をすすめることにより、3件取得している。

感想

シリアライザを作りながら仕様がなんとなくわかってきて、楽しかった。ドキュメントを何度読んでもわからないけど、自分でコードを書いて動かすのが一番理解できる。時間かかる。ただ、プログラミングまったくわからんし、設計もなにもわかっていないことがわかった。精進していきたい。もっとしっかり作るとよさそう。

普段はアプリケーションを開発しているのだけど、自分の経験上、ひとつ下のレイヤを理解していることがかなり有益になるので、その意味でも今後PostgreSQLと関わるなにかをやるときに役に立つと嬉しい。MySQLとか他のRDBMSと比較してみてもおもしろそう。だれかやってみてください。

参考

*1:認証フェーズでは先頭の2バイトまででメッセージを識別するが、今回は簡素化している

*2:https://www.postgresql.org/docs/12/sql-insert.html