actual_db_schema 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -2
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +3 -1
- data/README.md +124 -1
- data/actual_db_schema.gemspec +2 -0
- data/app/controllers/actual_db_schema/broken_versions_controller.rb +41 -0
- data/app/controllers/actual_db_schema/migrations_controller.rb +14 -0
- data/app/controllers/actual_db_schema/schema_controller.rb +18 -0
- data/app/views/actual_db_schema/broken_versions/index.html.erb +64 -0
- data/app/views/actual_db_schema/migrations/index.html.erb +62 -50
- data/app/views/actual_db_schema/schema/index.html.erb +31 -0
- data/app/views/actual_db_schema/shared/_js.html +6 -0
- data/app/views/actual_db_schema/shared/_style.html +45 -0
- data/config/routes.rb +10 -0
- data/lib/actual_db_schema/configuration.rb +4 -1
- data/lib/actual_db_schema/console_migrations.rb +47 -0
- data/lib/actual_db_schema/git_hooks.rb +1 -1
- data/lib/actual_db_schema/migration.rb +49 -1
- data/lib/actual_db_schema/migration_parser.rb +264 -0
- data/lib/actual_db_schema/patches/migration_context.rb +1 -1
- data/lib/actual_db_schema/railtie.rb +15 -0
- data/lib/actual_db_schema/schema_diff.rb +229 -0
- data/lib/actual_db_schema/schema_diff_html.rb +122 -0
- data/lib/actual_db_schema/schema_parser.rb +153 -0
- data/lib/actual_db_schema/version.rb +1 -1
- data/lib/actual_db_schema.rb +6 -1
- data/lib/generators/actual_db_schema/templates/actual_db_schema.rb +8 -0
- data/lib/tasks/actual_db_schema.rake +33 -0
- metadata +40 -2
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActualDbSchema
|
4
|
+
# Generates an HTML representation of the schema diff,
|
5
|
+
# annotated with the migrations responsible for each change.
|
6
|
+
class SchemaDiffHtml < SchemaDiff
|
7
|
+
def render_html(table_filter)
|
8
|
+
return unless old_schema_content && !old_schema_content.strip.empty?
|
9
|
+
|
10
|
+
@full_diff_html ||= generate_diff_html
|
11
|
+
filter = table_filter.to_s.strip.downcase
|
12
|
+
|
13
|
+
filter.empty? ? @full_diff_html : extract_table_section(@full_diff_html, filter)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_diff_html
|
19
|
+
diff_output = generate_full_diff(old_schema_content, new_schema_content)
|
20
|
+
return "<pre>#{ERB::Util.html_escape(new_schema_content)}</pre>" if diff_output.strip.empty?
|
21
|
+
|
22
|
+
process_diff_output_for_html(diff_output)
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_full_diff(old_content, new_content)
|
26
|
+
Tempfile.create("old_schema") do |old_file|
|
27
|
+
Tempfile.create("new_schema") do |new_file|
|
28
|
+
old_file.write(old_content)
|
29
|
+
new_file.write(new_content)
|
30
|
+
old_file.rewind
|
31
|
+
new_file.rewind
|
32
|
+
|
33
|
+
`diff -u -U 9999999 #{old_file.path} #{new_file.path}`
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_diff_output_for_html(diff_str)
|
39
|
+
current_table = nil
|
40
|
+
result_lines = []
|
41
|
+
@tables = {}
|
42
|
+
table_start = nil
|
43
|
+
block_depth = 1
|
44
|
+
|
45
|
+
diff_str.lines.each do |line|
|
46
|
+
next if line.start_with?("---") || line.start_with?("+++") || line.match(/^@@/)
|
47
|
+
|
48
|
+
current_table, table_start, block_depth =
|
49
|
+
process_table(line, current_table, table_start, result_lines.size, block_depth)
|
50
|
+
result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line_html(line, current_table) : line)
|
51
|
+
end
|
52
|
+
|
53
|
+
result_lines.join
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_table(line, current_table, table_start, table_end, block_depth)
|
57
|
+
if (ct = line.match(/create_table\s+["']([^"']+)["']/))
|
58
|
+
return [ct[1], table_end, block_depth]
|
59
|
+
end
|
60
|
+
|
61
|
+
return [current_table, table_start, block_depth] unless current_table
|
62
|
+
|
63
|
+
block_depth += line.scan(/\bdo\b/).size unless line.match(/create_table\s+["']([^"']+)["']/)
|
64
|
+
block_depth -= line.scan(/\bend\b/).size
|
65
|
+
|
66
|
+
if block_depth.zero?
|
67
|
+
@tables[current_table] = { start: table_start, end: table_end }
|
68
|
+
current_table = nil
|
69
|
+
block_depth = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
[current_table, table_start, block_depth]
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_diff_line_html(line, current_table)
|
76
|
+
sign = line[0]
|
77
|
+
line_content = line[1..]
|
78
|
+
color = SIGN_COLORS[sign]
|
79
|
+
|
80
|
+
action, name = detect_action_and_name(line_content, sign, current_table)
|
81
|
+
annotation = action ? find_migrations(action, current_table, name) : []
|
82
|
+
annotation.any? ? annotate_line(line, annotation, color) : colorize_html(line, color)
|
83
|
+
end
|
84
|
+
|
85
|
+
def annotate_line(line, migration_file_paths, color)
|
86
|
+
links_html = migration_file_paths.map { |path| link_to_migration(path) }.join(", ")
|
87
|
+
"#{colorize_html(line.chomp, color)}#{colorize_html(" // #{links_html} //", :gray)}\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
def colorize_html(text, color)
|
91
|
+
safe = ERB::Util.html_escape(text)
|
92
|
+
|
93
|
+
case color
|
94
|
+
when :green
|
95
|
+
%(<span style="color: green">#{safe}</span>)
|
96
|
+
when :red
|
97
|
+
%(<span style="color: red">#{safe}</span>)
|
98
|
+
when :gray
|
99
|
+
%(<span style="color: gray">#{text}</span>)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def link_to_migration(migration_file_path)
|
104
|
+
migration = migrations.detect { |m| m.filename == migration_file_path }
|
105
|
+
return ERB::Util.html_escape(migration_file_path) unless migration
|
106
|
+
|
107
|
+
url = "migrations/#{migration.version}?database=#{migration.database}"
|
108
|
+
"<a href=\"#{url}\">#{ERB::Util.html_escape(migration_file_path)}</a>"
|
109
|
+
end
|
110
|
+
|
111
|
+
def migrations
|
112
|
+
@migrations ||= ActualDbSchema::Migration.instance.all
|
113
|
+
end
|
114
|
+
|
115
|
+
def extract_table_section(full_diff_html, table_name)
|
116
|
+
return unless @tables[table_name]
|
117
|
+
|
118
|
+
range = @tables[table_name]
|
119
|
+
full_diff_html.lines[range[:start]..range[:end]].join
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require "parser/ast/processor"
|
5
|
+
|
6
|
+
module ActualDbSchema
|
7
|
+
# Parses the content of a `schema.rb` file into a structured hash representation.
|
8
|
+
module SchemaParser
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def parse_string(schema_content)
|
12
|
+
buffer = Parser::Source::Buffer.new("(schema)")
|
13
|
+
buffer.source = schema_content
|
14
|
+
parser = Parser::CurrentRuby.new
|
15
|
+
ast = parser.parse(buffer)
|
16
|
+
|
17
|
+
collector = SchemaCollector.new
|
18
|
+
collector.process(ast)
|
19
|
+
collector.schema
|
20
|
+
end
|
21
|
+
|
22
|
+
# Internal class used to process the AST and collect schema information.
|
23
|
+
class SchemaCollector < Parser::AST::Processor
|
24
|
+
attr_reader :schema
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
super()
|
28
|
+
@schema = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_block(node)
|
32
|
+
send_node, _args_node, body = *node
|
33
|
+
|
34
|
+
if create_table_call?(send_node)
|
35
|
+
table_name = extract_table_name(send_node)
|
36
|
+
columns = extract_columns(body)
|
37
|
+
@schema[table_name] = columns if table_name
|
38
|
+
end
|
39
|
+
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_send(node)
|
44
|
+
_receiver, method_name, *args = *node
|
45
|
+
if method_name == :create_table && args.any?
|
46
|
+
table_name = extract_table_name(node)
|
47
|
+
@schema[table_name] ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def create_table_call?(node)
|
56
|
+
return false unless node.is_a?(Parser::AST::Node)
|
57
|
+
|
58
|
+
_receiver, method_name, *_args = node.children
|
59
|
+
method_name == :create_table
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_table_name(send_node)
|
63
|
+
_receiver, _method_name, table_arg, *_rest = send_node.children
|
64
|
+
return unless table_arg
|
65
|
+
|
66
|
+
case table_arg.type
|
67
|
+
when :str then table_arg.children.first
|
68
|
+
when :sym then table_arg.children.first.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_columns(body_node)
|
73
|
+
return {} unless body_node
|
74
|
+
|
75
|
+
children = body_node.type == :begin ? body_node.children : [body_node]
|
76
|
+
|
77
|
+
columns = {}
|
78
|
+
children.each do |expr|
|
79
|
+
col = process_column_node(expr)
|
80
|
+
columns[col[:name]] = { type: col[:type], options: col[:options] } if col && col[:name]
|
81
|
+
end
|
82
|
+
columns
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_column_node(node)
|
86
|
+
return unless node.is_a?(Parser::AST::Node)
|
87
|
+
return unless node.type == :send
|
88
|
+
|
89
|
+
receiver, method_name, column_node, *args = node.children
|
90
|
+
|
91
|
+
return unless receiver && receiver.type == :lvar
|
92
|
+
|
93
|
+
return { name: "timestamps", type: :timestamps, options: {} } if method_name == :timestamps
|
94
|
+
|
95
|
+
col_name = extract_column_name(column_node)
|
96
|
+
options = extract_column_options(args)
|
97
|
+
|
98
|
+
{ name: col_name, type: method_name, options: options }
|
99
|
+
end
|
100
|
+
|
101
|
+
def extract_column_name(node)
|
102
|
+
return nil unless node.is_a?(Parser::AST::Node)
|
103
|
+
|
104
|
+
case node.type
|
105
|
+
when :str then node.children.first
|
106
|
+
when :sym then node.children.first.to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def extract_column_options(args)
|
111
|
+
opts = {}
|
112
|
+
args.each do |arg|
|
113
|
+
next unless arg && arg.type == :hash
|
114
|
+
|
115
|
+
opts.merge!(parse_hash(arg))
|
116
|
+
end
|
117
|
+
opts
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_hash(node)
|
121
|
+
hash = {}
|
122
|
+
return hash unless node && node.type == :hash
|
123
|
+
|
124
|
+
node.children.each do |pair|
|
125
|
+
key_node, value_node = pair.children
|
126
|
+
key = extract_key(key_node)
|
127
|
+
value = extract_literal(value_node)
|
128
|
+
hash[key] = value
|
129
|
+
end
|
130
|
+
hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def extract_key(node)
|
134
|
+
return unless node.is_a?(Parser::AST::Node)
|
135
|
+
|
136
|
+
case node.type
|
137
|
+
when :sym then node.children.first
|
138
|
+
when :str then node.children.first.to_sym
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def extract_literal(node)
|
143
|
+
return unless node.is_a?(Parser::AST::Node)
|
144
|
+
|
145
|
+
case node.type
|
146
|
+
when :int, :str, :sym then node.children.first
|
147
|
+
when true then true
|
148
|
+
when false then false
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/actual_db_schema.rb
CHANGED
@@ -10,12 +10,17 @@ require_relative "actual_db_schema/version"
|
|
10
10
|
require_relative "actual_db_schema/migration"
|
11
11
|
require_relative "actual_db_schema/failed_migration"
|
12
12
|
require_relative "actual_db_schema/migration_context"
|
13
|
+
require_relative "actual_db_schema/migration_parser"
|
13
14
|
require_relative "actual_db_schema/output_formatter"
|
14
15
|
require_relative "actual_db_schema/patches/migration_proxy"
|
15
16
|
require_relative "actual_db_schema/patches/migrator"
|
16
17
|
require_relative "actual_db_schema/patches/migration_context"
|
17
18
|
require_relative "actual_db_schema/git_hooks"
|
18
19
|
require_relative "actual_db_schema/multi_tenant"
|
20
|
+
require_relative "actual_db_schema/railtie"
|
21
|
+
require_relative "actual_db_schema/schema_diff"
|
22
|
+
require_relative "actual_db_schema/schema_diff_html"
|
23
|
+
require_relative "actual_db_schema/schema_parser"
|
19
24
|
|
20
25
|
require_relative "actual_db_schema/commands/base"
|
21
26
|
require_relative "actual_db_schema/commands/rollback"
|
@@ -54,7 +59,7 @@ module ActualDbSchema
|
|
54
59
|
end
|
55
60
|
|
56
61
|
def self.default_migrated_folder
|
57
|
-
Rails.root.join("tmp", "migrated")
|
62
|
+
config[:migrated_folder] || Rails.root.join("tmp", "migrated")
|
58
63
|
end
|
59
64
|
|
60
65
|
def self.migrations_paths
|
@@ -20,4 +20,12 @@ ActualDbSchema.configure do |config|
|
|
20
20
|
|
21
21
|
# If your application leverages multiple schemas for multi-tenancy, define the active schemas.
|
22
22
|
# config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] }
|
23
|
+
|
24
|
+
# Enable console migrations.
|
25
|
+
# config.console_migrations_enabled = true
|
26
|
+
config.console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?
|
27
|
+
|
28
|
+
# Define the migrated folder location.
|
29
|
+
# config.migrated_folder = Rails.root.join("custom", "migrated")
|
30
|
+
config.migrated_folder = Rails.root.join("tmp", "migrated")
|
23
31
|
end
|
@@ -53,4 +53,37 @@ namespace :actual_db_schema do # rubocop:disable Metrics/BlockLength
|
|
53
53
|
ActualDbSchema::GitHooks.new(strategy: strategy).install_post_checkout_hook
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
desc "Show the schema.rb diff annotated with the migrations that made the changes"
|
58
|
+
task :diff_schema_with_migrations, %i[schema_path migrations_path] => :environment do |_, args|
|
59
|
+
schema_path = args[:schema_path] || "./db/schema.rb"
|
60
|
+
migrations_path = args[:migrations_path] || "db/migrate"
|
61
|
+
|
62
|
+
schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)
|
63
|
+
puts schema_diff.render
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Delete broken migration versions from the database"
|
67
|
+
task :delete_broken_versions, %i[versions database] => :environment do |_, args|
|
68
|
+
extend ActualDbSchema::OutputFormatter
|
69
|
+
|
70
|
+
if args[:versions]
|
71
|
+
versions = args[:versions].split(" ").map(&:strip)
|
72
|
+
versions.each do |version|
|
73
|
+
ActualDbSchema::Migration.instance.delete(version, args[:database])
|
74
|
+
puts colorize("[ActualDbSchema] Migration #{version} was successfully deleted.", :green)
|
75
|
+
rescue StandardError => e
|
76
|
+
puts colorize("[ActualDbSchema] Error deleting version #{version}: #{e.message}", :red)
|
77
|
+
end
|
78
|
+
elsif ActualDbSchema::Migration.instance.broken_versions.empty?
|
79
|
+
puts colorize("[ActualDbSchema] No broken versions found.", :gray)
|
80
|
+
else
|
81
|
+
begin
|
82
|
+
ActualDbSchema::Migration.instance.delete_all
|
83
|
+
puts colorize("[ActualDbSchema] All broken versions were successfully deleted.", :green)
|
84
|
+
rescue StandardError => e
|
85
|
+
puts colorize("[ActualDbSchema] Error deleting all broken versions: #{e.message}", :red)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
56
89
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actual_db_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrei Kaleshka
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ast
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: csv
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: parser
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: appraisal
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -127,12 +155,16 @@ files:
|
|
127
155
|
- README.md
|
128
156
|
- Rakefile
|
129
157
|
- actual_db_schema.gemspec
|
158
|
+
- app/controllers/actual_db_schema/broken_versions_controller.rb
|
130
159
|
- app/controllers/actual_db_schema/migrations_controller.rb
|
131
160
|
- app/controllers/actual_db_schema/phantom_migrations_controller.rb
|
161
|
+
- app/controllers/actual_db_schema/schema_controller.rb
|
162
|
+
- app/views/actual_db_schema/broken_versions/index.html.erb
|
132
163
|
- app/views/actual_db_schema/migrations/index.html.erb
|
133
164
|
- app/views/actual_db_schema/migrations/show.html.erb
|
134
165
|
- app/views/actual_db_schema/phantom_migrations/index.html.erb
|
135
166
|
- app/views/actual_db_schema/phantom_migrations/show.html.erb
|
167
|
+
- app/views/actual_db_schema/schema/index.html.erb
|
136
168
|
- app/views/actual_db_schema/shared/_js.html
|
137
169
|
- app/views/actual_db_schema/shared/_style.html
|
138
170
|
- config/routes.rb
|
@@ -149,17 +181,23 @@ files:
|
|
149
181
|
- lib/actual_db_schema/commands/list.rb
|
150
182
|
- lib/actual_db_schema/commands/rollback.rb
|
151
183
|
- lib/actual_db_schema/configuration.rb
|
184
|
+
- lib/actual_db_schema/console_migrations.rb
|
152
185
|
- lib/actual_db_schema/engine.rb
|
153
186
|
- lib/actual_db_schema/failed_migration.rb
|
154
187
|
- lib/actual_db_schema/git.rb
|
155
188
|
- lib/actual_db_schema/git_hooks.rb
|
156
189
|
- lib/actual_db_schema/migration.rb
|
157
190
|
- lib/actual_db_schema/migration_context.rb
|
191
|
+
- lib/actual_db_schema/migration_parser.rb
|
158
192
|
- lib/actual_db_schema/multi_tenant.rb
|
159
193
|
- lib/actual_db_schema/output_formatter.rb
|
160
194
|
- lib/actual_db_schema/patches/migration_context.rb
|
161
195
|
- lib/actual_db_schema/patches/migration_proxy.rb
|
162
196
|
- lib/actual_db_schema/patches/migrator.rb
|
197
|
+
- lib/actual_db_schema/railtie.rb
|
198
|
+
- lib/actual_db_schema/schema_diff.rb
|
199
|
+
- lib/actual_db_schema/schema_diff_html.rb
|
200
|
+
- lib/actual_db_schema/schema_parser.rb
|
163
201
|
- lib/actual_db_schema/store.rb
|
164
202
|
- lib/actual_db_schema/version.rb
|
165
203
|
- lib/generators/actual_db_schema/templates/actual_db_schema.rb
|