kakudooo docs

schema.rb の enable_extension について

仕事で Rails のバージョンを 7 から 8 に上げる作業をしていたところ、db/schema.rbenable_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.rbenable_extensionについても調べたので、まとめておくことにする。

schema.rb

データベーススキーマの最新状態のキャプチャで、db/schema.rbとして管理される。データベースの作成時に使用する。 migration ファイルの履歴から構築するより、rails db:schema:loadでスキーマファイルを読み込んで構築する方がより速く、エラーも発生しにくいため、安定した方法とされている。 migration ファイルは、実行後に変更される可能性がある。(例えば、特定の環境で実行した後に migration ファイルだけ削除するなど) そのため、migration ファイルの履歴から現在のデータベースの状態を完全に再現することが難しいこともある。

db/schema.rbrails db:migrate時に毎回作成および更新される。スキーマのフォーマットは:rubyor:sqlのいずれかを選択できる。(デフォルトは:ruby)

db:schema:load 時の挙動

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
...

enable_extension

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

enable_extension の今

Rails 7.1 から

それまで 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
...

Rails 8.0 から

PR: #46894では、migration ファイルの実行時に schema を明示することが可能になったが、schema dumper によるschema.rbの作成までは考慮されていなかった。

schema dumper からも schema の指定を書き出すべきということで、出された Issue と PR は以下。

前述したように、この修正によって schema dumper は、データベーススキーマからenable_extensionに schema の情報も含めるようになった。

https://github.com/rails/rails/blob/d3d17c297a70d3e255a34693a1fb0074712e9930/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L515

...
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 のサンプルコードが更新されていない

Rails Guide (v8.0)を確認してもenable_extension "plpgsql"のままなので、出た差分が意図されたものかを確認する必要があった。

については、出た差分が意図されたものであると分かった。

plpgsqlpg_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.

参考