diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a89774b1..226586b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,33 +36,28 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['2.7', '3.0', '3.1'] + ruby-version: ['2.7', '3.0', '3.1', '3.2'] active-record-version-env: - - ACTIVE_RECORD_VERSION="~> 5.2.0" - ACTIVE_RECORD_VERSION="~> 6.0.0" - ACTIVE_RECORD_VERSION="~> 6.1.0" - ACTIVE_RECORD_VERSION="~> 7.0.0" allow-failure: [false] include: - - ruby-version: '3.1' + - ruby-version: '3.2' active-record-version-env: ACTIVE_RECORD_BRANCH="main" allow-failure: true - - ruby-version: '3.1' + - ruby-version: '3.2' active-record-version-env: ACTIVE_RECORD_BRANCH="7-0-stable" allow-failure: true - - ruby-version: '3.1' + - ruby-version: '3.2' active-record-version-env: ACTIVE_RECORD_BRANCH="6-1-stable" allow-failure: true - exclude: - - ruby-version: '3.0' - active-record-version-env: ACTIVE_RECORD_VERSION="~> 5.2.0" - allow-failure: false - - ruby-version: '3.1' - active-record-version-env: ACTIVE_RECORD_VERSION="~> 5.2.0" - allow-failure: false + - ruby-version: '3.3.0-preview1' + active-record-version-env: ACTIVE_RECORD_VERSION="~> 7.0.0" + allow-failure: true continue-on-error: ${{ matrix.allow-failure }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.rubocop.yml b/.rubocop.yml index 5bb6075e..7d946471 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,137 +1,34 @@ require: + - standard - rubocop-performance - rubocop-rails - rubocop-rake - rubocop-rspec +inherit_gem: + standard: config/base.yml + AllCops: TargetRubyVersion: 2.7 - NewCops: enable + NewCops: disable Exclude: - bin/**/* - vendor/**/* -Style/StringLiterals: - Enabled: false - -Layout/LineLength: - Max: 120 - -Metrics/MethodLength: - Max: 15 - -Metrics/BlockLength: - Exclude: - - spec/**/* - -Layout/ParameterAlignment: - EnforcedStyle: with_fixed_indentation - -Style/NumericPredicate: - Enabled: false - -Style/PercentLiteralDelimiters: - PreferredDelimiters: - '%w': '[]' - '%W': '[]' - -Style/GuardClause: - Enabled: false - -Naming/VariableNumber: - EnforcedStyle: snake_case - -Bundler/OrderedGems: - Enabled: false - -Bundler/DuplicatedGem: - Enabled: false - -Style/EmptyMethod: - EnforcedStyle: expanded - -Layout/FirstArrayElementIndentation: - EnforcedStyle: consistent - -Style/Documentation: - Enabled: false - -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 - -Style/HashEachMethods: +Lint/RedundantCopDisableDirective: Enabled: true -Style/HashTransformKeys: +Lint/RedundantCopEnableDirective: Enabled: true -Style/HashTransformValues: - Enabled: true - -Rails/ApplicationRecord: +Bundler/DuplicatedGem: Enabled: false -Rails/TimeZone: +Rails/ApplicationRecord: Enabled: false -RSpec/ContextWording: - Prefixes: - - using - - via - - when - - with - - without - -Lint/RaiseException: - Enabled: true - -Lint/StructNewOverride: - Enabled: true - -Layout/SpaceAroundMethodCallOperator: - Enabled: true - -Style/ExponentialNotation: - Enabled: true - -RSpec/DescribedClass: - Enabled: true - -RSpec/ExpectInHook: +Rails/RakeEnvironment: Enabled: false -RSpec/FilePath: - CustomTransform: - TSearch: "tsearch" - DMetaphone: "dmetaphone" - -Layout/EmptyLinesAroundAttributeAccessor: - Enabled: true - -Lint/DeprecatedOpenSSLConstant: - Enabled: true - -Style/SlicingWithRange: - Enabled: true - -Lint/MixedRegexpCaptureTypes: - Enabled: true - -Style/RedundantFetchBlock: - Enabled: true - -Style/RedundantRegexpCharacterClass: - Enabled: true - -Style/RedundantRegexpEscape: - Enabled: true - -RSpec/MultipleExpectations: - Max: 5 - -RSpec/ExampleLength: - Max: 15 - -Rails/RakeEnvironment: +Rails/TimeZone: Enabled: false diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..974865fc --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 2.7.6 diff --git a/Gemfile b/Gemfile index ea35689d..6d374066 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,30 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" gemspec -gem 'pg', '>= 0.21.0', platform: :ruby +gem "pg", ">= 0.21.0", platform: :ruby gem "activerecord-jdbcpostgresql-adapter", ">= 1.3.1", platform: :jruby -if ENV['ACTIVE_RECORD_BRANCH'] - gem 'activerecord', git: 'https://github.com/rails/rails.git', branch: ENV.fetch('ACTIVE_RECORD_BRANCH', nil) - gem 'arel', git: 'https://github.com/rails/arel.git' if ENV.fetch('ACTIVE_RECORD_BRANCH', nil) == 'master' +if ENV["ACTIVE_RECORD_BRANCH"] + gem "activerecord", git: "https://github.com/rails/rails.git", branch: ENV.fetch("ACTIVE_RECORD_BRANCH", nil) + gem "arel", git: "https://github.com/rails/arel.git" if ENV.fetch("ACTIVE_RECORD_BRANCH", nil) == "master" end -gem 'activerecord', ENV.fetch('ACTIVE_RECORD_VERSION', nil) if ENV['ACTIVE_RECORD_VERSION'] +gem "activerecord", ENV.fetch("ACTIVE_RECORD_VERSION", nil) if ENV["ACTIVE_RECORD_VERSION"] + +gem "pry" +gem "rake" +gem "rspec" +gem "rubocop" +gem "rubocop-performance" +gem "rubocop-rails" +gem "rubocop-rake" +gem "rubocop-rspec" +gem "simplecov" +gem "simplecov-lcov" +gem "standard", ">= 1.23.0" +gem "undercover" +gem "warning" +gem "with_model" diff --git a/README.md b/README.md index 3b70c3b5..b452b37e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Read the blog post introducing PgSearch at https://tanzu.vmware.com/content/blog ## REQUIREMENTS * Ruby 2.7+ -* ActiveRecord 5.2+ +* Active Record 6.0+ * PostgreSQL 9.2+ * [PostgreSQL extensions](https://github.com/Casecommons/pg_search/wiki/Installing-PostgreSQL-Extensions) for certain features @@ -979,7 +979,7 @@ Sentence.word_similarity_like("word") # => [sentence] ### Limiting Fields When Combining Features Sometimes when doing queries combining different features you -might want to searching against only some of the fields with certain features. +might want to search against only some of the fields with certain features. For example perhaps you want to only do a trigram search against the shorter fields so that you don't need to reduce the threshold excessively. You can specify which fields using the 'only' option: diff --git a/Rakefile b/Rakefile index fd5be07e..6167eb4c 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'bundler' +require "bundler" Bundler::GemHelper.install_tasks -require 'rspec/core/rake_task' +require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) require "rubocop/rake_task" diff --git a/lib/pg_search/configuration.rb b/lib/pg_search/configuration.rb index ce37edf9..5dd88f4d 100644 --- a/lib/pg_search/configuration.rb +++ b/lib/pg_search/configuration.rb @@ -80,7 +80,7 @@ def order_within_rank attr_reader :options def default_options - { using: :tsearch } + {using: :tsearch} end VALID_KEYS = %w[ diff --git a/lib/pg_search/configuration/association.rb b/lib/pg_search/configuration/association.rb index d9c17108..3a40e5a6 100644 --- a/lib/pg_search/configuration/association.rb +++ b/lib/pg_search/configuration/association.rb @@ -39,22 +39,17 @@ def selects def selects_for_singular_association columns.map do |column| - if column.tsvector_column - "tsvector_agg(#{column.full_name}) AS #{column.alias}" - else - case postgresql_version - when 0..90000 - "array_to_string(array_agg(#{column.full_name}::text), ' ') AS #{column.alias}" - else - "string_agg(#{column.full_name}::text, ' ') AS #{column.alias}" - end - end + "#{column.full_name}::text AS #{column.alias}" end.join(", ") end def selects_for_multiple_association columns.map do |column| - "string_agg(#{column.full_name}::text, ' ') AS #{column.alias}" + if column.tsvector_column + "tsvector_agg(#{column.full_name}) AS #{column.alias}" + else + "string_agg(#{column.full_name}::text, ' ') AS #{column.alias}" + end end.join(", ") end diff --git a/lib/pg_search/configuration/column.rb b/lib/pg_search/configuration/column.rb index 1cf9dedb..30d89fc2 100644 --- a/lib/pg_search/configuration/column.rb +++ b/lib/pg_search/configuration/column.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'digest' +require "digest" module PgSearch class Configuration @@ -8,8 +8,8 @@ class Column attr_reader :weight, :tsvector_column, :name def initialize(column_name, weight, model) - @name = column_name.to_s - @column_name = column_name.to_s + @name = column_name.to_s + @column_name = column_name if weight.is_a?(Hash) @weight = weight[:weight] @tsvector_column = weight[:tsvector_column] @@ -21,6 +21,8 @@ def initialize(column_name, weight, model) end def full_name + return @column_name if @column_name.is_a?(Arel::Nodes::SqlLiteral) + "#{table_name}.#{column_name}" end @@ -28,7 +30,7 @@ def to_sql if tsvector_column "coalesce(#{expression}, '')" else - "coalesce(#{expression}::text, '')" + "coalesce((#{expression})::text, '')" end end @@ -39,7 +41,7 @@ def table_name end def column_name - @connection.quote_column_name(@column_name) + @connection.quote_column_name(@name) end def expression diff --git a/lib/pg_search/configuration/foreign_column.rb b/lib/pg_search/configuration/foreign_column.rb index 3886c160..b4956b01 100644 --- a/lib/pg_search/configuration/foreign_column.rb +++ b/lib/pg_search/configuration/foreign_column.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'digest' +require "digest" module PgSearch class Configuration diff --git a/lib/pg_search/document.rb b/lib/pg_search/document.rb index e0f3d3ec..e41eaaa1 100644 --- a/lib/pg_search/document.rb +++ b/lib/pg_search/document.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'logger' +require "logger" module PgSearch class Document < ActiveRecord::Base include PgSearch::Model - self.table_name = 'pg_search_documents' + self.table_name = "pg_search_documents" belongs_to :searchable, polymorphic: true # The logger might not have loaded yet. @@ -17,12 +17,12 @@ def self.logger pg_search_scope :search, lambda { |*args| options = if PgSearch.multisearch_options.respond_to?(:call) - PgSearch.multisearch_options.call(*args) - else - { query: args.first }.merge(PgSearch.multisearch_options) - end + PgSearch.multisearch_options.call(*args) + else + {query: args.first}.merge(PgSearch.multisearch_options) + end - { against: :content }.merge(options) + {against: :content}.merge(options) } end end diff --git a/lib/pg_search/features/dmetaphone.rb b/lib/pg_search/features/dmetaphone.rb index 295f79be..eb6852c6 100644 --- a/lib/pg_search/features/dmetaphone.rb +++ b/lib/pg_search/features/dmetaphone.rb @@ -7,7 +7,7 @@ module Features class DMetaphone def initialize(query, options, columns, model, normalizer) dmetaphone_normalizer = Normalizer.new(normalizer) - options = (options || {}).merge(dictionary: 'simple') + options = (options || {}).merge(dictionary: "simple") @tsearch = TSearch.new(query, options, columns, model, dmetaphone_normalizer) end diff --git a/lib/pg_search/features/trigram.rb b/lib/pg_search/features/trigram.rb index 47f3cc68..80bdfed9 100644 --- a/lib/pg_search/features/trigram.rb +++ b/lib/pg_search/features/trigram.rb @@ -35,17 +35,17 @@ def word_similarity? def similarity_function if word_similarity? - 'word_similarity' + "word_similarity" else - 'similarity' + "similarity" end end def infix_operator if word_similarity? - '<%' + "<%" else - '%' + "%" end end diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index a0e5e89f..a4136bde 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true require "active_support/core_ext/module/delegation" -require 'active_support/deprecation' +require "active_support/deprecation" module PgSearch module Features - class TSearch < Feature # rubocop:disable Metrics/ClassLength + class TSearch < Feature def self.valid_options super + %i[dictionary prefix negation any_word normalization tsvector_column highlight] end @@ -36,7 +36,7 @@ def ts_headline end def ts_headline_options - return '' unless options[:highlight].is_a?(Hash) + return "" unless options[:highlight].is_a?(Hash) headline_options .merge(deprecated_headline_options) @@ -58,7 +58,7 @@ def headline_options end end - def deprecated_headline_options # rubocop:disable Metrics/MethodLength + def deprecated_headline_options indifferent_options = options.with_indifferent_access %w[ @@ -94,11 +94,11 @@ def ts_headline_option_value(value) end end - DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’ʻʼ]/.freeze + DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’ʻʼ]/ def tsquery_for_term(unsanitized_term) if options[:negation] && unsanitized_term.start_with?("!") - unsanitized_term[0] = '' + unsanitized_term[0] = "" negated = true end @@ -116,7 +116,7 @@ def tsquery_for_term(unsanitized_term) # If :negated is true, then the term will have ! prepended to the front. def tsquery_expression(term_sql, negated:, prefix:) terms = [ - (Arel::Nodes.build_quoted('!') if negated), + (Arel::Nodes.build_quoted("!") if negated), Arel::Nodes.build_quoted("' "), term_sql, Arel::Nodes.build_quoted(" '"), @@ -133,7 +133,7 @@ def tsquery query_terms = query.split.compact tsquery_terms = query_terms.map { |term| tsquery_for_term(term) } - tsquery_terms.join(options[:any_word] ? ' || ' : ' && ') + tsquery_terms.join(options[:any_word] ? " || " : " && ") end def tsdocument @@ -151,7 +151,7 @@ def tsdocument end end - tsdocument_terms.join(' || ') + tsdocument_terms.join(" || ") end # From http://www.postgresql.org/docs/8.3/static/textsearch-controls.html @@ -192,15 +192,10 @@ def columns_to_use end def column_to_tsvector(search_column) - tsvector = - if search_column.tsvector_column - search_column.to_sql - else - Arel::Nodes::NamedFunction.new( - "to_tsvector", - [dictionary, Arel.sql(normalize(search_column.to_sql))] - ).to_sql - end + tsvector = Arel::Nodes::NamedFunction.new( + "to_tsvector", + [dictionary, Arel.sql(normalize(search_column.to_sql))] + ).to_sql if search_column.weight.nil? tsvector diff --git a/lib/pg_search/migration/dmetaphone_generator.rb b/lib/pg_search/migration/dmetaphone_generator.rb index 46e7888c..556a22e6 100644 --- a/lib/pg_search/migration/dmetaphone_generator.rb +++ b/lib/pg_search/migration/dmetaphone_generator.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'pg_search/migration/generator' +require "pg_search/migration/generator" module PgSearch module Migration class DmetaphoneGenerator < Generator def migration_name - 'add_pg_search_dmetaphone_support_functions' + "add_pg_search_dmetaphone_support_functions" end end end diff --git a/lib/pg_search/migration/generator.rb b/lib/pg_search/migration/generator.rb index b0a422ae..2cf06a46 100644 --- a/lib/pg_search/migration/generator.rb +++ b/lib/pg_search/migration/generator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'active_record' -require 'rails/generators/base' +require "active_record" +require "rails/generators/base" module PgSearch module Migration @@ -10,19 +10,19 @@ class Generator < Rails::Generators::Base def self.inherited(subclass) super - subclass.source_root File.expand_path('templates', __dir__) + subclass.source_root File.expand_path("templates", __dir__) end def create_migration now = Time.now.utc - filename = "#{now.strftime('%Y%m%d%H%M%S')}_#{migration_name}.rb" + filename = "#{now.strftime("%Y%m%d%H%M%S")}_#{migration_name}.rb" template "#{migration_name}.rb.erb", "db/migrate/#{filename}", migration_version end private def read_sql_file(filename) - sql_directory = File.expand_path('../../../sql', __dir__) + sql_directory = File.expand_path("../../../sql", __dir__) source_path = File.join(sql_directory, "#{filename}.sql") File.read(source_path).strip end diff --git a/lib/pg_search/migration/multisearch_generator.rb b/lib/pg_search/migration/multisearch_generator.rb index c97fed4b..8f8cd860 100644 --- a/lib/pg_search/migration/multisearch_generator.rb +++ b/lib/pg_search/migration/multisearch_generator.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'pg_search/migration/generator' +require "pg_search/migration/generator" module PgSearch module Migration class MultisearchGenerator < Generator def migration_name - 'create_pg_search_documents' + "create_pg_search_documents" end end end diff --git a/lib/pg_search/model.rb b/lib/pg_search/model.rb index eaa5ddf6..ceb7a0f1 100644 --- a/lib/pg_search/model.rb +++ b/lib/pg_search/model.rb @@ -7,12 +7,12 @@ module Model module ClassMethods def pg_search_scope(name, options) options_proc = if options.respond_to?(:call) - options - elsif options.respond_to?(:merge) - ->(query) { { query: query }.merge(options) } - else - raise ArgumentError, 'pg_search_scope expects a Hash or Proc' - end + options + elsif options.respond_to?(:merge) + ->(query) { {query: query}.merge(options) } + else + raise ArgumentError, "pg_search_scope expects a Hash or Proc" + end define_singleton_method(name) do |*args| config = Configuration.new(options_proc.call(*args), self) diff --git a/lib/pg_search/multisearchable.rb b/lib/pg_search/multisearchable.rb index 10a142f6..5309c1c0 100644 --- a/lib/pg_search/multisearchable.rb +++ b/lib/pg_search/multisearchable.rb @@ -7,12 +7,12 @@ module Multisearchable def self.included(mod) mod.class_eval do has_one :pg_search_document, - as: :searchable, - class_name: "PgSearch::Document", - dependent: :delete + as: :searchable, + class_name: "PgSearch::Document", + dependent: :delete after_save :update_pg_search_document, - if: -> { PgSearch.multisearch_enabled? } + if: -> { PgSearch.multisearch_enabled? } end end @@ -39,7 +39,7 @@ def should_update_pg_search_document? conditions.all? { |condition| condition.to_proc.call(self) } end - def update_pg_search_document # rubocop:disable Metrics/AbcSize + def update_pg_search_document if_conditions = Array(pg_search_multisearchable_options[:if]) unless_conditions = Array(pg_search_multisearchable_options[:unless]) diff --git a/lib/pg_search/normalizer.rb b/lib/pg_search/normalizer.rb index aed3f721..370b6a83 100644 --- a/lib/pg_search/normalizer.rb +++ b/lib/pg_search/normalizer.rb @@ -10,11 +10,11 @@ def add_normalization(sql_expression) return sql_expression unless config.ignore.include?(:accents) sql_node = case sql_expression - when Arel::Nodes::Node - sql_expression - else - Arel.sql(sql_expression) - end + when Arel::Nodes::Node + sql_expression + else + Arel.sql(sql_expression) + end Arel::Nodes::NamedFunction.new( PgSearch.unaccent_function, diff --git a/lib/pg_search/scope_options.rb b/lib/pg_search/scope_options.rb index 369a603a..b03d13d0 100644 --- a/lib/pg_search/scope_options.rb +++ b/lib/pg_search/scope_options.rb @@ -91,9 +91,9 @@ def subquery def conditions config.features - .reject { |_feature_name, feature_options| feature_options && feature_options[:sort_only] } - .map { |feature_name, _feature_options| feature_for(feature_name).conditions } - .inject { |accumulator, expression| Arel::Nodes::Or.new(accumulator, expression) } + .reject { |_feature_name, feature_options| feature_options && feature_options[:sort_only] } + .map { |feature_name, _feature_options| feature_for(feature_name).conditions } + .inject { |accumulator, expression| Arel::Nodes::Or.new(accumulator, expression) } end def order_within_rank @@ -108,7 +108,7 @@ def subquery_join if config.associations.any? config.associations.map do |association| association.join(primary_key) - end.join(' ') + end.join(" ") end end diff --git a/lib/pg_search/tasks.rb b/lib/pg_search/tasks.rb index 04fec234..3b5740f4 100644 --- a/lib/pg_search/tasks.rb +++ b/lib/pg_search/tasks.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rake' -require 'pg_search' +require "rake" +require "pg_search" namespace :pg_search do namespace :multisearch do diff --git a/lib/pg_search/version.rb b/lib/pg_search/version.rb index 2a007da3..afff11ae 100644 --- a/lib/pg_search/version.rb +++ b/lib/pg_search/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module PgSearch - VERSION = '2.3.6' + VERSION = "2.3.6" end diff --git a/pg_search.gemspec b/pg_search.gemspec index ada1908b..103a9a38 100644 --- a/pg_search.gemspec +++ b/pg_search.gemspec @@ -1,39 +1,25 @@ # frozen_string_literal: true -$LOAD_PATH.push File.expand_path('lib', __dir__) -require 'pg_search/version' +$LOAD_PATH.push File.expand_path("lib", __dir__) +require "pg_search/version" Gem::Specification.new do |s| - s.name = 'pg_search' - s.version = PgSearch::VERSION - s.platform = Gem::Platform::RUBY - s.authors = ['Grant Hutchins', 'Case Commons, LLC'] - s.email = %w[gems@nertzy.com casecommons-dev@googlegroups.com] - s.homepage = 'https://github.com/Casecommons/pg_search' - s.summary = "PgSearch builds Active Record named scopes that take advantage of PostgreSQL's full text search" + s.name = "pg_search" + s.version = PgSearch::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ["Grant Hutchins", "Case Commons, LLC"] + s.email = %w[gems@nertzy.com casecommons-dev@googlegroups.com] + s.homepage = "https://github.com/Casecommons/pg_search" + s.summary = "PgSearch builds Active Record named scopes that take advantage of PostgreSQL's full text search" s.description = "PgSearch builds Active Record named scopes that take advantage of PostgreSQL's full text search" - s.licenses = ['MIT'] + s.licenses = ["MIT"] s.metadata["rubygems_mfa_required"] = "true" - s.files = `git ls-files -z`.split("\x0") - s.require_paths = ['lib'] + s.files = `git ls-files -z`.split("\x0") + s.require_paths = ["lib"] - s.add_dependency 'activerecord', '>= 5.2' - s.add_dependency 'activesupport', '>= 5.2' + s.add_dependency "activerecord", ">= 6.0" + s.add_dependency "activesupport", ">= 6.0" - s.add_development_dependency 'pry' - s.add_development_dependency 'rake' - s.add_development_dependency 'rspec' - s.add_development_dependency 'rubocop' - s.add_development_dependency 'rubocop-performance' - s.add_development_dependency 'rubocop-rails' - s.add_development_dependency 'rubocop-rake' - s.add_development_dependency 'rubocop-rspec' - s.add_development_dependency 'simplecov' - s.add_development_dependency 'simplecov-lcov' - s.add_development_dependency 'undercover' - s.add_development_dependency 'warning' - s.add_development_dependency 'with_model' - - s.required_ruby_version = '>= 2.7' + s.required_ruby_version = ">= 2.7" end diff --git a/spec/.rubocop.yml b/spec/.rubocop.yml index 6e3e3040..ee624fbe 100644 --- a/spec/.rubocop.yml +++ b/spec/.rubocop.yml @@ -1,14 +1,27 @@ inherit_from: - ../.rubocop.yml -Layout/LineLength: - Enabled: false +RSpec/ContextWording: + Prefixes: + - using + - via + - when + - with + - without -Lint/SuppressedException: - Enabled: false +RSpec/DescribedClass: + Enabled: true -Lint/UselessAssignment: - Enabled: false +RSpec/ExampleLength: + Max: 15 -Style/BlockDelimiters: +RSpec/ExpectInHook: Enabled: false + +RSpec/FilePath: + CustomTransform: + TSearch: "tsearch" + DMetaphone: "dmetaphone" + +RSpec/MultipleExpectations: + Max: 5 diff --git a/spec/integration/.rubocop.yml b/spec/integration/.rubocop.yml index 09ebbc02..b76a90e7 100644 --- a/spec/integration/.rubocop.yml +++ b/spec/integration/.rubocop.yml @@ -4,8 +4,8 @@ inherit_from: RSpec/DescribeClass: Enabled: false -RSpec/MultipleExpectations: +RSpec/ExampleLength: Enabled: false -RSpec/ExampleLength: +RSpec/MultipleExpectations: Enabled: false diff --git a/spec/integration/associations_spec.rb b/spec/integration/associations_spec.rb index a0801ed6..e929c2af 100644 --- a/spec/integration/associations_spec.rb +++ b/spec/integration/associations_spec.rb @@ -20,23 +20,23 @@ model do include PgSearch::Model - belongs_to :another_model, class_name: 'AssociatedModel' + belongs_to :another_model, class_name: "AssociatedModel" - pg_search_scope :with_another, associated_against: { another_model: :title } + pg_search_scope :with_another, associated_against: {another_model: :title} end end it "returns rows that match the query in the columns of the associated model only" do - associated = AssociatedModel.create!(title: 'abcdef') + associated = AssociatedModel.create!(title: "abcdef") included = [ - ModelWithoutAgainst.create!(title: 'abcdef', another_model: associated), - ModelWithoutAgainst.create!(title: 'ghijkl', another_model: associated) + ModelWithoutAgainst.create!(title: "abcdef", another_model: associated), + ModelWithoutAgainst.create!(title: "ghijkl", another_model: associated) ] excluded = [ - ModelWithoutAgainst.create!(title: 'abcdef') + ModelWithoutAgainst.create!(title: "abcdef") ] - results = ModelWithoutAgainst.with_another('abcdef') + results = ModelWithoutAgainst.with_another("abcdef") expect(results.map(&:title)).to match_array(included.map(&:title)) expect(results).not_to include(excluded) end @@ -45,34 +45,34 @@ context "via a belongs_to association" do with_model :AssociatedModel do table do |t| - t.string 'title' + t.string "title" end end with_model :ModelWithBelongsTo do table do |t| - t.string 'title' - t.belongs_to 'another_model', index: false + t.string "title" + t.belongs_to "another_model", index: false end model do include PgSearch::Model - belongs_to :another_model, class_name: 'AssociatedModel' + belongs_to :another_model, class_name: "AssociatedModel" - pg_search_scope :with_associated, against: :title, associated_against: { another_model: :title } + pg_search_scope :with_associated, against: :title, associated_against: {another_model: :title} end end it "returns rows that match the query in either its own columns or the columns of the associated model" do - associated = AssociatedModel.create!(title: 'abcdef') + associated = AssociatedModel.create!(title: "abcdef") included = [ - ModelWithBelongsTo.create!(title: 'ghijkl', another_model: associated), - ModelWithBelongsTo.create!(title: 'abcdef') + ModelWithBelongsTo.create!(title: "ghijkl", another_model: associated), + ModelWithBelongsTo.create!(title: "abcdef") ] - excluded = ModelWithBelongsTo.create!(title: 'mnopqr', - another_model: AssociatedModel.create!(title: 'stuvwx')) + excluded = ModelWithBelongsTo.create!(title: "mnopqr", + another_model: AssociatedModel.create!(title: "stuvwx")) - results = ModelWithBelongsTo.with_associated('abcdef') + results = ModelWithBelongsTo.with_associated("abcdef") expect(results.map(&:title)).to match_array(included.map(&:title)) expect(results).not_to include(excluded) end @@ -81,61 +81,61 @@ context "via a has_many association" do with_model :AssociatedModelWithHasMany do table do |t| - t.string 'title' - t.belongs_to 'ModelWithHasMany', index: false + t.string "title" + t.belongs_to "ModelWithHasMany", index: false end end with_model :ModelWithHasMany do table do |t| - t.string 'title' + t.string "title" end model do include PgSearch::Model - has_many :other_models, class_name: 'AssociatedModelWithHasMany', foreign_key: 'ModelWithHasMany_id' + has_many :other_models, class_name: "AssociatedModelWithHasMany", foreign_key: "ModelWithHasMany_id" - pg_search_scope :with_associated, against: [:title], associated_against: { other_models: :title } + pg_search_scope :with_associated, against: [:title], associated_against: {other_models: :title} end end it "returns rows that match the query in either its own columns or the columns of the associated model" do included = [ - ModelWithHasMany.create!(title: 'abcdef', other_models: [ - AssociatedModelWithHasMany.create!(title: 'foo'), - AssociatedModelWithHasMany.create!(title: 'bar') + ModelWithHasMany.create!(title: "abcdef", other_models: [ + AssociatedModelWithHasMany.create!(title: "foo"), + AssociatedModelWithHasMany.create!(title: "bar") ]), - ModelWithHasMany.create!(title: 'ghijkl', other_models: [ - AssociatedModelWithHasMany.create!(title: 'foo bar'), - AssociatedModelWithHasMany.create!(title: 'mnopqr') + ModelWithHasMany.create!(title: "ghijkl", other_models: [ + AssociatedModelWithHasMany.create!(title: "foo bar"), + AssociatedModelWithHasMany.create!(title: "mnopqr") ]), - ModelWithHasMany.create!(title: 'foo bar') + ModelWithHasMany.create!(title: "foo bar") ] - excluded = ModelWithHasMany.create!(title: 'stuvwx', other_models: [ - AssociatedModelWithHasMany.create!(title: 'abcdef') + excluded = ModelWithHasMany.create!(title: "stuvwx", other_models: [ + AssociatedModelWithHasMany.create!(title: "abcdef") ]) - results = ModelWithHasMany.with_associated('foo bar') + results = ModelWithHasMany.with_associated("foo bar") expect(results.map(&:title)).to match_array(included.map(&:title)) expect(results).not_to include(excluded) end it "uses an unscoped relation of the associated model" do - excluded = ModelWithHasMany.create!(title: 'abcdef', other_models: [ - AssociatedModelWithHasMany.create!(title: 'abcdef') + excluded = ModelWithHasMany.create!(title: "abcdef", other_models: [ + AssociatedModelWithHasMany.create!(title: "abcdef") ]) included = [ - ModelWithHasMany.create!(title: 'abcdef', other_models: [ - AssociatedModelWithHasMany.create!(title: 'foo'), - AssociatedModelWithHasMany.create!(title: 'bar') + ModelWithHasMany.create!(title: "abcdef", other_models: [ + AssociatedModelWithHasMany.create!(title: "foo"), + AssociatedModelWithHasMany.create!(title: "bar") ]) ] results = ModelWithHasMany - .limit(1) - .order(Arel.sql("#{ModelWithHasMany.quoted_table_name}.id ASC")) - .with_associated('foo bar') + .limit(1) + .order(Arel.sql("#{ModelWithHasMany.quoted_table_name}.id ASC")) + .with_associated("foo bar") expect(results.map(&:title)).to match_array(included.map(&:title)) expect(results).not_to include(excluded) @@ -146,36 +146,36 @@ context "when on different tables" do with_model :FirstAssociatedModel do table do |t| - t.string 'title' - t.belongs_to 'ModelWithManyAssociations', index: false + t.string "title" + t.belongs_to "ModelWithManyAssociations", index: false end end with_model :SecondAssociatedModel do table do |t| - t.string 'title' + t.string "title" end end with_model :ModelWithManyAssociations do table do |t| - t.string 'title' - t.belongs_to 'model_of_second_type', index: false + t.string "title" + t.belongs_to "model_of_second_type", index: false end model do include PgSearch::Model has_many :models_of_first_type, - class_name: 'FirstAssociatedModel', - foreign_key: 'ModelWithManyAssociations_id' + class_name: "FirstAssociatedModel", + foreign_key: "ModelWithManyAssociations_id" belongs_to :model_of_second_type, - class_name: 'SecondAssociatedModel' + class_name: "SecondAssociatedModel" pg_search_scope :with_associated, - against: :title, - associated_against: { models_of_first_type: :title, model_of_second_type: :title } + against: :title, + associated_against: {models_of_first_type: :title, model_of_second_type: :title} end end @@ -184,25 +184,25 @@ unmatching_second = SecondAssociatedModel.create!(title: "uiop") included = [ - ModelWithManyAssociations.create!(title: 'abcdef', models_of_first_type: [ - FirstAssociatedModel.create!(title: 'foo'), - FirstAssociatedModel.create!(title: 'bar') + ModelWithManyAssociations.create!(title: "abcdef", models_of_first_type: [ + FirstAssociatedModel.create!(title: "foo"), + FirstAssociatedModel.create!(title: "bar") ]), - ModelWithManyAssociations.create!(title: 'ghijkl', models_of_first_type: [ - FirstAssociatedModel.create!(title: 'foo bar'), - FirstAssociatedModel.create!(title: 'mnopqr') + ModelWithManyAssociations.create!(title: "ghijkl", models_of_first_type: [ + FirstAssociatedModel.create!(title: "foo bar"), + FirstAssociatedModel.create!(title: "mnopqr") ]), - ModelWithManyAssociations.create!(title: 'foo bar'), - ModelWithManyAssociations.create!(title: 'qwerty', model_of_second_type: matching_second) + ModelWithManyAssociations.create!(title: "foo bar"), + ModelWithManyAssociations.create!(title: "qwerty", model_of_second_type: matching_second) ] excluded = [ - ModelWithManyAssociations.create!(title: 'stuvwx', models_of_first_type: [ - FirstAssociatedModel.create!(title: 'abcdef') + ModelWithManyAssociations.create!(title: "stuvwx", models_of_first_type: [ + FirstAssociatedModel.create!(title: "abcdef") ]), - ModelWithManyAssociations.create!(title: 'qwerty', model_of_second_type: unmatching_second) + ModelWithManyAssociations.create!(title: "qwerty", model_of_second_type: unmatching_second) ] - results = ModelWithManyAssociations.with_associated('foo bar') + results = ModelWithManyAssociations.with_associated("foo bar") expect(results.map(&:title)).to match_array(included.map(&:title)) excluded.each { |object| expect(results).not_to include(object) } end @@ -211,58 +211,58 @@ context "when on the same table" do with_model :DoublyAssociatedModel do table do |t| - t.string 'title' - t.belongs_to 'ModelWithDoubleAssociation', index: false - t.belongs_to 'ModelWithDoubleAssociation_again', index: false + t.string "title" + t.belongs_to "ModelWithDoubleAssociation", index: false + t.belongs_to "ModelWithDoubleAssociation_again", index: false end end with_model :ModelWithDoubleAssociation do table do |t| - t.string 'title' + t.string "title" end model do include PgSearch::Model has_many :things, - class_name: 'DoublyAssociatedModel', - foreign_key: 'ModelWithDoubleAssociation_id' + class_name: "DoublyAssociatedModel", + foreign_key: "ModelWithDoubleAssociation_id" has_many :thingamabobs, - class_name: 'DoublyAssociatedModel', - foreign_key: 'ModelWithDoubleAssociation_again_id' + class_name: "DoublyAssociatedModel", + foreign_key: "ModelWithDoubleAssociation_again_id" pg_search_scope :with_associated, against: :title, - associated_against: { things: :title, thingamabobs: :title } + associated_against: {things: :title, thingamabobs: :title} end end it "returns rows that match the query in either its own columns or the columns of the associated model" do included = [ - ModelWithDoubleAssociation.create!(title: 'abcdef', things: [ - DoublyAssociatedModel.create!(title: 'foo'), - DoublyAssociatedModel.create!(title: 'bar') + ModelWithDoubleAssociation.create!(title: "abcdef", things: [ + DoublyAssociatedModel.create!(title: "foo"), + DoublyAssociatedModel.create!(title: "bar") ]), - ModelWithDoubleAssociation.create!(title: 'ghijkl', things: [ - DoublyAssociatedModel.create!(title: 'foo bar'), - DoublyAssociatedModel.create!(title: 'mnopqr') + ModelWithDoubleAssociation.create!(title: "ghijkl", things: [ + DoublyAssociatedModel.create!(title: "foo bar"), + DoublyAssociatedModel.create!(title: "mnopqr") ]), - ModelWithDoubleAssociation.create!(title: 'foo bar'), - ModelWithDoubleAssociation.create!(title: 'qwerty', thingamabobs: [ + ModelWithDoubleAssociation.create!(title: "foo bar"), + ModelWithDoubleAssociation.create!(title: "qwerty", thingamabobs: [ DoublyAssociatedModel.create!(title: "foo bar") ]) ] excluded = [ - ModelWithDoubleAssociation.create!(title: 'stuvwx', things: [ - DoublyAssociatedModel.create!(title: 'abcdef') + ModelWithDoubleAssociation.create!(title: "stuvwx", things: [ + DoublyAssociatedModel.create!(title: "abcdef") ]), - ModelWithDoubleAssociation.create!(title: 'qwerty', thingamabobs: [ + ModelWithDoubleAssociation.create!(title: "qwerty", thingamabobs: [ DoublyAssociatedModel.create!(title: "uiop") ]) ] - results = ModelWithDoubleAssociation.with_associated('foo bar') + results = ModelWithDoubleAssociation.with_associated("foo bar") expect(results.map(&:title)).to match_array(included.map(&:title)) excluded.each { |object| expect(results).not_to include(object) } end @@ -272,21 +272,21 @@ context "when against multiple attributes on one association" do with_model :AssociatedModel do table do |t| - t.string 'title' - t.text 'author' + t.string "title" + t.text "author" end end with_model :ModelWithAssociation do table do |t| - t.belongs_to 'another_model', index: false + t.belongs_to "another_model", index: false end model do include PgSearch::Model - belongs_to :another_model, class_name: 'AssociatedModel' + belongs_to :another_model, class_name: "AssociatedModel" - pg_search_scope :with_associated, associated_against: { another_model: %i[title author] } + pg_search_scope :with_associated, associated_against: {another_model: %i[title author]} end end @@ -314,7 +314,7 @@ ) ] - results = ModelWithAssociation.with_associated('foo bar') + results = ModelWithAssociation.with_associated("foo bar") expect(results.to_sql.scan("INNER JOIN #{AssociatedModel.quoted_table_name}").length).to eq(1) included.each { |object| expect(results).to include(object) } @@ -325,21 +325,21 @@ context "when against non-text columns" do with_model :AssociatedModel do table do |t| - t.integer 'number' + t.integer "number" end end with_model :Model do table do |t| - t.integer 'number' - t.belongs_to 'another_model', index: false + t.integer "number" + t.belongs_to "another_model", index: false end model do include PgSearch::Model - belongs_to :another_model, class_name: 'AssociatedModel' + belongs_to :another_model, class_name: "AssociatedModel" - pg_search_scope :with_associated, associated_against: { another_model: :number } + pg_search_scope :with_associated, associated_against: {another_model: :number} end end @@ -353,7 +353,7 @@ Model.create!(number: 123) ] - results = Model.with_associated('123') + results = Model.with_associated("123") expect(results.map(&:number)).to match_array(included.map(&:number)) expect(results).not_to include(excluded) end @@ -384,10 +384,10 @@ # https://github.com/Casecommons/pg_search/issues/14 it "supports queries with periods" do - included = Parent.create!(name: 'bar.foo') - excluded = Parent.create!(name: 'foo.bar') + included = Parent.create!(name: "bar.foo") + excluded = Parent.create!(name: "foo.bar") - results = Parent.search_name('bar.foo').includes(:children) + results = Parent.search_name("bar.foo").includes(:children) results.to_a expect(results).to include(included) @@ -477,7 +477,7 @@ Position.create!(company_id: company.id, title: "penn 1") ] - results = company.positions.search('teller 1') + results = company.positions.search("teller 1") expect(results).to include(*included) expect(results).not_to include(*excluded) diff --git a/spec/integration/pg_search_spec.rb b/spec/integration/pg_search_spec.rb index 64a86d7e..b19ff0e8 100644 --- a/spec/integration/pg_search_spec.rb +++ b/spec/integration/pg_search_spec.rb @@ -6,10 +6,10 @@ describe "an Active Record model which includes PgSearch" do with_model :ModelWithPgSearch do table do |t| - t.string 'title' - t.text 'content' - t.integer 'parent_model_id' - t.integer 'importance' + t.string "title" + t.text "content" + t.integer "parent_model_id" + t.integer "importance" end model do @@ -39,26 +39,38 @@ context "when passed a lambda" do it "builds a dynamic scope" do ModelWithPgSearch.pg_search_scope :search_title_or_content, - lambda { |query, pick_content| - { - query: query.gsub("-remove-", ""), - against: pick_content ? :content : :title - } - } + lambda { |query, pick_content| + { + query: query.gsub("-remove-", ""), + against: pick_content ? :content : :title + } + } - included = ModelWithPgSearch.create!(title: 'foo', content: 'bar') - excluded = ModelWithPgSearch.create!(title: 'bar', content: 'foo') + included = ModelWithPgSearch.create!(title: "foo", content: "bar") + ModelWithPgSearch.create!(title: "bar", content: "foo") - expect(ModelWithPgSearch.search_title_or_content('fo-remove-o', false)).to eq([included]) - expect(ModelWithPgSearch.search_title_or_content('b-remove-ar', true)).to eq([included]) + expect(ModelWithPgSearch.search_title_or_content("fo-remove-o", false)).to eq([included]) + expect(ModelWithPgSearch.search_title_or_content("b-remove-ar", true)).to eq([included]) + end + end + + context "when passed an invalid argument" do + it "builds a dynamic scope" do + expect { + ModelWithPgSearch.pg_search_scope :search_title_or_content, :some_symbol + }.to( + raise_exception(ArgumentError).with_message( + "pg_search_scope expects a Hash or Proc" + ) + ) end end context "when an unknown option is passed in" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_option, - against: :content, - foo: :bar + against: :content, + foo: :bar expect { ModelWithPgSearch.with_unknown_option("foo") @@ -68,7 +80,7 @@ context "with a lambda" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_option, - ->(*) { { against: :content, foo: :bar } } + ->(*) { {against: :content, foo: :bar} } expect { ModelWithPgSearch.with_unknown_option("foo") @@ -80,8 +92,8 @@ context "when an unknown :using is passed" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_using, - against: :content, - using: :foo + against: :content, + using: :foo expect { ModelWithPgSearch.with_unknown_using("foo") @@ -91,7 +103,7 @@ context "with a lambda" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_using, - ->(*) { { against: :content, using: :foo } } + ->(*) { {against: :content, using: :foo} } expect { ModelWithPgSearch.with_unknown_using("foo") @@ -103,8 +115,8 @@ context "when an unknown :ignoring is passed" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_ignoring, - against: :content, - ignoring: :foo + against: :content, + ignoring: :foo expect { ModelWithPgSearch.with_unknown_ignoring("foo") @@ -114,7 +126,7 @@ context "with a lambda" do it "raises an exception when invoked" do ModelWithPgSearch.pg_search_scope :with_unknown_ignoring, - ->(*) { { against: :content, ignoring: :foo } } + ->(*) { {against: :content, ignoring: :foo} } expect { ModelWithPgSearch.with_unknown_ignoring("foo") @@ -168,15 +180,15 @@ context "when chained after a select() scope" do it "honors the select" do - included = ModelWithPgSearch.create!(content: 'foo', title: 'bar') - excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo') + included = ModelWithPgSearch.create!(content: "foo", title: "bar") + excluded = ModelWithPgSearch.create!(content: "bar", title: "foo") - results = ModelWithPgSearch.select('id, title').search_content('foo') + results = ModelWithPgSearch.select("id, title").search_content("foo") expect(results).to include(included) expect(results).not_to include(excluded) - expect(results.first.attributes.key?('content')).to be false + expect(results.first.attributes.key?("content")).to be false expect(results.select { |record| record.title == "bar" }).to eq [included] expect(results.reject { |record| record.title == "bar" }).to be_empty @@ -185,15 +197,15 @@ context "when chained before a select() scope" do it "honors the select" do - included = ModelWithPgSearch.create!(content: 'foo', title: 'bar') - excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo') + included = ModelWithPgSearch.create!(content: "foo", title: "bar") + excluded = ModelWithPgSearch.create!(content: "bar", title: "foo") - results = ModelWithPgSearch.search_content('foo').select('id, title') + results = ModelWithPgSearch.search_content("foo").select("id, title") expect(results).to include(included) expect(results).not_to include(excluded) - expect(results.first.attributes.key?('content')).to be false + expect(results.first.attributes.key?("content")).to be false expect(results.select { |record| record.title == "bar" }).to eq [included] expect(results.reject { |record| record.title == "bar" }).to be_empty @@ -202,15 +214,15 @@ context "when surrouned by select() scopes" do it "honors the select" do - included = ModelWithPgSearch.create!(content: 'foo', title: 'bar') - excluded = ModelWithPgSearch.create!(content: 'bar', title: 'foo') + included = ModelWithPgSearch.create!(content: "foo", title: "bar") + excluded = ModelWithPgSearch.create!(content: "bar", title: "foo") - results = ModelWithPgSearch.select('id').search_content('foo').select('title') + results = ModelWithPgSearch.select("id").search_content("foo").select("title") expect(results).to include(included) expect(results).not_to include(excluded) - expect(results.first.attributes.key?('content')).to be false + expect(results.first.attributes.key?("content")).to be false expect(results.select { |record| record.title == "bar" }).to eq [included] expect(results.reject { |record| record.title == "bar" }).to be_empty @@ -241,7 +253,7 @@ has_many :houses pg_search_scope :named, against: [:name] scope :with_house_in_city, lambda { |city| - joins(:houses).where(House.table_name.to_sym => { city: city }) + joins(:houses).where(House.table_name.to_sym => {city: city}) } scope :house_search_city, lambda { |query| joins(:houses).merge(House.search_city(query)) @@ -285,7 +297,7 @@ context "when chaining merged scopes" do it "does not raise an exception" do - relation = Person.named('foo').house_search_city('bar') + relation = Person.named("foo").house_search_city("bar") expect { relation.to_a }.not_to raise_error end @@ -298,49 +310,49 @@ end it "does not raise an exception" do - relation = ModelWithPgSearch.search_content('foo').search_title('bar') + relation = ModelWithPgSearch.search_content("foo").search_title("bar") expect { relation.to_a }.not_to raise_error end end it "returns an empty array when a blank query is passed in" do - ModelWithPgSearch.create!(content: 'foo') + ModelWithPgSearch.create!(content: "foo") - results = ModelWithPgSearch.search_content('') + results = ModelWithPgSearch.search_content("") expect(results).to eq([]) end it "returns rows where the column contains the term in the query" do - included = ModelWithPgSearch.create!(content: 'foo') - excluded = ModelWithPgSearch.create!(content: 'bar') + included = ModelWithPgSearch.create!(content: "foo") + excluded = ModelWithPgSearch.create!(content: "bar") - results = ModelWithPgSearch.search_content('foo') + results = ModelWithPgSearch.search_content("foo") expect(results).to include(included) expect(results).not_to include(excluded) end it "returns the correct count" do - ModelWithPgSearch.create!(content: 'foo') - ModelWithPgSearch.create!(content: 'bar') + ModelWithPgSearch.create!(content: "foo") + ModelWithPgSearch.create!(content: "bar") - results = ModelWithPgSearch.search_content('foo') + results = ModelWithPgSearch.search_content("foo") expect(results.count).to eq 1 end it "returns the correct count(:all)" do - ModelWithPgSearch.create!(content: 'foo') - ModelWithPgSearch.create!(content: 'bar') + ModelWithPgSearch.create!(content: "foo") + ModelWithPgSearch.create!(content: "bar") - results = ModelWithPgSearch.search_content('foo') + results = ModelWithPgSearch.search_content("foo") expect(results.count(:all)).to eq 1 end it "supports #select" do - record = ModelWithPgSearch.create!(content: 'foo') - other_record = ModelWithPgSearch.create!(content: 'bar') + record = ModelWithPgSearch.create!(content: "foo") + ModelWithPgSearch.create!(content: "bar") - records_with_only_id = ModelWithPgSearch.search_content('foo').select('id') + records_with_only_id = ModelWithPgSearch.search_content("foo").select("id") expect(records_with_only_id.length).to eq 1 returned_record = records_with_only_id.first @@ -349,36 +361,36 @@ end it "supports #pluck" do - record = ModelWithPgSearch.create!(content: 'foo') - other_record = ModelWithPgSearch.create!(content: 'bar') + record = ModelWithPgSearch.create!(content: "foo") + ModelWithPgSearch.create!(content: "bar") - ids = ModelWithPgSearch.search_content('foo').pluck('id') + ids = ModelWithPgSearch.search_content("foo").pluck("id") expect(ids).to eq [record.id] end it "supports adding where clauses using the pg_search.rank" do - once = ModelWithPgSearch.create!(content: 'foo bar') - twice = ModelWithPgSearch.create!(content: 'foo foo') + ModelWithPgSearch.create!(content: "foo bar") + twice = ModelWithPgSearch.create!(content: "foo foo") - records = ModelWithPgSearch.search_content('foo') - .where("#{PgSearch::Configuration.alias(ModelWithPgSearch.table_name)}.rank > 0.07") + records = ModelWithPgSearch.search_content("foo") + .where("#{PgSearch::Configuration.alias(ModelWithPgSearch.table_name)}.rank > 0.07") expect(records).to eq [twice] end it "returns rows where the column contains all the terms in the query in any order" do - included = [ModelWithPgSearch.create!(content: 'foo bar'), - ModelWithPgSearch.create!(content: 'bar foo')] - excluded = ModelWithPgSearch.create!(content: 'foo') + included = [ModelWithPgSearch.create!(content: "foo bar"), + ModelWithPgSearch.create!(content: "bar foo")] + excluded = ModelWithPgSearch.create!(content: "foo") - results = ModelWithPgSearch.search_content('foo bar') + results = ModelWithPgSearch.search_content("foo bar") expect(results).to match_array(included) expect(results).not_to include(excluded) end it "returns rows that match the query but not its case" do included = [ModelWithPgSearch.create!(content: "foo"), - ModelWithPgSearch.create!(content: "FOO")] + ModelWithPgSearch.create!(content: "FOO")] results = ModelWithPgSearch.search_content("Foo") expect(results).to match_array(included) @@ -397,8 +409,8 @@ end it "returns rows that match the query but not rows that are prefixed by the query" do - included = ModelWithPgSearch.create!(content: 'pre') - excluded = ModelWithPgSearch.create!(content: 'prefix') + included = ModelWithPgSearch.create!(content: "pre") + excluded = ModelWithPgSearch.create!(content: "prefix") results = ModelWithPgSearch.search_content("pre") expect(results).to eq([included]) @@ -407,36 +419,36 @@ it "returns rows that match the query exactly and not those that match the query when stemmed by the default english dictionary" do included = ModelWithPgSearch.create!(content: "jumped") - excluded = [ModelWithPgSearch.create!(content: "jump"), - ModelWithPgSearch.create!(content: "jumping")] + ModelWithPgSearch.create!(content: "jump") + ModelWithPgSearch.create!(content: "jumping") results = ModelWithPgSearch.search_content("jumped") expect(results).to eq([included]) end it "returns rows that match sorted by rank" do - loser = ModelWithPgSearch.create!(content: 'foo') - winner = ModelWithPgSearch.create!(content: 'foo foo') + loser = ModelWithPgSearch.create!(content: "foo") + winner = ModelWithPgSearch.create!(content: "foo foo") results = ModelWithPgSearch.search_content("foo").with_pg_search_rank expect(results[0].pg_search_rank).to be > results[1].pg_search_rank expect(results).to eq([winner, loser]) end - it 'preserves column selection when with_pg_search_rank is chained after a select()' do - loser = ModelWithPgSearch.create!(title: 'foo', content: 'bar') + it "preserves column selection when with_pg_search_rank is chained after a select()" do + ModelWithPgSearch.create!(title: "foo", content: "bar") - results = ModelWithPgSearch.search_content('bar').select(:content).with_pg_search_rank + results = ModelWithPgSearch.search_content("bar").select(:content).with_pg_search_rank expect(results.length).to be 1 - expect(results.first.as_json.keys).to contain_exactly('id', 'content', 'pg_search_rank') + expect(results.first.as_json.keys).to contain_exactly("id", "content", "pg_search_rank") end - it 'allows pg_search_rank along with a join' do + it "allows pg_search_rank along with a join" do parent_1 = ParentModel.create!(id: 98) parent_2 = ParentModel.create!(id: 99) - loser = ModelWithPgSearch.create!(content: 'foo', parent_model: parent_2) - winner = ModelWithPgSearch.create!(content: 'foo foo', parent_model: parent_1) + loser = ModelWithPgSearch.create!(content: "foo", parent_model: parent_2) + winner = ModelWithPgSearch.create!(content: "foo foo", parent_model: parent_1) results = ModelWithPgSearch.joins(:parent_model).merge(ParentModel.active).search_content("foo").with_pg_search_rank expect(results.map(&:id)).to eq [winner.id, loser.id] @@ -445,8 +457,8 @@ end it "returns results that match sorted by primary key for records that rank the same" do - sorted_results = [ModelWithPgSearch.create!(content: 'foo'), - ModelWithPgSearch.create!(content: 'foo')].sort_by(&:id) + sorted_results = [ModelWithPgSearch.create!(content: "foo"), + ModelWithPgSearch.create!(content: "foo")].sort_by(&:id) results = ModelWithPgSearch.search_content("foo") expect(results).to eq(sorted_results) @@ -454,16 +466,16 @@ it "returns results that match a query with multiple space-separated search terms" do included = [ - ModelWithPgSearch.create!(content: 'foo bar'), - ModelWithPgSearch.create!(content: 'bar foo'), - ModelWithPgSearch.create!(content: 'bar foo baz') + ModelWithPgSearch.create!(content: "foo bar"), + ModelWithPgSearch.create!(content: "bar foo"), + ModelWithPgSearch.create!(content: "bar foo baz") ] excluded = [ - ModelWithPgSearch.create!(content: 'foo'), - ModelWithPgSearch.create!(content: 'foo baz') + ModelWithPgSearch.create!(content: "foo"), + ModelWithPgSearch.create!(content: "foo baz") ] - results = ModelWithPgSearch.search_content('foo bar') + results = ModelWithPgSearch.search_content("foo bar") expect(results).to match_array(included) expect(results).not_to include(excluded) end @@ -493,7 +505,7 @@ # WARNING: searching timestamps is not something PostgreSQL # full-text search is good at. Use at your own risk. pg_search_scope :search_timestamps, - against: %i[created_at updated_at] + against: %i[created_at updated_at] end end @@ -514,15 +526,15 @@ it "returns rows whose columns contain all of the terms in the query across columns" do included = [ - ModelWithPgSearch.create!(title: 'foo', content: 'bar'), - ModelWithPgSearch.create!(title: 'bar', content: 'foo') + ModelWithPgSearch.create!(title: "foo", content: "bar"), + ModelWithPgSearch.create!(title: "bar", content: "foo") ] excluded = [ - ModelWithPgSearch.create!(title: 'foo', content: 'foo'), - ModelWithPgSearch.create!(title: 'bar', content: 'bar') + ModelWithPgSearch.create!(title: "foo", content: "foo"), + ModelWithPgSearch.create!(title: "bar", content: "bar") ] - results = ModelWithPgSearch.search_title_and_content('foo bar') + results = ModelWithPgSearch.search_title_and_content("foo bar") expect(results).to match_array(included) excluded.each do |result| @@ -531,17 +543,17 @@ end it "returns rows where at one column contains all of the terms in the query and another does not" do - in_title = ModelWithPgSearch.create!(title: 'foo', content: 'bar') - in_content = ModelWithPgSearch.create!(title: 'bar', content: 'foo') + in_title = ModelWithPgSearch.create!(title: "foo", content: "bar") + in_content = ModelWithPgSearch.create!(title: "bar", content: "foo") - results = ModelWithPgSearch.search_title_and_content('foo') - expect(results).to match_array([in_title, in_content]) + results = ModelWithPgSearch.search_title_and_content("foo") + expect(results).to contain_exactly(in_title, in_content) end # Searching with a NULL column will prevent any matches unless we coalesce it. it "returns rows where at one column contains all of the terms in the query and another is NULL" do - included = ModelWithPgSearch.create!(title: 'foo', content: nil) - results = ModelWithPgSearch.search_title_and_content('foo') + included = ModelWithPgSearch.create!(title: "foo", content: nil) + results = ModelWithPgSearch.search_title_and_content("foo") expect(results).to eq([included]) end end @@ -552,21 +564,21 @@ end it "returns rows where one searchable column and the query share enough trigrams" do - included = ModelWithPgSearch.create!(title: 'abcdefghijkl', content: nil) - results = ModelWithPgSearch.with_trigrams('cdefhijkl') + included = ModelWithPgSearch.create!(title: "abcdefghijkl", content: nil) + results = ModelWithPgSearch.with_trigrams("cdefhijkl") expect(results).to eq([included]) end it "returns rows where multiple searchable columns and the query share enough trigrams" do - included = ModelWithPgSearch.create!(title: 'abcdef', content: 'ghijkl') - results = ModelWithPgSearch.with_trigrams('cdefhijkl') + included = ModelWithPgSearch.create!(title: "abcdef", content: "ghijkl") + results = ModelWithPgSearch.with_trigrams("cdefhijkl") expect(results).to eq([included]) end context "when a threshold is specified" do before do - ModelWithPgSearch.pg_search_scope :with_strict_trigrams, against: %i[title content], using: { trigram: { threshold: 0.5 } } - ModelWithPgSearch.pg_search_scope :with_permissive_trigrams, against: %i[title content], using: { trigram: { threshold: 0.1 } } + ModelWithPgSearch.pg_search_scope :with_strict_trigrams, against: %i[title content], using: {trigram: {threshold: 0.5}} + ModelWithPgSearch.pg_search_scope :with_permissive_trigrams, against: %i[title content], using: {trigram: {threshold: 0.1}} end it "uses the threshold in the trigram expression" do @@ -591,16 +603,16 @@ context "when using tsearch" do before do ModelWithPgSearch.pg_search_scope :search_title_with_prefixes, - against: :title, - using: { - tsearch: { prefix: true } - } + against: :title, + using: { + tsearch: {prefix: true} + } end context "with prefix: true" do it "returns rows that match the query and that are prefixed by the query" do - included = ModelWithPgSearch.create!(title: 'prefix') - excluded = ModelWithPgSearch.create!(title: 'postfix') + included = ModelWithPgSearch.create!(title: "prefix") + excluded = ModelWithPgSearch.create!(title: "postfix") results = ModelWithPgSearch.search_title_with_prefixes("pre") expect(results).to eq([included]) @@ -608,8 +620,8 @@ end it "returns rows that match the query when the query has a hyphen" do - included = ModelWithPgSearch.create!(title: 'foo-bar') - excluded = ModelWithPgSearch.create!(title: 'foo bar') + included = ModelWithPgSearch.create!(title: "foo-bar") + excluded = ModelWithPgSearch.create!(title: "foo bar") results = ModelWithPgSearch.search_title_with_prefixes("foo-bar") expect(results).to include(included) @@ -620,16 +632,16 @@ context "with the english dictionary" do before do ModelWithPgSearch.pg_search_scope :search_content_with_english, - against: :content, - using: { - tsearch: { dictionary: :english } - } + against: :content, + using: { + tsearch: {dictionary: :english} + } end it "returns rows that match the query when stemmed by the english dictionary" do included = [ModelWithPgSearch.create!(content: "jump"), - ModelWithPgSearch.create!(content: "jumped"), - ModelWithPgSearch.create!(content: "jumping")] + ModelWithPgSearch.create!(content: "jumped"), + ModelWithPgSearch.create!(content: "jumping")] results = ModelWithPgSearch.search_content_with_english("jump") expect(results).to match_array(included) @@ -639,14 +651,14 @@ describe "highlighting" do before do ["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name| - ModelWithPgSearch.create! title: 'Just a title', content: name + ModelWithPgSearch.create! title: "Just a title", content: name end end context "with highlight turned on" do before do ModelWithPgSearch.pg_search_scope :search_content, - against: :content + against: :content end it "adds a #pg_search_highlight method to each returned model record" do @@ -661,31 +673,31 @@ expect(result.pg_search_highlight).to eq("Won't Let You Down") end - it 'preserves column selection when with_pg_search_highlight is chained after a select()' do + it "preserves column selection when with_pg_search_highlight is chained after a select()" do result = ModelWithPgSearch.search_content("Let").select(:content).with_pg_search_highlight.first - expect(result.as_json.keys).to contain_exactly('id', 'content', 'pg_search_highlight') + expect(result.as_json.keys).to contain_exactly("id", "content", "pg_search_highlight") end end context "with custom highlighting options" do before do - ModelWithPgSearch.create! content: "#{'text ' * 2}Let #{'text ' * 2}Let #{'text ' * 2}" + ModelWithPgSearch.create! content: "#{"text " * 2}Let #{"text " * 2}Let #{"text " * 2}" ModelWithPgSearch.pg_search_scope :search_content, - against: :content, - using: { - tsearch: { - highlight: { - StartSel: '', - StopSel: '', - FragmentDelimiter: '', - MaxFragments: 2, - MaxWords: 2, - MinWords: 1 - } - } - } + against: :content, + using: { + tsearch: { + highlight: { + StartSel: '', + StopSel: "", + FragmentDelimiter: '', + MaxFragments: 2, + MaxWords: 2, + MinWords: 1 + } + } + } end it "applies the options to the excerpts" do @@ -714,10 +726,10 @@ context "with a normalization specified" do before do ModelWithPgSearch.pg_search_scope :search_content_with_normalization, - against: :content, - using: { - tsearch: { normalization: 2 } - } + against: :content, + using: { + tsearch: {normalization: 2} + } end it "ranks the results for documents with less text higher" do @@ -731,8 +743,8 @@ context "with no normalization" do before do ModelWithPgSearch.pg_search_scope :search_content_without_normalization, - against: :content, - using: :tsearch + against: :content, + using: :tsearch end it "ranks the results equally" do @@ -747,14 +759,14 @@ context "when against columns ranked with arrays" do before do ModelWithPgSearch.pg_search_scope :search_weighted_by_array_of_arrays, - against: [[:content, 'B'], [:title, 'A']] + against: [[:content, "B"], [:title, "A"]] end it "returns results sorted by weighted rank" do - loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo') - winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar') + loser = ModelWithPgSearch.create!(title: "bar", content: "foo") + winner = ModelWithPgSearch.create!(title: "foo", content: "bar") - results = ModelWithPgSearch.search_weighted_by_array_of_arrays('foo').with_pg_search_rank + results = ModelWithPgSearch.search_weighted_by_array_of_arrays("foo").with_pg_search_rank expect(results[0].pg_search_rank).to be > results[1].pg_search_rank expect(results).to eq([winner, loser]) end @@ -763,14 +775,14 @@ context "when against columns ranked with a hash" do before do ModelWithPgSearch.pg_search_scope :search_weighted_by_hash, - against: { content: 'B', title: 'A' } + against: {content: "B", title: "A"} end it "returns results sorted by weighted rank" do - loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo') - winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar') + loser = ModelWithPgSearch.create!(title: "bar", content: "foo") + winner = ModelWithPgSearch.create!(title: "foo", content: "bar") - results = ModelWithPgSearch.search_weighted_by_hash('foo').with_pg_search_rank + results = ModelWithPgSearch.search_weighted_by_hash("foo").with_pg_search_rank expect(results[0].pg_search_rank).to be > results[1].pg_search_rank expect(results).to eq([winner, loser]) end @@ -779,14 +791,14 @@ context "when against columns of which only some are ranked" do before do ModelWithPgSearch.pg_search_scope :search_weighted, - against: [:content, [:title, 'A']] + against: [:content, [:title, "A"]] end it "returns results sorted by weighted rank using an implied low rank for unranked columns" do - loser = ModelWithPgSearch.create!(title: 'bar', content: 'foo') - winner = ModelWithPgSearch.create!(title: 'foo', content: 'bar') + loser = ModelWithPgSearch.create!(title: "bar", content: "foo") + winner = ModelWithPgSearch.create!(title: "foo", content: "bar") - results = ModelWithPgSearch.search_weighted('foo').with_pg_search_rank + results = ModelWithPgSearch.search_weighted("foo").with_pg_search_rank expect(results[0].pg_search_rank).to be > results[1].pg_search_rank expect(results).to eq([winner, loser]) end @@ -795,17 +807,17 @@ context "when searching any_word option" do before do ModelWithPgSearch.pg_search_scope :search_title_with_any_word, - against: :title, - using: { - tsearch: { any_word: true } - } + against: :title, + using: { + tsearch: {any_word: true} + } ModelWithPgSearch.pg_search_scope :search_title_with_all_words, - against: :title + against: :title end it "returns all results containing any word in their title" do - numbers = %w[one two three four].map { |number| ModelWithPgSearch.create!(title: number) } + %w[one two three four].map { |number| ModelWithPgSearch.create!(title: number) } results = ModelWithPgSearch.search_title_with_any_word("one two three four") @@ -820,10 +832,10 @@ context "with :negation" do before do ModelWithPgSearch.pg_search_scope :search_with_negation, - against: :title, - using: { - tsearch: { negation: true } - } + against: :title, + using: { + tsearch: {negation: true} + } end it "doesn't return results that contain terms prepended with '!'" do @@ -847,10 +859,10 @@ context "without :negation" do before do ModelWithPgSearch.pg_search_scope :search_without_negation, - against: :title, - using: { - tsearch: {} - } + against: :title, + using: { + tsearch: {} + } end it "return results that contain terms prepended with '!'" do @@ -873,38 +885,37 @@ context "when using dmetaphone" do before do ModelWithPgSearch.pg_search_scope :with_dmetaphones, - against: %i[title content], - using: :dmetaphone + against: %i[title content], + using: :dmetaphone end it "returns rows where one searchable column and the query share enough dmetaphones" do - included = ModelWithPgSearch.create!(title: 'Geoff', content: nil) - excluded = ModelWithPgSearch.create!(title: 'Bob', content: nil) - results = ModelWithPgSearch.with_dmetaphones('Jeff') + included = ModelWithPgSearch.create!(title: "Geoff", content: nil) + ModelWithPgSearch.create!(title: "Bob", content: nil) + results = ModelWithPgSearch.with_dmetaphones("Jeff") expect(results).to eq([included]) end it "returns rows where multiple searchable columns and the query share enough dmetaphones" do - included = ModelWithPgSearch.create!(title: 'Geoff', content: 'George') - excluded = ModelWithPgSearch.create!(title: 'Bob', content: 'Jones') - results = ModelWithPgSearch.with_dmetaphones('Jeff Jorge') + included = ModelWithPgSearch.create!(title: "Geoff", content: "George") + ModelWithPgSearch.create!(title: "Bob", content: "Jones") + results = ModelWithPgSearch.with_dmetaphones("Jeff Jorge") expect(results).to eq([included]) end it "returns rows that match dmetaphones that are English stopwords" do - included = ModelWithPgSearch.create!(title: 'White', content: nil) - excluded = ModelWithPgSearch.create!(title: 'Black', content: nil) - results = ModelWithPgSearch.with_dmetaphones('Wight') + included = ModelWithPgSearch.create!(title: "White", content: nil) + ModelWithPgSearch.create!(title: "Black", content: nil) + results = ModelWithPgSearch.with_dmetaphones("Wight") expect(results).to eq([included]) end it "can handle terms that do not have a dmetaphone equivalent" do - term_with_blank_metaphone = "w" - - included = ModelWithPgSearch.create!(title: 'White', content: nil) - excluded = ModelWithPgSearch.create!(title: 'Black', content: nil) + included = ModelWithPgSearch.create!(title: "White", content: nil) + ModelWithPgSearch.create!(title: "Black", content: nil) - results = ModelWithPgSearch.with_dmetaphones('Wight W') + # "W" does not have a dmetaphone equivalent + results = ModelWithPgSearch.with_dmetaphones("Wight W") expect(results).to eq([included]) end end @@ -912,35 +923,35 @@ context "when using multiple features" do before do ModelWithPgSearch.pg_search_scope :with_tsearch, - against: :title, - using: [ - [:tsearch, { dictionary: 'english' }] - ] + against: :title, + using: [ + [:tsearch, {dictionary: "english"}] + ] ModelWithPgSearch.pg_search_scope :with_trigram, - against: :title, - using: :trigram + against: :title, + using: :trigram ModelWithPgSearch.pg_search_scope :with_trigram_and_ignoring_accents, - against: :title, - ignoring: :accents, - using: :trigram + against: :title, + ignoring: :accents, + using: :trigram ModelWithPgSearch.pg_search_scope :with_tsearch_and_trigram, - against: :title, - using: [ - [:tsearch, { dictionary: 'english' }], - :trigram - ] + against: :title, + using: [ + [:tsearch, {dictionary: "english"}], + :trigram + ] ModelWithPgSearch.pg_search_scope :complex_search, - against: %i[content title], - ignoring: :accents, - using: { - tsearch: { dictionary: 'english' }, - dmetaphone: {}, - trigram: {} - } + against: %i[content title], + ignoring: :accents, + using: { + tsearch: {dictionary: "english"}, + dmetaphone: {}, + trigram: {} + } end it "returns rows that match using any of the features" do @@ -982,13 +993,13 @@ end context "with feature-specific configuration" do - let(:tsearch_config) { { dictionary: 'english' } } - let(:trigram_config) { { foo: 'bar' } } + let(:tsearch_config) { {dictionary: "english"} } + let(:trigram_config) { {foo: "bar"} } before do ModelWithPgSearch.pg_search_scope :with_tsearch_and_trigram_using_hash, - against: :title, - using: { tsearch: tsearch_config, trigram: trigram_config } + against: :title, + using: {tsearch: tsearch_config, trigram: trigram_config} end it "passes the custom configuration down to the specified feature" do @@ -1029,8 +1040,8 @@ with_model :Post do table do |t| - t.text 'content' - t.tsvector 'content_tsvector' + t.text "content" + t.tsvector "content_tsvector" end model do @@ -1039,8 +1050,8 @@ end end - let!(:expected) { Post.create!(content: 'phooey') } - let!(:unexpected) { Post.create!(content: 'longcat is looooooooong') } + let!(:expected) { Post.create!(content: "phooey") } + let!(:unexpected) { Post.create!(content: "longcat is looooooooong") } before do ActiveRecord::Base.connection.execute <<~SQL.squish @@ -1048,17 +1059,17 @@ SET content_tsvector = to_tsvector('english'::regconfig, #{Post.quoted_table_name}."content") SQL - expected.comments.create(body: 'commentone') - unexpected.comments.create(body: 'commentwo') + expected.comments.create(body: "commentone") + unexpected.comments.create(body: "commentwo") Post.pg_search_scope :search_by_content_with_tsvector, - associated_against: { comments: [:body] }, - using: { - tsearch: { - tsvector_column: 'content_tsvector', - dictionary: 'english' - } - } + associated_against: {comments: [:body]}, + using: { + tsearch: { + tsvector_column: "content_tsvector", + dictionary: "english" + } + } end it "finds by the tsvector column" do @@ -1069,7 +1080,7 @@ expect(Post.search_by_content_with_tsvector("commentone").map(&:id)).to eq([expected.id]) end - it 'finds by a combination of the two' do + it "finds by a combination of the two" do expect(Post.search_by_content_with_tsvector("phooey commentone").map(&:id)).to eq([expected.id]) end end @@ -1183,7 +1194,7 @@ expected = "setweight(coalesce(#{ModelWithTsvector.quoted_table_name}.\"title_tsvector\", ''), 'A') || "\ "coalesce(#{ModelWithTsvector.quoted_table_name}.\"content_tsvector\", '') || "\ "setweight(coalesce(#{ModelWithTsvector.quoted_table_name}.\"message_tsvector\", ''), 'B')" - + expect(ModelWithTsvector.search_by_multiple_tsvector_columns("something").to_sql).to include(expected) end end @@ -1228,7 +1239,7 @@ with_model :AnotherModel do table do |t| t.string :content_tsvector # the type of the column doesn't matter - t.belongs_to :model_with_tsvector + t.belongs_to :model_with_tsvector, index: { name: :boopilooopi_id_ix } end end @@ -1248,7 +1259,7 @@ table do |t| t.text 'content' t.tsvector 'content_tsvector' - t.belongs_to :model_with_tsvector + t.belongs_to :model_with_tsvector, index: { name: :hoopaboopa_id_ix } end end @@ -1318,7 +1329,7 @@ table do |t| t.text 'content' t.tsvector 'content_tsvector' - t.belongs_to :model_with_tsvector + t.belongs_to :model_with_tsvector, index: { name: :anoterh_model_id_ix } end end @@ -1385,17 +1396,17 @@ include PgSearch::Model pg_search_scope :search_by_multiple_tsvector_columns, - against: ['content', 'message'], - using: { - tsearch: { - tsvector_column: ['content_tsvector', 'message_tsvector'], - dictionary: 'english' - } - } + against: ["content", "message"], + using: { + tsearch: { + tsvector_column: ["content_tsvector", "message_tsvector"], + dictionary: "english" + } + } end end - it 'concats tsvector columns' do + it "concats tsvector columns" do expected = "#{ModelWithTsvector.quoted_table_name}.\"content_tsvector\" || " \ "#{ModelWithTsvector.quoted_table_name}.\"message_tsvector\"" @@ -1406,17 +1417,17 @@ context "when using a tsvector column with" do with_model :ModelWithTsvector do table do |t| - t.text 'content' - t.tsvector 'content_tsvector' + t.text "content" + t.tsvector "content_tsvector" end model { include PgSearch::Model } end - let!(:expected) { ModelWithTsvector.create!(content: 'tiling is grouty') } + let!(:expected) { ModelWithTsvector.create!(content: "tiling is grouty") } before do - ModelWithTsvector.create!(content: 'longcat is looooooooong') + ModelWithTsvector.create!(content: "longcat is looooooooong") ActiveRecord::Base.connection.execute <<~SQL.squish UPDATE #{ModelWithTsvector.quoted_table_name} @@ -1424,13 +1435,13 @@ SQL ModelWithTsvector.pg_search_scope :search_by_content_with_tsvector, - against: :content, - using: { - tsearch: { - tsvector_column: 'content_tsvector', - dictionary: 'english' - } - } + against: :content, + using: { + tsearch: { + tsvector_column: "content_tsvector", + dictionary: "english" + } + } end it "does not use to_tsvector in the query" do @@ -1464,8 +1475,8 @@ context "when ignoring accents" do before do ModelWithPgSearch.pg_search_scope :search_title_without_accents, - against: :title, - ignoring: :accents + against: :title, + ignoring: :accents end it "returns rows that match the query but not its accents" do @@ -1484,7 +1495,7 @@ let(:results) { ModelWithPgSearch.search_title_without_accents(term) } before do - ModelWithPgSearch.create!(title: 'FooBar') + ModelWithPgSearch.create!(title: "FooBar") end it "does not create an erroneous tsquery expression" do @@ -1496,25 +1507,25 @@ context "when passed a :ranked_by expression" do before do ModelWithPgSearch.pg_search_scope :search_content_with_default_rank, - against: :content + against: :content ModelWithPgSearch.pg_search_scope :search_content_with_importance_as_rank, - against: :content, - ranked_by: "importance" + against: :content, + ranked_by: "importance" ModelWithPgSearch.pg_search_scope :search_content_with_importance_as_rank_multiplier, - against: :content, - ranked_by: ":tsearch * importance" + against: :content, + ranked_by: ":tsearch * importance" end it "returns records with a rank attribute equal to the :ranked_by expression" do - ModelWithPgSearch.create!(content: 'foo', importance: 10) + ModelWithPgSearch.create!(content: "foo", importance: 10) results = ModelWithPgSearch.search_content_with_importance_as_rank("foo").with_pg_search_rank expect(results.first.pg_search_rank).to eq(10) end it "substitutes :tsearch with the tsearch rank expression in the :ranked_by expression" do - ModelWithPgSearch.create!(content: 'foo', importance: 10) + ModelWithPgSearch.create!(content: "foo", importance: 10) tsearch_result = ModelWithPgSearch.search_content_with_default_rank("foo").with_pg_search_rank.first @@ -1523,8 +1534,8 @@ multiplied_result = ModelWithPgSearch.search_content_with_importance_as_rank_multiplier("foo") - .with_pg_search_rank - .first + .with_pg_search_rank + .first multiplied_rank = multiplied_result.pg_search_rank @@ -1533,9 +1544,9 @@ it "returns results in descending order of the value of the rank expression" do records = [ - ModelWithPgSearch.create!(content: 'foo', importance: 1), - ModelWithPgSearch.create!(content: 'foo', importance: 3), - ModelWithPgSearch.create!(content: 'foo', importance: 2) + ModelWithPgSearch.create!(content: "foo", importance: 1), + ModelWithPgSearch.create!(content: "foo", importance: 3), + ModelWithPgSearch.create!(content: "foo", importance: 2) ] results = ModelWithPgSearch.search_content_with_importance_as_rank("foo") @@ -1548,32 +1559,32 @@ before do ModelWithPgSearch.pg_search_scope scope_name, - against: :content, - ranked_by: ":#{feature}" + against: :content, + ranked_by: ":#{feature}" - ModelWithPgSearch.create!(content: 'foo') + ModelWithPgSearch.create!(content: "foo") end context "when .with_pg_search_rank is chained after" do specify "its results respond to #pg_search_rank" do - result = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank.first + result = ModelWithPgSearch.send(scope_name, "foo").with_pg_search_rank.first expect(result).to respond_to(:pg_search_rank) end it "returns the rank when #pg_search_rank is called on a result" do - results = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank + results = ModelWithPgSearch.send(scope_name, "foo").with_pg_search_rank expect(results.first.pg_search_rank).to be_a Float end end context "when .with_pg_search_rank is not chained after" do specify "its results do not respond to #pg_search_rank" do - result = ModelWithPgSearch.send(scope_name, 'foo').first + result = ModelWithPgSearch.send(scope_name, "foo").first expect(result).not_to respond_to(:pg_search_rank) end it "raises PgSearch::PgSearchRankNotSelected when #pg_search_rank is called on a result" do - result = ModelWithPgSearch.send(scope_name, 'foo').first + result = ModelWithPgSearch.send(scope_name, "foo").first expect { result.pg_search_rank }.to raise_exception(PgSearch::PgSearchRankNotSelected) @@ -1585,14 +1596,14 @@ context "when using the tsearch ranking algorithm" do it "sorts results by the tsearch rank" do ModelWithPgSearch.pg_search_scope :search_content_ranked_by_tsearch, - using: :tsearch, - against: :content, - ranked_by: ":tsearch" + using: :tsearch, + against: :content, + ranked_by: ":tsearch" - once = ModelWithPgSearch.create!(content: 'foo bar') - twice = ModelWithPgSearch.create!(content: 'foo foo') + once = ModelWithPgSearch.create!(content: "foo bar") + twice = ModelWithPgSearch.create!(content: "foo foo") - results = ModelWithPgSearch.search_content_ranked_by_tsearch('foo') + results = ModelWithPgSearch.search_content_ranked_by_tsearch("foo") expect(results.find_index(twice)).to be < results.find_index(once) end end @@ -1600,14 +1611,14 @@ context "when using the trigram ranking algorithm" do it "sorts results by the trigram rank" do ModelWithPgSearch.pg_search_scope :search_content_ranked_by_trigram, - using: :trigram, - against: :content, - ranked_by: ":trigram" + using: :trigram, + against: :content, + ranked_by: ":trigram" - close = ModelWithPgSearch.create!(content: 'abcdef') - exact = ModelWithPgSearch.create!(content: 'abc') + close = ModelWithPgSearch.create!(content: "abcdef") + exact = ModelWithPgSearch.create!(content: "abc") - results = ModelWithPgSearch.search_content_ranked_by_trigram('abc') + results = ModelWithPgSearch.search_content_ranked_by_trigram("abc") expect(results.find_index(exact)).to be < results.find_index(close) end end @@ -1615,14 +1626,14 @@ context "when using the dmetaphone ranking algorithm" do it "sorts results by the dmetaphone rank" do ModelWithPgSearch.pg_search_scope :search_content_ranked_by_dmetaphone, - using: :dmetaphone, - against: :content, - ranked_by: ":dmetaphone" + using: :dmetaphone, + against: :content, + ranked_by: ":dmetaphone" - once = ModelWithPgSearch.create!(content: 'Phoo Bar') - twice = ModelWithPgSearch.create!(content: 'Phoo Fu') + once = ModelWithPgSearch.create!(content: "Phoo Bar") + twice = ModelWithPgSearch.create!(content: "Phoo Fu") - results = ModelWithPgSearch.search_content_ranked_by_dmetaphone('foo') + results = ModelWithPgSearch.search_content_ranked_by_dmetaphone("foo") expect(results.find_index(twice)).to be < results.find_index(once) end end @@ -1631,17 +1642,17 @@ context "when there is a sort only feature" do it "excludes that feature from the conditions, but uses it in the sorting" do ModelWithPgSearch.pg_search_scope :search_content_ranked_by_dmetaphone, - against: :content, - using: { - tsearch: { any_word: true, prefix: true }, - dmetaphone: { any_word: true, prefix: true, sort_only: true } - }, - ranked_by: ":tsearch + (0.5 * :dmetaphone)" + against: :content, + using: { + tsearch: {any_word: true, prefix: true}, + dmetaphone: {any_word: true, prefix: true, sort_only: true} + }, + ranked_by: ":tsearch + (0.5 * :dmetaphone)" exact = ModelWithPgSearch.create!(content: "ash hines") one_exact_one_close = ModelWithPgSearch.create!(content: "ash heinz") one_exact = ModelWithPgSearch.create!(content: "ash smith") - one_close = ModelWithPgSearch.create!(content: "leigh heinz") + ModelWithPgSearch.create!(content: "leigh heinz") results = ModelWithPgSearch.search_content_ranked_by_dmetaphone("ash hines") expect(results).to eq [exact, one_exact_one_close, one_exact] diff --git a/spec/integration/single_table_inheritance_spec.rb b/spec/integration/single_table_inheritance_spec.rb index 8a068152..e4cb8668 100644 --- a/spec/integration/single_table_inheritance_spec.rb +++ b/spec/integration/single_table_inheritance_spec.rb @@ -6,8 +6,8 @@ context "with the standard type column" do with_model :SuperclassModel do table do |t| - t.text 'content' - t.string 'type' + t.text "content" + t.string "type" end model do @@ -46,13 +46,13 @@ context "with a custom type column" do with_model :SuperclassModel do table do |t| - t.text 'content' - t.string 'custom_type' + t.text "content" + t.string "custom_type" end model do include PgSearch::Model - self.inheritance_column = 'custom_type' + self.inheritance_column = "custom_type" pg_search_scope :search_content, against: :content end end diff --git a/spec/lib/pg_search/configuration/association_spec.rb b/spec/lib/pg_search/configuration/association_spec.rb index 6c7d79d2..05725e69 100644 --- a/spec/lib/pg_search/configuration/association_spec.rb +++ b/spec/lib/pg_search/configuration/association_spec.rb @@ -1,222 +1,143 @@ +# frozen_string_literal: true + require "spec_helper" +# rubocop:disable RSpec/NestedGroups describe PgSearch::Configuration::Association do - context "through a belongs_to association" do - with_model :AssociatedModel do - table do |t| - t.string "title" - end + with_model :Avatar do + table do |t| + t.string :url + t.references :user end + end - with_model :Model do - table do |t| - t.string "title" - t.belongs_to :another_model - end - - model do - include PgSearch - belongs_to :another_model, :class_name => 'AssociatedModel' - - pg_search_scope :with_another, :associated_against => {:another_model => :title} - end - end - - let(:association) { described_class.new(Model, :another_model, :title) } - - describe "#table_name" do - it "returns the table name for the associated model" do - expect(association.table_name).to eq AssociatedModel.table_name - end + with_model :User do + table do |t| + t.string :name + t.belongs_to :site end - describe "#join" do - context "given any postgresql_version" do - let(:column_select) do - "\"#{association.table_name}\".\"title\"" - end - - let(:expected_sql) do - <<-EOS.gsub(/\s+/, ' ').strip - LEFT OUTER JOIN - (SELECT model_id AS id, - #{column_select} AS #{association.columns.first.alias} - FROM \"#{Model.table_name}\" - INNER JOIN \"#{association.table_name}\" - ON \"#{association.table_name}\".\"id\" = \"#{Model.table_name}\".\"another_model_id\") #{association.subselect_alias} - ON #{association.subselect_alias}.id = model_id - EOS - end - - it "returns the correct SQL join (v1)" do - allow(Model.connection).to receive(:postgresql_version).and_return(1) - expect(association.join("model_id")).to eq(expected_sql) - end + model do + include PgSearch::Model + has_one :avatar, class_name: "Avatar" + belongs_to :site - it "returns the correct SQL join (v100)" do - allow(Model.connection).to receive(:postgresql_version).and_return(100_000) - expect(association.join("model_id")).to eq(expected_sql) - end - end - end - - describe "#subselect_alias" do - it "returns a consistent string" do - subselect_alias = association.subselect_alias - expect(subselect_alias).to be_a String - expect(association.subselect_alias).to eq subselect_alias - end + pg_search_scope :with_avatar, associated_against: {avatar: :url} + pg_search_scope :with_site, associated_against: {site: :title} end end - context "through a has_one association" do - with_model :Model do - table do |t| - t.string "title" - end - - model do - include PgSearch - has_one :another_model, :class_name => 'AssociatedModel', foreign_key: 'primary_model_id' - - pg_search_scope :with_another, :associated_against => {:another_model => :title} - end + with_model :Site do + table do |t| + t.string :title end - with_model :AssociatedModel do - table do |t| - t.string "title" - t.belongs_to :primary_model - end + model do + include PgSearch::Model + has_many :users, class_name: "User" - model do - belongs_to :primary_model, :class_name => 'Model' - end + pg_search_scope :with_users, associated_against: {users: :name} end + end - let(:association) { described_class.new(Model, :another_model, :title) } + context "with has_one" do + let(:association) { described_class.new(User, :avatar, :url) } describe "#table_name" do it "returns the table name for the associated model" do - expect(association.table_name).to eq AssociatedModel.table_name + expect(association.table_name).to eq Avatar.table_name end end describe "#join" do - context "given any postgresql_version" do - let(:column_select) do - "\"#{association.table_name}\".\"title\"" - end - - let(:expected_sql) do - <<-EOS.gsub(/\s+/, ' ').strip - LEFT OUTER JOIN - (SELECT model_id AS id, - #{column_select} AS #{association.columns.first.alias} - FROM \"#{Model.table_name}\" - INNER JOIN \"#{association.table_name}\" - ON \"#{association.table_name}\".\"primary_model_id\" = \"#{Model.table_name}\".\"id\") #{association.subselect_alias} - ON #{association.subselect_alias}.id = model_id - EOS - end - - it "returns the correct SQL join (v1)" do - allow(Model.connection).to receive(:postgresql_version).and_return(1) - expect(association.join("model_id")).to eq(expected_sql) - end - - it "returns the correct SQL join (v100)" do - allow(Model.connection).to receive(:postgresql_version).and_return(100_000) - expect(association.join("model_id")).to eq(expected_sql) - end + let(:expected_sql) do + <<~SQL.squish + LEFT OUTER JOIN + (SELECT model_id AS id, + #{column_select} AS #{association.columns.first.alias} + FROM "#{User.table_name}" + INNER JOIN "#{association.table_name}" + ON "#{association.table_name}"."user_id" = "#{User.table_name}"."id") #{association.subselect_alias} + ON #{association.subselect_alias}.id = model_id + SQL + end + let(:column_select) do + "\"#{association.table_name}\".\"url\"::text" end - end - describe "#subselect_alias" do - it "returns a consistent string" do - subselect_alias = association.subselect_alias - expect(subselect_alias).to be_a String - expect(association.subselect_alias).to eq subselect_alias + it "returns the correct SQL join" do + expect(association.join("model_id")).to eq(expected_sql) end end end - context "through a has_many association" do - with_model :Model do - table do |t| - t.string "title" - end - - model do - include PgSearch - has_many :associated_models, :class_name => 'AssociatedModel', foreign_key: 'primary_model_id' + context "with belongs_to" do + let(:association) { described_class.new(User, :site, :title) } - pg_search_scope :with_another, :associated_against => {:another_model => :title} + describe "#table_name" do + it "returns the table name for the associated model" do + expect(association.table_name).to eq Site.table_name end end - with_model :AssociatedModel do - table do |t| - t.string "title" - t.belongs_to :primary_model + describe "#join" do + let(:expected_sql) do + <<~SQL.squish + LEFT OUTER JOIN + (SELECT model_id AS id, + #{column_select} AS #{association.columns.first.alias} + FROM "#{User.table_name}" + INNER JOIN "#{association.table_name}" + ON "#{association.table_name}"."id" = "#{User.table_name}"."site_id") #{association.subselect_alias} + ON #{association.subselect_alias}.id = model_id + SQL + end + let(:column_select) do + "\"#{association.table_name}\".\"title\"::text" end - model do - belongs_to :primary_model, :class_name => 'Model' + it "returns the correct SQL join" do + expect(association.join("model_id")).to eq(expected_sql) end end + end - let(:association) { described_class.new(Model, :associated_models, :title) } + context "with has_many" do + let(:association) { described_class.new(Site, :users, :name) } describe "#table_name" do it "returns the table name for the associated model" do - expect(association.table_name).to eq AssociatedModel.table_name + expect(association.table_name).to eq User.table_name end end describe "#join" do let(:expected_sql) do - <<-EOS.gsub(/\s+/, ' ').strip + <<~SQL.squish LEFT OUTER JOIN (SELECT model_id AS id, - #{column_select} AS #{association.columns.first.alias} - FROM \"#{Model.table_name}\" - INNER JOIN \"#{association.table_name}\" - ON \"#{association.table_name}\".\"primary_model_id\" = \"#{Model.table_name}\".\"id\" + string_agg("#{association.table_name}"."name"::text, ' ') AS #{association.columns.first.alias} + FROM "#{Site.table_name}" + INNER JOIN "#{association.table_name}" + ON "#{association.table_name}"."site_id" = "#{Site.table_name}"."id" GROUP BY model_id) #{association.subselect_alias} ON #{association.subselect_alias}.id = model_id - EOS + SQL end - context "given postgresql_version 0..90_000" do - let(:column_select) do - "array_to_string(array_agg(\"#{association.table_name}\".\"title\"::text), ' ')" - end - - it "returns the correct SQL join" do - allow(Model.connection).to receive(:postgresql_version).and_return(1) - expect(association.join("model_id")).to eq(expected_sql) - end + it "returns the correct SQL join" do + expect(association.join("model_id")).to eq(expected_sql) end - context "given any other postgresql_version" do - let(:column_select) do - "string_agg(\"#{association.table_name}\".\"title\"::text, ' ')" - end - - it "returns the correct SQL join" do - allow(Model.connection).to receive(:postgresql_version).and_return(100_000) - expect(association.join("model_id")).to eq(expected_sql) + describe "#subselect_alias" do + it "returns a consistent string" do + subselect_alias = association.subselect_alias + expect(subselect_alias).to be_a String + expect(association.subselect_alias).to eq subselect_alias end end end - - describe "#subselect_alias" do - it "returns a consistent string" do - subselect_alias = association.subselect_alias - expect(subselect_alias).to be_a String - expect(association.subselect_alias).to eq subselect_alias - end - end end end + +# rubocop:enable RSpec/NestedGroups diff --git a/spec/lib/pg_search/configuration/column_spec.rb b/spec/lib/pg_search/configuration/column_spec.rb index 50f56b1e..9e933713 100644 --- a/spec/lib/pg_search/configuration/column_spec.rb +++ b/spec/lib/pg_search/configuration/column_spec.rb @@ -7,6 +7,7 @@ with_model :Model do table do |t| t.string :name + t.json :object end end @@ -14,18 +15,29 @@ column = described_class.new("name", nil, Model) expect(column.full_name).to eq(%(#{Model.quoted_table_name}."name")) end + + it "returns nested json attributes" do + column = described_class.new(Arel.sql("object->>'name'"), nil, Model) + expect(column.full_name).to eq(%(object->>'name')) + end end describe "#to_sql" do with_model :Model do table do |t| t.string :name + t.json :object end end it "returns an expression that casts the column to text and coalesces it with an empty string" do column = described_class.new("name", nil, Model) - expect(column.to_sql).to eq(%{coalesce(#{Model.quoted_table_name}."name"::text, '')}) + expect(column.to_sql).to eq(%{coalesce((#{Model.quoted_table_name}."name")::text, '')}) + end + + it "returns an expression that casts the nested json attribute to text and coalesces it with an empty string" do + column = described_class.new(Arel.sql("object->>'name'"), nil, Model) + expect(column.to_sql).to eq(%{coalesce((object->>'name')::text, '')}) end end end diff --git a/spec/lib/pg_search/configuration/foreign_column_spec.rb b/spec/lib/pg_search/configuration/foreign_column_spec.rb index 105b8eba..3537a203 100644 --- a/spec/lib/pg_search/configuration/foreign_column_spec.rb +++ b/spec/lib/pg_search/configuration/foreign_column_spec.rb @@ -18,16 +18,16 @@ model do include PgSearch::Model - belongs_to :another_model, class_name: 'AssociatedModel' + belongs_to :another_model, class_name: "AssociatedModel" - pg_search_scope :with_another, associated_against: { another_model: :title } + pg_search_scope :with_another, associated_against: {another_model: :title} end end it "returns a consistent string" do association = PgSearch::Configuration::Association.new(Model, - :another_model, - :title) + :another_model, + :title) foreign_column = described_class.new("title", nil, Model, association) column_alias = foreign_column.alias diff --git a/spec/lib/pg_search/features/dmetaphone_spec.rb b/spec/lib/pg_search/features/dmetaphone_spec.rb index 995f76b4..8327619d 100644 --- a/spec/lib/pg_search/features/dmetaphone_spec.rb +++ b/spec/lib/pg_search/features/dmetaphone_spec.rb @@ -23,7 +23,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.rank.to_sql).to eq( - %{(ts_rank((to_tsvector('simple', pg_search_dmetaphone(coalesce(#{Model.quoted_table_name}."name"::text, ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce(#{Model.quoted_table_name}."content"::text, '')))), (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')), 0))} + %{(ts_rank((to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."name")::text, ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."content")::text, '')))), (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')), 0))} ) end end @@ -48,7 +48,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.conditions.to_sql).to eq( - %{((to_tsvector('simple', pg_search_dmetaphone(coalesce(#{Model.quoted_table_name}."name"::text, ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce(#{Model.quoted_table_name}."content"::text, '')))) @@ (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')))} + %{((to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."name")::text, ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."content")::text, '')))) @@ (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')))} ) end end diff --git a/spec/lib/pg_search/features/trigram_spec.rb b/spec/lib/pg_search/features/trigram_spec.rb index 995c5dae..5686043a 100644 --- a/spec/lib/pg_search/features/trigram_spec.rb +++ b/spec/lib/pg_search/features/trigram_spec.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require 'spec_helper' -require 'ostruct' +require "spec_helper" +require "ostruct" # rubocop:disable RSpec/MultipleMemoizedHelpers, RSpec/NestedGroups describe PgSearch::Features::Trigram do subject(:feature) { described_class.new(query, options, columns, Model, normalizer) } - let(:query) { 'lolwut' } + let(:query) { "lolwut" } let(:options) { {} } let(:columns) { [ @@ -16,13 +16,13 @@ ] } let(:normalizer) { PgSearch::Normalizer.new(config) } - let(:config) { OpenStruct.new(ignore: []) } # rubocop:disable Style/OpenStructUse + let(:config) { OpenStruct.new(ignore: []) } let(:coalesced_columns) do <<~SQL.squish - coalesce(#{Model.quoted_table_name}."name"::text, '') + coalesce((#{Model.quoted_table_name}."name")::text, '') || ' ' - || coalesce(#{Model.quoted_table_name}."content"::text, '') + || coalesce((#{Model.quoted_table_name}."content")::text, '') SQL end @@ -33,15 +33,15 @@ end end - describe 'conditions' do - it 'escapes the search document and query' do + describe "conditions" do + it "escapes the search document and query" do config.ignore = [] expect(feature.conditions.to_sql).to eq("('#{query}' % (#{coalesced_columns}))") end - context 'when searching by word_similarity' do + context "when searching by word_similarity" do let(:options) do - { word_similarity: true } + {word_similarity: true} end it 'uses the "<%" operator when searching by word_similarity' do @@ -50,17 +50,17 @@ end end - context 'when ignoring accents' do - it 'escapes the search document and query, but not the accent function' do + context "when ignoring accents" do + it "escapes the search document and query, but not the accent function" do config.ignore = [:accents] expect(feature.conditions.to_sql).to eq("(unaccent('#{query}') % (unaccent(#{coalesced_columns})))") end end - context 'when a threshold is specified' do - context 'when searching by similarity' do + context "when a threshold is specified" do + context "when searching by similarity" do let(:options) do - { threshold: 0.5 } + {threshold: 0.5} end it 'uses a minimum similarity expression instead of the "%" operator' do @@ -70,9 +70,9 @@ end end - context 'when searching by word_similarity' do + context "when searching by word_similarity" do let(:options) do - { threshold: 0.5, word_similarity: true } + {threshold: 0.5, word_similarity: true} end it 'uses a minimum similarity expression instead of the "<%" operator' do @@ -83,28 +83,28 @@ end end - context 'when only certain columns are selected' do - context 'with one column' do - let(:options) { { only: :name } } + context "when only certain columns are selected" do + context "with one column" do + let(:options) { {only: :name} } - it 'only searches against the select column' do - coalesced_column = "coalesce(#{Model.quoted_table_name}.\"name\"::text, '')" + it "only searches against the select column" do + coalesced_column = "coalesce((#{Model.quoted_table_name}.\"name\")::text, '')" expect(feature.conditions.to_sql).to eq("('#{query}' % (#{coalesced_column}))") end end - context 'with multiple columns' do - let(:options) { { only: %i[name content] } } + context "with multiple columns" do + let(:options) { {only: %i[name content]} } - it 'concatenates when multiples columns are selected' do + it "concatenates when multiples columns are selected" do expect(feature.conditions.to_sql).to eq("('#{query}' % (#{coalesced_columns}))") end end end end - describe '#rank' do - it 'returns an expression using the similarity() function' do + describe "#rank" do + it "returns an expression using the similarity() function" do expect(feature.rank.to_sql).to eq("(similarity('#{query}', (#{coalesced_columns})))") end end diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index d702d69c..8b7456c1 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -24,7 +24,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.rank.to_sql).to eq( - %{(ts_rank((to_tsvector('simple', coalesce(#{Model.quoted_table_name}."name"::text, '')) || to_tsvector('simple', coalesce(#{Model.quoted_table_name}."content"::text, ''))), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 0))} + %{(ts_rank((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")::text, '')) || to_tsvector('simple', coalesce((#{Model.quoted_table_name}."content")::text, ''))), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 0))} ) end @@ -35,7 +35,7 @@ PgSearch::Configuration::Column.new(:name, nil, Model), PgSearch::Configuration::Column.new(:content, nil, Model) ] - options = { tsvector_column: :my_tsvector, normalization: 2 } + options = {tsvector_column: :my_tsvector, normalization: 2} config = instance_double(PgSearch::Configuration, :config, ignore: []) normalizer = PgSearch::Normalizer.new(config) @@ -67,7 +67,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.conditions.to_sql).to eq( - %{((to_tsvector('simple', coalesce(#{Model.quoted_table_name}."name"::text, '')) || to_tsvector('simple', coalesce(#{Model.quoted_table_name}."content"::text, ''))) @@ (to_tsquery('simple', ''' ' || 'query' || ' ''')))} + %{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")::text, '')) || to_tsvector('simple', coalesce((#{Model.quoted_table_name}."content")::text, ''))) @@ (to_tsquery('simple', ''' ' || 'query' || ' ''')))} ) end @@ -78,13 +78,13 @@ PgSearch::Configuration::Column.new(:name, nil, Model), PgSearch::Configuration::Column.new(:content, nil, Model) ] - options = { negation: true } + options = {negation: true} config = instance_double(PgSearch::Configuration, :config, ignore: []) normalizer = PgSearch::Normalizer.new(config) feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.conditions.to_sql).to eq( - %{((to_tsvector('simple', coalesce(#{Model.quoted_table_name}."name"::text, '')) || to_tsvector('simple', coalesce(#{Model.quoted_table_name}."content"::text, ''))) @@ (to_tsquery('simple', '!' || ''' ' || 'query' || ' ''')))} + %{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")::text, '')) || to_tsvector('simple', coalesce((#{Model.quoted_table_name}."content")::text, ''))) @@ (to_tsquery('simple', '!' || ''' ' || 'query' || ' ''')))} ) end end @@ -96,25 +96,25 @@ PgSearch::Configuration::Column.new(:name, nil, Model), PgSearch::Configuration::Column.new(:content, nil, Model) ] - options = { negation: false } + options = {negation: false} config = instance_double(PgSearch::Configuration, :config, ignore: []) normalizer = PgSearch::Normalizer.new(config) feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.conditions.to_sql).to eq( - %{((to_tsvector('simple', coalesce(#{Model.quoted_table_name}."name"::text, '')) || to_tsvector('simple', coalesce(#{Model.quoted_table_name}."content"::text, ''))) @@ (to_tsquery('simple', ''' ' || '!query' || ' ''')))} + %{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")::text, '')) || to_tsvector('simple', coalesce((#{Model.quoted_table_name}."content")::text, ''))) @@ (to_tsquery('simple', ''' ' || '!query' || ' ''')))} ) end end context "when options[:tsvector_column] is a string" do - it 'uses the tsvector column' do + it "uses the tsvector column" do query = "query" columns = [ PgSearch::Configuration::Column.new(:name, nil, Model), PgSearch::Configuration::Column.new(:content, nil, Model) ] - options = { tsvector_column: "my_tsvector" } + options = {tsvector_column: "my_tsvector"} config = instance_double(PgSearch::Configuration, :config, ignore: []) normalizer = PgSearch::Normalizer.new(config) @@ -126,13 +126,13 @@ end context "when options[:tsvector_column] is an array of strings" do - it 'uses the tsvector column' do + it "uses the tsvector column" do query = "query" columns = [ PgSearch::Configuration::Column.new(:name, nil, Model), PgSearch::Configuration::Column.new(:content, nil, Model) ] - options = { tsvector_column: ["tsvector1", "tsvector2"] } + options = {tsvector_column: ["tsvector1", "tsvector2"]} config = instance_double(PgSearch::Configuration, :config, ignore: []) normalizer = PgSearch::Normalizer.new(config) @@ -181,13 +181,13 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.highlight.to_sql).to eq( - "(ts_headline('simple', (coalesce(#{Model.quoted_table_name}.\"name\"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))" + "(ts_headline('simple', (coalesce((#{Model.quoted_table_name}.\"name\")::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))" ) end context "when options[:dictionary] is passed" do # rubocop:disable RSpec/ExampleLength - it 'uses the provided dictionary' do + it "uses the provided dictionary" do query = "query" columns = [ PgSearch::Configuration::Column.new(:name, nil, Model), @@ -206,7 +206,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) - expected_sql = %{(ts_headline('spanish', (coalesce(#{Model.quoted_table_name}."name"::text, '') || ' ' || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('spanish', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = ""'))} + expected_sql = %{(ts_headline('spanish', (coalesce((#{Model.quoted_table_name}."name")::text, '') || ' ' || coalesce((#{Model.quoted_table_name}."content")::text, '')), (to_tsquery('spanish', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = ""'))} expect(feature.highlight.to_sql).to eq(expected_sql) end @@ -223,13 +223,13 @@ options = { highlight: { StartSel: '', - StopSel: '', + StopSel: "", MaxWords: 123, MinWords: 456, ShortWord: 4, HighlightAll: true, MaxFragments: 3, - FragmentDelimiter: '…' + FragmentDelimiter: "…" } } @@ -238,7 +238,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) - expected_sql = %{(ts_headline('simple', (coalesce(#{Model.quoted_table_name}."name"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = "", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = TRUE'))} + expected_sql = %{(ts_headline('simple', (coalesce((#{Model.quoted_table_name}."name")::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = "", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = TRUE'))} expect(feature.highlight.to_sql).to eq(expected_sql) end @@ -253,13 +253,13 @@ options = { highlight: { start_sel: '', - stop_sel: '', + stop_sel: "", max_words: 123, min_words: 456, short_word: 4, highlight_all: false, max_fragments: 3, - fragment_delimiter: '…' + fragment_delimiter: "…" } } @@ -269,7 +269,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) highlight_sql = ActiveSupport::Deprecation.silence { feature.highlight.to_sql } - expected_sql = %{(ts_headline('simple', (coalesce(#{Model.quoted_table_name}."name"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = "", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = FALSE'))} + expected_sql = %{(ts_headline('simple', (coalesce((#{Model.quoted_table_name}."name")::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "", StopSel = "", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = FALSE'))} expect(highlight_sql).to eq(expected_sql) end diff --git a/spec/lib/pg_search/multisearch/rebuilder_spec.rb b/spec/lib/pg_search/multisearch/rebuilder_spec.rb index 5a462644..58a48c02 100644 --- a/spec/lib/pg_search/multisearch/rebuilder_spec.rb +++ b/spec/lib/pg_search/multisearch/rebuilder_spec.rb @@ -6,10 +6,10 @@ describe PgSearch::Multisearch::Rebuilder do with_table "pg_search_documents", &DOCUMENTS_SCHEMA - describe 'when initialized with a model that is not multisearchable' do + describe "when initialized with a model that is not multisearchable" do with_model :not_multisearchable - it 'raises an exception' do + it "raises an exception" do expect { described_class.new(NotMultisearchable) }.to raise_exception( @@ -253,7 +253,7 @@ def foo model do include PgSearch::Model multisearchable against: :name, - additional_attributes: ->(obj) { { additional_attribute_column: "#{obj.class}::#{obj.id}" } } + additional_attributes: ->(obj) { {additional_attribute_column: "#{obj.class}::#{obj.id}"} } end end diff --git a/spec/lib/pg_search/multisearchable_spec.rb b/spec/lib/pg_search/multisearchable_spec.rb index c0814e58..c155741b 100644 --- a/spec/lib/pg_search/multisearchable_spec.rb +++ b/spec/lib/pg_search/multisearchable_spec.rb @@ -149,7 +149,7 @@ end context "when searching against a single column" do - let(:multisearchable_options) { { against: :some_content } } + let(:multisearchable_options) { {against: :some_content} } let(:text) { "foo bar" } before do @@ -159,7 +159,7 @@ record.save end - describe '#content' do + describe "#content" do subject { super().pg_search_document.content } it { is_expected.to eq(text) } @@ -167,17 +167,17 @@ end context "when searching against multiple columns" do - let(:multisearchable_options) { { against: %i[attr_1 attr_2] } } + let(:multisearchable_options) { {against: %i[attr_1 attr_2]} } before do without_partial_double_verification do - allow(record).to receive(:attr_1).and_return('1') - allow(record).to receive(:attr_2).and_return('2') + allow(record).to receive(:attr_1).and_return("1") + allow(record).to receive(:attr_2).and_return("2") end record.save end - describe '#content' do + describe "#content" do subject { super().pg_search_document.content } it { is_expected.to eq("1 2") } @@ -195,7 +195,7 @@ end context "when searching against a single column" do - let(:multisearchable_options) { { against: :some_content } } + let(:multisearchable_options) { {against: :some_content} } let(:text) { "foo bar" } before do @@ -205,7 +205,7 @@ record.save end - describe '#content' do + describe "#content" do subject { super().pg_search_document.content } it { is_expected.to eq(text) } @@ -213,17 +213,17 @@ end context "when searching against multiple columns" do - let(:multisearchable_options) { { against: %i[attr_1 attr_2] } } + let(:multisearchable_options) { {against: %i[attr_1 attr_2]} } before do without_partial_double_verification do - allow(record).to receive(:attr_1).and_return('1') - allow(record).to receive(:attr_2).and_return('2') + allow(record).to receive(:attr_1).and_return("1") + allow(record).to receive(:attr_2).and_return("2") end record.save end - describe '#content' do + describe "#content" do subject { super().pg_search_document.content } it { is_expected.to eq("1 2") } @@ -234,7 +234,7 @@ let(:multisearchable_options) do { additional_attributes: lambda do |record| - { foo: record.bar } + {foo: record.bar} end } end @@ -247,7 +247,7 @@ record.save expect(record) .to have_received(:create_pg_search_document) - .with(content: '', foo: text) + .with(content: "", foo: text) end end end @@ -269,7 +269,7 @@ record.save expect(record) .to have_received(:create_pg_search_document) - .with(content: '') + .with(content: "") end end diff --git a/spec/lib/pg_search_spec.rb b/spec/lib/pg_search_spec.rb index 80cca230..6a178f2d 100644 --- a/spec/lib/pg_search_spec.rb +++ b/spec/lib/pg_search_spec.rb @@ -49,7 +49,7 @@ def clear_searchable_cache end end - let!(:soundalike_record) { MultisearchableModel.create!(title: 'foning') } + let!(:soundalike_record) { MultisearchableModel.create!(title: "foning") } let(:query) { "Phoning" } it { is_expected.to include(soundalike_record) } @@ -65,9 +65,9 @@ def clear_searchable_cache allow(described_class).to receive(:multisearch_options) do lambda do |query, soundalike| if soundalike - { using: :dmetaphone, query: query } + {using: :dmetaphone, query: query} else - { query: query } + {query: query} end end end @@ -83,7 +83,7 @@ def clear_searchable_cache end end - let!(:soundalike_record) { MultisearchableModel.create!(title: 'foning') } + let!(:soundalike_record) { MultisearchableModel.create!(title: "foning") } let(:query) { "Phoning" } context "with soundalike true" do @@ -103,8 +103,8 @@ def clear_searchable_cache context "with standard type column" do with_model :SuperclassModel do table do |t| - t.text 'content' - t.string 'type' + t.text "content" + t.string "type" end end @@ -191,12 +191,12 @@ def clear_searchable_cache context "with custom type column" do with_model :SuperclassModel do table do |t| - t.text 'content' - t.string 'inherit' + t.text "content" + t.string "inherit" end model do - self.inheritance_column = 'inherit' + self.inheritance_column = "inherit" end end @@ -254,7 +254,7 @@ def clear_searchable_cache multisearch_enabled_inside = described_class.multisearch_enabled? raise end - rescue StandardError + rescue end multisearch_enabled_after = described_class.multisearch_enabled? diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e25191aa..0498959b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true -require 'warning' +require "warning" # Ignore Ruby 2.7 warnings from Active Record Warning.ignore :keyword_separation # https://github.com/grodowski/undercover#setting-up-required-lcov-reporting -require 'simplecov' -require 'simplecov-lcov' +require "simplecov" +require "simplecov-lcov" SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter SimpleCov.start do add_filter(%r{^/spec/}) enable_coverage(:branch) end -require 'undercover' +require "undercover" require "bundler/setup" require "pg_search" @@ -29,11 +29,11 @@ mocks.verify_partial_doubles = true end - config.example_status_persistence_file_path = 'tmp/examples.txt' + config.example_status_persistence_file_path = "tmp/examples.txt" end -require 'support/database' -require 'support/with_model' +require "support/database" +require "support/with_model" DOCUMENTS_SCHEMA = lambda do |t| t.belongs_to :searchable, polymorphic: true, index: true diff --git a/spec/support/database.rb b/spec/support/database.rb index 11d3ef00..23ada4e5 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -10,10 +10,10 @@ end begin - connection_options = { adapter: 'postgresql', database: 'pg_search_test', min_messages: 'warning' } + connection_options = {adapter: "postgresql", database: "pg_search_test", min_messages: "warning"} if ENV["CI"] - connection_options[:username] = 'postgres' - connection_options[:password] = 'postgres' + connection_options[:username] = "postgres" + connection_options[:password] = "postgres" end ActiveRecord::Base.establish_connection(connection_options) connection = ActiveRecord::Base.connection @@ -40,7 +40,7 @@ def install_extension(name) return unless extension.none? connection.execute "CREATE EXTENSION #{name};" -rescue StandardError => e +rescue => e at_exit do puts "-" * 80 puts "Please install the #{name} extension" @@ -52,7 +52,7 @@ def install_extension(name) def install_extension_if_missing(name, query, expected_result) result = ActiveRecord::Base.connection.select_value(query) raise "Unexpected output for #{query}: #{result.inspect}" unless result.casecmp(expected_result).zero? -rescue StandardError +rescue install_extension(name) end @@ -62,10 +62,9 @@ def install_extension_if_missing(name, query, expected_result) def load_sql(filename) connection = ActiveRecord::Base.connection - file_contents = File.read(File.join(File.dirname(__FILE__), '..', '..', 'sql', filename)) + file_contents = File.read(File.join(File.dirname(__FILE__), "..", "..", "sql", filename)) connection.execute(file_contents) end load_sql("dmetaphone.sql") - -load_sql("tsvector_agg.sql") unless connection.select_value("SELECT 1 FROM pg_catalog.pg_aggregate WHERE aggfnoid = 'tsvector_agg'::REGPROC") == "1" +load_sql("tsvector_agg.sql") diff --git a/sql/tsvector_agg.sql b/sql/tsvector_agg.sql index 6848d975..c5221685 100644 --- a/sql/tsvector_agg.sql +++ b/sql/tsvector_agg.sql @@ -4,7 +4,7 @@ $function$ BEGIN END; $function$ LANGUAGE plpgsql; -CREATE AGGREGATE tsvector_agg(tsvector) ( +CREATE OR REPLACE AGGREGATE tsvector_agg(tsvector) ( SFUNC=concat_tsvectors, STYPE=tsvector, INITCOND=''