t.referencesで何ができるのか。create_table内で外部キー設定するときに使うreferencesやforeign_keyオプションについてのコードリーディング

# 背景
create_table実行時に毎回仕様を調べに行くので備忘録。
規約通りの外部キーの作り方(usersテーブル参照時に`user_id`)以外に柔軟なことができるか調べに行っている。
ドキュメントに`create_table`自体の説明は書いてあってもブロック内で設定できることの仕様が書いていないため。

間違った情報だったらすいません。繋がりが不確定な箇所があるため少し信頼性は低いです。
誤っている場合致命的に誤っていると思うので、その場合この記事は削除or訂正します。

# 結論
規約を超えた外部キーの設定は`create_table`ブロック内では難しそう。

- 外部キーを`(外部キーを設定するテーブル名)_id`に設定するコードが[ハードコーディング](https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L214)されているため、オプションなどで柔軟に変えることはできなさそう
- `foreign_key`は条件分岐の際の条件に使われるくらいなので、booleanで設定できるくらい
    - 「◯◯のテーブルと主キー以外の××カラムを関連付けたい」といった柔軟な設定はできない模様。
- 柔軟な外部キー設定したいなら`add_foreign_key`とかを`create_table`の外でやるのが良さそう。[ドキュメント](https://railsdoc.com/page/add_foreign_key)

# 調査

ドキュメント
https://railsdoc.com/page/create_table

↓
ドキュメントから飛べるGithubのコード
https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L294
ブロック部分の`yield`と`yield`の引数の`td`部分を抜粋

```activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
module ActiveRecord
  module ConnectionAdapters # :nodoc:
    module SchemaStatements
      ...

      def create_table(table_name, **options)
        td = create_table_definition(table_name, options)

        ...

        yield td if block_given?

        ...

        result = execute schema_creation.accept td

        unless supports_indexes_in_create?
          td.indexes.each do |column_name, index_options|
            add_index(table_name, column_name, index_options)
          end
        end

        if supports_comments? && !supports_comments_in_create?
          if table_comment = options[:comment].presence
            change_table_comment(table_name, table_comment)
          end

          td.columns.each do |column|
            change_column_comment(table_name, column.name, column.comment) if column.comment.present?
          end
        end

        result
      end
```

いつも`t.string`とかやってるのは`td`に対してであり、このファイルでごにょごにょしてるのではなくtdで色々やってるようなのでtdを見に行く

```activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
        def create_table_definition(*args)
          TableDefinition.new(self, *args)
        end
```

`TableDefinition`をgrepかけても確実にこれだといえそうなやつが見つからなかったけど、create_tableのmoduleである`ConnectionAdapters`でくくられていたのが
以下だったので、確実性はないがこれを見ていく
https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L289

※他は`rails/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb`([参考](https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb#L60))などMySQLやPostgreSQLに特化したものが多く、それらも↑を継承している。恐らく↑が一番候補として可能性が高い。フォークしてsuperclassとかするのが早いんでしょうが。

```activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
module ActiveRecord
  module ConnectionAdapters #:nodoc:
    ...

    class TableDefinition
      ...

      def initialize(
      ...

      def references(*args, **options)
        args.each do |ref_name|
          ReferenceDefinition.new(ref_name, **options).add_to(self)
        end
      end
```

ReferenceDefinitionを見よう
https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L145

```activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
module ActiveRecord
  module ConnectionAdapters #:nodoc:
    ...

    class ReferenceDefinition # :nodoc:
      def initialize(
        name,
        polymorphic: false,
        index: true,
        foreign_key: false,
        type: :bigint,
        **options
      )
        @name = name
        @polymorphic = polymorphic
        @index = index
        @foreign_key = foreign_key
        @type = type
        @options = options

        if polymorphic && foreign_key
          raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
        end
      end

      def add_to(table)
        columns.each do |name, type, options|
          table.column(name, type, **options)
        end

        if index
          table.index(column_names, **index_options(table.name))
        end

        if foreign_key
          table.foreign_key(foreign_table_name, **foreign_key_options)
        end
      end

      ...
        def foreign_key_options
          as_options(foreign_key).merge(column: column_name)
        end

        ...

        def column_name
          "#{name}_id"
        end

        ...

        def foreign_table_name
          foreign_key_options.fetch(:to_table) do
            Base.pluralize_table_names ? name.to_s.pluralize : name
          end
        end
```

`table.foreign_key`を一旦ブラックボックスとした上で、
`foreign_table_name`という外部キー参照テーブル名と `**foreign_key_options`オプションを渡している。
`foreign_key_options`はほとんど`{column: column_name}`=`column: "#{name}_id"`=`column: "#{外部キー参照テーブル名}_id`を設定しているに過ぎない。

`table.foreign_key`は
`TableDefinition`内で`add_to(self)`としてる前提で
`add_to(table)`メソッドで`table.foreign_key`としてるから
`TableDefinition`の`foreign_key`を呼び出している

https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L145

```activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
module ActiveRecord
  module ConnectionAdapters #:nodoc:
    ...

    class TableDefinition # :nodoc:
      ...

      def foreign_key(table_name, **options) # :nodoc:
        foreign_keys << [table_name, options]
      end
```

ただ単に配列に入れておりこのクラスが`TableDefinition`だから、定義が終わって、このあとよしなに処理が行われると予想。

結論外部キー設定時の`t.references`では色々設定できるように見えるけど、
`null: false`など共通のものを除くと、参照テーブル設定と`foreign_key: true`くらいしか設定できない。
`foreign_key`は見た感じ条件分岐の際の条件に使われるくらいなので、booleanで設定できるくらいで、「◯◯のテーブルと主キー以外の××カラムを関連付けたい」といった柔軟な設定はできない模様。
内部で外部キーを設定するテーブル名に`_id`を付けたカラムを外部キーにしている。