【Rails】カラム追加したときにannotaterb gemがたまに動かないので調査する
この Gemの挙動の調査である。
簡単にGemの紹介をすると、migrationしたとき model ファイルに そのテーブル情報を書き込んでくれる。
schema.rbを参照しなくてもModelを見たらテーブル情報がわかるというのが便利。
例) app/models/company.rb
# == Schema Information
#
# Table name: companies(利用会社)
#
# id :uuid not null, primary key
# subdomain(サブドメイン) :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_companies_on_subdomain (subdomain) UNIQUE
#
class Company < ApplicationRecord
has_one :company_profile, dependent: :restrict_with_error
end
前提
ber
は bundle exec rails
のエイリアスである。
alias ber='bundle exec rails'
各種バージョンはこんな感じ。
❯ ruby -v
ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [arm64-darwin24]
❯ ber -v
Rails 7.2.2
❯ be annotaterb -v
AnnotateRb v4.13.0
Annotaterbが動いている例
Postモデルを用意
ber g model Post
migrationファイルは以下のようにした
class CreatePosts < ActiveRecord::Migration[7.2]
def change
create_table :posts, id: :uuid do |t|
t.string :title, null: false, comment: "タイトル"
t.text :content, null: false, comment: "内容"
t.timestamps
end
end
end
実行する
❯ ber db:migrate
== 20241201061948 AddMemoToEmployeeSurveySubmissions: migrating ===============
-- add_column(:employee_survey_submissions, :memo, :text, {:comment=>"メモ"})
-> 0.0033s
== 20241201061948 AddMemoToEmployeeSurveySubmissions: migrated (0.0033s) ======
== 20241201062814 CreatePosts: migrating ======================================
-- create_table(:posts, {:id=>:uuid})
-> 0.0120s
== 20241201062814 CreatePosts: migrated (0.0120s) =============================
Annotating models
Annotated (3): app/models/post.rb, spec/models/post_spec.rb, spec/factories/posts.rb
動いている。
生成されたPostモデル
# == Schema Information
#
# Table name: posts
#
# id :uuid not null, primary key
# title(タイトル) :string not null
# content(内容) :text not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Post < ApplicationRecord
end
動いていない例
先のPostモデルが生成されている状態で以下のようなmigrationファイルを実行してみる
class AddMemoToPost < ActiveRecord::Migration[7.2]
def change
add_column :posts, :memo, :text, comment: "メモ"
end
end
postsテーブルに、memoカラムを挿入する。
実行結果
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"メモ"})
-> 0.0159s
== 20241201063248 AddMemoToPost: migrated (0.0161s) ===========================
Annotating models
Model files unchanged.
Model files unchanged.
になっている。
生成されたPostモデル
# == Schema Information
#
# Table name: posts
#
# id :uuid not null, primary key
# title(タイトル) :string not null
# content(内容) :text not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Post < ApplicationRecord
end
→ memoカラムがない。
annotaterb gemの動作の調査
migrateしたときに、annotaterbのgemで何が起きているか?
lib/annotate_rb/run.rb が ある。名前的にこれが動いていそうだ。
# frozen_string_literal: true
module AnnotateRb
class Runner
class << self
def run(args)
new.run(args)
end
end
def run(args)
# 中略
if @options[:command]
@options[:command].call(@options)
else
# TODO
raise "Didn't specify a command"
end
end
end
end
命名的には 具体的な実行の中身は @options[:command].call(@options)
が何かやっているのだろう。
@options
とは何か、@options[:command]
とは何か、をデバッグしてみる。
@options
is 何?
予想的には、.annotaterb.yml が クラスになったものだろう。
デバッグしてみる。
# デバッグ
pp "===@options==="
pp @options
pp "~~~@options[:command]~~~"
pp @options[:command]
if @options[:command]
@options[:command].call(@options)
else
# TODO
raise "Didn't specify a command"
end
この状態で ber db:migrate
を実行すると以下が出力された。
"===@options==="
#<AnnotateRb::Options:0x0000000138574728
@options=
{:position=>"before",
:position_in_additional_file_patterns=>"before",
:position_in_class=>"before",
:position_in_factory=>"before",
:position_in_fixture=>"before",
:position_in_routes=>"before",
:position_in_serializer=>"before",
:position_in_test=>"before",
:classified_sort=>false,
:exclude_controllers=>true,
:exclude_factories=>false,
:exclude_fixtures=>false,
:exclude_helpers=>true,
:exclude_scaffolds=>true,
:exclude_serializers=>false,
:exclude_sti_subclasses=>false,
:exclude_tests=>false,
:force=>false,
:format_markdown=>false,
:format_rdoc=>false,
:format_yard=>false,
:frozen=>false,
:ignore_model_sub_dir=>false,
:ignore_unknown_models=>false,
:include_version=>false,
:show_check_constraints=>false,
:show_complete_foreign_keys=>false,
:show_foreign_keys=>true,
:show_indexes=>true,
:simple_indexes=>false,
:sort=>false,
:timestamp=>false,
:trace=>false,
:with_comment=>true,
:with_column_comments=>true,
:with_table_comments=>true,
:active_admin=>false,
:command=>#<AnnotateRb::Commands::AnnotateModels:0x0000000138576f28>,
:debug=>false,
:hide_default_column_types=>"",
:hide_limit_column_types=>"",
:ignore_columns=>nil,
:ignore_routes=>nil,
:models=>true,
:routes=>false,
:skip_on_db_migrate=>false,
:target_action=>:do_annotations,
:wrapper=>nil,
:wrapper_close=>nil,
:wrapper_open=>nil,
:classes_default_to_s=>[],
:additional_file_patterns=>[],
:model_dir=>["app/models"],
:require=>[],
:root_dir=>[""],
:exit=>false,
:original_args=>["models"]},
@state={:working_args=>[]}>
"~~~@options[:command]~~~"
#<AnnotateRb::Commands::AnnotateModels:0x0000000138576f28>
@options
はやはり .anotaterb.yml が AnnotateRb::Options Class
になったものだ。
@options[:command]
は AnnotateRb::Commands::AnnotateModels Class
のことのようだ。
先ほど ber db:migrate
を動かしたときに、
-
lib/annotate_rb/run.rb
が動いてそのファイルでAnnotateRb::Commands::AnnotateModels
が 呼び出されている
ところまでわかった。
AnnotateRb::Commands::AnnotateModelsを見てみる
lib/annotate_rb/commands/annotate_models.rb
のファイル。
# frozen_string_literal: true
module AnnotateRb
module Commands
class AnnotateModels
def call(options)
puts "Annotating models"
if options[:debug]
puts "Running with debug mode, options:"
pp options.to_h
end
# Eager load Models when we're annotating models
AnnotateRb::EagerLoader.call(options)
AnnotateRb::ModelAnnotator::Annotator.send(options[:target_action], options)
end
end
end
end
重要そうなのは、最後の AnnotateRb::ModelAnnotator::Annotator.send(options[:target_action], options)
だろう。
options[:target]
は 先ほど @options
の デバッグをしたときに含まれているキーだ。
:target_action=>:do_annotations,
この値が、 AnnotateRb::ModelAnnotator::Annotator
に渡されているらしい。
sendで呼ばれているので、
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
というコードが実行されている。
先ほど ber db:migrate
を動かしたときに、
-
lib/annotate_rb/run.rb
が動いてそのファイルで -
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行されている
ところまでわかった。
AnnotateRb::ModelAnnotator::Annotator の中身を見てみる
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
class Annotator
class << self
def do_annotations(options)
new(options).do_annotations
end
def remove_annotations(options)
new(options).remove_annotations
end
end
def initialize(options)
@options = options
end
def do_annotations
ProjectAnnotator.new(@options).annotate
end
def remove_annotations
ProjectAnnotationRemover.new(@options).remove_annotations
end
end
end
end
do_annotations
メソッドがある。これが呼ばれていると言うことだ。
つまりProjectAnnotator.new(@options).annotate
が動いている。
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている
ところまでわかった。
ProjectAnnotatorは lib/annotate_rb/model_annotator/project_annotator.rb
で定義されている。
AnnotateRb::ModelAnnotator::ProjectAnnotator の中身を見てみる
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
class ProjectAnnotator
def initialize(options)
@options = options
end
def annotate
project_model_files = model_files
annotation_instructions = project_model_files.map do |path, filename|
file = File.join(path, filename)
if AnnotationDecider.new(file, @options).annotate?
_instructions = build_instructions_for_file(file)
end
end.flatten.compact
annotated = annotation_instructions.map do |instruction|
if SingleFileAnnotator.call_with_instructions(instruction)
instruction.file
end
end.compact
if annotated.empty?
puts "Model files unchanged."
else
puts "Annotated (#{annotated.length}): #{annotated.join(", ")}"
end
end
# 中略
end
annotateメソッドがある。ProjectAnnotator.new(@options).annotate
で動かされているメソッドだ。
puts "Model files unchanged."
もある。「Model files unchanged.」 は見覚えがある。
始めの方のmigrate実行結果を貼ったときに出てきたコメントだ。↓これ。
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"メモ"})
-> 0.0159s
== 20241201063248 AddMemoToPost: migrated (0.0161s) ===========================
Annotating models
Model files unchanged.
「Model files unchanged.」コメントは以下のコードを見ると
if annotated.empty?
puts "Model files unchanged."
else
puts "Annotated (#{annotated.length}): #{annotated.join(", ")}"
end
annotated
変数が empty?
だと Model files unchanged.
と出力されるようだ。
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている
annotated
変数がemptyになるのは怪しい
ところまでわかった
annotated変数がemptyになる理由を探りたい
テーブルに変更があるのに、annotated 変数がempty?になっているのは期待通りではない。
annotated変数がempty?になっている理由を探っていく。
annotated 変数の定義は2ステップある。
annotation_instructions = project_model_files.map do |path, filename|
file = File.join(path, filename)
if AnnotationDecider.new(file, @options).annotate?
_instructions = build_instructions_for_file(file)
end
end.flatten.compact
annotated = annotation_instructions.map do |instruction|
if SingleFileAnnotator.call_with_instructions(instruction)
instruction.file
end
end.compact
の2つ。
どっちのステップがおかしくて、annotatedがemptyになるのか調べる。
annotation_instructions
is 何?
まずは、Step2の最初に呼ばれている変数 annotation_instructions
が空配列か見てみよう。
これが空配列ならば、Step1の定義がおかしい。空配列でないならば、Step2の定義が怪しいかもしれない。
annotation_instructions
を出力してみる。
pp annotation_instructions
たくさん出力されたので一部の情報だけ抜粋して記載しておく。
#<AnnotateRb::ModelAnnotator::SingleFileAnnotatorInstruction:0x000000012cf5f9b0
@annotation=
"# == Schema Information\n" +
"#\n" +
"# Table name: versions(変更履歴)\n" +
"#\n" +
"# id :uuid not null, primary key\n" +
"# item_type(変更したモデル) :string not null\n" +
"# item_id(変更したレコードのID) :string not null\n" +
"# event(変更の種類) :string not null\n" +
"# whodunnit(変更したユーザー) :jsonb\n" +
"# object(変更前のデータ) :jsonb\n" +
"# object_changes(変更内容) :jsonb\n" +
"# request_info(リクエスト情報) :jsonb\n" +
"# created_at :datetime not null\n" +
"# updated_at :datetime not null\n" +
"#\n" +
"# Indexes\n" +
"#\n" +
"# index_versions_on_item_type_and_item_id (item_type,item_id)\n" +
"#\n",
@file="spec/factories/versions.rb",
@options=
#<AnnotateRb::Options:0x000000012aa57cf8
@options=
{:position=>"before",
:position_in_additional_file_patterns=>"before",
:position_in_class=>"before",
:position_in_factory=>"before",
:position_in_fixture=>"before",
:position_in_routes=>"before",
:position_in_serializer=>"before",
:position_in_test=>"before",
:classified_sort=>false,
:exclude_controllers=>true,
:exclude_factories=>false,
:exclude_fixtures=>false,
:exclude_helpers=>true,
:exclude_scaffolds=>true,
:exclude_serializers=>false,
:exclude_sti_subclasses=>false,
:exclude_tests=>false,
:force=>false,
:format_markdown=>false,
:format_rdoc=>false,
:format_yard=>false,
:frozen=>false,
:ignore_model_sub_dir=>false,
:ignore_unknown_models=>false,
:include_version=>false,
:show_check_constraints=>false,
:show_complete_foreign_keys=>false,
:show_foreign_keys=>true,
:show_indexes=>true,
:simple_indexes=>false,
:sort=>false,
:timestamp=>false,
:trace=>false,
:with_comment=>true,
:with_column_comments=>true,
:with_table_comments=>true,
:active_admin=>false,
:command=>#<AnnotateRb::Commands::AnnotateModels:0x000000012aa58d60>,
:debug=>false,
:hide_default_column_types=>"",
:hide_limit_column_types=>"",
:ignore_columns=>nil,
:ignore_routes=>nil,
:models=>true,
:routes=>false,
:skip_on_db_migrate=>false,
:target_action=>:do_annotations,
:wrapper=>nil,
:wrapper_close=>nil,
:wrapper_open=>nil,
:classes_default_to_s=>[],
:additional_file_patterns=>[],
:model_dir=>["app/models"],
:require=>[],
:root_dir=>[""],
:exit=>false,
:original_args=>["models"]},
@state={:working_args=>[], :current_version=>20241201062814}>,
@position=:position_in_factory>]
annotation_instructions
には全モデルの情報が詰め込まれていた。
そしてデバッグ結果を見るとAnnotateRb::ModelAnnotator::SingleFileAnnotatorInstruction Class
に@annotation
, @options
, @position
というインスタンス変数があってそれらが出力されていたことがわかる。
つまりannotation_instructions
が空配列でないことが分かった。
annotation_instructions
から annotated
を定義するときに、annotated
がemptyになっていると言うことだ。要はこのStep2が怪しい、と。
annotated = annotation_instructions.map do |instruction|
if SingleFileAnnotator.call_with_instructions(instruction)
instruction.file
end
end.compact
このStepでは
-
annotation_instructions
の配列から -
SingleFileAnnotator.call_with_instructions(instruction)
がTrueになるものを取り出し -
instruction.file
の結果を返した配列を作っている
ってことは SingleFileAnnotator.call_with_instructions(instruction)
が全部 False
になっていて annotation_instructions
から annotated
変数を作るときに 空配列になっているのか...?
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている annotated変数がemptyになるのは怪しい
- annotated変数がemptyになっているのは
SingleFileAnnotator.call_with_instructions(instruction)
が全て Falseになるためでは?
というところまでわかった
SingleFileAnnotator.call_with_instructions(instruction) の中身を見ていく
lib/annotate_rb/model_annotator/single_file_annotator.rb
のファイルで定義されている。
中身はこんな感じだ。
module AnnotateRb
module ModelAnnotator
class SingleFileAnnotator
class << self
def call_with_instructions(instruction)
call(instruction.file, instruction.annotation, instruction.position, instruction.options)
end
# 中略
def call(file_name, annotation, annotation_position, options)
# 中略
end
end
call_with_instructions
はcallメソッドを呼んでいるようだ。
call
メソッドの説明として、コード内に以下のようなコメントが書かれていた
# Add a schema block to a file. If the file already contains
# a schema info block (a comment starting with "== Schema Information"),
# check if it matches the block that is already there. If so, leave it be.
# If not, remove the old info block and write a new one.
#
# == Returns:
# true or false depending on whether the file was modified.
#
# === Options (opts)
# :force<Symbol>:: whether to update the file even if it doesn't seem to need it.
# :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
# :before, :top, :after or :bottom. Default is :before.
#
日本語に訳すと以下のような感じだ(DeepL訳)
スキーマ・ブロックをファイルに追加する。
ファイルにすでにスキーマ情報ブロック(「==スキーマ情報 」で始まるコメント)がある場合は、すでにあるブロックと一致するかどうかをチェックする。
一致する場合はそのままにしておく。そうでない場合は、古い情報ブロックを削除し、新しい情報ブロックを書き込む。
返り値:
ファイルが変更されたかどうかによって # trueかfalseを返します。
オプション (opts):
:force<Symbol>: ファイルを更新する必要がないように見えても更新するかどうか。
:position_in_*<Symbol>: フィクスチャやモデルファイルのどこにアノテーションセクションを配置するか、before、:top、:after、:bottom のいずれかです。デフォルトは :before です。
^ 要は、ここでファイルにスキーマ情報の書き込みをしているらしい。
- 変更があったら、新しいスキーマ情報を書き込む
- そして、ファイルが変更されたかどうかをTrue/Falseで返す
と書いてある。
元々の課題は
ber db:migrate
したときにテーブル情報に変更があるのに、annotaterbでファイル書き込みがされないことである。
この原因は、「変更があったら、新しいスキーマ情報を書き込む」 の「変更があったら」が正しく検知されていないのだろう。
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている annotated変数がemptyになるのは怪しい
- annotated変数がemptyになっているのは
SingleFileAnnotator.call_with_instructions(instruction)
が全て Falseになるため -
SingleFileAnnotator.call_with_instructions(instruction)
では 記載されているテーブル情報 の変更があったら追記するメソッドだが、「変更があったら」がうまく検知されていなそう
というところまでわかった。
変更の検知はどのように行われている?callメソッドを見る
SingleFileAnnotator.call_with_instructions(instruction)
のcall
メソッドを見てみると
def call(file_name, annotation, annotation_position, options)
# 中略
parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options).parse
# 中略
return false if parsed_file.has_skip_string?
return false if !parsed_file.annotations_changed? && !options[:force]
# 中略
end
とある。
つまり
-
parsed_file.has_skip_string?
がTrueの時にcallはFalseを返す -
parsed_file.annotations_changed?
がFalse かつoptions[:force]
が Falseのとき callはFalseを返す
options[:force]
は READMEには「変更がなくても強制的に注釈を記載し直す」のように書いてある。
何はともあれこの辺りをデバッグしてみよう。
def call(file_name, annotation, annotation_position, options)
# 中略
parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options).parse
# 中略
if file_name == "app/models/post.rb"
pp "="*30
puts "===parsed_file.has_skip_string?: #{parsed_file.has_skip_string?}==="
puts "~~~parsed_file.annotations_changed?: #{parsed_file.annotations_changed?}~~~"
end
return false if parsed_file.has_skip_string?
return false if !parsed_file.annotations_changed? && !options[:force]
# 中略
end
すると以下のようになった。
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"メモ"})
-> 0.0030s
== 20241201063248 AddMemoToPost: migrated (0.0031s) ===========================
Annotating models
"=============================="
===parsed_file.has_skip_string?: false===
~~~parsed_file.annotations_changed?: false~~~
Model files unchanged.
parsed_file.has_skip_string?
の結果は False。
parsed_file.annotations_changed?
の結果もFalse。
memoカラムを追加しているので、parsed_file.annotations_changed?
がFalseになっているのは期待通りではない。
parsed_file.annotations_changed?
がおかしそうだ。
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている annotated変数がemptyになるのは怪しい
- annotated変数がemptyになっているのは
SingleFileAnnotator.call_with_instructions(instruction)
が全て Falseになるため -
SingleFileAnnotator.call_with_instructions(instruction)
では 記載されているテーブル情報 の変更があったら追記するメソッドだが、「変更があったら」がうまく検知されてい - より具体的に言うと
parsed_file.annotations_changed?
がfalse
を返しているのが期待通りではない
ところまでわかった。
parsed_file.annotations_changed?
を調査
parsed_file
は以下のように定義されている。
begin
parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options).parse
rescue FileParser::AnnotationFinder::MalformedAnnotation => e
warn "Unable to process #{file_name}: #{e.message}"
warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
return false
end
つまり FileParser::ParsedFile
Classです。FileParser::ParsedFile
をnewしてparseメソッドを呼んだ返り値がparsed_file
である。
FileParser::ParsedFile
を調べる
中身は以下のようになっていた。
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
module FileParser
class ParsedFile
# 中略
def parse
# 中略
_result = ParsedFileResult.new(
has_annotations: has_annotations,
has_skip_string: has_skip_string,
annotations_changed: annotations_changed,
annotations: annotations,
annotations_with_whitespace: annotations_with_whitespace,
has_leading_whitespace: has_leading_whitespace,
has_trailing_whitespace: has_trailing_whitespace,
annotation_position: annotation_position,
starts: @file_parser.starts,
ends: @file_parser.ends
)
end
end
end
end
end
annotations_changed
がある。
annotations_changed
がFalseだと、parsed_file.annotations_changed?
ももちろんFalseだろう。
annotations_changed
の定義を見てみよう。
@diff = AnnotationDiffGenerator.new(annotations, @new_annotations).generate
has_skip_string = @file_parser.comments.any? { |comment, _lineno| comment.include?(SKIP_ANNOTATION_STRING) }
annotations_changed = @diff.changed?
AnnotationDiffGenerator
Classのインスタンスから定義されている。
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている annotated変数がemptyになるのは怪しい
- annotated変数がemptyになっているのは
SingleFileAnnotator.call_with_instructions(instruction)
が全て Falseになるため -
SingleFileAnnotator.call_with_instructions(instruction)
では 記載されているテーブル情報 の変更があったら追記するメソッドだが、「変更があったら」がうまく検知されていない - より具体的に言うと
parsed_file.annotations_changed?
がfalse
を返しているのが期待通りではない - それは
AnnotationDiffGenerator
クラスの インスタンスで 定義された@diff.changed?
の結果かが期待通りでない
ところまでわかった。
AnnotateRb::ModelAnnotator::AnnotationDiffGenerator の中身を見てみる
@diff = AnnotationDiffGenerator.new(annotations, @new_annotations).generate
@diff.changed?
で何が起きているか調べていきたい。AnnotationDiffGenerator
のコードは以下だ。
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
# Compares the current file content and new annotation block and generates the column annotation differences
class AnnotationDiffGenerator
def generate
# 中略
_result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns)
end
end
end
end
AnnotationDiffGenerator
が返しているのは _result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns)
である。
AnnotationDiff
クラスは以下のように定義されている。
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
# Plain old Ruby object for holding the differences
class AnnotationDiff
attr_reader :current_columns, :new_columns
def initialize(current_columns, new_columns)
@current_columns = current_columns.dup.freeze
@new_columns = new_columns.dup.freeze
end
def changed?
@changed ||= @current_columns != @new_columns
end
end
end
end
changed?
の定義は @current_columns != @new_columns
である。
これは、initializeの二つの引数def initialize(current_columns, new_columns)
が一緒か異なるかを管理している。
current_columns
とnew_columns
の定義はどのようになっているかというと、以下に書かれている current_annotation_columns
と new_annotation_columns
のことである
def generate
# Ignore the Schema version line because it changes with each migration
current_annotations = @file_content.match(HEADER_PATTERN).to_s
new_annotations = @annotation_block.match(HEADER_PATTERN).to_s
current_annotation_columns = if current_annotations.present?
current_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
new_annotation_columns = if new_annotations.present?
new_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
_result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns)
end
具体的に言うとcurrent_annotation_columns
は以下で
current_annotation_columns = if current_annotations.present?
current_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
new_annotation_columns
は以下である。
new_annotation_columns = if new_annotations.present?
new_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
それぞれをデバッグして出力してみる。
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"メモ"})
-> 0.0043s
== 20241201063248 AddMemoToPost: migrated (0.0044s) ===========================
Annotating models
"------current_annotation_columns------"
["# created_at :datetime not null", "# id :uuid not null, primary key", "# updated_at :datetime not null", "# Table name: posts"]
"++++++new_annotation_columns++++++"
["# created_at :datetime not null", "# id :uuid not null, primary key", "# updated_at :datetime not null", "# Table name: posts"]
Model files unchanged.
current_annotation_columns
とnew_annotation_columns
は同じ値を出力している。
new_annotation_columns
に memo
カラムの情報がないのがおかしそうだ。
current_annotation_columns
やnew_annotation_columns
はnew_annotations
やcurrent_annotations
から作られている変数だ。
new_annotations
やcurrent_annotations
はどんな値になっているかデバッグしてみる。
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"メモ"})
-> 0.0032s
== 20241201063248 AddMemoToPost: migrated (0.0032s) ===========================
Annotating models
"== current_annotations: # Table name: posts\n" +
"#\n" +
"# id :uuid not null, primary key\n" +
"# title(タイトル) :string not null\n" +
"# content(内容) :text not null\n" +
"# created_at :datetime not null\n" +
"# updated_at :datetime not null\n" +
"#\n"
"== new_annotations: # Table name: posts\n" +
"#\n" +
"# id :uuid not null, primary key\n" +
"# title(タイトル) :string not null\n" +
"# content(内容) :text not null\n" +
"# created_at :datetime not null\n" +
"# updated_at :datetime not null\n" +
"# memo(メモ) :text\n" +
"#\n"
Model files unchanged.
new_annotations
にはmemo
カラムがある。
よって以下のnew_annotations
にはmemo
カラムがあるが、new_annotation_columns
の時点ではmemo
カラムがなくなっているとわかる。
new_annotation_columns = if new_annotations.present?
new_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
つまりnew_annotations.scan(COLUMN_PATTERN)
がおかしい、と。
new_annotations.scan(COLUMN_PATTERN).sortみる
COLUMN_PATTERN
は
COLUMN_PATTERN = /^#[\t ]+[\w*.`\[\]():]+[\t ]+.+$/
と定義されている。
実際に正規表現を試してみると、以下のようなことが分かった。リンク
つまり、日本語のコメントがあるとうまく動かないようだ!!
先ほど ber db:migrate を動かしたときに、
- lib/annotate_rb/run.rb が動いてそのファイルで
-
AnnotateRb::Commands::AnnotateModels
が 呼び出され -
AnnotateRb::ModelAnnotator::Annotator.new(options).do_annotations
が実行され -
ProjectAnnotator.new(@options).annotate
が動いている - テーブルに変更あるのに ^ の中で 定義されている annotated変数がemptyになるのは怪しい
- annotated変数がemptyになっているのは
SingleFileAnnotator.call_with_instructions(instruction)
が全て Falseになるため -
SingleFileAnnotator.call_with_instructions(instruction)
では 記載されているテーブル情報 の変更があったら追記するメソッドだが、「変更があったら」がうまく検知されていない - より具体的に言うと
parsed_file.annotations_changed?
がfalse
を返しているのが期待通りではない - それは
AnnotationDiffGenerator
クラスの インスタンスで 定義された@diff.changed?
の結果かが期待通りでない -
@diff.changed?
の結果かが期待通りでないのは、正規表現で日本語を考慮されていないのでは!!?
と言うところまでわかった。
日本語が含まれているときに正規表現が正しく動かない!!?を検証
調べたいコードは以下だ。
# frozen_string_literal: true
module AnnotateRb
module ModelAnnotator
# Compares the current file content and new annotation block and generates the column annotation differences
class AnnotationDiffGenerator
# 中略
COLUMN_PATTERN = /^#[\t ]+[\w*.`\[\]():]+[\t ]+.+$/
# 中略
new_annotation_columns = if new_annotations.present?
new_annotations.scan(COLUMN_PATTERN).sort
else
[]
end
# 中略
end
もっと抜粋すると以下を調べたい。
new_annotations.scan(COLUMN_PATTERN).sort
ちなみに、この時点での new_annotations
は以下である。
# Table name: posts\n +
#\n +
# id :uuid not null, primary key\n +
# title(タイトル) :string not null\n +
# content(内容) :text not null\n +
# created_at :datetime not null\n +
# updated_at :datetime not null\n +
# memo(メモ) :text\n +
#\n
例えばカラムのコメントを日本語で書いているが、英語にしてみると動くか検証してみよう。
class AddMemoToPost < ActiveRecord::Migration[7.2]
def change
- add_column :posts, :memo, :text, comment: "メモ"
+ add_column :posts, :memo, :text, comment: "memo"
end
end
comment: "メモ" -> comment: "memo" にしてmigrateしてみる
実行結果は以下だ。
❯ ber db:migrate
== 20241201063248 AddMemoToPost: migrating ====================================
-- add_column(:posts, :memo, :text, {:comment=>"memo"})
-> 0.0039s
== 20241201063248 AddMemoToPost: migrated (0.0039s) ===========================
Annotating models
Annotated (3): app/models/post.rb, spec/models/post_spec.rb, spec/factories/posts.rb
動いている!!
やはり、正規表現で日本語が含まれていると動かないようだ。
COLUMN_PATTERN = /^#[\t ]+[\w*.`\[\]():]+[\t ]+.+$/
この正規表現を\w
だけでなくさまざまなUnicodeに対応したものに変えられたら動くのかもしれない。