a39cddf7b7
ActiveRecord supports `:order` natively, but it is skipped because of our specific patch to support opclasses. Relevant AR code: https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L1167
211 lines
8.6 KiB
Ruby
211 lines
8.6 KiB
Ruby
# rubocop:disable all
|
|
|
|
# These changes add support for PostgreSQL operator classes when creating
|
|
# indexes and dumping/loading schemas. Taken from Rails pull request
|
|
# https://github.com/rails/rails/pull/19090.
|
|
#
|
|
# License:
|
|
#
|
|
# Copyright (c) 2004-2016 David Heinemeier Hansson
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
require 'date'
|
|
require 'set'
|
|
require 'bigdecimal'
|
|
require 'bigdecimal/util'
|
|
|
|
# As the Struct definition is changed in this PR/patch we have to first remove
|
|
# the existing one.
|
|
ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition)
|
|
|
|
module ActiveRecord
|
|
module ConnectionAdapters #:nodoc:
|
|
# Abstract representation of an index definition on a table. Instances of
|
|
# this type are typically created and returned by methods in database
|
|
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
|
|
attrs = [:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment, :opclasses]
|
|
|
|
class IndexDefinition < Struct.new(*attrs) #:nodoc:
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
module ActiveRecord
|
|
module ConnectionAdapters # :nodoc:
|
|
module SchemaStatements
|
|
def add_index_options(table_name, column_name, options = {}) #:nodoc:
|
|
column_names = Array(column_name)
|
|
index_name = index_name(table_name, column: column_names)
|
|
|
|
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses)
|
|
|
|
index_type = options[:unique] ? "UNIQUE" : ""
|
|
index_type = options[:type].to_s if options.key?(:type)
|
|
index_name = options[:name].to_s if options.key?(:name)
|
|
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
|
|
|
|
if options.key?(:algorithm)
|
|
algorithm = index_algorithms.fetch(options[:algorithm]) {
|
|
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
|
|
}
|
|
end
|
|
|
|
using = "USING #{options[:using]}" if options[:using].present?
|
|
|
|
if supports_partial_index?
|
|
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
|
|
end
|
|
|
|
if index_name.length > max_index_length
|
|
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
|
|
end
|
|
if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false)
|
|
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
|
|
end
|
|
index_columns = quoted_columns_for_index(column_names, options).join(", ")
|
|
|
|
[index_name, index_type, index_columns, index_options, algorithm, using]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
module ActiveRecord
|
|
module ConnectionAdapters
|
|
module PostgreSQL
|
|
module SchemaStatements
|
|
# Returns an array of indexes for the given table.
|
|
def indexes(table_name, name = nil)
|
|
result = query(<<-SQL, 'SCHEMA')
|
|
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
|
|
FROM pg_class t
|
|
INNER JOIN pg_index d ON t.oid = d.indrelid
|
|
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
|
WHERE i.relkind = 'i'
|
|
AND d.indisprimary = 'f'
|
|
AND t.relname = '#{table_name}'
|
|
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
|
ORDER BY i.relname
|
|
SQL
|
|
|
|
result.map do |row|
|
|
index_name = row[0]
|
|
unique = row[1]
|
|
indkey = row[2].split(" ").map(&:to_i)
|
|
inddef = row[3]
|
|
oid = row[4]
|
|
|
|
columns = Hash[query(<<-SQL, "SCHEMA")]
|
|
SELECT a.attnum, a.attname
|
|
FROM pg_attribute a
|
|
WHERE a.attrelid = #{oid}
|
|
AND a.attnum IN (#{indkey.join(",")})
|
|
SQL
|
|
|
|
column_names = columns.values_at(*indkey).compact
|
|
|
|
unless column_names.empty?
|
|
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
|
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
|
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
|
|
where = inddef.scan(/WHERE (.+)$/).flatten[0]
|
|
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
|
|
opclasses = Hash[inddef.scan(/\((.+?)\)(?:$| WHERE )/).flatten[0].split(',').map do |column_and_opclass|
|
|
column, opclass = column_and_opclass.split(' ').map(&:strip)
|
|
end.reject do |column, opclass|
|
|
['desc', 'asc'].include?(opclass&.downcase)
|
|
end.map do |column, opclass|
|
|
[column, opclass] if opclass
|
|
end.compact]
|
|
|
|
index_attrs = [table_name, index_name, unique, column_names, [], orders, where, nil, using, nil, opclasses]
|
|
|
|
IndexDefinition.new(*index_attrs)
|
|
end
|
|
end.compact
|
|
end
|
|
|
|
def add_index(table_name, column_name, options = {}) #:nodoc:
|
|
index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
|
|
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}"
|
|
end
|
|
|
|
protected
|
|
|
|
def quoted_columns_for_index(column_names, options = {})
|
|
column_opclasses = options[:opclasses] || {}
|
|
column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"}
|
|
|
|
quoted_columns = Hash[column_names.map { |name| [name.to_sym, "#{quote_column_name(name)} #{column_opclasses[name]}"] }]
|
|
add_options_for_index_columns(quoted_columns, options).values
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
module ActiveRecord
|
|
class SchemaDumper
|
|
private
|
|
|
|
def indexes(table, stream)
|
|
if (indexes = @connection.indexes(table)).any?
|
|
add_index_statements = indexes.map do |index|
|
|
table_name = remove_prefix_and_suffix(index.table).inspect
|
|
" add_index #{([table_name]+index_parts(index)).join(', ')}"
|
|
end
|
|
|
|
stream.puts add_index_statements.sort.join("\n")
|
|
stream.puts
|
|
end
|
|
end
|
|
|
|
def indexes_in_create(table, stream)
|
|
if (indexes = @connection.indexes(table)).any?
|
|
index_statements = indexes.map do |index|
|
|
" t.index #{index_parts(index).join(', ')}"
|
|
end
|
|
stream.puts index_statements.sort.join("\n")
|
|
end
|
|
end
|
|
|
|
def index_parts(index)
|
|
index_parts = [
|
|
index.columns.inspect,
|
|
"name: #{index.name.inspect}",
|
|
]
|
|
index_parts << "unique: true" if index.unique
|
|
index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present?
|
|
index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present?
|
|
index_parts << "where: #{index.where.inspect}" if index.where
|
|
index_parts << "using: #{index.using.inspect}" if index.using
|
|
index_parts << "type: #{index.type.inspect}" if index.type
|
|
index_parts << "opclasses: #{index.opclasses.inspect}" if index.opclasses.present?
|
|
index_parts << "comment: #{index.comment.inspect}" if index.comment
|
|
index_parts
|
|
end
|
|
|
|
def format_options(options)
|
|
options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
|
|
end
|
|
end
|
|
end
|