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:
Enabled: false
Style/CollectionMethods:
Enabled: true
PreferredMethods:
find: detect
reduce: inject

View File

@ -49,6 +49,30 @@ module Shoulda
# 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
@ -171,9 +195,16 @@ module Shoulda
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

View File

@ -40,5 +40,9 @@ module UnitTests
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

View File

@ -16,5 +16,6 @@ module UnitTests
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

View File

@ -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
@ -237,6 +242,99 @@ Expected the examples table to have an index on [:geocodable_id,
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
@ -352,6 +450,44 @@ Expected the examples table to have an index on [:geocodable_id,
)
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(