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
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/core_ext/module/delegation"
|
|
4
|
-
|
|
5
3
|
module PgSearch
|
|
6
4
|
class ScopeOptions
|
|
7
5
|
attr_reader :config, :feature_options, :model
|
|
@@ -15,10 +13,12 @@ module PgSearch
|
|
|
15
13
|
def apply(scope)
|
|
16
14
|
scope = include_table_aliasing_for_rank(scope)
|
|
17
15
|
rank_table_alias = scope.pg_search_rank_table_alias(include_counter: true)
|
|
16
|
+
rank_subquery = subquery.arel.as(rank_table_alias)
|
|
18
17
|
|
|
19
18
|
scope
|
|
20
|
-
.joins(rank_join(
|
|
21
|
-
.order(
|
|
19
|
+
.joins(rank_join(rank_subquery))
|
|
20
|
+
.order(rank_subquery[:rank].desc)
|
|
21
|
+
.order(order_within_rank)
|
|
22
22
|
.extend(WithPgSearchRank)
|
|
23
23
|
.extend(WithPgSearchHighlight[feature_for(:tsearch)])
|
|
24
24
|
end
|
|
@@ -38,20 +38,18 @@ module PgSearch
|
|
|
38
38
|
|
|
39
39
|
def with_pg_search_highlight
|
|
40
40
|
scope = self
|
|
41
|
-
scope = scope.select(
|
|
42
|
-
scope.select("
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def highlight
|
|
46
|
-
tsearch.highlight.to_sql
|
|
41
|
+
scope = scope.select(arel_table[Arel.star]) if scope.select_values.empty?
|
|
42
|
+
scope.select(tsearch.highlight.as("pg_search_highlight"))
|
|
47
43
|
end
|
|
48
44
|
end
|
|
49
45
|
|
|
50
46
|
module WithPgSearchRank
|
|
51
47
|
def with_pg_search_rank
|
|
52
48
|
scope = self
|
|
53
|
-
scope = scope.select(
|
|
54
|
-
|
|
49
|
+
scope = scope.select(arel_table[Arel.star]) if scope.select_values.empty?
|
|
50
|
+
rank_table = arel_table.alias(pg_search_rank_table_alias)
|
|
51
|
+
rank_column = Arel::Attributes::Attribute.new(rank_table, "rank")
|
|
52
|
+
scope.select(rank_column.as("pg_search_rank"))
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
|
|
@@ -77,13 +75,11 @@ module PgSearch
|
|
|
77
75
|
|
|
78
76
|
private
|
|
79
77
|
|
|
80
|
-
delegate :connection, :quoted_table_name, to: :model
|
|
81
|
-
|
|
82
78
|
def subquery
|
|
83
79
|
model
|
|
84
80
|
.unscoped
|
|
85
|
-
.select("
|
|
86
|
-
.select("
|
|
81
|
+
.select(model.arel_table[model.primary_key].as("pg_search_id"))
|
|
82
|
+
.select(rank.as("rank"))
|
|
87
83
|
.joins(subquery_join)
|
|
88
84
|
.where(conditions)
|
|
89
85
|
.limit(nil)
|
|
@@ -96,42 +92,23 @@ module PgSearch
|
|
|
96
92
|
.reject { |_feature_name, feature_options| feature_options && feature_options[:sort_only] }
|
|
97
93
|
.map { |feature_name, _feature_options| feature_for(feature_name).conditions }
|
|
98
94
|
|
|
99
|
-
|
|
95
|
+
Arel::Nodes::Or.new(expressions)
|
|
100
96
|
end
|
|
101
97
|
|
|
102
|
-
# https://github.com/rails/rails/pull/51492
|
|
103
|
-
# :nocov:
|
|
104
|
-
# standard:disable Lint/DuplicateMethods
|
|
105
|
-
or_arity = Arel::Nodes::Or.instance_method(:initialize).arity
|
|
106
|
-
case or_arity
|
|
107
|
-
when 1
|
|
108
|
-
def or_node(expressions)
|
|
109
|
-
Arel::Nodes::Or.new(expressions)
|
|
110
|
-
end
|
|
111
|
-
when 2
|
|
112
|
-
def or_node(expressions)
|
|
113
|
-
expressions.inject { |accumulator, expression| Arel::Nodes::Or.new(accumulator, expression) }
|
|
114
|
-
end
|
|
115
|
-
else
|
|
116
|
-
raise "Unsupported arity #{or_arity} for Arel::Nodes::Or#initialize"
|
|
117
|
-
end
|
|
118
|
-
# :nocov:
|
|
119
|
-
# standard:enable Lint/DuplicateMethods
|
|
120
|
-
|
|
121
98
|
def order_within_rank
|
|
122
|
-
config.order_within_rank
|
|
99
|
+
if config.order_within_rank
|
|
100
|
+
Arel.sql(config.order_within_rank)
|
|
101
|
+
else
|
|
102
|
+
model.arel_table[model.primary_key].asc
|
|
103
|
+
end
|
|
123
104
|
end
|
|
124
105
|
|
|
125
106
|
def primary_key
|
|
126
|
-
|
|
107
|
+
Arel::Attributes::Attribute.new(model.arel_table, model.primary_key)
|
|
127
108
|
end
|
|
128
109
|
|
|
129
110
|
def subquery_join
|
|
130
|
-
|
|
131
|
-
config.associations.map do |association|
|
|
132
|
-
association.join(primary_key)
|
|
133
|
-
end.join(" ")
|
|
134
|
-
end
|
|
111
|
+
config.associations.map { |association| association.to_arel(primary_key) }
|
|
135
112
|
end
|
|
136
113
|
|
|
137
114
|
FEATURE_CLASSES = { # standard:disable Lint/UselessConstantScoping
|
|
@@ -158,18 +135,17 @@ module PgSearch
|
|
|
158
135
|
end
|
|
159
136
|
|
|
160
137
|
def rank
|
|
161
|
-
(
|
|
138
|
+
return feature_for(:tsearch).rank unless config.ranking_sql
|
|
139
|
+
|
|
140
|
+
Arel.sql(config.ranking_sql.gsub(/:(\w*)/) do
|
|
162
141
|
feature_for(Regexp.last_match(1)).rank.to_sql
|
|
163
|
-
end
|
|
142
|
+
end)
|
|
164
143
|
end
|
|
165
144
|
|
|
166
|
-
def rank_join(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
arel_table
|
|
171
|
-
.join(subquery_arel)
|
|
172
|
-
.on(arel_table[model.primary_key].eq(subquery_arel[:pg_search_id]))
|
|
145
|
+
def rank_join(rank_subquery)
|
|
146
|
+
model.arel_table
|
|
147
|
+
.join(rank_subquery)
|
|
148
|
+
.on(model.arel_table[model.primary_key].eq(rank_subquery[:pg_search_id]))
|
|
173
149
|
.join_sources
|
|
174
150
|
end
|
|
175
151
|
|
data/lib/pg_search/version.rb
CHANGED
data/pg_search.gemspec
CHANGED
|
@@ -7,8 +7,8 @@ Gem::Specification.new do |s|
|
|
|
7
7
|
s.name = "pg_search"
|
|
8
8
|
s.version = PgSearch::VERSION
|
|
9
9
|
s.platform = Gem::Platform::RUBY
|
|
10
|
-
s.authors = ["Grant Hutchins"
|
|
11
|
-
s.email =
|
|
10
|
+
s.authors = ["Grant Hutchins"]
|
|
11
|
+
s.email = ["gems@nertzy.com"]
|
|
12
12
|
s.homepage = "https://github.com/Casecommons/pg_search"
|
|
13
13
|
s.summary = "PgSearch builds Active Record named scopes that take advantage of PostgreSQL's full text search"
|
|
14
14
|
s.description = "PgSearch builds Active Record named scopes that take advantage of PostgreSQL's full text search"
|
|
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
|
|
|
18
18
|
s.files = `git ls-files -z`.split("\x0")
|
|
19
19
|
s.require_paths = ["lib"]
|
|
20
20
|
|
|
21
|
-
s.add_dependency "activerecord", ">=
|
|
22
|
-
s.add_dependency "activesupport", ">=
|
|
21
|
+
s.add_dependency "activerecord", ">= 8.0"
|
|
22
|
+
s.add_dependency "activesupport", ">= 8.0"
|
|
23
23
|
|
|
24
24
|
s.required_ruby_version = ">= 3.3"
|
|
25
25
|
end
|
|
@@ -145,6 +145,127 @@ describe "a pg_search_scope" do
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
context "via a scoped has_many association with a quoted literal condition" do
|
|
149
|
+
with_model :Book do
|
|
150
|
+
table do |t|
|
|
151
|
+
t.string "title"
|
|
152
|
+
t.string "author"
|
|
153
|
+
t.belongs_to :shelf
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
model do
|
|
157
|
+
belongs_to :shelf
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
with_model :Shelf do
|
|
162
|
+
table do |t|
|
|
163
|
+
t.string "title"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
model do
|
|
167
|
+
include PgSearch::Model
|
|
168
|
+
|
|
169
|
+
has_many :obrien_books,
|
|
170
|
+
-> { where(author: "O'Brien") },
|
|
171
|
+
class_name: "Book"
|
|
172
|
+
|
|
173
|
+
pg_search_scope :with_obrien_books,
|
|
174
|
+
associated_against: {obrien_books: :title}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "preserves scoped values through searches" do
|
|
179
|
+
shelf_with_obrien = Shelf.create!(title: "Main Shelf")
|
|
180
|
+
shelf_without_obrien = Shelf.create!(title: "Other Shelf")
|
|
181
|
+
|
|
182
|
+
Book.create!(
|
|
183
|
+
title: "The Third Policeman",
|
|
184
|
+
author: "O'Brien",
|
|
185
|
+
shelf: shelf_with_obrien
|
|
186
|
+
)
|
|
187
|
+
Book.create!(
|
|
188
|
+
title: "The Third Policeman",
|
|
189
|
+
author: "Someone Else",
|
|
190
|
+
shelf: shelf_without_obrien
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
results = Shelf.with_obrien_books("Third Policeman")
|
|
194
|
+
expect(results).to include(shelf_with_obrien)
|
|
195
|
+
expect(results).not_to include(shelf_without_obrien)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context "with a physical source column shadowed by a model alias" do
|
|
200
|
+
with_model :Book do
|
|
201
|
+
table do |t|
|
|
202
|
+
t.string :title
|
|
203
|
+
t.string :name
|
|
204
|
+
t.belongs_to :shelf
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
model do
|
|
208
|
+
belongs_to :shelf
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
with_model :Shelf do
|
|
213
|
+
table
|
|
214
|
+
|
|
215
|
+
model do
|
|
216
|
+
include PgSearch::Model
|
|
217
|
+
|
|
218
|
+
has_many :books
|
|
219
|
+
|
|
220
|
+
pg_search_scope :search, associated_against: {books: :title}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "searches the physical association source through the derived alias" do
|
|
225
|
+
shelf = Shelf.create!
|
|
226
|
+
shelf.books.create!(title: "physical", name: "alias")
|
|
227
|
+
Book.alias_attribute :title, :name
|
|
228
|
+
|
|
229
|
+
expect(Shelf.search("physical")).to eq [shelf]
|
|
230
|
+
expect(Shelf.search("alias")).to be_empty
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
context "with a trusted SQL expression as the association source" do
|
|
235
|
+
with_model :Book do
|
|
236
|
+
table do |t|
|
|
237
|
+
t.json :metadata
|
|
238
|
+
t.belongs_to :shelf
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
model do
|
|
242
|
+
belongs_to :shelf
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
with_model :Shelf do
|
|
247
|
+
table
|
|
248
|
+
|
|
249
|
+
model do
|
|
250
|
+
include PgSearch::Model
|
|
251
|
+
|
|
252
|
+
has_many :books
|
|
253
|
+
|
|
254
|
+
pg_search_scope :search,
|
|
255
|
+
associated_against: {books: Arel.sql("metadata->>'title'")}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it "evaluates the expression before aggregating the associated rows" do
|
|
260
|
+
shelf = Shelf.create!
|
|
261
|
+
shelf.books.create!(metadata: {title: "Hello"})
|
|
262
|
+
shelf.books.create!(metadata: {title: "world"})
|
|
263
|
+
|
|
264
|
+
expect(Shelf.search("Hello world")).to eq [shelf]
|
|
265
|
+
expect(Shelf.search("title")).to be_empty
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
148
269
|
context "when across multiple associations" do
|
|
149
270
|
context "when on different tables" do
|
|
150
271
|
with_model :FirstAssociatedModel do
|
|
@@ -1112,6 +1112,37 @@ describe "an Active Record model which includes PgSearch" do
|
|
|
1112
1112
|
end
|
|
1113
1113
|
end
|
|
1114
1114
|
|
|
1115
|
+
context "with quoted identifiers and a rank attribute alias" do
|
|
1116
|
+
with_model :Article do
|
|
1117
|
+
table primary_key: "ArticleID" do |t|
|
|
1118
|
+
t.text :content
|
|
1119
|
+
t.tsvector "SearchVector"
|
|
1120
|
+
t.integer :importance
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
model do
|
|
1124
|
+
include PgSearch::Model
|
|
1125
|
+
|
|
1126
|
+
alias_attribute :rank, :importance
|
|
1127
|
+
pg_search_scope :search_content,
|
|
1128
|
+
using: {tsearch: {tsvector_column: "SearchVector"}}
|
|
1129
|
+
end
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
it "searches the physical vector and selects the synthetic rank" do
|
|
1133
|
+
article = Article.create!(content: "phooey", importance: 10)
|
|
1134
|
+
Article.update_all(
|
|
1135
|
+
%("SearchVector" = to_tsvector('simple', "content"))
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
result = Article.search_content("phooey").with_pg_search_rank.first
|
|
1139
|
+
|
|
1140
|
+
expect(result).to eq(article)
|
|
1141
|
+
expect(result.pg_search_rank).to be_a(Float)
|
|
1142
|
+
expect(result.pg_search_rank).not_to eq(result.importance)
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1115
1146
|
context "when using multiple tsvector columns" do
|
|
1116
1147
|
with_model :ModelWithTsvector do
|
|
1117
1148
|
table
|
|
@@ -1228,6 +1259,23 @@ describe "an Active Record model which includes PgSearch" do
|
|
|
1228
1259
|
end
|
|
1229
1260
|
end
|
|
1230
1261
|
|
|
1262
|
+
context "when passed an :order_within_rank expression" do
|
|
1263
|
+
before do
|
|
1264
|
+
ModelWithPgSearch.pg_search_scope :search_content_ordered_by_importance,
|
|
1265
|
+
against: :content,
|
|
1266
|
+
order_within_rank: "importance DESC"
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
it "breaks ties in rank by the custom expression" do
|
|
1270
|
+
# Both records match equally; importance determines order
|
|
1271
|
+
low = ModelWithPgSearch.create!(content: "foo", importance: 1)
|
|
1272
|
+
high = ModelWithPgSearch.create!(content: "foo", importance: 5)
|
|
1273
|
+
|
|
1274
|
+
results = ModelWithPgSearch.search_content_ordered_by_importance("foo")
|
|
1275
|
+
expect(results).to eq([high, low])
|
|
1276
|
+
end
|
|
1277
|
+
end
|
|
1278
|
+
|
|
1231
1279
|
context "when passed a :ranked_by expression" do
|
|
1232
1280
|
before do
|
|
1233
1281
|
ModelWithPgSearch.pg_search_scope :search_content_with_default_rank,
|
|
@@ -20,7 +20,7 @@ describe PgSearch::Configuration::Association do
|
|
|
20
20
|
model do
|
|
21
21
|
include PgSearch::Model
|
|
22
22
|
|
|
23
|
-
has_one :avatar
|
|
23
|
+
has_one :avatar
|
|
24
24
|
belongs_to :site
|
|
25
25
|
|
|
26
26
|
pg_search_scope :with_avatar, associated_against: {avatar: :url}
|
|
@@ -36,7 +36,7 @@ describe PgSearch::Configuration::Association do
|
|
|
36
36
|
model do
|
|
37
37
|
include PgSearch::Model
|
|
38
38
|
|
|
39
|
-
has_many :users
|
|
39
|
+
has_many :users
|
|
40
40
|
|
|
41
41
|
pg_search_scope :with_users, associated_against: {users: :name}
|
|
42
42
|
end
|
|
@@ -50,27 +50,6 @@ describe PgSearch::Configuration::Association do
|
|
|
50
50
|
expect(association.table_name).to eq Avatar.table_name
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
-
|
|
54
|
-
describe "#join" do
|
|
55
|
-
let(:expected_sql) do
|
|
56
|
-
<<~SQL.squish
|
|
57
|
-
LEFT OUTER JOIN
|
|
58
|
-
(SELECT model_id AS id,
|
|
59
|
-
#{column_select} AS #{association.columns.first.alias}
|
|
60
|
-
FROM "#{User.table_name}"
|
|
61
|
-
INNER JOIN "#{association.table_name}"
|
|
62
|
-
ON "#{association.table_name}"."user_id" = "#{User.table_name}"."id") #{association.subselect_alias}
|
|
63
|
-
ON #{association.subselect_alias}.id = model_id
|
|
64
|
-
SQL
|
|
65
|
-
end
|
|
66
|
-
let(:column_select) do
|
|
67
|
-
"\"#{association.table_name}\".\"url\"::text"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it "returns the correct SQL join" do
|
|
71
|
-
expect(association.join("model_id")).to eq(expected_sql)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
53
|
end
|
|
75
54
|
|
|
76
55
|
context "with belongs_to" do
|
|
@@ -81,27 +60,6 @@ describe PgSearch::Configuration::Association do
|
|
|
81
60
|
expect(association.table_name).to eq Site.table_name
|
|
82
61
|
end
|
|
83
62
|
end
|
|
84
|
-
|
|
85
|
-
describe "#join" do
|
|
86
|
-
let(:expected_sql) do
|
|
87
|
-
<<~SQL.squish
|
|
88
|
-
LEFT OUTER JOIN
|
|
89
|
-
(SELECT model_id AS id,
|
|
90
|
-
#{column_select} AS #{association.columns.first.alias}
|
|
91
|
-
FROM "#{User.table_name}"
|
|
92
|
-
INNER JOIN "#{association.table_name}"
|
|
93
|
-
ON "#{association.table_name}"."id" = "#{User.table_name}"."site_id") #{association.subselect_alias}
|
|
94
|
-
ON #{association.subselect_alias}.id = model_id
|
|
95
|
-
SQL
|
|
96
|
-
end
|
|
97
|
-
let(:column_select) do
|
|
98
|
-
"\"#{association.table_name}\".\"title\"::text"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "returns the correct SQL join" do
|
|
102
|
-
expect(association.join("model_id")).to eq(expected_sql)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
63
|
end
|
|
106
64
|
|
|
107
65
|
context "with has_many" do
|
|
@@ -113,22 +71,38 @@ describe PgSearch::Configuration::Association do
|
|
|
113
71
|
end
|
|
114
72
|
end
|
|
115
73
|
|
|
116
|
-
describe "#
|
|
117
|
-
let(:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
74
|
+
describe "#to_arel" do
|
|
75
|
+
let(:projected_column) do
|
|
76
|
+
Site.arel_table.alias(association.subselect_alias)[
|
|
77
|
+
association.columns.first.alias
|
|
78
|
+
].as("document")
|
|
79
|
+
end
|
|
80
|
+
let(:joined_sites) do
|
|
81
|
+
Site.joins(association.to_arel(Site.arel_table[:id]))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "ignores NULL inputs without padding the aggregate" do
|
|
85
|
+
site = Site.create!
|
|
86
|
+
site.users.create!(name: nil)
|
|
87
|
+
site.users.create!(name: "visible")
|
|
88
|
+
site.users.create!(name: nil)
|
|
89
|
+
|
|
90
|
+
expect(joined_sites.pluck(projected_column)).to eq ["visible"]
|
|
128
91
|
end
|
|
129
92
|
|
|
130
|
-
it "
|
|
131
|
-
|
|
93
|
+
it "preserves an all-NULL aggregate" do
|
|
94
|
+
site = Site.create!
|
|
95
|
+
site.users.create!(name: nil)
|
|
96
|
+
site.users.create!(name: nil)
|
|
97
|
+
|
|
98
|
+
expect(joined_sites.pluck(projected_column)).to eq [nil]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "retains the parent when no associated rows exist" do
|
|
102
|
+
site = Site.create!
|
|
103
|
+
|
|
104
|
+
expect(joined_sites.pluck(Site.arel_table[:id])).to eq [site.id]
|
|
105
|
+
expect(joined_sites.pluck(projected_column)).to eq [nil]
|
|
132
106
|
end
|
|
133
107
|
|
|
134
108
|
describe "#subselect_alias" do
|
|
@@ -3,41 +3,99 @@
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
5
|
describe PgSearch::Configuration::Column do
|
|
6
|
-
describe "#
|
|
6
|
+
describe "#to_arel" do
|
|
7
7
|
with_model :Model do
|
|
8
8
|
table do |t|
|
|
9
9
|
t.string :name
|
|
10
|
+
t.integer :count
|
|
10
11
|
t.json :object
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
it "
|
|
15
|
+
it "coalesces NULL values to an empty string when composed into a query" do
|
|
16
|
+
model = Model.create!(name: nil)
|
|
15
17
|
column = described_class.new("name", nil, Model)
|
|
16
|
-
|
|
18
|
+
query = Model.arel_table
|
|
19
|
+
.project(column.to_arel.as("value"))
|
|
20
|
+
.where(Model.arel_table[:id].eq(model.id))
|
|
21
|
+
|
|
22
|
+
sql = Model.connection.visitor.compile(query.ast)
|
|
23
|
+
expect(Model.connection.select_value(sql)).to eq("")
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
it "
|
|
26
|
+
it "casts a non-text column to text when composed into a query" do
|
|
27
|
+
model = Model.create!(count: 42)
|
|
28
|
+
column = described_class.new("count", nil, Model)
|
|
29
|
+
query = Model.arel_table
|
|
30
|
+
.project(column.to_arel.as("value"))
|
|
31
|
+
.where(Model.arel_table[:id].eq(model.id))
|
|
32
|
+
|
|
33
|
+
sql = Model.connection.visitor.compile(query.ast)
|
|
34
|
+
expect(Model.connection.select_value(sql)).to eq("42")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "evaluates a trusted SQL literal JSON expression when composed into a query" do
|
|
38
|
+
model = Model.create!(object: {name: "hello world"})
|
|
20
39
|
column = described_class.new(Arel.sql("object->>'name'"), nil, Model)
|
|
21
|
-
|
|
40
|
+
query = Model.arel_table
|
|
41
|
+
.project(column.to_arel.as("value"))
|
|
42
|
+
.where(Model.arel_table[:id].eq(model.id))
|
|
43
|
+
|
|
44
|
+
sql = Model.connection.visitor.compile(query.ast)
|
|
45
|
+
expect(Model.connection.select_value(sql)).to eq("hello world")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "with quoted identifiers" do
|
|
49
|
+
with_model :QuotedModel do
|
|
50
|
+
table do |t|
|
|
51
|
+
t.string "select"
|
|
52
|
+
t.string "order"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "executes a reserved-word column through a projected Arel node" do
|
|
57
|
+
model = QuotedModel.create!(select: "value1")
|
|
58
|
+
column = described_class.new("select", nil, QuotedModel)
|
|
59
|
+
query = QuotedModel.arel_table
|
|
60
|
+
.project(column.to_arel.as("value"))
|
|
61
|
+
.where(QuotedModel.arel_table[:id].eq(model.id))
|
|
62
|
+
|
|
63
|
+
sql = QuotedModel.connection.visitor.compile(query.ast)
|
|
64
|
+
expect(QuotedModel.connection.select_value(sql)).to eq("value1")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "composes multiple quoted column nodes before execution" do
|
|
68
|
+
model = QuotedModel.create!(select: "abc", order: "def")
|
|
69
|
+
select_column = described_class.new("select", nil, QuotedModel)
|
|
70
|
+
order_column = described_class.new("order", nil, QuotedModel)
|
|
71
|
+
combined = Arel::Nodes::NamedFunction.new(
|
|
72
|
+
"concat",
|
|
73
|
+
[select_column.to_arel, order_column.to_arel]
|
|
74
|
+
)
|
|
75
|
+
query = QuotedModel.arel_table
|
|
76
|
+
.project(combined.as("value"))
|
|
77
|
+
.where(QuotedModel.arel_table[:id].eq(model.id))
|
|
78
|
+
|
|
79
|
+
sql = QuotedModel.connection.visitor.compile(query.ast)
|
|
80
|
+
expect(QuotedModel.connection.select_value(sql)).to eq("abcdef")
|
|
81
|
+
end
|
|
22
82
|
end
|
|
23
83
|
end
|
|
24
84
|
|
|
25
|
-
|
|
26
|
-
with_model :
|
|
85
|
+
context "when a model alias shadows a physical column" do
|
|
86
|
+
with_model :Book do
|
|
27
87
|
table do |t|
|
|
88
|
+
t.string :title
|
|
28
89
|
t.string :name
|
|
29
|
-
t.json :object
|
|
30
90
|
end
|
|
31
91
|
end
|
|
32
92
|
|
|
33
|
-
it "
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
it "normalizes the aliased model attribute" do
|
|
94
|
+
Book.create!(title: "physical", name: "alias")
|
|
95
|
+
Book.alias_attribute :title, :name
|
|
96
|
+
column = described_class.new(:title, nil, Book)
|
|
37
97
|
|
|
38
|
-
|
|
39
|
-
column = described_class.new(Arel.sql("object->>'name'"), nil, Model)
|
|
40
|
-
expect(column.to_sql).to eq(%{coalesce((object->>'name')::text, '')})
|
|
98
|
+
expect(Book.pluck(column.to_arel)).to eq ["alias"]
|
|
41
99
|
end
|
|
42
100
|
end
|
|
43
101
|
end
|