aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorGreg Navis <contact@gregnavis.com>2017-05-13 03:09:58 +0200
committerGreg Navis <contact@gregnavis.com>2017-11-30 10:46:38 +0100
commit1dca75c2c8f930b58d86cd2216af5e14307b3e53 (patch)
tree2c124dd35e87e3fc27448751e9d4001dd7e90fa5 /activerecord
parent1bee2fb600c07625b830afd33b43ead3364c9715 (diff)
downloadrails-1dca75c2c8f930b58d86cd2216af5e14307b3e53.tar.gz
rails-1dca75c2c8f930b58d86cd2216af5e14307b3e53.tar.bz2
rails-1dca75c2c8f930b58d86cd2216af5e14307b3e53.zip
Add support for PostgreSQL operator classes to add_index
Add support for specifying non-default operator classes in PostgreSQL indexes. An example CREATE INDEX query that becomes possible is: CREATE INDEX users_name ON users USING gist (name gist_trgm_ops); Previously it was possible to specify the `gist` index but not the custom operator class. The `add_index` call for the above query is: add_index :users, :name, using: :gist, opclasses: {name: :gist_trgm_ops}
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb32
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb17
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb32
9 files changed, 115 insertions, 20 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 60ceffac5e..048a1eeb2f 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Add support for PostgreSQL operator classes to `add_index`.
+
+ Example:
+
+ add_index :users, :name, using: :gist, opclass: name: :gist_trgm_ops
+
+ *Greg Navis*
+
* Don't allow scopes to be defined which conflict with instance methods on `Relation`.
Fixes #31120.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index be2f625d74..b66009bdc6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
class IndexDefinition # :nodoc:
- attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment
+ attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclass, :comment
def initialize(
table, name,
@@ -17,6 +17,7 @@ module ActiveRecord
where: nil,
type: nil,
using: nil,
+ opclass: {},
comment: nil
)
@table = table
@@ -28,6 +29,7 @@ module ActiveRecord
@where = where
@type = type
@using = using
+ @opclass = opclass
@comment = comment
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 9b7345f7c3..2a335e8f2b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -738,6 +738,29 @@ module ActiveRecord
#
# Note: only supported by PostgreSQL and MySQL
#
+ # ====== Creating an index with a specific operator class
+ #
+ # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops)
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops })
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops })
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
+ #
+ #
+ # Note: only supported by PostgreSQL
+ #
# ====== Creating an index with a specific type
#
# add_index(:developers, :name, type: :fulltext)
@@ -1120,7 +1143,7 @@ module ActiveRecord
def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
column_names = index_column_names(column_name)
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
@@ -1186,7 +1209,8 @@ module ActiveRecord
quoted_columns
end
- # Overridden by the MySQL adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths and by
+ # the PostgreSQL adapter for supporting operator classes.
def add_options_for_index_columns(quoted_columns, **options)
if supports_index_sort_order?
quoted_columns = add_index_sort_order(quoted_columns, options)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 846e721983..15a1dfd102 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -87,10 +87,7 @@ module ActiveRecord
result = query(<<-SQL, "SCHEMA")
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment,
- (SELECT COUNT(*) FROM pg_opclass o
- JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c
- ON o.oid = c.oid WHERE o.opcdefault = 'f')
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -109,11 +106,10 @@ module ActiveRecord
inddef = row[3]
oid = row[4]
comment = row[5]
- opclass = row[6]
using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten
- if indkey.include?(0) || opclass > 0
+ if indkey.include?(0)
columns = expressions
else
columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
@@ -123,10 +119,21 @@ module ActiveRecord
AND a.attnum IN (#{indkey.join(",")})
SQL
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- orders = Hash[
- expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
- ]
+ orders = {}
+ opclasses = {}
+
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
+ # and non-default opclasses
+ expressions.scan(/(\w+)(?: (?!DESC)(\w+))?(?: (DESC))?/).each do |column, opclass, desc|
+ opclasses[column] = opclass if opclass
+ orders[column] = :desc if desc
+ end
+
+ # Use a string for the opclass description (instead of a hash) when all columns
+ # have the same opclass specified.
+ if columns.count == opclasses.count && opclasses.values.uniq.count == 1
+ opclasses = opclasses.values.first
+ end
end
IndexDefinition.new(
@@ -137,6 +144,7 @@ module ActiveRecord
orders: orders,
where: where,
using: using.to_sym,
+ opclass: opclasses,
comment: comment.presence
)
end
@@ -458,8 +466,8 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = 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})#{index_options}").tap do
+ index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = 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}").tap do
execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 7b27f6b7a0..eee61780b7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -388,6 +388,23 @@ module ActiveRecord
private
+ def add_index_opclass(column_names, options = {})
+ opclass =
+ if options[:opclass].is_a?(Hash)
+ options[:opclass].symbolize_keys
+ else
+ Hash.new { |hash, column| hash[column] = options[:opclass].to_s }
+ end
+ column_names.each do |name, column|
+ column << " #{opclass[name]}" if opclass[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_opclass(quoted_columns, options)
+ super
+ end
+
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 66f7d29886..8cb2851557 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -189,6 +189,7 @@ HEADER
index_parts << "where: #{index.where.inspect}" if index.where
index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
index_parts << "type: #{index.type.inspect}" if index.type
+ index_parts << "opclass: #{index.opclass.inspect}" if index.opclass.present?
index_parts << "comment: #{index.comment.inspect}" if index.comment
index_parts
end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 9929237546..99c53dadeb 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -59,6 +59,9 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true)
end
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops))
+ assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops })
+
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index f199519d86..1951230c8a 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -248,12 +248,12 @@ module ActiveRecord
def test_index_with_opclass
with_example_table do
- @connection.add_index "ex", "data varchar_pattern_ops"
- index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
- assert_equal "data varchar_pattern_ops", index.columns
+ @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+ index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+ assert_equal ["data"], index.columns
- @connection.remove_index "ex", "data varchar_pattern_ops"
- assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
+ @connection.remove_index "ex", "data"
+ assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 5a64da028b..a6ae18a826 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -500,6 +500,38 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
end
end
+class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_string_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: "text_pattern_ops"/, output)
+ end
+
+ def test_non_default_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: \{"description"=>"text_pattern_ops"\}/, output)
+ end
+end
+
class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection