rails_lens 0.2.2 → 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 +19 -0
- data/lib/rails_lens/analyzers/generated_columns.rb +1 -1
- data/lib/rails_lens/analyzers/notes.rb +114 -6
- data/lib/rails_lens/annotation_pipeline.rb +26 -21
- data/lib/rails_lens/connection.rb +4 -4
- data/lib/rails_lens/erd/visualizer.rb +50 -3
- data/lib/rails_lens/model_detector.rb +73 -0
- data/lib/rails_lens/providers/base.rb +1 -1
- data/lib/rails_lens/providers/extension_notes_provider.rb +3 -2
- data/lib/rails_lens/providers/extensions_provider.rb +1 -1
- data/lib/rails_lens/providers/index_notes_provider.rb +3 -2
- data/lib/rails_lens/providers/inheritance_provider.rb +1 -1
- data/lib/rails_lens/providers/notes_provider_base.rb +3 -2
- data/lib/rails_lens/providers/schema_provider.rb +10 -8
- data/lib/rails_lens/providers/section_provider_base.rb +1 -1
- data/lib/rails_lens/providers/view_notes_provider.rb +22 -0
- data/lib/rails_lens/providers/view_provider.rb +67 -0
- data/lib/rails_lens/schema/adapters/database_info.rb +2 -2
- data/lib/rails_lens/schema/adapters/mysql.rb +113 -1
- data/lib/rails_lens/schema/adapters/postgresql.rb +161 -1
- data/lib/rails_lens/schema/adapters/sqlite3.rb +114 -1
- data/lib/rails_lens/schema/annotation_manager.rb +90 -38
- data/lib/rails_lens/version.rb +1 -1
- data/lib/rails_lens/view_metadata.rb +98 -0
- metadata +4 -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,24 @@
|
|
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
|
+
|
10
|
+
## [0.2.3](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.2...rails_lens/v0.2.3) (2025-07-31)
|
11
|
+
|
12
|
+
|
13
|
+
### Features
|
14
|
+
|
15
|
+
* add database view annotation support ([#7](https://github.com/seuros/rails_lens/issues/7)) ([a42fdcd](https://github.com/seuros/rails_lens/commit/a42fdcdfe4da9e2a086488e0c5e0c72d2f3c5d3d))
|
16
|
+
|
17
|
+
|
18
|
+
### Bug Fixes
|
19
|
+
|
20
|
+
* centralize connection management to prevent "too many clients" errors ([#9](https://github.com/seuros/rails_lens/issues/9)) ([c5d85c7](https://github.com/seuros/rails_lens/commit/c5d85c7239d1eff49494a05582cb00a8e7402618))
|
21
|
+
|
3
22
|
## [0.2.2](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.1...rails_lens/v0.2.2) (2025-07-31)
|
4
23
|
|
5
24
|
|
@@ -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
|
@@ -29,12 +29,22 @@ module RailsLens
|
|
29
29
|
|
30
30
|
notes = []
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
32
|
+
# Check if this model is backed by a view
|
33
|
+
is_view = ModelDetector.view_exists?(model_class)
|
34
|
+
|
35
|
+
if is_view
|
36
|
+
# For views, add view-specific checks
|
37
|
+
notes.concat(analyze_view_readonly)
|
38
|
+
notes.concat(analyze_view_gotchas)
|
39
|
+
else
|
40
|
+
# For tables, run all standard checks
|
41
|
+
notes.concat(analyze_indexes)
|
42
|
+
notes.concat(analyze_foreign_keys)
|
43
|
+
notes.concat(analyze_associations)
|
44
|
+
notes.concat(analyze_columns)
|
45
|
+
notes.concat(analyze_performance)
|
46
|
+
notes.concat(analyze_best_practices)
|
47
|
+
end
|
38
48
|
|
39
49
|
notes.compact.uniq
|
40
50
|
rescue ActiveRecord::StatementInvalid => e
|
@@ -47,6 +57,54 @@ module RailsLens
|
|
47
57
|
|
48
58
|
private
|
49
59
|
|
60
|
+
def analyze_view_readonly
|
61
|
+
notes = []
|
62
|
+
|
63
|
+
# Check if this model is backed by a database view
|
64
|
+
if ModelDetector.view_exists?(model_class)
|
65
|
+
notes << '👁️ View-backed model: read-only'
|
66
|
+
|
67
|
+
# Check if model has readonly implementation
|
68
|
+
unless has_readonly_implementation?
|
69
|
+
notes << 'Add readonly? method'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
notes
|
74
|
+
rescue StandardError => e
|
75
|
+
Rails.logger.debug { "Error checking view readonly status for #{model_class.name}: #{e.message}" }
|
76
|
+
[]
|
77
|
+
end
|
78
|
+
|
79
|
+
def analyze_view_gotchas
|
80
|
+
notes = []
|
81
|
+
view_metadata = ViewMetadata.new(model_class)
|
82
|
+
|
83
|
+
# Check for materialized view specific issues
|
84
|
+
if view_metadata.materialized_view?
|
85
|
+
notes << '🔄 Materialized view: data may be stale until refreshed'
|
86
|
+
unless has_refresh_methods?
|
87
|
+
notes << 'Add refresh! method for manual updates'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check for nested views (view depending on other views)
|
92
|
+
dependencies = view_metadata.dependencies
|
93
|
+
if dependencies.any? { |dep| view_exists_by_name?(dep) }
|
94
|
+
notes << '⚠️ Nested views detected: may impact query performance'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check for readonly implementation
|
98
|
+
unless has_readonly_implementation?
|
99
|
+
notes << '🔒 Add readonly protection to prevent write operations'
|
100
|
+
end
|
101
|
+
|
102
|
+
notes
|
103
|
+
rescue StandardError => e
|
104
|
+
Rails.logger.debug { "Error analyzing view gotchas for #{model_class.name}: #{e.message}" }
|
105
|
+
[]
|
106
|
+
end
|
107
|
+
|
50
108
|
def analyze_indexes
|
51
109
|
notes = []
|
52
110
|
|
@@ -320,6 +378,56 @@ module RailsLens
|
|
320
378
|
column.type == :uuid || (column.type == :string && column.name.match?(/uuid|guid/))
|
321
379
|
end
|
322
380
|
end
|
381
|
+
|
382
|
+
def has_readonly_implementation?
|
383
|
+
# Check if model has readonly? method defined (not just inherited from ActiveRecord)
|
384
|
+
model_class.method_defined?(:readonly?) &&
|
385
|
+
model_class.instance_method(:readonly?).owner != ActiveRecord::Base
|
386
|
+
rescue StandardError
|
387
|
+
false
|
388
|
+
end
|
389
|
+
|
390
|
+
def has_refresh_methods?
|
391
|
+
# Check if model has refresh! method for materialized views
|
392
|
+
model_class.respond_to?(:refresh!) || model_class.respond_to?(:refresh_concurrently!)
|
393
|
+
rescue StandardError
|
394
|
+
false
|
395
|
+
end
|
396
|
+
|
397
|
+
def view_exists_by_name?(view_name)
|
398
|
+
# Check if a view exists in the database by name
|
399
|
+
case @connection.adapter_name.downcase
|
400
|
+
when 'postgresql'
|
401
|
+
result = @connection.exec_query(<<~SQL.squish, 'Check PostgreSQL View Existence')
|
402
|
+
SELECT 1 FROM information_schema.views
|
403
|
+
WHERE table_name = '#{@connection.quote_string(view_name)}'
|
404
|
+
UNION ALL
|
405
|
+
SELECT 1 FROM pg_matviews
|
406
|
+
WHERE matviewname = '#{@connection.quote_string(view_name)}'
|
407
|
+
LIMIT 1
|
408
|
+
SQL
|
409
|
+
result.rows.any?
|
410
|
+
when 'mysql', 'mysql2'
|
411
|
+
result = @connection.exec_query(<<~SQL.squish, 'Check MySQL View Existence')
|
412
|
+
SELECT 1 FROM information_schema.views
|
413
|
+
WHERE table_name = '#{@connection.quote_string(view_name)}'
|
414
|
+
LIMIT 1
|
415
|
+
SQL
|
416
|
+
result.rows.any?
|
417
|
+
when 'sqlite', 'sqlite3'
|
418
|
+
result = @connection.exec_query(<<~SQL.squish, 'Check SQLite View Existence')
|
419
|
+
SELECT 1 FROM sqlite_master
|
420
|
+
WHERE type = 'view' AND name = '#{@connection.quote_string(view_name)}'
|
421
|
+
LIMIT 1
|
422
|
+
SQL
|
423
|
+
result.rows.any?
|
424
|
+
else
|
425
|
+
false
|
426
|
+
end
|
427
|
+
rescue StandardError => e
|
428
|
+
Rails.logger.debug { "Error checking view existence for #{view_name}: #{e.message}" }
|
429
|
+
false
|
430
|
+
end
|
323
431
|
end
|
324
432
|
end
|
325
433
|
end
|
@@ -27,30 +27,33 @@ module RailsLens
|
|
27
27
|
notes: []
|
28
28
|
}
|
29
29
|
|
30
|
-
|
31
|
-
|
30
|
+
# Use the model's connection pool to manage a single connection for all providers
|
31
|
+
model_class.connection_pool.with_connection do |connection|
|
32
|
+
@providers.each do |provider|
|
33
|
+
next unless provider.applicable?(model_class)
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
+
begin
|
36
|
+
result = provider.process(model_class, connection)
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
case provider.type
|
39
|
+
when :schema
|
40
|
+
results[:schema] = result
|
41
|
+
when :section
|
42
|
+
results[:sections] << result if result
|
43
|
+
when :notes
|
44
|
+
results[:notes].concat(Array(result))
|
45
|
+
end
|
46
|
+
rescue ActiveRecord::StatementInvalid => e
|
47
|
+
warn "Provider #{provider.class} database error for #{model_class}: #{e.message}"
|
48
|
+
rescue ActiveRecord::ConnectionNotDefined => e
|
49
|
+
warn "Provider #{provider.class} connection error for #{model_class}: #{e.message}"
|
50
|
+
rescue NameError, NoMethodError => e
|
51
|
+
warn "Provider #{provider.class} method error for #{model_class}: #{e.message}"
|
52
|
+
rescue RailsLens::Error => e
|
53
|
+
warn "Provider #{provider.class} rails_lens error for #{model_class}: #{e.message}"
|
54
|
+
rescue StandardError => e
|
55
|
+
warn "Provider #{provider.class} unexpected error for #{model_class}: #{e.message}"
|
43
56
|
end
|
44
|
-
rescue ActiveRecord::StatementInvalid => e
|
45
|
-
warn "Provider #{provider.class} database error for #{model_class}: #{e.message}"
|
46
|
-
rescue ActiveRecord::ConnectionNotDefined => e
|
47
|
-
warn "Provider #{provider.class} connection error for #{model_class}: #{e.message}"
|
48
|
-
rescue NameError, NoMethodError => e
|
49
|
-
warn "Provider #{provider.class} method error for #{model_class}: #{e.message}"
|
50
|
-
rescue RailsLens::Error => e
|
51
|
-
warn "Provider #{provider.class} rails_lens error for #{model_class}: #{e.message}"
|
52
|
-
rescue StandardError => e
|
53
|
-
warn "Provider #{provider.class} unexpected error for #{model_class}: #{e.message}"
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
@@ -65,6 +68,7 @@ module RailsLens
|
|
65
68
|
|
66
69
|
# Section providers (additional structured content)
|
67
70
|
register(Providers::ExtensionsProvider.new) if RailsLens.config.extensions[:enabled]
|
71
|
+
register(Providers::ViewProvider.new)
|
68
72
|
register(Providers::InheritanceProvider.new)
|
69
73
|
register(Providers::EnumsProvider.new)
|
70
74
|
register(Providers::DelegatedTypesProvider.new)
|
@@ -75,6 +79,7 @@ module RailsLens
|
|
75
79
|
# Notes providers (analysis and recommendations)
|
76
80
|
return unless RailsLens.config.schema[:include_notes]
|
77
81
|
|
82
|
+
register(Providers::ViewNotesProvider.new)
|
78
83
|
register(Providers::IndexNotesProvider.new)
|
79
84
|
register(Providers::ForeignKeyNotesProvider.new)
|
80
85
|
register(Providers::AssociationNotesProvider.new)
|
@@ -3,12 +3,12 @@
|
|
3
3
|
module RailsLens
|
4
4
|
class Connection
|
5
5
|
class << self
|
6
|
-
def adapter_for(model_class)
|
7
|
-
|
8
|
-
adapter_name = detect_adapter_name(
|
6
|
+
def adapter_for(model_class, connection = nil)
|
7
|
+
conn = connection || model_class.connection
|
8
|
+
adapter_name = detect_adapter_name(conn)
|
9
9
|
|
10
10
|
adapter_class = resolve_adapter_class(adapter_name)
|
11
|
-
adapter_class.new(
|
11
|
+
adapter_class.new(conn, model_class.table_name)
|
12
12
|
end
|
13
13
|
|
14
14
|
def resolve_adapter_class(adapter_name)
|
@@ -66,8 +66,11 @@ module RailsLens
|
|
66
66
|
# Additional safety check: Skip abstract models that might have slipped through
|
67
67
|
next if model.abstract_class?
|
68
68
|
|
69
|
-
# Skip models without valid tables or columns
|
70
|
-
|
69
|
+
# Skip models without valid tables/views or columns
|
70
|
+
# Include both table-backed and view-backed models
|
71
|
+
is_view = ModelDetector.view_exists?(model)
|
72
|
+
has_data_source = is_view || (model.table_exists? && model.columns.present?)
|
73
|
+
next unless has_data_source
|
71
74
|
|
72
75
|
model_display_name = format_model_name(model)
|
73
76
|
|
@@ -105,12 +108,19 @@ module RailsLens
|
|
105
108
|
end
|
106
109
|
end
|
107
110
|
|
111
|
+
# Add visual styling for views vs tables
|
112
|
+
add_visual_styling(output, models)
|
113
|
+
|
108
114
|
# Add relationships
|
109
115
|
output << ' %% Relationships'
|
110
116
|
models.each do |model|
|
111
117
|
# Skip abstract models in relationship generation too
|
112
118
|
next if model.abstract_class?
|
113
|
-
|
119
|
+
|
120
|
+
# Include both table-backed and view-backed models
|
121
|
+
is_view = ModelDetector.view_exists?(model)
|
122
|
+
has_data_source = is_view || (model.table_exists? && model.columns.present?)
|
123
|
+
next unless has_data_source
|
114
124
|
|
115
125
|
add_model_relationships(output, model, models)
|
116
126
|
end
|
@@ -253,6 +263,43 @@ module RailsLens
|
|
253
263
|
output << ' }}%%'
|
254
264
|
end
|
255
265
|
|
266
|
+
def add_visual_styling(output, models)
|
267
|
+
# Add class definitions for visual distinction between tables and views
|
268
|
+
output << ''
|
269
|
+
output << ' %% Entity Styling'
|
270
|
+
|
271
|
+
# Define styling classes
|
272
|
+
output << ' classDef tableEntity fill:#f9f9f9,stroke:#333,stroke-width:2px'
|
273
|
+
output << ' classDef viewEntity fill:#e6f3ff,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5'
|
274
|
+
output << ' classDef materializedViewEntity fill:#ffe6e6,stroke:#333,stroke-width:3px,stroke-dasharray: 5 5'
|
275
|
+
|
276
|
+
# Apply styling to each model
|
277
|
+
models.each do |model|
|
278
|
+
next if model.abstract_class?
|
279
|
+
|
280
|
+
is_view = ModelDetector.view_exists?(model)
|
281
|
+
has_data_source = is_view || (model.table_exists? && model.columns.present?)
|
282
|
+
next unless has_data_source
|
283
|
+
|
284
|
+
model_display_name = format_model_name(model)
|
285
|
+
|
286
|
+
if is_view
|
287
|
+
view_metadata = ViewMetadata.new(model)
|
288
|
+
output << if view_metadata.materialized_view?
|
289
|
+
" class #{model_display_name} materializedViewEntity"
|
290
|
+
else
|
291
|
+
" class #{model_display_name} viewEntity"
|
292
|
+
end
|
293
|
+
else
|
294
|
+
output << " class #{model_display_name} tableEntity"
|
295
|
+
end
|
296
|
+
rescue StandardError => e
|
297
|
+
Rails.logger.debug { "Warning: Could not apply styling to #{model.name}: #{e.message}" }
|
298
|
+
end
|
299
|
+
|
300
|
+
output << ''
|
301
|
+
end
|
302
|
+
|
256
303
|
def group_models_by_database(models)
|
257
304
|
grouped = Hash.new { |h, k| h[k] = [] }
|
258
305
|
|
@@ -37,8 +37,81 @@ module RailsLens
|
|
37
37
|
concrete_models.select { |model| model.superclass != ActiveRecord::Base && concrete_models.include?(model.superclass) }
|
38
38
|
end
|
39
39
|
|
40
|
+
def view_backed_models
|
41
|
+
detect_models.select { |model| view_exists?(model) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def table_backed_models
|
45
|
+
detect_models.reject { |model| view_exists?(model) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def view_exists?(model_class)
|
49
|
+
return false if model_class.abstract_class?
|
50
|
+
return false unless model_class.table_name
|
51
|
+
|
52
|
+
# Cache view existence checks for performance
|
53
|
+
@view_cache ||= {}
|
54
|
+
cache_key = "#{model_class.connection.object_id}_#{model_class.table_name}"
|
55
|
+
|
56
|
+
return @view_cache[cache_key] if @view_cache.key?(cache_key)
|
57
|
+
|
58
|
+
@view_cache[cache_key] = check_view_existence(model_class)
|
59
|
+
end
|
60
|
+
|
40
61
|
private
|
41
62
|
|
63
|
+
def check_view_existence(model_class)
|
64
|
+
connection = model_class.connection
|
65
|
+
table_name = model_class.table_name
|
66
|
+
|
67
|
+
case connection.adapter_name.downcase
|
68
|
+
when 'postgresql'
|
69
|
+
check_postgresql_view(connection, table_name)
|
70
|
+
when 'mysql', 'mysql2'
|
71
|
+
check_mysql_view(connection, table_name)
|
72
|
+
when 'sqlite', 'sqlite3'
|
73
|
+
check_sqlite_view(connection, table_name)
|
74
|
+
else
|
75
|
+
false # Unsupported adapter
|
76
|
+
end
|
77
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined
|
78
|
+
false # If we can't check, assume it's not a view
|
79
|
+
end
|
80
|
+
|
81
|
+
# rubocop:disable Naming/PredicateMethod
|
82
|
+
def check_postgresql_view(connection, table_name)
|
83
|
+
# Check both regular views and materialized views
|
84
|
+
result = connection.exec_query(<<~SQL.squish, 'Check PostgreSQL View')
|
85
|
+
SELECT 1 FROM information_schema.views
|
86
|
+
WHERE table_name = '#{connection.quote_string(table_name)}'
|
87
|
+
UNION ALL
|
88
|
+
SELECT 1 FROM pg_matviews
|
89
|
+
WHERE matviewname = '#{connection.quote_string(table_name)}'
|
90
|
+
LIMIT 1
|
91
|
+
SQL
|
92
|
+
result.rows.any?
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_mysql_view(connection, table_name)
|
96
|
+
result = connection.exec_query(<<~SQL.squish, 'Check MySQL View')
|
97
|
+
SELECT 1 FROM information_schema.views
|
98
|
+
WHERE table_name = '#{connection.quote_string(table_name)}'
|
99
|
+
AND table_schema = DATABASE()
|
100
|
+
LIMIT 1
|
101
|
+
SQL
|
102
|
+
result.rows.any?
|
103
|
+
end
|
104
|
+
|
105
|
+
def check_sqlite_view(connection, table_name)
|
106
|
+
result = connection.exec_query(<<~SQL.squish, 'Check SQLite View')
|
107
|
+
SELECT 1 FROM sqlite_master
|
108
|
+
WHERE type = 'view' AND name = '#{connection.quote_string(table_name)}'
|
109
|
+
LIMIT 1
|
110
|
+
SQL
|
111
|
+
result.rows.any?
|
112
|
+
end
|
113
|
+
# rubocop:enable Naming/PredicateMethod
|
114
|
+
|
42
115
|
def eager_load_models
|
43
116
|
# Zeitwerk is always available in Rails 7+
|
44
117
|
Zeitwerk::Loader.eager_load_all
|
@@ -21,7 +21,7 @@ module RailsLens
|
|
21
21
|
# For :schema type - returns a string with the schema content
|
22
22
|
# For :section type - returns a hash with { title: String, content: String } or nil
|
23
23
|
# For :notes type - returns an array of note strings
|
24
|
-
def process(model_class)
|
24
|
+
def process(model_class, connection = nil)
|
25
25
|
raise NotImplementedError, "#{self.class} must implement #process"
|
26
26
|
end
|
27
27
|
|
@@ -8,10 +8,11 @@ module RailsLens
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def applicable?(model_class)
|
11
|
-
|
11
|
+
# Only applicable to tables, not views
|
12
|
+
RailsLens.config.extensions[:enabled] && model_has_table?(model_class) && !ModelDetector.view_exists?(model_class)
|
12
13
|
end
|
13
14
|
|
14
|
-
def process(model_class)
|
15
|
+
def process(model_class, connection = nil)
|
15
16
|
results = ExtensionLoader.apply_extensions(model_class)
|
16
17
|
results[:notes]
|
17
18
|
end
|
@@ -8,10 +8,11 @@ module RailsLens
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def applicable?(model_class)
|
11
|
-
|
11
|
+
# Only applicable to tables, not views
|
12
|
+
model_has_table?(model_class) && !ModelDetector.view_exists?(model_class)
|
12
13
|
end
|
13
14
|
|
14
|
-
def process(model_class)
|
15
|
+
def process(model_class, connection = nil)
|
15
16
|
analyzer = Analyzers::IndexAnalyzer.new(model_class)
|
16
17
|
analyzer.analyze
|
17
18
|
end
|
@@ -9,14 +9,15 @@ module RailsLens
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def applicable?(model_class)
|
12
|
-
|
12
|
+
# Only applicable to tables, not views
|
13
|
+
model_has_table?(model_class) && !ModelDetector.view_exists?(model_class)
|
13
14
|
end
|
14
15
|
|
15
16
|
def analyzer_class
|
16
17
|
raise NotImplementedError, "#{self.class} must implement #analyzer_class"
|
17
18
|
end
|
18
19
|
|
19
|
-
def process(model_class)
|
20
|
+
def process(model_class, connection = nil)
|
20
21
|
analyzer = analyzer_class.new(model_class)
|
21
22
|
analyzer.analyze
|
22
23
|
end
|
@@ -11,17 +11,19 @@ module RailsLens
|
|
11
11
|
true # Always applicable - handles both abstract and regular models
|
12
12
|
end
|
13
13
|
|
14
|
-
def process(model_class)
|
14
|
+
def process(model_class, connection = nil)
|
15
|
+
# Use passed connection or fall back to model's connection
|
16
|
+
conn = connection || model_class.connection
|
17
|
+
|
15
18
|
if model_class.abstract_class?
|
16
19
|
# For abstract classes, show database connection information in TOML format
|
17
|
-
|
18
|
-
adapter_name = connection.adapter_name
|
20
|
+
adapter_name = conn.adapter_name
|
19
21
|
|
20
22
|
lines = []
|
21
23
|
|
22
24
|
# Get connection name
|
23
25
|
begin
|
24
|
-
connection_name =
|
26
|
+
connection_name = conn.pool.db_config.name
|
25
27
|
lines << "connection = \"#{connection_name}\""
|
26
28
|
rescue StandardError
|
27
29
|
lines << 'connection = "unknown"'
|
@@ -31,7 +33,7 @@ module RailsLens
|
|
31
33
|
|
32
34
|
# Add database version information
|
33
35
|
begin
|
34
|
-
db_version =
|
36
|
+
db_version = conn.database_version
|
35
37
|
lines << "database_version = \"#{db_version}\""
|
36
38
|
rescue StandardError
|
37
39
|
lines << 'database_version = "unknown"'
|
@@ -39,7 +41,7 @@ module RailsLens
|
|
39
41
|
|
40
42
|
# Add database name if available
|
41
43
|
begin
|
42
|
-
db_name =
|
44
|
+
db_name = conn.current_database
|
43
45
|
lines << "database_name = \"#{db_name}\"" if db_name
|
44
46
|
rescue StandardError
|
45
47
|
# Skip if can't get database name
|
@@ -51,8 +53,8 @@ module RailsLens
|
|
51
53
|
|
52
54
|
lines.join("\n")
|
53
55
|
else
|
54
|
-
# Add schema information for regular models
|
55
|
-
adapter = Connection.adapter_for(model_class)
|
56
|
+
# Add schema information for regular models (tables or views)
|
57
|
+
adapter = Connection.adapter_for(model_class, conn)
|
56
58
|
adapter.generate_annotation(model_class)
|
57
59
|
end
|
58
60
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Providers
|
5
|
+
# View-specific notes provider that wraps the Notes analyzer for views only
|
6
|
+
class ViewNotesProvider < Base
|
7
|
+
def type
|
8
|
+
:notes
|
9
|
+
end
|
10
|
+
|
11
|
+
def applicable?(model_class)
|
12
|
+
# Only applicable to views
|
13
|
+
model_has_table?(model_class) && ModelDetector.view_exists?(model_class)
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(model_class, connection = nil)
|
17
|
+
analyzer = Analyzers::Notes.new(model_class)
|
18
|
+
analyzer.analyze
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsLens
|
4
|
+
module Providers
|
5
|
+
class ViewProvider < SectionProviderBase
|
6
|
+
def applicable?(model_class)
|
7
|
+
# Only applicable for models backed by views
|
8
|
+
ModelDetector.view_exists?(model_class)
|
9
|
+
end
|
10
|
+
|
11
|
+
def process(model_class, connection = nil)
|
12
|
+
view_metadata = ViewMetadata.new(model_class)
|
13
|
+
|
14
|
+
return nil unless view_metadata.view_exists?
|
15
|
+
|
16
|
+
{
|
17
|
+
title: '== View Information',
|
18
|
+
content: generate_view_content(view_metadata)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generate_view_content(view_metadata)
|
25
|
+
lines = []
|
26
|
+
|
27
|
+
# View type (regular or materialized)
|
28
|
+
if view_metadata.view_type
|
29
|
+
lines << "View Type: #{view_metadata.view_type}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Updatable status
|
33
|
+
lines << "Updatable: #{view_metadata.updatable? ? 'Yes' : 'No'}"
|
34
|
+
|
35
|
+
# Dependencies
|
36
|
+
dependencies = view_metadata.dependencies
|
37
|
+
if dependencies.any?
|
38
|
+
lines << "Dependencies: #{dependencies.join(', ')}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Refresh strategy for materialized views
|
42
|
+
if view_metadata.materialized_view? && view_metadata.refresh_strategy
|
43
|
+
lines << "Refresh Strategy: #{view_metadata.refresh_strategy}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Last refreshed timestamp for materialized views
|
47
|
+
if view_metadata.materialized_view? && view_metadata.last_refreshed
|
48
|
+
lines << "Last Refreshed: #{view_metadata.last_refreshed}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# View definition (truncated for readability)
|
52
|
+
if view_metadata.view_definition
|
53
|
+
definition = view_metadata.view_definition
|
54
|
+
# Truncate long definitions
|
55
|
+
if definition.length > 200
|
56
|
+
definition = "#{definition[0..200]}..."
|
57
|
+
end
|
58
|
+
# Remove extra whitespace and newlines
|
59
|
+
definition = definition.gsub(/\s+/, ' ').strip
|
60
|
+
lines << "Definition: #{definition}"
|
61
|
+
end
|
62
|
+
|
63
|
+
lines.join("\n")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|