rails_lens 0.2.3 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eaa2e258c8608dfd893bb5220840fc9febce43478aca9573ae0dbb885b33bf5
4
- data.tar.gz: ed99614fc744e7f32e6c439a8f61e99b5d94787a684c694fbca7ed1eb546e6c6
3
+ metadata.gz: b150328b6771f2de4015820d40e3942c10b41903687f800fd4b068f79605deeb
4
+ data.tar.gz: 2d67fc7fd815b1af80c3cb9e1ad8b94e1166129fc3a1eb24e28958ad3e625b47
5
5
  SHA512:
6
- metadata.gz: 0fc9a2a6d597e0d5a5e9b5e1c5eddf79bdca77448e780686fd28eabc26c5085829fe543595bdde05d31949a0e5ca42475bd379d89b9d552c128c06070d7a555c
7
- data.tar.gz: 3982294a0d8a11b0ba516a7f90dc5595b488a3dcae79ef874c8d7d0cb0369f4a6438304adf1dd476d3aa8542bd445ec44022b549dcdeb48bf63084b5bebdd22d
6
+ metadata.gz: 0b9f944f8e7be0f11789817a49f168f7be310e6742207897029c20540b701c525da861a1b49cdcbeee0690a9bd6424fbace720e6bb42fd5d9d81b46cd244ed88
7
+ data.tar.gz: 22f3bc6e6223ad726c2fc3cf617c37af1e587e466119fd1138965e38ff51cb9128b441b5081214e79f9e2d334b8ec7990e437bdfb20257783a40a194f572ebfc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.4...rails_lens/v0.2.5) (2025-07-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * change strategy to cover database with hundrends of connections pool ([#12](https://github.com/seuros/rails_lens/issues/12)) ([7054d35](https://github.com/seuros/rails_lens/commit/7054d3582bfee41f0050725c2bd23e80c5898486))
9
+
10
+ ## [0.2.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.3...rails_lens/v0.2.4) (2025-07-31)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * centralize connection management to prevent "too many clients" errors ([#10](https://github.com/seuros/rails_lens/issues/10)) ([1f9adf9](https://github.com/seuros/rails_lens/commit/1f9adf9b7dd0648add324492189c1322726da52f))
16
+
3
17
  ## [0.2.3](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.2...rails_lens/v0.2.3) (2025-07-31)
4
18
 
5
19
 
@@ -25,7 +25,7 @@ module RailsLens
25
25
  def detect_generated_columns
26
26
  # PostgreSQL system query to find generated columns
27
27
  sql = <<-SQL.squish
28
- SELECT#{' '}
28
+ SELECT
29
29
  a.attname AS column_name,
30
30
  pg_get_expr(d.adbin, d.adrelid) AS generation_expression
31
31
  FROM pg_attribute a
@@ -399,24 +399,24 @@ module RailsLens
399
399
  case @connection.adapter_name.downcase
400
400
  when 'postgresql'
401
401
  result = @connection.exec_query(<<~SQL.squish, 'Check PostgreSQL View Existence')
402
- SELECT 1 FROM information_schema.views#{' '}
402
+ SELECT 1 FROM information_schema.views
403
403
  WHERE table_name = '#{@connection.quote_string(view_name)}'
404
404
  UNION ALL
405
- SELECT 1 FROM pg_matviews#{' '}
405
+ SELECT 1 FROM pg_matviews
406
406
  WHERE matviewname = '#{@connection.quote_string(view_name)}'
407
407
  LIMIT 1
408
408
  SQL
409
409
  result.rows.any?
410
410
  when 'mysql', 'mysql2'
411
411
  result = @connection.exec_query(<<~SQL.squish, 'Check MySQL View Existence')
412
- SELECT 1 FROM information_schema.views#{' '}
412
+ SELECT 1 FROM information_schema.views
413
413
  WHERE table_name = '#{@connection.quote_string(view_name)}'
414
414
  LIMIT 1
415
415
  SQL
416
416
  result.rows.any?
417
417
  when 'sqlite', 'sqlite3'
418
418
  result = @connection.exec_query(<<~SQL.squish, 'Check SQLite View Existence')
419
- SELECT 1 FROM sqlite_master#{' '}
419
+ SELECT 1 FROM sqlite_master
420
420
  WHERE type = 'view' AND name = '#{@connection.quote_string(view_name)}'
421
421
  LIMIT 1
422
422
  SQL
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
4
-
5
3
  module RailsLens
6
4
  class ModelDetector
7
5
  class << self
@@ -82,10 +80,10 @@ module RailsLens
82
80
  def check_postgresql_view(connection, table_name)
83
81
  # Check both regular views and materialized views
84
82
  result = connection.exec_query(<<~SQL.squish, 'Check PostgreSQL View')
85
- SELECT 1 FROM information_schema.views#{' '}
83
+ SELECT 1 FROM information_schema.views
86
84
  WHERE table_name = '#{connection.quote_string(table_name)}'
87
85
  UNION ALL
88
- SELECT 1 FROM pg_matviews#{' '}
86
+ SELECT 1 FROM pg_matviews
89
87
  WHERE matviewname = '#{connection.quote_string(table_name)}'
90
88
  LIMIT 1
91
89
  SQL
@@ -94,7 +92,7 @@ module RailsLens
94
92
 
95
93
  def check_mysql_view(connection, table_name)
96
94
  result = connection.exec_query(<<~SQL.squish, 'Check MySQL View')
97
- SELECT 1 FROM information_schema.views#{' '}
95
+ SELECT 1 FROM information_schema.views
98
96
  WHERE table_name = '#{connection.quote_string(table_name)}'
99
97
  AND table_schema = DATABASE()
100
98
  LIMIT 1
@@ -104,7 +102,7 @@ module RailsLens
104
102
 
105
103
  def check_sqlite_view(connection, table_name)
106
104
  result = connection.exec_query(<<~SQL.squish, 'Check SQLite View')
107
- SELECT 1 FROM sqlite_master#{' '}
105
+ SELECT 1 FROM sqlite_master
108
106
  WHERE type = 'view' AND name = '#{connection.quote_string(table_name)}'
109
107
  LIMIT 1
110
108
  SQL
@@ -198,7 +196,10 @@ module RailsLens
198
196
 
199
197
  # Exclude abstract models and models without valid tables
200
198
  before_count = models.size
201
- models = filter_models_concurrently(models, trace_filtering, options)
199
+
200
+ # Use connection management during model filtering to prevent connection exhaustion
201
+ models = filter_models_with_connection_management(models, trace_filtering, options)
202
+
202
203
  log_filter_step('Abstract/invalid table removal', before_count, models.size, trace_filtering)
203
204
 
204
205
  # Exclude tables from configuration
@@ -257,59 +258,178 @@ module RailsLens
257
258
  end
258
259
 
259
260
  def filter_models_concurrently(models, trace_filtering, options = {})
260
- # Use concurrent futures to check table existence in parallel
261
- futures = models.map do |model|
262
- Concurrent::Future.execute do
263
- should_exclude = false
264
- reason = nil
265
-
266
- begin
267
- # Skip abstract models unless explicitly included
268
- if model.abstract_class? && !options[:include_abstract]
269
- should_exclude = true
270
- reason = 'abstract class'
271
- # For abstract models that are included, skip table checks
272
- elsif model.abstract_class? && options[:include_abstract]
273
- reason = 'abstract class (included)'
274
- # Skip models without configured tables
275
- elsif !model.table_name
276
- should_exclude = true
277
- reason = 'no table name'
278
- # Skip models whose tables don't exist
279
- elsif !model.table_exists?
280
- should_exclude = true
281
- reason = "table '#{model.table_name}' does not exist"
282
- # Additional check: Skip models that don't have any columns
283
- elsif model.columns.empty?
284
- should_exclude = true
285
- reason = "table '#{model.table_name}' has no columns"
286
- else
287
- reason = "table '#{model.table_name}' exists with #{model.columns.size} columns"
288
- end
289
- rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
261
+ puts "ModelDetector: Sequential filtering #{models.size} models..." if options[:verbose]
262
+ # Process models sequentially to prevent concurrent database connections
263
+ results = models.map do |model|
264
+ should_exclude = false
265
+ reason = nil
266
+
267
+ begin
268
+ # Skip abstract models unless explicitly included
269
+ if model.abstract_class? && !options[:include_abstract]
270
+ should_exclude = true
271
+ reason = 'abstract class'
272
+ # For abstract models that are included, skip table checks
273
+ elsif model.abstract_class? && options[:include_abstract]
274
+ reason = 'abstract class (included)'
275
+ # Skip models without configured tables
276
+ elsif !model.table_name
290
277
  should_exclude = true
291
- reason = "database error checking model - #{e.message}"
292
- rescue NameError, NoMethodError => e
278
+ reason = 'no table name'
279
+ # Skip models whose tables don't exist
280
+ elsif !model.table_exists?
293
281
  should_exclude = true
294
- reason = "method error checking model - #{e.message}"
295
- rescue StandardError => e
296
- # Catch any other errors and exclude the model to prevent ERD corruption
282
+ reason = "table '#{model.table_name}' does not exist"
283
+ # Additional check: Skip models that don't have any columns
284
+ elsif model.columns.empty?
297
285
  should_exclude = true
298
- reason = "unexpected error checking model - #{e.message}"
286
+ reason = "table '#{model.table_name}' has no columns"
287
+ else
288
+ reason = "table '#{model.table_name}' exists with #{model.columns.size} columns"
299
289
  end
290
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
291
+ should_exclude = true
292
+ reason = "database error checking model - #{e.message}"
293
+ rescue NameError, NoMethodError => e
294
+ should_exclude = true
295
+ reason = "method error checking model - #{e.message}"
296
+ rescue StandardError => e
297
+ # Catch any other errors and exclude the model to prevent ERD corruption
298
+ should_exclude = true
299
+ reason = "unexpected error checking model - #{e.message}"
300
+ end
300
301
 
301
- if trace_filtering
302
- action = should_exclude ? 'Excluding' : 'Keeping'
303
- Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
302
+ if trace_filtering
303
+ action = should_exclude ? 'Excluding' : 'Keeping'
304
+ Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
305
+ end
306
+
307
+ { model: model, exclude: should_exclude }
308
+ end
309
+
310
+ # Filter out excluded models
311
+ results.reject { |result| result[:exclude] }.pluck(:model)
312
+ end
313
+
314
+ def filter_models_with_connection_management(models, trace_filtering, options = {})
315
+ # Group models by connection pool first
316
+ models_by_pool = models.group_by do |model|
317
+ model.connection_pool
318
+ rescue StandardError
319
+ nil
320
+ end
321
+
322
+ # Assign orphaned models to primary pool
323
+ if models_by_pool[nil]&.any?
324
+ begin
325
+ primary_pool = ApplicationRecord.connection_pool
326
+ models_by_pool[primary_pool] ||= []
327
+ models_by_pool[primary_pool].concat(models_by_pool[nil])
328
+ models_by_pool.delete(nil)
329
+ rescue StandardError
330
+ # Keep orphaned models for individual processing
331
+ end
332
+ end
333
+
334
+ all_pools = models_by_pool.keys.compact
335
+ valid_models = []
336
+
337
+ models_by_pool.each do |pool, pool_models|
338
+ if pool
339
+ # Disconnect other pools before processing this one
340
+ all_pools.each do |p|
341
+ next if p == pool
342
+
343
+ begin
344
+ p.disconnect! if p.connected?
345
+ rescue StandardError
346
+ # Ignore disconnect errors
347
+ end
304
348
  end
305
349
 
306
- { model: model, exclude: should_exclude }
350
+ # Process models with managed connection
351
+ pool.with_connection do |connection|
352
+ pool_models.each do |model|
353
+ result = filter_single_model(model, connection, trace_filtering, options)
354
+ valid_models << model unless result[:exclude]
355
+ end
356
+ end
357
+ else
358
+ # Fallback for models without pools
359
+ pool_models.each do |model|
360
+ result = filter_single_model(model, nil, trace_filtering, options)
361
+ valid_models << model unless result[:exclude]
362
+ end
307
363
  end
308
364
  end
309
365
 
310
- # Wait for all futures to complete and filter results
311
- results = futures.map(&:value!)
312
- results.reject { |result| result[:exclude] }.pluck(:model)
366
+ valid_models
367
+ end
368
+
369
+ def filter_single_model(model, connection, trace_filtering, options)
370
+ should_exclude = false
371
+ reason = nil
372
+
373
+ begin
374
+ # Skip abstract models unless explicitly included
375
+ if model.abstract_class? && !options[:include_abstract]
376
+ should_exclude = true
377
+ reason = 'abstract class'
378
+ # For abstract models that are included, skip table checks
379
+ elsif model.abstract_class? && options[:include_abstract]
380
+ reason = 'abstract class (included)'
381
+ # Skip models without configured tables
382
+ elsif !model.table_name
383
+ should_exclude = true
384
+ reason = 'no table name'
385
+ # Skip models whose tables don't exist (use connection if available)
386
+ elsif connection ? !table_exists_with_connection?(model, connection) : !model.table_exists?
387
+ should_exclude = true
388
+ reason = "table '#{model.table_name}' does not exist"
389
+ # Additional check: Skip models that don't have any columns
390
+ elsif connection ? columns_empty_with_connection?(model, connection) : model.columns.empty?
391
+ should_exclude = true
392
+ reason = "table '#{model.table_name}' has no columns"
393
+ else
394
+ column_count = connection ? get_column_count_with_connection(model, connection) : model.columns.size
395
+ reason = "table '#{model.table_name}' exists with #{column_count} columns"
396
+ end
397
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
398
+ should_exclude = true
399
+ reason = "database error checking model - #{e.message}"
400
+ rescue NameError, NoMethodError => e
401
+ should_exclude = true
402
+ reason = "method error checking model - #{e.message}"
403
+ rescue StandardError => e
404
+ # Catch any other errors and exclude the model to prevent corruption
405
+ should_exclude = true
406
+ reason = "unexpected error checking model - #{e.message}"
407
+ end
408
+
409
+ if trace_filtering
410
+ action = should_exclude ? 'Excluding' : 'Keeping'
411
+ Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
412
+ end
413
+
414
+ { model: model, exclude: should_exclude }
415
+ end
416
+
417
+ def table_exists_with_connection?(model, connection)
418
+ connection.table_exists?(model.table_name)
419
+ rescue StandardError
420
+ false
421
+ end
422
+
423
+ def columns_empty_with_connection?(model, connection)
424
+ connection.columns(model.table_name).empty?
425
+ rescue StandardError
426
+ true
427
+ end
428
+
429
+ def get_column_count_with_connection(model, connection)
430
+ connection.columns(model.table_name).size
431
+ rescue StandardError
432
+ 0
313
433
  end
314
434
 
315
435
  def has_sti_column?(model)
@@ -12,8 +12,12 @@ module RailsLens
12
12
  end
13
13
 
14
14
  def process(model_class, connection = nil)
15
- # Use passed connection or fall back to model's connection
16
- conn = connection || model_class.connection
15
+ # Always require a connection to be passed to prevent connection pool exhaustion
16
+ if connection.nil?
17
+ raise ArgumentError, 'SchemaProvider requires a connection to be passed to prevent connection pool exhaustion'
18
+ end
19
+
20
+ conn = connection
17
21
 
18
22
  if model_class.abstract_class?
19
23
  # For abstract classes, show database connection information in TOML format
@@ -104,8 +104,8 @@ module RailsLens
104
104
  return [] unless adapter_name == 'PostgreSQL'
105
105
 
106
106
  connection.select_values(<<-SQL.squish)
107
- SELECT schema_name#{' '}
108
- FROM information_schema.schemata#{' '}
107
+ SELECT schema_name
108
+ FROM information_schema.schemata
109
109
  WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
110
110
  ORDER BY schema_name
111
111
  SQL
@@ -319,7 +319,7 @@ module RailsLens
319
319
  # Fetch all view metadata in a single consolidated query
320
320
  def fetch_view_metadata
321
321
  result = connection.exec_query(<<~SQL.squish, 'MySQL View Metadata')
322
- SELECT#{' '}
322
+ SELECT
323
323
  v.is_updatable,
324
324
  COALESCE(
325
325
  (
@@ -247,17 +247,15 @@ module RailsLens
247
247
  result = connection.exec_query(<<~SQL.squish, 'PostgreSQL View Metadata')
248
248
  WITH view_info AS (
249
249
  -- Check for materialized view
250
- SELECT#{' '}
250
+ SELECT
251
251
  'materialized' as view_type,
252
252
  false as is_updatable,
253
253
  mv.matviewname as view_name
254
254
  FROM pg_matviews mv
255
255
  WHERE mv.matviewname = '#{connection.quote_string(table_name)}'
256
- #{' '}
257
256
  UNION ALL
258
- #{' '}
259
257
  -- Check for regular view
260
- SELECT#{' '}
258
+ SELECT
261
259
  'regular' as view_type,
262
260
  CASE WHEN v.is_updatable = 'YES' THEN true ELSE false END as is_updatable,
263
261
  v.table_name as view_name
@@ -274,7 +272,7 @@ module RailsLens
274
272
  AND c2.relkind IN ('r', 'v', 'm')
275
273
  AND d.deptype = 'n'
276
274
  )
277
- SELECT#{' '}
275
+ SELECT
278
276
  vi.view_type,
279
277
  vi.is_updatable,
280
278
  COALESCE(
@@ -136,7 +136,7 @@ module RailsLens
136
136
  # Fetch all view metadata in a single consolidated query
137
137
  def fetch_view_metadata
138
138
  result = connection.exec_query(<<~SQL.squish, 'SQLite View Metadata')
139
- SELECT sql FROM sqlite_master#{' '}
139
+ SELECT sql FROM sqlite_master
140
140
  WHERE type = 'view' AND name = '#{connection.quote_string(table_name)}'
141
141
  LIMIT 1
142
142
  SQL
@@ -186,7 +186,7 @@ module RailsLens
186
186
 
187
187
  def view_definition
188
188
  result = connection.exec_query(<<~SQL.squish, 'SQLite View Definition')
189
- SELECT sql FROM sqlite_master#{' '}
189
+ SELECT sql FROM sqlite_master
190
190
  WHERE type = 'view' AND name = '#{connection.quote_string(table_name)}'
191
191
  LIMIT 1
192
192
  SQL
@@ -61,7 +61,58 @@ module RailsLens
61
61
 
62
62
  def generate_annotation
63
63
  pipeline = AnnotationPipeline.new
64
- results = pipeline.process(model_class)
64
+
65
+ # If we have a connection set by annotate_all, use it to process all providers
66
+ if @connection
67
+ results = { schema: nil, sections: [], notes: [] }
68
+
69
+ pipeline.instance_variable_get(:@providers).each do |provider|
70
+ next unless provider.applicable?(model_class)
71
+
72
+ begin
73
+ result = provider.process(model_class, @connection)
74
+
75
+ case provider.type
76
+ when :schema
77
+ results[:schema] = result
78
+ when :section
79
+ results[:sections] << result if result
80
+ when :notes
81
+ results[:notes].concat(Array(result))
82
+ end
83
+ rescue StandardError => e
84
+ warn "Provider #{provider.class} error for #{model_class}: #{e.message}"
85
+ end
86
+ end
87
+ else
88
+ # Fallback: Use the model's connection pool with proper management
89
+ # This path is used when annotating individual models
90
+ warn "Using fallback connection management for #{model_class.name}" if RailsLens.config.verbose
91
+
92
+ # Force connection management even in fallback mode
93
+ results = { schema: nil, sections: [], notes: [] }
94
+
95
+ model_class.connection_pool.with_connection do |connection|
96
+ pipeline.instance_variable_get(:@providers).each do |provider|
97
+ next unless provider.applicable?(model_class)
98
+
99
+ begin
100
+ result = provider.process(model_class, connection)
101
+
102
+ case provider.type
103
+ when :schema
104
+ results[:schema] = result
105
+ when :section
106
+ results[:sections] << result if result
107
+ when :notes
108
+ results[:notes].concat(Array(result))
109
+ end
110
+ rescue StandardError => e
111
+ warn "Provider #{provider.class} error for #{model_class}: #{e.message}"
112
+ end
113
+ end
114
+ end
115
+ end
65
116
 
66
117
  annotation = Annotation.new
67
118
 
@@ -91,7 +142,13 @@ module RailsLens
91
142
  end
92
143
 
93
144
  def self.annotate_all(options = {})
145
+ # Convert models option to include option for ModelDetector
146
+ if options[:models]
147
+ options[:include] = options[:models]
148
+ end
149
+
94
150
  models = ModelDetector.detect_models(options)
151
+ puts "Detected #{models.size} models for annotation" if options[:verbose]
95
152
 
96
153
  # Filter abstract classes based on options
97
154
  if options[:include_abstract]
@@ -105,53 +162,110 @@ module RailsLens
105
162
 
106
163
  results = { annotated: [], skipped: [], failed: [] }
107
164
 
108
- models.each do |model|
109
- # Ensure model is actually a class, not a hash or other object
110
- unless model.is_a?(Class)
111
- results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" }
112
- next
113
- end
165
+ # Group models by their connection pool to process each database separately
166
+ models_by_connection_pool = models.group_by do |model|
167
+ pool = model.connection_pool
168
+ pool
169
+ rescue StandardError => e
170
+ puts "Model #{model.name} -> NO POOL (#{e.message})" if options[:verbose]
171
+ nil # Models without connection pools will use primary pool
172
+ end
114
173
 
115
- # Skip models without tables or with missing tables (but not abstract classes)
116
- unless model.abstract_class? || model.table_exists?
117
- results[:skipped] << model.name
118
- warn "Skipping #{model.name} - table does not exist" if options[:verbose]
119
- next
174
+ # Force models without connection pools to use the primary connection pool
175
+ if models_by_connection_pool[nil]&.any?
176
+ begin
177
+ primary_pool = ApplicationRecord.connection_pool
178
+ models_by_connection_pool[primary_pool] ||= []
179
+ models_by_connection_pool[primary_pool].concat(models_by_connection_pool[nil])
180
+ models_by_connection_pool.delete(nil)
181
+ rescue StandardError => e
182
+ puts "Failed to assign to primary pool: #{e.message}" if options[:verbose]
120
183
  end
184
+ end
121
185
 
122
- manager = new(model)
186
+ # Get all connection pools first
187
+ all_pools = get_all_connection_pools(models_by_connection_pool)
123
188
 
124
- # Determine file path based on options
125
- file_path = if options[:models_path]
126
- File.join(options[:models_path], "#{model.name.underscore}.rb")
127
- else
128
- nil # Use default model_file_path
129
- end
189
+ # Log initial connection status (removed verbose output)
130
190
 
131
- # Allow external files when models_path is provided (for testing)
132
- allow_external = options[:models_path].present?
191
+ models_by_connection_pool.each do |connection_pool, pool_models|
192
+ if connection_pool
193
+ # Disconnect all OTHER connection pools before processing this one
194
+ disconnect_other_pools(connection_pool, all_pools, options)
133
195
 
134
- if manager.annotate_file(file_path, allow_external_files: allow_external)
135
- results[:annotated] << model.name
196
+ # Process all models for this database using a single connection
197
+ connection_pool.with_connection do |connection|
198
+ pool_models.each do |model|
199
+ process_model_with_connection(model, connection, results, options)
200
+ end
201
+ end
136
202
  else
137
- results[:skipped] << model.name
203
+ # This should not happen anymore since we assign orphaned models to primary pool
204
+ # Use primary connection pool as fallback to avoid creating new connections
205
+ begin
206
+ primary_pool = ApplicationRecord.connection_pool
207
+ primary_pool.with_connection do |connection|
208
+ pool_models.each do |model|
209
+ process_model_with_connection(model, connection, results, options)
210
+ end
211
+ end
212
+ rescue StandardError => e
213
+ # Last resort: process without connection management (will create multiple connections)
214
+ pool_models.each do |model|
215
+ process_model_with_connection(model, nil, results, options)
216
+ end
217
+ end
138
218
  end
139
- rescue ActiveRecord::StatementInvalid => e
140
- # Handle database-related errors (missing tables, schemas, etc.)
141
- results[:skipped] << model.name
142
- warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
143
- rescue StandardError => e
144
- model_name = if model.is_a?(Class) && model.respond_to?(:name)
145
- model.name
146
- else
147
- model.inspect
148
- end
149
- results[:failed] << { model: model_name, error: e.message }
150
219
  end
151
220
 
152
221
  results
153
222
  end
154
223
 
224
+ def self.process_model_with_connection(model, connection, results, options)
225
+ # Ensure model is actually a class, not a hash or other object
226
+ unless model.is_a?(Class)
227
+ results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" }
228
+ return
229
+ end
230
+
231
+ # Skip models without tables or with missing tables (but not abstract classes)
232
+ unless model.abstract_class? || model.table_exists?
233
+ results[:skipped] << model.name
234
+ return
235
+ end
236
+
237
+ manager = new(model)
238
+
239
+ # Set the connection in the manager if provided
240
+ manager.instance_variable_set(:@connection, connection) if connection
241
+
242
+ # Determine file path based on options
243
+ file_path = if options[:models_path]
244
+ File.join(options[:models_path], "#{model.name.underscore}.rb")
245
+ else
246
+ nil # Use default model_file_path
247
+ end
248
+
249
+ # Allow external files when models_path is provided (for testing)
250
+ allow_external = options[:models_path].present?
251
+
252
+ if manager.annotate_file(file_path, allow_external_files: allow_external)
253
+ results[:annotated] << model.name
254
+ else
255
+ results[:skipped] << model.name
256
+ end
257
+ rescue ActiveRecord::StatementInvalid
258
+ # Handle database-related errors (missing tables, schemas, etc.)
259
+ results[:skipped] << model.name
260
+ rescue StandardError => e
261
+ model_name = if model.is_a?(Class) && model.respond_to?(:name)
262
+ model.name
263
+ else
264
+ model.inspect
265
+ end
266
+ results[:failed] << { model: model_name, error: e.message }
267
+ end
268
+
155
269
  def self.remove_all(options = {})
156
270
  models = ModelDetector.detect_models(options)
157
271
  results = { removed: [], skipped: [], failed: [] }
@@ -170,6 +284,45 @@ module RailsLens
170
284
  results
171
285
  end
172
286
 
287
+ def self.disconnect_other_pools(current_pool, all_pools, options = {})
288
+ all_pools.each do |pool|
289
+ next if pool == current_pool || pool.nil?
290
+
291
+ begin
292
+ if pool.connected?
293
+ pool.disconnect!
294
+ end
295
+ rescue StandardError => e
296
+ warn "Failed to disconnect pool: #{e.message}"
297
+ end
298
+ end
299
+ end
300
+
301
+ def self.get_all_connection_pools(models_by_pool)
302
+ models_by_pool.keys.compact
303
+ end
304
+
305
+ def self.log_connection_status(all_pools, options = {})
306
+ return unless options[:verbose]
307
+
308
+ puts "\n=== Connection Pool Status ==="
309
+ all_pools.each do |pool|
310
+ next unless pool
311
+
312
+ begin
313
+ name = pool.db_config&.name || 'unknown'
314
+ connected = pool.connected?
315
+ size = pool.size
316
+ checked_out = pool.stat[:busy]
317
+
318
+ puts "Pool #{name}: connected=#{connected}, size=#{size}, busy=#{checked_out}"
319
+ rescue StandardError => e
320
+ puts "Pool status error: #{e.message}"
321
+ end
322
+ end
323
+ puts "================================\n"
324
+ end
325
+
173
326
  private
174
327
 
175
328
  def add_annotation(content, _file_path = nil)
@@ -9,7 +9,14 @@ namespace :rails_lens do
9
9
  options = {}
10
10
  options[:include_abstract] = true if ENV['INCLUDE_ABSTRACT'] == 'true'
11
11
 
12
- puts 'Annotating models with schema information...'
12
+ # Support model filtering via environment variable
13
+ if ENV['MODELS']
14
+ model_list = ENV['MODELS'].split(',').map(&:strip)
15
+ options[:models] = model_list
16
+ puts "Filtering to specific models: #{model_list.join(', ')}"
17
+ end
18
+
19
+ options[:verbose] = true # Force verbose mode to see connection management
13
20
  results = RailsLens.annotate_models(options)
14
21
 
15
22
  if results[:annotated].any?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih