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
|
@@ -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 :
|
|
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(
|
|
16
|
+
described_class.new(NotSearchable)
|
|
17
17
|
}.to raise_exception(
|
|
18
18
|
PgSearch::Multisearch::ModelNotMultisearchable,
|
|
19
|
-
"
|
|
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 :
|
|
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
|
-
|
|
36
|
-
|
|
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 "
|
|
41
|
-
|
|
41
|
+
it "dispatches to .rebuild_pg_search_documents" do
|
|
42
|
+
described_class.new(Book).rebuild
|
|
42
43
|
|
|
43
|
-
|
|
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 |
|
|
53
|
-
context "via :#{
|
|
54
|
-
with_model :
|
|
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
|
|
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 "
|
|
70
|
-
|
|
69
|
+
it "dispatches to .rebuild_pg_search_documents" do
|
|
70
|
+
described_class.new(Book).rebuild
|
|
71
71
|
|
|
72
|
-
|
|
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 :
|
|
82
|
+
with_model :Book do
|
|
87
83
|
table do |t|
|
|
88
|
-
t.string :
|
|
84
|
+
t.string :title
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
model do
|
|
92
88
|
include PgSearch::Model
|
|
93
89
|
|
|
94
|
-
multisearchable against: :
|
|
90
|
+
multisearchable against: :title
|
|
95
91
|
end
|
|
96
92
|
end
|
|
97
93
|
|
|
98
|
-
it "
|
|
99
|
-
|
|
94
|
+
it "inserts a document for each record via bulk INSERT" do
|
|
95
|
+
book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
|
|
100
96
|
|
|
101
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
142
|
-
ActiveSupport::Notifications.unsubscribe(notifier)
|
|
108
|
+
described_class.new(Book).rebuild
|
|
143
109
|
|
|
144
|
-
expect(
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
include PgSearch::Model
|
|
121
|
+
book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
|
|
157
122
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
139
|
+
model do
|
|
140
|
+
include PgSearch::Model
|
|
141
|
+
|
|
142
|
+
multisearchable against: %i[title body]
|
|
166
143
|
end
|
|
167
144
|
end
|
|
168
145
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
include PgSearch::Model
|
|
149
|
+
described_class.new(Book).rebuild
|
|
177
150
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
end
|
|
151
|
+
expect(book.pg_search_document.content).to eq("Dune The spice")
|
|
152
|
+
end
|
|
181
153
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
206
|
-
ActiveSupport::Notifications.unsubscribe(notifier)
|
|
157
|
+
described_class.new(Book).rebuild
|
|
207
158
|
|
|
208
|
-
|
|
209
|
-
|
|
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 "
|
|
216
|
-
with_model :
|
|
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:
|
|
223
|
-
|
|
224
|
-
def foo
|
|
225
|
-
"bar"
|
|
226
|
-
end
|
|
173
|
+
multisearchable against: :camelName
|
|
227
174
|
end
|
|
228
175
|
end
|
|
229
176
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
180
|
+
described_class.new(Book).rebuild
|
|
235
181
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
192
|
+
model do
|
|
193
|
+
include PgSearch::Model
|
|
250
194
|
|
|
251
|
-
|
|
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(
|
|
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 :
|
|
209
|
+
with_model :Book do
|
|
261
210
|
table do |t|
|
|
262
|
-
t.string :
|
|
211
|
+
t.string :title
|
|
263
212
|
end
|
|
264
213
|
|
|
265
214
|
model do
|
|
266
215
|
include PgSearch::Model
|
|
267
216
|
|
|
268
|
-
multisearchable against: :
|
|
217
|
+
multisearchable against: :title
|
|
269
218
|
end
|
|
270
219
|
end
|
|
271
220
|
|
|
272
|
-
it "quotes
|
|
221
|
+
it "quotes a malicious primary key in the generated SQL" do
|
|
273
222
|
malicious_pk = %(id"; DROP TABLE pg_search_documents; --)
|
|
274
|
-
allow(
|
|
223
|
+
allow(Book).to receive(:primary_key).and_return(malicious_pk)
|
|
275
224
|
|
|
276
|
-
|
|
277
|
-
sql = rebuilder.send(:rebuild_sql)
|
|
225
|
+
sql = described_class.new(Book).send(:rebuild_sql)
|
|
278
226
|
|
|
279
|
-
expect(sql).to include(
|
|
227
|
+
expect(sql).to include(Book.connection.quote_column_name(malicious_pk))
|
|
280
228
|
end
|
|
281
229
|
|
|
282
|
-
it "quotes
|
|
230
|
+
it "quotes a malicious inheritance column in the generated SQL" do
|
|
283
231
|
malicious_column = %(type"; DROP TABLE pg_search_documents; --)
|
|
284
|
-
allow(
|
|
285
|
-
allow(
|
|
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
|
-
|
|
288
|
-
sql = rebuilder.send(:rebuild_sql)
|
|
235
|
+
sql = described_class.new(Book).send(:rebuild_sql)
|
|
289
236
|
|
|
290
|
-
expect(sql).to include(
|
|
237
|
+
expect(sql).to include(Book.connection.quote_column_name(malicious_column))
|
|
291
238
|
end
|
|
292
239
|
end
|
|
293
240
|
|
|
294
|
-
context "
|
|
295
|
-
with_model :
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
record_2 = Model.create!(name: "record_2", id: 2)
|
|
255
|
+
with_model :Cat, superclass: :Animal do
|
|
256
|
+
table(false)
|
|
311
257
|
|
|
312
|
-
|
|
258
|
+
model do
|
|
259
|
+
include PgSearch::Model
|
|
313
260
|
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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 :
|
|
349
|
+
with_model :Book do
|
|
326
350
|
table do |t|
|
|
327
|
-
t.boolean :
|
|
351
|
+
t.boolean :published
|
|
328
352
|
end
|
|
329
353
|
|
|
330
354
|
model do
|
|
331
355
|
include PgSearch::Model
|
|
332
356
|
|
|
333
|
-
multisearchable if: :
|
|
357
|
+
multisearchable if: :published?
|
|
334
358
|
end
|
|
335
359
|
end
|
|
336
360
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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(
|
|
361
|
-
expect(
|
|
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 :
|
|
374
|
+
with_model :Book do
|
|
368
375
|
table do |t|
|
|
369
|
-
t.boolean :
|
|
376
|
+
t.boolean :archived
|
|
370
377
|
end
|
|
371
378
|
|
|
372
379
|
model do
|
|
373
380
|
include PgSearch::Model
|
|
374
381
|
|
|
375
|
-
multisearchable unless: :
|
|
382
|
+
multisearchable unless: :archived?
|
|
376
383
|
end
|
|
377
384
|
end
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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(
|
|
403
|
-
expect(
|
|
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}
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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}
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 = []
|