thoughtbot--shoulda-matchers/lib/shoulda/matchers/active_record/have_db_index_matcher.rb

277 lines
7.2 KiB
Ruby

module Shoulda
module Matchers
module ActiveRecord
# The `have_db_index` matcher tests that the table that backs your model
# has a specific index.
#
# You can specify one column:
#
# class CreateBlogs < ActiveRecord::Migration
# def change
# create_table :blogs do |t|
# t.integer :user_id
# end
#
# add_index :blogs, :user_id
# end
# end
#
# # RSpec
# RSpec.describe Blog, type: :model do
# it { should have_db_index(:user_id) }
# end
#
# # Minitest (Shoulda)
# class BlogTest < ActiveSupport::TestCase
# should have_db_index(:user_id)
# end
#
# Or you can specify a group of columns:
#
# class CreateBlogs < ActiveRecord::Migration
# def change
# create_table :blogs do |t|
# t.integer :user_id
# t.string :name
# end
#
# add_index :blogs, :user_id, :name
# end
# end
#
# # RSpec
# RSpec.describe Blog, type: :model do
# it { should have_db_index([:user_id, :name]) }
# end
#
# # Minitest (Shoulda)
# class BlogTest < ActiveSupport::TestCase
# 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
#
# Use `unique` to assert that the index is either unique or non-unique:
#
# class CreateBlogs < ActiveRecord::Migration
# def change
# create_table :blogs do |t|
# t.string :domain
# t.integer :user_id
# end
#
# add_index :blogs, :domain, unique: true
# add_index :blogs, :user_id
# end
# end
#
# # RSpec
# RSpec.describe Blog, type: :model do
# it { should have_db_index(:name).unique }
# it { should have_db_index(:name).unique(true) } # if you want to be explicit
# it { should have_db_index(:user_id).unique(false) }
# end
#
# # Minitest (Shoulda)
# class BlogTest < ActiveSupport::TestCase
# should have_db_index(:name).unique
# should have_db_index(:name).unique(true) # if you want to be explicit
# should have_db_index(:user_id).unique(false)
# end
#
# @return [HaveDbIndexMatcher]
#
def have_db_index(columns)
HaveDbIndexMatcher.new(columns)
end
# @private
class HaveDbIndexMatcher
def initialize(columns)
@expected_columns = normalize_columns_to_array(columns)
@qualifiers = {}
end
def unique(unique = true)
@qualifiers[:unique] = unique
self
end
def matches?(subject)
@subject = subject
index_exists? && correct_unique?
end
def failure_message
message =
"Expected #{described_table_name} to #{positive_expectation}"
message <<
if index_exists?
". The index does exist, but #{reason}."
elsif reason
", but #{reason}."
else
', but it does not.'
end
Shoulda::Matchers.word_wrap(message)
end
def failure_message_when_negated
Shoulda::Matchers.word_wrap(
"Expected #{described_table_name} not to " +
"#{negative_expectation}, but it does.",
)
end
def description
description = 'have '
description <<
if qualifiers.include?(:unique)
Shoulda::Matchers::Util.a_or_an(index_type) + ' '
else
'an '
end
description << 'index on '
description << inspected_expected_columns
end
private
attr_reader :expected_columns, :qualifiers, :subject, :reason
def normalize_columns_to_array(columns)
Array.wrap(columns).map(&:to_s)
end
def index_exists?
!matched_index.nil?
end
def correct_unique?
if qualifiers.include?(:unique)
if qualifiers[:unique] && !matched_index.unique
@reason = 'it is not unique'
false
elsif !qualifiers[:unique] && matched_index.unique
@reason = 'it is unique'
false
else
true
end
else
true
end
end
def matched_index
@_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
model.connection.indexes(table_name)
end
def described_table_name
if model
"the #{table_name} table"
else
'a table'
end
end
def table_name
model.table_name
end
def positive_expectation
if index_exists?
expectation = "have an index on #{inspected_expected_columns}"
if qualifiers.include?(:unique)
expectation << " and for it to be #{index_type}"
end
expectation
else
description
end
end
def negative_expectation
description
end
def inspected_expected_columns
if formatted_expected_columns.one?
formatted_expected_columns.first.inspect
else
formatted_expected_columns.inspect
end
end
def index_type
if qualifiers[:unique]
'unique'
else
'non-unique'
end
end
def formatted_expected_columns
expected_columns.map do |column|
if column.match?(/^\w+$/)
column.to_sym
else
column
end
end
end
def model
subject&.class
end
end
end
end
end