diff --git a/.rubocop.yml b/.rubocop.yml index e1d6acf7..037ef6a9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -101,6 +101,7 @@ Style/CharacterLiteral: Style/ClassAndModuleChildren: Enabled: false Style/CollectionMethods: + Enabled: true PreferredMethods: find: detect reduce: inject diff --git a/lib/shoulda/matchers/active_record/have_db_index_matcher.rb b/lib/shoulda/matchers/active_record/have_db_index_matcher.rb index 24b8edf2..6cfe5d85 100644 --- a/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +++ b/lib/shoulda/matchers/active_record/have_db_index_matcher.rb @@ -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 diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb index 44eee019..1d35db63 100644 --- a/spec/support/unit/helpers/active_record_versions.rb +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -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 diff --git a/spec/support/unit/helpers/database_helpers.rb b/spec/support/unit/helpers/database_helpers.rb index f7a267b9..df7c9f4c 100644 --- a/spec/support/unit/helpers/database_helpers.rb +++ b/spec/support/unit/helpers/database_helpers.rb @@ -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 diff --git a/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb index f19eaad4..2c1025c5 100644 --- a/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/have_db_index_matcher_spec.rb @@ -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(