pg_search 2.3.8 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -6
- data/.github/workflows/codeql.yml +0 -1
- data/.gitignore +0 -1
- data/.standard.yml +1 -1
- data/CHANGELOG.md +70 -65
- data/CONTRIBUTING.md +1 -1
- data/LICENSE +1 -1
- data/README.md +2 -2
- data/doc/plans/2026-09-05-arel-stack.md +43 -0
- data/doc/plans/2026-09-06-arel-remaining-expressions.md +48 -0
- data/lib/pg_search/configuration/association.rb +37 -23
- data/lib/pg_search/configuration/column.rb +17 -12
- data/lib/pg_search/configuration/foreign_column.rb +3 -5
- data/lib/pg_search/features/dmetaphone.rb +3 -7
- data/lib/pg_search/features/feature.rb +8 -4
- data/lib/pg_search/features/trigram.rb +6 -20
- data/lib/pg_search/features/tsearch.rb +30 -36
- data/lib/pg_search/features.rb +2 -0
- data/lib/pg_search/migration/generator.rb +1 -5
- data/lib/pg_search/multisearch/rebuilder.rb +69 -65
- data/lib/pg_search/normalizer.rb +37 -18
- data/lib/pg_search/scope_options.rb +28 -52
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +4 -4
- data/spec/integration/associations_spec.rb +121 -0
- data/spec/integration/pg_search_spec.rb +48 -0
- data/spec/lib/pg_search/configuration/association_spec.rb +32 -58
- data/spec/lib/pg_search/configuration/column_spec.rb +73 -15
- data/spec/lib/pg_search/configuration/foreign_column_spec.rb +52 -20
- data/spec/lib/pg_search/features/dmetaphone_spec.rb +2 -2
- data/spec/lib/pg_search/features/trigram_spec.rb +3 -3
- data/spec/lib/pg_search/features/tsearch_spec.rb +8 -8
- data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +219 -229
- data/spec/lib/pg_search/multisearch_spec.rb +16 -16
- data/spec/lib/pg_search/normalizer_spec.rb +64 -11
- data/spec/lib/pg_search_spec.rb +2 -21
- metadata +8 -8
|
@@ -3,37 +3,69 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
5
|
describe PgSearch::Configuration::ForeignColumn do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
t.string "title"
|
|
10
|
-
end
|
|
6
|
+
with_model :AssociatedModel do
|
|
7
|
+
table do |t|
|
|
8
|
+
t.string :title
|
|
11
9
|
end
|
|
10
|
+
end
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
with_model :Model do
|
|
13
|
+
table do |t|
|
|
14
|
+
t.string :name
|
|
15
|
+
t.belongs_to :associated_model, index: false, null: true
|
|
16
|
+
end
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
model do
|
|
19
|
+
include PgSearch::Model
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
belongs_to :associated_model, class_name: "AssociatedModel", optional: true
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
end
|
|
23
|
+
pg_search_scope :with_title, associated_against: {associated_model: :title}
|
|
26
24
|
end
|
|
25
|
+
end
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
foreign_column = described_class.new("title", nil, Model, association)
|
|
27
|
+
let(:association) do
|
|
28
|
+
PgSearch::Configuration::Association.new(Model, :associated_model, :title)
|
|
29
|
+
end
|
|
30
|
+
let(:foreign_column) { described_class.new("title", nil, Model, association) }
|
|
33
31
|
|
|
32
|
+
describe "#alias" do
|
|
33
|
+
it "returns a consistent string" do
|
|
34
34
|
column_alias = foreign_column.alias
|
|
35
35
|
expect(column_alias).to be_a String
|
|
36
36
|
expect(foreign_column.alias).to eq column_alias
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
describe "#to_arel" do
|
|
41
|
+
def selected_value(model)
|
|
42
|
+
relation = Model
|
|
43
|
+
.joins(association.to_arel(Model.arel_table[:id]))
|
|
44
|
+
.where(Model.arel_table[:id].eq(model.id))
|
|
45
|
+
.select(foreign_column.to_arel.as("value"))
|
|
46
|
+
sql = Model.connection.visitor.compile(relation.arel.ast)
|
|
47
|
+
|
|
48
|
+
Model.connection.select_value(sql)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "evaluates the associated value through the association join alias" do
|
|
52
|
+
associated = AssociatedModel.create!(title: "hello world")
|
|
53
|
+
model = Model.create!(name: "example", associated_model: associated)
|
|
54
|
+
|
|
55
|
+
expect(selected_value(model)).to eq("hello world")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "coalesces a NULL associated value through the association join alias" do
|
|
59
|
+
associated = AssociatedModel.create!(title: nil)
|
|
60
|
+
model = Model.create!(name: "example", associated_model: associated)
|
|
61
|
+
|
|
62
|
+
expect(selected_value(model)).to eq("")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "coalesces a missing association through the outer join alias" do
|
|
66
|
+
model = Model.create!(name: "example", associated_model: nil)
|
|
67
|
+
|
|
68
|
+
expect(selected_value(model)).to eq("")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
39
71
|
end
|
|
@@ -23,7 +23,7 @@ describe PgSearch::Features::DMetaphone do
|
|
|
23
23
|
|
|
24
24
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
25
25
|
expect(feature.rank.to_sql).to eq(
|
|
26
|
-
%{(ts_rank((to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."name")
|
|
26
|
+
%{(ts_rank((to_tsvector('simple', pg_search_dmetaphone(coalesce(cast(#{Model.quoted_table_name}."name" AS text), ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce(cast(#{Model.quoted_table_name}."content" AS text), '')))), (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')), 0))}
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -48,7 +48,7 @@ describe PgSearch::Features::DMetaphone do
|
|
|
48
48
|
|
|
49
49
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
50
50
|
expect(feature.conditions.to_sql).to eq(
|
|
51
|
-
%{((to_tsvector('simple', pg_search_dmetaphone(coalesce((#{Model.quoted_table_name}."name")
|
|
51
|
+
%{((to_tsvector('simple', pg_search_dmetaphone(coalesce(cast(#{Model.quoted_table_name}."name" AS text), ''))) || to_tsvector('simple', pg_search_dmetaphone(coalesce(cast(#{Model.quoted_table_name}."content" AS text), '')))) @@ (to_tsquery('simple', ''' ' || pg_search_dmetaphone('query') || ' ''')))}
|
|
52
52
|
)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -20,9 +20,9 @@ describe PgSearch::Features::Trigram do
|
|
|
20
20
|
|
|
21
21
|
let(:coalesced_columns) do
|
|
22
22
|
<<~SQL.squish
|
|
23
|
-
coalesce((#{Model.quoted_table_name}."name")
|
|
23
|
+
coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')
|
|
24
24
|
|| ' '
|
|
25
|
-
|| coalesce((#{Model.quoted_table_name}."content")
|
|
25
|
+
|| coalesce(cast(#{Model.quoted_table_name}."content" AS text), '')
|
|
26
26
|
SQL
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -87,7 +87,7 @@ describe PgSearch::Features::Trigram do
|
|
|
87
87
|
let(:options) { {only: :name} }
|
|
88
88
|
|
|
89
89
|
it "only searches against the select column" do
|
|
90
|
-
coalesced_column = "coalesce((#{Model.quoted_table_name}.\"name\")
|
|
90
|
+
coalesced_column = "coalesce(cast(#{Model.quoted_table_name}.\"name\" AS text), '')"
|
|
91
91
|
expect(feature.conditions.to_sql).to eq("('#{query}' % (#{coalesced_column}))")
|
|
92
92
|
end
|
|
93
93
|
end
|
|
@@ -24,7 +24,7 @@ describe PgSearch::Features::TSearch do
|
|
|
24
24
|
|
|
25
25
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
26
26
|
expect(feature.rank.to_sql).to eq(
|
|
27
|
-
%{(ts_rank((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")
|
|
27
|
+
%{(ts_rank((to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')) || to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."content" AS text), ''))), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 0))}
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -67,7 +67,7 @@ describe PgSearch::Features::TSearch do
|
|
|
67
67
|
|
|
68
68
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
69
69
|
expect(feature.conditions.to_sql).to eq(
|
|
70
|
-
%{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")
|
|
70
|
+
%{((to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')) || to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."content" AS text), ''))) @@ (to_tsquery('simple', ''' ' || 'query' || ' ''')))}
|
|
71
71
|
)
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -84,7 +84,7 @@ describe PgSearch::Features::TSearch do
|
|
|
84
84
|
|
|
85
85
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
86
86
|
expect(feature.conditions.to_sql).to eq(
|
|
87
|
-
%{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")
|
|
87
|
+
%{((to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')) || to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."content" AS text), ''))) @@ (to_tsquery('simple', '!' || ''' ' || 'query' || ' ''')))}
|
|
88
88
|
)
|
|
89
89
|
end
|
|
90
90
|
end
|
|
@@ -102,7 +102,7 @@ describe PgSearch::Features::TSearch do
|
|
|
102
102
|
|
|
103
103
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
104
104
|
expect(feature.conditions.to_sql).to eq(
|
|
105
|
-
%{((to_tsvector('simple', coalesce((#{Model.quoted_table_name}."name")
|
|
105
|
+
%{((to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')) || to_tsvector('simple', coalesce(cast(#{Model.quoted_table_name}."content" AS text), ''))) @@ (to_tsquery('simple', ''' ' || '!query' || ' ''')))}
|
|
106
106
|
)
|
|
107
107
|
end
|
|
108
108
|
end
|
|
@@ -164,7 +164,7 @@ describe PgSearch::Features::TSearch do
|
|
|
164
164
|
|
|
165
165
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
166
166
|
expect(feature.highlight.to_sql).to eq(
|
|
167
|
-
"(ts_headline('simple', (coalesce((#{Model.quoted_table_name}.\"name\")
|
|
167
|
+
"(ts_headline('simple', (coalesce(cast(#{Model.quoted_table_name}.\"name\" AS text), '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))"
|
|
168
168
|
)
|
|
169
169
|
end
|
|
170
170
|
|
|
@@ -189,7 +189,7 @@ describe PgSearch::Features::TSearch do
|
|
|
189
189
|
|
|
190
190
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
191
191
|
|
|
192
|
-
expected_sql = %{(ts_headline('spanish', (coalesce((#{Model.quoted_table_name}."name")
|
|
192
|
+
expected_sql = %{(ts_headline('spanish', (coalesce(cast(#{Model.quoted_table_name}."name" AS text), '') || ' ' || coalesce(cast(#{Model.quoted_table_name}."content" AS text), '')), (to_tsquery('spanish', ''' ' || 'query' || ' ''')), 'StartSel = "<b>", StopSel = "</b>"'))}
|
|
193
193
|
|
|
194
194
|
expect(feature.highlight.to_sql).to eq(expected_sql)
|
|
195
195
|
end
|
|
@@ -221,7 +221,7 @@ describe PgSearch::Features::TSearch do
|
|
|
221
221
|
|
|
222
222
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
223
223
|
|
|
224
|
-
expected_sql = %{(ts_headline('simple', (coalesce((#{Model.quoted_table_name}."name")
|
|
224
|
+
expected_sql = %{(ts_headline('simple', (coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "<start class=""search"">", StopSel = "<stop>", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = TRUE'))}
|
|
225
225
|
|
|
226
226
|
expect(feature.highlight.to_sql).to eq(expected_sql)
|
|
227
227
|
end
|
|
@@ -252,7 +252,7 @@ describe PgSearch::Features::TSearch do
|
|
|
252
252
|
feature = described_class.new(query, options, columns, Model, normalizer)
|
|
253
253
|
|
|
254
254
|
highlight_sql = silence_warnings { feature.highlight.to_sql }
|
|
255
|
-
expected_sql = %{(ts_headline('simple', (coalesce((#{Model.quoted_table_name}."name")
|
|
255
|
+
expected_sql = %{(ts_headline('simple', (coalesce(cast(#{Model.quoted_table_name}."name" AS text), '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = "<start class=""search"">", StopSel = "<stop>", MaxFragments = 3, MaxWords = 123, MinWords = 456, ShortWord = 4, FragmentDelimiter = "…", HighlightAll = FALSE'))}
|
|
256
256
|
|
|
257
257
|
expect(highlight_sql).to eq(expected_sql)
|
|
258
258
|
end
|