仕事で Rails のバージョンを 7 から 8 に上げる作業をしていたところ、db/schema.rbのenable_extensionに差分が出ていることに気づいた。
以下のような差分。
→ Rails7
enable_extension "plpgsql"
→ Rails8
enable_extension "pg_catalog.plpgsql"
Rails Guide (v8.0)を確認してもenable_extension "plpgsql"のままなので、出た差分が意図されたものかを確認する必要があった。
調べたところ、どうやらPR: #52313で schema dumper に以下のような修正が入っている。
migration ファイルのenable_extensionにスキーマが指定されている場合、schema.rbも同じくスキーマが明示的に指定される。
→ migration ファイル
enable_extension "heroku_ext.pgcrypto"
enable_extension "pg_stat_statements"
→ schema.rb
enable_extension "heroku_ext.pgcrypto"
enable_extension "pg_stat_statements"
詳しくはchangelogを参照
上記のようなことを調査する過程で、schema.rbやenable_extensionについても調べたので、まとめておくことにする。
データベーススキーマの最新状態のキャプチャで、db/schema.rbとして管理される。データベースの作成時に使用する。 migration ファイルの履歴から構築するより、rails db:schema:loadでスキーマファイルを読み込んで構築する方がより速く、エラーも発生しにくいため、安定した方法とされている。
migration ファイルは、実行後に変更される可能性がある。(例えば、特定の環境で実行した後に migration ファイルだけ削除するなど)
そのため、migration ファイルの履歴から現在のデータベースの状態を完全に再現することが難しいこともある。
db/schema.rbはrails db:migrate時に毎回作成および更新される。スキーマのフォーマットは:rubyor:sqlのいずれかを選択できる。(デフォルトは:ruby)
load_schema_currentメソッドが呼び出される。
rails/activerecord/lib/active_record/railties /databases.rake
...
desc "Load a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `config.active_record.schema_format`) into the database"
task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(ENV["SCHEMA_FORMAT"], ENV["SCHEMA"])
end
...
load_schema_currentメソッドが呼び出される。
rails/activerecord/lib/active_record/tasks /database_tasks.rb
...
def load_schema_current(format = nil, file = nil, environment = env)
each_current_configuration(environment) do |db_config|
with_temporary_connection(db_config) do
load_schema(db_config, format || db_config.schema_format, file)
end
end
end
...
load_schemaメソッドが呼び出し、db/schema.rbが実行される。(format が:ruby の場合)
rails/activerecord/lib/active_record/tasks /database_tasks.rb
...
def load_schema(db_config, format = db_config.schema_format, file = nil) # :nodoc:
format = format.to_sym
file ||= schema_dump_path(db_config, format)
return unless file
verbose_was, Migration.verbose = Migration.verbose, verbose? && ENV["VERBOSE"]
check_schema_file(file)
case format
when :ruby
load(file) # db/schema.rbを読み込んで実行
when :sql
structure_load(db_config, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
migration_connection_pool.internal_metadata.create_table_and_set_flags(db_config.env_name, schema_sha1(file))
ensure
Migration.verbose = verbose_was
end
...
PostgreSQL を DB として使用している前提で、PostgreSQL クラスタにインストールされている任意の extension を使用しているデータベースで有効化するためのもの。
たとえば、postgisを extension として導入する場合は、以下のように migration ファイルに記述する。
rails generate migration AddPostgisExtensionToDatabase
class AddPostgisExtensionToDatabase < ActiveRecord::Migration[7.2]
def change
enable_extension 'postgis'
end
end
enable_extensionは内部でCREATE EXTENSION IF NOT EXISTS...を実行している。
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L487
def enable_extension(name, **)
schema, name = name.to_s.split(".").values_at(-2, -1)
sql = +"CREATE EXTENSION IF NOT EXISTS \"#{name}\"" # extensionがなければ作成する
sql << " SCHEMA #{schema}" if schema # schemaが指定されている場合は明示する
internal_exec_query(sql).tap { reload_type_map }
end
それまで migration ファイルのenable_extensionは、schema を指定できなかった。current_schemaに設定された schema が使用されていた。(デフォルトはpublic)
PR: #46894で schema の明示的な指定に対応された。(extension が current_schema 以外のスキーマに存在する場合、スキーマを明示しないと期待どおりの拡張が有効化されない可能性があるため)
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L487
def enable_extension(name, **)
schema, name = name.to_s.split(".").values_at(-2, -1)
sql = +"CREATE EXTENSION IF NOT EXISTS \"#{name}\""
sql << " SCHEMA #{schema}" if schema # ← schemaが指定されている場合は明示する
exec_query(sql).tap { reload_type_map }
end
...
def change
enable_extension 'heroku_ext.hstore'
end
...
PR: #46894では、migration ファイルの実行時に schema を明示することが可能になったが、schema dumper によるschema.rbの作成までは考慮されていなかった。
schema dumper からも schema の指定を書き出すべきということで、出された Issue と PR は以下。
前述したように、この修正によって schema dumper は、データベーススキーマからenable_extensionに schema の情報も含めるようになった。
...
def extensions
query = <<~SQL
SELECT
pg_extension.extname,
n.nspname AS schema
FROM pg_extension
JOIN pg_namespace n ON pg_extension.extnamespace = n.oid
SQL
internal_exec_query(query, "SCHEMA", allow_retry: true, materialize_transactions: false).cast_values.map do |row|
name, schema = row[0], row[1]
schema = nil if schema == current_schema # current_schemaと異なるschemaが指定されている場合は、schemaを含める
[schema, name].compact.join(".")
end
end
...
Rails Guide (v8.0)を確認しても
enable_extension "plpgsql"のままなので、出た差分が意図されたものかを確認する必要があった。
については、出た差分が意図されたものであると分かった。
plpgsqlはpg_catalogスキーマに存在している。
以下は、PostgreSQL(v16)で確認。
select * from pg_available_extension_versions where name = 'plpgsql';
|name |version|installed|superuser|trusted|relocatable|schema |requires|comment |
|-------|-------|---------|---------|-------|-----------|----------|--------|----------------------------|
|plpgsql|1.0 |true |true |true |false |pg_catalog| |PL/pgSQL procedural language|
Rails Guide (v8.0)以降のサンプルコードは、enable_extension "pg_catalog.plpgsql"であるべきなので、PR を作成することにした。
[ci skip] Add extension schema to the db/schema.rb example.