rails_lens 0.2.4 → 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 +7 -0
- data/lib/rails_lens/model_detector.rb +166 -46
- data/lib/rails_lens/providers/schema_provider.rb +6 -2
- data/lib/rails_lens/schema/annotation_manager.rb +112 -11
- 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,12 @@
|
|
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
|
+
|
3
10
|
## [0.2.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.3...rails_lens/v0.2.4) (2025-07-31)
|
4
11
|
|
5
12
|
|
@@ -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
|
@@ -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
|
@@ -85,8 +85,33 @@ module RailsLens
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
else
|
88
|
-
#
|
89
|
-
|
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
|
90
115
|
end
|
91
116
|
|
92
117
|
annotation = Annotation.new
|
@@ -117,7 +142,13 @@ module RailsLens
|
|
117
142
|
end
|
118
143
|
|
119
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
|
+
|
120
150
|
models = ModelDetector.detect_models(options)
|
151
|
+
puts "Detected #{models.size} models for annotation" if options[:verbose]
|
121
152
|
|
122
153
|
# Filter abstract classes based on options
|
123
154
|
if options[:include_abstract]
|
@@ -133,13 +164,35 @@ module RailsLens
|
|
133
164
|
|
134
165
|
# Group models by their connection pool to process each database separately
|
135
166
|
models_by_connection_pool = models.group_by do |model|
|
136
|
-
model.connection_pool
|
137
|
-
|
138
|
-
|
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
|
139
172
|
end
|
140
173
|
|
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]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Get all connection pools first
|
187
|
+
all_pools = get_all_connection_pools(models_by_connection_pool)
|
188
|
+
|
189
|
+
# Log initial connection status (removed verbose output)
|
190
|
+
|
141
191
|
models_by_connection_pool.each do |connection_pool, pool_models|
|
142
192
|
if connection_pool
|
193
|
+
# Disconnect all OTHER connection pools before processing this one
|
194
|
+
disconnect_other_pools(connection_pool, all_pools, options)
|
195
|
+
|
143
196
|
# Process all models for this database using a single connection
|
144
197
|
connection_pool.with_connection do |connection|
|
145
198
|
pool_models.each do |model|
|
@@ -147,9 +200,20 @@ module RailsLens
|
|
147
200
|
end
|
148
201
|
end
|
149
202
|
else
|
150
|
-
#
|
151
|
-
|
152
|
-
|
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
|
153
217
|
end
|
154
218
|
end
|
155
219
|
end
|
@@ -167,7 +231,6 @@ module RailsLens
|
|
167
231
|
# Skip models without tables or with missing tables (but not abstract classes)
|
168
232
|
unless model.abstract_class? || model.table_exists?
|
169
233
|
results[:skipped] << model.name
|
170
|
-
warn "Skipping #{model.name} - table does not exist" if options[:verbose]
|
171
234
|
return
|
172
235
|
end
|
173
236
|
|
@@ -191,10 +254,9 @@ module RailsLens
|
|
191
254
|
else
|
192
255
|
results[:skipped] << model.name
|
193
256
|
end
|
194
|
-
rescue ActiveRecord::StatementInvalid
|
257
|
+
rescue ActiveRecord::StatementInvalid
|
195
258
|
# Handle database-related errors (missing tables, schemas, etc.)
|
196
259
|
results[:skipped] << model.name
|
197
|
-
warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
|
198
260
|
rescue StandardError => e
|
199
261
|
model_name = if model.is_a?(Class) && model.respond_to?(:name)
|
200
262
|
model.name
|
@@ -222,6 +284,45 @@ module RailsLens
|
|
222
284
|
results
|
223
285
|
end
|
224
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
|
+
|
225
326
|
private
|
226
327
|
|
227
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