Skip to content

Commit

Permalink
Support functions on multiple columns
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaudanza committed Jan 16, 2014
1 parent 688626f commit 180dac4
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :function) #:nodoc:
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :functions) #:nodoc:
end

# Abstract representation of a column definition. Instances of this type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,11 +763,19 @@ def quoted_columns_for_index(column_names, options = {})
option_strings = add_index_sort_order(option_strings, column_names, options)
end

column_names.map do |name|
if options[:functions]
functions = Array(options[:functions])
if functions.length != column_names.length
raise(ArgumentError,
"The number of functions must match the number of column names")
end
end

column_names.each_with_index.map do |name, index|
quoted = quote_column_name(name)

if supports_index_functions? && options[:function]
quoted = "#{options[:function]}(#{quoted})"
if supports_index_functions? && functions
quoted = "#{functions[index]}(#{quoted})"
end

quoted + option_strings[name]
Expand All @@ -782,7 +790,7 @@ def add_index_options(table_name, column_name, options = {})
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, :function)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :functions)

index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,29 +153,57 @@ def indexes(table_name, name = nil)
oid = row[6]
using = row[7].to_sym

# Matches "lower((column)::text)" or "lower(column)"
if indkey == ['0'] && (expression =~ /\A(\w+)\(\((\w+)\)::[\w\s]+\)\Z/ ||
expression =~ /\A(\w+)\((\w+)\)\Z/)
function = $~[1]
column_names = [$~[2]]
else
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
function = nil
index_supported = true

function_names = []
function_columns = []

if expression
expression.split(',').each do |part|
part.strip!

# Matches "lower((column)::text)" or "lower(column)"
if part =~ /\A(\w+)\(\((\w+)\)::[\w\s]+\)\Z/ ||
part =~ /\A(\w+)\((\w+)\)\Z/

function_names << $~[1]
function_columns << $~[2]
else
# Complex expression indexes aren't supported
index_supported = false
break
end
end
end

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 = []
functions = []

indkey.each do |i|
if i == '0'
functions << function_names.shift
column_names << function_columns.shift
else
functions << nil
column_names << columns[i]
end
end

unless column_names.empty?
functions = nil if functions.compact.empty?

if index_supported && !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]}] : {}

IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, function)
IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, functions)
end
end.compact
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/schema_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def indexes(table, stream)
index_orders = (index.orders || {})
statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?

%w(where using type function).each do |attr|
%w(where using type functions).each do |attr|
value = index.public_send(attr)
statement_parts << ("#{attr}: " + value.inspect) if value
end
Expand Down
16 changes: 13 additions & 3 deletions activerecord/test/cases/adapters/postgresql/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ class SchemaTest < ActiveRecord::TestCase
INDEX_D_NAME = 'd_index_things_on_description_desc'
INDEX_E_NAME = 'e_index_things_on_name_vector'
INDEX_F_NAME = 'f_index_lower_name_on_things'
INDEX_G_NAME = 'g_index_composite_index_on_things'
INDEX_A_COLUMN = 'name'
INDEX_B_COLUMN_S1 = 'email'
INDEX_B_COLUMN_S2 = 'moment'
INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))}
INDEX_D_COLUMN = 'description'
INDEX_E_COLUMN = 'name_vector'
INDEX_F_COLUMN = 'LOWER(name)'
INDEX_G_COLUMN = 'UPPER(name),active,LOWER(email)'
COLUMNS = [
'id integer',
'name character varying(50)',
Expand Down Expand Up @@ -71,6 +73,8 @@ def setup
@connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_F_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_F_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_F_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_F_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_G_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_G_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_G_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_G_COLUMN});"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
@connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))"
Expand Down Expand Up @@ -357,17 +361,22 @@ def with_schema_search_path(schema_search_path)
def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name}
assert_equal 5,indexes.size
assert_equal 6,indexes.size

do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name)
do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name)
do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name)
do_dump_index_assertions_for_one_index(indexes[4], INDEX_F_NAME, 'name')

assert_equal 'active', indexes[0].where

assert_equal 'lower', indexes[4].function
assert_equal INDEX_F_NAME, indexes[4].name
assert_equal ['name'], indexes[4].columns
assert_equal ['lower'], indexes[4].functions

assert_equal INDEX_G_NAME, indexes[5].name
assert_equal ['name', 'active', 'email'], indexes[5].columns
assert_equal ['upper', nil, 'lower'], indexes[5].functions

indexes.select{|i| i.name != INDEX_E_NAME}.each do |index|
assert_equal :btree, index.using
Expand All @@ -382,5 +391,6 @@ def do_dump_index_assertions_for_one_index(this_index, this_index_name, this_ind
assert_equal 1, this_index.columns.size
assert_equal this_index_column, this_index.columns[0]
assert_equal this_index_name, this_index.name
assert_nil this_index.functions
end
end
18 changes: 17 additions & 1 deletion activerecord/test/cases/migration/index_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,25 @@ def test_add_partial_index
end

def test_index_functions
connection.add_index("testings", "last_name", :function => "lower")
connection.add_index("testings", "last_name", :functions => "lower")
assert connection.index_exists?("testings", "last_name")

connection.add_index("testings",
["first_name", "last_name", "administrator"],
name: 'test_composite_index',
functions: ["lower", "upper", nil])

index = connection.indexes("testings").find do |i|
i.columns == ["first_name", "last_name", "administrator"]
end
assert_not_nil index
assert_equal ["lower", "upper", nil], index.functions

assert_raise ArgumentError do
# pass in an incorrect number of functions
connection.add_index :testings, :foo, unique: true, :functions => ["lower", "upper"]
end

connection.remove_index("testings", "last_name")
assert !connection.index_exists?("testings", "last_name")
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/schema_dumper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def test_schema_dumps_partial_indices
def test_schema_dumps_index_functions
index_definition = standard_dump.split(/\n/).grep(/add_index.*company_lower_name_index/).first.strip
if current_adapter?(:PostgreSQLAdapter)
assert_equal 'add_index "companies", ["name"], name: "company_lower_name_index", using: :btree, function: "lower"', index_definition
assert_equal 'add_index "companies", ["name"], name: "company_lower_name_index", using: :btree, functions: ["lower"]', index_definition
elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
assert_equal 'add_index "companies", ["name"], name: "company_lower_name_index", using: :btree', index_definition
else
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/schema/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def create_table(*args, &block)
add_index :companies, [:firm_id, :type, :rating], name: "company_index"
add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
add_index :companies, :name, name: 'company_name_index', using: :btree
add_index :companies, :name, name: 'company_lower_name_index', using: :btree, function: 'lower'
add_index :companies, :name, name: 'company_lower_name_index', using: :btree, functions: 'lower'

create_table :vegetables, force: true do |t|
t.string :name
Expand Down

0 comments on commit 180dac4

Please sign in to comment.