arel-extensions 8.1.0 → 9.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26af903829c4fe9d8b881fcd316d09a03f9253af796ab813523d6c074e7fc3fe
4
- data.tar.gz: 4c8ff81dc39a82b8dabd1fe792b18c43e9d683bc5f8ede192932fda4d0864667
3
+ metadata.gz: 99efadd34c12e304c5204c9dc2717bad533bfb20c9c280a036a8abc3110ab34a
4
+ data.tar.gz: 66c064d703b2531c7676e3a91bef9f0dd813355e24108c7966770ea1fba298ff
5
5
  SHA512:
6
- metadata.gz: 9a596cde6baa6b9b54fe900786dd63980da37bbe10408ca09ed9bf7a1df3ddb23de11d79e64f75634120e93e134dd739f36872131e13cba2fc1bb90eaa064deb
7
- data.tar.gz: 53b6e4cd418d977fb825e1f9753b117bf5b64644dc29be64c530f9f15f44f158aa16f4dc0acd0a39a78eea7856ebf0ec27aa3b65cb7f5b83e09c9e93398e899d
6
+ metadata.gz: 043be028082fea8d31c8b427db8a703b7f4a2f1916879e77455f05847086964840b4cae0886e73930ace056dc9e86bcb7c375c4794a64f5ac5a85c19d5f9b61b
7
+ data.tar.gz: 89d8e35f5503774cc499c93fa0cc9ea427dec2cecc27cd6ee01f5d52f9f66f9c278078c80c865d7fc4bac8362e9c860f7fe33b1a90b572c4823621cfe6be9ba3
data/CHANGELOG.md ADDED
@@ -0,0 +1,64 @@
1
+ ## [9.0.1] - 2026-08-30
2
+
3
+ ### Security
4
+ - Fixed a SQL injection in the PostgreSQL visitor's JSON path handling
5
+ ([GHSA-75hc-9q9v-9cv2], CWE-89, high). `key`/`dig` path segments were
6
+ interpolated straight into a `#>'{...}'` array literal, so a segment
7
+ containing `}'` could close the literal and have the rest of it executed as
8
+ SQL. Most reachable through activerecord-filter, where a filter key like
9
+ `"metadata.<payload>"` on a json/jsonb column puts request input into `dig`.
10
+ Segments are now emitted as a quoted `#> array[...]`, which PostgreSQL folds
11
+ back to the same `text[]` constant (existing expression indexes still match).
12
+ Reported by [@saidM](https://github.com/saidM).
13
+ - `cast_as` now validates the type name and raises `ArgumentError` unless it
14
+ looks like a type identifier. Not reachable from activerecord-filter, but it
15
+ was the same class of raw interpolation.
16
+
17
+ ### Changed
18
+ - A path segment is now always a single segment: `key('a,b')` emits
19
+ `array['a,b']`, where the old raw `'{a,b}'` literal let PostgreSQL split it
20
+ on the comma into two segments. Use `dig('a', 'b')` for multi-segment paths.
21
+
22
+ [GHSA-75hc-9q9v-9cv2]: https://github.com/malomalo/arel-extensions/security/advisories/GHSA-75hc-9q9v-9cv2
23
+
24
+ ## [9.0.0] - 2026-08-27
25
+
26
+ ### Changed
27
+ - Switched to independent Semantic Versioning. Prior releases tracked the Rails
28
+ major/minor line; the version number no longer maps to a Rails version.
29
+ - Require Ruby >= 3.3.
30
+ - Require ActiveRecord >= 8.0, < 9.0 (dropped support for Rails older than 8.0).
31
+
32
+ ### Added
33
+ - `gem.description` and gemspec `metadata` (source, changelog, MFA-required).
34
+
35
+ ### Packaging
36
+ - Slimmed the published gem to `lib`, `ext`, `LICENSE`, and `README.md`; removed
37
+ the deprecated `test_files` declaration.
38
+
39
+ ### Documentation
40
+ - Documented the features arel-extensions adds (DISTINCT ON, NULLS ordering,
41
+ array/JSON predicates, full-text search, GIS, binary values) in the README.
42
+ - Documented that the GIS predicates (`#intersects`, `#within`) require the
43
+ optional `rgeo` gem unless passed an Arel node.
44
+
45
+ ## Earlier releases
46
+
47
+ Prior versions tracked the matching Rails release. See the Git tags for details:
48
+
49
+ - [8.1.0] - 2025-12-22
50
+ - [8.0.0] - 2025-01-27
51
+ - [7.0.1] - 2023-05-31
52
+ - [7.0.0] - 2022-12-07
53
+ - [6.1.0] - 2021-01-14
54
+ - [6.0.0.7] - 2019-11-04
55
+ - [6.0.0.2] - 2019-08-12
56
+
57
+ [9.0.0]: https://github.com/malomalo/arel-extensions/compare/v8.1.0...master
58
+ [8.1.0]: https://github.com/malomalo/arel-extensions/releases/tag/v8.1.0
59
+ [8.0.0]: https://github.com/malomalo/arel-extensions/releases/tag/v8.0.0
60
+ [7.0.1]: https://github.com/malomalo/arel-extensions/releases/tag/v7.0.1
61
+ [7.0.0]: https://github.com/malomalo/arel-extensions/releases/tag/v7.0.0
62
+ [6.1.0]: https://github.com/malomalo/arel-extensions/releases/tag/v6.1.0
63
+ [6.0.0.7]: https://github.com/malomalo/arel-extensions/releases/tag/v6.0.0.7
64
+ [6.0.0.2]: https://github.com/malomalo/arel-extensions/releases/tag/v6.0.0.2
data/README.md CHANGED
@@ -18,5 +18,116 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- Just `require 'arel-extensions'` and use Arel/ActiveRecord as you normally would!
22
- arel-extensions extends ActiveRecord's query methods in both Arel and ActiveRecord.
21
+ Just `require 'arel-extensions'` and use `Arel` / `ActiveRecord` as you normally
22
+ would.
23
+
24
+ It adds the methods and nodes below to `Arel::Attributes::Attribute` and to
25
+ `ActiveRecord` relations. Most operators generate PostgreSQL-specific SQL.
26
+
27
+ Attribute-level helpers are used through an Arel attribute, e.g.
28
+ `Model.arel_table[:column]`, typically inside `where` or `order`.
29
+
30
+ ## What it adds
31
+
32
+ ### `DISTINCT ON` (ActiveRecord)
33
+
34
+ Added to relations (and as a class method via `Model.distinct_on`):
35
+
36
+ ```ruby
37
+ User.distinct_on(:email) # SELECT DISTINCT ON (users.email) ...
38
+ User.distinct_on(:a, :b) # DISTINCT ON (users.a, users.b)
39
+ ```
40
+
41
+ `uniq_on` is an alias of `distinct_on`.
42
+
43
+ ### Ordering with NULLS FIRST / LAST
44
+
45
+ ```ruby
46
+ col = User.arel_table[:name]
47
+ User.order(col.asc(:nulls_last)) # ... ORDER BY users.name ASC NULLS LAST
48
+ User.order(col.desc(:nulls_first)) # ... ORDER BY users.name DESC NULLS FIRST
49
+ ```
50
+
51
+ A `RANDOM()` ordering node is also provided.
52
+
53
+ ### Array predicates
54
+
55
+ ```ruby
56
+ tags = Post.arel_table[:tags]
57
+ tags.contained_by(other) # tags <@ other
58
+ tags.excludes(other) # NOT (tags @> other)
59
+ ```
60
+
61
+ ### JSON / JSONB predicates
62
+
63
+ ```ruby
64
+ data = User.arel_table[:data]
65
+
66
+ # Keys in a Object
67
+ data['name'] # data #> array['name']
68
+ data.key('name') # data #> array['name']
69
+ data.index('name') # data #> array['name']
70
+
71
+ # Integer index in a Array
72
+ data[0] # data #> array['0']
73
+ data.key(0) # data #> array['0']
74
+ data.index(0) # data #> array['0']
75
+
76
+ # Dig and other operators
77
+ data.dig('address', 'zip') # data #> array['address','zip']
78
+ data.dig('tags', -1) # data #> array['tags','-1']
79
+ data.has_key('name') # data ? 'name'
80
+ data.has_keys('a', 'b') # data ?& array['a','b']
81
+ data.has_any_key('a', 'b') # data ?| array['a','b']
82
+ ```
83
+
84
+ A segment may be an integer, which PostgreSQL reads as an array index (negative
85
+ counts from the end); out of range yields `NULL`.
86
+
87
+ Path segments are quoted, so they are safe to build from untrusted input.
88
+ PostgreSQL folds the array back to `'{address,zip}'::text[]`, so expression
89
+ indexes written against the literal form still match.
90
+
91
+ ### Casting
92
+
93
+ ```ruby
94
+ User.arel_table[:id].cast_as('text') # (users.id)::text
95
+ User.arel_table[:created_at].cast_as('date') # (users.created_at)::date
96
+ data.dig('age').cast_as('int') # (data #> array['age'])::int
97
+ ```
98
+
99
+ A type name can't be quoted or bound, so `cast_as` accepts only something that
100
+ looks like one; optionally schema qualified, with a modifier. For example:
101
+ `text`, `varchar(255)`, `numeric(10,2)`, `timestamp(6) with time zone`,
102
+ `int[]`, `public.geometry`. An `ArgumentError` will be raised otherwise.
103
+
104
+ ### Full-text search
105
+
106
+ ```ruby
107
+ body = Article.arel_table[:body]
108
+ body.ts_query('quick & fox') # to_tsvector(body) @@ to_tsquery('quick & fox')
109
+ body.ts_query('quick & fox', 'english')
110
+ ```
111
+
112
+ Nodes for `to_tsvector`, `to_tsquery`, `@@` (`TSMatch`), `ts_rank`, and
113
+ `ts_rank_cd` are available for building ranking expressions.
114
+
115
+ ### GIS predicates (optional)
116
+
117
+ ```ruby
118
+ area = Place.arel_table[:area]
119
+ area.intersects(value) # ST_Intersects(area, value)
120
+ area.within(value) # ST_Within(area, value)
121
+ ```
122
+
123
+ `value` may be an Arel node, an RGeo geometry, a WKT/WKB string, or a GeoJSON
124
+ hash. Anything other than an Arel node requires the
125
+ [`rgeo`](https://github.com/rgeo/rgeo) gem, so add it to your Gemfile if you use
126
+ these:
127
+
128
+ gem 'rgeo'
129
+
130
+ ### Binary values
131
+
132
+ `BinaryValue` and `HexEncodedBinaryValue` nodes for embedding binary data
133
+ (escaped `bytea` and `\x`-encoded hex, respectively).
@@ -5,51 +5,45 @@ require 'active_record/relation'
5
5
  require 'active_record/querying'
6
6
  require 'active_record/relation/query_methods'
7
7
 
8
- module ActiveRecord::QueryMethods
9
- def distinct_on_values
10
- @values["distinct_on_values"] || []
11
- end
8
+ module ActiveRecord
9
+ # Prepended onto ActiveRecord::QueryMethods rather than reopening it, so
10
+ # these methods don't appear in QueryMethods.public_instance_methods(false)
11
+ # -- which Rails' QueryingMethodsDelegationTest asserts against -- while
12
+ # relations still respond to them.
13
+ module DistinctOn
14
+ def distinct_on_values
15
+ @values["distinct_on_values"] || []
16
+ end
12
17
 
13
- def distinct_on_values=(value)
14
- @values["distinct_on_values"] = value
15
- end
18
+ def distinct_on_values=(value)
19
+ @values["distinct_on_values"] = value
20
+ end
16
21
 
17
- def distinct_on(*fields)
18
- spawn.distinct_on!(*fields)
19
- end
20
- alias uniq_on distinct_on
22
+ def distinct_on(*fields)
23
+ spawn.distinct_on!(*fields)
24
+ end
25
+ alias uniq_on distinct_on
21
26
 
22
- def distinct_on!(*fields)
23
- fields.flatten!
24
- self.distinct_on_values = fields.map { |x| x.is_a?(Arel::Attributes::Attribute) ? x : klass.arel_table[x] }
25
- self
26
- end
27
- alias uniq_on! distinct_on!
27
+ def distinct_on!(*fields)
28
+ fields.flatten!
29
+ self.distinct_on_values = fields.map { |x| x.is_a?(Arel::Attributes::Attribute) ? x : klass.arel_table[x] }
30
+ self
31
+ end
32
+ alias uniq_on! distinct_on!
28
33
 
29
- private
34
+ private
30
35
 
31
- if ActiveRecord.version >= "8.1"
32
- def build_arel_with_distinct_on(aliases = nil)
33
- arel = build_arel_without_distinct_on(aliases)
34
- arel.distinct_on(self.distinct_on_values) if !self.distinct_on_values.empty?
35
- arel
36
- end
37
- elsif ActiveRecord.version >= "7.2"
38
- def build_arel_with_distinct_on(connection, aliases = nil)
39
- arel = build_arel_without_distinct_on(connection, aliases)
40
- arel.distinct_on(self.distinct_on_values) if !self.distinct_on_values.empty?
41
- arel
42
- end
43
- else
44
- def build_arel_with_distinct_on(aliases = nil)
45
- arel = build_arel_without_distinct_on(aliases)
36
+ # Version-agnostic: forward whatever build_arel receives (its signature
37
+ # varies across Rails versions) to the original via super, then apply
38
+ # the accumulated distinct_on values.
39
+ def build_arel(*args)
40
+ arel = super
46
41
  arel.distinct_on(self.distinct_on_values) if !self.distinct_on_values.empty?
47
42
  arel
48
43
  end
49
44
  end
50
- alias_method :build_arel_without_distinct_on, :build_arel
51
- alias_method :build_arel, :build_arel_with_distinct_on
52
45
  end
53
46
 
47
+ ActiveRecord::QueryMethods.prepend(ActiveRecord::DistinctOn)
54
48
  ActiveRecord::Querying.delegate :distinct_on, to: :all
55
- ActiveRecord::Querying.delegate :distinct_on!, to: :all
49
+ ActiveRecord::Querying.delegate :uniq_on, to: :all
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Arel
4
4
  module Extensions
5
- VERSION = '8.1.0'
5
+ VERSION = '9.0.1'
6
6
  end
7
7
  end
@@ -39,24 +39,24 @@ module Arel
39
39
  collector
40
40
  end
41
41
 
42
- def visit_Arel_Attributes_Key(o, collector, last_key = true)
43
- if o.relation.is_a?(Arel::Attributes::Key)
44
- visit_Arel_Attributes_Key(o.relation, collector, false)
45
- if last_key
46
- collector << o.name.to_s
47
- collector << "}'"
48
- else
49
- collector << o.name.to_s
50
- collector << ","
51
- end
52
- else
53
- visit(o.relation, collector)
54
- collector << "\#>'{" << o.name.to_s
55
- collector << (last_key ? "}'" : ",")
42
+ # Path segments are emitted as a quoted `array[...]` rather than
43
+ # interpolated into a `'{...}'` array literal, so a segment can never
44
+ # break out of the path and inject SQL (GHSA-75hc-9q9v-9cv2). PostgreSQL
45
+ # const-folds the array back to `'{a,b}'::text[]`, so expression indexes
46
+ # on the literal form still match.
47
+ def visit_Arel_Attributes_Key(o, collector)
48
+ keys = []
49
+ node = o
50
+ while node.is_a?(Arel::Attributes::Key)
51
+ keys.unshift(quote(node.name.to_s))
52
+ node = node.relation
56
53
  end
54
+
55
+ visit(node, collector)
56
+ collector << " #> array[" << keys.join(',') << "]"
57
57
  collector
58
58
  end
59
-
59
+
60
60
  def visit_Arel_Nodes_HasKey(o, collector)
61
61
  right = o.right
62
62
 
@@ -84,10 +84,24 @@ module Arel
84
84
  collector
85
85
  end
86
86
 
87
+ # A type name can't be bound or quoted, so only allow something that
88
+ # actually looks like one -- optionally schema qualified, with a modifier
89
+ # and/or array suffix. Keeps user input from reaching the SQL as-is.
90
+ CAST_TYPE = /\A
91
+ [a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)? # type, optionally schema qualified
92
+ (\(\d+(\s*,\s*\d+)?\))? # "varchar(255)", "numeric(10,2)"
93
+ (\ [a-z]+)* # "timestamp with time zone"
94
+ (\(\d+(\s*,\s*\d+)?\))? # "character varying(255)"
95
+ (\[\])* # "int[]"
96
+ \z/xi
97
+
87
98
  def visit_Arel_Attributes_Cast(o, collector)
99
+ type = o.name.to_s
100
+ raise ArgumentError, "invalid cast type: #{type.inspect}" unless CAST_TYPE.match?(type)
101
+
88
102
  collector << "("
89
103
  visit(o.relation, collector)
90
- collector << ")::#{o.name}"
104
+ collector << ")::#{type}"
91
105
  collector
92
106
  end
93
107
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arel-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0
4
+ version: 9.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 7.0.0
18
+ version: 8.0.0
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '9.0'
@@ -25,24 +25,10 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: 7.0.0
28
+ version: 8.0.0
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9.0'
32
- - !ruby/object:Gem::Dependency
33
- name: bundler
34
- requirement: !ruby/object:Gem::Requirement
35
- requirements:
36
- - - ">="
37
- - !ruby/object:Gem::Version
38
- version: '0'
39
- type: :development
40
- prerelease: false
41
- version_requirements: !ruby/object:Gem::Requirement
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: '0'
46
32
  - !ruby/object:Gem::Dependency
47
33
  name: debug
48
34
  requirement: !ruby/object:Gem::Requirement
@@ -169,19 +155,18 @@ dependencies:
169
155
  - - ">="
170
156
  - !ruby/object:Gem::Version
171
157
  version: '8.0'
158
+ description: Extends Arel and ActiveRecord with SQL operators and functions that aren't
159
+ available out of the box, including array and JSON predicates, PostgreSQL full-text
160
+ search (tsvector/tsquery), GIS/geometry predicates, and additional ordering helpers.
172
161
  email:
173
162
  - jonbracy@gmail.com
174
163
  executables: []
175
164
  extensions: []
176
165
  extra_rdoc_files: []
177
166
  files:
178
- - ".github/workflows/ci.yml"
179
- - ".gitignore"
180
- - Gemfile
167
+ - CHANGELOG.md
181
168
  - LICENSE
182
169
  - README.md
183
- - Rakefile
184
- - arel-extensions.gemspec
185
170
  - ext/arel/nodes/ascending.rb
186
171
  - ext/arel/nodes/descending.rb
187
172
  - ext/arel/order_predications.rb
@@ -214,17 +199,13 @@ files:
214
199
  - lib/arel/visitors/postgresql_extensions.rb
215
200
  - lib/arel/visitors/sunstone_extensions.rb
216
201
  - lib/arel/visitors/to_sql_extensions.rb
217
- - test/binary_value_test.rb
218
- - test/database.rb
219
- - test/gis_test.rb
220
- - test/order_test.rb
221
- - test/sunstone_test.rb
222
- - test/test_helper.rb
223
- - test/ts_test.rb
224
202
  homepage: https://github.com/malomalo/arel-extensions
225
203
  licenses:
226
204
  - MIT
227
- metadata: {}
205
+ metadata:
206
+ source_code_uri: https://github.com/malomalo/arel-extensions
207
+ changelog_uri: https://github.com/malomalo/arel-extensions/blob/master/CHANGELOG.md
208
+ rubygems_mfa_required: 'true'
228
209
  rdoc_options: []
229
210
  require_paths:
230
211
  - lib
@@ -232,21 +213,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
232
213
  requirements:
233
214
  - - ">="
234
215
  - !ruby/object:Gem::Version
235
- version: '0'
216
+ version: '3.3'
236
217
  required_rubygems_version: !ruby/object:Gem::Requirement
237
218
  requirements:
238
219
  - - ">="
239
220
  - !ruby/object:Gem::Version
240
221
  version: '0'
241
222
  requirements: []
242
- rubygems_version: 4.0.2
223
+ rubygems_version: 4.0.11
243
224
  specification_version: 4
244
225
  summary: Adds support for missing SQL operators and functions to Arel
245
- test_files:
246
- - test/binary_value_test.rb
247
- - test/database.rb
248
- - test/gis_test.rb
249
- - test/order_test.rb
250
- - test/sunstone_test.rb
251
- - test/test_helper.rb
252
- - test/ts_test.rb
226
+ test_files: []