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
@@ -7,14 +7,16 @@ describe PgSearch::Multisearch::Rebuilder do
7
7
  with_table "pg_search_documents", &DOCUMENTS_SCHEMA
8
8
 
9
9
  describe "when initialized with a model that is not multisearchable" do
10
- with_model :not_multisearchable
10
+ with_model :NotSearchable do
11
+ table
12
+ end
11
13
 
12
14
  it "raises an exception" do
13
15
  expect {
14
- described_class.new(NotMultisearchable)
16
+ described_class.new(NotSearchable)
15
17
  }.to raise_exception(
16
18
  PgSearch::Multisearch::ModelNotMultisearchable,
17
- "NotMultisearchable is not multisearchable. See PgSearch::ClassMethods#multisearchable"
19
+ "NotSearchable is not multisearchable. See PgSearch::ClassMethods#multisearchable"
18
20
  )
19
21
  end
20
22
  end
@@ -22,52 +24,52 @@ describe PgSearch::Multisearch::Rebuilder do
22
24
  describe "#rebuild" do
23
25
  context "when the model defines .rebuild_pg_search_documents" do
24
26
  context "when multisearchable is not conditional" do
25
- with_model :Model do
27
+ with_model :Book do
28
+ table
29
+
26
30
  model do
27
31
  include PgSearch::Model
32
+
28
33
  multisearchable
29
34
 
30
- def rebuild_pg_search_documents
31
- end
35
+ class_attribute :documents_rebuilt, default: false
36
+
37
+ def self.rebuild_pg_search_documents = self.documents_rebuilt = true
32
38
  end
33
39
  end
34
40
 
35
- it "calls .rebuild_pg_search_documents" do
36
- rebuilder = described_class.new(Model)
41
+ it "dispatches to .rebuild_pg_search_documents" do
42
+ described_class.new(Book).rebuild
37
43
 
38
- without_partial_double_verification do
39
- allow(Model).to receive(:rebuild_pg_search_documents)
40
- rebuilder.rebuild
41
- expect(Model).to have_received(:rebuild_pg_search_documents)
42
- end
44
+ expect(Book.documents_rebuilt).to be true
43
45
  end
44
46
  end
45
47
 
46
48
  context "when multisearchable is conditional" do
47
- %i[if unless].each do |conditional_key|
48
- context "via :#{conditional_key}" do
49
- with_model :Model do
49
+ %i[if unless].each do |key|
50
+ context "via :#{key}" do
51
+ with_model :Book do
50
52
  table do |t|
51
53
  t.boolean :active
52
54
  end
53
55
 
54
56
  model do
55
57
  include PgSearch::Model
56
- multisearchable conditional_key => :active?
57
58
 
58
- def rebuild_pg_search_documents
59
+ multisearchable key => :active?
60
+
61
+ class_attribute :documents_rebuilt, default: false
62
+
63
+ def self.rebuild_pg_search_documents
64
+ self.documents_rebuilt = true
59
65
  end
60
66
  end
61
67
  end
62
68
 
63
- it "calls .rebuild_pg_search_documents" do
64
- rebuilder = described_class.new(Model)
69
+ it "dispatches to .rebuild_pg_search_documents" do
70
+ described_class.new(Book).rebuild
65
71
 
66
- without_partial_double_verification do
67
- allow(Model).to receive(:rebuild_pg_search_documents)
68
- rebuilder.rebuild
69
- expect(Model).to have_received(:rebuild_pg_search_documents)
70
- end
72
+ expect(Book.documents_rebuilt).to be true
71
73
  end
72
74
  end
73
75
  end
@@ -77,282 +79,320 @@ describe PgSearch::Multisearch::Rebuilder do
77
79
  context "when the model does not define .rebuild_pg_search_documents" do
78
80
  context "when multisearchable is not conditional" do
79
81
  context "when :against only includes columns" do
80
- with_model :Model do
82
+ with_model :Book do
81
83
  table do |t|
82
- t.string :name
84
+ t.string :title
83
85
  end
84
86
 
85
87
  model do
86
88
  include PgSearch::Model
87
- multisearchable against: :name
89
+
90
+ multisearchable against: :title
88
91
  end
89
92
  end
90
93
 
91
- it "does not call :rebuild_pg_search_documents" do
92
- rebuilder = described_class.new(Model)
94
+ it "inserts a document for each record via bulk INSERT" do
95
+ book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
93
96
 
94
- # stub respond_to? to return false since should_not_receive defines the method
95
- original_respond_to = Model.method(:respond_to?)
96
- allow(Model).to receive(:respond_to?) do |method_name, *args|
97
- if method_name == :rebuild_pg_search_documents
98
- false
99
- else
100
- original_respond_to.call(method_name, *args)
101
- end
102
- end
97
+ described_class.new(Book).rebuild
103
98
 
104
- without_partial_double_verification do
105
- allow(Model).to receive(:rebuild_pg_search_documents)
106
- rebuilder.rebuild
107
- expect(Model).not_to have_received(:rebuild_pg_search_documents)
108
- end
99
+ document = book.pg_search_document
100
+ expect(document.searchable_type).to eq("Book")
101
+ expect(document.searchable_id).to eq(book.id)
102
+ expect(document.content).to eq("Dune")
109
103
  end
110
104
 
111
- # standard:disable RSpec/ExampleLength
112
- it "executes the default SQL" do
113
- time = Time.utc(2001, 1, 1, 0, 0, 0)
114
- rebuilder = described_class.new(Model, -> { time })
115
-
116
- expected_sql = <<~SQL.squish
117
- INSERT INTO "pg_search_documents" (searchable_type, searchable_id, content, created_at, updated_at)
118
- SELECT 'Model' AS searchable_type,
119
- #{Model.quoted_table_name}.#{Model.primary_key} AS searchable_id,
120
- (
121
- coalesce(#{Model.quoted_table_name}."name"::text, '')
122
- ) AS content,
123
- '2001-01-01 00:00:00' AS created_at,
124
- '2001-01-01 00:00:00' AS updated_at
125
- FROM #{Model.quoted_table_name}
126
- SQL
127
-
128
- executed_sql = []
129
-
130
- notifier = ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
131
- executed_sql << payload[:sql] if payload[:sql].include?(%(INSERT INTO "pg_search_documents"))
132
- end
105
+ it "coalesces NULL column values to empty string" do
106
+ book = PgSearch.disable_multisearch { Book.create!(title: nil) }
133
107
 
134
- rebuilder.rebuild
135
- ActiveSupport::Notifications.unsubscribe(notifier)
108
+ described_class.new(Book).rebuild
136
109
 
137
- expect(executed_sql.length).to eq(1)
138
- expect(executed_sql.first.strip).to eq(expected_sql.strip)
110
+ expect(book.pg_search_document.content).to eq("")
139
111
  end
140
- # standard:enable RSpec/ExampleLength
141
112
 
142
- context "with a model with a camel case column" do
143
- with_model :ModelWithCamelCaseColumn do
144
- table do |t|
145
- t.string :camelName
146
- end
113
+ it "stamps both timestamps with the precision and offset of one clock call" do
114
+ time = Time.iso8601("2001-01-01T06:30:00.123456+05:30")
115
+ call_count = 0
116
+ time_source = lambda do
117
+ call_count += 1
118
+ time
119
+ end
147
120
 
148
- model do
149
- include PgSearch::Model
150
- multisearchable against: :name
151
- end
121
+ book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
122
+
123
+ described_class.new(Book, time_source).rebuild
124
+
125
+ expect(call_count).to eq(1)
126
+ document = book.pg_search_document
127
+ expect(document.created_at).to eq(time)
128
+ expect(document.updated_at).to eq(time)
129
+ end
130
+ end
131
+
132
+ context "when :against includes multiple columns" do
133
+ with_model :Book do
134
+ table do |t|
135
+ t.string :title
136
+ t.text :body
152
137
  end
153
138
 
154
- it "rebuilds without error" do
155
- time = Time.utc(2001, 1, 1, 0, 0, 0)
156
- rebuilder = described_class.new(Model, -> { time })
157
- expect { rebuilder.rebuild }.not_to raise_error
139
+ model do
140
+ include PgSearch::Model
141
+
142
+ multisearchable against: %i[title body]
158
143
  end
159
144
  end
160
145
 
161
- context "with a model with a non-standard primary key" do
162
- with_model :ModelWithNonStandardPrimaryKey do
163
- table primary_key: :non_standard_primary_key do |t|
164
- t.string :name
165
- end
146
+ it "joins column values with a space" do
147
+ book = PgSearch.disable_multisearch { Book.create!(title: "Dune", body: "The spice") }
166
148
 
167
- model do
168
- include PgSearch::Model
169
- multisearchable against: :name
170
- end
171
- end
149
+ described_class.new(Book).rebuild
172
150
 
173
- # standard:disable RSpec/ExampleLength
174
- it "generates SQL with the correct primary key" do
175
- time = Time.utc(2001, 1, 1, 0, 0, 0)
176
- rebuilder = described_class.new(ModelWithNonStandardPrimaryKey, -> { time })
177
-
178
- expected_sql = <<~SQL.squish
179
- INSERT INTO "pg_search_documents" (searchable_type, searchable_id, content, created_at, updated_at)
180
- SELECT 'ModelWithNonStandardPrimaryKey' AS searchable_type,
181
- #{ModelWithNonStandardPrimaryKey.quoted_table_name}.non_standard_primary_key AS searchable_id,
182
- (
183
- coalesce(#{ModelWithNonStandardPrimaryKey.quoted_table_name}."name"::text, '')
184
- ) AS content,
185
- '2001-01-01 00:00:00' AS created_at,
186
- '2001-01-01 00:00:00' AS updated_at
187
- FROM #{ModelWithNonStandardPrimaryKey.quoted_table_name}
188
- SQL
189
-
190
- executed_sql = []
191
-
192
- notifier = ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
193
- executed_sql << payload[:sql] if payload[:sql].include?(%(INSERT INTO "pg_search_documents"))
194
- end
151
+ expect(book.pg_search_document.content).to eq("Dune The spice")
152
+ end
195
153
 
196
- rebuilder.rebuild
197
- ActiveSupport::Notifications.unsubscribe(notifier)
154
+ it "coalesces NULL in any column to empty string" do
155
+ book = PgSearch.disable_multisearch { Book.create!(title: "Dune", body: nil) }
198
156
 
199
- expect(executed_sql.length).to eq(1)
200
- expect(executed_sql.first.strip).to eq(expected_sql.strip)
201
- end
202
- # standard:enable RSpec/ExampleLength
157
+ described_class.new(Book).rebuild
158
+
159
+ # Keep the separator when the NULL body becomes an empty string.
160
+ expect(book.pg_search_document.content).to eq("Dune ")
203
161
  end
204
162
  end
205
163
 
206
- context "when :against includes non-column dynamic methods" do
207
- with_model :Model do
164
+ context "with a camelCase column name" do
165
+ with_model :Book do
166
+ table do |t|
167
+ t.string :camelName
168
+ end
169
+
208
170
  model do
209
171
  include PgSearch::Model
210
- multisearchable against: [:foo]
211
172
 
212
- def foo
213
- "bar"
214
- end
173
+ multisearchable against: :camelName
215
174
  end
216
175
  end
217
176
 
218
- # standard:disable RSpec/ExampleLength
219
- it "calls update_pg_search_document on each record" do
220
- record = Model.create!
177
+ it "quotes the column name and inserts correct content" do
178
+ book = PgSearch.disable_multisearch { Book.create!(camelName: "CamelValue") }
221
179
 
222
- rebuilder = described_class.new(Model)
180
+ described_class.new(Book).rebuild
223
181
 
224
- # stub respond_to? to return false since should_not_receive defines the method
225
- original_respond_to = Model.method(:respond_to?)
226
- allow(Model).to receive(:respond_to?) do |method_name, *args|
227
- if method_name == :rebuild_pg_search_documents
228
- false
229
- else
230
- original_respond_to.call(method_name, *args)
231
- end
232
- end
182
+ expect(book.pg_search_document.content).to eq("CamelValue")
183
+ end
184
+ end
233
185
 
234
- without_partial_double_verification do
235
- allow(Model).to receive(:rebuild_pg_search_documents)
186
+ context "with a non-standard primary key" do
187
+ with_model :Book do
188
+ table primary_key: :isbn do |t|
189
+ t.string :title
190
+ end
236
191
 
237
- rebuilder.rebuild
192
+ model do
193
+ include PgSearch::Model
238
194
 
239
- expect(Model).not_to have_received(:rebuild_pg_search_documents)
195
+ multisearchable against: :title
240
196
  end
197
+ end
241
198
 
242
- expect(record.pg_search_document).to be_present
199
+ it "stores the non-standard primary key in searchable_id" do
200
+ book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
201
+
202
+ described_class.new(Book).rebuild
203
+
204
+ expect(book.pg_search_document.searchable_id).to eq(book.isbn)
243
205
  end
244
- # standard:enable RSpec/ExampleLength
245
206
  end
246
207
 
247
- context "when only additional_attributes is set" do
248
- with_model :Model do
208
+ context "with an identifier that requires quoting" do
209
+ with_model :Book do
249
210
  table do |t|
250
- t.string :name
211
+ t.string :title
251
212
  end
252
213
 
253
214
  model do
254
215
  include PgSearch::Model
255
- multisearchable against: :name,
256
- additional_attributes: ->(obj) { {additional_attribute_column: "#{obj.class}::#{obj.id}"} }
216
+
217
+ multisearchable against: :title
257
218
  end
258
219
  end
259
220
 
260
- it "calls update_pg_search_document on each record" do
261
- record_1 = Model.create!(name: "record_1", id: 1)
262
- record_2 = Model.create!(name: "record_2", id: 2)
221
+ it "quotes a malicious primary key in the generated SQL" do
222
+ malicious_pk = %(id"; DROP TABLE pg_search_documents; --)
223
+ allow(Book).to receive(:primary_key).and_return(malicious_pk)
263
224
 
264
- PgSearch::Document.delete_all
225
+ sql = described_class.new(Book).send(:rebuild_sql)
226
+
227
+ expect(sql).to include(Book.connection.quote_column_name(malicious_pk))
228
+ end
229
+
230
+ it "quotes a malicious inheritance column in the generated SQL" do
231
+ malicious_column = %(type"; DROP TABLE pg_search_documents; --)
232
+ allow(Book).to receive(:inheritance_column).and_return(malicious_column)
233
+ allow(Book).to receive(:column_names).and_return(Book.column_names + [malicious_column])
265
234
 
266
- rebuilder = described_class.new(Model)
267
- rebuilder.rebuild
235
+ sql = described_class.new(Book).send(:rebuild_sql)
268
236
 
269
- expect(record_1.reload.pg_search_document.additional_attribute_column).to eq("Model::1")
270
- expect(record_2.reload.pg_search_document.additional_attribute_column).to eq("Model::2")
237
+ expect(sql).to include(Book.connection.quote_column_name(malicious_column))
271
238
  end
272
239
  end
273
- end
274
240
 
275
- context "when multisearchable is conditional" do
276
- context "via :if" do
277
- with_model :Model do
241
+ context "with STI" do
242
+ with_model :Animal do
278
243
  table do |t|
279
- t.boolean :active
244
+ t.string :type
245
+ t.string :name
280
246
  end
281
247
 
282
248
  model do
283
249
  include PgSearch::Model
284
- multisearchable if: :active?
250
+
251
+ multisearchable against: :name
285
252
  end
286
253
  end
287
254
 
288
- # standard:disable RSpec/ExampleLength
289
- it "calls update_pg_search_document on each record" do
290
- record_1 = Model.create!(active: true)
291
- record_2 = Model.create!(active: false)
255
+ with_model :Cat, superclass: :Animal do
256
+ table(false)
292
257
 
293
- rebuilder = described_class.new(Model)
258
+ model do
259
+ include PgSearch::Model
294
260
 
295
- # stub respond_to? to return false since should_not_receive defines the method
296
- original_respond_to = Model.method(:respond_to?)
297
- allow(Model).to receive(:respond_to?) do |method_name, *args|
298
- if method_name == :rebuild_pg_search_documents
299
- false
300
- else
301
- original_respond_to.call(method_name, *args)
302
- end
261
+ multisearchable against: :name
303
262
  end
263
+ end
304
264
 
305
- without_partial_double_verification do
306
- allow(Model).to receive(:rebuild_pg_search_documents)
307
- rebuilder.rebuild
308
- expect(Model).not_to have_received(:rebuild_pg_search_documents)
309
- end
265
+ it "rebuilds base class documents for null-typed rows only" do
266
+ base = PgSearch.disable_multisearch { Animal.create!(name: "Base") }
267
+ PgSearch.disable_multisearch { Cat.create!(name: "Whiskers") }
268
+
269
+ described_class.new(Animal).rebuild
270
+
271
+ # Only the directly-typed Animal row is included; Cat rows require their own rebuild
272
+ ids = PgSearch::Document.where(searchable_type: "Animal").map(&:searchable_id)
273
+ expect(ids).to contain_exactly(base.id)
274
+ end
275
+
276
+ it "rebuilds subclass documents with exact type match" do
277
+ PgSearch.disable_multisearch { Animal.create!(name: "Base") }
278
+ cat = PgSearch.disable_multisearch { Cat.create!(name: "Whiskers") }
279
+
280
+ described_class.new(Cat).rebuild
281
+
282
+ # Cat rebuild stores searchable_type = base class name
283
+ ids = PgSearch::Document.where(searchable_type: "Animal").map(&:searchable_id)
284
+ expect(ids).to contain_exactly(cat.id)
285
+ end
310
286
 
311
- expect(record_1.pg_search_document).to be_present
312
- expect(record_2.pg_search_document).not_to be_present
287
+ it "stores base_class.name as searchable_type for subclass rows" do
288
+ cat = PgSearch.disable_multisearch { Cat.create!(name: "Whiskers") }
289
+
290
+ described_class.new(Cat).rebuild
291
+
292
+ document = PgSearch::Document.find_by!(searchable_id: cat.id, searchable_type: "Animal")
293
+ expect(document.content).to eq("Whiskers")
313
294
  end
314
- # standard:enable RSpec/ExampleLength
315
295
  end
296
+ end
316
297
 
317
- context "via :unless" do
318
- with_model :Model do
298
+ context "when :against includes non-column dynamic methods" do
299
+ with_model :Book do
300
+ table
301
+
302
+ model do
303
+ include PgSearch::Model
304
+
305
+ multisearchable against: [:summary]
306
+
307
+ def summary = "dynamic"
308
+ end
309
+ end
310
+
311
+ it "falls back to find_each and creates a document per record" do
312
+ book = PgSearch.disable_multisearch { Book.create! }
313
+ PgSearch::Document.delete_all
314
+
315
+ described_class.new(Book).rebuild
316
+
317
+ expect(book.pg_search_document).to be_present
318
+ end
319
+ end
320
+
321
+ context "when only additional_attributes is set" do
322
+ with_model :Book do
323
+ table do |t|
324
+ t.string :title
325
+ end
326
+
327
+ model do
328
+ include PgSearch::Model
329
+
330
+ multisearchable against: :title,
331
+ additional_attributes: ->(obj) { {additional_attribute_column: "#{obj.class}::#{obj.id}"} }
332
+ end
333
+ end
334
+
335
+ it "falls back to find_each and populates additional attributes" do
336
+ book1 = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
337
+ book2 = PgSearch.disable_multisearch { Book.create!(title: "Foundation") }
338
+ PgSearch::Document.delete_all
339
+
340
+ described_class.new(Book).rebuild
341
+
342
+ expect(book1.reload.pg_search_document.additional_attribute_column).to eq("Book::#{book1.id}")
343
+ expect(book2.reload.pg_search_document.additional_attribute_column).to eq("Book::#{book2.id}")
344
+ end
345
+ end
346
+
347
+ context "when multisearchable is conditional" do
348
+ context "via :if" do
349
+ with_model :Book do
319
350
  table do |t|
320
- t.boolean :inactive
351
+ t.boolean :published
321
352
  end
322
353
 
323
354
  model do
324
355
  include PgSearch::Model
325
- multisearchable unless: :inactive?
356
+
357
+ multisearchable if: :published?
326
358
  end
327
359
  end
328
360
 
329
- # standard:disable RSpec/ExampleLength
330
- it "calls update_pg_search_document on each record" do
331
- record_1 = Model.create!(inactive: true)
332
- record_2 = Model.create!(inactive: false)
361
+ it "falls back to find_each and only creates documents for matching records" do
362
+ pub = PgSearch.disable_multisearch { Book.create!(published: true) }
363
+ draft = PgSearch.disable_multisearch { Book.create!(published: false) }
364
+ PgSearch::Document.delete_all
333
365
 
334
- rebuilder = described_class.new(Model)
366
+ described_class.new(Book).rebuild
335
367
 
336
- # stub respond_to? to return false since should_not_receive defines the method
337
- original_respond_to = Model.method(:respond_to?)
338
- allow(Model).to receive(:respond_to?) do |method_name, *args|
339
- if method_name == :rebuild_pg_search_documents
340
- false
341
- else
342
- original_respond_to.call(method_name, *args)
343
- end
368
+ expect(pub.pg_search_document).to be_present
369
+ expect(draft.pg_search_document).to be_nil
370
+ end
371
+ end
372
+
373
+ context "via :unless" do
374
+ with_model :Book do
375
+ table do |t|
376
+ t.boolean :archived
344
377
  end
345
378
 
346
- without_partial_double_verification do
347
- allow(Model).to receive(:rebuild_pg_search_documents)
348
- rebuilder.rebuild
349
- expect(Model).not_to have_received(:rebuild_pg_search_documents)
379
+ model do
380
+ include PgSearch::Model
381
+
382
+ multisearchable unless: :archived?
350
383
  end
384
+ end
385
+
386
+ it "falls back to find_each and skips records matching the condition" do
387
+ live = PgSearch.disable_multisearch { Book.create!(archived: false) }
388
+ archived = PgSearch.disable_multisearch { Book.create!(archived: true) }
389
+ PgSearch::Document.delete_all
390
+
391
+ described_class.new(Book).rebuild
351
392
 
352
- expect(record_1.pg_search_document).not_to be_present
353
- expect(record_2.pg_search_document).to be_present
393
+ expect(live.pg_search_document).to be_present
394
+ expect(archived.pg_search_document).to be_nil
354
395
  end
355
- # standard:enable RSpec/ExampleLength
356
396
  end
357
397
  end
358
398
  end
@@ -146,15 +146,14 @@ describe PgSearch::Multisearch do
146
146
 
147
147
  it "generates the proper SQL code" do
148
148
  expected_sql = <<~SQL.squish
149
- INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
149
+ INSERT INTO #{PgSearch::Document.quoted_table_name}
150
+ ("searchable_type", "searchable_id", "content", "created_at", "updated_at")
150
151
  SELECT #{connection.quote(model.name)} AS searchable_type,
151
- #{model.quoted_table_name}.id AS searchable_id,
152
- (
153
- coalesce(#{model.quoted_table_name}."title"::text, '')
154
- ) AS content,
155
- #{connection.quote(connection.quoted_date(now))} AS created_at,
156
- #{connection.quote(connection.quoted_date(now))} AS updated_at
157
- FROM #{model.quoted_table_name}
152
+ #{model.quoted_table_name}.#{connection.quote_column_name(model.primary_key)} AS searchable_id,
153
+ coalesce(cast(#{model.quoted_table_name}."title" AS text), '') AS content,
154
+ #{connection.quote(connection.quoted_date(now))} AS created_at,
155
+ #{connection.quote(connection.quoted_date(now))} AS updated_at
156
+ FROM #{model.quoted_table_name}
158
157
  SQL
159
158
 
160
159
  statements = []
@@ -173,15 +172,16 @@ describe PgSearch::Multisearch do
173
172
 
174
173
  it "generates the proper SQL code" do
175
174
  expected_sql = <<~SQL.squish
176
- INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
175
+ INSERT INTO #{PgSearch::Document.quoted_table_name}
176
+ ("searchable_type", "searchable_id", "content", "created_at", "updated_at")
177
177
  SELECT #{connection.quote(model.name)} AS searchable_type,
178
- #{model.quoted_table_name}.id AS searchable_id,
179
- (
180
- coalesce(#{model.quoted_table_name}."title"::text, '') || ' ' || coalesce(#{model.quoted_table_name}."content"::text, '')
181
- ) AS content,
182
- #{connection.quote(connection.quoted_date(now))} AS created_at,
183
- #{connection.quote(connection.quoted_date(now))} AS updated_at
184
- FROM #{model.quoted_table_name}
178
+ #{model.quoted_table_name}.#{connection.quote_column_name(model.primary_key)} AS searchable_id,
179
+ coalesce(cast(#{model.quoted_table_name}."title" AS text), '') ||
180
+ ' ' ||
181
+ coalesce(cast(#{model.quoted_table_name}."content" AS text), '') AS content,
182
+ #{connection.quote(connection.quoted_date(now))} AS created_at,
183
+ #{connection.quote(connection.quoted_date(now))} AS updated_at
184
+ FROM #{model.quoted_table_name}
185
185
  SQL
186
186
 
187
187
  statements = []