@@ -29,12 +29,22 @@ def analyze
2929
3030 notes = [ ]
3131
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
3848
3949 notes . compact . uniq
4050 rescue ActiveRecord ::StatementInvalid => e
@@ -47,6 +57,54 @@ def analyze
4757
4858 private
4959
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+
50108 def analyze_indexes
51109 notes = [ ]
52110
@@ -320,6 +378,56 @@ def uuid_columns
320378 column . type == :uuid || ( column . type == :string && column . name . match? ( /uuid|guid/ ) )
321379 end
322380 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
323431 end
324432 end
325433end
0 commit comments