rails_lens 0.0.0 → 0.2.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.txt +2 -2
  4. data/README.md +463 -9
  5. data/exe/rails_lens +25 -0
  6. data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
  7. data/lib/rails_lens/analyzers/base.rb +35 -0
  8. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
  9. data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
  10. data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
  11. data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
  12. data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
  13. data/lib/rails_lens/analyzers/enums.rb +34 -0
  14. data/lib/rails_lens/analyzers/error_handling.rb +66 -0
  15. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
  16. data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
  17. data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
  18. data/lib/rails_lens/analyzers/inheritance.rb +212 -0
  19. data/lib/rails_lens/analyzers/notes.rb +325 -0
  20. data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
  21. data/lib/rails_lens/annotation_pipeline.rb +87 -0
  22. data/lib/rails_lens/cli.rb +176 -0
  23. data/lib/rails_lens/cli_error_handler.rb +86 -0
  24. data/lib/rails_lens/commands.rb +164 -0
  25. data/lib/rails_lens/connection.rb +133 -0
  26. data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
  27. data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
  28. data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
  29. data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
  30. data/lib/rails_lens/erd/visualizer.rb +329 -0
  31. data/lib/rails_lens/errors.rb +78 -0
  32. data/lib/rails_lens/extension_loader.rb +261 -0
  33. data/lib/rails_lens/extensions/base.rb +194 -0
  34. data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
  35. data/lib/rails_lens/file_insertion_helper.rb +168 -0
  36. data/lib/rails_lens/mailer/annotator.rb +226 -0
  37. data/lib/rails_lens/mailer/extractor.rb +201 -0
  38. data/lib/rails_lens/model_detector.rb +252 -0
  39. data/lib/rails_lens/parsers/class_info.rb +46 -0
  40. data/lib/rails_lens/parsers/module_info.rb +33 -0
  41. data/lib/rails_lens/parsers/parser_result.rb +55 -0
  42. data/lib/rails_lens/parsers/prism_parser.rb +90 -0
  43. data/lib/rails_lens/parsers.rb +10 -0
  44. data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
  45. data/lib/rails_lens/providers/base.rb +37 -0
  46. data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
  47. data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
  48. data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
  49. data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
  50. data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
  51. data/lib/rails_lens/providers/enums_provider.rb +11 -0
  52. data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
  53. data/lib/rails_lens/providers/extensions_provider.rb +22 -0
  54. data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
  55. data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
  56. data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
  57. data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
  58. data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
  59. data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
  60. data/lib/rails_lens/providers/schema_provider.rb +61 -0
  61. data/lib/rails_lens/providers/section_provider_base.rb +28 -0
  62. data/lib/rails_lens/railtie.rb +17 -0
  63. data/lib/rails_lens/rake_bootstrapper.rb +18 -0
  64. data/lib/rails_lens/route/annotator.rb +268 -0
  65. data/lib/rails_lens/route/extractor.rb +133 -0
  66. data/lib/rails_lens/route/parser.rb +59 -0
  67. data/lib/rails_lens/schema/adapters/base.rb +345 -0
  68. data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
  69. data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
  70. data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
  71. data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
  72. data/lib/rails_lens/schema/annotation.rb +144 -0
  73. data/lib/rails_lens/schema/annotation_manager.rb +202 -0
  74. data/lib/rails_lens/tasks/annotate.rake +35 -0
  75. data/lib/rails_lens/tasks/erd.rake +24 -0
  76. data/lib/rails_lens/tasks/mailers.rake +27 -0
  77. data/lib/rails_lens/tasks/routes.rake +27 -0
  78. data/lib/rails_lens/tasks/schema.rake +108 -0
  79. data/lib/rails_lens/version.rb +5 -0
  80. data/lib/rails_lens.rb +138 -5
  81. metadata +215 -11
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ class ExtensionLoader
5
+ class << self
6
+ def load_extensions
7
+ return [] unless extensions_enabled?
8
+
9
+ extensions = []
10
+
11
+ # Load built-in extensions
12
+ extensions.concat(load_builtin_extensions)
13
+
14
+ # Load gem-provided extensions
15
+ extensions.concat(load_gem_extensions) if autoload_enabled?
16
+
17
+ # Load custom extensions
18
+ extensions.concat(load_custom_extensions)
19
+
20
+ # Filter out ignored extensions
21
+ extensions.reject { |ext| ignored?(ext) }
22
+ end
23
+
24
+ def find_extension_for(_model_class)
25
+ load_extensions.find do |extension_class|
26
+ extension_class.detect? && extension_class.compatible?
27
+ end
28
+ end
29
+
30
+ def apply_extensions(model_class)
31
+ results = {
32
+ annotations: [],
33
+ notes: [],
34
+ erd_additions: {
35
+ relationships: [],
36
+ badges: [],
37
+ attributes: {}
38
+ }
39
+ }
40
+
41
+ load_extensions.each do |extension_class|
42
+ next unless extension_class.detect?
43
+ next unless extension_class.compatible?
44
+
45
+ extension = extension_class.new(model_class)
46
+
47
+ # Collect annotations with individual error handling
48
+ begin
49
+ if (annotation = extension.annotate)
50
+ results[:annotations] << annotation
51
+ end
52
+ rescue StandardError => e
53
+ log_extension_method_error(extension_class, 'annotate', e, model_class)
54
+ end
55
+
56
+ # Collect notes with individual error handling
57
+ begin
58
+ notes = extension.notes
59
+ results[:notes].concat(notes) if notes.is_a?(Array)
60
+ rescue StandardError => e
61
+ log_extension_method_error(extension_class, 'notes', e, model_class)
62
+ end
63
+
64
+ # Collect ERD additions with individual error handling
65
+ begin
66
+ erd = extension.erd_additions
67
+ if erd.is_a?(Hash)
68
+ results[:erd_additions][:relationships].concat(erd[:relationships] || [])
69
+ results[:erd_additions][:badges].concat(erd[:badges] || [])
70
+ results[:erd_additions][:attributes].merge!(erd[:attributes] || {})
71
+ end
72
+ rescue StandardError => e
73
+ log_extension_method_error(extension_class, 'erd_additions', e, model_class)
74
+ end
75
+ rescue StandardError => e
76
+ log_extension_error("Failed to initialize or detect extension #{extension_class}: #{e.message}",
77
+ extension_class.gem_name)
78
+ end
79
+
80
+ results
81
+ end
82
+
83
+ private
84
+
85
+ def extensions_enabled?
86
+ extensions_config = RailsLens.config.extensions
87
+ extensions_config && extensions_config[:enabled]
88
+ end
89
+
90
+ def autoload_enabled?
91
+ extensions_config = RailsLens.config.extensions
92
+ extensions_config && extensions_config[:autoload]
93
+ end
94
+
95
+ def ignored?(extension_class)
96
+ extensions_config = RailsLens.config.extensions
97
+ ignored_gems = extensions_config ? extensions_config[:ignore] : []
98
+ ignored_gems.include?(extension_class.gem_name)
99
+ end
100
+
101
+ def load_builtin_extensions
102
+ extensions = []
103
+
104
+ # Load all Ruby files in the extensions directory
105
+ Dir[File.join(__dir__, 'extensions', '*.rb')].each do |file|
106
+ next if file.end_with?('base.rb') # Skip the base class
107
+
108
+ begin
109
+ require file
110
+
111
+ # Find the extension class
112
+ basename = File.basename(file, '.rb')
113
+ class_name = basename.split('_').map(&:capitalize).join
114
+
115
+ if RailsLens::Extensions.const_defined?(class_name)
116
+ extension_class = RailsLens::Extensions.const_get(class_name)
117
+ if valid_extension?(extension_class)
118
+ extensions << extension_class
119
+ else
120
+ log_extension_error("Builtin extension #{class_name} failed validation", file)
121
+ end
122
+ else
123
+ log_extension_error("Expected class #{class_name} not found", file)
124
+ end
125
+ rescue LoadError, SyntaxError => e
126
+ log_extension_error("Failed to load extension file: #{e.message}", file)
127
+ rescue StandardError => e
128
+ log_extension_error("Unexpected error loading extension: #{e.message}", file)
129
+ end
130
+ end
131
+
132
+ extensions
133
+ end
134
+
135
+ def load_gem_extensions
136
+ extensions = []
137
+
138
+ # Check each loaded gem for RailsLens extensions
139
+
140
+ Gem.loaded_specs.each_key do |gem_name|
141
+ # Try to find extension in the gem
142
+ gem_constant_name = gem_name.gsub('-', '_').split('_').map(&:capitalize).join
143
+ extension_constant_name = "#{gem_constant_name}::RailsLensExtension"
144
+
145
+ # Skip gems that are likely to cause autoload issues
146
+ next if %w[digest openssl uri net json].include?(gem_name)
147
+
148
+ # First check if the gem constant exists without triggering autoload
149
+ next unless Object.const_defined?(gem_constant_name, false)
150
+
151
+ gem_constant = Object.const_get(gem_constant_name)
152
+ next unless gem_constant.is_a?(Module)
153
+
154
+ # Then check if it has a RailsLensExtension without triggering autoload
155
+ next unless gem_constant.const_defined?('RailsLensExtension', false)
156
+
157
+ extension_class = gem_constant.const_get('RailsLensExtension')
158
+ if valid_extension?(extension_class)
159
+ extensions << extension_class
160
+ else
161
+ log_extension_error("Gem extension #{extension_constant_name} failed validation", gem_name)
162
+ end
163
+ rescue NameError
164
+ # No extension found in this gem - this is normal, not an error
165
+ rescue StandardError => e
166
+ log_extension_error("Error loading gem extension: #{e.message}", gem_name)
167
+ end
168
+
169
+ extensions
170
+ end
171
+
172
+ def load_custom_extensions
173
+ extensions = []
174
+
175
+ custom_paths = RailsLens.config.extensions[:custom_paths]
176
+ custom_paths.each do |path|
177
+ next unless File.directory?(path)
178
+
179
+ Dir[File.join(path, '*.rb')].each do |file|
180
+ require file
181
+
182
+ # Try to determine the class name from the file
183
+ basename = File.basename(file, '.rb')
184
+ class_name = basename.split('_').map(&:capitalize).join
185
+
186
+ # Check in various namespaces
187
+ found = false
188
+ [class_name, "RailsLens::Extensions::#{class_name}"].each do |full_name|
189
+ extension_class = Object.const_get(full_name)
190
+ if valid_extension?(extension_class)
191
+ extensions << extension_class
192
+ found = true
193
+ break
194
+ else
195
+ log_extension_error("Custom extension #{full_name} failed validation", file)
196
+ end
197
+ rescue NameError
198
+ # Try next namespace
199
+ end
200
+
201
+ unless found
202
+ log_extension_error(
203
+ "No valid extension class found (tried: #{class_name}, RailsLens::Extensions::#{class_name})", file
204
+ )
205
+ end
206
+ rescue LoadError, SyntaxError => e
207
+ log_extension_error("Failed to load custom extension file: #{e.message}", file)
208
+ rescue StandardError => e
209
+ log_extension_error("Unexpected error loading custom extension: #{e.message}", file)
210
+ end
211
+ end
212
+
213
+ extensions
214
+ end
215
+
216
+ def valid_extension?(klass)
217
+ klass.is_a?(Class) &&
218
+ klass.respond_to?(:gem_name) &&
219
+ klass.respond_to?(:detect?) &&
220
+ klass.respond_to?(:interface_version) &&
221
+ klass.respond_to?(:compatible?) &&
222
+ klass.compatible?
223
+ end
224
+
225
+ def log_extension_error(message, context = nil)
226
+ error_reporting = RailsLens.config.extensions[:error_reporting] || :warn
227
+
228
+ case error_reporting
229
+ when :silent
230
+ # Do nothing
231
+ when :warn
232
+ if Rails.logger
233
+ Rails.logger.warn "[RailsLens Extensions] #{message}#{" (#{context})" if context}"
234
+ else
235
+ Rails.logger.debug { "Warning: [RailsLens Extensions] #{message}#{" (#{context})" if context}" }
236
+ end
237
+ when :verbose
238
+ if Rails.logger
239
+ Rails.logger.error "[RailsLens Extensions] #{message}#{" (#{context})" if context}"
240
+ else
241
+ Rails.logger.debug { "Error: [RailsLens Extensions] #{message}#{" (#{context})" if context}" }
242
+ end
243
+ end
244
+ end
245
+
246
+ def log_extension_method_error(extension_class, method_name, error, model_class)
247
+ context = "#{extension_class.name}##{method_name} for #{model_class.name}"
248
+ error_reporting = RailsLens.config.extensions[:error_reporting] || :warn
249
+
250
+ message = case error_reporting
251
+ when :verbose
252
+ "#{error.message}\n#{error.backtrace&.first(5)&.join("\n")}"
253
+ else
254
+ error.message
255
+ end
256
+
257
+ log_extension_error("Method failed: #{message}", context)
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Extensions
5
+ class Base
6
+ INTERFACE_VERSION = '1.0'
7
+
8
+ class << self
9
+ def gem_name
10
+ raise NotImplementedError, "#{self.class.name} must implement .gem_name"
11
+ end
12
+
13
+ def detect?
14
+ raise NotImplementedError, "#{self.class.name} must implement .detect?"
15
+ end
16
+
17
+ def interface_version
18
+ self::INTERFACE_VERSION
19
+ rescue NameError
20
+ INTERFACE_VERSION
21
+ end
22
+
23
+ def compatible?
24
+ required_version = RailsLens.config.extensions[:interface_version]
25
+ Gem::Version.new(interface_version) >= Gem::Version.new(required_version)
26
+ end
27
+
28
+ # Helper method for gem-based detection
29
+ def gem_available?(gem_name)
30
+ Gem::Specification.find_by_name(gem_name)
31
+ true
32
+ rescue Gem::LoadError
33
+ false
34
+ end
35
+ end
36
+
37
+ attr_reader :model_class
38
+
39
+ def initialize(model_class)
40
+ @model_class = model_class
41
+ end
42
+
43
+ # Override this method to provide model-specific annotations
44
+ def annotate
45
+ nil
46
+ end
47
+
48
+ # Override this method to provide analysis notes
49
+ def notes
50
+ []
51
+ end
52
+
53
+ # Override this method to provide ERD additions
54
+ def erd_additions
55
+ {
56
+ relationships: [],
57
+ badges: [],
58
+ attributes: {}
59
+ }
60
+ end
61
+
62
+ # Validation helper methods
63
+ def safe_method_call(method_name, default_value = nil)
64
+ return default_value unless respond_to?(method_name)
65
+
66
+ send(method_name)
67
+ rescue StandardError => e
68
+ log_method_error(method_name, e)
69
+ default_value
70
+ end
71
+
72
+ def validate_array_result(result, method_name = 'unknown')
73
+ return [] unless result.is_a?(Array)
74
+
75
+ result.select { |item| item.is_a?(String) }
76
+ rescue StandardError => e
77
+ log_method_error(method_name, e)
78
+ []
79
+ end
80
+
81
+ def validate_hash_result(result, required_keys = [], method_name = 'unknown')
82
+ unless result.is_a?(Hash)
83
+ log_method_error(method_name, "Expected Hash, got #{result.class}")
84
+ return required_keys.index_with { |_key| [] }
85
+ end
86
+
87
+ # Ensure required keys exist with default values
88
+ required_keys.each do |key|
89
+ result[key] ||= key == :attributes ? {} : []
90
+ end
91
+
92
+ result
93
+ rescue StandardError => e
94
+ log_method_error(method_name, e)
95
+ required_keys.index_with { |key| key == :attributes ? {} : [] }
96
+ end
97
+
98
+ protected
99
+
100
+ # Helper methods for extensions
101
+
102
+ def table_name
103
+ model_class.table_name
104
+ end
105
+
106
+ def connection
107
+ model_class.connection
108
+ end
109
+
110
+ def columns
111
+ @columns ||= connection.columns(table_name)
112
+ end
113
+
114
+ def column_names
115
+ @column_names ||= columns.map(&:name)
116
+ end
117
+
118
+ def has_column?(column_name)
119
+ column_names.include?(column_name.to_s)
120
+ end
121
+
122
+ def indexes
123
+ @indexes ||= connection.indexes(table_name)
124
+ end
125
+
126
+ def index_names
127
+ @index_names ||= indexes.map(&:name)
128
+ end
129
+
130
+ def has_index?(column_name)
131
+ indexes.any? { |index| index.columns.include?(column_name.to_s) }
132
+ end
133
+
134
+ def foreign_keys
135
+ @foreign_keys ||= if connection.respond_to?(:foreign_keys)
136
+ connection.foreign_keys(table_name)
137
+ else
138
+ []
139
+ end
140
+ end
141
+
142
+ def associations
143
+ @associations ||= model_class.reflect_on_all_associations
144
+ end
145
+
146
+ def has_many_associations
147
+ associations.select { |a| a.macro == :has_many }
148
+ end
149
+
150
+ def belongs_to_associations
151
+ associations.select { |a| a.macro == :belongs_to }
152
+ end
153
+
154
+ def has_one_associations
155
+ associations.select { |a| a.macro == :has_one }
156
+ end
157
+
158
+ def has_and_belongs_to_many_associations
159
+ associations.select { |a| a.macro == :has_and_belongs_to_many }
160
+ end
161
+
162
+ private
163
+
164
+ def log_method_error(method_name, error)
165
+ error_reporting = RailsLens.config.extensions[:error_reporting] || :warn
166
+ context = "#{self.class.name}##{method_name} for #{model_class.name}"
167
+
168
+ message = case error_reporting
169
+ when :verbose
170
+ error.is_a?(String) ? error : "#{error.message}\n#{error.backtrace&.first(3)&.join("\n")}"
171
+ else
172
+ error.is_a?(String) ? error : error.message
173
+ end
174
+
175
+ case error_reporting
176
+ when :silent
177
+ # Do nothing
178
+ when :warn
179
+ if Rails.logger
180
+ Rails.logger.warn "[RailsLens Extensions] Method failed: #{message} (#{context})"
181
+ else
182
+ Rails.logger.debug { "Warning: [RailsLens Extensions] Method failed: #{message} (#{context})" }
183
+ end
184
+ when :verbose
185
+ if Rails.logger
186
+ Rails.logger.error "[RailsLens Extensions] Method failed: #{message} (#{context})"
187
+ else
188
+ Rails.logger.debug { "Error: [RailsLens Extensions] Method failed: #{message} (#{context})" }
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Extensions
5
+ class ClosureTreeExt < Base
6
+ def self.gem_name
7
+ 'closure_tree'
8
+ end
9
+
10
+ def self.detect?
11
+ if gem_available?(gem_name)
12
+ require gem_name
13
+ true
14
+ else
15
+ false
16
+ end
17
+ end
18
+
19
+ def annotate
20
+ return nil unless model_uses_closure_tree?
21
+
22
+ lines = []
23
+ lines << '== Hierarchy (ClosureTree)'
24
+ lines << "Parent Column: #{parent_column_name}"
25
+ lines << "Hierarchy Table: #{hierarchy_table_name}"
26
+
27
+ lines << "Order Column: #{order_column}" if order_column
28
+
29
+ lines << "Depth Column: #{depth_column}" if depth_column && has_column?(depth_column)
30
+
31
+ lines.join("\n")
32
+ end
33
+
34
+ def notes
35
+ return [] unless model_uses_closure_tree?
36
+
37
+ notes = []
38
+
39
+ # Check parent column index
40
+ notes << "Missing index on parent column '#{parent_column_name}'" unless has_index?(parent_column_name)
41
+
42
+ # Check hierarchy table existence and indexes
43
+ if hierarchy_table_exists?
44
+ unless has_hierarchy_indexes?
45
+ notes << "Hierarchy table '#{hierarchy_table_name}' needs compound index on (ancestor_id, descendant_id)"
46
+ end
47
+
48
+ unless has_hierarchy_depth_index?
49
+ notes << 'Consider adding index on generations column in hierarchy table for depth queries'
50
+ end
51
+ else
52
+ notes << "Hierarchy table '#{hierarchy_table_name}' does not exist - run migrations"
53
+ end
54
+
55
+ # Check for counter cache
56
+ if should_have_counter_cache? && !has_counter_cache?
57
+ notes << "Consider adding counter cache '#{suggested_counter_cache_name}' for children count"
58
+ end
59
+
60
+ # Check depth column
61
+ if model_class.respond_to?(:cache_depth?) && model_class.cache_depth? && !has_column?(depth_column)
62
+ notes << "Depth caching enabled but column '#{depth_column}' is missing"
63
+ end
64
+
65
+ notes
66
+ end
67
+
68
+ def erd_additions
69
+ return default_erd_additions unless model_uses_closure_tree?
70
+
71
+ {
72
+ relationships: [
73
+ {
74
+ type: 'hierarchy',
75
+ from: table_name,
76
+ to: hierarchy_table_name,
77
+ label: 'closure table',
78
+ style: 'dashed'
79
+ }
80
+ ],
81
+ badges: ['tree'],
82
+ attributes: {
83
+ tree_type: 'closure_tree',
84
+ hierarchy_table: hierarchy_table_name
85
+ }
86
+ }
87
+ end
88
+
89
+ private
90
+
91
+ def model_uses_closure_tree?
92
+ # Check for both acts_as_tree and has_closure_tree
93
+ (model_class.respond_to?(:acts_as_tree) || model_class.respond_to?(:has_closure_tree)) &&
94
+ model_class.respond_to?(:_ct)
95
+ end
96
+
97
+ def parent_column_name
98
+ model_class._ct.parent_column_name
99
+ end
100
+
101
+ def hierarchy_table_name
102
+ model_class._ct.hierarchy_table_name
103
+ end
104
+
105
+ def order_column
106
+ model_class._ct.order_column
107
+ end
108
+
109
+ def depth_column
110
+ "#{model_class._ct.name_column}_depth"
111
+ end
112
+
113
+ def hierarchy_table_exists?
114
+ connection.table_exists?(hierarchy_table_name)
115
+ end
116
+
117
+ def has_hierarchy_indexes?
118
+ return false unless hierarchy_table_exists?
119
+
120
+ indexes = connection.indexes(hierarchy_table_name)
121
+ indexes.any? do |index|
122
+ index.columns.sort == %w[ancestor_id descendant_id].sort
123
+ end
124
+ end
125
+
126
+ def has_hierarchy_depth_index?
127
+ return false unless hierarchy_table_exists?
128
+
129
+ indexes = connection.indexes(hierarchy_table_name)
130
+ indexes.any? do |index|
131
+ index.columns.include?('generations')
132
+ end
133
+ end
134
+
135
+ def should_have_counter_cache?
136
+ # Suggest counter cache for models that likely have many children
137
+ has_many_associations.any? { |a| a.name.to_s.match?(/child|children|descendant/) }
138
+ end
139
+
140
+ def has_counter_cache?
141
+ column_names.any? { |col| col.match?(/children_count|descendants_count/) }
142
+ end
143
+
144
+ def suggested_counter_cache_name
145
+ 'children_count'
146
+ end
147
+
148
+ def default_erd_additions
149
+ {
150
+ relationships: [],
151
+ badges: [],
152
+ attributes: {}
153
+ }
154
+ end
155
+ end
156
+ end
157
+ end