rails_lens 0.2.12 → 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +88 -72
  4. data/lib/rails_lens/analyzers/association_analyzer.rb +3 -10
  5. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +11 -36
  6. data/lib/rails_lens/analyzers/callbacks.rb +302 -0
  7. data/lib/rails_lens/analyzers/column_analyzer.rb +6 -6
  8. data/lib/rails_lens/analyzers/composite_keys.rb +2 -5
  9. data/lib/rails_lens/analyzers/database_constraints.rb +4 -6
  10. data/lib/rails_lens/analyzers/delegated_types.rb +4 -7
  11. data/lib/rails_lens/analyzers/enums.rb +5 -11
  12. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +2 -2
  13. data/lib/rails_lens/analyzers/generated_columns.rb +4 -6
  14. data/lib/rails_lens/analyzers/index_analyzer.rb +4 -10
  15. data/lib/rails_lens/analyzers/inheritance.rb +30 -31
  16. data/lib/rails_lens/analyzers/notes.rb +29 -39
  17. data/lib/rails_lens/analyzers/performance_analyzer.rb +3 -26
  18. data/lib/rails_lens/annotation_pipeline.rb +1 -0
  19. data/lib/rails_lens/cli.rb +1 -0
  20. data/lib/rails_lens/commands.rb +23 -1
  21. data/lib/rails_lens/configuration.rb +4 -1
  22. data/lib/rails_lens/erd/visualizer.rb +0 -1
  23. data/lib/rails_lens/extensions/closure_tree_ext.rb +11 -11
  24. data/lib/rails_lens/mailer/annotator.rb +3 -3
  25. data/lib/rails_lens/model_detector.rb +49 -3
  26. data/lib/rails_lens/note_codes.rb +59 -0
  27. data/lib/rails_lens/providers/callbacks_provider.rb +24 -0
  28. data/lib/rails_lens/providers/extensions_provider.rb +1 -1
  29. data/lib/rails_lens/providers/view_provider.rb +6 -20
  30. data/lib/rails_lens/schema/adapters/base.rb +39 -2
  31. data/lib/rails_lens/schema/adapters/database_info.rb +11 -17
  32. data/lib/rails_lens/schema/adapters/mysql.rb +75 -0
  33. data/lib/rails_lens/schema/adapters/postgresql.rb +123 -3
  34. data/lib/rails_lens/schema/annotation_manager.rb +24 -58
  35. data/lib/rails_lens/schema/database_annotator.rb +197 -0
  36. data/lib/rails_lens/version.rb +1 -1
  37. data/lib/rails_lens.rb +1 -1
  38. metadata +5 -1
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ # Compact note codes for LLM-readable annotations
5
+ # Format: "column_name:CODE" or "association:CODE"
6
+ module NoteCodes
7
+ # Column constraint codes
8
+ NOT_NULL = 'NOT_NULL'
9
+ DEFAULT = 'DEFAULT'
10
+ LIMIT = 'LIMIT'
11
+
12
+ # Index codes
13
+ INDEX = 'INDEX'
14
+ POLY_INDEX = 'POLY_INDEX'
15
+ COMP_INDEX = 'COMP_INDEX'
16
+ REDUND_IDX = 'REDUND_IDX'
17
+
18
+ # Type codes
19
+ USE_DECIMAL = 'USE_DECIMAL'
20
+ USE_INTEGER = 'USE_INTEGER'
21
+
22
+ # Association codes
23
+ INVERSE_OF = 'INVERSE_OF'
24
+ N_PLUS_ONE = 'N_PLUS_ONE'
25
+ COUNTER_CACHE = 'COUNTER_CACHE'
26
+ FK_CONSTRAINT = 'FK_CONSTRAINT'
27
+
28
+ # Best practices codes
29
+ NO_TIMESTAMPS = 'NO_TIMESTAMPS'
30
+ PARTIAL_TS = 'PARTIAL_TS'
31
+ STORAGE = 'STORAGE'
32
+
33
+ # STI codes
34
+ STI_INDEX = 'STI_INDEX'
35
+ STI_NOT_NULL = 'STI_NOT_NULL'
36
+
37
+ # View codes
38
+ VIEW_READONLY = 'VIEW_READONLY'
39
+ ADD_READONLY = 'ADD_READONLY'
40
+ MATVIEW_STALE = 'MATVIEW_STALE'
41
+ ADD_REFRESH = 'ADD_REFRESH'
42
+ NESTED_VIEW = 'NESTED_VIEW'
43
+ VIEW_PROTECT = 'VIEW_PROTECT'
44
+
45
+ # Structure codes
46
+ MISSING = 'MISSING'
47
+ DEPTH_CACHE = 'DEPTH_CACHE'
48
+
49
+ class << self
50
+ # Build a compact note string
51
+ # @param subject [String, nil] column/association name (nil for model-level)
52
+ # @param code [String] note code constant
53
+ # @return [String] formatted note
54
+ def note(subject, code)
55
+ subject ? "#{subject}:#{code}" : code
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Providers
5
+ class CallbacksProvider < SectionProviderBase
6
+ def analyzer_class
7
+ Analyzers::Callbacks
8
+ end
9
+
10
+ def applicable?(model_class)
11
+ # Only applicable to non-abstract models with callbacks
12
+ return false if model_class.abstract_class?
13
+
14
+ # Check if model has any callbacks defined (Rails 8+ uses unified chains)
15
+ RailsLens::Analyzers::Callbacks::CALLBACK_CHAINS.any? do |chain_name|
16
+ chain_method = "_#{chain_name}_callbacks"
17
+ model_class.respond_to?(chain_method) && model_class.public_send(chain_method).present?
18
+ end
19
+ rescue StandardError
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
@@ -13,7 +13,7 @@ module RailsLens
13
13
  return nil if results[:annotations].empty?
14
14
 
15
15
  {
16
- title: '== Extensions',
16
+ title: '[extensions]',
17
17
  content: results[:annotations].join("\n")
18
18
  }
19
19
  end
@@ -14,7 +14,7 @@ module RailsLens
14
14
  return nil unless view_metadata.view_exists?
15
15
 
16
16
  {
17
- title: '== View Information',
17
+ title: '[view]',
18
18
  content: generate_view_content(view_metadata)
19
19
  }
20
20
  end
@@ -25,39 +25,25 @@ module RailsLens
25
25
  lines = []
26
26
 
27
27
  # View type (regular or materialized)
28
- if view_metadata.view_type
29
- lines << "View Type: #{view_metadata.view_type}"
30
- end
28
+ lines << "type = \"#{view_metadata.view_type}\"" if view_metadata.view_type
31
29
 
32
30
  # Updatable status
33
- lines << "Updatable: #{view_metadata.updatable? ? 'Yes' : 'No'}"
31
+ lines << "updatable = #{view_metadata.updatable?}"
34
32
 
35
33
  # Dependencies
36
34
  dependencies = view_metadata.dependencies
37
35
  if dependencies.any?
38
- lines << "Dependencies: #{dependencies.join(', ')}"
36
+ lines << "dependencies = [#{dependencies.map { |d| "\"#{d}\"" }.join(', ')}]"
39
37
  end
40
38
 
41
39
  # Refresh strategy for materialized views
42
40
  if view_metadata.materialized_view? && view_metadata.refresh_strategy
43
- lines << "Refresh Strategy: #{view_metadata.refresh_strategy}"
41
+ lines << "refresh_strategy = \"#{view_metadata.refresh_strategy}\""
44
42
  end
45
43
 
46
44
  # Last refreshed timestamp for materialized views
47
45
  if view_metadata.materialized_view? && view_metadata.last_refreshed
48
- lines << "Last Refreshed: #{view_metadata.last_refreshed}"
49
- end
50
-
51
- # View definition (truncated for readability)
52
- if view_metadata.view_definition
53
- definition = view_metadata.view_definition
54
- # Truncate long definitions
55
- if definition.length > 200
56
- definition = "#{definition[0..200]}..."
57
- end
58
- # Remove extra whitespace and newlines
59
- definition = definition.gsub(/\s+/, ' ').strip
60
- lines << "Definition: #{definition}"
46
+ lines << "last_refreshed = \"#{view_metadata.last_refreshed}\""
61
47
  end
62
48
 
63
49
  lines.join("\n")
@@ -175,6 +175,42 @@ module RailsLens
175
175
  connection.supports_comments?
176
176
  end
177
177
 
178
+ def show_triggers?
179
+ RailsLens.config.schema[:format_options][:show_triggers]
180
+ end
181
+
182
+ def fetch_triggers
183
+ # Override in database-specific adapters
184
+ []
185
+ end
186
+
187
+ def add_triggers_toml(lines)
188
+ triggers = fetch_triggers
189
+ return if triggers.empty?
190
+
191
+ lines << ''
192
+ lines << 'triggers = ['
193
+ triggers.each_with_index do |trigger, i|
194
+ line = ' { '
195
+ attrs = []
196
+ attrs << "name = \"#{trigger[:name]}\""
197
+ attrs << "event = \"#{trigger[:event]}\""
198
+ attrs << "timing = \"#{trigger[:timing]}\""
199
+ attrs << "function = \"#{trigger[:function]}\""
200
+ attrs << "for_each = \"#{trigger[:for_each]}\"" if trigger[:for_each]
201
+ attrs << "condition = \"#{escape_toml_string(trigger[:condition])}\"" if trigger[:condition]
202
+ line += attrs.join(', ')
203
+ line += ' }'
204
+ line += ',' if i < triggers.length - 1
205
+ lines << line
206
+ end
207
+ lines << ']'
208
+ end
209
+
210
+ def escape_toml_string(str)
211
+ str.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
212
+ end
213
+
178
214
  # Structured formatting methods
179
215
  def add_columns_structured(lines)
180
216
  lines << 'COLUMNS:'
@@ -252,8 +288,9 @@ module RailsLens
252
288
  attrs = []
253
289
  attrs << "name = \"#{column.name}\""
254
290
  attrs << "type = \"#{column.type}\""
255
- attrs << 'primary_key = true' if primary_key?(column)
256
- attrs << "nullable = #{column.null}"
291
+ attrs << 'pk = true' if primary_key?(column)
292
+ # Only add null = false when NOT nullable (omit when nullable for brevity)
293
+ attrs << 'null = false' unless column.null
257
294
  attrs << "default = #{format_toml_value(column.default)}" if column.default && show_defaults?
258
295
  line += attrs.join(', ')
259
296
  line += ' }'
@@ -13,30 +13,24 @@ module RailsLens
13
13
 
14
14
  def generate_annotation
15
15
  lines = []
16
- lines << '== Database Information'
17
- lines << "Adapter: #{adapter_name}"
18
- lines << "Database: #{database_name}"
19
- lines << "Version: #{database_version}"
20
- lines << "Encoding: #{database_encoding}" if respond_to?(:database_encoding)
21
- lines << "Collation: #{database_collation}" if respond_to?(:database_collation)
22
- lines << ''
16
+ lines << '[database_info]'
17
+ lines << "adapter = \"#{adapter_name}\""
18
+ lines << "database = \"#{database_name}\""
19
+ lines << "version = \"#{database_version}\""
20
+ enc = database_encoding
21
+ lines << "encoding = \"#{enc}\"" if enc
22
+ coll = database_collation
23
+ lines << "collation = \"#{coll}\"" if coll
23
24
 
24
25
  # Add extensions for PostgreSQL
25
26
  if adapter_name == 'PostgreSQL' && extensions.any?
26
- lines << 'Enabled Extensions:'
27
- extensions.each do |ext|
28
- lines << " - #{ext['name']} (#{ext['version']})"
29
- end
30
- lines << ''
27
+ ext_list = extensions.map { |e| "{ name = \"#{e['name']}\", version = \"#{e['version']}\" }" }
28
+ lines << "extensions = [#{ext_list.join(', ')}]"
31
29
  end
32
30
 
33
31
  # Add schemas for PostgreSQL
34
32
  if adapter_name == 'PostgreSQL' && schemas.any?
35
- lines << 'Database Schemas:'
36
- schemas.each do |schema|
37
- lines << " - #{schema}"
38
- end
39
- lines << ''
33
+ lines << "schemas = [#{schemas.map { |s| "\"#{s}\"" }.join(', ')}]"
40
34
  end
41
35
 
42
36
  lines.join("\n")
@@ -40,6 +40,7 @@ module RailsLens
40
40
  add_columns_toml(lines)
41
41
  add_indexes_toml(lines) if show_indexes?
42
42
  add_foreign_keys_toml(lines) if show_foreign_keys?
43
+ add_triggers_toml(lines) if show_triggers?
43
44
  add_partitions_toml(lines) if has_partitions?
44
45
 
45
46
  lines.join("\n")
@@ -385,6 +386,80 @@ module RailsLens
385
386
  def view_last_refreshed
386
387
  nil # MySQL doesn't have materialized views
387
388
  end
389
+
390
+ # Fetch triggers for the table
391
+ def fetch_triggers
392
+ quoted_table = connection.quote(unqualified_table_name)
393
+ result = connection.exec_query(<<~SQL.squish, 'MySQL Table Triggers')
394
+ SELECT
395
+ TRIGGER_NAME AS name,
396
+ ACTION_TIMING AS timing,
397
+ EVENT_MANIPULATION AS event,
398
+ 'ROW' AS for_each,
399
+ ACTION_STATEMENT AS action_statement
400
+ FROM information_schema.TRIGGERS
401
+ WHERE TRIGGER_SCHEMA = DATABASE()
402
+ AND EVENT_OBJECT_TABLE = #{quoted_table}
403
+ ORDER BY TRIGGER_NAME
404
+ SQL
405
+
406
+ result.rows.map do |row|
407
+ {
408
+ name: row[0],
409
+ timing: row[1],
410
+ event: row[2],
411
+ for_each: row[3],
412
+ function: extract_function_call(row[4]),
413
+ definition: row[4]
414
+ }
415
+ end
416
+ rescue ActiveRecord::StatementInvalid, Mysql2::Error => e
417
+ RailsLens.logger.debug { "Failed to fetch triggers for #{table_name}: #{e.message}" }
418
+ []
419
+ end
420
+
421
+ # Extract function/procedure call from trigger action statement
422
+ def extract_function_call(action_statement)
423
+ return nil unless action_statement
424
+
425
+ # Match CALL with optional schema prefix and backticks: CALL `schema`.`proc` or CALL schema.proc or CALL proc
426
+ match = action_statement.match(/CALL\s+(?:`?(\w+)`?\.)?`?(\w+)`?/i)
427
+ if match
428
+ schema_part = match[1]
429
+ proc_name = match[2]
430
+ schema_part ? "#{schema_part}.#{proc_name}" : proc_name
431
+ else
432
+ 'inline'
433
+ end
434
+ end
435
+
436
+ # Fetch all user-defined functions
437
+ def self.fetch_functions(connection)
438
+ result = connection.exec_query(<<~SQL.squish, 'MySQL Functions')
439
+ SELECT
440
+ ROUTINE_NAME AS name,
441
+ ROUTINE_SCHEMA AS `schema`,
442
+ DATA_TYPE AS return_type,
443
+ ROUTINE_COMMENT AS description
444
+ FROM information_schema.ROUTINES
445
+ WHERE ROUTINE_SCHEMA = DATABASE()
446
+ AND ROUTINE_TYPE = 'FUNCTION'
447
+ ORDER BY ROUTINE_NAME
448
+ SQL
449
+
450
+ result.rows.map do |row|
451
+ {
452
+ name: row[0],
453
+ schema: row[1],
454
+ language: 'SQL',
455
+ return_type: row[2],
456
+ description: row[3]
457
+ }
458
+ end
459
+ rescue ActiveRecord::StatementInvalid, Mysql2::Error => e
460
+ RailsLens.logger.debug { "Failed to fetch functions: #{e.message}" }
461
+ []
462
+ end
388
463
  end
389
464
  end
390
465
  end
@@ -29,6 +29,7 @@ module RailsLens
29
29
  add_indexes_toml(lines) if show_indexes?
30
30
  add_foreign_keys_toml(lines) if show_foreign_keys?
31
31
  add_check_constraints_toml(lines) if show_check_constraints?
32
+ add_triggers_toml(lines) if show_triggers?
32
33
  add_table_comment_toml(lines) if show_comments?
33
34
 
34
35
  lines.join("\n")
@@ -294,23 +295,24 @@ module RailsLens
294
295
 
295
296
  # Fetch all view metadata in a single consolidated query
296
297
  def fetch_view_metadata
298
+ target_schema = schema_name || 'public'
297
299
  result = connection.exec_query(<<~SQL.squish, 'PostgreSQL View Metadata')
298
300
  WITH view_info AS (
299
- -- Check for materialized view
300
301
  SELECT
301
302
  'materialized' as view_type,
302
303
  false as is_updatable,
303
304
  mv.matviewname as view_name
304
305
  FROM pg_matviews mv
305
306
  WHERE mv.matviewname = '#{connection.quote_string(unqualified_table_name)}'
307
+ AND mv.schemaname = '#{connection.quote_string(target_schema)}'
306
308
  UNION ALL
307
- -- Check for regular view
308
309
  SELECT
309
310
  'regular' as view_type,
310
311
  CASE WHEN v.is_updatable = 'YES' THEN true ELSE false END as is_updatable,
311
312
  v.table_name as view_name
312
313
  FROM information_schema.views v
313
314
  WHERE v.table_name = '#{connection.quote_string(unqualified_table_name)}'
315
+ AND v.table_schema = '#{connection.quote_string(target_schema)}'
314
316
  ),
315
317
  dependencies AS (
316
318
  SELECT DISTINCT c2.relname as dependency_name
@@ -336,10 +338,19 @@ module RailsLens
336
338
  return nil if result.rows.empty?
337
339
 
338
340
  row = result.rows.first
341
+ # Parse PostgreSQL array string (e.g., "{table1,table2}" or "{}")
342
+ deps_raw = row[2]
343
+ deps = if deps_raw.is_a?(Array)
344
+ deps_raw
345
+ elsif deps_raw.is_a?(String) && deps_raw.start_with?('{')
346
+ deps_raw.gsub(/[{}]/, '').split(',').map(&:strip).reject(&:empty?)
347
+ else
348
+ []
349
+ end
339
350
  {
340
351
  type: row[0],
341
352
  updatable: ['t', true].include?(row[1]),
342
- dependencies: row[2] || []
353
+ dependencies: deps
343
354
  }
344
355
  rescue ActiveRecord::StatementInvalid, PG::Error => e
345
356
  RailsLens.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
@@ -401,6 +412,115 @@ module RailsLens
401
412
  rescue ActiveRecord::StatementInvalid, PG::Error
402
413
  nil
403
414
  end
415
+
416
+ # Fetch triggers for the table, excluding extension-owned triggers
417
+ def fetch_triggers
418
+ result = connection.exec_query(<<~SQL.squish, 'PostgreSQL Table Triggers')
419
+ SELECT
420
+ t.tgname AS name,
421
+ CASE t.tgtype::integer & 66
422
+ WHEN 2 THEN 'BEFORE'
423
+ WHEN 64 THEN 'INSTEAD OF'
424
+ ELSE 'AFTER'
425
+ END AS timing,
426
+ CASE t.tgtype::integer & 60
427
+ WHEN 4 THEN 'INSERT'
428
+ WHEN 8 THEN 'DELETE'
429
+ WHEN 16 THEN 'UPDATE'
430
+ WHEN 20 THEN 'INSERT OR UPDATE'
431
+ WHEN 24 THEN 'UPDATE OR DELETE'
432
+ WHEN 12 THEN 'INSERT OR DELETE'
433
+ WHEN 28 THEN 'INSERT OR UPDATE OR DELETE'
434
+ WHEN 32 THEN 'TRUNCATE'
435
+ ELSE 'UNKNOWN'
436
+ END AS event,
437
+ CASE WHEN t.tgtype::integer & 1 = 1 THEN 'ROW' ELSE 'STATEMENT' END AS for_each,
438
+ p.proname AS function_name,
439
+ n.nspname AS function_schema,
440
+ pg_get_triggerdef(t.oid) AS definition
441
+ FROM pg_trigger t
442
+ JOIN pg_class c ON t.tgrelid = c.oid
443
+ JOIN pg_namespace cn ON c.relnamespace = cn.oid
444
+ JOIN pg_proc p ON t.tgfoid = p.oid
445
+ JOIN pg_namespace n ON p.pronamespace = n.oid
446
+ WHERE c.relname = '#{connection.quote_string(unqualified_table_name)}'
447
+ AND cn.nspname = '#{connection.quote_string(schema_name || 'public')}'
448
+ AND NOT t.tgisinternal
449
+ -- Exclude triggers owned by extensions
450
+ AND NOT EXISTS (
451
+ SELECT 1 FROM pg_depend d
452
+ JOIN pg_extension e ON d.refobjid = e.oid
453
+ WHERE d.objid = t.oid
454
+ AND d.deptype = 'e'
455
+ )
456
+ ORDER BY t.tgname
457
+ SQL
458
+
459
+ result.rows.map do |row|
460
+ {
461
+ name: row[0],
462
+ timing: row[1],
463
+ event: row[2],
464
+ for_each: row[3],
465
+ function: row[5] == 'public' ? row[4] : "#{row[5]}.#{row[4]}",
466
+ definition: row[6],
467
+ condition: extract_trigger_condition(row[6])
468
+ }
469
+ end
470
+ rescue ActiveRecord::StatementInvalid, PG::Error => e
471
+ RailsLens.logger.debug { "Failed to fetch triggers for #{table_name}: #{e.message}" }
472
+ []
473
+ end
474
+
475
+ # Extract WHEN condition from trigger definition if present
476
+ def extract_trigger_condition(definition)
477
+ return nil unless definition
478
+
479
+ match = definition.match(/WHEN\s*\((.+?)\)\s*EXECUTE/i)
480
+ match ? match[1] : nil
481
+ end
482
+
483
+ # Fetch all user-defined functions, excluding extension-owned functions
484
+ # This is a database-level operation, returns all functions in user schemas
485
+ def self.fetch_functions(connection)
486
+ result = connection.exec_query(<<~SQL.squish, 'PostgreSQL Functions')
487
+ SELECT
488
+ p.proname AS name,
489
+ n.nspname AS schema,
490
+ l.lanname AS language,
491
+ CASE
492
+ WHEN p.prorettype = 'trigger'::regtype THEN 'trigger'
493
+ ELSE pg_catalog.format_type(p.prorettype, NULL)
494
+ END AS return_type,
495
+ pg_get_functiondef(p.oid) AS definition,
496
+ obj_description(p.oid, 'pg_proc') AS description
497
+ FROM pg_proc p
498
+ JOIN pg_namespace n ON p.pronamespace = n.oid
499
+ JOIN pg_language l ON p.prolang = l.oid
500
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
501
+ AND NOT EXISTS (
502
+ SELECT 1 FROM pg_depend d
503
+ JOIN pg_extension e ON d.refobjid = e.oid
504
+ WHERE d.objid = p.oid
505
+ AND d.deptype = 'e'
506
+ )
507
+ ORDER BY n.nspname, p.proname
508
+ SQL
509
+
510
+ result.rows.map do |row|
511
+ {
512
+ name: row[0],
513
+ schema: row[1],
514
+ language: row[2],
515
+ return_type: row[3],
516
+ definition: row[4],
517
+ description: row[5]
518
+ }
519
+ end
520
+ rescue ActiveRecord::StatementInvalid, PG::Error => e
521
+ RailsLens.logger.debug { "Failed to fetch functions: #{e.message}" }
522
+ []
523
+ end
404
524
  end
405
525
  end
406
526
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../file_insertion_helper'
4
-
5
3
  module RailsLens
6
4
  module Schema
7
5
  class AnnotationManager
@@ -140,13 +138,10 @@ module RailsLens
140
138
  annotation.add_lines(section[:content].split("\n"))
141
139
  end
142
140
 
143
- # Add notes
141
+ # Add notes as TOML array (already in compact format from analyzers)
144
142
  if results[:notes].any?
145
143
  annotation.add_line('')
146
- annotation.add_line('== Notes')
147
- results[:notes].uniq.each do |note|
148
- annotation.add_line("- #{note}")
149
- end
144
+ annotation.add_line("notes = [#{results[:notes].uniq.map { |n| "\"#{n}\"" }.join(', ')}]")
150
145
  end
151
146
 
152
147
  annotation.to_s
@@ -194,16 +189,8 @@ module RailsLens
194
189
  end
195
190
  end
196
191
 
197
- # Get all connection pools first
198
- all_pools = get_all_connection_pools(models_by_connection_pool)
199
-
200
- # Log initial connection status (removed verbose output)
201
-
202
192
  models_by_connection_pool.each do |connection_pool, pool_models|
203
193
  if connection_pool
204
- # Disconnect all OTHER connection pools before processing this one
205
- disconnect_other_pools(connection_pool, all_pools, options)
206
-
207
194
  # Process all models for this database using a single connection
208
195
  connection_pool.with_connection do |connection|
209
196
  pool_models.each do |model|
@@ -278,60 +265,39 @@ module RailsLens
278
265
  end
279
266
 
280
267
  def self.remove_all(options = {})
281
- models = ModelDetector.detect_models(options)
268
+ # Use filesystem-based removal (doesn't require database)
269
+ remove_all_by_filesystem(options)
270
+ end
271
+
272
+ def self.remove_all_by_filesystem(options = {})
273
+ base_path = options[:models_path] || default_models_path
282
274
  results = { removed: [], skipped: [], failed: [] }
275
+ pattern = File.join(base_path, '**', '*.rb')
276
+ files = Dir.glob(pattern)
277
+
278
+ files.each do |file_path|
279
+ content = File.read(file_path)
280
+ next unless Annotation.extract(content)
283
281
 
284
- models.each do |model|
285
- manager = new(model)
286
- if manager.remove_annotations
287
- results[:removed] << model.name
282
+ cleaned = Annotation.remove(content)
283
+ if cleaned == content
284
+ results[:skipped] << File.basename(file_path, '.rb').camelize
288
285
  else
289
- results[:skipped] << model.name
286
+ File.write(file_path, cleaned)
287
+ model_name = File.basename(file_path, '.rb').camelize
288
+ results[:removed] << model_name
290
289
  end
291
290
  rescue StandardError => e
292
- results[:failed] << { model: model.name, error: e.message }
291
+ results[:failed] << { model: File.basename(file_path, '.rb').camelize, error: e.message }
293
292
  end
294
293
 
295
294
  results
296
295
  end
297
296
 
298
- def self.disconnect_other_pools(current_pool, all_pools, options = {})
299
- all_pools.each do |pool|
300
- next if pool == current_pool || pool.nil?
297
+ def self.default_models_path
298
+ return Rails.root.join('app/models') if defined?(Rails.root)
301
299
 
302
- begin
303
- if pool.connected?
304
- pool.disconnect!
305
- end
306
- rescue StandardError => e
307
- warn "Failed to disconnect pool: #{e.message}"
308
- end
309
- end
310
- end
311
-
312
- def self.get_all_connection_pools(models_by_pool)
313
- models_by_pool.keys.compact
314
- end
315
-
316
- def self.log_connection_status(all_pools, options = {})
317
- return unless options[:verbose]
318
-
319
- puts "\n=== Connection Pool Status ==="
320
- all_pools.each do |pool|
321
- next unless pool
322
-
323
- begin
324
- name = pool.db_config&.name || 'unknown'
325
- connected = pool.connected?
326
- size = pool.size
327
- checked_out = pool.stat[:busy]
328
-
329
- puts "Pool #{name}: connected=#{connected}, size=#{size}, busy=#{checked_out}"
330
- rescue StandardError => e
331
- puts "Pool status error: #{e.message}"
332
- end
333
- end
334
- puts "================================\n"
300
+ File.join(Dir.pwd, 'app', 'models')
335
301
  end
336
302
 
337
303
  private