query_guard 0.5.0 → 0.5.2
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/lib/query_guard/cli/command.rb +15 -2
- data/lib/query_guard/migrations/migration_risk_detectors.rb +103 -0
- data/lib/query_guard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9039102f1515cf998b7912bc94cb82f002f2f94acfc2158e77321f0ffbb9af1
|
|
4
|
+
data.tar.gz: 7fb92f2bdc470b21c2e31068bcdb6944b19a49073bd07fb76a404c95c0eff452
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 764cd0bc1304751ab303313cd9bd34f9ef9f675551eb32de9c0b30088a65ffc9f3a5403eb9303ff1e8f51d6834521ca31488716c61c155dcc1708b6e1595eea2
|
|
7
|
+
data.tar.gz: da30bd2ff0ad091ca5479f8b38d83db8690180a8166a3b9ab3ff363d2de22ade3d9380297c2dbadb21e7333c2ec367f7f6abff5411d5ac51e02a38a2e9ac3544
|
|
@@ -32,8 +32,21 @@ module QueryGuard
|
|
|
32
32
|
begin
|
|
33
33
|
file_findings = analyzer.analyze_migration(file)
|
|
34
34
|
file_findings.each do |finding|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Normalize Finding objects to Hash for Formatter compatibility
|
|
36
|
+
if finding.respond_to?(:to_json_h)
|
|
37
|
+
h = finding.to_json_h
|
|
38
|
+
elsif finding.respond_to?(:to_h)
|
|
39
|
+
h = finding.to_h
|
|
40
|
+
else
|
|
41
|
+
h = finding
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Ensure file path is present on the hash
|
|
45
|
+
if h.is_a?(Hash)
|
|
46
|
+
h[:file_path] ||= file
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
findings << h
|
|
37
50
|
end
|
|
38
51
|
rescue => e
|
|
39
52
|
puts "Warning: Failed to analyze #{file}: #{e.message}" if @options[:verbose]
|
|
@@ -12,9 +12,11 @@ module QueryGuard
|
|
|
12
12
|
|
|
13
13
|
# Run all detectors
|
|
14
14
|
risks.concat(detect_unsafe_index_additions(migration_content, migration_name))
|
|
15
|
+
risks.concat(detect_concurrent_index_without_disable_ddl(migration_content, migration_name))
|
|
15
16
|
risks.concat(detect_table_locking_operations(migration_content, migration_name))
|
|
16
17
|
risks.concat(detect_non_null_additions(migration_content, migration_name))
|
|
17
18
|
risks.concat(detect_full_table_updates(migration_content, migration_name))
|
|
19
|
+
risks.concat(detect_data_backfill_in_migration(migration_content, migration_name))
|
|
18
20
|
risks.concat(detect_unsafe_raw_sql(migration_content, migration_name))
|
|
19
21
|
|
|
20
22
|
risks
|
|
@@ -29,6 +31,8 @@ module QueryGuard
|
|
|
29
31
|
# Find all add_index occurrences
|
|
30
32
|
lines = content.lines
|
|
31
33
|
lines.each_with_index do |line, index|
|
|
34
|
+
# Skip commented-out lines
|
|
35
|
+
next if line.strip.start_with?("#")
|
|
32
36
|
next unless line.include?("add_index")
|
|
33
37
|
|
|
34
38
|
# Check if line includes algorithm: :concurrently
|
|
@@ -54,6 +58,105 @@ module QueryGuard
|
|
|
54
58
|
risks
|
|
55
59
|
end
|
|
56
60
|
|
|
61
|
+
# Detect algorithm: :concurrently without disable_ddl_transaction!
|
|
62
|
+
# This is necessary for PostgreSQL to allow concurrent index creation
|
|
63
|
+
def self.detect_concurrent_index_without_disable_ddl(content, migration_name)
|
|
64
|
+
risks = []
|
|
65
|
+
|
|
66
|
+
# Check if migration uses algorithm: :concurrently
|
|
67
|
+
has_concurrent_index = content.include?("algorithm: :concurrently")
|
|
68
|
+
return [] unless has_concurrent_index
|
|
69
|
+
|
|
70
|
+
# Check if disable_ddl_transaction! is present (but not in a comment)
|
|
71
|
+
lines = content.lines
|
|
72
|
+
has_disable_ddl = lines.any? do |line|
|
|
73
|
+
# Remove comment part
|
|
74
|
+
code_part = line.split('#').first
|
|
75
|
+
code_part.include?("disable_ddl_transaction!")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless has_disable_ddl
|
|
79
|
+
lines.each_with_index do |line, index|
|
|
80
|
+
# Skip if this line is commented out
|
|
81
|
+
code_part = line.split('#').first
|
|
82
|
+
next unless code_part.include?("algorithm: :concurrently")
|
|
83
|
+
|
|
84
|
+
risks << {
|
|
85
|
+
type: :concurrent_index_no_disable_ddl,
|
|
86
|
+
severity: :error,
|
|
87
|
+
line_number: index + 1,
|
|
88
|
+
migration_name: migration_name,
|
|
89
|
+
title: "algorithm: :concurrently Without disable_ddl_transaction!",
|
|
90
|
+
description: "Using algorithm: :concurrently requires disable_ddl_transaction! in the migration class to avoid transaction errors.",
|
|
91
|
+
message: "algorithm: :concurrently found but disable_ddl_transaction! not set",
|
|
92
|
+
recommendation: "Add `disable_ddl_transaction!` to the migration class definition",
|
|
93
|
+
metadata: {
|
|
94
|
+
operation: "add_index",
|
|
95
|
+
risk_level: :high,
|
|
96
|
+
module: :schema_safety
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
risks
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Detect data backfill / app model usage inside migrations
|
|
106
|
+
# Migrations that use ActiveRecord models are risky because:
|
|
107
|
+
# - Models can change independently of migrations
|
|
108
|
+
# - Queries can fail if code changes
|
|
109
|
+
# - Large updates lock tables
|
|
110
|
+
def self.detect_data_backfill_in_migration(content, migration_name)
|
|
111
|
+
risks = []
|
|
112
|
+
lines = content.lines
|
|
113
|
+
|
|
114
|
+
has_model_usage = false
|
|
115
|
+
model_usage_lines = []
|
|
116
|
+
|
|
117
|
+
lines.each_with_index do |line, index|
|
|
118
|
+
next if line.strip.start_with?("#")
|
|
119
|
+
next if line.strip.empty?
|
|
120
|
+
|
|
121
|
+
# Detect direct model class usage (User.find_each, Comment.update_all, etc.)
|
|
122
|
+
if line.match?(/\b[A-Z]\w*\.(find|find_each|find_in_batches|all|where|update|create|delete|update_all|delete_all|execute)\b/)
|
|
123
|
+
# Skip if it's obviously not a model (like Date, Time, etc.)
|
|
124
|
+
unless line.match?(/\b(Date|Time|DateTime|Hash|Array|String|Integer|Float|Symbol|Regexp)\b/)
|
|
125
|
+
has_model_usage = true
|
|
126
|
+
model_usage_lines << { line_num: index + 1, content: line.strip }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Also detect batched iterations (common pattern in data migrations)
|
|
131
|
+
if line.include?("find_each") || line.include?("find_in_batches")
|
|
132
|
+
has_model_usage = true
|
|
133
|
+
model_usage_lines << { line_num: index + 1, content: line.strip }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if has_model_usage
|
|
138
|
+
model_usage_lines.each do |item|
|
|
139
|
+
risks << {
|
|
140
|
+
type: :data_backfill_in_migration,
|
|
141
|
+
severity: :error,
|
|
142
|
+
line_number: item[:line_num],
|
|
143
|
+
migration_name: migration_name,
|
|
144
|
+
title: "Data Backfill Using App Models in Migration",
|
|
145
|
+
description: "Using ActiveRecord models in migrations is risky because models can change independently. Large data operations should use batching and happen separately from schema changes.",
|
|
146
|
+
message: "ActiveRecord model usage detected (#{item[:content][0..50]}...)",
|
|
147
|
+
recommendation: "Move data backfill to a separate rake task or post-deploy job. Use raw SQL with batching if migration-embedded, or use a data migration gem.",
|
|
148
|
+
metadata: {
|
|
149
|
+
operation: "data_backfill",
|
|
150
|
+
risk_level: :high,
|
|
151
|
+
module: :schema_safety
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
risks
|
|
158
|
+
end
|
|
159
|
+
|
|
57
160
|
# Detect operations that lock the table: remove_column, change_column, rename_column
|
|
58
161
|
def self.detect_table_locking_operations(content, migration_name)
|
|
59
162
|
risks = []
|
data/lib/query_guard/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: query_guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chitradevi36
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|