rails_lens 0.2.0 → 0.2.3
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 +27 -0
- data/README.md +70 -0
- data/lib/rails_lens/analyzers/delegated_types.rb +6 -1
- data/lib/rails_lens/analyzers/notes.rb +114 -6
- data/lib/rails_lens/annotation_pipeline.rb +26 -21
- data/lib/rails_lens/cli.rb +6 -0
- data/lib/rails_lens/connection.rb +4 -4
- data/lib/rails_lens/erd/visualizer.rb +86 -11
- data/lib/rails_lens/model_detector.rb +89 -5
- 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 +28 -13
- 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/mysql.rb +119 -7
- data/lib/rails_lens/schema/adapters/postgresql.rb +163 -1
- data/lib/rails_lens/schema/adapters/sqlite3.rb +114 -1
- data/lib/rails_lens/schema/annotation_manager.rb +2 -2
- data/lib/rails_lens/version.rb +1 -1
- data/lib/rails_lens/view_metadata.rb +98 -0
- metadata +4 -1
@@ -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,25 +11,40 @@ 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 = []
|
23
|
+
|
24
|
+
# Get connection name
|
25
|
+
begin
|
26
|
+
connection_name = conn.pool.db_config.name
|
27
|
+
lines << "connection = \"#{connection_name}\""
|
28
|
+
rescue StandardError
|
29
|
+
lines << 'connection = "unknown"'
|
30
|
+
end
|
31
|
+
|
21
32
|
lines << "database_dialect = \"#{adapter_name}\""
|
22
33
|
|
23
|
-
# Add
|
34
|
+
# Add database version information
|
35
|
+
begin
|
36
|
+
db_version = conn.database_version
|
37
|
+
lines << "database_version = \"#{db_version}\""
|
38
|
+
rescue StandardError
|
39
|
+
lines << 'database_version = "unknown"'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add database name if available
|
24
43
|
begin
|
25
|
-
db_name =
|
26
|
-
|
27
|
-
rescue StandardError
|
28
|
-
'unknown'
|
29
|
-
end
|
30
|
-
lines << "database_version = \"#{db_name}\""
|
44
|
+
db_name = conn.current_database
|
45
|
+
lines << "database_name = \"#{db_name}\"" if db_name
|
31
46
|
rescue StandardError
|
32
|
-
# Skip if can't get
|
47
|
+
# Skip if can't get database name
|
33
48
|
end
|
34
49
|
|
35
50
|
lines << ''
|
@@ -38,8 +53,8 @@ module RailsLens
|
|
38
53
|
|
39
54
|
lines.join("\n")
|
40
55
|
else
|
41
|
-
# Add schema information for regular models
|
42
|
-
adapter = Connection.adapter_for(model_class)
|
56
|
+
# Add schema information for regular models (tables or views)
|
57
|
+
adapter = Connection.adapter_for(model_class, conn)
|
43
58
|
adapter.generate_annotation(model_class)
|
44
59
|
end
|
45
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
|
@@ -8,7 +8,15 @@ module RailsLens
|
|
8
8
|
'MySQL'
|
9
9
|
end
|
10
10
|
|
11
|
-
def generate_annotation(
|
11
|
+
def generate_annotation(model_class)
|
12
|
+
if model_class && ModelDetector.view_exists?(model_class)
|
13
|
+
generate_view_annotation(model_class)
|
14
|
+
else
|
15
|
+
generate_table_annotation(model_class)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_table_annotation(_model_class)
|
12
20
|
lines = []
|
13
21
|
lines << "table = \"#{table_name}\""
|
14
22
|
lines << "database_dialect = \"#{database_dialect}\""
|
@@ -37,6 +45,27 @@ module RailsLens
|
|
37
45
|
lines.join("\n")
|
38
46
|
end
|
39
47
|
|
48
|
+
def generate_view_annotation(model_class)
|
49
|
+
lines = []
|
50
|
+
lines << "view = \"#{table_name}\""
|
51
|
+
lines << "database_dialect = \"#{database_dialect}\""
|
52
|
+
|
53
|
+
# Fetch all view metadata in a single query
|
54
|
+
view_info = fetch_view_metadata
|
55
|
+
|
56
|
+
if view_info
|
57
|
+
lines << "view_type = \"#{view_info[:type]}\"" if view_info[:type]
|
58
|
+
lines << "updatable = #{view_info[:updatable]}"
|
59
|
+
end
|
60
|
+
|
61
|
+
lines << ''
|
62
|
+
|
63
|
+
add_columns_toml(lines)
|
64
|
+
add_view_dependencies_toml(lines, view_info)
|
65
|
+
|
66
|
+
lines.join("\n")
|
67
|
+
end
|
68
|
+
|
40
69
|
protected
|
41
70
|
|
42
71
|
def format_column(column)
|
@@ -117,7 +146,7 @@ module RailsLens
|
|
117
146
|
rescue ActiveRecord::StatementInvalid => e
|
118
147
|
Rails.logger.debug { "Failed to fetch storage engine for #{table_name}: #{e.message}" }
|
119
148
|
nil
|
120
|
-
rescue
|
149
|
+
rescue => e
|
121
150
|
Rails.logger.debug { "MySQL error fetching storage engine: #{e.message}" }
|
122
151
|
nil
|
123
152
|
end
|
@@ -137,7 +166,7 @@ module RailsLens
|
|
137
166
|
rescue ActiveRecord::StatementInvalid => e
|
138
167
|
Rails.logger.debug { "Failed to fetch charset for #{table_name}: #{e.message}" }
|
139
168
|
nil
|
140
|
-
rescue
|
169
|
+
rescue => e
|
141
170
|
Rails.logger.debug { "MySQL error fetching charset: #{e.message}" }
|
142
171
|
nil
|
143
172
|
end
|
@@ -155,7 +184,7 @@ module RailsLens
|
|
155
184
|
rescue ActiveRecord::StatementInvalid => e
|
156
185
|
Rails.logger.debug { "Failed to fetch collation for #{table_name}: #{e.message}" }
|
157
186
|
nil
|
158
|
-
rescue
|
187
|
+
rescue => e
|
159
188
|
Rails.logger.debug { "MySQL error fetching collation: #{e.message}" }
|
160
189
|
nil
|
161
190
|
end
|
@@ -202,7 +231,7 @@ module RailsLens
|
|
202
231
|
# Table doesn't exist or no permission to query information_schema
|
203
232
|
Rails.logger.debug { "Failed to check partitions for #{table_name}: #{e.message}" }
|
204
233
|
false
|
205
|
-
rescue
|
234
|
+
rescue => e
|
206
235
|
# MySQL specific errors (connection issues, etc)
|
207
236
|
Rails.logger.debug { "MySQL error checking partitions: #{e.message}" }
|
208
237
|
false
|
@@ -231,7 +260,7 @@ module RailsLens
|
|
231
260
|
rescue ActiveRecord::StatementInvalid => e
|
232
261
|
# Permission denied or table doesn't exist
|
233
262
|
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
234
|
-
rescue
|
263
|
+
rescue => e
|
235
264
|
# MySQL specific errors
|
236
265
|
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
237
266
|
end
|
@@ -269,10 +298,93 @@ module RailsLens
|
|
269
298
|
rescue ActiveRecord::StatementInvalid => e
|
270
299
|
# Permission denied or table doesn't exist
|
271
300
|
Rails.logger.debug { "Failed to fetch partitions for #{table_name}: #{e.message}" }
|
272
|
-
rescue
|
301
|
+
rescue => e
|
273
302
|
# MySQL specific errors
|
274
303
|
Rails.logger.debug { "MySQL error fetching partitions: #{e.message}" }
|
275
304
|
end
|
305
|
+
|
306
|
+
def add_view_dependencies_toml(lines, view_info)
|
307
|
+
return unless view_info && view_info[:dependencies]
|
308
|
+
|
309
|
+
dependencies = view_info[:dependencies]
|
310
|
+
return if dependencies.empty?
|
311
|
+
|
312
|
+
lines << ''
|
313
|
+
lines << "view_dependencies = [#{dependencies.map { |d| "\"#{d}\"" }.join(', ')}]"
|
314
|
+
end
|
315
|
+
|
316
|
+
# MySQL-specific view methods
|
317
|
+
public
|
318
|
+
|
319
|
+
# Fetch all view metadata in a single consolidated query
|
320
|
+
def fetch_view_metadata
|
321
|
+
result = connection.exec_query(<<~SQL.squish, 'MySQL View Metadata')
|
322
|
+
SELECT#{' '}
|
323
|
+
v.is_updatable,
|
324
|
+
COALESCE(
|
325
|
+
(
|
326
|
+
SELECT GROUP_CONCAT(DISTINCT vtu.table_name ORDER BY vtu.table_name)
|
327
|
+
FROM information_schema.view_table_usage vtu
|
328
|
+
WHERE vtu.view_schema = DATABASE()
|
329
|
+
AND vtu.view_name = '#{connection.quote_string(table_name)}'
|
330
|
+
),
|
331
|
+
''
|
332
|
+
) as dependencies
|
333
|
+
FROM information_schema.views v
|
334
|
+
WHERE v.table_schema = DATABASE()
|
335
|
+
AND v.table_name = '#{connection.quote_string(table_name)}'
|
336
|
+
LIMIT 1
|
337
|
+
SQL
|
338
|
+
|
339
|
+
return nil if result.rows.empty?
|
340
|
+
|
341
|
+
row = result.rows.first
|
342
|
+
{
|
343
|
+
type: 'regular', # MySQL only supports regular views
|
344
|
+
updatable: row[0] == 'YES',
|
345
|
+
dependencies: row[1].to_s.split(',').reject(&:empty?)
|
346
|
+
}
|
347
|
+
rescue ActiveRecord::StatementInvalid, Mysql2::Error => e
|
348
|
+
Rails.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
|
349
|
+
nil
|
350
|
+
end
|
351
|
+
|
352
|
+
# Legacy methods - kept for backward compatibility but now use consolidated query
|
353
|
+
def view_type
|
354
|
+
@view_metadata ||= fetch_view_metadata
|
355
|
+
@view_metadata&.dig(:type)
|
356
|
+
end
|
357
|
+
|
358
|
+
def view_updatable?
|
359
|
+
@view_metadata ||= fetch_view_metadata
|
360
|
+
@view_metadata&.dig(:updatable) || false
|
361
|
+
end
|
362
|
+
|
363
|
+
def view_dependencies
|
364
|
+
@view_metadata ||= fetch_view_metadata
|
365
|
+
@view_metadata&.dig(:dependencies) || []
|
366
|
+
end
|
367
|
+
|
368
|
+
def view_definition
|
369
|
+
result = connection.exec_query(<<~SQL.squish, 'MySQL View Definition')
|
370
|
+
SELECT view_definition FROM information_schema.views
|
371
|
+
WHERE table_schema = DATABASE()
|
372
|
+
AND table_name = '#{connection.quote_string(table_name)}'
|
373
|
+
LIMIT 1
|
374
|
+
SQL
|
375
|
+
|
376
|
+
result.rows.first&.first&.strip
|
377
|
+
rescue ActiveRecord::StatementInvalid, Mysql2::Error
|
378
|
+
nil
|
379
|
+
end
|
380
|
+
|
381
|
+
def view_refresh_strategy
|
382
|
+
nil # MySQL doesn't have materialized views
|
383
|
+
end
|
384
|
+
|
385
|
+
def view_last_refreshed
|
386
|
+
nil # MySQL doesn't have materialized views
|
387
|
+
end
|
276
388
|
end
|
277
389
|
end
|
278
390
|
end
|
@@ -8,7 +8,15 @@ module RailsLens
|
|
8
8
|
'PostgreSQL'
|
9
9
|
end
|
10
10
|
|
11
|
-
def generate_annotation(
|
11
|
+
def generate_annotation(model_class)
|
12
|
+
if model_class && ModelDetector.view_exists?(model_class)
|
13
|
+
generate_view_annotation(model_class)
|
14
|
+
else
|
15
|
+
generate_table_annotation(model_class)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_table_annotation(_model_class)
|
12
20
|
lines = []
|
13
21
|
lines << "table = \"#{table_name}\""
|
14
22
|
lines << "database_dialect = \"#{database_dialect}\""
|
@@ -26,6 +34,35 @@ module RailsLens
|
|
26
34
|
lines.join("\n")
|
27
35
|
end
|
28
36
|
|
37
|
+
def generate_view_annotation(model_class)
|
38
|
+
lines = []
|
39
|
+
lines << "view = \"#{table_name}\""
|
40
|
+
lines << "database_dialect = \"#{database_dialect}\""
|
41
|
+
|
42
|
+
# Add schema information for PostgreSQL
|
43
|
+
lines << "schema = \"#{schema_name}\"" if schema_name && schema_name != 'public'
|
44
|
+
|
45
|
+
# Fetch all view metadata in a single query
|
46
|
+
view_info = fetch_view_metadata
|
47
|
+
|
48
|
+
if view_info
|
49
|
+
lines << "view_type = \"#{view_info[:type]}\"" if view_info[:type]
|
50
|
+
lines << "updatable = #{view_info[:updatable]}"
|
51
|
+
|
52
|
+
if view_info[:type] == 'materialized'
|
53
|
+
lines << 'materialized = true'
|
54
|
+
lines << 'refresh_strategy = "manual"'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
lines << ''
|
59
|
+
|
60
|
+
add_columns_toml(lines)
|
61
|
+
add_view_dependencies_toml(lines, view_info)
|
62
|
+
|
63
|
+
lines.join("\n")
|
64
|
+
end
|
65
|
+
|
29
66
|
protected
|
30
67
|
|
31
68
|
def schema_name
|
@@ -191,6 +228,131 @@ module RailsLens
|
|
191
228
|
lines << ''
|
192
229
|
lines << "table_comment = \"#{comment.gsub('"', '\"')}\""
|
193
230
|
end
|
231
|
+
|
232
|
+
def add_view_dependencies_toml(lines, view_info)
|
233
|
+
return unless view_info && view_info[:dependencies]
|
234
|
+
|
235
|
+
dependencies = view_info[:dependencies]
|
236
|
+
return if dependencies.empty?
|
237
|
+
|
238
|
+
lines << ''
|
239
|
+
lines << "view_dependencies = [#{dependencies.map { |d| "\"#{d}\"" }.join(', ')}]"
|
240
|
+
end
|
241
|
+
|
242
|
+
# PostgreSQL-specific view methods
|
243
|
+
public
|
244
|
+
|
245
|
+
# Fetch all view metadata in a single consolidated query
|
246
|
+
def fetch_view_metadata
|
247
|
+
result = connection.exec_query(<<~SQL.squish, 'PostgreSQL View Metadata')
|
248
|
+
WITH view_info AS (
|
249
|
+
-- Check for materialized view
|
250
|
+
SELECT#{' '}
|
251
|
+
'materialized' as view_type,
|
252
|
+
false as is_updatable,
|
253
|
+
mv.matviewname as view_name
|
254
|
+
FROM pg_matviews mv
|
255
|
+
WHERE mv.matviewname = '#{connection.quote_string(table_name)}'
|
256
|
+
#{' '}
|
257
|
+
UNION ALL
|
258
|
+
#{' '}
|
259
|
+
-- Check for regular view
|
260
|
+
SELECT#{' '}
|
261
|
+
'regular' as view_type,
|
262
|
+
CASE WHEN v.is_updatable = 'YES' THEN true ELSE false END as is_updatable,
|
263
|
+
v.table_name as view_name
|
264
|
+
FROM information_schema.views v
|
265
|
+
WHERE v.table_name = '#{connection.quote_string(table_name)}'
|
266
|
+
),
|
267
|
+
dependencies AS (
|
268
|
+
SELECT DISTINCT c2.relname as dependency_name
|
269
|
+
FROM pg_class c1
|
270
|
+
JOIN pg_depend d ON c1.oid = d.objid
|
271
|
+
JOIN pg_class c2 ON d.refobjid = c2.oid
|
272
|
+
WHERE c1.relname = '#{connection.quote_string(table_name)}'
|
273
|
+
AND c1.relkind IN ('v', 'm')
|
274
|
+
AND c2.relkind IN ('r', 'v', 'm')
|
275
|
+
AND d.deptype = 'n'
|
276
|
+
)
|
277
|
+
SELECT#{' '}
|
278
|
+
vi.view_type,
|
279
|
+
vi.is_updatable,
|
280
|
+
COALESCE(
|
281
|
+
(SELECT array_agg(dependency_name ORDER BY dependency_name) FROM dependencies),
|
282
|
+
ARRAY[]::text[]
|
283
|
+
) as dependencies
|
284
|
+
FROM view_info vi
|
285
|
+
LIMIT 1
|
286
|
+
SQL
|
287
|
+
|
288
|
+
return nil if result.rows.empty?
|
289
|
+
|
290
|
+
row = result.rows.first
|
291
|
+
{
|
292
|
+
type: row[0],
|
293
|
+
updatable: ['t', true].include?(row[1]),
|
294
|
+
dependencies: row[2] || []
|
295
|
+
}
|
296
|
+
rescue ActiveRecord::StatementInvalid, PG::Error => e
|
297
|
+
Rails.logger.debug { "Failed to fetch view metadata for #{table_name}: #{e.message}" }
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
|
301
|
+
# Legacy methods - kept for backward compatibility but now use consolidated query
|
302
|
+
def view_type
|
303
|
+
@view_metadata ||= fetch_view_metadata
|
304
|
+
@view_metadata&.dig(:type)
|
305
|
+
end
|
306
|
+
|
307
|
+
def view_updatable?
|
308
|
+
@view_metadata ||= fetch_view_metadata
|
309
|
+
@view_metadata&.dig(:updatable) || false
|
310
|
+
end
|
311
|
+
|
312
|
+
def view_dependencies
|
313
|
+
@view_metadata ||= fetch_view_metadata
|
314
|
+
@view_metadata&.dig(:dependencies) || []
|
315
|
+
end
|
316
|
+
|
317
|
+
def view_definition
|
318
|
+
result = if view_type == 'materialized'
|
319
|
+
connection.exec_query(<<~SQL.squish, 'PostgreSQL Materialized View Definition')
|
320
|
+
SELECT definition FROM pg_matviews
|
321
|
+
WHERE matviewname = '#{connection.quote_string(table_name)}'
|
322
|
+
LIMIT 1
|
323
|
+
SQL
|
324
|
+
else
|
325
|
+
connection.exec_query(<<~SQL.squish, 'PostgreSQL View Definition')
|
326
|
+
SELECT view_definition FROM information_schema.views
|
327
|
+
WHERE table_name = '#{connection.quote_string(table_name)}'
|
328
|
+
LIMIT 1
|
329
|
+
SQL
|
330
|
+
end
|
331
|
+
|
332
|
+
result.rows.first&.first&.strip
|
333
|
+
rescue ActiveRecord::StatementInvalid, PG::Error
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
def view_refresh_strategy
|
338
|
+
view_type == 'materialized' ? 'manual' : nil
|
339
|
+
end
|
340
|
+
|
341
|
+
def view_last_refreshed
|
342
|
+
return nil unless view_type == 'materialized'
|
343
|
+
|
344
|
+
# Get the last refresh time from pg_stat_user_tables
|
345
|
+
result = connection.exec_query(<<~SQL.squish, 'PostgreSQL Materialized View Last Refresh')
|
346
|
+
SELECT COALESCE(last_vacuum, last_autovacuum) as last_refreshed
|
347
|
+
FROM pg_stat_user_tables
|
348
|
+
WHERE relname = '#{connection.quote_string(table_name)}'
|
349
|
+
LIMIT 1
|
350
|
+
SQL
|
351
|
+
|
352
|
+
result.rows.first&.first
|
353
|
+
rescue ActiveRecord::StatementInvalid, PG::Error
|
354
|
+
nil
|
355
|
+
end
|
194
356
|
end
|
195
357
|
end
|
196
358
|
end
|