本節では、メッセージの流れと各メッセージ種類のセマンティクスを説明します。
(各メッセージの正確な表現の詳細については55.7で説明します。)
開始、問い合わせ、関数呼び出し、COPY
、終了といった接続状態に応じて、複数の異なるサブプロトコルがあります。
また、開始段階の後の任意の時点で発生する可能性がある、非同期操作(通知応答やコマンドのキャンセルを含む)用の特別な準備もあります。
セッションを開始するために、フロントエンドはサーバへの接続を開き、開始メッセージを送信します。
このメッセージには、ユーザ名と接続を希望するデータベース名が含まれます。
これはまた、使用する特定のプロトコルバージョンを識別します。
(オプションとして、開始メッセージに、実行時パラメータの追加設定を含めることもできます。)
サーバはその後、この情報と設定ファイル(pg_hba.conf
など)の内容を使用して、接続が暫定的に受け付けられるかどうか、そして(もしあれば)どのような追加認証が必要かを決定します。
サーバはその後、適切な認証要求メッセージを送信します。 フロントエンドはこれに適切な認証応答メッセージ(パスワードなど)で答えなければなりません。 GSSAPI、SSPI、SASLを除くすべての認証方式では、多くても1つの要求と1つの応答が存在します。 認証方式の中には、フロントエンドからの応答をまったく必要としないものもあり、その場合、認証要求も発生しません。 GSSAPI、SSPI、SASLでは認証を完了するために複数のパケットの交換が必要となるかもしれません。
認証サイクルは、サーバによって接続要求を拒絶する(ErrorResponse)か、あるいはAuthenticationOkを送信することで終わります。
この段階でサーバから送信される可能性があるメッセージを以下に示します。
接続試行が拒絶されました。 サーバはその後即座に接続を閉ざします。
認証情報の交換が正常に完了しました。
フロントエンドはここでサーバとのKerberos V5認証ダイアログ(ここでは説明しません。Kerberos仕様の一部)に参加する必要があります。 これが成功すれば、サーバはAuthenticationOk応答を行います。 失敗すれば、ErrorResponse応答を行います。 これはもはやサポートされていません。
フロントエンドはここで平文形式のパスワードを含むPasswordMessageを送信する必要があります。 これが正しいパスワードであればサーバはAuthenticationOk応答を行います。 さもなくば、ErrorResponse応答を行います。
フロントエンドはここでMD5で暗号化したパスワード(とユーザ名)を再度AuthenticationMD5Passwordメッセージで指定されたランダムな4バイトのソルトを使用して暗号化したものを含むPasswordMessageを送信する必要があります。
これが正しいパスワードであればサーバはAuthenticationOk応答を行います。
さもなくば、ErrorResponse応答を行います。
実際のPasswordMessageをconcat('md5', md5(concat(md5(concat(password, username)), random-salt)))
というSQLで計算することができます。
(md5()
関数がその結果を16進数表記で返すことに注意してください。)
ここでフロントエンドはGSSAPIの調停を開始しなければなりません。 これに対する応答におけるGSSAPIデータストリームの最初の部分で、フロントエンドはGSSResponseを送信します。 さらにメッセージが必要となる場合、サーバはAuthenticationGSSContinueで応答します。
ここでフロントエンドはSSPI調停を開始しなければなりません。 これに対する応答におけるSSPIデータストリームの最初の部分で、フロントエンドはGSSResponseを送信します。 さらにメッセージが必要となる場合、サーバはAuthenticationGSSContinueで応答します。
このメッセージには、GSSAPIまたはSSPI調停の直前の段階(AuthenticationGSS、AuthenticationSSPIまたは前回のAuthenticationGSSContinue)についての応答データが含まれます。 このメッセージ内のGSSAPIまたはSSPIデータが認証を完結させるため、更なる追加データが必要であることを指示している場合、フロントエンドは他のGSSResponseとしてデータを送信しなければなりません。 このメッセージでGSSAPIまたはSSPI認証が完了すれば、次にサーバはAuthenticationOkを送信して認証が成功したことを示すか、あるいはErrorResponseを送信して失敗したことを示します。
ここでフロントエンドはメッセージ内に列挙されているSASL機構の1つを使ってSASL調停を開始しなければなりません。 これに応答するSASLデータストリームの最初の部分で、フロントエンドはSASLInitialResponseと選択した機構の名前を送信します。 さらにメッセージが必要な場合、サーバはAuthenticationSASLContinueで応答します。 詳細については55.3を参照してください。
このメッセージには、SASL調停における直前の段階(AuthenticationSASLまたは以前のAuthenticationSASLContinue)のチャレンジデータが含まれます。 フロントエンドはSASLResponseメッセージで応答しなければなりません。
機構固有のクライアント用の追加データを伴ってSASL認証が完了します。 サーバは次に認証成功を示すAuthenticationOkを送信するか、あるいは失敗を示すErrorResponseを送信します。 このメッセージはSASLの機構が完了時にサーバからクライアントに送信する追加データを指定しているときにのみ送信されます。
サーバはクライアントが要求したマイナープロトコルバージョンをサポートしませんが、それ以前のバージョンをサポートします。
このメッセージは、サポートしている最も高いマイナーバージョンを示します。
このメッセージは、クライアントがサポートされないプロトコルオプション(つまり_pq_.
で始まる)をスタートアップパケットの中で指定した場合にも送られます。
このメッセージの後に、ErrorResponseか、認証が成功あるいは失敗したことを示すメッセージが続きます。
サーバが要求した認証方式をフロントエンドがサポートしていない場合、フロントエンドは即座に接続を閉ざします。
AuthenticationOkを受け取った後、フロントエンドはさらにサーバからのメッセージを待機する必要があります。 この段階で、バックエンドプロセスが起動し、このフロントエンドは単なる関心を有する第三者となります。 開始試行が失敗(ErrorResponse)するか、サーバが要求されたマイナープロトコルバージョンを拒否する(NegotiateProtocolVersion)可能性がまだありますが、通常、バックエンドは何らかのParameterStatusメッセージ、BackendKeyData、そして最後にReadyForQueryを送信します。
この段階の期間中、バックエンドは開始メッセージで与えられた任意の実行時パラメータの追加設定を適用しようとします。 成功した場合は、これらの値はセッションのデフォルトになります。 エラーが発生した場合はErrorResponseを行い、終了します。
この段階でバックエンドから送信される可能性があるメッセージを以下に示します。
このメッセージは、フロントエンドがキャンセル要求を後で送信できるようにしたい場合に保存しなければならない、秘密キーデータを用意します。 フロントエンドはこのメッセージに応答してはいけませんが、ReadyForQueryメッセージの監視を続けなくてはなりません。
このメッセージは、フロントエンドに現在(初期)のclient_encodingやDateStyleなどのバックエンドパラメータの設定情報を通知します。 フロントエンドはこのメッセージを無視しても、将来の使用に備えてその設定を記録しても構いません。 詳細は55.2.7を参照してください。 フロントエンドはこのメッセージに応答してはいけませんが、ReadyForQueryメッセージの監視を続けなくてはなりません。
開始処理が完了しました。 フロントエンドはここでコマンドを発行することができます。
開始処理が失敗しました。 接続はこのメッセージの送信後に閉ざされます。
警告メッセージが発行されました。 フロントエンドはこのメッセージを表示し、ReadyForQueryもしくはErrorResponseメッセージの監視を続けなければなりません。
ReadyForQueryメッセージは各コマンドサイクルの後にバックエンドが発行するものと同じものです。 フロントエンドのコーディングにおいて必要であれば、ReadyForQueryをコマンドサイクルの開始とみなしても構いませんし、ReadyForQueryを開始段階とその後の各コマンドサイクルの終端とみなしても構いません。
フロントエンドがQueryメッセージをバックエンドに送信することで、簡易問い合わせサイクルが開始されます。 このメッセージには、テキスト文字列で表現されたSQLコマンド(またはコマンド)が含まれます。 そうすると、バックエンドは、問い合わせコマンド文字列の内容に応じて1つ以上の応答を送信し、最終的にReadyForQueryを応答します。 ReadyForQueryは、新しいコマンドを安全に送信できることをフロントエンドに知らせます。 (実際には、フロントエンドが他のコマンドを発行する前にReadyForQueryを待機することは不要です。 しかし、フロントエンドは、前のコマンドが失敗し、発行済みの後のコマンドが成功した場合に何が起きるかを了解する責任を持たなければなりません。)
バックエンドから送信される可能性がある応答メッセージを以下に示します。
SQLコマンドが正常に終了しました。
バックエンドがフロントエンドからのデータをテーブルにコピーする準備ができました。 55.2.6を参照してください。
バックエンドがデータをテーブルからフロントエンドにコピーする準備ができました。 55.2.6を参照してください。
SELECT
やFETCH
などの問い合わせの応答の行がまさに返されようとしていることを示します。
このメッセージには、行の列レイアウトに関する説明が含まれます。
このメッセージの後に、フロントエンドに返される各行に対するDataRowメッセージが続きます。
SELECT
やFETCH
などの問い合わせで返される行の集合の1つです。
空の問い合わせ文字列が検知されました。
エラーが起こりました。
問い合わせ文字列の処理が終了しました。 問い合わせ文字列は複数のSQLコマンドが含まれる場合があるため、このことを通知するために分離したメッセージが送出されます。 (CommandCompleteは文字列全体ではなく、1つのSQLコマンドの処理の終了を明らかにします。) 処理が成功またはエラーで終了したかどうかにかかわらずReadyForQueryは常に送出されます。
問い合わせに関して警告メッセージが発行されました。 警告メッセージは他の応答に対する追加のメッセージです。 したがって、バックエンドはそのコマンドの処理を続行します。
SELECT
問い合わせ(あるいは、EXPLAIN
やSHOW
などの行集合を返す他の問い合わせ)に対する応答は、通常、RowDescription、0個以上のDataRowメッセージ、そしてその後のCommandCompleteから構成されます。
フロントエンドへのCOPY
もしくはフロントエンドからのCOPY
は55.2.6で説明する特別なプロトコルを呼び出します。
他の種類の問い合わせは通常CommandCompleteメッセージのみを生成します。
問い合わせ文字列には(セミコロンで区切られた)複数の問い合わせが含まれることがありますので、バックエンドが問い合わせ文字列の処理を完了する前に、こうした応答シーケンスが複数発生する可能性があります。 ReadyForQueryは、文字列全体が処理され、バックエンドが新しい問い合わせ文字列を受け付ける準備が整った時点で発行されます。
完全に空の(空白文字以外の文字がない)問い合わせ文字列を受け取った場合、その応答は、EmptyQueryResponse、続いて、ReadyForQueryとなります。
エラーが発生した場合、ErrorResponse、続いて、ReadyForQueryが発行されます。 その問い合わせ文字列に対する以降の処理は(複数の問い合わせが残っていたとしても)すべて、ErrorResponseによって中断されます。 これは、個々の問い合わせで生成されるメッセージの並びの途中で発生する可能性があることに注意してください。
簡易問い合わせモードでは、読み出される値の書式は常にテキストです。
ただし、与えられたコマンドがBINARY
オプション付きで宣言されたカーソルからのFETCH
であった場合は例外です。
この場合は、読み出される値はバイナリ書式になります。
RowDescriptionメッセージ内で与えられる書式コードは、どの書式が使用されているかを通知します。
他の種類のメッセージの受信を待機している時、フロントエンドは常にErrorResponseとNoticeResponseメッセージを受け取る準備ができていなければなりません。 また、外部イベントのためにバックエンドが生成する可能性があるメッセージの扱いについては55.2.7を参照してください。
メッセージの正しい並びを前提としてコーディングするのではなく、任意のメッセージ種類を、そのメッセージが意味を持つ任意の時点で受け付ける状態マシン形式でフロントエンドのコーディングを行うことを推奨します。
簡易Queryメッセージが二つ以上の(セミコロンで区切られた)SQL文を含むとき、振る舞いを変えるように明示的なトランザクション制御コマンドが含まれていない限り、これらの文は単一トランザクションで実行されます。 例えばメッセージが以下を含む場合、
INSERT INTO mytable VALUES(1); SELECT 1/0; INSERT INTO mytable VALUES(2);
SELECT
でのゼロ除算エラーは最初のINSERT
のロールバックを強制します。
さらに、メッセージの実行が最初のエラー時点で中止されるので、二番目のINSERT
は全く試みられません。
代わりにメッセージが以下を含んでいる場合、
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELECT 1/0;
最初のINSERT
は明示的なCOMMIT
コマンドによりコミットされます。
二番目のINSERT
とSELECT
は、やはり単一トランザクションとして処理されます。
そのためゼロ除算エラーが二番目のINSERT
をロールバックしますが、最初のINSERT
はロールバックされません。
この振る舞いは、暗黙トランザクションブロックで複文Queryメッセージ内の文を実行することで、その中に明示的なトランザクションブロックがある場合を除き、発現します。 暗黙トランザクションブロックと通常のトランザクションブロックとの主な違いは、暗黙ブロックは自動的にQueryメッセージの最後にて、エラーが無いなら暗黙のコミット、エラーがあるなら暗黙のロールバックで、自動的に閉じられることです。 これは(トランザクションブロック内に無いときの)文の単体実行に対して生じる暗黙のコミットあるいはロールバックに似ています。
何らか手前のメッセージでのBEGIN
の結果として、セッションが既にトランザクションブロック内である場合、Queryメッセージは、含まれるのが単一文でもいくつかの文でも、単にそのトランザクションを継続します。
しかしながら、Queryメッセージが既存トランザクションブロックを閉じるCOMMIT
やROLLBACK
を含む場合、続く全ての文は暗黙トランザクションブロックで実行されます。
逆に言えば、複文QueryメッセージでBEGIN
が現れたなら、このQueryメッセージ内または後のメッセージのいずれかにあらわれる明示的なCOMMIT
やROLLBACK
でのみ終了する、通常のトランザクションブロックが開始されます。
BEGIN
が暗黙トランザクションブロックとして実行されたいくつかの文に続く場合、これらの文が直ちにコミットされることはありません。
結果として、これらは遡って新たな通常のトランザクションブロックに含められます。
暗黙トランザクションブロック内に現れたCOMMIT
やROLLBACK
は通常通り実行され、暗黙ブロックを閉じますが、手前のBEGIN
無しのCOMMIT
やROLLBACK
は誤りであるかもしれないので警告が発行されます。
さらに文が続く場合、それらに対して新たな暗黙トランザクションブロックが開始されます。
エラー時の自動ブロッククローズの振る舞いと競合するので暗黙トランザクションブロックでセーブポイントは使えません。
現状のいかなるトランザクション制御コマンドでも、Queryメッセージの実行は最初のエラー時点で打ち切られることに留意してください。 例を示します。
BEGIN; SELECT 1/0; ROLLBACK;
上記が単一Queryメッセージにあるとして、ゼロ除算エラーの後にROLLBACK
に達することがないため、このセッションは失敗した通常のトランザクション内のままとなります。
このセッションを通常の状態に回復させるには別のROLLBACK
が必要となります。
その他の注意すべき振る舞いは、初期の字句および構文解析が少しも実行されない段階で問い合わせ文字列全体に対して行われることです。 従って、後ろの分にある(スペルミスしたキーワードなどの)単純なエラーは全ての文の実行を妨げることがあります。 暗黙トランザクションブロックとして起きたとき、いずれにせよ全ての文はロールバックされるので、これは通常はユーザに見えません。 しかしながら、複文問い合わせの中で複数のトランザクションを実行しようとするとき、この挙動が明らかになることがあります。 例えば、タイプミスで先の例を以下のようにします。
BEGIN; INSERT INTO mytable VALUES(1); COMMIT; INSERT INTO mytable VALUES(2); SELCT 1/0;
そうすると、含まれる文は一つも実行されず、最初のINSERT
がコミットされないという違いが明らかになります。
エラーは、ミススペルしたテーブルやカラム名など、語彙の解析かその後に検出され、コマンドの効力はありません。
拡張問い合わせプロトコルは、上述の簡易問い合わせプロトコルを複数段階に分解します。 予備段階の結果は複数回再利用できますので、効率が上がります。 さらに、問い合わせ文字列に直接埋め込むのではなく、データ値をパラメータとして分離して提供できる機能など、利用できる追加機能があります。
拡張プロトコルでは、フロントエンドはまず、テキストの問い合わせ文字列とオプションとしてパラメータプレースホルダのデータ型情報やプリペアド文のオブジェクトの宛先名(空文字列は無名のプリペアド文を選択)を含む、Parseメッセージを送信します。 この応答はParseCompleteまたはErrorResponseです。 パラメータデータ型はそのOIDで指定することができます。 指定がなければ、パーサは型指定のないリテラル文字列定数に対する方法と同じ方法でデータ型を推定します。
パラメータデータ型をゼロと設定する、または、問い合わせ文字列内で使用されているパラメータ記号($
n
)の数より短くパラメータ型のOIDの配列を作成することで、指定しないまま残すことができます。
他にも、パラメータの型をvoid
(つまりvoid
仮想型のOID)と指定するという特別な場合があります。
これは、パラメータ記号を、実際のOUTパラメータである関数パラメータとして使用することができることを意味します。
通常では、void
パラメータが使用される文脈はありませんが、関数パラメータリストにこうしたパラメータ記号があると、実質的には無視されます。
例えば、$3
と$4
がvoid
型を持つと指定された場合、foo($1,$2,$3,$4)
といった関数呼び出しは、2つのINと2つのOUT引数を持つ関数に一致します。
Parseメッセージ内の問い合わせ文字列には、複数のSQL文を含めることはできません。 さもないと、構文エラーが報告されます。 この制限は簡易問い合わせプロトコルにはありませんが、複数のコマンドを持つプリペアド文やポータルを許すと、プロトコルが複雑になり過ぎるため、拡張プロトコルではこの制限があります。
作成に成功すると、名前付きプリペアド文オブジェクトは明示的に破棄されない限り現在のセッションが終わるまで残ります。
無名のプリペアド文オブジェクトは、次に無名のプリペアド文を宛先に指定したParse文が発行されるまでの間のみに残ります。
(単なるQueryメッセージでも無名のプリペアド文オブジェクトは破壊されることに注意してください。)
名前付きプリペアド文は、他のParseメッセージで再定義する前に明示的に閉じなければなりません。
しかし、これは無名のプリペアド文では必要ありません。
名前付きプリペアド文はまた、SQLコマンドレベルでPREPARE
とEXECUTE
を使用して作成しアクセスすることができます。
プリペアド文が存在すると、Bindメッセージを使用してそれを実行可能状態にすることができます。
Bindメッセージは、元となるプリペアド文(空文字列は無名のプリペアド文を表します)の名前、宛先となるポータル(空文字列は無名ポータルを表します)の名前、およびプリペアド文内のパラメータプレースホルダに使用する値を与えます。
与えられたパラメータ集合はプリペアド文で必要とするものと一致しなければなりません。
(Parseメッセージ内でvoid
パラメータを1つでも宣言した場合、BindメッセージではそれらにはNULLを渡します。)
また、Bindは問い合わせで返されるデータに使用する書式を指定します。
書式は全体に対して指定することも、列単位で指定することも可能です。
応答はBindCompleteもしくはErrorResponseです。
テキスト出力とバイナリ出力との選択は、含まれるSQLコマンドに関係なく、Bindで与えられた書式コードで決定されます。
拡張問い合わせプロトコルを使用する場合、カーソル宣言のBINARY
属性は役に立ちません。
典型的に問い合わせ計画はBindメッセージが処理される時に作成されます。 プリペアド文がパラメータを持たない場合、または繰り返し実行される場合、サーバは作成した計画を保管し、その後の同じプリペアド文に対するBindメッセージの際に再利用する可能性があります。 しかし、作成できる汎用的な計画が提供された特定のパラメータ値に依存する計画より効率が大して劣化しないことが分かった場合のみ、このように動作します。 プロトコルに注目している限り、これは透過的に行われます。
作成に成功すると、名前付きポータルオブジェクトは明示的に破棄されない限り現在のセッションが終わるまで残ります。
無名ポータルは、トランザクションの終わり、もしくは、次に無名ポータルを宛先に指定したBind文が発行されるまでの間のみに残ります。
(単なるQueryメッセージでも無名ポータルは破壊されることに注意してください。)
名前付きポータルは、他のBindメッセージで再定義する前に明示的に閉じなければなりません。
しかし、これは無名ポータルでは必要ありません。
名前付きポータルはまた、SQLコマンドレベルでDECLARE CURSOR
とFETCH
を使用して作成しアクセスすることができます。
ポータルが存在すると、Executeメッセージを使用してそれを実行することができます。 Executeメッセージは、ポータル名(空文字列は無名ポータルを表します)と結果行数の最大値(ゼロは「fetch all rows」を意味します)を指定します。 結果行数は、ポータルが行集合を返すコマンドを含む場合のみ意味があります。 その他の場合では、コマンドは常に終わりまで実行され、行数は無視されます。 Executeで起こり得る応答は、ExecuteではReadyForQueryやRowDescriptionが発行されない点を除き、上述の簡易問い合わせプロトコル経由で発行された問い合わせの場合と同じです。
Executeがポータルの実行を完了する前に(非ゼロの結果行数に達したために)終了した場合、PortalSuspendedを送信します。 このメッセージの出現は、フロントエンドに操作を完了させるためには同一のポータルに対して、別のExecuteを発行しなければならないことを通知します。 元となったSQLコマンドが完了したことを示すCommandCompleteメッセージはポータルが完了するまで送信されません。 したがって、Execute段階は常にCommandComplete、EmptyQueryResponse(空の問い合わせ文字列からポータルが作成された場合)、ErrorResponse、またはPortalSuspendedの中の、正確にどれかが出現することによって常に終了します。
拡張問い合わせメッセージの一連の流れのそれぞれの終了時、フロントエンドはSyncメッセージを発行しなければなりません。
このパラメータのないメッセージにより、もしBEGIN
/COMMIT
トランザクションブロックの内部でなければ、バックエンドは現在のトランザクションを閉ざします
(「閉ざす」とは、エラーがなければコミット、エラーがあればロールバックすることを意味します)。
そして、ReadyForQuery応答が発行されます。
Syncの目的は、エラーからの復旧用の再同期を行う時点を提供することです。
拡張問い合わせメッセージの処理中にエラーが検出されると、バックエンドはErrorResponseを発行し、Syncが届くまでメッセージを読み、それを破棄します。
その後、ReadyForQueryを発行し、通常のメッセージ処理に戻ります。
(しかし、Sync処理中にエラーが検出された場合に処理が飛ばされないことに注意してください。
これにより、各Syncに対してReadyForQueryが1つのみであることを保証します。)
Syncによって、BEGIN
で開かれたトランザクションブロックが閉ざされることはありません。
ReadyForQueryメッセージにはトランザクションの状態情報が含まれていますので、この状況を検出することができます。
これらの基本的な必要操作に加え、拡張問い合わせプロトコルで使用することができる、複数の省略可能な操作があります。
Describeメッセージ(ポータルの亜種)は、既存のポータルの名前(もしくは、無名ポータル用の空文字)を指定します。 応答は、実行中のポータルで返される予定の行を記述するRowDescriptionメッセージです。 ポータルが行を返す問い合わせを含まない場合はNoDataメッセージです。 指定したポータルが存在しない場合はErrorResponseです。
Describeメッセージ(ステートメントの亜種)は、既存のプリペアド文の名前(もしくは無名のプリペアド文用の空文字)を指定します。 応答は、文で必要とされるパラメータを記述するParameterDescriptionメッセージ、続いて、文が実行された場合に返される予定の行を記述するRowDescriptionメッセージ(もしくは文が行を返さない場合のNoDataメッセージ)です。 指定したプリペアド文が存在しない場合はErrorResponseが発行されます。 Bindがまだ発行されていませんので、返される列の書式はまだバックエンドでは不明であることに注意してください。 RowDescriptionメッセージ内の書式コードフィールドはこの場合はゼロになります。
ほとんどの状況では、フロントエンドはExecuteを発行する前に、返ってくる結果を解釈する方法を確実に判断できるように、Describeもしくはその亜種を実行すべきです。
Closeメッセージは、既存のプリペアド文、もしくはポータルを閉ざし、リソースを解放します。 存在しない文やポータルに対してCloseを発行してもエラーになりません。 応答は通常CloseCompleteですが、リソースの解放に何らかの問題が発生した場合はErrorResponseになります。 プリペアド文を閉じると、そこから構築され、開いたポータルが暗黙的に閉ざされることに注意してください。
Flushメッセージにより特定の出力が生成されることはありません。 しかし、バックエンドに対して、出力バッファ内で待機しているデータを強制的に配送させます。 フロントエンドが他のコマンドを発行する前にコマンドの結果を検証したい場合に、FlushはSync以外の拡張問い合わせコマンドの後に送信される必要があります。 Flushを行わないと、バックエンドで返されるメッセージは、ネットワークオーバーヘッドを最小化する、最小限のパケット数にまとめられます。
簡易問い合わせメッセージは、おおよそ、無名のプリペアド文とポータルオブジェクトを使用したパラメータなしのParse、Bind、ポータル用Describe、Execute、Close、Syncの流れと同一です。 違いは、問い合わせ文字列内に複数のSQL文を受け付けられ、bind/describe/executeという流れがそれぞれが成功すれば自動的に行われる点です。 他の違いとして、ParseCompleteやBindComplete、CloseComplete、NoDataメッセージが返されない点があります。
拡張問い合わせプロトコルの利用により、パイプライン化(pipelining)が可能となります。 これは、先に送った問い合わせの完了を待つことなく一連の問い合わせを送るということです。 これにより、指定された操作を完了するためのネットワークのやり取りの回数が減ります。 しかし、ある段階が失敗した時に必要とされる振る舞いを、ユーザは注意深く検討しなければなりません。 それ以降の問い合わせはすでにサーバに送信中だからです。
これに対処するための一つの方法は、一連の問い合わせ全体を単一のトランザクションにすることです。
すなわち、BEGIN
... COMMIT
で囲みます。
しかし、あるコマンドを他のコマンドとは独立してコミットしたい時にはこの方法は役に立ちません。
拡張問い合わせプロトコルは、この問題に対応する別の方法を提供しています。
これは、お互いに依存するステップの間ではSyncメッセージを送るのを省略するというものです。
エラーが起こると、バックエンドはがyncが見つかるまでコマンドメッセージをスキップすることにより、それよりも前のコマンドが失敗した時に、クライアントが明示的にBEGIN
とCOMMIT
で管理することなく、パイプラインの中の後続のコマンドが自動的にスキップされるからです。
パイプラインの中の独立してコミットできる部分は、Syncメッセージで分けておくことができます。
クライアントが明示的なBEGIN
を発行しないと、先行のステップが成功した場合は各々のSyncは通常暗黙的なCOMMIT
、失敗した場合には暗黙的なROLLBACK
をもたらします。
しかし、トランザクションブロックの内側で実行できない少数のDDLコマンド(CREATE DATABASE
のような)があります。
これらのいずれかがパイプラインで実行されると、それがパイプラインの最初のコマンドでないかぎり失敗します。
さらに、成功した場合は、即時のコミットによってデータベース一貫性が保持されます。
したがって、これらのコマンドのいずれかの直後に続くSyncは、ReadyForQueryで応答することを除き、何の効果もありません。
この方法を使うときには、パイプラインの終了は、ReadyForQueryメッセージの数が送信したSyncメッセージの数と一致することで決定しなければなりません。 コマンドのうちいくつかはスキップされたかも知れず、その場合は完了メッセージを生成しないので、コマンド完了メッセージを数えるのは信頼性に欠けます。
関数呼び出しサブプロトコルにより、クライアントはデータベースのpg_proc
システムカタログに存在する任意の関数を直接呼び出す要求を行うことができます。
クライアントはその関数を実行する権限を持たなければなりません。
関数呼び出しサブプロトコルは、おそらく新しく作成するコードでは使用すべきではない古い機能です。
同様の結果は、SELECT function($1, ...)
を行うプリペアド文を設定することで得ることができます。
そして、この関数呼び出しサイクルをBind/Executeで置き換えることができます。
関数呼び出しサイクルはフロントエンドがFunctionCallメッセージをバックエンドに送ることで起動されます。 バックエンドは1つまたは複数の応答メッセージを関数呼び出しの結果に基づいて送り、最終的にReadyForQueryメッセージを送出します。 ReadyForQueryはフロントエンドに対し新規の問い合わせまたは関数呼び出しを行っても安全確実であることを通知します。
バックエンドから送信される可能性がある応答メッセージを以下に示します。
エラーが起こりました。
関数呼び出しが完了し、メッセージで与えられた結果が返されました。 (関数呼び出しプロトコルは単一のスカラ結果のみを扱うことができます。行型や結果集合を扱うことはできません。)
関数呼び出しの処理が終了しました。 処理が成功またはエラーで終了したかどうかにかかわらずReadyForQueryは常に送出されます。
関数呼び出しに関して警告メッセージが出されました。 警告メッセージは他の応答に対する追加のメッセージです。 したがって、バックエンドはそのコマンドの処理を続行します。
COPY
コマンドにより、サーバとの間で高速な大量データ転送を行うことができます。
コピーインとコピーアウト操作はそれぞれ接続を別のサブプロトコルに切り替えます。
これは操作が完了するまで残ります。
コピーインモード(サーバへのデータ転送)は、バックエンドがCOPY FROM STDIN
SQL文を実行した時に起動されます。
バックエンドはフロントエンドにCopyInResponseメッセージを送信します。
フロントエンドはその後、ゼロ個以上のCopyDataメッセージを送信し、入力データのストリームを形成します。
(このメッセージの境界は行の境界に何かしら合わせる必要ありませんが、往々にしてそれが合理的な選択となります。)
フロントエンドは、CopyDoneメッセージ(正常に終了させる)、もしくは、CopyFailメッセージ(COPY
SQL文をエラーで失敗させる)を送信することで、コピーインモードを終了させることができます。
そして、バックエンドは、COPY
が始まる前の、簡易もしくは拡張問い合わせプロトコルを使用するコマンド処理モードに戻ります。
そして次に、CommandComplete(成功時)またはErrorResponse(失敗時)のどちらかを送信します。
コピーインモードの期間中にバックエンドがエラー(CopyFailメッセージの受信を含む)を検知すると、バックエンドはErrorResponseメッセージを発行します。
拡張問い合わせメッセージ経由でCOPY
コマンドが発行された場合、バックエンドはSyncメッセージを受け取るまでフロントエンドのメッセージを破棄するようになります。
Syncメッセージを受け取ると、ReadyForQueryを発行し、通常処理に戻ります。
簡易問い合わせメッセージでCOPY
が発行された場合、メッセージの残りは破棄され、ReadyForQueryメッセージを発行します。
どちらの場合でも、その後にフロントエンドによって発行されたCopyData、CopyDone、CopyFailは単に削除されます。
バックエンドは、コピーインモード期間中、FlushとSyncメッセージを無視します。
その他の種類の非コピーメッセージを受け取ると、エラーになり、上述のようにコピーイン状態を中断します
(クライアントライブラリがExecuteメッセージの後に、実行したコマンドがCOPY FROM STDIN
かどうかを検査することなく、常にFlushまたはSyncを送信できる、という便利さのためにFlushとSyncは例外です)。
コピーアウトモード(サーバからのデータ転送)は、バックエンドがCOPY TO STDOUT
SQL文を実行した時に起動します。
バックエンドはCopyOutResponseメッセージをフロントエンドに送信し、その後、ゼロ個以上のCopyDataメッセージ(常に行ごとに1つ)を、そして、CopyDoneを送信します。
その後、バックエンドはCOPY
が始まる前のコマンド処理モードに戻り、CommandCompleteを送信します。
フロントエンドは(接続の切断やCancel要求の発行は例外ですが)転送を中断することはできません。
しかし、不要なCopyDataとCopyDoneメッセージを破棄することは可能です。
コピーアウトモード期間中バックエンドはエラーを検知すると、ErrorResponseメッセージを発行し、通常処理に戻ります。 フロントエンドはErrorResponseの受信をコピーアウトモードの終了として扱うべきです。
NoticeResponseおよびParameterStatusメッセージがCopyDataメッセージ間に散在することがあります。 フロントエンドはこのような場合も扱わなければなりません。 また、他の種類の非同期メッセージ(55.2.7を参照)も同様に準備すべきです。 さもなくば、CopyDataまたはCopyDone以外の種類のメッセージがコピーアウトモードの終了として扱われてしまう可能性があります。
他にも、サーバへ、およびサーバからの高速な一括データ転送を行うことができるコピーボースというコピーに関連したモードがあります。
コピーボースモードは、walsenderモードのバックエンドがSTART_REPLICATION
文を実行した時に初期化されます。
バックエンドはCopyBothResponseメッセージをフロントエンドに送信します。
この後バックエンドとフロントエンドの両方が、接続が終了するまでの間にCopyDataメッセージを送信できるようになります。
同様に、サーバがCopyDoneメッセージを送信した場合、接続はコピーインモードとなり、サーバはそれ以上のCopyDataメッセージを送信できません。
両方の側がCopyDoneメッセージを送信した後、コピーモードは終了し、バックエンドはコマンド処理モードに戻ります。
コピーボースモード中にバックエンドが検出したエラーのイベントにおいては、バックエンドはErrorResponseメッセージを発行し、Syncメッセージの受信までフロントエンドのメッセージを破棄し、その後ReadyForQueryを発行して通常の処理に戻ります。
フロントエンドは両方向のコピーを終了するように、ErrorResponse受理の処理をするべきです。
この場合CopyDoneを送信するべきではありません。
コピーボースモードにおけるサブプロトコル転送の詳細は55.4を参照してください。
CopyInResponse、CopyOutResponse、CopyBothResponseメッセージには、フロントエンドに1行当たりの列数と各列で使用する書式コードを通知するためのフィールドが含まれています。
(現在の実装では、COPY
操作で与えられるすべての列は同一の書式を使用します。
しかし、メッセージ設計においては、これを前提としていません。)
バックエンドが、フロントエンドのコマンドストリームで特に依頼されていないメッセージを送信する場合が複数あります。 フロントエンドは、問い合わせ作業を行っていない時であっても常に、これらのメッセージを扱う準備をしなければなりません。 少なくとも、問い合わせの応答の読み込みを始める前にこれらを検査すべきです。
外部の活動によって、NoticeResponseメッセージが生成されることがあり得ます。 例えば、データベース管理者が「高速」データベース停止コマンドを実行した場合、バックエンドは接続を閉ざす前にこれを通知するためにNoticeResponseを送信します。 したがって、たとえ接続が名目上待機状態であったとしても、フロントエンドは常にNoticeResponseメッセージを受け付け、表示する準備をすべきです。
ParameterStatusメッセージは、任意のパラメータの実際の値が変更され、それをバックエンドがフロントエンドに通知するべきであるとみなした場合は常に生成されます。
ほとんどの場合、これはフロントエンドによるSET
SQLコマンド実行に対する応答の中で起こります。
これは実質的には同期していますが、管理者が設定ファイルを変更し、SIGHUPシグナルをサーバに送った場合にも、パラメータ状態の変更が発生することがあります。
また、SET
コマンドがロールバックされた場合、現在の有効値を報告するために適切なParameterStatusメッセージが生成されます。
現時点では、ParameterStatusを生成するパラメータ群は固定されています。 それらは次の通りです。
application_name | is_superuser |
client_encoding | scram_iterations |
DateStyle | server_encoding |
default_transaction_read_only | server_version |
in_hot_standby | session_authorization |
integer_datetimes | standard_conforming_strings |
IntervalStyle | TimeZone |
(8.0より前までのリリースでは、server_encoding
、TimeZone
およびinteger_datetimes
は送信されませんでした。
8.1より前までのリリースでは、standard_conforming_strings
は送信されませんでした。
8.4より前のリリースでは、IntervalStyle
は送信されませんでした。
9.0より前のリリースでは、application_name
は送信されませんでした。
14より前のリリースでは、default_transaction_read_only
とin_hot_standby
は送信されませんでした。
16より前のリリースでは、scram_iterations
は送信されませんでした。)
server_version
、server_encoding
およびinteger_datetimes
は仮想的なパラメータであり、起動後に変更できないことに注意してください。
これは今後変更される、あるいは設定変更可能になる可能性があります。
したがって、フロントエンドは未知または注目していないParameterStatusを単に無視すべきです。
フロントエンドがLISTEN
コマンドを発行した場合、同じチャネル名に対しNOTIFY
コマンドが実行された時にバックエンドはNotificationResponseメッセージ(NoticeResponseと間違えないように!)を送出します。
現在、NotificationResponseをトランザクションの外部でのみ送信することができます。 このため、これはコマンド応答の流れの途中では起こりませんが、ReadyForQueryの直前に発生する可能性があります。 しかし、これを前提にフロントエンドのロジックを設計することは避けてください。 プロトコル内の任意の時点でNotificationResponseを受け付けられるようにすることを勧めます。
問い合わせの処理中に、フロントエンドが問い合わせを取り消す可能性があります。 取り消し要求は、効率を高めるために、接続を開いたバックエンドに対して直接送信されません。 その問い合わせを処理中のバックエンドが、フロントエンドからの新しい入力があるかどうかを定期的に確認することは好ましくありません。 取り消し要求はたいていの場合、頻繁には起こらないので、通常の状態においての負担を避けるため、多少扱いにくくなっています。
取り消し要求を出す場合、フロントエンドは通常の新規接続の時に送出されるStartupMessageメッセージではなくCancelRequestメッセージをサーバに送り、新規接続を開始します。 サーバはこの要求を処理し、接続を切断します。 セキュリティ上の理由から、取り消し要求メッセージに対し直接の回答はありません。
CancelRequestメッセージは、接続開始段階でフロントエンドに送られたものと同一の鍵データ(PIDと秘密鍵)を含んでいない場合は無視されます。 現在バックエンドが実行中の処理に対するPIDと秘密鍵が要求と一致した場合、その現在の問い合わせ処理は中断されます。 (現状では、これは、その問い合わせを処理しているバックエンドプロセスに対し特別なシグナルを送ることで実装されています。)
この取り消しシグナルは何の効果も生まないことがあります。 例えば、バックエンドが問い合わせの処理を完了した後に届いた場合、効果がありません。 もし取り消し処理が有効であれば、エラーメッセージとともに、現在のコマンドは終了されます。
セキュリティと効率上の理由による上述の実装の結果、フロントエンドは取り消し要求が成功したかどうかを直接判断することはできません。 フロントエンドはバックエンドからの問い合わせの回答を待ち続けなければいけません。 取り消しを要求することは単に現在の問い合わせを早めに終わらせ、成功ではなくエラーメッセージを出して不成功とする可能性を単に高める程度のものです。
取り消し要求は、通常のフロントエンドとバックエンドの通信接続を通してではなく新規のサーバとの接続に送られるため、取り消される問い合わせを実行したフロントエンドだけでなく、任意のプロセスによっても送信することができます。 このことはマルチプロセスアプリケーションを作るに当たって柔軟性を提供します。 同時に、権限のない者が問い合わせを取り消そうとするといったセキュリティ上のリスクも持ち込みます。 このセキュリティ上のリスクは、取り消し要求内に動的に生成される秘密キーを供給することを必須とすることで回避されます。
通常の洗練された終了手順はフロントエンドがTerminateメッセージを出し、すぐに接続を閉じることです。 このメッセージを受け取るとすぐにバックエンドは接続を閉じ終了します。
まれに(管理者によるデータベース停止コマンドなど)、バックエンドはフロントエンドからの要求なしに切断することがあります。 こうした場合、バックエンドは、接続を閉ざす前に切断理由を通知するエラーまたは警報メッセージの送信を試みます。
他にも、どちらかの側のコアダンプ、通信リンクの消失、メッセージ境界の同期の消失など各種失敗によって終了する状況があります。 フロントエンドかバックエンドいずれかが予期しない接続の中断を検知した場合、後始末を行い終了しなければいけません。 フロントエンドはもし自身が終了を望まない場合、サーバに再交信し新規のバックエンドを立ち上げる選択権を持っています。 解釈できないメッセージ種類を受け取った時、おそらくメッセージ境界の同期が消失したことを示しますので、接続を閉ざすことを勧めます。
通常の終了、異常な終了のどちらの場合でも、開いているトランザクションはすべてコミットされずにロールバックされます。
しかし、フロントエンドがSELECT
以外の問い合わせを処理中に切断した場合、バックエンドはおそらく切断に気付く前にその問い合わせを完了させてしまうでしょう。
その問い合わせがトランザクションブロック(BEGIN
... COMMIT
の並び)外部であった場合、切断に気付く前にその結果はコミットされる可能性があります。
PostgreSQLがSSLサポート付きで構築された場合、フロントエンドとバックエンド間の通信をSSLで暗号化することができます。 攻撃者がセッショントラフィックをキャプチャできるような環境における通信を安全にすることができます。 SSLを使用したPostgreSQLセッションの暗号化に関する詳細は19.9を参照してください。
SSL暗号化接続を開始するには、フロントエンドはまず、StartupMessageではなくSSLRequestを送信します。
その後サーバはそれぞれSSLの実行を行うか行わないかを示すS
かN
かを持つ1バイトの応答を返します。
フロントエンドはその応答に満足できなければ、この時点で接続を切断することができます。
S
の後に継続するのであれば、サーバと間でSSL起動ハンドシェーク(ここではSSLの仕様に関しては説明しません)を行います。
これが成功した場合、続いて通常のStartupMessageの送信を行います。
この場合、StartupMessageおよびその後のデータはSSLにより暗号化されます。
N
の後に、通常のStartupMessageを送信することで暗号化なしで進みます。
(他に、SSLの代わりにGSSAPI暗号化の利用を試行するために、N
応答の後にGSSENCRequestメッセージを送信することが認められています。)
また、フロントエンドはサーバからのSSLRequestに対するErrorMessage応答を取り扱うための用意もすべきです。 これは、PostgreSQLにSSLサポートが追加される前のサーバの場合のみで発生します。 (現在ではこうしたサーバは非常に古いものといえ、ほとんど存在しません。) この場合接続を切断しなければなりませんが、フロントエンドはSSL要求なしで新しく接続を開き、処理を進めることもできます。
SSL暗号化が実行可能なら、サーバはS
バイトだけを送信し、フロントエンドがSSLハンドシェイクを開始するのを待つことが期待されます。
この段階でそれ以上のバイトが読み取り可能であるなら、中間者がバッファスタッフィング攻撃(CVE-2021-23222)を開始しようとしている可能性が高いです。
フロントエンドは、ソケットをSSLライブラリに渡す前に正確に1バイトだけをソケットから読み出すべきです。
でなければ、追加のバイトが読み取られた場合には、プロトコル違反として扱うべきです。
最初のSSLRequestはCancelRequestメッセージを送信するために開いた接続で使用することもできます。
プロトコル自体には、サーバにSSL暗号化を強制する方法は用意されていませんが、管理者は認証検査の一方法として、暗号化されていないセッションを拒否するようにサーバを設定することができます。
PostgreSQLがGSSAPIサポートを有効にして構築されていれば、GSSAPIを使ってフロントエンド/バックエンド通信を暗号化できます。 これにより、攻撃者がセッションのやり取りを読み取れるかもしれない環境における通信のセキュリティが提供されます。 PostgreSQLでの通信をGSSAPIで暗号化するための情報に関しては、19.10をご覧ください。
GSSAPI暗号化接続を開始するには、フロントエンドは最初にStartupMessageではなく、GSSENCRequestメッセージを送ります。
次にサーバは、それぞれGSSAPI暗号化を希望する、しないを意味するG
またはN
を含む1バイトを送信します。
このレスポンスに満足できなければ、この時点でフロントエンドは接続を打ち切って構いません。
G
の受信後継続するには、RFC 2744あるいは同様の文書で説明されているGSSAPI Cバインディングを使い、ループの中でgss_init_sec_context()
を呼び出してGSSAPIを初期化し、結果をサーバに送信し、空の入力を受け取ることから始めて、サーバが出力を返さなくなるまでサーバからの出力を受け取ります。
gss_init_sec_context()
の結果をサーバに送る際には、ネットワークバイトオーダーで4バイトの整数にメッセージ長を先頭に付与します。
N
の後継続するには、通常のStartupMessageを送信し、暗号化せずに続けてください。
(他に、GSSAPIの代わりにSSL暗号化の使用を試行するために、N
応答の後にSSLRequestメッセージを送信することが認められています。)
また、フロントエンドはサーバからのGSSENCRequestへのErrorMessage応答に備えるべきです。 これはサーバがPostgreSQLへのGSSAPI暗号化サポートを追加する以前だったときにのみ発生します。 この場合は接続を切断しなければなりませんが、フロントエンドは新しい接続を開いてGSSAPI暗号化を要求せずに進めることを選択するかもしれません。
GSSAPI暗号化が実行可能なら、サーバはG
バイトだけを送信し、フロントエンドがGSSAPIハンドシェイクを開始するのを待つことが期待されます。
この段階でそれ以上のバイトが読み取り可能であるなら、中間者がバッファスタッフィング攻撃(CVE-2021-23222)を開始しようとしている可能性が高いです。
フロントエンドは、ソケットをGSSAPIライブラリに渡す前に正確に1バイトだけをソケットから読み出すべきです。
でなければ、追加のバイトが読み取られた場合には、プロトコル違反として扱うべきです。
最初のGSSENCRequestは、CancelRequestメッセージを送信するために開いている接続でも利用できます。
GSSAPI暗号化の確立に成功したら、gss_wrap()
を使って通常のStartupMessageと後続のすべてのメッセージを暗号化します。
実際に暗号化した送信データの前に、gss_wrap()
の結果をネットワークバイトオーダーで4バイトの整数にしたものを付与します。
サーバは16kB未満のクライアントからの暗号化パケットだけを受け付けることに注意してください。
クライアントはgss_wrap_size_limit()
を使って暗号化前のメッセージの大きさがこの制限に収まるかどうかを確認し、それより大きなメッセージは複数のgss_wrap()
呼び出しに分解すべきです。
典型的なセグメントは暗号化前で8kBのデータで、暗号化後のパケットは8kBより少し大きくなりますが、最大長の16kB以内には問題なく収まります。
サーバは16kBよりも大きな暗号化パケットをクライアントに送らないものと期待することができます。
プロトコル自身はサーバにGSSAPI暗号化を強制する方法を提供していませんが、管理者は認証チェックの副次的効果として暗号化されていないセッションをサーバが拒否するように設定できます。