Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/test/dummy_app/tmp/
/test/dummy_app/custom/
/test/dummy_app/db/**/*.rb
/test/dummy_app/db/structure.sql
/test/dummy_app/config/database.yml
.ruby-version
.ruby-gemset
Expand Down
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ Metrics/ClassLength:

Metrics/ModuleLength:
Enabled: false

Metrics/AbcSize:
Max: 25

Metrics/CyclomaticComplexity:
Max: 10

Metrics/PerceivedComplexity:
Max: 10
3 changes: 2 additions & 1 deletion app/controllers/actual_db_schema/schema_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def index; end
private

helper_method def schema_diff_html
schema_diff = ActualDbSchema::SchemaDiffHtml.new("./db/schema.rb", "db/migrate")
schema_path = Rails.configuration.active_record.schema_format == :sql ? "./db/structure.sql" : "./db/schema.rb"
schema_diff = ActualDbSchema::SchemaDiffHtml.new(schema_path, "db/migrate")
schema_diff.render_html(params[:table])
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/actual_db_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require_relative "actual_db_schema/schema_diff"
require_relative "actual_db_schema/schema_diff_html"
require_relative "actual_db_schema/schema_parser"
require_relative "actual_db_schema/structure_sql_parser"

require_relative "actual_db_schema/commands/base"
require_relative "actual_db_schema/commands/rollback"
Expand Down
48 changes: 40 additions & 8 deletions lib/actual_db_schema/schema_diff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ class SchemaDiff
/create_table\s+["']([^"']+)["']/ => :table
}.freeze

SQL_CHANGE_PATTERNS = {
/CREATE (?:UNIQUE\s+)?INDEX\s+["']?([^"'\s]+)["']?\s+ON\s+([\w.]+)/i => :index,
/CREATE TABLE\s+(\S+)\s+\(/i => :table,
/CREATE SEQUENCE\s+(\S+)/i => :table,
/ALTER SEQUENCE\s+(\S+)\s+OWNED BY\s+([\w.]+)/i => :table,
/ALTER TABLE\s+ONLY\s+(\S+)\s+/i => :table
}.freeze

def initialize(schema_path, migrations_path)
@schema_path = schema_path
@migrations_path = migrations_path
Expand Down Expand Up @@ -48,11 +56,19 @@ def new_schema_content
end

def parsed_old_schema
@parsed_old_schema ||= SchemaParser.parse_string(old_schema_content.to_s)
@parsed_old_schema ||= parser_class.parse_string(old_schema_content.to_s)
end

def parsed_new_schema
@parsed_new_schema ||= SchemaParser.parse_string(new_schema_content.to_s)
@parsed_new_schema ||= parser_class.parse_string(new_schema_content.to_s)
end

def parser_class
structure_sql? ? StructureSqlParser : SchemaParser
end

def structure_sql?
File.extname(@schema_path) == ".sql"
end

def migration_changes
Expand Down Expand Up @@ -105,8 +121,9 @@ def process_diff_output(diff_str)
lines.each do |line|
if (hunk_match = line.match(/^@@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+@@/))
current_table = find_table_in_new_schema(hunk_match[3].to_i)
elsif (ct = line.match(/create_table\s+["']([^"']+)["']/))
current_table = ct[1]
elsif (ct = line.match(/create_table\s+["']([^"']+)["']/) ||
line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i) || line.match(/ALTER TABLE\s+ONLY\s+(\S+)/i))
current_table = normalize_table_name(ct[1])
end

result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line(line, current_table) : line)
Expand All @@ -128,19 +145,24 @@ def handle_diff_line(line, current_table)
end

def detect_action_and_name(line_content, sign, current_table)
patterns = structure_sql? ? SQL_CHANGE_PATTERNS : CHANGE_PATTERNS
action_map = {
column: ->(md) { [guess_action(sign, current_table, md[2]), md[2]] },
index: ->(md) { [sign == "+" ? :add_index : :remove_index, md[1]] },
table: ->(_) { [sign == "+" ? :create_table : :drop_table, nil] }
}

CHANGE_PATTERNS.each do |regex, kind|
patterns.each do |regex, kind|
next unless (md = line_content.match(regex))

action_proc = action_map[kind]
return action_proc.call(md) if action_proc
end

if structure_sql? && current_table && (md = line_content.match(/^\s*"?(\w+)"?\s+(.+?)(?:,|\s*$)/i))
return [guess_action(sign, current_table, md[1]), md[1]]
end

[nil, nil]
end

Expand All @@ -159,8 +181,8 @@ def find_table_in_new_schema(new_line_number)
current_table = nil

new_schema_content.lines[0...new_line_number].each do |line|
if (match = line.match(/create_table\s+["']([^"']+)["']/))
current_table = match[1]
if (match = line.match(/create_table\s+["']([^"']+)["']/) || line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i))
current_table = normalize_table_name(match[1])
end
end
current_table
Expand All @@ -171,7 +193,7 @@ def find_migrations(action, table_name, col_or_index_name)

migration_changes.each do |file_path, changes|
changes.each do |chg|
next unless chg[:table].to_s == table_name.to_s
next unless (structure_sql? && index_action?(action)) || chg[:table].to_s == table_name.to_s

matches << file_path if migration_matches?(chg, action, col_or_index_name)
end
Expand All @@ -180,6 +202,10 @@ def find_migrations(action, table_name, col_or_index_name)
matches
end

def index_action?(action)
%i[add_index remove_index rename_index].include?(action)
end

def migration_matches?(chg, action, col_or_index_name)
return (chg[:action] == action) if col_or_index_name.nil?

Expand Down Expand Up @@ -225,5 +251,11 @@ def extract_migration_index_name(chg, table_name)
def annotate_line(line, migration_file_paths)
"#{line.chomp}#{colorize(" // #{migration_file_paths.join(", ")} //", :gray)}\n"
end

def normalize_table_name(table_name)
return table_name unless structure_sql? && table_name.include?(".")

table_name.split(".").last
end
end
end
16 changes: 11 additions & 5 deletions lib/actual_db_schema/schema_diff_html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def render_html(table_filter)

def generate_diff_html
diff_output = generate_full_diff(old_schema_content, new_schema_content)
return "<pre>#{ERB::Util.html_escape(new_schema_content)}</pre>" if diff_output.strip.empty?
diff_output = new_schema_content if diff_output.strip.empty?

process_diff_output_for_html(diff_output)
end
Expand All @@ -43,7 +43,7 @@ def process_diff_output_for_html(diff_str)
block_depth = 1

diff_str.lines.each do |line|
next if line.start_with?("---") || line.start_with?("+++") || line.match(/^@@/)
next if skip_line?(line)

current_table, table_start, block_depth =
process_table(line, current_table, table_start, result_lines.size, block_depth)
Expand All @@ -53,15 +53,21 @@ def process_diff_output_for_html(diff_str)
result_lines.join
end

def skip_line?(line)
line != "---\n" && !line.match(/^--- Name/) &&
(line.start_with?("---") || line.start_with?("+++") || line.match(/^@@/))
end

def process_table(line, current_table, table_start, table_end, block_depth)
if (ct = line.match(/create_table\s+["']([^"']+)["']/))
return [ct[1], table_end, block_depth]
if (ct = line.match(/create_table\s+["']([^"']+)["']/) || line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i))
return [normalize_table_name(ct[1]), table_end, block_depth]
end

return [current_table, table_start, block_depth] unless current_table

block_depth += line.scan(/\bdo\b/).size unless line.match(/create_table\s+["']([^"']+)["']/)
block_depth -= line.scan(/\bend\b/).size
block_depth -= line.scan(/\);\s*$/).size

if block_depth.zero?
@tables[current_table] = { start: table_start, end: table_end }
Expand Down Expand Up @@ -101,7 +107,7 @@ def colorize_html(text, color)
end

def link_to_migration(migration_file_path)
migration = migrations.detect { |m| m.filename == migration_file_path }
migration = migrations.detect { |m| File.expand_path(m.filename) == File.expand_path(migration_file_path) }
return ERB::Util.html_escape(migration_file_path) unless migration

url = "migrations/#{migration.version}?database=#{migration.database}"
Expand Down
41 changes: 41 additions & 0 deletions lib/actual_db_schema/structure_sql_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module ActualDbSchema
# Parses the content of a `structure.sql` file into a structured hash representation.
module StructureSqlParser
module_function

def parse_string(sql_content)
schema = {}
table_regex = /CREATE TABLE\s+(?:"?([\w.]+)"?)\s*\((.*?)\);/m
sql_content.scan(table_regex) do |table_name, columns_section|
schema[normalize_table_name(table_name)] = parse_columns(columns_section)
end
schema
end

def parse_columns(columns_section)
columns = {}
columns_section.each_line do |line|
line.strip!
next if line.empty? || line =~ /^(CONSTRAINT|PRIMARY KEY|FOREIGN KEY)/i

match = line.match(/\A"?(?<col>\w+)"?\s+(?<type>\w+)(?<size>\s*\([\d,]+\))?/i)
next unless match

col_name = match[:col]
col_type = match[:type].strip.downcase.to_sym
options = {}
columns[col_name] = { type: col_type, options: options }
end

columns
end

def normalize_table_name(table_name)
return table_name unless table_name.include?(".")

table_name.split(".").last
end
end
end
3 changes: 2 additions & 1 deletion lib/tasks/actual_db_schema.rake
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ namespace :actual_db_schema do # rubocop:disable Metrics/BlockLength

desc "Show the schema.rb diff annotated with the migrations that made the changes"
task :diff_schema_with_migrations, %i[schema_path migrations_path] => :environment do |_, args|
schema_path = args[:schema_path] || "./db/schema.rb"
default_schema = Rails.configuration.active_record.schema_format == :sql ? "./db/structure.sql" : "./db/schema.rb"
schema_path = args[:schema_path] || default_schema
migrations_path = args[:migrations_path] || "db/migrate"

schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)
Expand Down
Loading