arel_extensions 2.1.5 → 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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