From d70e0236df61d69c9299fe63df94da35c87ee2d8 Mon Sep 17 00:00:00 2001 From: Marcelo Silveira Date: Thu, 9 Feb 2012 01:20:52 -0200 Subject: [PATCH 1/3] Added where option to add_index to support postgresql partial indices The `add_index` method now supports a `where` option that receives a string with the partial index criteria. add_index(:accounts, :code, :where => "active") Generates CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active --- activerecord/CHANGELOG.md | 13 +++++++++++++ .../abstract/schema_statements.rb | 16 +++++++++++++--- .../connection_adapters/abstract_adapter.rb | 5 +++++ .../connection_adapters/postgresql_adapter.rb | 4 ++++ .../adapters/postgresql/active_schema_test.rb | 12 ++++++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 69cf1193b6363..3de5af22c5d3e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* Added support for partial indices to PostgreSQL adapter + + The `add_index` method now supports a `where` option that receives a + string with the partial index criteria. + + add_index(:accounts, :code, :where => "active") + + Generates + + CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active + + *Marcelo Silveira* + * Implemented ActiveRecord::Relation#none method The `none` method returns a chainable relation with zero records 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 1f9321edb65b8..ea6071ea46e91 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -381,9 +381,16 @@ def rename_column(table_name, column_name, new_column_name) # # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) # + # ====== Creating a partial index + # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active") + # generates + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # + # Note: only supported by PostgreSQL + # def add_index(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" + index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end # Remove the given index from the table. @@ -581,6 +588,9 @@ def add_index_options(table_name, column_name, options = {}) if Hash === options # legacy support, since this param was a string index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end else index_type = options end @@ -593,7 +603,7 @@ def add_index_options(table_name, column_name, options = {}) end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns] + [index_name, index_type, index_columns, index_options] end def index_name_for_remove(table_name, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index edea414db7deb..dd421b2054cb8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -142,6 +142,11 @@ def supports_index_sort_order? false end + # Does this adapter support partial indices? + def supports_partial_index? + false + end + # Does this adapter support explain? As of this writing sqlite3, # mysql2, and postgresql are the only ones that do. def supports_explain? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 194c814e5be3b..da5d6fcf3c3ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -302,6 +302,10 @@ def supports_index_sort_order? true end + def supports_partial_index? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index e4746d4aa3413..447d729e52a54 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -21,6 +21,18 @@ def test_create_database_with_encoding assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1) end + def test_add_index + # add_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) do |*| + false + end + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'") + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?) + end + private def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) From 7ef22fce7cdb955aba3b2f45629a711592336b1f Mon Sep 17 00:00:00 2001 From: Marcelo Silveira Date: Thu, 9 Feb 2012 03:28:11 -0200 Subject: [PATCH 2/3] Made schema dumper recognize partial indices' where statements --- .../connection_adapters/abstract/schema_definitions.rb | 2 +- .../connection_adapters/postgresql_adapter.rb | 3 ++- activerecord/lib/active_record/schema_dumper.rb | 2 ++ .../cases/adapters/postgresql/postgresql_adapter_test.rb | 6 ++++++ activerecord/test/cases/migration/index_test.rb | 6 ++++++ activerecord/test/cases/schema_dumper_test.rb | 9 +++++++++ activerecord/test/schema/schema.rb | 1 + 7 files changed, 27 insertions(+), 2 deletions(-) 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 132ca10f79716..ad2e8634eb952 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 module ConnectionAdapters #:nodoc: - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end # Abstract representation of a column definition. Instances of this type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index da5d6fcf3c3ed..f1940ea15f88c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -889,8 +889,9 @@ def indexes(table_name, name = nil) # 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] - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) end.compact end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2a565b51c62e5..dcbd165e5841b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -197,6 +197,8 @@ def indexes(table, stream) index_orders = (index.orders || {}) statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + statement_parts << (':where => ' + index.where.inspect) if index.where + ' ' + statement_parts.join(', ') end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index d57794daf8cba..898d28456b6e5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -179,6 +179,12 @@ def test_substitute_at assert_equal Arel.sql('$2'), bind end + def test_partial_index + @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" + index = @connection.indexes('ex').find { |idx| idx.name == 'partial' } + assert_equal "(number > 100)", index.where + end + private def insert(ctx, data) binds = data.map { |name, value| diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 89cf0f5e93ae0..c5a479fcdb946 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -169,6 +169,12 @@ def test_add_index connection.add_index("testings", ["last_name", "first_name"], :order => :desc) connection.remove_index("testings", ["last_name", "first_name"]) end + + # Selected adapters support partial indices + if current_adapter?(:PostgreSQLAdapter) + connection.add_index("testings", ["last_name"], :where => "first_name = 'john doe'") + connection.remove_index("testings", ["last_name"]) + end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index abeb56fd3ff89..c79382a984ba9 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -185,6 +185,15 @@ def test_schema_dumps_index_columns_in_right_order assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition end + def test_schema_dumps_partial_indices + index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + if current_adapter?(:PostgreSQLAdapter) + assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index", :where => "(rating > 10)"', index_definition + else + assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index"', index_definition + end + end + def test_schema_dump_should_honor_nonstandard_primary_keys output = standard_dump match = output.match(%r{create_table "movies"(.*)do}) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index da0c4cecdd87c..d473680fdc142 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -175,6 +175,7 @@ def create_table(*args, &block) end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" + add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" create_table :computers, :force => true do |t| t.integer :developer, :null => false From aaffc2acd5fa3104fa936c334ef9d50774071c8b Mon Sep 17 00:00:00 2001 From: Marcelo Silveira Date: Thu, 9 Feb 2012 19:04:07 -0200 Subject: [PATCH 3/3] improved test case for partial indices --- activerecord/test/cases/migration/index_test.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index c5a479fcdb946..dd9492924c3ad 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -169,14 +169,17 @@ def test_add_index connection.add_index("testings", ["last_name", "first_name"], :order => :desc) connection.remove_index("testings", ["last_name", "first_name"]) end - - # Selected adapters support partial indices - if current_adapter?(:PostgreSQLAdapter) - connection.add_index("testings", ["last_name"], :where => "first_name = 'john doe'") - connection.remove_index("testings", ["last_name"]) - end end + def test_add_partial_index + skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter) + + connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") + assert connection.index_exists?("testings", "last_name") + + connection.remove_index("testings", "last_name") + assert !connection.index_exists?("testings", "last_name") + end end end end