From 8d6bcfad92c20f51b4fbca80a5791b0f0649398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Anselmo?= Date: Mon, 4 May 2015 09:56:28 +0200 Subject: [PATCH 1/2] Adds support for tsvector_column in associated_against (pg_search_scope) Adds alternative option syntax for tsvector_column. Adds migration for tsvector aggregation. Adds support for tsvector columns in associated models. Missing: More tests and documentation. --- lib/pg_search/configuration/association.rb | 11 +- lib/pg_search/configuration/column.rb | 15 +- lib/pg_search/features/tsearch.rb | 13 +- .../associated_against_tsvector_generator.rb | 13 + ..._against_tsvector_support_functions.rb.erb | 17 + lib/pg_search/railtie.rb | 2 + spec/integration/pg_search_spec.rb | 305 ++++++++++++++++++ .../configuration/association_spec.rb | 246 +++++++++----- spec/lib/pg_search/features/tsearch_spec.rb | 34 ++ spec/support/database.rb | 2 + sql/tsvector_agg.sql | 11 + sql/uninstall_tsvector_agg.sql | 3 + 12 files changed, 581 insertions(+), 91 deletions(-) create mode 100644 lib/pg_search/migration/associated_against_tsvector_generator.rb create mode 100644 lib/pg_search/migration/templates/add_pg_search_associated_against_tsvector_support_functions.rb.erb create mode 100644 sql/tsvector_agg.sql create mode 100644 sql/uninstall_tsvector_agg.sql diff --git a/lib/pg_search/configuration/association.rb b/lib/pg_search/configuration/association.rb index 8946c194..d9c17108 100644 --- a/lib/pg_search/configuration/association.rb +++ b/lib/pg_search/configuration/association.rb @@ -39,7 +39,16 @@ def selects def selects_for_singular_association columns.map do |column| - "#{column.full_name}::text AS #{column.alias}" + 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 end.join(", ") end diff --git a/lib/pg_search/configuration/column.rb b/lib/pg_search/configuration/column.rb index ad1397bc..1cf9dedb 100644 --- a/lib/pg_search/configuration/column.rb +++ b/lib/pg_search/configuration/column.rb @@ -5,12 +5,17 @@ module PgSearch class Configuration class Column - attr_reader :weight, :name + attr_reader :weight, :tsvector_column, :name def initialize(column_name, weight, model) @name = column_name.to_s @column_name = column_name.to_s - @weight = weight + if weight.is_a?(Hash) + @weight = weight[:weight] + @tsvector_column = weight[:tsvector_column] + else + @weight = weight + end @model = model @connection = model.connection end @@ -20,7 +25,11 @@ def full_name end def to_sql - "coalesce(#{expression}::text, '')" + if tsvector_column + "coalesce(#{expression}, '')" + else + "coalesce(#{expression}::text, '')" + end end private diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 365eddad..a0e5e89f 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -192,10 +192,15 @@ def columns_to_use end def column_to_tsvector(search_column) - tsvector = Arel::Nodes::NamedFunction.new( - "to_tsvector", - [dictionary, Arel.sql(normalize(search_column.to_sql))] - ).to_sql + 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 if search_column.weight.nil? tsvector diff --git a/lib/pg_search/migration/associated_against_tsvector_generator.rb b/lib/pg_search/migration/associated_against_tsvector_generator.rb new file mode 100644 index 00000000..9af543c9 --- /dev/null +++ b/lib/pg_search/migration/associated_against_tsvector_generator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'pg_search/migration/generator' + +module PgSearch + module Migration + class AssociatedAgainstTsvectorGenerator < Generator + def migration_name + 'add_pg_search_associated_against_tsvector_support_functions' + end + end + end +end diff --git a/lib/pg_search/migration/templates/add_pg_search_associated_against_tsvector_support_functions.rb.erb b/lib/pg_search/migration/templates/add_pg_search_associated_against_tsvector_support_functions.rb.erb new file mode 100644 index 00000000..8e0c0fb3 --- /dev/null +++ b/lib/pg_search/migration/templates/add_pg_search_associated_against_tsvector_support_functions.rb.erb @@ -0,0 +1,17 @@ +class AddPgSearchAssociatedAgainstTsvectorSupportFunctions < ActiveRecord::Migration + def self.up + say_with_time("Adding tsvector support functions for pg_search :associated_against") do + execute <<-'SQL' +<%= read_sql_file "tsvector_agg" %> + SQL + end + end + + def self.down + say_with_time("Dropping tsvector support functions for pg_search :associated_against") do + execute <<-'SQL' +<%= read_sql_file "uninstall_tsvector_agg" %> + SQL + end + end +end diff --git a/lib/pg_search/railtie.rb b/lib/pg_search/railtie.rb index a6e9c0b2..36b3a4d3 100644 --- a/lib/pg_search/railtie.rb +++ b/lib/pg_search/railtie.rb @@ -9,6 +9,8 @@ class Railtie < Rails::Railtie generators do require "pg_search/migration/multisearch_generator" require "pg_search/migration/dmetaphone_generator" + require "pg_search/migration/associated_against_generator" + require "pg_search/migration/associated_against_tsvector_generator" end end end diff --git a/spec/integration/pg_search_spec.rb b/spec/integration/pg_search_spec.rb index 7005316d..64a86d7e 100644 --- a/spec/integration/pg_search_spec.rb +++ b/spec/integration/pg_search_spec.rb @@ -1074,6 +1074,311 @@ end end + context "with new tsvector column syntax" do + context "when using a tsvector column and an association" do + with_model :Comment do + table do |t| + t.integer :post_id + t.string :body + end + + model do + belongs_to :post + end + end + + with_model :Post do + table do |t| + t.text 'content' + t.tsvector 'content_tsvector' + end + + model do + include PgSearch::Model + has_many :comments + end + end + + let!(:expected) { Post.create!(content: 'phooey') } + let!(:unexpected) { Post.create!(content: 'longcat is looooooooong') } + + before do + ActiveRecord::Base.connection.execute <<~SQL.squish + UPDATE #{Post.quoted_table_name} + SET content_tsvector = to_tsvector('english'::regconfig, #{Post.quoted_table_name}."content") + SQL + + expected.comments.create(body: 'commentone') + unexpected.comments.create(body: 'commentwo') + + Post.pg_search_scope :search_by_content_with_tsvector, + against: { content_tsvector: { tsvector_column: true } }, + associated_against: { comments: [:body] }, + using: { + tsearch: { + dictionary: 'english' + } + } + end + + it "finds by the tsvector column" do + expect(Post.search_by_content_with_tsvector("phooey").map(&:id)).to eq([expected.id]) + end + + it "finds by the associated record" do + expect(Post.search_by_content_with_tsvector("commentone").map(&:id)).to eq([expected.id]) + end + + 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 + + context "when using multiple tsvector columns" do + with_model :ModelWithTsvector do + model do + include PgSearch::Model + + pg_search_scope :search_by_multiple_tsvector_columns, + against: { + content_tsvector: { tsvector_column: true }, + message_tsvector: { tsvector_column: true } + }, + using: { + tsearch: { + dictionary: 'english' + } + } + end + end + + it "concats tsvector columns" do + expected = "coalesce(#{ModelWithTsvector.quoted_table_name}.\"content_tsvector\", '') || "\ + "coalesce(#{ModelWithTsvector.quoted_table_name}.\"message_tsvector\", '')" + + expect(ModelWithTsvector.search_by_multiple_tsvector_columns("something").to_sql).to include(expected) + end + end + + context 'using multiple tsvector columns with weight' do + with_model :ModelWithTsvector do + model do + include PgSearch::Model + + pg_search_scope :search_by_multiple_tsvector_columns, + against: { + title_tsvector: { tsvector_column: true, weight: 'A' }, + content_tsvector: { tsvector_column: true }, + message_tsvector: { tsvector_column: true, weight: 'B' } + }, + using: { + tsearch: { + dictionary: 'english' + } + } + end + end + + it 'concats tsvector columns' do + 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 + + context "using a tsvector column with" do + with_model :ModelWithTsvector do + table do |t| + t.text 'content' + t.tsvector 'content_tsvector' + end + + model { include PgSearch::Model } + end + + let!(:expected) { ModelWithTsvector.create!(content: 'tiling is grouty') } + let!(:unexpected) { ModelWithTsvector.create!(content: 'longcat is looooooooong') } + + before do + ActiveRecord::Base.connection.execute <<~SQL.squish + UPDATE #{ModelWithTsvector.quoted_table_name} + SET content_tsvector = to_tsvector('english'::regconfig, #{ModelWithTsvector.quoted_table_name}."content") + SQL + + ModelWithTsvector.pg_search_scope :search_by_content_with_tsvector, + against: { content_tsvector: { tsvector_column: true } }, + using: { + tsearch: { + dictionary: 'english' + } + } + end + + it "does not use to_tsvector in the query" do + expect(ModelWithTsvector.search_by_content_with_tsvector("tiles").to_sql).not_to match(/to_tsvector/) + end + + it "finds the expected result" do + expect(ModelWithTsvector.search_by_content_with_tsvector("tiles").map(&:id)).to eq([expected.id]) + end + + context "when joining to a table with a column of the same name" do + 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 + end + end + + before do + ModelWithTsvector.has_many :another_models + end + + it "refers to the tsvector column in the query unambiguously" do + expect { + ModelWithTsvector.joins(:another_models).search_by_content_with_tsvector("test").to_a + }.not_to raise_exception + end + end + + context "when joining to a table with a tsvector column" do + with_model :AnotherModelWithTsvector do + table do |t| + t.text 'content' + t.tsvector 'content_tsvector' + t.belongs_to :model_with_tsvector + end + end + + before do + ModelWithTsvector.has_many :another_model_with_tsvectors + + ModelWithTsvector.pg_search_scope :search_by_content_with_tsvectors, + against: { content_tsvector: { tsvector_column: true } }, + associated_against: { + another_model_with_tsvectors: { + content_tsvector: { tsvector_column: true } + } + }, + using: { + tsearch: { + dictionary: 'english' + } + } + end + + it "refers to each tsvector column in the query unambiguously" do + expect { + ModelWithTsvector.joins(:another_model_with_tsvectors).search_by_content_with_tsvectors("test").to_a + }.not_to raise_exception + end + + it "does not use to_tsvector in the query" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("tiles").to_sql).not_to match(/to_tsvector/) + end + + it "finds the expected result" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("tiles").map(&:id)).to eq([expected.id]) + end + + describe 'with associated records' do + before do + AnotherModelWithTsvector.create!(content: 'monkeys like bananas', model_with_tsvector_id: expected.id) + + ActiveRecord::Base.connection.execute <<~SQL.squish + UPDATE #{AnotherModelWithTsvector.quoted_table_name} + SET content_tsvector = to_tsvector('english'::regconfig, #{AnotherModelWithTsvector.quoted_table_name}."content") + SQL + end + + it "does not use to_tsvector in the query" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("bananas").to_sql).not_to match(/to_tsvector/) + end + + it "finds the expected result" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("bananas").map(&:id)).to eq([expected.id]) + end + end + end + end + + context "mixed with old syntax" do + with_model :ModelWithTsvector do + table do |t| + t.text 'content' + t.tsvector 'content_tsvector' + end + + model { include PgSearch::Model } + end + + with_model :AnotherModelWithTsvector do + table do |t| + t.text 'content' + t.tsvector 'content_tsvector' + t.belongs_to :model_with_tsvector + end + end + + let!(:expected) { ModelWithTsvector.create!(content: 'tiling is grouty') } + let!(:unexpected) { ModelWithTsvector.create!(content: 'longcat is looooooooong') } + + before do + ModelWithTsvector.has_many :another_model_with_tsvectors + + ModelWithTsvector.pg_search_scope :search_by_content_with_tsvectors, + against: :content, + associated_against: { + another_model_with_tsvectors: { + content_tsvector: { tsvector_column: true } + } + }, + using: { + tsearch: { + tsvector_column: 'content_tsvector', + dictionary: 'english' + } + } + + AnotherModelWithTsvector.create!(content: 'monkeys like bananas', model_with_tsvector_id: expected.id) + + ActiveRecord::Base.connection.execute <<~SQL.squish + UPDATE #{ModelWithTsvector.quoted_table_name} + SET content_tsvector = to_tsvector('english'::regconfig, #{ModelWithTsvector.quoted_table_name}."content") + SQL + + ActiveRecord::Base.connection.execute <<~SQL.squish + UPDATE #{AnotherModelWithTsvector.quoted_table_name} + SET content_tsvector = to_tsvector('english'::regconfig, #{AnotherModelWithTsvector.quoted_table_name}."content") + SQL + end + + it "refers to each tsvector column in the query unambiguously" do + expect { + ModelWithTsvector.joins(:another_model_with_tsvectors).search_by_content_with_tsvectors("test").to_a + }.not_to raise_exception + end + + it "does not use to_tsvector in the query" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("tiles").to_sql).not_to match(/to_tsvector/) + end + + it "finds the expected result" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("tiles").map(&:id)).to eq([expected.id]) + end + + it "does not use to_tsvector in the query" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("bananas").to_sql).not_to match(/to_tsvector/) + end + + it "finds the expected result" do + expect(ModelWithTsvector.search_by_content_with_tsvectors("bananas").map(&:id)).to eq([expected.id]) + end + end + end + context 'when using multiple tsvector columns' do with_model :ModelWithTsvector do model do diff --git a/spec/lib/pg_search/configuration/association_spec.rb b/spec/lib/pg_search/configuration/association_spec.rb index 90a08f50..6c7d79d2 100644 --- a/spec/lib/pg_search/configuration/association_spec.rb +++ b/spec/lib/pg_search/configuration/association_spec.rb @@ -1,142 +1,222 @@ -# frozen_string_literal: true - require "spec_helper" -# rubocop:disable RSpec/NestedGroups describe PgSearch::Configuration::Association do - with_model :Avatar do - table do |t| - t.string :url - t.references :user + context "through a belongs_to association" do + with_model :AssociatedModel do + table do |t| + t.string "title" + end end - end - with_model :User do - table do |t| - t.string :name - t.belongs_to :site + 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 - model do - include PgSearch::Model - has_one :avatar, class_name: "Avatar" - belongs_to :site + let(:association) { described_class.new(Model, :another_model, :title) } - pg_search_scope :with_avatar, associated_against: { avatar: :url } - pg_search_scope :with_site, associated_against: { site: :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 + 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 + + 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 end end - with_model :Site do - table do |t| - t.string :title + 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 end - model do - include PgSearch::Model - has_many :users, class_name: "User" + with_model :AssociatedModel do + table do |t| + t.string "title" + t.belongs_to :primary_model + end - pg_search_scope :with_users, associated_against: { users: :name } + model do + belongs_to :primary_model, :class_name => 'Model' + end end - end - context "with has_one" do - let(:association) { described_class.new(User, :avatar, :url) } + 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 Avatar.table_name + expect(association.table_name).to eq AssociatedModel.table_name end end 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}"."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" + 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 end + end - it "returns the correct SQL join" do - 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 - context "with belongs_to" do - let(:association) { described_class.new(User, :site, :title) } + 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' - describe "#table_name" do - it "returns the table name for the associated model" do - expect(association.table_name).to eq Site.table_name + pg_search_scope :with_another, :associated_against => {:another_model => :title} end end - 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" + with_model :AssociatedModel do + table do |t| + t.string "title" + t.belongs_to :primary_model end - it "returns the correct SQL join" do - expect(association.join("model_id")).to eq(expected_sql) + model do + belongs_to :primary_model, :class_name => 'Model' end end - end - context "with has_many" do - let(:association) { described_class.new(Site, :users, :name) } + let(:association) { described_class.new(Model, :associated_models, :title) } describe "#table_name" do it "returns the table name for the associated model" do - expect(association.table_name).to eq User.table_name + expect(association.table_name).to eq AssociatedModel.table_name end end describe "#join" do let(:expected_sql) do - <<~SQL.squish + <<-EOS.gsub(/\s+/, ' ').strip LEFT OUTER JOIN (SELECT model_id AS 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" + #{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\" GROUP BY model_id) #{association.subselect_alias} ON #{association.subselect_alias}.id = model_id - SQL + EOS end - it "returns the correct SQL join" do - expect(association.join("model_id")).to eq(expected_sql) + 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 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 + 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) 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/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index 929a4134..d702d69c 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -142,6 +142,23 @@ ) end end + + context "when column is a tsvector_column" do + it 'uses the tsvector column' do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:my_tsvector, { tsvector_column: true }, Model), + ] + options = { } + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) + + feature = described_class.new(query, options, columns, Model, normalizer) + expect(feature.conditions.to_sql).to eq( + %Q{((coalesce(#{Model.quoted_table_name}.\"my_tsvector\", '')) @@ (to_tsquery('simple', ''' ' || 'query' || ' ''')))} + ) + end + end end describe "#highlight" do @@ -258,5 +275,22 @@ end # rubocop:enable RSpec/ExampleLength end + + context "when column is a tsvector_column" do + it 'uses the tsvector column' do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:my_tsvector, { tsvector_column: true }, Model) + ] + options = {} + config = double(:config, ignore: []) + normalizer = PgSearch::Normalizer.new(config) + + feature = described_class.new(query, options, columns, Model, normalizer) + expect(feature.conditions.to_sql).to eq( + %{((coalesce(#{Model.quoted_table_name}.\"my_tsvector\", '')) @@ (to_tsquery('simple', ''' ' || 'query' || ' ''')))} + ) + end + end end end diff --git a/spec/support/database.rb b/spec/support/database.rb index ea31b63f..11d3ef00 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -67,3 +67,5 @@ def load_sql(filename) 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" diff --git a/sql/tsvector_agg.sql b/sql/tsvector_agg.sql new file mode 100644 index 00000000..6848d975 --- /dev/null +++ b/sql/tsvector_agg.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION concat_tsvectors(tsv1 tsvector, tsv2 tsvector) RETURNS tsvector AS +$function$ BEGIN + RETURN tsv1 || tsv2; +END; $function$ +LANGUAGE plpgsql; + +CREATE AGGREGATE tsvector_agg(tsvector) ( + SFUNC=concat_tsvectors, + STYPE=tsvector, + INITCOND='' +); diff --git a/sql/uninstall_tsvector_agg.sql b/sql/uninstall_tsvector_agg.sql new file mode 100644 index 00000000..06356b37 --- /dev/null +++ b/sql/uninstall_tsvector_agg.sql @@ -0,0 +1,3 @@ +DROP AGGREGATE IF EXISTS tsvector_agg(tsvector); + +DROP FUNCTION IF EXISTS concat_tsvectors(tsvector, tsvector); From b0fd8a7c868e0d7c89841bac75bc4647ffb09a2c Mon Sep 17 00:00:00 2001 From: mhenrixon Date: Thu, 24 Aug 2023 05:26:06 +0200 Subject: [PATCH 2/2] fix: comment out to allow migrations --- lib/pg_search/railtie.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pg_search/railtie.rb b/lib/pg_search/railtie.rb index 36b3a4d3..1584facd 100644 --- a/lib/pg_search/railtie.rb +++ b/lib/pg_search/railtie.rb @@ -9,8 +9,8 @@ class Railtie < Rails::Railtie generators do require "pg_search/migration/multisearch_generator" require "pg_search/migration/dmetaphone_generator" - require "pg_search/migration/associated_against_generator" - require "pg_search/migration/associated_against_tsvector_generator" + # require "pg_search/migration/associated_against_generator" + # require "pg_search/migration/associated_against_tsvector_generator" end end end