rails_lens 0.2.3 → 0.2.4

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: 7b17142784a27f1ffd45512665eea05a135e1c97b0e1dcc38505f8a07b067ad0
4
+ data.tar.gz: 9ae11ad7619a74b5662a8b6bb2e767c33ad1523f5c6a2cf590cb809389d0ab9e
5
5
  SHA512:
6
- metadata.gz: 0fc9a2a6d597e0d5a5e9b5e1c5eddf79bdca77448e780686fd28eabc26c5085829fe543595bdde05d31949a0e5ca42475bd379d89b9d552c128c06070d7a555c
7
- data.tar.gz: 3982294a0d8a11b0ba516a7f90dc5595b488a3dcae79ef874c8d7d0cb0369f4a6438304adf1dd476d3aa8542bd445ec44022b549dcdeb48bf63084b5bebdd22d
6
+ metadata.gz: 7ebe5bfc79a4415138fc65f03f393796780e514a32f5226c0fbf66acb2d4d6dd2ac5e1b8878429f3383023f7489b5b40a35d9930876d8ffecf563a14f8bdbfb5
7
+ data.tar.gz: 2b40b476f42536db544130081063e3a0e992bbeda51d4ff993b4b3be3d0b8073a5ea00c3df98d57321de995d3dca6120f335fc9c2fca652abe3794cc4c704cdd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.3...rails_lens/v0.2.4) (2025-07-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 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))
9
+
3
10
  ## [0.2.3](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.2...rails_lens/v0.2.3) (2025-07-31)
4
11
 
5
12
 
@@ -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
@@ -82,10 +82,10 @@ module RailsLens
82
82
  def check_postgresql_view(connection, table_name)
83
83
  # Check both regular views and materialized views
84
84
  result = connection.exec_query(<<~SQL.squish, 'Check PostgreSQL View')
85
- SELECT 1 FROM information_schema.views#{' '}
85
+ SELECT 1 FROM information_schema.views
86
86
  WHERE table_name = '#{connection.quote_string(table_name)}'
87
87
  UNION ALL
88
- SELECT 1 FROM pg_matviews#{' '}
88
+ SELECT 1 FROM pg_matviews
89
89
  WHERE matviewname = '#{connection.quote_string(table_name)}'
90
90
  LIMIT 1
91
91
  SQL
@@ -94,7 +94,7 @@ module RailsLens
94
94
 
95
95
  def check_mysql_view(connection, table_name)
96
96
  result = connection.exec_query(<<~SQL.squish, 'Check MySQL View')
97
- SELECT 1 FROM information_schema.views#{' '}
97
+ SELECT 1 FROM information_schema.views
98
98
  WHERE table_name = '#{connection.quote_string(table_name)}'
99
99
  AND table_schema = DATABASE()
100
100
  LIMIT 1
@@ -104,7 +104,7 @@ module RailsLens
104
104
 
105
105
  def check_sqlite_view(connection, table_name)
106
106
  result = connection.exec_query(<<~SQL.squish, 'Check SQLite View')
107
- SELECT 1 FROM sqlite_master#{' '}
107
+ SELECT 1 FROM sqlite_master
108
108
  WHERE type = 'view' AND name = '#{connection.quote_string(table_name)}'
109
109
  LIMIT 1
110
110
  SQL
@@ -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,33 @@ 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
+ # Fall back to normal processing without connection management
89
+ results = pipeline.process(model_class)
90
+ end
65
91
 
66
92
  annotation = Annotation.new
67
93
 
@@ -105,51 +131,77 @@ module RailsLens
105
131
 
106
132
  results = { annotated: [], skipped: [], failed: [] }
107
133
 
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
134
+ # Group models by their connection pool to process each database separately
135
+ models_by_connection_pool = models.group_by do |model|
136
+ model.connection_pool
137
+ rescue StandardError
138
+ nil # Models without connection pools will be processed separately
139
+ end
114
140
 
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
141
+ models_by_connection_pool.each do |connection_pool, pool_models|
142
+ if connection_pool
143
+ # Process all models for this database using a single connection
144
+ connection_pool.with_connection do |connection|
145
+ pool_models.each do |model|
146
+ process_model_with_connection(model, connection, results, options)
147
+ end
148
+ end
149
+ else
150
+ # Process models without connection pools individually
151
+ pool_models.each do |model|
152
+ process_model_with_connection(model, nil, results, options)
153
+ end
120
154
  end
155
+ end
121
156
 
122
- manager = new(model)
123
-
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
157
+ results
158
+ end
130
159
 
131
- # Allow external files when models_path is provided (for testing)
132
- allow_external = options[:models_path].present?
160
+ def self.process_model_with_connection(model, connection, results, options)
161
+ # Ensure model is actually a class, not a hash or other object
162
+ unless model.is_a?(Class)
163
+ results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" }
164
+ return
165
+ end
133
166
 
134
- if manager.annotate_file(file_path, allow_external_files: allow_external)
135
- results[:annotated] << model.name
136
- else
137
- results[:skipped] << model.name
138
- end
139
- rescue ActiveRecord::StatementInvalid => e
140
- # Handle database-related errors (missing tables, schemas, etc.)
167
+ # Skip models without tables or with missing tables (but not abstract classes)
168
+ unless model.abstract_class? || model.table_exists?
141
169
  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 }
170
+ warn "Skipping #{model.name} - table does not exist" if options[:verbose]
171
+ return
150
172
  end
151
173
 
152
- results
174
+ manager = new(model)
175
+
176
+ # Set the connection in the manager if provided
177
+ manager.instance_variable_set(:@connection, connection) if connection
178
+
179
+ # Determine file path based on options
180
+ file_path = if options[:models_path]
181
+ File.join(options[:models_path], "#{model.name.underscore}.rb")
182
+ else
183
+ nil # Use default model_file_path
184
+ end
185
+
186
+ # Allow external files when models_path is provided (for testing)
187
+ allow_external = options[:models_path].present?
188
+
189
+ if manager.annotate_file(file_path, allow_external_files: allow_external)
190
+ results[:annotated] << model.name
191
+ else
192
+ results[:skipped] << model.name
193
+ end
194
+ rescue ActiveRecord::StatementInvalid => e
195
+ # Handle database-related errors (missing tables, schemas, etc.)
196
+ results[:skipped] << model.name
197
+ warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
198
+ rescue StandardError => e
199
+ model_name = if model.is_a?(Class) && model.respond_to?(:name)
200
+ model.name
201
+ else
202
+ model.inspect
203
+ end
204
+ results[:failed] << { model: model_name, error: e.message }
153
205
  end
154
206
 
155
207
  def self.remove_all(options = {})
@@ -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.4'
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih