[Rails] SQLite3のスキーマダンプでAUTOINCREMENTなし整数主キーを正しく復元可能に
背景
RailsでSQLite3を使用する際、create_table :payments, id: :integer, default: nil のように非AUTOINCREMENT整数主キーを持つテーブルを作成すると、スキーマダンパーが default: nil オプションを無視していました。その結果、db/schema.rb から復元されたテーブルは意図せずAUTOINCREMENT属性を持ってしまい、元のテーブル定義と異なる挙動を示す問題がありました。
この問題は #56485 で報告されており、本PRで修正されました。
問題の詳細
元のマイグレーション
create_table :payments, id: :integer, default: nil
このマイグレーションは以下のSQLを生成します:
CREATE TABLE "payments" ("id" integer NOT NULL PRIMARY KEY)
注目すべきは、AUTOINCREMENTキーワードが含まれていない点です。SQLite3では、主キーにAUTOINCREMENTを明示的に指定しない場合、再利用可能なROWIDが使用されます。
スキーマダンプの問題(修正前)
修正前のスキーマダンパーは以下のような出力を生成していました:
create_table "payments", force: :cascade do |t|
end
id: :integer, default: nil が欠落しているため、このスキーマから復元すると:
CREATE TABLE "payments" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL)
AUTOINCREMENTが誤って追加され、元のテーブル定義と挙動が変わってしまいます。
技術的な解決策
本PRでは、ActiveRecord::ConnectionAdapters::SQLite3::SchemaDumper に以下の変更を加えています。
1. 主キーがAUTOINCREMENTを持つかの判定メソッドを追加
def primary_key_has_autoincrement?
return false unless table_name
table_sql = @connection.query_value(<<~SQL, "SCHEMA")
SELECT sql FROM sqlite_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
UNION ALL
SELECT sql FROM sqlite_temp_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
SQL
table_sql.to_s.match?(/\bAUTOINCREMENT\b/i)
end
このメソッドは sqlite_master テーブルを直接クエリし、テーブル作成時のCREATE TABLE文を取得します。その文字列内にAUTOINCREMENTキーワードが含まれているかを正規表現で判定します。
技術的な注目点:
- sqlite_master と sqlite_temp_master の両方をクエリすることで、通常テーブルと一時テーブルの両方に対応
- 単語境界 \b を使用することで、部分一致を避けて正確な判定を実現
- 大文字小文字を区別しない /i フラグで柔軟な検出
2. デフォルト主キー判定ロジックの修正
def default_primary_key?(column)
schema_type(column) == :integer && primary_key_has_autoincrement?
end
従来は単に schema_type(column) == :integer だけで判定していましたが、AUTOINCREMENTの有無も考慮するように変更されました。
3. 明示的なデフォルト値が必要なケースの判定
def explicit_primary_key_default?(column)
column.bigint? || (column.type == :integer && !primary_key_has_autoincrement?)
end
このメソッドは、スキーマダンプ時に default: nil を明示的に出力すべきかを判定します。整数型の主キーでAUTOINCREMENTがない場合、default: nil を出力するようになりました。
修正後の挙動
修正後のスキーマダンプは以下のように出力されます:
create_table "payments", id: :integer, default: nil, force: :cascade do |t|
end
id: :integer, default: nil が正しく含まれるため、このスキーマから復元されるテーブルは元の定義と一致します。
テストの改善
def test_schema_dump_primary_key_integer_with_default_nil
# skip if current_adapter?(:SQLite3Adapter) # この行が削除された
@connection.create_table(:int_defaults, id: :integer, default: nil, force: true)
schema = dump_table_schema "int_defaults"
assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema
end
既存のテストケースからSQLite3アダプター用のスキップ処理が削除されました。これにより、この問題が修正されたことが自動テストで継続的に検証されます。
まとめ
本PRは、SQLite3における非AUTOINCREMENT整数主キーのスキーマダンプ問題を、sqlite_master への直接クエリという実用的なアプローチで解決しています。この修正により、db/schema.rb を通じたデータベース復元の忠実性が向上し、特にROWIDの再利用挙動に依存するアプリケーションで重要な改善となります。
SQLite3のメタデータテーブルを活用した判定ロジックは、カラム情報だけでは取得できないテーブル作成時のオプションを正確に検出する優れた手法です。