pg_search 2.3.7 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +38 -37
  3. data/.github/workflows/codeql.yml +86 -0
  4. data/.gitignore +0 -1
  5. data/.standard.yml +2 -2
  6. data/CHANGELOG.md +81 -64
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +4 -2
  9. data/LICENSE +1 -1
  10. data/README.md +89 -87
  11. data/Rakefile +1 -7
  12. data/doc/plans/2026-09-05-arel-stack.md +43 -0
  13. data/doc/plans/2026-09-06-arel-remaining-expressions.md +48 -0
  14. data/lib/pg_search/configuration/association.rb +37 -23
  15. data/lib/pg_search/configuration/column.rb +17 -12
  16. data/lib/pg_search/configuration/foreign_column.rb +3 -5
  17. data/lib/pg_search/configuration.rb +10 -26
  18. data/lib/pg_search/features/dmetaphone.rb +3 -7
  19. data/lib/pg_search/features/feature.rb +8 -4
  20. data/lib/pg_search/features/trigram.rb +6 -20
  21. data/lib/pg_search/features/tsearch.rb +47 -61
  22. data/lib/pg_search/features.rb +2 -0
  23. data/lib/pg_search/migration/generator.rb +1 -5
  24. data/lib/pg_search/model.rb +6 -8
  25. data/lib/pg_search/multisearch/rebuilder.rb +69 -64
  26. data/lib/pg_search/normalizer.rb +40 -11
  27. data/lib/pg_search/scope_options.rb +32 -49
  28. data/lib/pg_search/version.rb +1 -1
  29. data/pg_search.gemspec +5 -5
  30. data/spec/integration/associations_spec.rb +133 -0
  31. data/spec/integration/deprecation_spec.rb +5 -1
  32. data/spec/integration/pagination_spec.rb +1 -0
  33. data/spec/integration/pg_search_spec.rb +78 -1
  34. data/spec/integration/single_table_inheritance_spec.rb +2 -0
  35. data/spec/lib/pg_search/configuration/association_spec.rb +34 -58
  36. data/spec/lib/pg_search/configuration/column_spec.rb +73 -15
  37. data/spec/lib/pg_search/configuration/foreign_column_spec.rb +53 -20
  38. data/spec/lib/pg_search/features/dmetaphone_spec.rb +2 -2
  39. data/spec/lib/pg_search/features/trigram_spec.rb +8 -9
  40. data/spec/lib/pg_search/features/tsearch_spec.rb +8 -8
  41. data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +251 -211
  42. data/spec/lib/pg_search/multisearch_spec.rb +16 -16
  43. data/spec/lib/pg_search/multisearchable_spec.rb +8 -0
  44. data/spec/lib/pg_search/normalizer_spec.rb +64 -11
  45. data/spec/lib/pg_search_spec.rb +6 -21
  46. data/spec/spec_helper.rb +0 -13
  47. metadata +11 -15
  48. data/spec/.rubocop.yml +0 -27
  49. data/spec/integration/.rubocop.yml +0 -11
@@ -8,8 +8,11 @@ describe PgSearch::Multisearchable do
8
8
 
9
9
  describe "a model that is multisearchable" do
10
10
  with_model :ModelThatIsMultisearchable do
11
+ table
12
+
11
13
  model do
12
14
  include PgSearch::Model
15
+
13
16
  multisearchable
14
17
  end
15
18
  end
@@ -21,6 +24,7 @@ describe PgSearch::Multisearchable do
21
24
 
22
25
  model do
23
26
  include PgSearch::Model
27
+
24
28
  multisearchable
25
29
 
26
30
  has_many :multisearchable_children, dependent: :destroy
@@ -319,6 +323,7 @@ describe PgSearch::Multisearchable do
319
323
 
320
324
  model do
321
325
  include PgSearch::Model
326
+
322
327
  multisearchable if: ->(record) { record.multisearchable? }
323
328
  end
324
329
  end
@@ -450,6 +455,7 @@ describe PgSearch::Multisearchable do
450
455
 
451
456
  model do
452
457
  include PgSearch::Model
458
+
453
459
  multisearchable unless: ->(record) { record.not_multisearchable? }
454
460
  end
455
461
  end
@@ -585,6 +591,7 @@ describe PgSearch::Multisearchable do
585
591
 
586
592
  model do
587
593
  include PgSearch::Model
594
+
588
595
  multisearchable if: :multisearchable?
589
596
  end
590
597
  end
@@ -723,6 +730,7 @@ describe PgSearch::Multisearchable do
723
730
 
724
731
  model do
725
732
  include PgSearch::Model
733
+
726
734
  multisearchable unless: :not_multisearchable?
727
735
  end
728
736
  end
@@ -7,56 +7,109 @@ describe PgSearch::Normalizer do
7
7
  describe "#add_normalization" do
8
8
  context "when config[:ignore] includes :accents" do
9
9
  context "when passed an Arel node" do
10
- it "wraps the expression in unaccent()" do
10
+ it "returns an Arel node wrapping the expression in unaccent()" do
11
11
  config = instance_double(PgSearch::Configuration, "config", ignore: [:accents])
12
12
  node = Arel::Nodes::NamedFunction.new("foo", [Arel::Nodes.build_quoted("bar")])
13
13
 
14
14
  normalizer = described_class.new(config)
15
- expect(normalizer.add_normalization(node)).to eq("unaccent(foo('bar'))")
15
+ result = normalizer.add_normalization(node)
16
+ expect(result).to be_an(Arel::Nodes::Node)
17
+ expect(result.to_sql).to eq("regexp_replace(unaccent(foo('bar')), '[''?\\:]', '', 'g')")
16
18
  end
17
19
 
18
20
  context "when a custom unaccent function is specified" do
19
- it "wraps the expression in that function" do
21
+ it "returns an Arel node using that function" do
20
22
  allow(PgSearch).to receive(:unaccent_function).and_return("my_unaccent")
21
23
  node = Arel::Nodes::NamedFunction.new("foo", [Arel::Nodes.build_quoted("bar")])
22
24
 
23
25
  config = instance_double(PgSearch::Configuration, "config", ignore: [:accents])
24
26
 
25
27
  normalizer = described_class.new(config)
26
- expect(normalizer.add_normalization(node)).to eq("my_unaccent(foo('bar'))")
28
+ result = normalizer.add_normalization(node)
29
+ expect(result).to be_an(Arel::Nodes::Node)
30
+ expect(result.to_sql).to eq("regexp_replace(my_unaccent(foo('bar')), '[''?\\:]', '', 'g')")
27
31
  end
28
32
  end
29
33
  end
30
34
 
31
- context "when passed a String" do
32
- it "wraps the expression in unaccent()" do
35
+ context "when passed a String (trusted SQL expression)" do
36
+ it "returns an Arel node wrapping the expression in unaccent()" do
33
37
  config = instance_double(PgSearch::Configuration, "config", ignore: [:accents])
34
38
 
35
39
  normalizer = described_class.new(config)
36
- expect(normalizer.add_normalization("foo")).to eq("unaccent(foo)")
40
+ result = normalizer.add_normalization("foo")
41
+ expect(result).to be_an(Arel::Nodes::Node)
42
+ expect(result.to_sql).to eq("regexp_replace(unaccent(foo), '[''?\\:]', '', 'g')")
37
43
  end
38
44
 
39
45
  context "when a custom unaccent function is specified" do
40
- it "wraps the expression in that function" do
46
+ it "returns an Arel node using that function" do
41
47
  allow(PgSearch).to receive(:unaccent_function).and_return("my_unaccent")
42
48
 
43
49
  config = instance_double(PgSearch::Configuration, "config", ignore: [:accents])
44
50
 
45
51
  normalizer = described_class.new(config)
46
- expect(normalizer.add_normalization("foo")).to eq("my_unaccent(foo)")
52
+ result = normalizer.add_normalization("foo")
53
+ expect(result).to be_an(Arel::Nodes::Node)
54
+ expect(result.to_sql).to eq("regexp_replace(my_unaccent(foo), '[''?\\:]', '', 'g')")
47
55
  end
48
56
  end
49
57
  end
50
58
  end
51
59
 
60
+ it "rejects unsupported inputs instead of turning them into SQL" do
61
+ config = instance_double(PgSearch::Configuration, ignore: [])
62
+ normalizer = described_class.new(config)
63
+
64
+ [nil, Object.new, 123].each do |input|
65
+ expect { normalizer.add_normalization(input) }.to raise_error(TypeError)
66
+ end
67
+ end
68
+
52
69
  context "when config[:ignore] does not include :accents" do
53
- it "passes the expression through" do
70
+ it "passes the expression through as an Arel-compatible object" do
54
71
  config = instance_double(PgSearch::Configuration, "config", ignore: [])
55
72
 
56
73
  normalizer = described_class.new(config)
57
- expect(normalizer.add_normalization("foo")).to eq("foo")
74
+ result = normalizer.add_normalization("foo")
75
+ expect(result).to eq("foo") # SqlLiteral is a String subclass, usable in Arel contexts
58
76
  end
59
77
  end
60
78
  end
79
+
80
+ describe "executed against PostgreSQL" do
81
+ with_model :Book do
82
+ table { |t| t.string :title }
83
+ end
84
+
85
+ it "strips accents and disallowed punctuation but preserves backslashes" do
86
+ Book.create!(title: "café's? déjà: vu\\path")
87
+ config = instance_double(PgSearch::Configuration, ignore: [:accents])
88
+ normalizer = described_class.new(config)
89
+ expression = normalizer.add_normalization(Book.arel_table[:title])
90
+
91
+ expect(Book.pick(expression)).to eq("cafes deja vu\\path")
92
+ end
93
+
94
+ it "preserves attributes when accents are not ignored" do
95
+ book = Book.create!(title: "café's? déjà: vu\\path")
96
+ config = instance_double(PgSearch::Configuration, ignore: [])
97
+ normalizer = described_class.new(config)
98
+ expression = normalizer.add_normalization(Book.arel_table[:title])
99
+
100
+ expect(Book.pick(expression)).to eq(book.title)
101
+ end
102
+
103
+ it "preserves quoted data rather than interpreting it as SQL" do
104
+ Book.create!
105
+ value = Arel::Nodes.build_quoted("café's? déjà: vu\\path")
106
+ config = instance_double(PgSearch::Configuration, ignore: [])
107
+ normalizer = described_class.new(config)
108
+
109
+ expect(normalizer.add_normalization(value)).to equal(value)
110
+ expect(Book.pick(normalizer.add_normalization(value)))
111
+ .to eq("café's? déjà: vu\\path")
112
+ end
113
+ end
61
114
  end
62
115
  # standard:enable RSpec/NestedGroups
@@ -2,17 +2,6 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
- # For Active Record 5.x, the association reflection's cache needs be cleared
6
- # because we're stubbing the related constants.
7
- if ActiveRecord::VERSION::MAJOR == 5
8
- def clear_searchable_cache
9
- PgSearch::Document.reflect_on_association(:searchable).clear_association_scope_cache
10
- end
11
- else
12
- def clear_searchable_cache
13
- end
14
- end
15
-
16
5
  # standard:disable RSpec/NestedGroups
17
6
  describe PgSearch do
18
7
  describe ".multisearch" do
@@ -32,10 +21,7 @@ describe PgSearch do
32
21
  end
33
22
 
34
23
  context "with PgSearch.multisearch_options set to a Hash" do
35
- subject do
36
- clear_searchable_cache
37
- described_class.multisearch(query).map(&:searchable)
38
- end
24
+ subject { described_class.multisearch(query).map(&:searchable) }
39
25
 
40
26
  before { allow(described_class).to receive(:multisearch_options).and_return(using: :dmetaphone) }
41
27
 
@@ -45,6 +31,7 @@ describe PgSearch do
45
31
  end
46
32
  model do
47
33
  include PgSearch::Model
34
+
48
35
  multisearchable against: :title
49
36
  end
50
37
  end
@@ -56,10 +43,7 @@ describe PgSearch do
56
43
  end
57
44
 
58
45
  context "with PgSearch.multisearch_options set to a Proc" do
59
- subject do
60
- clear_searchable_cache
61
- described_class.multisearch(query, soundalike).map(&:searchable)
62
- end
46
+ subject { described_class.multisearch(query, soundalike).map(&:searchable) }
63
47
 
64
48
  before do
65
49
  allow(described_class).to receive(:multisearch_options) do
@@ -79,6 +63,7 @@ describe PgSearch do
79
63
  end
80
64
  model do
81
65
  include PgSearch::Model
66
+
82
67
  multisearchable against: :title
83
68
  end
84
69
  end
@@ -111,6 +96,7 @@ describe PgSearch do
111
96
  before do
112
97
  searchable_subclass_model = Class.new(SuperclassModel) do
113
98
  include PgSearch::Model
99
+
114
100
  multisearchable against: :content
115
101
  end
116
102
  stub_const("SearchableSubclassModel", searchable_subclass_model)
@@ -166,7 +152,6 @@ describe PgSearch do
166
152
 
167
153
  PgSearch::Multisearch.rebuild(SearchableSubclassModel)
168
154
 
169
- clear_searchable_cache
170
155
  expect(PgSearch::Document.count).to be 1
171
156
  expect(PgSearch::Document.first.searchable.class).to be SearchableSubclassModel
172
157
  expect(PgSearch::Document.first.searchable).to eq expected
@@ -181,7 +166,6 @@ describe PgSearch do
181
166
  PgSearch::Multisearch.rebuild(SearchableSubclassModel)
182
167
  expect(PgSearch::Document.count).to be 2
183
168
 
184
- clear_searchable_cache
185
169
  classes = PgSearch::Document.all.collect { |d| d.searchable.class }
186
170
  expect(classes).to include SearchableSubclassModel
187
171
  expect(classes).to include AnotherSearchableSubclassModel
@@ -203,6 +187,7 @@ describe PgSearch do
203
187
  before do
204
188
  searchable_subclass_model = Class.new(SuperclassModel) do
205
189
  include PgSearch::Model
190
+
206
191
  multisearchable against: :content
207
192
  end
208
193
  stub_const("SearchableSubclassModel", searchable_subclass_model)
data/spec/spec_helper.rb CHANGED
@@ -1,18 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "warning"
4
-
5
- # https://github.com/grodowski/undercover#setting-up-required-lcov-reporting
6
- require "simplecov"
7
- require "simplecov-lcov"
8
- SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
9
- SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
10
- SimpleCov.start do
11
- add_filter(%r{^/spec/})
12
- enable_coverage(:branch)
13
- end
14
- require "undercover"
15
-
16
3
  require "bundler/setup"
17
4
  require "pg_search"
18
5
 
metadata CHANGED
@@ -1,15 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.7
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grant Hutchins
8
- - Case Commons, LLC
9
- autorequire:
10
8
  bindir: bin
11
9
  cert_chain: []
12
- date: 2024-08-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
13
11
  dependencies:
14
12
  - !ruby/object:Gem::Dependency
15
13
  name: activerecord
@@ -17,33 +15,32 @@ dependencies:
17
15
  requirements:
18
16
  - - ">="
19
17
  - !ruby/object:Gem::Version
20
- version: '6.1'
18
+ version: '8.0'
21
19
  type: :runtime
22
20
  prerelease: false
23
21
  version_requirements: !ruby/object:Gem::Requirement
24
22
  requirements:
25
23
  - - ">="
26
24
  - !ruby/object:Gem::Version
27
- version: '6.1'
25
+ version: '8.0'
28
26
  - !ruby/object:Gem::Dependency
29
27
  name: activesupport
30
28
  requirement: !ruby/object:Gem::Requirement
31
29
  requirements:
32
30
  - - ">="
33
31
  - !ruby/object:Gem::Version
34
- version: '6.1'
32
+ version: '8.0'
35
33
  type: :runtime
36
34
  prerelease: false
37
35
  version_requirements: !ruby/object:Gem::Requirement
38
36
  requirements:
39
37
  - - ">="
40
38
  - !ruby/object:Gem::Version
41
- version: '6.1'
39
+ version: '8.0'
42
40
  description: PgSearch builds Active Record named scopes that take advantage of PostgreSQL's
43
41
  full text search
44
42
  email:
45
43
  - gems@nertzy.com
46
- - casecommons-dev@googlegroups.com
47
44
  executables: []
48
45
  extensions: []
49
46
  extra_rdoc_files: []
@@ -52,6 +49,7 @@ files:
52
49
  - ".editorconfig"
53
50
  - ".github/dependabot.yml"
54
51
  - ".github/workflows/ci.yml"
52
+ - ".github/workflows/codeql.yml"
55
53
  - ".gitignore"
56
54
  - ".jrubyrc"
57
55
  - ".rspec"
@@ -63,6 +61,8 @@ files:
63
61
  - LICENSE
64
62
  - README.md
65
63
  - Rakefile
64
+ - doc/plans/2026-09-05-arel-stack.md
65
+ - doc/plans/2026-09-06-arel-remaining-expressions.md
66
66
  - lib/pg_search.rb
67
67
  - lib/pg_search/configuration.rb
68
68
  - lib/pg_search/configuration/association.rb
@@ -89,8 +89,6 @@ files:
89
89
  - lib/pg_search/tasks.rb
90
90
  - lib/pg_search/version.rb
91
91
  - pg_search.gemspec
92
- - spec/.rubocop.yml
93
- - spec/integration/.rubocop.yml
94
92
  - spec/integration/associations_spec.rb
95
93
  - spec/integration/deprecation_spec.rb
96
94
  - spec/integration/pagination_spec.rb
@@ -117,7 +115,6 @@ licenses:
117
115
  - MIT
118
116
  metadata:
119
117
  rubygems_mfa_required: 'true'
120
- post_install_message:
121
118
  rdoc_options: []
122
119
  require_paths:
123
120
  - lib
@@ -125,15 +122,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
122
  requirements:
126
123
  - - ">="
127
124
  - !ruby/object:Gem::Version
128
- version: '3.0'
125
+ version: '3.3'
129
126
  required_rubygems_version: !ruby/object:Gem::Requirement
130
127
  requirements:
131
128
  - - ">="
132
129
  - !ruby/object:Gem::Version
133
130
  version: '0'
134
131
  requirements: []
135
- rubygems_version: 3.5.11
136
- signing_key:
132
+ rubygems_version: 4.0.20
137
133
  specification_version: 4
138
134
  summary: PgSearch builds Active Record named scopes that take advantage of PostgreSQL's
139
135
  full text search
data/spec/.rubocop.yml DELETED
@@ -1,27 +0,0 @@
1
- inherit_from:
2
- - ../.rubocop.yml
3
-
4
- RSpec/ContextWording:
5
- Prefixes:
6
- - using
7
- - via
8
- - when
9
- - with
10
- - without
11
-
12
- RSpec/DescribedClass:
13
- Enabled: true
14
-
15
- RSpec/ExampleLength:
16
- Max: 15
17
-
18
- RSpec/ExpectInHook:
19
- Enabled: false
20
-
21
- RSpec/FilePath:
22
- CustomTransform:
23
- TSearch: "tsearch"
24
- DMetaphone: "dmetaphone"
25
-
26
- RSpec/MultipleExpectations:
27
- Max: 5
@@ -1,11 +0,0 @@
1
- inherit_from:
2
- - ../.rubocop.yml
3
-
4
- RSpec/DescribeClass:
5
- Enabled: false
6
-
7
- RSpec/ExampleLength:
8
- Enabled: false
9
-
10
- RSpec/MultipleExpectations:
11
- Enabled: false