Skip to content

Commit e42d9f7

Browse files
steve-abramsSteven Abrams
authored andcommitted
Add support for include index option
Add support for including non-key columns in btree indexes for PostgreSQL with the INCLUDE parameter. Example: def change add_index :users, :email, include: [:id, :created_at] end Will result in: CREATE INDEX index_users_on_email USING btree (email) INCLUDE (id, created_at) The INCLUDE parameter is described in the PostgreSQL docs: https://www.postgresql.org/docs/current/sql-createindex.html
1 parent 1fd0bc1 commit e42d9f7

File tree

13 files changed

+124
-6
lines changed

13 files changed

+124
-6
lines changed

activerecord/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@
8181

8282
*Leonardo Luarte*
8383

84+
* Add `:include` index option
85+
86+
Add support for including non-key columns in indexes for PostgreSQL
87+
with the `INCLUDE` parameter.
88+
89+
```ruby
90+
add_index(:users, :email, include: [:id, :created_at])
91+
```
92+
93+
will result in:
94+
95+
```sql
96+
CREATE INDEX index_users_on_email USING btree (email) INCLUDE (id,
97+
created_at)
98+
```
99+
100+
*Steve Abrams*
101+
84102
* `ActiveRecord::Relation`’s `#any?`, `#none?`, and `#one?` methods take an optional pattern
85103
argument, more closely matching their `Enumerable` equivalents.
86104

activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module ConnectionAdapters # :nodoc:
66
# this type are typically created and returned by methods in database
77
# adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
88
class IndexDefinition # :nodoc:
9-
attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment, :valid
9+
attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :include, :comment, :valid
1010

1111
def initialize(
1212
table, name,
@@ -18,6 +18,7 @@ def initialize(
1818
where: nil,
1919
type: nil,
2020
using: nil,
21+
include: nil,
2122
comment: nil,
2223
valid: true
2324
)
@@ -31,6 +32,7 @@ def initialize(
3132
@where = where
3233
@type = type
3334
@using = using
35+
@include = include
3436
@comment = comment
3537
@valid = valid
3638
end
@@ -47,12 +49,13 @@ def column_options
4749
}
4850
end
4951

50-
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, **options)
52+
def defined_for?(columns = nil, name: nil, unique: nil, valid: nil, include: nil, **options)
5153
columns = options[:column] if columns.blank?
5254
(columns.nil? || Array(self.columns) == Array(columns).map(&:to_s)) &&
5355
(name.nil? || self.name == name.to_s) &&
5456
(unique.nil? || self.unique == unique) &&
55-
(valid.nil? || self.valid == valid)
57+
(valid.nil? || self.valid == valid) &&
58+
(include.nil? || Array(self.include) == Array(include))
5659
end
5760

5861
private

activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,16 @@ def rename_column(table_name, column_name, new_column_name)
828828
#
829829
# Note: Partial indexes are only supported for PostgreSQL and SQLite.
830830
#
831+
# ====== Creating an index that includes additional columns
832+
#
833+
# add_index(:accounts, :branch_id, include: :party_id)
834+
#
835+
# generates:
836+
#
837+
# CREATE INDEX index_accounts_on_branch_id ON accounts USING btree(branch_id) INCLUDE (party_id)
838+
#
839+
# Note: only supported by PostgreSQL.
840+
#
831841
# ====== Creating an index with a specific method
832842
#
833843
# add_index(:developers, :name, using: 'btree')
@@ -1385,7 +1395,7 @@ def update_table_definition(table_name, base) # :nodoc:
13851395
end
13861396

13871397
def add_index_options(table_name, column_name, name: nil, if_not_exists: false, internal: false, **options) # :nodoc:
1388-
options.assert_valid_keys(:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm)
1398+
options.assert_valid_keys(:unique, :length, :order, :opclass, :where, :type, :using, :comment, :algorithm, :include)
13891399

13901400
column_names = index_column_names(column_name)
13911401

@@ -1404,6 +1414,7 @@ def add_index_options(table_name, column_name, name: nil, if_not_exists: false,
14041414
where: options[:where],
14051415
type: options[:type],
14061416
using: options[:using],
1417+
include: options[:include],
14071418
comment: options[:comment]
14081419
)
14091420

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,11 @@ def supports_partial_index?
444444
false
445445
end
446446

447+
# Does this adapter support including non-key columns?
448+
def supports_index_include?
449+
false
450+
end
451+
447452
# Does this adapter support expression indices?
448453
def supports_expression_index?
449454
false

activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module ConnectionAdapters
55
module PostgreSQL
66
class SchemaCreation < SchemaCreation # :nodoc:
77
private
8+
delegate :quoted_include_columns_for_index, :supports_index_include?,
9+
to: :@conn
10+
811
def visit_AlterTable(o)
912
sql = super
1013
sql << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
@@ -27,6 +30,12 @@ def visit_CheckConstraintDefinition(o)
2730
super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
2831
end
2932

33+
def visit_CreateIndexDefinition(o)
34+
super.dup.tap do |sql|
35+
sql << " INCLUDE (#{quoted_include_columns(o.index.include)})" if supports_index_include? && o.index.include
36+
end
37+
end
38+
3039
def visit_ValidateConstraint(name)
3140
"VALIDATE CONSTRAINT #{quote_column_name(name)}"
3241
end
@@ -115,6 +124,10 @@ def add_column_options!(sql, options)
115124
super
116125
end
117126

127+
def quoted_include_columns(o)
128+
String === o ? o : quoted_include_columns_for_index(o)
129+
end
130+
118131
# Returns any SQL string to go between CREATE and TABLE. May be nil.
119132
def table_modifier_in_create(o)
120133
# A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY

activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ def indexes(table_name) # :nodoc:
108108
oid = row[4]
109109
comment = row[5]
110110
valid = row[6]
111-
using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: NULLS(?: NOT)? DISTINCT)?(?: WHERE (.+))?\z/m).flatten
111+
using, expressions, where, include = inddef.scan(/ USING (\w+?) \((.+?)\)(?: NULLS(?: NOT)? DISTINCT)?(?: WHERE (.+?))?(?: INCLUDE \((.+)\))?\z/m).flatten
112112

113113
orders = {}
114114
opclasses = {}
115+
include_columns = include ? include.split(",").map(&:strip) : []
115116

116117
if indkey.include?(0)
117118
columns = expressions
@@ -123,6 +124,9 @@ def indexes(table_name) # :nodoc:
123124
AND a.attnum IN (#{indkey.join(",")})
124125
SQL
125126

127+
# prevent INCLUDE columns from being matched
128+
columns.reject! { |c| include_columns.include?(c) }
129+
126130
# add info on sort order (only desc order is explicitly specified, asc is the default)
127131
# and non-default opclasses
128132
expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
@@ -144,6 +148,7 @@ def indexes(table_name) # :nodoc:
144148
opclasses: opclasses,
145149
where: where,
146150
using: using.to_sym,
151+
include: include_columns.map(&:to_sym).presence,
147152
comment: comment.presence,
148153
valid: valid
149154
)
@@ -759,6 +764,15 @@ def foreign_key_column_for(table_name) # :nodoc:
759764
super
760765
end
761766

767+
def quoted_include_columns_for_index(column_names) # :nodoc:
768+
return quote_column_name(column_names) if column_names.is_a?(Symbol)
769+
770+
quoted_columns = column_names.each_with_object({}) do |name, result|
771+
result[name.to_sym] = quote_column_name(name).dup
772+
end
773+
add_options_for_index_columns(quoted_columns).values.join(", ")
774+
end
775+
762776
def schema_creation # :nodoc:
763777
PostgreSQL::SchemaCreation.new(self)
764778
end

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ def supports_partial_index?
200200
true
201201
end
202202

203+
def supports_index_include?
204+
database_version >= 11_00_00 # >= 11.0
205+
end
206+
203207
def supports_expression_index?
204208
true
205209
end

activerecord/lib/active_record/schema_dumper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def index_parts(index)
230230
index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present?
231231
index_parts << "where: #{index.where.inspect}" if index.where
232232
index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
233+
index_parts << "include: #{index.include.inspect}" if index.include
233234
index_parts << "type: #{index.type.inspect}" if index.type
234235
index_parts << "comment: #{index.comment.inspect}" if index.comment
235236
index_parts

activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,22 @@ def test_partial_index
315315
end
316316
end
317317

318+
def test_include_index
319+
with_example_table do
320+
@connection.add_index "ex", %w{ id }, name: "include", include: :number
321+
index = @connection.indexes("ex").find { |idx| idx.name == "include" }
322+
assert_equal [:number], index.include
323+
end
324+
end
325+
326+
def test_include_multiple_columns_index
327+
with_example_table do
328+
@connection.add_index "ex", %w{ id }, name: "include", include: [:number, :data]
329+
index = @connection.indexes("ex").find { |idx| idx.name == "include" }
330+
assert_equal [:number, :data], index.include
331+
end
332+
end
333+
318334
def test_expression_index
319335
with_example_table do
320336
expr = "mod(id, 10), abs(number)"

activerecord/test/cases/adapters/postgresql/schema_test.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,16 @@ def test_create_join_table
768768
assert_not @connection.table_exists?("test_schema.comments_posts")
769769
end
770770
end
771+
772+
class SchemaIndexIncludeColumnsTest < ActiveRecord::PostgreSQLTestCase
773+
include SchemaDumpingHelper
774+
775+
def test_schema_dumps_index_included_columns
776+
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_include_index/).first.strip
777+
if ActiveRecord::Base.connection.supports_index_include?
778+
assert_equal 't.index ["firm_id", "type"], name: "company_include_index", include: [:name, :account_id]', index_definition
779+
else
780+
assert_equal 't.index ["firm_ids", "type"], name: "company_include_index"', index_definition
781+
end
782+
end
783+
end

0 commit comments

Comments
 (0)