droppable_table 0.1.0
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 +7 -0
- data/.rubocop.yml +58 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +154 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +12 -0
- data/bin/console +11 -0
- data/bin/droppable_table +7 -0
- data/bin/setup +8 -0
- data/config/known_gems.yml +34 -0
- data/config/rails_internal_tables.yml +19 -0
- data/droppable_table.yml.sample +19 -0
- data/lib/droppable_table/analyzer.rb +230 -0
- data/lib/droppable_table/cli.rb +87 -0
- data/lib/droppable_table/config.rb +80 -0
- data/lib/droppable_table/model_collector.rb +131 -0
- data/lib/droppable_table/schema_parser.rb +57 -0
- data/lib/droppable_table/version.rb +5 -0
- data/lib/droppable_table.rb +15 -0
- data/sig/droppable_table.rbs +103 -0
- metadata +141 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module DroppableTable
|
7
|
+
class Analyzer
|
8
|
+
attr_reader :config, :schema_tables, :model_tables, :sti_base_tables,
|
9
|
+
:habtm_tables, :droppable_tables, :excluded_tables
|
10
|
+
|
11
|
+
def initialize(config = nil)
|
12
|
+
@config = config.is_a?(Config) ? config : Config.new(config)
|
13
|
+
@schema_tables = {} # DB name => array of table info
|
14
|
+
@model_tables = Set.new # Table names with corresponding models
|
15
|
+
@sti_base_tables = Set.new # STI base tables
|
16
|
+
@habtm_tables = Set.new # HABTM join tables
|
17
|
+
@droppable_tables = [] # Potentially droppable tables
|
18
|
+
@excluded_tables = @config.all_excluded_tables # Rails internal + gems + user defined
|
19
|
+
@model_collector = nil
|
20
|
+
@schema_format = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def analyze
|
24
|
+
check_migration_status # Check for pending migrations
|
25
|
+
detect_schema_format # Detect schema.rb vs structure.sql
|
26
|
+
collect_schema_tables # Collect tables from all schema files
|
27
|
+
eager_load_models # Ensure all models are loaded
|
28
|
+
collect_model_tables # Collect from ActiveRecord::Base.descendants
|
29
|
+
collect_sti_tables # Detect STI hierarchies
|
30
|
+
collect_habtm_tables # Detect HABTM associations
|
31
|
+
identify_droppable_tables # Identify droppable tables
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def report(format: :text)
|
37
|
+
# TODO: Implement report generation
|
38
|
+
case format
|
39
|
+
when :json
|
40
|
+
generate_json_report
|
41
|
+
else
|
42
|
+
generate_text_report
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def load_excluded_tables
|
49
|
+
@config.all_excluded_tables
|
50
|
+
end
|
51
|
+
|
52
|
+
def check_migration_status
|
53
|
+
# Skip migration check in test environment or if Rails/ActiveRecord not available
|
54
|
+
return if ENV["RAILS_ENV"] == "test"
|
55
|
+
return unless defined?(ActiveRecord::Base) && defined?(Rails)
|
56
|
+
|
57
|
+
begin
|
58
|
+
# Ensure database connection exists
|
59
|
+
ActiveRecord::Base.connection
|
60
|
+
|
61
|
+
# Try to check for pending migrations using available methods
|
62
|
+
if ActiveRecord::Base.connection.respond_to?(:migration_context)
|
63
|
+
# Rails 6.0+
|
64
|
+
context = ActiveRecord::Base.connection.migration_context
|
65
|
+
if context.needs_migration?
|
66
|
+
raise MigrationPendingError, "There are pending migrations. Please run migrations before analyzing."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
# If migration check methods aren't available, skip the check
|
70
|
+
rescue ActiveRecord::NoDatabaseError
|
71
|
+
raise Error, "Database does not exist. Please create and migrate the database first."
|
72
|
+
rescue StandardError
|
73
|
+
# If any other error occurs during migration check, skip it
|
74
|
+
# This ensures the tool remains functional even with different Rails versions
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def detect_schema_format
|
79
|
+
schema_rb = File.join(Dir.pwd, "db", "schema.rb")
|
80
|
+
structure_sql = File.join(Dir.pwd, "db", "structure.sql")
|
81
|
+
|
82
|
+
if File.exist?(schema_rb)
|
83
|
+
@schema_format = :ruby
|
84
|
+
elsif File.exist?(structure_sql)
|
85
|
+
@schema_format = :sql
|
86
|
+
else
|
87
|
+
raise SchemaNotFoundError, "No schema.rb or structure.sql found in db/ directory"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def collect_schema_tables
|
92
|
+
# Find all schema files (including from multiple databases)
|
93
|
+
schema_files = find_schema_files
|
94
|
+
|
95
|
+
schema_files.each do |schema_file|
|
96
|
+
parser = SchemaParser.new(schema_file)
|
97
|
+
tables = parser.parse
|
98
|
+
|
99
|
+
# Determine database name from file path
|
100
|
+
db_name = extract_database_name(schema_file)
|
101
|
+
@schema_tables[db_name] = tables
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_schema_files
|
106
|
+
files = []
|
107
|
+
|
108
|
+
# Main schema file
|
109
|
+
main_schema = File.join(Dir.pwd, "db", @schema_format == :sql ? "structure.sql" : "schema.rb")
|
110
|
+
files << main_schema if File.exist?(main_schema)
|
111
|
+
|
112
|
+
# Look for additional schema files (e.g., secondary_schema.rb)
|
113
|
+
Dir.glob(File.join(Dir.pwd, "db", "*_schema.rb")).each do |file|
|
114
|
+
files << file unless file == main_schema
|
115
|
+
end
|
116
|
+
|
117
|
+
files
|
118
|
+
end
|
119
|
+
|
120
|
+
def extract_database_name(schema_file)
|
121
|
+
basename = File.basename(schema_file, ".*")
|
122
|
+
|
123
|
+
case basename
|
124
|
+
when "schema", "structure"
|
125
|
+
"primary"
|
126
|
+
else
|
127
|
+
# Extract database name from filename like 'secondary_schema.rb'
|
128
|
+
basename.sub(/_schema$/, "")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def eager_load_models
|
133
|
+
@model_collector = ModelCollector.new
|
134
|
+
@model_collector.collect
|
135
|
+
end
|
136
|
+
|
137
|
+
def collect_model_tables
|
138
|
+
return unless @model_collector
|
139
|
+
|
140
|
+
@model_tables = @model_collector.table_names
|
141
|
+
end
|
142
|
+
|
143
|
+
def collect_sti_tables
|
144
|
+
return unless @model_collector
|
145
|
+
|
146
|
+
@sti_base_tables = @model_collector.sti_base_tables
|
147
|
+
end
|
148
|
+
|
149
|
+
def collect_habtm_tables
|
150
|
+
return unless @model_collector
|
151
|
+
|
152
|
+
@habtm_tables = @model_collector.habtm_tables
|
153
|
+
end
|
154
|
+
|
155
|
+
def identify_droppable_tables
|
156
|
+
all_schema_tables = @schema_tables.values.flatten.to_set { |t| t[:name] }
|
157
|
+
|
158
|
+
# Tables that exist in schema but have no corresponding model
|
159
|
+
@droppable_tables = all_schema_tables.reject do |table|
|
160
|
+
# Skip if table is excluded
|
161
|
+
@excluded_tables.include?(table) ||
|
162
|
+
# Skip if table has a model
|
163
|
+
@model_tables.include?(table) ||
|
164
|
+
# Skip if it's a HABTM join table
|
165
|
+
@habtm_tables.include?(table) ||
|
166
|
+
# Skip if it's an STI base table (these are important)
|
167
|
+
@sti_base_tables.include?(table)
|
168
|
+
end.to_a.sort
|
169
|
+
end
|
170
|
+
|
171
|
+
def generate_json_report
|
172
|
+
{
|
173
|
+
summary: {
|
174
|
+
total_tables: @schema_tables.values.flatten.map { |t| t[:name] }.uniq.size,
|
175
|
+
tables_with_models: @model_tables.size,
|
176
|
+
sti_base_tables: @sti_base_tables.size,
|
177
|
+
habtm_tables: @habtm_tables.size,
|
178
|
+
excluded_tables: @excluded_tables.size,
|
179
|
+
droppable_tables: @droppable_tables.size
|
180
|
+
},
|
181
|
+
droppable_tables: @droppable_tables,
|
182
|
+
tables_by_database: @schema_tables.transform_values { |tables| tables.map { |t| t[:name] } },
|
183
|
+
model_tables: @model_tables.to_a.sort,
|
184
|
+
sti_base_tables: @sti_base_tables.to_a.sort,
|
185
|
+
habtm_tables: @habtm_tables.to_a.sort,
|
186
|
+
excluded_tables: @excluded_tables.to_a.sort
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
def generate_text_report
|
191
|
+
report = []
|
192
|
+
report << "DroppableTable Analysis Report"
|
193
|
+
report << ("=" * 40)
|
194
|
+
report << ""
|
195
|
+
|
196
|
+
# Summary
|
197
|
+
all_tables = @schema_tables.values.flatten.map { |t| t[:name] }.uniq
|
198
|
+
report << "Summary:"
|
199
|
+
report << " Total tables in schema: #{all_tables.size}"
|
200
|
+
report << " Tables with models: #{@model_tables.size}"
|
201
|
+
report << " STI base tables: #{@sti_base_tables.size}"
|
202
|
+
report << " HABTM join tables: #{@habtm_tables.size}"
|
203
|
+
report << " Excluded tables: #{@excluded_tables.size}"
|
204
|
+
report << " Potentially droppable: #{@droppable_tables.size}"
|
205
|
+
report << ""
|
206
|
+
|
207
|
+
# Droppable tables
|
208
|
+
if @droppable_tables.empty?
|
209
|
+
report << "No droppable tables found."
|
210
|
+
else
|
211
|
+
report << "Potentially droppable tables:"
|
212
|
+
@droppable_tables.each do |table|
|
213
|
+
report << " - #{table}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
report << ""
|
217
|
+
|
218
|
+
# Tables by database
|
219
|
+
if @schema_tables.size > 1
|
220
|
+
report << "Tables by database:"
|
221
|
+
@schema_tables.each do |db_name, tables|
|
222
|
+
report << " #{db_name}: #{tables.size} tables"
|
223
|
+
end
|
224
|
+
report << ""
|
225
|
+
end
|
226
|
+
|
227
|
+
report.join("\n")
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module DroppableTable
|
7
|
+
class CLI < Thor
|
8
|
+
package_name "DroppableTable"
|
9
|
+
|
10
|
+
desc "analyze", "Analyze Rails application for potentially droppable tables"
|
11
|
+
option :json, type: :boolean, desc: "Output results in JSON format"
|
12
|
+
option :config, type: :string, desc: "Path to configuration file"
|
13
|
+
option :strict, type: :boolean, desc: "Strict mode for CI (fail if new droppable tables found)"
|
14
|
+
def analyze
|
15
|
+
# Load configuration
|
16
|
+
config = Config.new(options[:config])
|
17
|
+
|
18
|
+
# Run analysis
|
19
|
+
analyzer = Analyzer.new(config)
|
20
|
+
analyzer.analyze
|
21
|
+
|
22
|
+
# Generate report
|
23
|
+
if options[:json]
|
24
|
+
puts JSON.pretty_generate(analyzer.report(format: :json))
|
25
|
+
else
|
26
|
+
puts analyzer.report(format: :text)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Handle strict mode
|
30
|
+
handle_strict_mode(analyzer, config) if options[:strict] && config.strict_mode_enabled?
|
31
|
+
|
32
|
+
# Exit with appropriate code
|
33
|
+
exit(analyzer.droppable_tables.empty? ? 0 : 1) if options[:strict]
|
34
|
+
rescue RailsNotFoundError => e
|
35
|
+
error_exit("Rails application not found: #{e.message}")
|
36
|
+
rescue MigrationPendingError => e
|
37
|
+
error_exit("Migration pending: #{e.message}")
|
38
|
+
rescue SchemaNotFoundError => e
|
39
|
+
error_exit("Schema not found: #{e.message}")
|
40
|
+
rescue StandardError => e
|
41
|
+
error_exit("Error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "version", "Display version"
|
45
|
+
def version
|
46
|
+
puts "DroppableTable #{DroppableTable::VERSION}"
|
47
|
+
end
|
48
|
+
map %w[-v --version] => :version
|
49
|
+
|
50
|
+
def self.exit_on_failure?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
default_task :analyze
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def handle_strict_mode(analyzer, config)
|
59
|
+
baseline_file = config.baseline_file
|
60
|
+
current_droppable = analyzer.droppable_tables.sort
|
61
|
+
|
62
|
+
if File.exist?(baseline_file)
|
63
|
+
baseline = JSON.parse(File.read(baseline_file))["droppable_tables"] || []
|
64
|
+
new_tables = current_droppable - baseline
|
65
|
+
|
66
|
+
if new_tables.any?
|
67
|
+
puts "\nERROR: New droppable tables found in strict mode:"
|
68
|
+
new_tables.each { |table| puts " - #{table}" }
|
69
|
+
puts "\nTo update the baseline, run without --strict flag."
|
70
|
+
exit(1)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
# Create baseline file
|
74
|
+
File.write(baseline_file, JSON.pretty_generate({
|
75
|
+
droppable_tables: current_droppable,
|
76
|
+
generated_at: Time.now.iso8601
|
77
|
+
}))
|
78
|
+
puts "\nCreated baseline file: #{baseline_file}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def error_exit(message)
|
83
|
+
warn "ERROR: #{message}"
|
84
|
+
exit(1)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
module DroppableTable
|
7
|
+
class Config
|
8
|
+
DEFAULT_CONFIG_FILE = "droppable_table.yml"
|
9
|
+
RAILS_INTERNAL_TABLES_FILE = File.expand_path("../../config/rails_internal_tables.yml", __dir__)
|
10
|
+
KNOWN_GEMS_FILE = File.expand_path("../../config/known_gems.yml", __dir__)
|
11
|
+
|
12
|
+
attr_reader :excluded_tables, :excluded_gems, :strict_mode, :config_file_path
|
13
|
+
|
14
|
+
def initialize(config_file_path = nil)
|
15
|
+
@config_file_path = config_file_path || DEFAULT_CONFIG_FILE
|
16
|
+
@excluded_tables = Set.new
|
17
|
+
@excluded_gems = Set.new
|
18
|
+
@strict_mode = {}
|
19
|
+
|
20
|
+
load_default_config
|
21
|
+
load_user_config if File.exist?(@config_file_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def rails_internal_tables
|
25
|
+
@rails_internal_tables ||= load_yaml_file(RAILS_INTERNAL_TABLES_FILE)
|
26
|
+
end
|
27
|
+
|
28
|
+
def known_gem_tables
|
29
|
+
@known_gem_tables ||= load_yaml_file(KNOWN_GEMS_FILE)
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_excluded_tables
|
33
|
+
Set.new(excluded_tables) + Set.new(rails_internal_tables) + gem_excluded_tables
|
34
|
+
end
|
35
|
+
|
36
|
+
def strict_mode_enabled?
|
37
|
+
strict_mode["enabled"] == true
|
38
|
+
end
|
39
|
+
|
40
|
+
def baseline_file
|
41
|
+
strict_mode["baseline_file"] || ".droppable_table_baseline.json"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def load_default_config
|
47
|
+
@rails_internal_tables = load_yaml_file(RAILS_INTERNAL_TABLES_FILE) || []
|
48
|
+
@known_gem_tables = load_yaml_file(KNOWN_GEMS_FILE) || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_user_config
|
52
|
+
# TODO: Load user configuration from YAML file
|
53
|
+
config = load_yaml_file(@config_file_path)
|
54
|
+
return unless config.is_a?(Hash)
|
55
|
+
|
56
|
+
@excluded_tables = Set.new(config["excluded_tables"] || [])
|
57
|
+
@excluded_gems = Set.new(config["excluded_gems"] || [])
|
58
|
+
@strict_mode = config["strict_mode"] || {}
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_yaml_file(path)
|
62
|
+
return [] unless File.exist?(path)
|
63
|
+
|
64
|
+
content = File.read(path)
|
65
|
+
YAML.safe_load(content, permitted_classes: [Symbol], aliases: true) || []
|
66
|
+
rescue StandardError
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
|
70
|
+
def gem_excluded_tables
|
71
|
+
tables = Set.new
|
72
|
+
|
73
|
+
excluded_gems.each do |gem_name|
|
74
|
+
tables += known_gem_tables[gem_name] if known_gem_tables.is_a?(Hash) && known_gem_tables[gem_name]
|
75
|
+
end
|
76
|
+
|
77
|
+
tables
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module DroppableTable
|
6
|
+
class ModelCollector
|
7
|
+
attr_reader :models, :table_mapping
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@models = []
|
11
|
+
@table_mapping = {} # model_class => table_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def collect
|
15
|
+
ensure_rails_loaded
|
16
|
+
eager_load_all_models
|
17
|
+
collect_all_descendants
|
18
|
+
build_table_mapping
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def table_names
|
24
|
+
Set.new(table_mapping.values)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sti_base_tables
|
28
|
+
sti_tables = Set.new
|
29
|
+
|
30
|
+
models.each do |model|
|
31
|
+
next if model.abstract_class?
|
32
|
+
|
33
|
+
# Check if this model uses STI (has a 'type' column)
|
34
|
+
sti_tables << model.table_name if model.columns_hash.key?("type") && model.base_class == model
|
35
|
+
rescue StandardError
|
36
|
+
# Skip models that can't be inspected
|
37
|
+
end
|
38
|
+
|
39
|
+
sti_tables
|
40
|
+
end
|
41
|
+
|
42
|
+
def habtm_tables
|
43
|
+
habtm_join_tables = Set.new
|
44
|
+
|
45
|
+
models.each do |model|
|
46
|
+
next if model.abstract_class?
|
47
|
+
|
48
|
+
# Find HABTM associations
|
49
|
+
model.reflect_on_all_associations(:has_and_belongs_to_many).each do |association|
|
50
|
+
join_table = association.join_table
|
51
|
+
habtm_join_tables << join_table if join_table
|
52
|
+
end
|
53
|
+
rescue StandardError
|
54
|
+
# Skip models that can't be inspected
|
55
|
+
end
|
56
|
+
|
57
|
+
habtm_join_tables
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def ensure_rails_loaded
|
63
|
+
return if defined?(Rails)
|
64
|
+
|
65
|
+
# Try to load Rails if we're in a Rails directory
|
66
|
+
rails_path = File.join(Dir.pwd, "config", "environment.rb")
|
67
|
+
unless File.exist?(rails_path)
|
68
|
+
raise RailsNotFoundError, "Rails not found. Please run this command from a Rails application directory."
|
69
|
+
end
|
70
|
+
|
71
|
+
ENV["RAILS_ENV"] ||= "development"
|
72
|
+
require rails_path
|
73
|
+
end
|
74
|
+
|
75
|
+
def eager_load_all_models
|
76
|
+
return unless defined?(Rails)
|
77
|
+
|
78
|
+
# Eager load the application to ensure all models are loaded
|
79
|
+
Rails.application.eager_load!
|
80
|
+
|
81
|
+
# Also load models from engines
|
82
|
+
Rails::Engine.subclasses.each do |engine|
|
83
|
+
engine.instance.eager_load!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def collect_all_descendants
|
88
|
+
return unless defined?(ActiveRecord::Base)
|
89
|
+
|
90
|
+
# Collect all ActiveRecord descendants, excluding abstract classes
|
91
|
+
@models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_table_mapping
|
95
|
+
models.each do |model|
|
96
|
+
next if model.abstract_class?
|
97
|
+
|
98
|
+
begin
|
99
|
+
# Check if model responds to table_name
|
100
|
+
if model.respond_to?(:table_name)
|
101
|
+
table_name = model.table_name
|
102
|
+
@table_mapping[model] = table_name if table_name && !table_name.empty?
|
103
|
+
end
|
104
|
+
rescue StandardError
|
105
|
+
# Skip models that raise errors when accessing table_name
|
106
|
+
# This can happen with models that have database connection issues
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def resolve_table_name(model)
|
112
|
+
return nil unless model.respond_to?(:table_name)
|
113
|
+
|
114
|
+
# Get the table name, considering custom configurations
|
115
|
+
table_name = model.table_name
|
116
|
+
|
117
|
+
# Apply any table name prefix/suffix if configured
|
118
|
+
if model.respond_to?(:table_name_prefix) && model.table_name_prefix
|
119
|
+
table_name = "#{model.table_name_prefix}#{table_name}"
|
120
|
+
end
|
121
|
+
|
122
|
+
if model.respond_to?(:table_name_suffix) && model.table_name_suffix
|
123
|
+
table_name = "#{table_name}#{model.table_name_suffix}"
|
124
|
+
end
|
125
|
+
|
126
|
+
table_name
|
127
|
+
rescue StandardError
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DroppableTable
|
4
|
+
class SchemaParser
|
5
|
+
attr_reader :schema_path, :format
|
6
|
+
|
7
|
+
def initialize(schema_path)
|
8
|
+
@schema_path = schema_path
|
9
|
+
@format = detect_format
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse
|
13
|
+
case format
|
14
|
+
when :ruby
|
15
|
+
parse_ruby_schema
|
16
|
+
when :sql
|
17
|
+
parse_sql_schema
|
18
|
+
else
|
19
|
+
raise SchemaNotFoundError, "Unknown schema format: #{schema_path}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def detect_format
|
26
|
+
# TODO: Detect schema format based on file extension
|
27
|
+
case File.basename(schema_path)
|
28
|
+
when /\.rb$/
|
29
|
+
:ruby
|
30
|
+
when /\.sql$/
|
31
|
+
:sql
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_ruby_schema
|
36
|
+
return [] unless File.exist?(schema_path)
|
37
|
+
|
38
|
+
tables = []
|
39
|
+
File.readlines(schema_path).each do |line|
|
40
|
+
next unless (match = line.match(/^\s*create_table\s+"([^"]+)"/))
|
41
|
+
|
42
|
+
table_name = match[1]
|
43
|
+
tables << {
|
44
|
+
name: table_name,
|
45
|
+
type: "table"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
tables
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_sql_schema
|
53
|
+
# TODO: Parse structure.sql
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "droppable_table/version"
|
4
|
+
require_relative "droppable_table/cli"
|
5
|
+
require_relative "droppable_table/analyzer"
|
6
|
+
require_relative "droppable_table/schema_parser"
|
7
|
+
require_relative "droppable_table/model_collector"
|
8
|
+
require_relative "droppable_table/config"
|
9
|
+
|
10
|
+
module DroppableTable
|
11
|
+
class Error < StandardError; end
|
12
|
+
class RailsNotFoundError < Error; end
|
13
|
+
class MigrationPendingError < Error; end
|
14
|
+
class SchemaNotFoundError < Error; end
|
15
|
+
end
|