Class: Railbow::MigrationParser

Inherits:
Object
  • Object
show all
Defined in:
lib/railbow/migration_parser.rb

Constant Summary collapse

SINGLE_TABLE_METHODS =

Methods that take a single table name as the first argument

%w[
  create_table drop_table change_table
  add_column remove_column rename_column change_column
  change_column_default change_column_null
  add_index remove_index
  add_reference remove_reference
  add_belongs_to remove_belongs_to
  add_timestamps remove_timestamps
].freeze
DUAL_TABLE_METHODS =

Methods that take two table names as arguments

%w[
  add_foreign_key remove_foreign_key
].freeze
SINGLE_TABLE_PATTERN =

Dual-table methods are included here too: their first argument is always a table, even when the second is passed as to_table: instead of positionally.

/\b(?:#{(SINGLE_TABLE_METHODS + DUAL_TABLE_METHODS).join("|")})\s+[:"](\w+)/
DUAL_TABLE_PATTERN =
/\b(?:#{DUAL_TABLE_METHODS.join("|")})\s+[:"](\w+)["\s,]+[:"](\w+)/
SQL_TABLE_PATTERNS =

SQL keywords followed by a table name

[
  /\bUPDATE\s+["']?(\w+)["']?/i,
  /\bINSERT\s+INTO\s+["']?(\w+)["']?/i,
  /\bDELETE\s+FROM\s+["']?(\w+)["']?/i,
  /\bALTER\s+TABLE\s+["']?(\w+)["']?/i,
  /\bTRUNCATE\s+(?:TABLE\s+)?["']?(\w+)["']?/i,
  /\bDROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?["']?(\w+)["']?/i,
  /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i
].freeze
KEYWORD_TABLE_PATTERN =

Helper/DSL calls that name their target table via a keyword argument, e.g. convert_to_monthly_partitions(table: :apples) or remove_foreign_key :accounts, to_table: :owners. Catches project-level migration helpers regardless of the method name. Bare from:/to: are deliberately not matched — change_column_default :users, :status, from: nil, to: "active" would yield phantom tables.

/\b(?:table|to_table|from_table):\s*[:"']?(\w+)/
COMMENT_PATTERN =

Ruby # comments up to end of line, but not #{} interpolation.

/#(?!\{).*/
MODEL_AR_METHODS =

AR class-level query/mutation methods that strongly signal a constant is a model. Kept narrow on purpose — methods like find, all, first, count, create, new, transaction, order, includes collide with stdlib/non-model constants (e.g. Time.new, File.find, Array#first) and would produce false positives.

%w[
  find_each find_in_batches
  find_by find_by!
  find_or_create_by find_or_create_by!
  find_or_initialize_by
  where update_all delete_all destroy_all
  upsert_all insert_all
  pluck pick
  in_batches
].freeze
MODEL_REFERENCE_PATTERN =
/\b([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)*)\.(?:#{MODEL_AR_METHODS.join("|")})\b/

Class Method Summary collapse

Class Method Details

.extract_tables(filepath) ⇒ Object



67
68
69
70
71
72
# File 'lib/railbow/migration_parser.rb', line 67

def self.extract_tables(filepath)
  return [] if filepath.nil? || filepath.empty?
  return [] unless File.exist?(filepath)

  extract_tables_from_content(File.read(filepath))
end

.extract_tables_from_content(content) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/railbow/migration_parser.rb', line 74

def self.extract_tables_from_content(content)
  return [] if content.nil? || content.empty?

  # Comments would otherwise produce phantom tables — an explanatory
  # "# ALTER TABLE foo ..." or a commented-out create_table reads exactly
  # like the real thing to the patterns below.
  content = content.gsub(COMMENT_PATTERN, "")

  tables = []

  content.scan(SINGLE_TABLE_PATTERN) { |match| tables << match[0] }
  content.scan(DUAL_TABLE_PATTERN) { |match| tables.concat(match) }
  SQL_TABLE_PATTERNS.each do |pattern|
    content.scan(pattern) { |match| tables << match[0] }
  end
  content.scan(KEYWORD_TABLE_PATTERN) { |match| tables << match[0] }
  tables.uniq!

  # Model-based detection is heuristic (a constant that happens to respond
  # to one of these methods isn't necessarily an AR model). Only use it to
  # supplement sparse DDL/SQL findings — skip once we already know of ≥2
  # tables, and cap the final total at 2.
  if tables.size < 2
    content.scan(MODEL_REFERENCE_PATTERN) do |match|
      break if tables.size >= 2
      table = ActiveSupport::Inflector.tableize(match[0].split("::").last)
      tables << table unless tables.include?(table)
    end
  end

  tables
end