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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41ebdd3c52cca1c77790aa896dcc5962acb92c44eec90a2dceeda98f310aefb7
4
- data.tar.gz: 72e49695a796e9d030fd80aedab53d7dea055db82c98e8023a6a2bb4293e139f
3
+ metadata.gz: 7b17142784a27f1ffd45512665eea05a135e1c97b0e1dcc38505f8a07b067ad0
4
+ data.tar.gz: 9ae11ad7619a74b5662a8b6bb2e767c33ad1523f5c6a2cf590cb809389d0ab9e
5
5
  SHA512:
6
- metadata.gz: d0e890e0ae21326d6176542668277a1c8e5d6336f013dec8cc8854f892c0136c6a40fb4557789dc661c47de70fbf82c0e59dcc75769b4a41e613f681e75f9a9f
7
- data.tar.gz: d5c0d6aaecbd3a46986cc86507b3550347bd09e69f4126721fdb9acec05110a2f622fa2555d8c37151144efdd89ef5b87129576d3bc66c02f2b8cdd3404fcb77
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
- notes.concat(analyze_indexes)
33
- notes.concat(analyze_foreign_keys)
34
- notes.concat(analyze_associations)
35
- notes.concat(analyze_columns)
36
- notes.concat(analyze_performance)
37
- notes.concat(analyze_best_practices)
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
- @providers.each do |provider|
31
- next unless provider.applicable?(model_class)
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
- begin
34
- result = provider.process(model_class)
35
+ begin
36
+ result = provider.process(model_class, connection)
35
37
 
36
- case provider.type
37
- when :schema
38
- results[:schema] = result
39
- when :section
40
- results[:sections] << result if result
41
- when :notes
42
- results[:notes].concat(Array(result))
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
- connection = model_class.connection
8
- adapter_name = detect_adapter_name(connection)
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(connection, model_class.table_name)
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
- next unless model.table_exists? && model.columns.present?
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
- next unless model.table_exists? && model.columns.present?
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
- RailsLens.config.extensions[:enabled] && model_has_table?(model_class)
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
@@ -7,7 +7,7 @@ module RailsLens
7
7
  :section
8
8
  end
9
9
 
10
- def process(model_class)
10
+ def process(model_class, connection = nil)
11
11
  results = ExtensionLoader.apply_extensions(model_class)
12
12
 
13
13
  return nil if results[:annotations].empty?
@@ -8,10 +8,11 @@ module RailsLens
8
8
  end
9
9
 
10
10
  def applicable?(model_class)
11
- model_has_table?(model_class)
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
@@ -7,7 +7,7 @@ module RailsLens
7
7
  :section
8
8
  end
9
9
 
10
- def process(model_class)
10
+ def process(model_class, connection = nil)
11
11
  analyzer = Analyzers::Inheritance.new(model_class)
12
12
  content = analyzer.analyze
13
13
 
@@ -9,14 +9,15 @@ module RailsLens
9
9
  end
10
10
 
11
11
  def applicable?(model_class)
12
- model_has_table?(model_class)
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
- connection = model_class.connection
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 = connection.pool.db_config.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 = connection.database_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 = connection.current_database
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
@@ -12,7 +12,7 @@ module RailsLens
12
12
  raise NotImplementedError, "#{self.class} must implement #analyzer_class"
13
13
  end
14
14
 
15
- def process(model_class)
15
+ def process(model_class, connection = nil)
16
16
  analyzer = analyzer_class.new(model_class)
17
17
  content = analyzer.analyze
18
18
 
@@ -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