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