PostgreSQL の `RESET` コマンドを読み取り専用コンテキストで実行可能に
PostgreSQL アダプターの READ_QUERY 正規表現に :reset を追加することで、prevent_writes: true の読み取り専用コンテキスト内でも RESET コマンドを実行できるようになりました。
背景
prevent_writes: true の読み取り専用コンテキストでは、ActiveRecord は書き込みクエリの検出に READ_QUERY 正規表現を使用し、マッチしないクエリを ActiveRecord::ReadOnlyError として拒否します。RESET コマンドはこの正規表現に含まれていなかったため、読み取り専用コンテキスト内で実行すると常にエラーが発生していました。
PostgreSQL の RESET は SET configuration_parameter TO DEFAULT の構文的ショートカットであり、セッション内で一時的に変更したパラメータを元の値に戻す操作です。たとえば SET statement_timeout = '7s' で設定したタイムアウトを RESET statement_timeout で解除するといった用途が典型例です。このようなセッション管理操作は実際にはデータを変更せず、既存の :set と同様に読み取り安全なクエリとして扱われるべきでした。
その結果、読み取り専用ロールに接続しつつセッション変数を管理するというユースケースが、RESET の実行時に ActiveRecord::ReadOnlyError によってブロックされていました。
技術的な変更
変更は activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb の1行のみです。READ_QUERY 定数のキーワードリストに :reset を追加しています。
変更前:
READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
:close, :declare, :fetch, :move, :set, :show
) # :nodoc:
変更後:
READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
:close, :declare, :fetch, :move, :set, :show, :reset
) # :nodoc:
これにより、以下のパターンが読み取り専用コンテキストでエラーなく動作するようになります。
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
ActiveRecord::Base.with_connection do |c|
c.execute("SET statement_timeout = '7s'")
# some queries
c.execute("RESET statement_timeout")
# => no longer raises ActiveRecord::ReadOnlyError
end
end
テストは activerecord/test/cases/adapters/postgresql/postgresql_adapter_prevent_writes_test.rb に追加されており、既存の test_doesnt_error_when_a_set_query_is_called_while_preventing_writes に倣った構造になっています。また、既存のテストに ensure ブロックで @connection.close を追加し、接続の後処理を保証する修正も含まれています。
設計判断
READ_QUERY キーワードリストへの追記という最小限の修正が採用されています。PostgreSQL アダプター固有のリストに追加されているのは、RESET が PostgreSQL 固有の構文であるためです。他のデータベースアダプターの READ_QUERY は変更されていません。
:set が既にリストに含まれていたという先例が、:reset を同じ扱いにする根拠を明確にしています。SET ... TO DEFAULT と等価なコマンドを異なる扱いにする理由がなく、セッション設定の変更はデータの永続的な書き込みとは異なるという分類がここでも一貫して適用されています。
まとめ
1行の追加により、PostgreSQL のセッション管理操作(SET と対をなす RESET)が読み取り専用コンテキストで一貫して使えるようになりました。ステートメントタイムアウトなどのセッション変数を管理しながらリードレプリカに接続するユースケースで、不要なエラーを回避できます。