migflow 0.2.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 +44 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +45 -0
- data/CLAUDE.md +124 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +157 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +11 -0
- data/SECURITY.md +27 -0
- data/app/assets/migflow/app.css +1 -0
- data/app/assets/migflow/app.js +28 -0
- data/app/assets/migflow/index.html +14 -0
- data/app/assets/migflow/vite.svg +1 -0
- data/app/controllers/migflow/api/diff_controller.rb +73 -0
- data/app/controllers/migflow/api/migrations_controller.rb +97 -0
- data/app/controllers/migflow/application_controller.rb +62 -0
- data/app/views/migflow/application/index.html.erb +16 -0
- data/config/routes.rb +10 -0
- data/docs/architecture.md +130 -0
- data/lib/migflow/analyzers/audit_analyzer.rb +58 -0
- data/lib/migflow/analyzers/rules/base_rule.rb +32 -0
- data/lib/migflow/analyzers/rules/dangerous_migration_rule.rb +44 -0
- data/lib/migflow/analyzers/rules/missing_foreign_key_rule.rb +30 -0
- data/lib/migflow/analyzers/rules/missing_index_rule.rb +32 -0
- data/lib/migflow/analyzers/rules/missing_timestamps_rule.rb +38 -0
- data/lib/migflow/analyzers/rules/null_column_without_default_rule.rb +46 -0
- data/lib/migflow/analyzers/rules/string_without_limit_rule.rb +28 -0
- data/lib/migflow/app/assets/migflow/app.css +1 -0
- data/lib/migflow/app/assets/migflow/app.js +17 -0
- data/lib/migflow/app/assets/migflow/index.html +14 -0
- data/lib/migflow/app/assets/migflow/vite.svg +1 -0
- data/lib/migflow/configuration.rb +36 -0
- data/lib/migflow/engine.rb +14 -0
- data/lib/migflow/models/migration_snapshot.rb +15 -0
- data/lib/migflow/models/schema_diff.rb +9 -0
- data/lib/migflow/models/warning.rb +7 -0
- data/lib/migflow/parsers/migration_parser.rb +52 -0
- data/lib/migflow/parsers/schema_parser.rb +105 -0
- data/lib/migflow/reporters/json_reporter.rb +13 -0
- data/lib/migflow/reporters/markdown_reporter.rb +58 -0
- data/lib/migflow/reporters.rb +38 -0
- data/lib/migflow/services/diff_builder.rb +77 -0
- data/lib/migflow/services/migration_dsl_scanner.rb +161 -0
- data/lib/migflow/services/migration_summary_builder.rb +43 -0
- data/lib/migflow/services/report_generator.rb +76 -0
- data/lib/migflow/services/risk_scorer.rb +38 -0
- data/lib/migflow/services/schema_builder.rb +25 -0
- data/lib/migflow/services/schema_patch_builder.rb +237 -0
- data/lib/migflow/services/scoped_migration_warnings.rb +93 -0
- data/lib/migflow/services/snapshot_builder.rb +542 -0
- data/lib/migflow/services/touched_tables_from_migration.rb +60 -0
- data/lib/migflow/version.rb +5 -0
- data/lib/migflow.rb +20 -0
- data/lib/tasks/migflow.rake +31 -0
- data/sig/migflow.rbs +3 -0
- metadata +124 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parsers/migration_parser"
|
|
4
|
+
require_relative "snapshot_builder"
|
|
5
|
+
require_relative "scoped_migration_warnings"
|
|
6
|
+
require_relative "risk_scorer"
|
|
7
|
+
|
|
8
|
+
module Migflow
|
|
9
|
+
module Services
|
|
10
|
+
class ReportGenerator
|
|
11
|
+
def call(migrations_path:)
|
|
12
|
+
migrations = Parsers::MigrationParser.call(migrations_path: migrations_path)
|
|
13
|
+
scorer = RiskScorer.new
|
|
14
|
+
|
|
15
|
+
analyzed = migrations.map { |m| analyze(m, migrations, scorer) }
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
generated_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
19
|
+
summary: build_summary(analyzed),
|
|
20
|
+
migrations: analyzed
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def analyze(migration, all_migrations, scorer)
|
|
27
|
+
result = SnapshotBuilder.call(migrations: all_migrations, up_to_version: migration[:version])
|
|
28
|
+
snapshot = snapshot_model(result[:schema_after], migration[:version])
|
|
29
|
+
warnings = ScopedMigrationWarnings.call(snapshot: snapshot, migration: migration, diff: result[:diff])
|
|
30
|
+
risk = scorer.call(warnings)
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
version: migration[:version],
|
|
34
|
+
name: migration[:name],
|
|
35
|
+
risk_score: risk[:score],
|
|
36
|
+
risk_level: risk[:level],
|
|
37
|
+
warnings: warnings.map { |w| serialize_warning(w) }
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def snapshot_model(schema_after, version)
|
|
42
|
+
Models::MigrationSnapshot.new(
|
|
43
|
+
version: version,
|
|
44
|
+
name: "Historical schema",
|
|
45
|
+
tables: schema_after[:tables],
|
|
46
|
+
raw_content: ""
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serialize_warning(warning)
|
|
51
|
+
{
|
|
52
|
+
rule: warning.rule,
|
|
53
|
+
severity: warning.severity.to_s,
|
|
54
|
+
table: warning.table,
|
|
55
|
+
column: warning.column,
|
|
56
|
+
message: warning.message
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_summary(analyzed)
|
|
61
|
+
with_warnings = analyzed.count { |m| m[:warnings].any? }
|
|
62
|
+
with_errors = analyzed.count { |m| m[:warnings].any? { |w| w[:severity] == "error" } }
|
|
63
|
+
max_score = analyzed.map { |m| m[:risk_score] }.max || 0
|
|
64
|
+
max_level = analyzed.max_by { |m| m[:risk_score] }&.fetch(:risk_level) || "safe"
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
total_migrations: analyzed.size,
|
|
68
|
+
migrations_with_warnings: with_warnings,
|
|
69
|
+
migrations_with_errors: with_errors,
|
|
70
|
+
highest_risk_score: max_score,
|
|
71
|
+
highest_risk_level: max_level
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Migflow
|
|
4
|
+
module Services
|
|
5
|
+
class RiskScorer
|
|
6
|
+
RULE_WEIGHTS = {
|
|
7
|
+
"dangerous_migration_rule" => 40,
|
|
8
|
+
"missing_index_rule" => 15,
|
|
9
|
+
"missing_foreign_key_rule" => 20,
|
|
10
|
+
"string_without_limit_rule" => 5,
|
|
11
|
+
"null_column_without_default_rule" => 20,
|
|
12
|
+
"missing_timestamps_rule" => 5
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
LEVELS = [
|
|
16
|
+
{ min: 71, max: 100, level: "high" },
|
|
17
|
+
{ min: 31, max: 70, level: "medium" },
|
|
18
|
+
{ min: 1, max: 30, level: "low" },
|
|
19
|
+
{ min: 0, max: 0, level: "safe" }
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
def call(warnings)
|
|
23
|
+
factors = warnings.filter_map do |w|
|
|
24
|
+
weight = RULE_WEIGHTS[w.rule]
|
|
25
|
+
next unless weight
|
|
26
|
+
|
|
27
|
+
{ rule: w.rule, message: w.message, weight: weight }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
raw = factors.sum { |f| f[:weight] }
|
|
31
|
+
score = [raw, 100].min
|
|
32
|
+
level = LEVELS.find { |l| score.between?(l[:min], l[:max]) }&.fetch(:level, "safe")
|
|
33
|
+
|
|
34
|
+
{ score: score, level: level, factors: factors }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Migflow
|
|
4
|
+
module Services
|
|
5
|
+
class SchemaBuilder
|
|
6
|
+
def self.call(schema_path:)
|
|
7
|
+
new(schema_path: schema_path).build
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(schema_path:)
|
|
11
|
+
@schema_path = schema_path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
parsed = Parsers::SchemaParser.call(schema_path: @schema_path)
|
|
16
|
+
Models::MigrationSnapshot.new(
|
|
17
|
+
version: parsed[:version],
|
|
18
|
+
name: "Current schema",
|
|
19
|
+
tables: parsed[:tables],
|
|
20
|
+
raw_content: Pathname.new(@schema_path).read
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Migflow
|
|
4
|
+
module Services
|
|
5
|
+
class SchemaPatchBuilder
|
|
6
|
+
def self.call(from_tables:, to_tables:, changed_tables: nil, include_unchanged: false)
|
|
7
|
+
new(
|
|
8
|
+
from_tables: from_tables || {},
|
|
9
|
+
to_tables: to_tables || {},
|
|
10
|
+
changed_tables: changed_tables,
|
|
11
|
+
include_unchanged: include_unchanged
|
|
12
|
+
).build
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(from_tables:, to_tables:, changed_tables:, include_unchanged:)
|
|
16
|
+
@from_tables = from_tables
|
|
17
|
+
@to_tables = to_tables
|
|
18
|
+
@changed_tables = changed_tables&.to_set
|
|
19
|
+
@include_unchanged = include_unchanged
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build
|
|
23
|
+
document = build_document
|
|
24
|
+
lines = document[:lines]
|
|
25
|
+
has_diff = lines.any? { |line| ["+", "-"].include?(line[:prefix]) }
|
|
26
|
+
return "" unless has_diff
|
|
27
|
+
|
|
28
|
+
return build_full_patch(lines) if @include_unchanged
|
|
29
|
+
|
|
30
|
+
build_collapsed_patch(lines, document[:sections])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_full_patch(lines)
|
|
36
|
+
old_count = lines.count { |line| line[:prefix] != "+" }
|
|
37
|
+
new_count = lines.count { |line| line[:prefix] != "-" }
|
|
38
|
+
|
|
39
|
+
[
|
|
40
|
+
"diff --git a/schema.rb b/schema.rb",
|
|
41
|
+
"--- a/schema.rb",
|
|
42
|
+
"+++ b/schema.rb",
|
|
43
|
+
"@@ -1,#{old_count} +1,#{new_count} @@",
|
|
44
|
+
*lines.map { |line| "#{line[:prefix]}#{line[:content]}" },
|
|
45
|
+
""
|
|
46
|
+
].join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_collapsed_patch(lines, sections)
|
|
50
|
+
hunk_ranges = table_hunk_ranges(lines, sections)
|
|
51
|
+
return "" if hunk_ranges.empty?
|
|
52
|
+
|
|
53
|
+
hunk_blocks = hunk_ranges.map do |range|
|
|
54
|
+
old_start = line_number_before(lines, range.begin, :old) + 1
|
|
55
|
+
new_start = line_number_before(lines, range.begin, :new) + 1
|
|
56
|
+
hunk_lines = lines[range]
|
|
57
|
+
old_count = hunk_lines.count { |line| line[:prefix] != "+" }
|
|
58
|
+
new_count = hunk_lines.count { |line| line[:prefix] != "-" }
|
|
59
|
+
|
|
60
|
+
[
|
|
61
|
+
"@@ -#{old_start},#{old_count} +#{new_start},#{new_count} @@",
|
|
62
|
+
*hunk_lines.map { |line| "#{line[:prefix]}#{line[:content]}" }
|
|
63
|
+
].join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
[
|
|
67
|
+
"diff --git a/schema.rb b/schema.rb",
|
|
68
|
+
"--- a/schema.rb",
|
|
69
|
+
"+++ b/schema.rb",
|
|
70
|
+
*hunk_blocks,
|
|
71
|
+
""
|
|
72
|
+
].join("\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def table_hunk_ranges(lines, sections)
|
|
76
|
+
sections.each_with_object([]) do |section, ranges|
|
|
77
|
+
next unless section_changed?(lines, section)
|
|
78
|
+
|
|
79
|
+
# Keep schema block anchor visible so columns are not "floating"
|
|
80
|
+
# Always include full table block so each `create_table` has its
|
|
81
|
+
# matching `end` and changes never look nested/confusing.
|
|
82
|
+
ranges << (section[:start]..section[:end])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def line_number_before(lines, idx_exclusive, side)
|
|
87
|
+
slice = idx_exclusive.zero? ? [] : lines[0...idx_exclusive]
|
|
88
|
+
case side
|
|
89
|
+
when :old
|
|
90
|
+
slice.count { |line| line[:prefix] != "+" }
|
|
91
|
+
when :new
|
|
92
|
+
slice.count { |line| line[:prefix] != "-" }
|
|
93
|
+
else
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def section_changed?(lines, section)
|
|
99
|
+
has_diff_lines = (section[:start]..section[:end]).any? { |idx| %w[+ -].include?(lines[idx][:prefix]) }
|
|
100
|
+
return has_diff_lines if @changed_tables.nil?
|
|
101
|
+
return false unless @changed_tables.include?(section[:table_name])
|
|
102
|
+
|
|
103
|
+
has_diff_lines
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_document
|
|
107
|
+
lines = []
|
|
108
|
+
sections = []
|
|
109
|
+
all_table_names.each_with_index do |table_name, idx|
|
|
110
|
+
from_t = @from_tables[table_name]
|
|
111
|
+
to_t = @to_tables[table_name]
|
|
112
|
+
section_start = lines.length
|
|
113
|
+
section_lines = table_lines(table_name, from_t, to_t)
|
|
114
|
+
lines.concat(section_lines)
|
|
115
|
+
section_end = lines.length - 1
|
|
116
|
+
sections << { table_name: table_name, start: section_start, end: section_end }
|
|
117
|
+
lines << { prefix: " ", content: "" } if idx < all_table_names.length - 1
|
|
118
|
+
end
|
|
119
|
+
{ lines: lines, sections: sections }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def all_table_names
|
|
123
|
+
(@from_tables.keys + @to_tables.keys).uniq.sort
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def table_lines(table_name, from_t, to_t)
|
|
127
|
+
lines = []
|
|
128
|
+
is_added = from_t.nil? && !to_t.nil?
|
|
129
|
+
is_removed = !from_t.nil? && to_t.nil?
|
|
130
|
+
|
|
131
|
+
header_prefix = if is_added
|
|
132
|
+
"+"
|
|
133
|
+
else
|
|
134
|
+
is_removed ? "-" : " "
|
|
135
|
+
end
|
|
136
|
+
lines << { prefix: header_prefix, content: "create_table \"#{table_name}\" do |t|" }
|
|
137
|
+
|
|
138
|
+
from_columns = columns_map(from_t)
|
|
139
|
+
to_columns = columns_map(to_t)
|
|
140
|
+
(from_columns.keys + to_columns.keys).uniq.sort.each do |col_name|
|
|
141
|
+
from_col = from_columns[col_name]
|
|
142
|
+
to_col = to_columns[col_name]
|
|
143
|
+
if from_col.nil?
|
|
144
|
+
lines << { prefix: "+", content: " #{format_column(to_col)}" }
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if to_col.nil?
|
|
149
|
+
lines << { prefix: "-", content: " #{format_column(from_col)}" }
|
|
150
|
+
next
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if equivalent_column?(from_col, to_col)
|
|
154
|
+
lines << { prefix: " ", content: " #{format_column(to_col)}" }
|
|
155
|
+
else
|
|
156
|
+
lines << { prefix: "-", content: " #{format_column(from_col)}" }
|
|
157
|
+
lines << { prefix: "+", content: " #{format_column(to_col)}" }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
from_indexes = indexes_map(from_t)
|
|
162
|
+
to_indexes = indexes_map(to_t)
|
|
163
|
+
(from_indexes.keys + to_indexes.keys).uniq.sort.each do |idx_key|
|
|
164
|
+
from_idx = from_indexes[idx_key]
|
|
165
|
+
to_idx = to_indexes[idx_key]
|
|
166
|
+
if from_idx.nil?
|
|
167
|
+
lines << { prefix: "+", content: " #{format_index(to_idx)}" }
|
|
168
|
+
next
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if to_idx.nil?
|
|
172
|
+
lines << { prefix: "-", content: " #{format_index(from_idx)}" }
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if equivalent_index?(from_idx, to_idx)
|
|
177
|
+
lines << { prefix: " ", content: " #{format_index(to_idx)}" }
|
|
178
|
+
else
|
|
179
|
+
lines << { prefix: "-", content: " #{format_index(from_idx)}" }
|
|
180
|
+
lines << { prefix: "+", content: " #{format_index(to_idx)}" }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
lines << { prefix: " ", content: "end" }
|
|
185
|
+
lines
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def columns_map(table)
|
|
189
|
+
return {} unless table
|
|
190
|
+
|
|
191
|
+
table[:columns].to_h { |col| [col[:name], col] }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def indexes_map(table)
|
|
195
|
+
return {} unless table
|
|
196
|
+
|
|
197
|
+
table[:indexes].each_with_object({}) do |idx, memo|
|
|
198
|
+
key = idx[:name] || idx[:columns].join("_")
|
|
199
|
+
memo[key] = idx
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def format_column(col)
|
|
204
|
+
type = col[:type] || "string"
|
|
205
|
+
opts = []
|
|
206
|
+
opts << "null: false" if col[:null] == false
|
|
207
|
+
opts << "default: #{col[:default]}" unless col[:default].nil?
|
|
208
|
+
opts << "limit: #{col[:limit]}" unless col[:limit].nil?
|
|
209
|
+
base = "t.#{type} \"#{col[:name]}\""
|
|
210
|
+
opts.empty? ? base : "#{base}, #{opts.join(", ")}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def format_index(idx)
|
|
214
|
+
columns = "[#{idx[:columns].map { |c| "\"#{c}\"" }.join(", ")}]"
|
|
215
|
+
opts = []
|
|
216
|
+
opts << "name: \"#{idx[:name]}\"" if idx[:name]
|
|
217
|
+
opts << "unique: true" if idx[:unique]
|
|
218
|
+
base = "t.index #{columns}"
|
|
219
|
+
opts.empty? ? base : "#{base}, #{opts.join(", ")}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def equivalent_column?(one, other)
|
|
223
|
+
one[:name] == other[:name] &&
|
|
224
|
+
one[:type] == other[:type] &&
|
|
225
|
+
one[:null] == other[:null] &&
|
|
226
|
+
one[:default] == other[:default] &&
|
|
227
|
+
one[:limit] == other[:limit]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def equivalent_index?(one, other)
|
|
231
|
+
one[:name] == other[:name] &&
|
|
232
|
+
one[:unique] == other[:unique] &&
|
|
233
|
+
one[:columns] == other[:columns]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analyzers/audit_analyzer"
|
|
4
|
+
require_relative "../models/warning"
|
|
5
|
+
require_relative "touched_tables_from_migration"
|
|
6
|
+
|
|
7
|
+
module Migflow
|
|
8
|
+
module Services
|
|
9
|
+
class ScopedMigrationWarnings
|
|
10
|
+
MIGRATION_LEVEL_RULES = %w[
|
|
11
|
+
dangerous_migration_rule
|
|
12
|
+
null_column_without_default_rule
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
NOOP_INFO_RULE = "no_schema_change_migration_rule"
|
|
16
|
+
|
|
17
|
+
def self.call(snapshot:, migration:, diff: nil)
|
|
18
|
+
new(snapshot: snapshot, migration: migration, diff: diff).call
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(snapshot:, migration:, diff: nil)
|
|
22
|
+
@snapshot = snapshot
|
|
23
|
+
@migration = migration
|
|
24
|
+
@diff = diff
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
warnings = Analyzers::AuditAnalyzer.call(snapshot: @snapshot, raw_migrations: [@migration])
|
|
29
|
+
touched_tables = TouchedTablesFromMigration.call(raw_content: @migration[:raw_content])
|
|
30
|
+
|
|
31
|
+
if touched_tables.empty?
|
|
32
|
+
result = warnings.select { migration_level_warning?(_1) }
|
|
33
|
+
result << noop_migration_warning if noop_migration?(@migration[:raw_content].to_s)
|
|
34
|
+
return result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
warnings.select do |warning|
|
|
38
|
+
migration_level_warning?(warning) || relevant_schema_warning?(warning, touched_tables)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def relevant_schema_warning?(warning, touched_tables)
|
|
45
|
+
table = warning.table.to_s
|
|
46
|
+
return false unless touched_tables.include?(table)
|
|
47
|
+
return true if @diff.nil?
|
|
48
|
+
|
|
49
|
+
change_scoped_relevant?(warning, table)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def change_scoped_relevant?(warning, table)
|
|
53
|
+
return true if added_table?(table)
|
|
54
|
+
|
|
55
|
+
col = warning.column.to_s
|
|
56
|
+
return false if col.empty?
|
|
57
|
+
|
|
58
|
+
added_columns_for(table).include?(col)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def added_table?(table)
|
|
62
|
+
(@diff[:added_tables] || []).map(&:to_s).include?(table)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def added_columns_for(table)
|
|
66
|
+
entry = (@diff[:modified_tables] || {})[table] || {}
|
|
67
|
+
(entry[:added_columns] || []).map(&:to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def migration_level_warning?(warning)
|
|
71
|
+
MIGRATION_LEVEL_RULES.include?(warning.rule.to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def noop_migration?(content)
|
|
75
|
+
return false if content.match?(/\bexecute\b/)
|
|
76
|
+
return false if content.match?(/\benable_extension\b/)
|
|
77
|
+
return false if content.match?(/\bcreate_extension\b/)
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def noop_migration_warning
|
|
83
|
+
Models::Warning.new(
|
|
84
|
+
rule: NOOP_INFO_RULE,
|
|
85
|
+
severity: :info,
|
|
86
|
+
table: "_",
|
|
87
|
+
column: nil,
|
|
88
|
+
message: "No schema operations detected (empty migration or DSL not recognized by Migflow)."
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|