arel_extensions 2.1.5 → 2.1.6

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.
@@ -14,6 +14,52 @@ module ArelExtensions
14
14
  '%M' => '%i', '%S' => '%S', '%L' => '', '%N' => '%f', '%z' => ''
15
15
  }.freeze
16
16
 
17
+ # This helper method did not exist in rails < 5.2
18
+ if !Arel::Visitors::MySQL.method_defined?(:collect_nodes_for)
19
+ def collect_nodes_for(nodes, collector, spacer, connector = ", ")
20
+ if nodes&.any?
21
+ collector << spacer
22
+ inject_join nodes, collector, connector
23
+ end
24
+ end
25
+ end
26
+
27
+ # The whole purpose of this override is to fix the behavior of RollUp.
28
+ # All other databases treat RollUp sanely, execpt MySQL which requires
29
+ # that it figures as the last element of a GROUP BY.
30
+ def visit_Arel_Nodes_SelectCore(o, collector)
31
+ collector << "SELECT"
32
+
33
+ collector = collect_optimizer_hints(o, collector) if self.respond_to?(:collect_optimizer_hinsts)
34
+ collector = maybe_visit o.set_quantifier, collector
35
+
36
+ collect_nodes_for o.projections, collector, " "
37
+
38
+ if o.source && !o.source.empty?
39
+ collector << " FROM "
40
+ collector = visit o.source, collector
41
+ end
42
+
43
+ # The actual work
44
+ groups = o.groups
45
+ rollup = groups.select { |g| g.expr.class == Arel::Nodes::RollUp }.map { |r| r.expr.value }
46
+ if rollup && !rollup.empty?
47
+ groups = o.groups.reject { |g| g.expr.class == Arel::Nodes::RollUp }
48
+ groups << Arel::Nodes::RollUp.new(rollup)
49
+ end
50
+ # FIN
51
+
52
+ collect_nodes_for o.wheres, collector, " WHERE ", " AND "
53
+ collect_nodes_for groups, collector, " GROUP BY " # Look ma, I'm viring a group
54
+ collect_nodes_for o.havings, collector, " HAVING ", " AND "
55
+ collect_nodes_for o.windows, collector, " WINDOW "
56
+
57
+ if o.respond_to?(:comment)
58
+ maybe_visit o.comment, collector
59
+ else
60
+ collector
61
+ end
62
+ end
17
63
 
18
64
  # Math functions
19
65
  def visit_ArelExtensions_Nodes_Log10 o, collector
@@ -138,6 +184,11 @@ module ArelExtensions
138
184
  collector
139
185
  end
140
186
 
187
+ def visit_Arel_Nodes_RollUp(o, collector)
188
+ visit o.expr, collector
189
+ collector << " WITH ROLLUP"
190
+ end
191
+
141
192
  def visit_ArelExtensions_Nodes_GroupConcat o, collector
142
193
  collector << 'GROUP_CONCAT('
143
194
  collector = visit o.left, collector
@@ -201,7 +252,18 @@ module ArelExtensions
201
252
  end
202
253
 
203
254
  def visit_ArelExtensions_Nodes_Format o, collector
204
- case o.col_type
255
+ # One use case we met is
256
+ # `case…when…then(valid_date).else(Arel.null).format(…)`.
257
+ #
258
+ # In this case, `o.col_type` is `nil` but we have a legitimate type in
259
+ # the expression to be formatted. The following is a best effort to
260
+ # infer the proper type.
261
+ type =
262
+ o.col_type.nil? && !o.expressions[0].return_type.nil? \
263
+ ? o.expressions[0].return_type \
264
+ : o.col_type
265
+
266
+ case type
205
267
  when :date, :datetime, :time
206
268
  fmt = ArelExtensions::Visitors::strftime_to_format(o.iso_format, DATE_FORMAT_DIRECTIVES)
207
269
  collector << 'DATE_FORMAT('
@@ -126,6 +126,11 @@ module ArelExtensions
126
126
  collector
127
127
  end
128
128
 
129
+ def visit_Arel_Nodes_RollUp(o, collector)
130
+ collector << "ROLLUP"
131
+ grouping_array_or_grouping_element o, collector
132
+ end
133
+
129
134
  def visit_ArelExtensions_Nodes_GroupConcat o, collector
130
135
  collector << '(LISTAGG('
131
136
  collector = visit o.left, collector
@@ -705,6 +710,18 @@ module ArelExtensions
705
710
  collector << ')'
706
711
  collector
707
712
  end
713
+
714
+ # Utilized by GroupingSet, Cube & RollUp visitors to
715
+ # handle grouping aggregation semantics
716
+ def grouping_array_or_grouping_element(o, collector)
717
+ if o.expr.is_a? Array
718
+ collector << "( "
719
+ visit o.expr, collector
720
+ collector << " )"
721
+ else
722
+ visit o.expr, collector
723
+ end
724
+ end
708
725
  end
709
726
  end
710
727
  end
@@ -19,6 +19,14 @@ if defined?(Arel::Visitors::SQLServer)
19
19
  end
20
20
  end
21
21
 
22
+ if defined?(Arel::Visitors::DepthFirst)
23
+ class Arel::Visitors::DepthFirst
24
+ def visit_Arel_SelectManager o
25
+ visit o.ast
26
+ end
27
+ end
28
+ end
29
+
22
30
  if defined?(Arel::Visitors::MSSQL)
23
31
  class Arel::Visitors::MSSQL
24
32
  include ArelExtensions::Visitors::MSSQL
@@ -78,6 +78,8 @@ require 'arel_extensions/nodes/case'
78
78
  require 'arel_extensions/nodes/soundex'
79
79
  require 'arel_extensions/nodes/cast'
80
80
  require 'arel_extensions/nodes/json'
81
+ require 'arel_extensions/nodes/rollup'
82
+ require 'arel_extensions/nodes/select'
81
83
 
82
84
  # It seems like the code in lib/arel_extensions/visitors.rb that is supposed
83
85
  # to inject ArelExtension is not enough. Different versions of the sqlserver
@@ -132,6 +134,10 @@ module Arel
132
134
  ArelExtensions::Nodes::Rand.new
133
135
  end
134
136
 
137
+ def self.rollup(*args)
138
+ Arel::Nodes::RollUp.new(args)
139
+ end
140
+
135
141
  def self.shorten s
136
142
  Base64.urlsafe_encode64(Digest::MD5.new.digest(s)).tr('=', '').tr('-', '_')
137
143
  end
@@ -277,4 +283,14 @@ class Arel::Attributes::Attribute
277
283
  collector = engine.connection.visitor.accept self, collector
278
284
  collector.value
279
285
  end
286
+
287
+ def rollup
288
+ Arel::Nodes::RollUp.new([self])
289
+ end
290
+ end
291
+
292
+ class Arel::Nodes::Node
293
+ def rollup
294
+ Arel::Nodes::RollUp.new([self])
295
+ end
280
296
  end
@@ -73,6 +73,8 @@ module ArelExtensions
73
73
  @neg = User.where(id: u.id)
74
74
  u = User.create age: 15, name: 'Justin', created_at: d, score: 11.0
75
75
  @justin = User.where(id: u.id)
76
+ u = User.create age: nil, name: 'nilly', created_at: nil, score: nil
77
+ @nilly = User.where(id: u.id)
76
78
 
77
79
  @age = User.arel_table[:age]
78
80
  @name = User.arel_table[:name]
@@ -149,7 +151,7 @@ module ArelExtensions
149
151
  def test_rand
150
152
  assert 42 != User.select(Arel.rand.as('res')).first.res
151
153
  assert 0 <= User.select(Arel.rand.abs.as('res')).first.res
152
- assert_equal 9, User.order(Arel.rand).limit(50).count
154
+ assert_equal 10, User.order(Arel.rand).limit(50).count
153
155
  end
154
156
 
155
157
  def test_round
@@ -186,6 +188,49 @@ module ArelExtensions
186
188
  assert User.group(:score).count(:id).values.all?{|e| !e.nil?}
187
189
  end
188
190
 
191
+ def test_rollup
192
+ skip "sqlite not supported" if $sqlite
193
+ at = User.arel_table
194
+ # single
195
+ q = User.select(at[:name], at[:age].sum).group(Arel::Nodes::RollUp.new([at[:name]]))
196
+ assert q.to_a.length > 0
197
+
198
+ # multi
199
+ q = User.select(at[:name], at[:score], at[:age].sum).group(Arel::Nodes::RollUp.new([at[:score], at[:name]]))
200
+ assert q.to_a.length > 0
201
+
202
+ # hybrid
203
+ q = User.select(at[:name], at[:score], at[:age].sum).group(at[:score], Arel::Nodes::RollUp.new([at[:name]]))
204
+ assert q.to_a.length > 0
205
+
206
+ ## Using Arel.rollup which is less verbose than the original way
207
+
208
+ # simple
209
+ q = User.select(at[:name], at[:age].sum).group(Arel.rollup(at[:name]))
210
+ assert q.to_a.length > 0
211
+
212
+ # multi
213
+ q = User.select(at[:name], at[:score], at[:age].sum).group(Arel.rollup([at[:score], at[:name]]))
214
+ assert q.to_a.length > 0
215
+
216
+ # hybrid
217
+ q = User.select(at[:name], at[:score], at[:age].sum).group(at[:score], Arel.rollup([at[:name]]))
218
+ assert q.to_a.length > 0
219
+
220
+ ## Using at[:col].rollup which is handy for single column rollups
221
+
222
+ # simple
223
+ q = User.select(at[:name], at[:age].sum).group(at[:name].rollup)
224
+ assert q.to_a.length > 0
225
+
226
+ q = User.select(at[:name], at[:score], at[:age].sum).group(at[:name].rollup, at[:score].rollup)
227
+ assert q.to_a.length > 0
228
+
229
+ # hybrid
230
+ q = User.select(at[:name], at[:score], at[:age].sum).group(at[:name], at[:score].rollup)
231
+ assert q.to_a.length > 0
232
+ end
233
+
189
234
  # String Functions
190
235
  def test_concat
191
236
  assert_equal 'Camille Camille', t(@camille, @name + ' ' + @name)
@@ -294,16 +339,16 @@ module ArelExtensions
294
339
  skip "Sqlite version can't load extension for regexp" if $sqlite && $load_extension_disabled
295
340
  skip 'SQL Server does not know about REGEXP without extensions' if @env_db == 'mssql'
296
341
  assert_equal 1, User.where(@name =~ '^M').count
297
- assert_equal 7, User.where(@name !~ '^L').count
342
+ assert_equal 8, User.where(@name !~ '^L').count
298
343
  assert_equal 1, User.where(@name =~ /^M/).count
299
- assert_equal 7, User.where(@name !~ /^L/).count
344
+ assert_equal 8, User.where(@name !~ /^L/).count
300
345
  end
301
346
 
302
347
  def test_imatches
303
348
  # puts User.where(@name.imatches('m%')).to_sql
304
349
  assert_equal 1, User.where(@name.imatches('m%')).count
305
350
  assert_equal 4, User.where(@name.imatches_any(['L%', '%e'])).count
306
- assert_equal 7, User.where(@name.idoes_not_match('L%')).count
351
+ assert_equal 8, User.where(@name.idoes_not_match('L%')).count
307
352
  end
308
353
 
309
354
  def test_replace
@@ -328,8 +373,8 @@ module ArelExtensions
328
373
  skip "Sqlite version can't load extension for soundex" if $sqlite && $load_extension_disabled
329
374
  skip "PostgreSql version can't load extension for soundex" if @env_db == 'postgresql'
330
375
  assert_equal 'C540', t(@camille, @name.soundex)
331
- assert_equal 9, User.where(@name.soundex.eq(@name.soundex)).count
332
- assert_equal 9, User.where(@name.soundex == @name.soundex).count
376
+ assert_equal 10, User.where(@name.soundex.eq(@name.soundex)).count
377
+ assert_equal 10, User.where(@name.soundex == @name.soundex).count
333
378
  end
334
379
 
335
380
  def test_change_case
@@ -374,6 +419,23 @@ module ArelExtensions
374
419
  assert_equal 'true', t(@neg, @comments.not_blank.then('true', 'false'))
375
420
  end
376
421
 
422
+ # This test repeats a lot of `test_blank` cases.
423
+ def test_present
424
+ if @env_db == 'postgresql'
425
+ assert_includes [true, 't'], t(@myung, @name.present) # depends of adapter
426
+ assert_includes [false, 'f'], t(@myung, @comments.present)
427
+ end
428
+ assert_equal 1, @myung.where(@name.present).count
429
+ assert_equal 0, @myung.where(@comments.present).count
430
+ assert_equal 0, @sophie.where(@comments.present).count
431
+ assert_equal 0, @camille.where(@comments.present).count
432
+
433
+ assert_equal 1, @neg.where(@comments.present).count
434
+ assert_equal 'true', t(@myung, @name.present.then('true', 'false'))
435
+ assert_equal 'false', t(@myung, @comments.present.then('true', 'false'))
436
+ assert_equal 'true', t(@neg, @comments.present.then('true', 'false'))
437
+ end
438
+
377
439
  def test_format
378
440
  assert_equal '2016-05-23', t(@lucas, @created_at.format('%Y-%m-%d'))
379
441
  assert_equal '2014/03/03 12:42:00', t(@lucas, @updated_at.format('%Y/%m/%d %H:%M:%S'))
@@ -565,6 +627,24 @@ module ArelExtensions
565
627
  assert_equal 'Laure10', t(@laure, @comments.coalesce('Laure') + 10)
566
628
  end
567
629
 
630
+ def test_coalesce_blank
631
+ assert_equal 'Myung', t(@myung, @comments.coalesce_blank('Myung').coalesce_blank('ignored'))
632
+ assert_equal 'Myung', t(@myung, @comments.coalesce_blank('', ' ', ' ').coalesce_blank('Myung'))
633
+ assert_equal 'Myung', t(@myung, @comments.coalesce_blank('', ' ', ' ', 'Myung'))
634
+ assert_equal '2016-05-23', t(@myung, @created_at.coalesce_blank(Date.new(2022, 1, 1)).format('%Y-%m-%d'))
635
+ assert_equal 'Laure', t(@laure, @comments.coalesce_blank('Laure'))
636
+ assert_equal 100, t(@test, @age.coalesce_blank(100))
637
+ assert_equal 20, t(@test, @age.coalesce_blank(20))
638
+ assert_equal 20, t(@test, @age.coalesce_blank(10) + 10)
639
+ assert_equal 'Laure10', t(@laure, @comments.coalesce_blank('Laure') + 10)
640
+
641
+ skip 'mssql does not support null in case results' if @env_db == 'mssql'
642
+
643
+ assert_equal 'Camille concat', t(@camille, @name.coalesce_blank(Arel.null, 'default') + ' concat')
644
+ assert_equal 'Myung', t(@myung, @comments.coalesce_blank(Arel.null, 'Myung'))
645
+ assert_equal 'Camille', t(@camille, @name.coalesce_blank(Arel.null, 'default'))
646
+ end
647
+
568
648
  # Comparators
569
649
  def test_number_comparator
570
650
  assert_equal 2, User.where(@age < 6).count
@@ -577,7 +657,7 @@ module ArelExtensions
577
657
  def test_date_comparator
578
658
  d = Date.new(2016, 5, 23)
579
659
  assert_equal 0, User.where(@created_at < d).count
580
- assert_equal 9, User.where(@created_at >= d).count
660
+ assert_equal 10, User.where(@created_at >= d).count
581
661
  end
582
662
 
583
663
  def test_date_duration
@@ -690,6 +770,26 @@ module ArelExtensions
690
770
  end
691
771
  end
692
772
 
773
+ def test_if_present
774
+ assert_nil t(@myung, @comments.if_present)
775
+ assert_equal 0, t(@myung, @comments.if_present.count)
776
+ assert_equal 20.16, t(@myung, @score.if_present)
777
+ assert_equal '2016-05-23', t(@myung, @created_at.if_present.format('%Y-%m-%d'))
778
+ assert_nil t(@laure, @comments.if_present)
779
+
780
+ assert_nil t(@nilly, @duration.if_present.format('%Y-%m-%d'))
781
+
782
+ # NOTE: here we're testing the capacity to format a nil value,
783
+ # however, @comments is a text field, and not a date/datetime field,
784
+ # so Postgres will rightfully complain when we format the text:
785
+ # we need to cast it first.
786
+ if @env_db == 'postgresql'
787
+ assert_nil t(@laure, @comments.cast(:date).if_present.format('%Y-%m-%d'))
788
+ else
789
+ assert_nil t(@laure, @comments.if_present.format('%Y-%m-%d'))
790
+ end
791
+ end
792
+
693
793
  def test_is_null
694
794
  # puts User.where(@age.is_null).select(@name).to_sql
695
795
  # puts @age.is_null
@@ -725,7 +825,7 @@ module ArelExtensions
725
825
  def test_math_minus
726
826
  d = Date.new(2016, 5, 20)
727
827
  # Datediff
728
- assert_equal 9, User.where((@created_at - @created_at).eq(0)).count
828
+ assert_equal 10, User.where((@created_at - @created_at).eq(0)).count
729
829
  assert_equal 3, @laure.select((@created_at - d).as('res')).first.res.abs.to_i
730
830
  # Substraction
731
831
  assert_equal 0, User.where((@age - 10).eq(50)).count
@@ -845,8 +945,8 @@ module ArelExtensions
845
945
 
846
946
  def test_subquery_with_order
847
947
  skip if ['mssql'].include?(@env_db) && Arel::VERSION.to_i < 10
848
- assert_equal 9, User.where(name: User.select(:name).order(:name)).count
849
- assert_equal 9, User.where(@ut[:name].in(@ut.project(@ut[:name]).order(@ut[:name]))).count
948
+ assert_equal 10, User.where(name: User.select(:name).order(:name)).count
949
+ assert_equal 10, User.where(@ut[:name].in(@ut.project(@ut[:name]).order(@ut[:name]))).count
850
950
  if !['mysql'].include?(@env_db) # MySql can't have limit in IN subquery
851
951
  assert_equal 2, User.where(name: User.select(:name).order(:name).limit(2)).count
852
952
  # assert_equal 6, User.where(name: User.select(:name).order(:name).offset(2)).count
data/version_v1.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ArelExtensions
2
- VERSION = '1.3.5'.freeze
2
+ VERSION = '1.3.6'.freeze
3
3
  end
data/version_v2.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ArelExtensions
2
- VERSION = '2.1.5'.freeze
2
+ VERSION = '2.1.6'.freeze
3
3
  end
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: 2.1.5
4
+ version: 2.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yann Azoury
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-07-25 00:00:00.000000000 Z
13
+ date: 2022-11-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -40,20 +40,6 @@ dependencies:
40
40
  - - "~>"
41
41
  - !ruby/object:Gem::Version
42
42
  version: '5.9'
43
- - !ruby/object:Gem::Dependency
44
- name: rdoc
45
- requirement: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 6.3.1
50
- type: :development
51
- prerelease: false
52
- version_requirements: !ruby/object:Gem::Requirement
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: 6.3.1
57
43
  - !ruby/object:Gem::Dependency
58
44
  name: rake
59
45
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +72,7 @@ files:
86
72
  - ".rubocop.yml"
87
73
  - Gemfile
88
74
  - MIT-LICENSE.txt
75
+ - NEWS.md
89
76
  - README.md
90
77
  - Rakefile
91
78
  - SQL_Challenges.md
@@ -94,8 +81,8 @@ files:
94
81
  - arel_extensions.gemspec
95
82
  - functions.html
96
83
  - gemfiles/rails3.gemfile
97
- - gemfiles/rails4.gemfile
98
- - gemfiles/rails5_0.gemfile
84
+ - gemfiles/rails4_2.gemfile
85
+ - gemfiles/rails5.gemfile
99
86
  - gemfiles/rails5_1_4.gemfile
100
87
  - gemfiles/rails5_2.gemfile
101
88
  - gemfiles/rails6.gemfile
@@ -150,7 +137,9 @@ files:
150
137
  - lib/arel_extensions/nodes/rand.rb
151
138
  - lib/arel_extensions/nodes/repeat.rb
152
139
  - lib/arel_extensions/nodes/replace.rb
140
+ - lib/arel_extensions/nodes/rollup.rb
153
141
  - lib/arel_extensions/nodes/round.rb
142
+ - lib/arel_extensions/nodes/select.rb
154
143
  - lib/arel_extensions/nodes/soundex.rb
155
144
  - lib/arel_extensions/nodes/std.rb
156
145
  - lib/arel_extensions/nodes/substring.rb