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 +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/rails_lens/analyzers/generated_columns.rb +1 -1
- data/lib/rails_lens/analyzers/notes.rb +4 -4
- data/lib/rails_lens/model_detector.rb +170 -50
- data/lib/rails_lens/providers/schema_provider.rb +6 -2
- data/lib/rails_lens/schema/adapters/database_info.rb +2 -2
- data/lib/rails_lens/schema/adapters/mysql.rb +1 -1
- data/lib/rails_lens/schema/adapters/postgresql.rb +3 -5
- data/lib/rails_lens/schema/adapters/sqlite3.rb +2 -2
- data/lib/rails_lens/schema/annotation_manager.rb +188 -35
- data/lib/rails_lens/tasks/schema.rake +8 -1
- data/lib/rails_lens/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b150328b6771f2de4015820d40e3942c10b41903687f800fd4b068f79605deeb
|
4
|
+
data.tar.gz: 2d67fc7fd815b1af80c3cb9e1ad8b94e1166129fc3a1eb24e28958ad3e625b47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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 =
|
292
|
-
|
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 = "
|
295
|
-
|
296
|
-
|
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 = "
|
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
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
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
|
-
#
|
16
|
-
|
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
|
@@ -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
|
-
|
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
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
186
|
+
# Get all connection pools first
|
187
|
+
all_pools = get_all_connection_pools(models_by_connection_pool)
|
123
188
|
|
124
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
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
|
-
|
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?
|
data/lib/rails_lens/version.rb
CHANGED