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
@@ -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(rank_table_alias))
21
- .order(Arel.sql("#{rank_table_alias}.rank DESC, #{order_within_rank}"))
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("#{table_name}.*") unless scope.select_values.any?
42
- scope.select("(#{highlight}) AS pg_search_highlight")
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("#{table_name}.*") unless scope.select_values.any?
54
- scope.select("#{pg_search_rank_table_alias}.rank AS pg_search_rank")
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("#{primary_key} AS pg_search_id")
86
- .select("#{rank} AS rank")
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
- or_node(expressions)
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 || "#{primary_key} ASC"
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
- "#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
107
+ Arel::Attributes::Attribute.new(model.arel_table, model.primary_key)
127
108
  end
128
109
 
129
110
  def subquery_join
130
- if config.associations.any?
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
- (config.ranking_sql || ":tsearch").gsub(/:(\w*)/) do
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(rank_table_alias)
167
- arel_table = model.arel_table
168
- subquery_arel = subquery.arel.as(rank_table_alias)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgSearch
4
- VERSION = "2.3.8"
4
+ VERSION = "2.4.0"
5
5
  end
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", "Case Commons, LLC"]
11
- s.email = %w[gems@nertzy.com casecommons-dev@googlegroups.com]
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", ">= 7.2"
22
- s.add_dependency "activesupport", ">= 7.2"
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, class_name: "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, class_name: "User"
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 "#join" do
117
- let(:expected_sql) do
118
- <<~SQL.squish
119
- LEFT OUTER JOIN
120
- (SELECT model_id AS id,
121
- string_agg("#{association.table_name}"."name"::text, ' ') AS #{association.columns.first.alias}
122
- FROM "#{Site.table_name}"
123
- INNER JOIN "#{association.table_name}"
124
- ON "#{association.table_name}"."site_id" = "#{Site.table_name}"."id"
125
- GROUP BY model_id) #{association.subselect_alias}
126
- ON #{association.subselect_alias}.id = model_id
127
- SQL
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 "returns the correct SQL join" do
131
- expect(association.join("model_id")).to eq(expected_sql)
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 "#full_name" do
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 "returns the fully-qualified table and column name" do
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
- expect(column.full_name).to eq(%(#{Model.quoted_table_name}."name"))
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 "returns nested json attributes" do
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
- expect(column.full_name).to eq(%(object->>'name'))
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
- describe "#to_sql" do
26
- with_model :Model do
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 "returns an expression that casts the column to text and coalesces it with an empty string" do
34
- column = described_class.new("name", nil, Model)
35
- expect(column.to_sql).to eq(%{coalesce((#{Model.quoted_table_name}."name")::text, '')})
36
- end
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
- it "returns an expression that casts the nested json attribute to text and coalesces it with an empty string" do
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