Add system check for missing database indexes (#30888)
This commit is contained in:
		
					parent
					
						
							
								1fc14e324b
							
						
					
				
			
			
				commit
				
					
						ebd8e1bbb6
					
				
			
		
					 8 changed files with 248 additions and 0 deletions
				
			
		
							
								
								
									
										92
									
								
								app/lib/admin/db/schema_parser.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/lib/admin/db/schema_parser.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Admin::Db::SchemaParser | ||||
|   class Index | ||||
|     attr_reader :name, :table_name, :columns, :options | ||||
| 
 | ||||
|     def initialize(name:, table_name:, columns:, options:) | ||||
|       @name = name | ||||
|       @table_name = table_name | ||||
|       @columns = columns | ||||
|       @options = options | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   attr_reader :indexes_by_table | ||||
| 
 | ||||
|   def initialize(source) | ||||
|     parse(source) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def parse(source) | ||||
|     @indexes_by_table = {} | ||||
|     queue = [Prism.parse(source).value] | ||||
|     while (node = queue.shift) | ||||
|       if node.type == :call_node && node.name == :create_table | ||||
|         parse_create_table(node) | ||||
|       elsif node.type == :call_node && node.name == :add_index | ||||
|         parse_add_index(node) | ||||
|       else | ||||
|         queue.concat(node.compact_child_nodes) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def parse_create_table(node) | ||||
|     table_name = parse_arguments(node).first | ||||
|     queue = node.compact_child_nodes | ||||
|     while (node = queue.shift) | ||||
|       if node.type == :call_node && node.name == :index | ||||
|         parse_index(node, table_name:) | ||||
|       else | ||||
|         queue.concat(node.compact_child_nodes) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def parse_index(node, table_name:) | ||||
|     arguments = parse_arguments(node) | ||||
|     save_index( | ||||
|       name: arguments.last[:name], | ||||
|       table_name: table_name, | ||||
|       columns: arguments.first, | ||||
|       options: arguments.last | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def parse_add_index(node) | ||||
|     arguments = parse_arguments(node) | ||||
|     save_index( | ||||
|       name: arguments.last[:name], | ||||
|       table_name: arguments.first, | ||||
|       columns: arguments[1], | ||||
|       options: arguments.last | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def parse_arguments(node) | ||||
|     node.arguments.arguments.map { |a| parse_argument(a) } | ||||
|   end | ||||
| 
 | ||||
|   def parse_argument(argument) | ||||
|     case argument | ||||
|     when Prism::StringNode | ||||
|       argument.unescaped | ||||
|     when Prism::SymbolNode | ||||
|       argument.unescaped.to_sym | ||||
|     when Prism::ArrayNode | ||||
|       argument.elements.map { |e| parse_argument(e) } | ||||
|     when Prism::KeywordHashNode | ||||
|       argument.elements.to_h do |element| | ||||
|         [element.key.unescaped.to_sym, parse_argument(element.value)] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def save_index(name:, table_name:, columns:, options:) | ||||
|     @indexes_by_table[table_name] ||= [] | ||||
|     @indexes_by_table[table_name] << Index.new(name:, table_name:, columns:, options:) | ||||
|   end | ||||
| end | ||||
|  | @ -8,6 +8,7 @@ class Admin::SystemCheck | |||
|     Admin::SystemCheck::SidekiqProcessCheck, | ||||
|     Admin::SystemCheck::RulesCheck, | ||||
|     Admin::SystemCheck::ElasticsearchCheck, | ||||
|     Admin::SystemCheck::MissingIndexesCheck, | ||||
|   ].freeze | ||||
| 
 | ||||
|   def self.perform(current_user) | ||||
|  |  | |||
							
								
								
									
										36
									
								
								app/lib/admin/system_check/missing_indexes_check.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/lib/admin/system_check/missing_indexes_check.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Admin::SystemCheck::MissingIndexesCheck < Admin::SystemCheck::BaseCheck | ||||
|   def skip? | ||||
|     !current_user.can?(:view_devops) | ||||
|   end | ||||
| 
 | ||||
|   def pass? | ||||
|     missing_indexes.none? | ||||
|   end | ||||
| 
 | ||||
|   def message | ||||
|     Admin::SystemCheck::Message.new(:missing_indexes_check, missing_indexes.join(', ')) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def missing_indexes | ||||
|     @missing_indexes ||= begin | ||||
|       expected_indexes_by_table.flat_map do |table, indexes| | ||||
|         expected_indexes = indexes.map(&:name) | ||||
|         expected_indexes - existing_indexes_for(table) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def expected_indexes_by_table | ||||
|     schema_rb = Rails.root.join('db', 'schema.rb').read | ||||
|     schema_parser = Admin::Db::SchemaParser.new(schema_rb) | ||||
|     schema_parser.indexes_by_table | ||||
|   end | ||||
| 
 | ||||
|   def existing_indexes_for(table) | ||||
|     ActiveRecord::Base.connection.indexes(table).map(&:name) | ||||
|   end | ||||
| end | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue