rails_lens 0.2.11 → 0.3.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +88 -72
- data/lib/rails_lens/analyzers/association_analyzer.rb +3 -10
- data/lib/rails_lens/analyzers/best_practices_analyzer.rb +11 -36
- data/lib/rails_lens/analyzers/callbacks.rb +302 -0
- data/lib/rails_lens/analyzers/column_analyzer.rb +6 -6
- data/lib/rails_lens/analyzers/composite_keys.rb +2 -5
- data/lib/rails_lens/analyzers/database_constraints.rb +4 -6
- data/lib/rails_lens/analyzers/delegated_types.rb +4 -7
- data/lib/rails_lens/analyzers/enums.rb +5 -11
- data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +2 -2
- data/lib/rails_lens/analyzers/generated_columns.rb +4 -6
- data/lib/rails_lens/analyzers/index_analyzer.rb +4 -10
- data/lib/rails_lens/analyzers/inheritance.rb +30 -31
- data/lib/rails_lens/analyzers/notes.rb +29 -39
- data/lib/rails_lens/analyzers/performance_analyzer.rb +3 -26
- data/lib/rails_lens/annotation_pipeline.rb +1 -0
- data/lib/rails_lens/cli.rb +1 -0
- data/lib/rails_lens/commands.rb +23 -1
- data/lib/rails_lens/configuration.rb +4 -1
- data/lib/rails_lens/erd/visualizer.rb +0 -1
- data/lib/rails_lens/extensions/closure_tree_ext.rb +11 -11
- data/lib/rails_lens/mailer/annotator.rb +3 -3
- data/lib/rails_lens/model_detector.rb +49 -3
- data/lib/rails_lens/note_codes.rb +59 -0
- data/lib/rails_lens/providers/callbacks_provider.rb +24 -0
- data/lib/rails_lens/providers/extensions_provider.rb +1 -1
- data/lib/rails_lens/providers/view_provider.rb +6 -20
- data/lib/rails_lens/schema/adapters/base.rb +39 -2
- data/lib/rails_lens/schema/adapters/database_info.rb +11 -17
- data/lib/rails_lens/schema/adapters/mysql.rb +75 -0
- data/lib/rails_lens/schema/adapters/postgresql.rb +123 -3
- data/lib/rails_lens/schema/annotation_manager.rb +37 -60
- data/lib/rails_lens/schema/database_annotator.rb +197 -0
- data/lib/rails_lens/version.rb +1 -1
- data/lib/rails_lens.rb +1 -1
- metadata +5 -1
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../errors'
|
|
4
|
-
require_relative 'error_handling'
|
|
5
|
-
|
|
6
3
|
module RailsLens
|
|
7
4
|
module Analyzers
|
|
8
5
|
class Inheritance < Base
|
|
@@ -42,23 +39,23 @@ module RailsLens
|
|
|
42
39
|
|
|
43
40
|
def analyze_sti
|
|
44
41
|
lines = []
|
|
45
|
-
lines << '
|
|
46
|
-
lines << "
|
|
42
|
+
lines << '[sti]'
|
|
43
|
+
lines << "type_column = \"#{model_class.inheritance_column}\""
|
|
47
44
|
|
|
48
45
|
# Check if this is a base class or subclass
|
|
49
46
|
if model_class.base_class == model_class
|
|
50
47
|
# This is the base class
|
|
51
48
|
subclasses = find_sti_subclasses
|
|
52
|
-
lines << "
|
|
53
|
-
lines << '
|
|
49
|
+
lines << "subclasses = [#{subclasses.map { |s| "\"#{s}\"" }.join(', ')}]" if subclasses.any?
|
|
50
|
+
lines << 'base = true'
|
|
54
51
|
else
|
|
55
52
|
# This is a subclass
|
|
56
|
-
lines << "
|
|
57
|
-
lines << "
|
|
53
|
+
lines << "base_class = \"#{model_class.base_class.name}\""
|
|
54
|
+
lines << "type_value = \"#{model_class.sti_name}\""
|
|
58
55
|
|
|
59
56
|
# Find siblings
|
|
60
57
|
siblings = find_sti_siblings
|
|
61
|
-
lines << "
|
|
58
|
+
lines << "siblings = [#{siblings.map { |s| "\"#{s}\"" }.join(', ')}]" if siblings.any?
|
|
62
59
|
end
|
|
63
60
|
|
|
64
61
|
lines.join("\n")
|
|
@@ -69,14 +66,14 @@ module RailsLens
|
|
|
69
66
|
return nil unless reflection
|
|
70
67
|
|
|
71
68
|
lines = []
|
|
72
|
-
lines << '
|
|
73
|
-
lines << "
|
|
74
|
-
lines << "
|
|
75
|
-
lines << "
|
|
69
|
+
lines << '[delegated_type]'
|
|
70
|
+
lines << "delegate = \"#{reflection.name}\""
|
|
71
|
+
lines << "type_column = \"#{reflection.foreign_type}\""
|
|
72
|
+
lines << "id_column = \"#{reflection.foreign_key}\""
|
|
76
73
|
|
|
77
74
|
# Try to find known types
|
|
78
75
|
types = find_delegated_types(reflection)
|
|
79
|
-
lines << "
|
|
76
|
+
lines << "types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}]" if types.any?
|
|
80
77
|
|
|
81
78
|
lines.join("\n")
|
|
82
79
|
end
|
|
@@ -151,38 +148,40 @@ module RailsLens
|
|
|
151
148
|
|
|
152
149
|
def analyze_polymorphic
|
|
153
150
|
lines = []
|
|
154
|
-
lines << '
|
|
151
|
+
lines << '[polymorphic]'
|
|
155
152
|
|
|
156
|
-
# Find polymorphic belongs_to associations
|
|
153
|
+
# Find polymorphic belongs_to associations (references)
|
|
157
154
|
polymorphic_belongs_to = model_class.reflect_on_all_associations(:belongs_to).select do |r|
|
|
158
155
|
r.options[:polymorphic]
|
|
159
156
|
end
|
|
160
157
|
|
|
161
158
|
if polymorphic_belongs_to.any?
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
159
|
+
refs = polymorphic_belongs_to.map do |reflection|
|
|
160
|
+
types = if model_class.table_exists? && model_class.columns_hash[reflection.foreign_type.to_s]
|
|
161
|
+
find_polymorphic_types(reflection)
|
|
162
|
+
else
|
|
163
|
+
[]
|
|
164
|
+
end
|
|
165
|
+
if types.any?
|
|
166
|
+
"{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\", types = [#{types.map { |t| "\"#{t}\"" }.join(', ')}] }"
|
|
167
|
+
else
|
|
168
|
+
"{ name = \"#{reflection.name}\", type_col = \"#{reflection.foreign_type}\", id_col = \"#{reflection.foreign_key}\" }"
|
|
169
|
+
end
|
|
171
170
|
end
|
|
171
|
+
lines << "references = [#{refs.join(', ')}]"
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
# Find associations that reference this model polymorphically
|
|
174
|
+
# Find associations that reference this model polymorphically (targets)
|
|
175
175
|
polymorphic_has_many = model_class.reflect_on_all_associations.select do |r|
|
|
176
176
|
r.options[:as]
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
if polymorphic_has_many.any?
|
|
180
|
-
|
|
181
|
-
lines << 'Polymorphic Targets:'
|
|
182
|
-
polymorphic_has_many.each do |reflection|
|
|
180
|
+
targets = polymorphic_has_many.map do |reflection|
|
|
183
181
|
as_name = reflection.options[:as]
|
|
184
|
-
|
|
182
|
+
"{ name = \"#{reflection.name}\", as = \"#{as_name}\" }"
|
|
185
183
|
end
|
|
184
|
+
lines << "targets = [#{targets.join(', ')}]"
|
|
186
185
|
end
|
|
187
186
|
|
|
188
187
|
return nil if lines.size == 1 # Only header
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../errors'
|
|
4
|
-
require_relative 'error_handling'
|
|
5
|
-
|
|
6
3
|
module RailsLens
|
|
7
4
|
module Analyzers
|
|
8
5
|
class Notes < Base
|
|
@@ -61,13 +58,9 @@ module RailsLens
|
|
|
61
58
|
notes = []
|
|
62
59
|
|
|
63
60
|
# Check if this model is backed by a database view
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Check if model has readonly implementation
|
|
68
|
-
unless has_readonly_implementation?
|
|
69
|
-
notes << 'Add readonly? method'
|
|
70
|
-
end
|
|
61
|
+
# Only note if readonly protection is missing (view status is already obvious)
|
|
62
|
+
if ModelDetector.view_exists?(model_class) && !has_readonly_implementation?
|
|
63
|
+
notes << NoteCodes::ADD_READONLY
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
notes
|
|
@@ -81,22 +74,14 @@ module RailsLens
|
|
|
81
74
|
view_metadata = ViewMetadata.new(model_class)
|
|
82
75
|
|
|
83
76
|
# Check for materialized view specific issues
|
|
84
|
-
if view_metadata.materialized_view?
|
|
85
|
-
notes <<
|
|
86
|
-
unless has_refresh_methods?
|
|
87
|
-
notes << 'Add refresh! method for manual updates'
|
|
88
|
-
end
|
|
77
|
+
if view_metadata.materialized_view? && !has_refresh_methods?
|
|
78
|
+
notes << NoteCodes::ADD_REFRESH
|
|
89
79
|
end
|
|
90
80
|
|
|
91
81
|
# Check for nested views (view depending on other views)
|
|
92
82
|
dependencies = view_metadata.dependencies
|
|
93
83
|
if dependencies.any? { |dep| view_exists_by_name?(dep) }
|
|
94
|
-
notes <<
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Check for readonly implementation
|
|
98
|
-
unless has_readonly_implementation?
|
|
99
|
-
notes << '🔒 Add readonly protection to prevent write operations'
|
|
84
|
+
notes << NoteCodes::NESTED_VIEW
|
|
100
85
|
end
|
|
101
86
|
|
|
102
87
|
notes
|
|
@@ -110,19 +95,19 @@ module RailsLens
|
|
|
110
95
|
|
|
111
96
|
# Check for missing indexes on foreign keys
|
|
112
97
|
foreign_key_columns.each do |column|
|
|
113
|
-
notes <<
|
|
98
|
+
notes << NoteCodes.note(column, NoteCodes::INDEX) unless has_index?(column)
|
|
114
99
|
end
|
|
115
100
|
|
|
116
101
|
# Check for missing composite indexes
|
|
117
102
|
common_query_patterns.each do |columns|
|
|
118
103
|
unless has_composite_index?(columns)
|
|
119
|
-
notes <<
|
|
104
|
+
notes << NoteCodes.note(columns.join('+'), NoteCodes::COMP_INDEX)
|
|
120
105
|
end
|
|
121
106
|
end
|
|
122
107
|
|
|
123
108
|
# Check for redundant indexes
|
|
124
109
|
redundant_indexes.each do |index|
|
|
125
|
-
notes <<
|
|
110
|
+
notes << NoteCodes.note(index.name, NoteCodes::REDUND_IDX)
|
|
126
111
|
end
|
|
127
112
|
|
|
128
113
|
notes
|
|
@@ -137,7 +122,7 @@ module RailsLens
|
|
|
137
122
|
next unless column_exists?(column)
|
|
138
123
|
|
|
139
124
|
unless has_foreign_key_constraint?(column)
|
|
140
|
-
notes <<
|
|
125
|
+
notes << NoteCodes.note(column, NoteCodes::FK_CONSTRAINT)
|
|
141
126
|
end
|
|
142
127
|
end
|
|
143
128
|
|
|
@@ -147,19 +132,19 @@ module RailsLens
|
|
|
147
132
|
def analyze_associations
|
|
148
133
|
# Check for missing inverse_of
|
|
149
134
|
notes = associations_needing_inverse.map do |association|
|
|
150
|
-
|
|
135
|
+
NoteCodes.note(association.name.to_s, NoteCodes::INVERSE_OF)
|
|
151
136
|
end
|
|
152
137
|
|
|
153
138
|
# Check for N+1 query risks
|
|
154
139
|
has_many_associations.each do |association|
|
|
155
140
|
if likely_n_plus_one?(association)
|
|
156
|
-
notes <<
|
|
141
|
+
notes << NoteCodes.note(association.name.to_s, NoteCodes::N_PLUS_ONE)
|
|
157
142
|
end
|
|
158
143
|
end
|
|
159
144
|
|
|
160
145
|
# Check for missing counter caches
|
|
161
146
|
associations_needing_counter_cache.each do |association|
|
|
162
|
-
notes <<
|
|
147
|
+
notes << NoteCodes.note(association.name.to_s, NoteCodes::COUNTER_CACHE)
|
|
163
148
|
end
|
|
164
149
|
|
|
165
150
|
notes
|
|
@@ -168,22 +153,22 @@ module RailsLens
|
|
|
168
153
|
def analyze_columns
|
|
169
154
|
# Check for missing NOT NULL constraints
|
|
170
155
|
notes = columns_needing_not_null.map do |column|
|
|
171
|
-
|
|
156
|
+
NoteCodes.note(column.name, NoteCodes::NOT_NULL)
|
|
172
157
|
end
|
|
173
158
|
|
|
174
159
|
# Check for missing defaults
|
|
175
160
|
columns_needing_defaults.each do |column|
|
|
176
|
-
notes <<
|
|
161
|
+
notes << NoteCodes.note(column.name, NoteCodes::DEFAULT)
|
|
177
162
|
end
|
|
178
163
|
|
|
179
164
|
# Check for inappropriate column types
|
|
180
165
|
columns.each do |column|
|
|
181
166
|
if column.name.end_with?('_count') && column.type != :integer
|
|
182
|
-
notes <<
|
|
167
|
+
notes << NoteCodes.note(column.name, NoteCodes::USE_INTEGER)
|
|
183
168
|
end
|
|
184
169
|
|
|
185
170
|
if column.name.match?(/price|amount|cost/) && column.type == :float
|
|
186
|
-
notes <<
|
|
171
|
+
notes << NoteCodes.note(column.name, NoteCodes::USE_DECIMAL)
|
|
187
172
|
end
|
|
188
173
|
end
|
|
189
174
|
|
|
@@ -193,7 +178,7 @@ module RailsLens
|
|
|
193
178
|
def analyze_performance
|
|
194
179
|
# Large text columns without separate storage
|
|
195
180
|
notes = large_text_columns.map do |column|
|
|
196
|
-
|
|
181
|
+
NoteCodes.note(column.name, NoteCodes::STORAGE)
|
|
197
182
|
end
|
|
198
183
|
|
|
199
184
|
# Polymorphic associations without indexes
|
|
@@ -204,14 +189,14 @@ module RailsLens
|
|
|
204
189
|
foreign_key = association.foreign_key.to_s
|
|
205
190
|
type_column = "#{association.foreign_type || association.name}_type"
|
|
206
191
|
unless has_composite_index?([foreign_key, type_column])
|
|
207
|
-
notes <<
|
|
192
|
+
notes << NoteCodes.note(association.name.to_s, NoteCodes::POLY_INDEX)
|
|
208
193
|
end
|
|
209
194
|
end
|
|
210
195
|
|
|
211
196
|
# UUID columns without proper indexes
|
|
212
197
|
uuid_columns.each do |column|
|
|
213
198
|
if column.name.end_with?('_id') && !has_index?(column.name)
|
|
214
|
-
notes <<
|
|
199
|
+
notes << NoteCodes.note(column.name, NoteCodes::INDEX)
|
|
215
200
|
end
|
|
216
201
|
end
|
|
217
202
|
|
|
@@ -222,18 +207,23 @@ module RailsLens
|
|
|
222
207
|
notes = []
|
|
223
208
|
|
|
224
209
|
# Check for updated_at/created_at
|
|
225
|
-
|
|
210
|
+
has_created = column_exists?('created_at')
|
|
211
|
+
has_updated = column_exists?('updated_at')
|
|
226
212
|
|
|
227
|
-
|
|
213
|
+
if !has_created && !has_updated
|
|
214
|
+
notes << NoteCodes::NO_TIMESTAMPS
|
|
215
|
+
elsif !has_created || !has_updated
|
|
216
|
+
notes << NoteCodes::PARTIAL_TS
|
|
217
|
+
end
|
|
228
218
|
|
|
229
219
|
# Check for soft deletes without index
|
|
230
220
|
if column_exists?('deleted_at') && !has_index?('deleted_at')
|
|
231
|
-
notes <<
|
|
221
|
+
notes << NoteCodes.note('deleted_at', NoteCodes::INDEX)
|
|
232
222
|
end
|
|
233
223
|
|
|
234
224
|
# Check for STI without index
|
|
235
225
|
if model_class.inheritance_column && column_exists?(model_class.inheritance_column) && !has_index?(model_class.inheritance_column)
|
|
236
|
-
notes <<
|
|
226
|
+
notes << NoteCodes.note(model_class.inheritance_column, NoteCodes::STI_INDEX)
|
|
237
227
|
end
|
|
238
228
|
|
|
239
229
|
notes
|
|
@@ -5,7 +5,6 @@ module RailsLens
|
|
|
5
5
|
class PerformanceAnalyzer < Base
|
|
6
6
|
def analyze
|
|
7
7
|
notes = []
|
|
8
|
-
notes.concat(analyze_large_text_columns)
|
|
9
8
|
notes.concat(analyze_uuid_indexes)
|
|
10
9
|
notes.concat(analyze_query_performance)
|
|
11
10
|
notes
|
|
@@ -13,18 +12,6 @@ module RailsLens
|
|
|
13
12
|
|
|
14
13
|
private
|
|
15
14
|
|
|
16
|
-
def analyze_large_text_columns
|
|
17
|
-
notes = []
|
|
18
|
-
|
|
19
|
-
text_columns.each do |column|
|
|
20
|
-
if frequently_queried_column?(column)
|
|
21
|
-
notes << "Large text column '#{column.name}' is frequently queried - consider separate storage"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
notes
|
|
26
|
-
end
|
|
27
|
-
|
|
28
15
|
def analyze_uuid_indexes
|
|
29
16
|
notes = []
|
|
30
17
|
|
|
@@ -32,7 +19,7 @@ module RailsLens
|
|
|
32
19
|
next if column.name == 'id' # Primary keys are already indexed
|
|
33
20
|
|
|
34
21
|
if should_be_indexed?(column) && !indexed?(column)
|
|
35
|
-
notes <<
|
|
22
|
+
notes << NoteCodes.note(column.name, NoteCodes::INDEX)
|
|
36
23
|
end
|
|
37
24
|
end
|
|
38
25
|
|
|
@@ -46,34 +33,24 @@ module RailsLens
|
|
|
46
33
|
commonly_queried_columns.each do |column|
|
|
47
34
|
next if indexed?(column)
|
|
48
35
|
|
|
49
|
-
notes <<
|
|
36
|
+
notes << NoteCodes.note(column.name, NoteCodes::INDEX)
|
|
50
37
|
end
|
|
51
38
|
|
|
52
39
|
# Check for missing indexes on scoped columns
|
|
53
40
|
scoped_columns.each do |column|
|
|
54
41
|
next if indexed?(column)
|
|
55
42
|
|
|
56
|
-
notes <<
|
|
43
|
+
notes << NoteCodes.note(column.name, NoteCodes::INDEX)
|
|
57
44
|
end
|
|
58
45
|
|
|
59
46
|
notes
|
|
60
47
|
end
|
|
61
48
|
|
|
62
|
-
def text_columns
|
|
63
|
-
model_class.columns.select { |c| c.type == :text }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
49
|
def uuid_columns
|
|
67
50
|
model_class.columns.select { |c| c.type == :uuid || (c.type == :string && c.name.match?(/uuid|guid/i)) }
|
|
68
51
|
end
|
|
69
52
|
|
|
70
|
-
def frequently_queried_column?(column)
|
|
71
|
-
# Heuristic: columns with certain names are likely to be queried frequently
|
|
72
|
-
column.name.match?(/title|name|slug|description|summary|content|body/i)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
53
|
def should_be_indexed?(column)
|
|
76
|
-
# UUID columns used as foreign keys or identifiers should be indexed
|
|
77
54
|
column.name.end_with?('_id', '_uuid', '_guid') ||
|
|
78
55
|
column.name.match?(/identifier|reference|token/i)
|
|
79
56
|
end
|
|
@@ -75,6 +75,7 @@ module RailsLens
|
|
|
75
75
|
register(Providers::CompositeKeysProvider.new)
|
|
76
76
|
register(Providers::DatabaseConstraintsProvider.new)
|
|
77
77
|
register(Providers::GeneratedColumnsProvider.new)
|
|
78
|
+
register(Providers::CallbacksProvider.new)
|
|
78
79
|
|
|
79
80
|
# Notes providers (analysis and recommendations)
|
|
80
81
|
return unless RailsLens.config.schema[:include_notes]
|
data/lib/rails_lens/cli.rb
CHANGED
|
@@ -21,6 +21,7 @@ module RailsLens
|
|
|
21
21
|
desc 'annotate', 'Annotate Rails models with schema information'
|
|
22
22
|
option :models, type: :array, desc: 'Specific models to annotate'
|
|
23
23
|
option :include_abstract, type: :boolean, desc: 'Include abstract classes'
|
|
24
|
+
option :include_database_objects, type: :boolean, default: true, desc: 'Include database objects (functions, etc.) in abstract classes'
|
|
24
25
|
option :position, type: :string, enum: %w[before after top bottom], desc: 'Annotation position'
|
|
25
26
|
option :routes, type: :boolean, desc: 'Annotate controller routes'
|
|
26
27
|
option :mailers, type: :boolean, desc: 'Annotate mailer methods'
|
data/lib/rails_lens/commands.rb
CHANGED
|
@@ -24,6 +24,28 @@ module RailsLens
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Also annotate database-level objects (functions, etc.)
|
|
28
|
+
if options[:include_database_objects]
|
|
29
|
+
db_results = annotate_database_objects(options)
|
|
30
|
+
results.merge!(database_objects: db_results)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
results
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def annotate_database_objects(options = {})
|
|
37
|
+
results = Schema::DatabaseAnnotator.annotate_all(options)
|
|
38
|
+
|
|
39
|
+
output.say "Annotated #{results[:annotated].length} abstract base classes with database objects", :green
|
|
40
|
+
output.say "Skipped #{results[:skipped].length} abstract classes", :yellow if results[:skipped].any?
|
|
41
|
+
|
|
42
|
+
if results[:failed].any?
|
|
43
|
+
output.say "Failed to annotate #{results[:failed].length} abstract classes:", :red
|
|
44
|
+
results[:failed].each do |failure|
|
|
45
|
+
output.say " - #{failure[:model]}: #{failure[:error]}", :red
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
27
49
|
results
|
|
28
50
|
end
|
|
29
51
|
|
|
@@ -183,7 +205,7 @@ module RailsLens
|
|
|
183
205
|
end
|
|
184
206
|
|
|
185
207
|
# Create lib/tasks directory if it doesn't exist
|
|
186
|
-
FileUtils.mkdir_p(tasks_dir)
|
|
208
|
+
FileUtils.mkdir_p(tasks_dir)
|
|
187
209
|
|
|
188
210
|
# Write the rake task
|
|
189
211
|
File.write(rake_file, rake_task_template)
|
|
@@ -20,13 +20,13 @@ module RailsLens
|
|
|
20
20
|
return nil unless model_uses_closure_tree?
|
|
21
21
|
|
|
22
22
|
lines = []
|
|
23
|
-
lines << '
|
|
24
|
-
lines << "
|
|
25
|
-
lines << "
|
|
23
|
+
lines << '[closure_tree]'
|
|
24
|
+
lines << "parent_column = \"#{parent_column_name}\""
|
|
25
|
+
lines << "hierarchy_table = \"#{hierarchy_table_name}\""
|
|
26
26
|
|
|
27
|
-
lines << "
|
|
27
|
+
lines << "order_column = \"#{order_column}\"" if order_column
|
|
28
28
|
|
|
29
|
-
lines << "
|
|
29
|
+
lines << "depth_column = \"#{depth_column}\"" if depth_column && has_column?(depth_column)
|
|
30
30
|
|
|
31
31
|
lines.join("\n")
|
|
32
32
|
end
|
|
@@ -37,29 +37,29 @@ module RailsLens
|
|
|
37
37
|
notes = []
|
|
38
38
|
|
|
39
39
|
# Check parent column index
|
|
40
|
-
notes <<
|
|
40
|
+
notes << NoteCodes.note(parent_column_name, NoteCodes::INDEX) unless has_index?(parent_column_name)
|
|
41
41
|
|
|
42
42
|
# Check hierarchy table existence and indexes
|
|
43
43
|
if hierarchy_table_exists?
|
|
44
44
|
unless has_hierarchy_indexes?
|
|
45
|
-
notes <<
|
|
45
|
+
notes << NoteCodes.note(hierarchy_table_name, NoteCodes::COMP_INDEX)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
unless has_hierarchy_depth_index?
|
|
49
|
-
notes << '
|
|
49
|
+
notes << NoteCodes.note('generations', NoteCodes::INDEX)
|
|
50
50
|
end
|
|
51
51
|
else
|
|
52
|
-
notes <<
|
|
52
|
+
notes << NoteCodes.note(hierarchy_table_name, NoteCodes::MISSING)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Check for counter cache
|
|
56
56
|
if should_have_counter_cache? && !has_counter_cache?
|
|
57
|
-
notes <<
|
|
57
|
+
notes << NoteCodes.note('children', NoteCodes::COUNTER_CACHE)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Check depth column
|
|
61
61
|
if model_class.respond_to?(:cache_depth?) && model_class.cache_depth? && !has_column?(depth_column)
|
|
62
|
-
notes <<
|
|
62
|
+
notes << NoteCodes.note(depth_column, NoteCodes::DEPTH_CACHE)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
notes
|
|
@@ -7,14 +7,14 @@ module RailsLens
|
|
|
7
7
|
def initialize(dry_run: false)
|
|
8
8
|
@dry_run = dry_run
|
|
9
9
|
@mailers = RailsLens::Mailer::Extractor.call
|
|
10
|
-
@changed_files =
|
|
10
|
+
@changed_files = Set.new
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# Annotate all mailer files with mailer information
|
|
14
14
|
#
|
|
15
15
|
# @param pattern [String] Glob pattern for mailer files
|
|
16
16
|
# @param exclusion [String] Glob pattern for files to exclude
|
|
17
|
-
# @return [
|
|
17
|
+
# @return [Set<String>] Set of changed files
|
|
18
18
|
def annotate_all(pattern: '**/*_mailer.rb', exclusion: 'vendor/**/*_mailer.rb')
|
|
19
19
|
# Simply annotate all mailer files we found via their source locations
|
|
20
20
|
source_paths_map.each do |source_path, methods|
|
|
@@ -31,7 +31,7 @@ module RailsLens
|
|
|
31
31
|
#
|
|
32
32
|
# @param pattern [String] Glob pattern for mailer files
|
|
33
33
|
# @param exclusion [String] Glob pattern for files to exclude
|
|
34
|
-
# @return [
|
|
34
|
+
# @return [Set<String>] Set of changed files
|
|
35
35
|
def remove_all(pattern: '**/*_mailer.rb', exclusion: 'vendor/**/*_mailer.rb')
|
|
36
36
|
# Remove annotations from all mailer files we found via their source locations
|
|
37
37
|
source_paths_map.each_key do |source_path|
|
|
@@ -415,23 +415,69 @@ module RailsLens
|
|
|
415
415
|
end
|
|
416
416
|
|
|
417
417
|
def table_exists_with_connection?(model, connection)
|
|
418
|
-
|
|
418
|
+
table_name = model.table_name
|
|
419
|
+
|
|
420
|
+
# Handle schema-qualified table names for PostgreSQL (e.g., 'audit.audit_logs')
|
|
421
|
+
if connection.adapter_name == 'PostgreSQL' && table_name.include?('.')
|
|
422
|
+
with_schema_in_search_path(connection, table_name) do |unqualified_name|
|
|
423
|
+
connection.table_exists?(unqualified_name) || connection.views.include?(unqualified_name)
|
|
424
|
+
end
|
|
425
|
+
else
|
|
426
|
+
# Check both tables and views
|
|
427
|
+
return true if connection.table_exists?(table_name)
|
|
428
|
+
return true if connection.views.include?(table_name)
|
|
429
|
+
|
|
430
|
+
# Fallback for SQLite: direct sqlite_master query for views
|
|
431
|
+
if connection.adapter_name.downcase.include?('sqlite')
|
|
432
|
+
check_sqlite_view(connection, table_name)
|
|
433
|
+
else
|
|
434
|
+
false
|
|
435
|
+
end
|
|
436
|
+
end
|
|
419
437
|
rescue StandardError
|
|
420
438
|
false
|
|
421
439
|
end
|
|
422
440
|
|
|
423
441
|
def columns_empty_with_connection?(model, connection)
|
|
424
|
-
|
|
442
|
+
table_name = model.table_name
|
|
443
|
+
|
|
444
|
+
if connection.adapter_name == 'PostgreSQL' && table_name.include?('.')
|
|
445
|
+
with_schema_in_search_path(connection, table_name) do |unqualified_name|
|
|
446
|
+
connection.columns(unqualified_name).empty?
|
|
447
|
+
end
|
|
448
|
+
else
|
|
449
|
+
connection.columns(table_name).empty?
|
|
450
|
+
end
|
|
425
451
|
rescue StandardError
|
|
426
452
|
true
|
|
427
453
|
end
|
|
428
454
|
|
|
429
455
|
def get_column_count_with_connection(model, connection)
|
|
430
|
-
|
|
456
|
+
table_name = model.table_name
|
|
457
|
+
|
|
458
|
+
if connection.adapter_name == 'PostgreSQL' && table_name.include?('.')
|
|
459
|
+
with_schema_in_search_path(connection, table_name) do |unqualified_name|
|
|
460
|
+
connection.columns(unqualified_name).size
|
|
461
|
+
end
|
|
462
|
+
else
|
|
463
|
+
connection.columns(table_name).size
|
|
464
|
+
end
|
|
431
465
|
rescue StandardError
|
|
432
466
|
0
|
|
433
467
|
end
|
|
434
468
|
|
|
469
|
+
# Helper to execute block with schema in PostgreSQL search_path
|
|
470
|
+
def with_schema_in_search_path(connection, qualified_table_name)
|
|
471
|
+
schema_name, unqualified_name = qualified_table_name.split('.', 2)
|
|
472
|
+
original_search_path = connection.schema_search_path
|
|
473
|
+
begin
|
|
474
|
+
connection.schema_search_path = "#{schema_name}, #{original_search_path}"
|
|
475
|
+
yield unqualified_name
|
|
476
|
+
ensure
|
|
477
|
+
connection.schema_search_path = original_search_path
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
435
481
|
def has_sti_column?(model)
|
|
436
482
|
return false unless model.table_exists?
|
|
437
483
|
|