Skip to content

Commit

Permalink
Teach have_db_index about expression indexes
Browse files Browse the repository at this point in the history
In Rails 5, the schema layer was updated so that indexes could be
created on expressions rather that simply columns. Update
`have_db_index` so that you can test for this.

More reading: <rails/rails@edc2b77>
  • Loading branch information
mcmire committed May 31, 2019
1 parent 646469e commit 4e2448d
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 3 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Style/CharacterLiteral:
Style/ClassAndModuleChildren:
Enabled: false
Style/CollectionMethods:
Enabled: true
PreferredMethods:
find: detect
reduce: inject
Expand Down
37 changes: 34 additions & 3 deletions lib/shoulda/matchers/active_record/have_db_index_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ module ActiveRecord
# should have_db_index([:user_id, :name])
# end
#
# Finally, if you're using Rails 5 and PostgreSQL, you can also specify an
# expression:
#
# class CreateLoggedErrors < ActiveRecord::Migration
# def change
# create_table :logged_errors do |t|
# t.string :code
# t.jsonb :content
# end
#
# add_index :logged_errors, 'lower(code)::text'
# end
# end
#
# # RSpec
# RSpec.describe LoggedError, type: :model do
# it { should have_db_index('lower(code)::text') }
# end
#
# # Minitest (Shoulda)
# class LoggedErrorTest < ActiveSupport::TestCase
# should have_db_index('lower(code)::text')
# end
#
# #### Qualifiers
#
# ##### unique
Expand Down Expand Up @@ -171,9 +195,16 @@ def correct_unique?
end

def matched_index
@_matched_index ||= actual_indexes.find do |index|
index.columns == expected_columns
end
@_matched_index ||=
if expected_columns.one?
actual_indexes.detect do |index|
Array.wrap(index.columns) == expected_columns
end
else
actual_indexes.detect do |index|
index.columns == expected_columns
end
end
end

def actual_indexes
Expand Down
4 changes: 4 additions & 0 deletions spec/support/unit/helpers/active_record_versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@ def active_record_uniqueness_supports_array_columns?
def active_record_supports_optional_for_associations?
active_record_version >= 5
end

def active_record_supports_expression_indexes?
active_record_version >= 5
end
end
end
1 change: 1 addition & 0 deletions spec/support/unit/helpers/database_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ def postgresql?
alias_method :database_supports_array_columns?, :postgresql?
alias_method :database_supports_uuid_columns?, :postgresql?
alias_method :database_supports_money_columns?, :postgresql?
alias_method :database_supports_expression_indexes?, :postgresql?
end
end
136 changes: 136 additions & 0 deletions spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
require 'unit_spec_helper'

describe Shoulda::Matchers::ActiveRecord::HaveDbIndexMatcher, type: :model do
def self.can_test_expression_indexes?
active_record_supports_expression_indexes? &&
database_supports_expression_indexes?
end

describe 'the matcher' do
# rubocop:disable Layout/MultilineBlockLayout
# rubocop:disable Layout/SpaceAroundBlockParameters
Expand Down Expand Up @@ -237,6 +242,99 @@
end
end
end

if can_test_expression_indexes?
context 'when given an expression' do
context 'qualified with nothing' do
context 'when the table has the given index' do
it 'matches when used in the positive' do
record = record_with_index_on(
'lower((code)::text)',
columns: { code: :string },
)
expect(record).to have_db_index('lower((code)::text)')
end

it 'does not match when used in the negative' do
record = record_with_index_on(
'lower((code)::text)',
model_name: 'Example',
columns: { code: :string },
)

assertion = lambda do
expect(record).not_to have_db_index('lower((code)::text)')
end

expect(&assertion).to fail_with_message(<<-MESSAGE, wrap: true)
Expected the examples table not to have an index on "lower((code)::text)", but
it does.
MESSAGE
end
end

context 'when the table does not have the given index' do
it 'matches when used in the negative' do
record = record_with_index_on(
'code',
columns: { code: :string },
)
expect(record).not_to have_db_index('lower((code)::text)')
end

it 'does not match when used in the positive' do
record = record_with_index_on(
'code',
model_name: 'Example',
columns: { code: :string },
)

assertion = lambda do
expect(record).to have_db_index('lower((code)::text)')
end

expect(&assertion).to fail_with_message(<<-MESSAGE, wrap: true)
Expected the examples table to have an index on "lower((code)::text)", but it
does not.
MESSAGE
end
end
end

context 'when qualified with unique' do
include_examples(
'for when the matcher is qualified',
index: 'lower((code)::text)',
other_index: 'code',
columns: { code: :string },
unique: true,
qualifier_args: [],
)
end

context 'when qualified with unique: true' do
include_examples(
'for when the matcher is qualified',
index: 'lower((code)::text)',
other_index: 'code',
columns: { code: :string },
unique: true,
qualifier_args: [true],
)
end

context 'when qualified with unique: false' do
include_examples(
'for when the matcher is qualified',
index: 'lower((code)::text)',
other_index: 'code',
columns: { code: :string },
unique: false,
qualifier_args: [false],
)
end
end
end
end

context 'when not all models are connected to the same database' do
Expand Down Expand Up @@ -352,6 +450,44 @@
)
end
end

if can_test_expression_indexes?
context 'when given an expression' do
context 'when not qualified with anything' do
it 'returns the correct description' do
matcher = have_db_index('lower(code)')
expect(matcher.description).to eq('have an index on "lower(code)"')
end
end

context 'when qualified with unique' do
include_examples(
'for when the matcher is qualified',
index: 'lower(code)',
index_type: 'unique',
qualifier_args: [],
)
end

context 'when qualified with unique: true' do
include_examples(
'for when the matcher is qualified',
index: 'lower(code)',
index_type: 'unique',
qualifier_args: [true],
)
end

context 'when qualified with unique: false' do
include_examples(
'for when the matcher is qualified',
index: 'lower(code)',
index_type: 'non-unique',
qualifier_args: [false],
)
end
end
end
end

def record_with_index_on(
Expand Down

0 comments on commit 4e2448d

Please sign in to comment.