Teach have_db_index about expression indexes

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: <edc2b77187>
This commit is contained in:
Elliot Winkler 2019-05-30 00:24:24 -06:00
parent 646469e5c2
commit 4e2448d775
5 changed files with 176 additions and 3 deletions

View File

@ -101,6 +101,7 @@ Style/CharacterLiteral:
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false
Style/CollectionMethods: Style/CollectionMethods:
Enabled: true
PreferredMethods: PreferredMethods:
find: detect find: detect
reduce: inject reduce: inject

View File

@ -49,6 +49,30 @@ module Shoulda
# should have_db_index([:user_id, :name]) # should have_db_index([:user_id, :name])
# end # 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 # #### Qualifiers
# #
# ##### unique # ##### unique
@ -171,9 +195,16 @@ module Shoulda
end end
def matched_index def matched_index
@_matched_index ||= actual_indexes.find do |index| @_matched_index ||=
index.columns == expected_columns if expected_columns.one?
end 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 end
def actual_indexes def actual_indexes

View File

@ -40,5 +40,9 @@ module UnitTests
def active_record_supports_optional_for_associations? def active_record_supports_optional_for_associations?
active_record_version >= 5 active_record_version >= 5
end end
def active_record_supports_expression_indexes?
active_record_version >= 5
end
end end
end end

View File

@ -16,5 +16,6 @@ module UnitTests
alias_method :database_supports_array_columns?, :postgresql? alias_method :database_supports_array_columns?, :postgresql?
alias_method :database_supports_uuid_columns?, :postgresql? alias_method :database_supports_uuid_columns?, :postgresql?
alias_method :database_supports_money_columns?, :postgresql? alias_method :database_supports_money_columns?, :postgresql?
alias_method :database_supports_expression_indexes?, :postgresql?
end end
end end

View File

@ -1,6 +1,11 @@
require 'unit_spec_helper' require 'unit_spec_helper'
describe Shoulda::Matchers::ActiveRecord::HaveDbIndexMatcher, type: :model do 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 describe 'the matcher' do
# rubocop:disable Layout/MultilineBlockLayout # rubocop:disable Layout/MultilineBlockLayout
# rubocop:disable Layout/SpaceAroundBlockParameters # rubocop:disable Layout/SpaceAroundBlockParameters
@ -237,6 +242,99 @@ Expected the examples table to have an index on [:geocodable_id,
end end
end 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 end
context 'when not all models are connected to the same database' do context 'when not all models are connected to the same database' do
@ -352,6 +450,44 @@ Expected the examples table to have an index on [:geocodable_id,
) )
end end
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 end
def record_with_index_on( def record_with_index_on(