Skip to content

Commit

Permalink
Merge pull request #4956 from mhfs/pg_partial_indices
Browse files Browse the repository at this point in the history
Add support for partial indices to PostgreSQL adapter
  • Loading branch information
tenderlove committed Feb 10, 2012
2 parents fc3fad8 + aaffc2a commit 3092324
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 5 deletions.
13 changes: 13 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,5 +1,18 @@
## Rails 4.0.0 (unreleased) ## ## 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 * Implemented ActiveRecord::Relation#none method


The `none` method returns a chainable relation with zero records The `none` method returns a chainable relation with zero records
Expand Down
Expand Up @@ -6,7 +6,7 @@


module ActiveRecord module ActiveRecord
module ConnectionAdapters #:nodoc: 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 end


# Abstract representation of a column definition. Instances of this type # Abstract representation of a column definition. Instances of this type
Expand Down
Expand Up @@ -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) # 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 = {}) def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options) 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})" execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
end end


# Remove the given index from the table. # Remove the given index from the table.
Expand Down Expand Up @@ -581,6 +588,9 @@ def add_index_options(table_name, column_name, options = {})
if Hash === options # legacy support, since this param was a string if Hash === options # legacy support, since this param was a string
index_type = options[:unique] ? "UNIQUE" : "" index_type = options[:unique] ? "UNIQUE" : ""
index_name = options[:name].to_s if options.key?(:name) index_name = options[:name].to_s if options.key?(:name)
if supports_partial_index?
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end
else else
index_type = options index_type = options
end end
Expand All @@ -593,7 +603,7 @@ def add_index_options(table_name, column_name, options = {})
end end
index_columns = quoted_columns_for_index(column_names, options).join(", ") 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 end


def index_name_for_remove(table_name, options = {}) def index_name_for_remove(table_name, options = {})
Expand Down
Expand Up @@ -142,6 +142,11 @@ def supports_index_sort_order?
false false
end end


# Does this adapter support partial indices?
def supports_partial_index?
false
end

# Does this adapter support explain? As of this writing sqlite3, # Does this adapter support explain? As of this writing sqlite3,
# mysql2, and postgresql are the only ones that do. # mysql2, and postgresql are the only ones that do.
def supports_explain? def supports_explain?
Expand Down
Expand Up @@ -302,6 +302,10 @@ def supports_index_sort_order?
true true
end end


def supports_partial_index?
true
end

class StatementPool < ConnectionAdapters::StatementPool class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max) def initialize(connection, max)
super super
Expand Down Expand Up @@ -885,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) # 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 desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} 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.compact
end end


Expand Down
2 changes: 2 additions & 0 deletions activerecord/lib/active_record/schema_dumper.rb
Expand Up @@ -197,6 +197,8 @@ def indexes(table, stream)
index_orders = (index.orders || {}) index_orders = (index.orders || {})
statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?


statement_parts << (':where => ' + index.where.inspect) if index.where

' ' + statement_parts.join(', ') ' ' + statement_parts.join(', ')
end end


Expand Down
12 changes: 12 additions & 0 deletions activerecord/test/cases/adapters/postgresql/active_schema_test.rb
Expand Up @@ -21,6 +21,18 @@ def test_create_database_with_encoding
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1) assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1)
end 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 private
def method_missing(method_symbol, *arguments) def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments)
Expand Down
Expand Up @@ -179,6 +179,12 @@ def test_substitute_at
assert_equal Arel.sql('$2'), bind assert_equal Arel.sql('$2'), bind
end 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 private
def insert(ctx, data) def insert(ctx, data)
binds = data.map { |name, value| binds = data.map { |name, value|
Expand Down
9 changes: 9 additions & 0 deletions activerecord/test/cases/migration/index_test.rb
Expand Up @@ -171,6 +171,15 @@ def test_add_index
end end
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 end
end end
9 changes: 9 additions & 0 deletions activerecord/test/cases/schema_dumper_test.rb
Expand Up @@ -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 assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition
end 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 def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump output = standard_dump
match = output.match(%r{create_table "movies"(.*)do}) match = output.match(%r{create_table "movies"(.*)do})
Expand Down
1 change: 1 addition & 0 deletions activerecord/test/schema/schema.rb
Expand Up @@ -175,6 +175,7 @@ def create_table(*args, &block)
end end


add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" 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| create_table :computers, :force => true do |t|
t.integer :developer, :null => false t.integer :developer, :null => false
Expand Down

0 comments on commit 3092324

Please sign in to comment.