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 +4 -4
- data/CHANGELOG.md +7 -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 +4 -4
- 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 +90 -38
- 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: 7b17142784a27f1ffd45512665eea05a135e1c97b0e1dcc38505f8a07b067ad0
|
4
|
+
data.tar.gz: 9ae11ad7619a74b5662a8b6bb2e767c33ad1523f5c6a2cf590cb809389d0ab9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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
|
-
|
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
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
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} -
|
143
|
-
|
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
|
-
|
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 = {})
|
data/lib/rails_lens/version.rb
CHANGED