rails_lens 0.2.4 → 0.2.6

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.
@@ -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
@@ -128,7 +126,7 @@ module RailsLens
128
126
  trace_filtering = options[:trace_filtering] || ENV.fetch('RAILS_LENS_TRACE_FILTERING', nil)
129
127
 
130
128
  original_count = models.size
131
- Rails.logger.debug { "[ModelDetector] Starting with #{original_count} models" } if trace_filtering
129
+ RailsLens.logger.debug { "[ModelDetector] Starting with #{original_count} models" } if trace_filtering
132
130
 
133
131
  # Remove anonymous classes and non-class objects
134
132
  before_count = models.size
@@ -159,7 +157,7 @@ module RailsLens
159
157
  end
160
158
  end
161
159
  if excluded && trace_filtering
162
- Rails.logger.debug do
160
+ RailsLens.logger.debug do
163
161
  "[ModelDetector] Excluding #{model.name}: matched exclude pattern"
164
162
  end
165
163
  end
@@ -184,12 +182,12 @@ module RailsLens
184
182
  end
185
183
  end
186
184
  if included && trace_filtering
187
- Rails.logger.debug do
185
+ RailsLens.logger.debug do
188
186
  "[ModelDetector] Including #{model.name}: matched include pattern"
189
187
  end
190
188
  end
191
189
  if !included && trace_filtering
192
- Rails.logger.debug { "[ModelDetector] Excluding #{model.name}: did not match include patterns" }
190
+ RailsLens.logger.debug { "[ModelDetector] Excluding #{model.name}: did not match include patterns" }
193
191
  end
194
192
  included
195
193
  end
@@ -198,17 +196,20 @@ 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
205
- excluded_tables = RailsLens.config.schema[:exclude_tables]
206
+ excluded_tables = RailsLens.excluded_tables
206
207
  before_count = models.size
207
208
  models = models.reject do |model|
208
209
  begin
209
210
  excluded = excluded_tables.include?(model.table_name)
210
211
  if excluded && trace_filtering
211
- Rails.logger.debug do
212
+ RailsLens.logger.debug do
212
213
  "[ModelDetector] Excluding #{model.name}: table '#{model.table_name}' in exclude_tables config"
213
214
  end
214
215
  end
@@ -217,7 +218,7 @@ module RailsLens
217
218
  # This can happen in multi-db setups if the connection is not yet established
218
219
  # We will assume the model should be kept in this case
219
220
  if trace_filtering
220
- Rails.logger.debug do
221
+ RailsLens.logger.debug do
221
222
  "[ModelDetector] Keeping #{model.name}: connection not defined, assuming keep"
222
223
  end
223
224
  end
@@ -225,7 +226,7 @@ module RailsLens
225
226
  end
226
227
  rescue ActiveRecord::StatementInvalid => e
227
228
  if trace_filtering
228
- Rails.logger.debug do
229
+ RailsLens.logger.debug do
229
230
  "[ModelDetector] Keeping #{model.name}: database error checking exclude_tables - #{e.message}"
230
231
  end
231
232
  end
@@ -234,11 +235,11 @@ module RailsLens
234
235
  log_filter_step('Configuration exclude_tables', before_count, models.size, trace_filtering)
235
236
 
236
237
  if trace_filtering
237
- Rails.logger.debug do
238
+ RailsLens.logger.debug do
238
239
  "[ModelDetector] Final result: #{models.size} models after all filtering"
239
240
  end
240
241
  end
241
- Rails.logger.debug { "[ModelDetector] Final models: #{models.map(&:name).join(', ')}" } if trace_filtering
242
+ RailsLens.logger.debug { "[ModelDetector] Final models: #{models.map(&:name).join(', ')}" } if trace_filtering
242
243
 
243
244
  models
244
245
  end
@@ -248,68 +249,187 @@ module RailsLens
248
249
 
249
250
  filtered_count = before_count - after_count
250
251
  if filtered_count.positive?
251
- Rails.logger.debug do
252
+ RailsLens.logger.debug do
252
253
  "[ModelDetector] #{step_name}: filtered out #{filtered_count} models (#{before_count} -> #{after_count})"
253
254
  end
254
255
  else
255
- Rails.logger.debug { "[ModelDetector] #{step_name}: no models filtered (#{after_count} remain)" }
256
+ RailsLens.logger.debug { "[ModelDetector] #{step_name}: no models filtered (#{after_count} remain)" }
256
257
  end
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
+ RailsLens.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
+ RailsLens.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
@@ -144,10 +144,10 @@ module RailsLens
144
144
  result[1] # Engine is typically the second column
145
145
  end
146
146
  rescue ActiveRecord::StatementInvalid => e
147
- Rails.logger.debug { "Failed to fetch storage engine for #{table_name}: #{e.message}" }
147
+ RailsLens.logger.debug { "Failed to fetch storage engine for #{table_name}: #{e.message}" }
148
148
  nil
149
149
  rescue => e
150
- Rails.logger.debug { "MySQL error fetching storage engine: #{e.message}" }
150
+ RailsLens.logger.debug { "MySQL error fetching storage engine: #{e.message}" }
151
151
  nil
152
152
  end
153
153
 
@@ -164,10 +164,10 @@ module RailsLens
164
164
 
165
165
  collation&.split('_')&.first
166
166
  rescue ActiveRecord::StatementInvalid => e
167
- Rails.logger.debug { "Failed to fetch charset for #{table_name}: #{e.message}" }
167
+ RailsLens.logger.debug { "Failed to fetch charset for #{table_name}: #{e.message}" }
168
168
  nil
169
169
  rescue => e
170
- Rails.logger.debug { "MySQL error fetching charset: #{e.message}" }
170
+ RailsLens.logger.debug { "MySQL error fetching charset: #{e.message}" }
171
171
  nil
172
172
  end
173
173
 
@@ -182,10 +182,10 @@ module RailsLens
182
182
  result[14] # Collation is typically the 15th column
183
183
  end
184
184
  rescue ActiveRecord::StatementInvalid => e
185
- Rails.logger.debug { "Failed to fetch collation for #{table_name}: #{e.message}" }
185
+ RailsLens.logger.debug { "Failed to fetch collation for #{table_name}: #{e.message}" }
186
186
  nil
187
187
  rescue => e
188
- Rails.logger.debug { "MySQL error fetching collation: #{e.message}" }
188
+ RailsLens.logger.debug { "MySQL error fetching collation: #{e.message}" }
189
189
  nil
190
190
  end
191
191
 
@@ -229,11 +229,11 @@ module RailsLens
229
229
  count.to_i.positive?
230
230
  rescue ActiveRecord::StatementInvalid => e
231
231
  # Table doesn't exist or no permission to query information_schema
232
- Rails.logger.debug { "Failed to check partitions for #{table_name}: #{e.message}" }
232
+ RailsLens.logger.debug { "Failed to check partitions for #{table_name}: #{e.message}" }
233
233
  false
234
234
  rescue => e
235
235
  # MySQL specific errors (connection issues, etc)
236
- Rails.logger.debug { "MySQL error checking partitions: #{e.message}" }
236
+ RailsLens.logger.debug { "MySQL error checking partitions: #{e.message}" }
237
237
  false
238
238
  end
239
239
 
@@ -259,10 +259,10 @@ module RailsLens
259
259
  end
260
260
  rescue ActiveRecord::StatementInvalid => e
261
261
  # Permission denied or table doesn't exist
262
- Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
262
+ RailsLens.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
263
263
  rescue => e
264
264
  # MySQL specific errors
265
- Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
265
+ RailsLens.logger.debug { "MySQL error fetching partitions: #{e.message}" }
266
266
  end
267
267
 
268
268
  def add_partitions_toml(lines)
@@ -297,10 +297,10 @@ module RailsLens
297
297
  lines << ']'
298
298
  rescue ActiveRecord::StatementInvalid => e
299
299
  # Permission denied or table doesn't exist
300
- Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
300
+ RailsLens.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
301
301
  rescue => e
302
302
  # MySQL specific errors
303
- Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
303
+ RailsLens.logger.debug { "MySQL error fetching partitions: #{e.message}" }
304
304
  end
305
305
 
306
306
  def add_view_dependencies_toml(lines, view_info)
@@ -345,7 +345,7 @@ module RailsLens
345
345
  dependencies: row[1].to_s.split(',').reject(&:empty?)
346
346
  }
347
347
  rescue ActiveRecord::StatementInvalid, Mysql2::Error => e
348
- Rails.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
348
+ RailsLens.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
349
349
  nil
350
350
  end
351
351
 
@@ -150,11 +150,11 @@ module RailsLens
150
150
  end
151
151
  rescue ActiveRecord::StatementInvalid => e
152
152
  # Table doesn't exist or other database error
153
- Rails.logger.debug { "Failed to fetch check constraints for #{table_name}: #{e.message}" }
153
+ RailsLens.logger.debug { "Failed to fetch check constraints for #{table_name}: #{e.message}" }
154
154
  []
155
155
  rescue PG::Error => e
156
156
  # PostgreSQL specific errors
157
- Rails.logger.debug { "PostgreSQL error fetching check constraints: #{e.message}" }
157
+ RailsLens.logger.debug { "PostgreSQL error fetching check constraints: #{e.message}" }
158
158
  []
159
159
  end
160
160
 
@@ -164,11 +164,11 @@ module RailsLens
164
164
  connection.column_comment(table_name, column_name)
165
165
  rescue ActiveRecord::StatementInvalid => e
166
166
  # Table or column doesn't exist
167
- Rails.logger.debug { "Failed to fetch column comment for #{table_name}.#{column_name}: #{e.message}" }
167
+ RailsLens.logger.debug { "Failed to fetch column comment for #{table_name}.#{column_name}: #{e.message}" }
168
168
  nil
169
169
  rescue PG::Error => e
170
170
  # PostgreSQL specific errors
171
- Rails.logger.debug { "PostgreSQL error fetching column comment: #{e.message}" }
171
+ RailsLens.logger.debug { "PostgreSQL error fetching column comment: #{e.message}" }
172
172
  nil
173
173
  end
174
174
 
@@ -178,11 +178,11 @@ module RailsLens
178
178
  connection.table_comment(table_name)
179
179
  rescue ActiveRecord::StatementInvalid => e
180
180
  # Table doesn't exist
181
- Rails.logger.debug { "Failed to fetch table comment for #{table_name}: #{e.message}" }
181
+ RailsLens.logger.debug { "Failed to fetch table comment for #{table_name}: #{e.message}" }
182
182
  nil
183
183
  rescue PG::Error => e
184
184
  # PostgreSQL specific errors
185
- Rails.logger.debug { "PostgreSQL error fetching table comment: #{e.message}" }
185
+ RailsLens.logger.debug { "PostgreSQL error fetching table comment: #{e.message}" }
186
186
  nil
187
187
  end
188
188
 
@@ -292,7 +292,7 @@ module RailsLens
292
292
  dependencies: row[2] || []
293
293
  }
294
294
  rescue ActiveRecord::StatementInvalid, PG::Error => e
295
- Rails.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
295
+ RailsLens.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
296
296
  nil
297
297
  end
298
298
 
@@ -93,10 +93,10 @@ module RailsLens
93
93
  lines << 'FOREIGN_KEYS_ENABLED: false' if fk_status && fk_status['foreign_keys'].zero?
94
94
  rescue ActiveRecord::StatementInvalid => e
95
95
  # SQLite doesn't recognize the pragma or access denied
96
- Rails.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
96
+ RailsLens.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
97
97
  rescue SQLite3::Exception => e
98
98
  # SQLite specific errors (database locked, etc)
99
- Rails.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
99
+ RailsLens.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
100
100
  end
101
101
  end
102
102
 
@@ -113,10 +113,10 @@ module RailsLens
113
113
  end
114
114
  rescue ActiveRecord::StatementInvalid => e
115
115
  # SQLite doesn't recognize the pragma or access denied
116
- Rails.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
116
+ RailsLens.logger.debug { "Failed to fetch SQLite foreign_keys pragma: #{e.message}" }
117
117
  rescue SQLite3::Exception => e
118
118
  # SQLite specific errors (database locked, etc)
119
- Rails.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
119
+ RailsLens.logger.debug { "SQLite error fetching pragmas: #{e.message}" }
120
120
  end
121
121
  end
122
122
 
@@ -164,7 +164,7 @@ module RailsLens
164
164
  dependencies: tables.sort
165
165
  }
166
166
  rescue ActiveRecord::StatementInvalid, SQLite3::Exception => e
167
- Rails.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
167
+ RailsLens.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
168
168
  nil
169
169
  end
170
170