PostgreSQL接続時のタイムゾーン設定を必要な場合のみ発行するよう改善
#57070のフォローアップとして、PostgreSQLアダプターにおけるGUC名の大文字小文字処理を修正し、TimeZoneなど大文字を含む設定値のparameter_statusチェックが正しく機能するようになりました。
背景
#57070では、PostgreSQLのconfigure_connectionをリファクタリングし、全セッション設定を一つのループで処理するよう統合しました。この実装の核心は、parameter_statusを参照してサーバーがすでに目的の値を持っている場合にSETコマンドをスキップする最適化です。
しかしparameter_statusはケースセンシティブであり、PostgreSQLが報告するGUC名の正確な表記と一致しなければ検索がヒットしません。たとえばIntervalStyleをキーとしてsettingsハッシュに格納していた場合、parameter_statusハッシュ側のキーがIntervalStyleと一致していないと、常にSETコマンドが発行されてしまいます。TimeZoneも同様に、ユーザーがvariables:から独自のタイムゾーンを指定した場合、Railsのデフォルト設定と重複してSETが発行される問題がありました。
さらに、ユーザーがvariables:で指定するキーはケースを問わず受け入れる必要がある一方、内部での重複排除はケースを無視して行う必要があるという、ケース処理の二重要件が存在します。
技術的な変更
PostgreSQLアダプターにはCANONICAL_GUC_NAMESという定数と、variables:キーの正規化ロジックが追加されました。これにより、parameter_statusチェックに必要な正確なGUC名を保持しつつ、ユーザー指定の変数との重複排除をケースインセンシティブに行えるようになっています。
CANONICAL_GUC_NAMES定数の追加:
# Canonical spellings for PostgreSQL GUC names that use non-lowercase
# casing. parameter_status is case-sensitive, so we need the exact
# name the server reports.
CANONICAL_GUC_NAMES = {
"datestyle" => "DateStyle",
"intervalstyle" => "IntervalStyle",
"timezone" => "TimeZone",
}.freeze
private_constant :CANONICAL_GUC_NAMES
CANONICAL_GUC_NAMESは小文字キーから正式なGUC名へのマッピングを保持します。parameter_statusはサーバーが報告する表記(例: TimeZone、IntervalStyle)をキーとして使うため、SETコマンドをスキップするには完全一致が必要です。
configure_connection内の変数処理変更:
# 変更前
settings["IntervalStyle"] = "iso_8601" unless @config[:intervalstyle] == false
# 変更後
settings["intervalstyle"] = "iso_8601" unless @config[:intervalstyle] == false
Railsが内部でビルドするsettingsハッシュのキーは小文字に統一されました。これにより、@config[:variables]から来るユーザー指定キーとの重複排除を小文字ベースで行えます。original_variable_namesという補助ハッシュがCANONICAL_GUC_NAMESのコピーとして初期化され、ユーザーが大文字を含むキー(例: "my_app.SomeOption")を指定した場合にその元の表記が保持されます。
original_variable_names = CANONICAL_GUC_NAMES.dup
@config.fetch(:variables, {}).stringify_keys.each do |k, v|
lower = k.dup
original_variable_names[lower] ||= k if lower.downcase!
case v
when :default, ":default"
# Server default; remove any Rails-default we've set above
settings.delete(lower)
lower.downcase!がnilを返す(すでに小文字の場合)ときはoriginal_variable_namesへの追記をスキップし、大文字が含まれる場合のみユーザーの元表記を記録します。settingsから削除する際も小文字キーで検索するため、ユーザーがvariables: { TimeZone: :default }と書いてRailsの"timezone"設定を上書きできます。
テストにも大きな追加がありました。サーバー設定に依存しないよう既知のGUCデフォルト値を強制するKNOWN_SERVER_DEFAULTS定数が導入され、接続時のクエリ数アサーションを決定論的に検証できるようになっています。
KNOWN_SERVER_DEFAULTS = "-c TimeZone=US/Eastern -c IntervalStyle=postgres -c standard_conforming_strings=on -c client_min_messages=notice"
TimeZoneをUTC以外(US/Eastern)に設定することで、:utcと:localの区別がテストで明確に確認できます。
設計判断
小文字をsettingsの正規キーとし、正式GUC名はoriginal_variable_namesで別管理する二層構造が採用されました。
settingsハッシュへのアクセスはすべて小文字キーで統一することで、variables:からのユーザー入力との重複チェックをケースインセンシティブに実現します。一方、実際にSETコマンドを発行する際はoriginal_variable_namesから正式なGUC名を取り出してサーバーに送ります。これにより、parameter_statusとの照合に必要な正確な大文字小文字を保ちながら、ユーザーが意図した変数名(my_app.SomeOptionのようなカスタム名前空間)もそのまま尊重できます。
また、CANONICAL_GUC_NAMESをprivate_constantとして定義することで、このGUC名マッピングが内部実装の詳細であることを明示しています。将来PostgreSQLが新たな大文字GUC名を追加した場合でも、この定数にエントリを追加するだけで対応できます。
まとめ
この変更は、parameter_statusチェックによるSETコマンドのスキップ最適化をTimeZoneなどの大文字GUC名に対しても正しく機能させる修正です。内部キーの小文字統一とCANONICAL_GUC_NAMESによる正式名管理を組み合わせることで、ユーザーがvariables:で指定したタイムゾーンが既にサーバーに設定済みであれば余分なSETクエリが発行されない、という#57070の設計意図が完全に実現されています。