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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +38 -37
- data/.github/workflows/codeql.yml +86 -0
- data/.gitignore +0 -1
- data/.standard.yml +2 -2
- data/CHANGELOG.md +81 -64
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +4 -2
- data/LICENSE +1 -1
- data/README.md +89 -87
- data/Rakefile +1 -7
- 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/configuration.rb +10 -26
- 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 +47 -61
- data/lib/pg_search/features.rb +2 -0
- data/lib/pg_search/migration/generator.rb +1 -5
- data/lib/pg_search/model.rb +6 -8
- data/lib/pg_search/multisearch/rebuilder.rb +69 -64
- data/lib/pg_search/normalizer.rb +40 -11
- data/lib/pg_search/scope_options.rb +32 -49
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +5 -5
- data/spec/integration/associations_spec.rb +133 -0
- data/spec/integration/deprecation_spec.rb +5 -1
- data/spec/integration/pagination_spec.rb +1 -0
- data/spec/integration/pg_search_spec.rb +78 -1
- data/spec/integration/single_table_inheritance_spec.rb +2 -0
- data/spec/lib/pg_search/configuration/association_spec.rb +34 -58
- data/spec/lib/pg_search/configuration/column_spec.rb +73 -15
- data/spec/lib/pg_search/configuration/foreign_column_spec.rb +53 -20
- data/spec/lib/pg_search/features/dmetaphone_spec.rb +2 -2
- data/spec/lib/pg_search/features/trigram_spec.rb +8 -9
- data/spec/lib/pg_search/features/tsearch_spec.rb +8 -8
- data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +251 -211
- data/spec/lib/pg_search/multisearch_spec.rb +16 -16
- data/spec/lib/pg_search/multisearchable_spec.rb +8 -0
- data/spec/lib/pg_search/normalizer_spec.rb +64 -11
- data/spec/lib/pg_search_spec.rb +6 -21
- data/spec/spec_helper.rb +0 -13
- metadata +11 -15
- data/spec/.rubocop.yml +0 -27
- 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 :
|
|
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(
|
|
16
|
+
described_class.new(NotSearchable)
|
|
15
17
|
}.to raise_exception(
|
|
16
18
|
PgSearch::Multisearch::ModelNotMultisearchable,
|
|
17
|
-
"
|
|
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 :
|
|
27
|
+
with_model :Book do
|
|
28
|
+
table
|
|
29
|
+
|
|
26
30
|
model do
|
|
27
31
|
include PgSearch::Model
|
|
32
|
+
|
|
28
33
|
multisearchable
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
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 "
|
|
36
|
-
|
|
41
|
+
it "dispatches to .rebuild_pg_search_documents" do
|
|
42
|
+
described_class.new(Book).rebuild
|
|
37
43
|
|
|
38
|
-
|
|
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 |
|
|
48
|
-
context "via :#{
|
|
49
|
-
with_model :
|
|
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
|
-
|
|
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 "
|
|
64
|
-
|
|
69
|
+
it "dispatches to .rebuild_pg_search_documents" do
|
|
70
|
+
described_class.new(Book).rebuild
|
|
65
71
|
|
|
66
|
-
|
|
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 :
|
|
82
|
+
with_model :Book do
|
|
81
83
|
table do |t|
|
|
82
|
-
t.string :
|
|
84
|
+
t.string :title
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
model do
|
|
86
88
|
include PgSearch::Model
|
|
87
|
-
|
|
89
|
+
|
|
90
|
+
multisearchable against: :title
|
|
88
91
|
end
|
|
89
92
|
end
|
|
90
93
|
|
|
91
|
-
it "
|
|
92
|
-
|
|
94
|
+
it "inserts a document for each record via bulk INSERT" do
|
|
95
|
+
book = PgSearch.disable_multisearch { Book.create!(title: "Dune") }
|
|
93
96
|
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
135
|
-
ActiveSupport::Notifications.unsubscribe(notifier)
|
|
108
|
+
described_class.new(Book).rebuild
|
|
136
109
|
|
|
137
|
-
expect(
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
139
|
+
model do
|
|
140
|
+
include PgSearch::Model
|
|
141
|
+
|
|
142
|
+
multisearchable against: %i[title body]
|
|
158
143
|
end
|
|
159
144
|
end
|
|
160
145
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
include PgSearch::Model
|
|
169
|
-
multisearchable against: :name
|
|
170
|
-
end
|
|
171
|
-
end
|
|
149
|
+
described_class.new(Book).rebuild
|
|
172
150
|
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 "
|
|
207
|
-
with_model :
|
|
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
|
-
|
|
213
|
-
"bar"
|
|
214
|
-
end
|
|
173
|
+
multisearchable against: :camelName
|
|
215
174
|
end
|
|
216
175
|
end
|
|
217
176
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
180
|
+
described_class.new(Book).rebuild
|
|
223
181
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
192
|
+
model do
|
|
193
|
+
include PgSearch::Model
|
|
238
194
|
|
|
239
|
-
|
|
195
|
+
multisearchable against: :title
|
|
240
196
|
end
|
|
197
|
+
end
|
|
241
198
|
|
|
242
|
-
|
|
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 "
|
|
248
|
-
with_model :
|
|
208
|
+
context "with an identifier that requires quoting" do
|
|
209
|
+
with_model :Book do
|
|
249
210
|
table do |t|
|
|
250
|
-
t.string :
|
|
211
|
+
t.string :title
|
|
251
212
|
end
|
|
252
213
|
|
|
253
214
|
model do
|
|
254
215
|
include PgSearch::Model
|
|
255
|
-
|
|
256
|
-
|
|
216
|
+
|
|
217
|
+
multisearchable against: :title
|
|
257
218
|
end
|
|
258
219
|
end
|
|
259
220
|
|
|
260
|
-
it "
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
rebuilder.rebuild
|
|
235
|
+
sql = described_class.new(Book).send(:rebuild_sql)
|
|
268
236
|
|
|
269
|
-
expect(
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
with_model :Model do
|
|
241
|
+
context "with STI" do
|
|
242
|
+
with_model :Animal do
|
|
278
243
|
table do |t|
|
|
279
|
-
t.
|
|
244
|
+
t.string :type
|
|
245
|
+
t.string :name
|
|
280
246
|
end
|
|
281
247
|
|
|
282
248
|
model do
|
|
283
249
|
include PgSearch::Model
|
|
284
|
-
|
|
250
|
+
|
|
251
|
+
multisearchable against: :name
|
|
285
252
|
end
|
|
286
253
|
end
|
|
287
254
|
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
258
|
+
model do
|
|
259
|
+
include PgSearch::Model
|
|
294
260
|
|
|
295
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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 :
|
|
351
|
+
t.boolean :published
|
|
321
352
|
end
|
|
322
353
|
|
|
323
354
|
model do
|
|
324
355
|
include PgSearch::Model
|
|
325
|
-
|
|
356
|
+
|
|
357
|
+
multisearchable if: :published?
|
|
326
358
|
end
|
|
327
359
|
end
|
|
328
360
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
366
|
+
described_class.new(Book).rebuild
|
|
335
367
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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(
|
|
353
|
-
expect(
|
|
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}
|
|
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 = []
|