Class: Idxfence::SchemaParser

Inherits:
Object
  • Object
show all
Defined in:
lib/idxfence/schema_parser.rb

Overview

Parses db/schema.rb (Rails' generated, declarative snapshot of the database schema) with Ripper looking for each create_table block's boundary structurally, then a narrower regex pass over that block's own text extracting every t.index [...], unique: true line -- the same hybrid technique Idxfence::ModelParser uses on model files. See DETAILS.md's "two passes" section for why this has to be a second, separate pass over a different file rather than something ModelParser could infer on its own.

Returns a Hash of table_name => Array<Array>, one entry per unique index found in that table's create_table block, each an array of the index's own column names in the order t.index declared them (order doesn't matter for matching -- Checker compares as sets). A non-unique t.index (no unique: true) is not collected at all -- it can't enforce uniqueness at the database level regardless of what columns it covers.

Defined Under Namespace

Classes: MissingSchemaError

Constant Summary collapse

CREATE_TABLE_IDENT =
"create_table".freeze
TABLE_NAME_LINE =
/create_table\s+["']([^"']+)["']/.freeze
INDEX_LINE =
/t\.index\s+\[([^\]]*)\](?:[^\n]*?unique:\s*true)?/.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:) ⇒ SchemaParser

Returns a new instance of SchemaParser.



33
34
35
# File 'lib/idxfence/schema_parser.rb', line 33

def initialize(path:)
  @path = path
end

Class Method Details

.parse(path) ⇒ Object

Raises:



27
28
29
30
31
# File 'lib/idxfence/schema_parser.rb', line 27

def self.parse(path)
  raise MissingSchemaError, "#{path}: no such file" unless File.exist?(path)

  new(path: path).call
end

Instance Method Details

#call ⇒ Object

Raises:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/idxfence/schema_parser.rb', line 37

def call
  source = File.read(path)
  sexp = Ripper.sexp(source)
  raise MissingSchemaError, "#{path}: could not parse (invalid Ruby syntax)" unless sexp

  lines = source.lines
  tables = Hash.new { |h, k| h[k] = [] }

  each_create_table_block(sexp) do |block_node|
    start_line = min_line(block_node)
    end_line = max_line(block_node)
    next unless start_line && end_line

    block_source = lines[(start_line - 1)...end_line].join
    table_name = (m = block_source.match(TABLE_NAME_LINE)) && m[1]
    next unless table_name

    block_source.each_line do |line|
      next unless line =~ /t\.index\b/

      idx_match = line.match(INDEX_LINE)
      next unless idx_match
      next unless line.match?(/unique:\s*true/)

      columns = idx_match[1].scan(/["']([^"']+)["']/).flatten
      tables[table_name] << columns unless columns.empty?
    end
  end

  tables
end