activerecord-refined 0.3.3 → 0.5.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.
@@ -2,11 +2,11 @@ require_relative 'test_helper'
2
2
 
3
3
  class TestBlockSyntax < Minitest::Test
4
4
  def test_equal
5
- assert_sql(/WHERE "users"."name" = 'matz'/, User.where { :name == 'matz' }.to_sql)
5
+ assert_sql(/WHERE "users"."name" = 'alice'/, User.where { :name == 'alice' }.to_sql)
6
6
  end
7
7
 
8
8
  def test_not_equal
9
- assert_sql(/WHERE "users"."name" != 'nobu'/, User.where { :name != 'nobu' }.to_sql)
9
+ assert_sql(/WHERE "users"."name" != 'bob'/, User.where { :name != 'bob' }.to_sql)
10
10
  end
11
11
 
12
12
  def test_greater_than
@@ -34,23 +34,23 @@ class TestBlockSyntax < Minitest::Test
34
34
  end
35
35
 
36
36
  def test_and
37
- assert_sql(/WHERE "users"."name" = 'matz' AND "users"."age" > 18/,
38
- User.where { (:name == 'matz') & (:age > 18) }.to_sql)
37
+ assert_sql(/WHERE "users"."name" = 'alice' AND "users"."age" > 18/,
38
+ User.where { (:name == 'alice') & (:age > 18) }.to_sql)
39
39
  end
40
40
 
41
41
  def test_or
42
- assert_sql(/WHERE \(?\"users\".\"name\" = 'matz' OR \"users\".\"name\" = 'nobu'\)?/,
43
- User.where { (:name == 'matz') | (:name == 'nobu') }.to_sql)
42
+ assert_sql(/WHERE \(?\"users\".\"name\" = 'alice' OR \"users\".\"name\" = 'bob'\)?/,
43
+ User.where { (:name == 'alice') | (:name == 'bob') }.to_sql)
44
44
  end
45
45
 
46
46
  def test_not
47
- assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'matz'\)?/,
48
- User.where { !(:name == 'matz') }.to_sql)
47
+ assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'alice'\)?/,
48
+ User.where { !(:name == 'alice') }.to_sql)
49
49
  end
50
50
 
51
51
  def test_complex_combination
52
- sql = User.where { ((:name == 'matz') & (:age > 18)) | !(:name == 'nobu') }.to_sql
53
- assert_sql(/"users"."name" = 'matz'/, sql)
52
+ sql = User.where { ((:name == 'alice') & (:age > 18)) | !(:name == 'bob') }.to_sql
53
+ assert_sql(/"users"."name" = 'alice'/, sql)
54
54
  assert_sql(/"users"."age" > 18/, sql)
55
55
  assert_sql(/NOT/, sql)
56
56
  assert_sql(/OR/, sql)
@@ -61,6 +61,36 @@ class TestBlockSyntax < Minitest::Test
61
61
  User.where { :users[:name].like?('tender%') }.to_sql)
62
62
  end
63
63
 
64
+ # ILIKE is PostgreSQL's; elsewhere Arel emits LIKE, which those adapters
65
+ # already match case-insensitively by default.
66
+ def test_ilike
67
+ expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
68
+ assert_sql(/WHERE "users"."name" #{expected} 'ma%'/,
69
+ User.where { :name.ilike?('ma%') }.to_sql)
70
+ end
71
+
72
+ def test_casecmp
73
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
74
+ User.where { :name.casecmp?('Alice') }.to_sql)
75
+ end
76
+
77
+ def test_casecmp_qualified
78
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
79
+ User.where { :users[:name].casecmp?('Alice') }.to_sql)
80
+ end
81
+
82
+ def test_casecmp_nil_is_rejected
83
+ e = assert_raises(ArgumentError) { User.where { :name.casecmp?(nil) } }
84
+ assert_match(/null\?/, e.message)
85
+ end
86
+
87
+ def test_casecmp_execution
88
+ User.delete_all
89
+ User.create!(name: 'Alice')
90
+ User.create!(name: 'bob')
91
+ assert_equal(['Alice'], User.where { :name.casecmp?('aLiCe') }.pluck(:name))
92
+ end
93
+
64
94
  def test_not_like
65
95
  assert_sql(/WHERE NOT \("users"."name" LIKE 'tender%'\)/,
66
96
  User.where { !:name.like?('tender%') }.to_sql)
@@ -85,8 +115,8 @@ class TestBlockSyntax < Minitest::Test
85
115
  # of literals; matching any one of them is enough.
86
116
  def test_start_with_multiple
87
117
  assert_sql(
88
- /WHERE \("users"."name" LIKE 'ma%' ESCAPE '\\' OR "users"."name" LIKE 'no%' ESCAPE '\\'\)/,
89
- User.where { :name.start_with?('ma', 'no') }.to_sql)
118
+ /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\)/,
119
+ User.where { :name.start_with?('al', 'bo') }.to_sql)
90
120
  end
91
121
 
92
122
  def test_end_with_multiple
@@ -98,8 +128,8 @@ class TestBlockSyntax < Minitest::Test
98
128
  # The OR arrives grouped, so a following & applies to the whole list.
99
129
  def test_start_with_multiple_combined
100
130
  assert_sql(
101
- /WHERE \("users"."name" LIKE 'ma%' ESCAPE '\\' OR "users"."name" LIKE 'no%' ESCAPE '\\'\) AND "users"."age" > 18/,
102
- User.where { :name.start_with?('ma', 'no') & (:age > 18) }.to_sql)
131
+ /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\) AND "users"."age" > 18/,
132
+ User.where { :name.start_with?('al', 'bo') & (:age > 18) }.to_sql)
103
133
  end
104
134
 
105
135
  def test_start_with_no_arguments
@@ -135,9 +165,52 @@ class TestBlockSyntax < Minitest::Test
135
165
  User.where { !:tags.member?('ruby') }.to_sql)
136
166
  end
137
167
 
138
- def test_member_multiple_elements
168
+ # Ruby's [1, 2].member?([1]) is false: member? tests one element, and an
169
+ # Array argument would have to mean something the namesake does not.
170
+ def test_member_array_is_rejected
171
+ e = assert_raises(ArgumentError) { User.where { :tags.member?(%w[ruby rails]) } }
172
+ assert_match(/superset\?/, e.message)
173
+ end
174
+
175
+ def test_superset
176
+ assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
177
+ User.where { :tags.superset?(%w[ruby rails]) }.to_sql)
178
+ end
179
+
180
+ def test_superset_takes_a_set
139
181
  assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
140
- User.where { :tags.member?(%w[ruby rails]) }.to_sql)
182
+ User.where { :tags.superset?(Set['ruby', 'rails']) }.to_sql)
183
+ end
184
+
185
+ def test_superset_rejects_a_scalar
186
+ assert_raises(ArgumentError) { User.where { :tags.superset?('ruby') } }
187
+ end
188
+
189
+ def test_subset
190
+ assert_sql(/WHERE "users"."tags" <@ '\{ruby,rails,go\}'/,
191
+ User.where { :tags.subset?(%w[ruby rails go]) }.to_sql)
192
+ end
193
+
194
+ def test_intersect
195
+ assert_sql(/WHERE "users"."tags" && '\{ruby,go\}'/,
196
+ User.where { :tags.intersect?(%w[ruby go]) }.to_sql)
197
+ end
198
+
199
+ def test_intersect_negated
200
+ assert_sql(/WHERE NOT \("users"."tags" && '\{ruby,go\}'\)/,
201
+ User.where { !:tags.intersect?(%w[ruby go]) }.to_sql)
202
+ end
203
+
204
+ def test_array_comparisons_execution
205
+ skip_without_array_columns
206
+ User.delete_all
207
+ User.create!(name: 'both', tags: %w[ruby rails])
208
+ User.create!(name: 'one', tags: %w[ruby go])
209
+ User.create!(name: 'neither', tags: %w[python])
210
+ assert_equal(['both'], User.where { :tags.superset?(%w[ruby rails]) }.pluck(:name))
211
+ assert_equal(['neither'], User.where { :tags.subset?(%w[python js]) }.pluck(:name))
212
+ assert_equal(%w[both one],
213
+ User.where { :tags.intersect?(%w[ruby js]) }.pluck(:name).sort)
141
214
  end
142
215
 
143
216
  # MySQL additionally escapes the double quotes inside its string literal,
@@ -265,6 +338,56 @@ class TestBlockSyntax < Minitest::Test
265
338
  User.where { !:age.in?([1, 2, 3]) }.to_sql)
266
339
  end
267
340
 
341
+ # Spelled IS [NOT] DISTINCT FROM on PostgreSQL, IS / IS NOT on SQLite and
342
+ # <=> on MySQL, so only the resulting rows are portable.
343
+ def test_not_distinct_from_execution
344
+ User.delete_all
345
+ User.create!(name: 'named')
346
+ User.create!(name: nil)
347
+ assert_equal([nil], User.where { :name.not_distinct_from?(nil) }.pluck(:name))
348
+ assert_equal(['named'], User.where { :name.distinct_from?(nil) }.pluck(:name))
349
+ end
350
+
351
+ def test_not_distinct_from_a_value_execution
352
+ User.delete_all
353
+ User.create!(name: 'alice')
354
+ User.create!(name: nil)
355
+ assert_equal(['alice'], User.where { :name.not_distinct_from?('alice') }.pluck(:name))
356
+ # Unlike !=, this keeps the NULL row.
357
+ assert_equal([nil], User.where { :name.distinct_from?('alice') }.pluck(:name))
358
+ end
359
+
360
+ def test_distinct_from_postgresql_syntax
361
+ skip "#{ADAPTER} spells it differently" unless ADAPTER == 'postgresql'
362
+ assert_sql(/WHERE "users"."name" IS NOT DISTINCT FROM 'x'/,
363
+ User.where { :name.not_distinct_from?('x') }.to_sql)
364
+ assert_sql(/WHERE "users"."name" IS DISTINCT FROM 'x'/,
365
+ User.where { :name.distinct_from?('x') }.to_sql)
366
+ end
367
+
368
+ def test_comparison_with_scalar_subquery
369
+ assert_sql(/WHERE "users"."age" >= \(SELECT AVG\("users"."age"\) FROM "users"\)/,
370
+ User.where { :age >= User.select { avg(:age) } }.to_sql)
371
+ end
372
+
373
+ def test_equality_with_scalar_subquery
374
+ assert_sql(/WHERE "users"."age" = \(SELECT MAX\("users"."age"\) FROM "users"\)/,
375
+ User.where { :age == User.select { max(:age) } }.to_sql)
376
+ end
377
+
378
+ # A scalar comparison has no sensible default select list, unlike in?.
379
+ def test_scalar_subquery_without_select_is_rejected
380
+ e = assert_raises(ArgumentError) { User.where { :age >= User.all } }
381
+ assert_match(/select/, e.message)
382
+ end
383
+
384
+ def test_scalar_subquery_execution
385
+ User.delete_all
386
+ User.create!(name: 'young', age: 20)
387
+ User.create!(name: 'old', age: 60)
388
+ assert_equal(['old'], User.where { :age >= User.select { avg(:age) } }.pluck(:name))
389
+ end
390
+
268
391
  def test_in_subquery
269
392
  assert_sql(
270
393
  /WHERE "authors"."id" IN \(SELECT "posts"."author_id" FROM "posts" WHERE "posts"."title" = 'pub'\)/,
@@ -298,8 +421,8 @@ class TestBlockSyntax < Minitest::Test
298
421
 
299
422
  def test_exists_combined
300
423
  assert_sql(
301
- /WHERE "authors"."name" = 'matz' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
302
- Author.where { (:name == 'matz') & exists?(Post.where(title: 'pub')) }.to_sql)
424
+ /WHERE "authors"."name" = 'alice' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
425
+ Author.where { (:name == 'alice') & exists?(Post.where(title: 'pub')) }.to_sql)
303
426
  end
304
427
 
305
428
  def test_exists_execution
@@ -354,8 +477,8 @@ class TestBlockSyntax < Minitest::Test
354
477
  end
355
478
 
356
479
  def test_qualified_column
357
- assert_sql(/WHERE "users"."name" = 'matz'/,
358
- User.where { :users[:name] == 'matz' }.to_sql)
480
+ assert_sql(/WHERE "users"."name" = 'alice'/,
481
+ User.where { :users[:name] == 'alice' }.to_sql)
359
482
  end
360
483
 
361
484
  def test_column_to_column_comparison
@@ -373,6 +496,84 @@ class TestBlockSyntax < Minitest::Test
373
496
  assert_sql(/LEFT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
374
497
  end
375
498
 
499
+ # The alias is what the block's qualified columns name, which is what makes
500
+ # a self join expressible at all. Adapters differ on writing the AS
501
+ # keyword, so the assertions allow either.
502
+ def test_joins_with_alias
503
+ assert_sql(
504
+ /INNER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
505
+ Author.joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
506
+ end
507
+
508
+ def test_left_outer_joins_with_alias
509
+ assert_sql(
510
+ /LEFT OUTER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
511
+ Author.left_outer_joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
512
+ end
513
+
514
+ def test_joins_alias_needs_a_block
515
+ assert_raises(ArgumentError) { Author.joins(:posts, as: :p) }
516
+ assert_raises(ArgumentError) { Author.left_outer_joins(:posts, as: :p) }
517
+ end
518
+
519
+ def test_joins_without_alias_still_delegates
520
+ assert_sql(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
521
+ Author.joins(:posts).to_sql)
522
+ end
523
+
524
+ def test_self_join_execution
525
+ Author.delete_all
526
+ Author.create!(name: 'shared')
527
+ Author.create!(name: 'other')
528
+ assert_equal(%w[other shared],
529
+ Author.joins(:authors, as: :mentors) { :mentors[:name] == :authors[:name] }.
530
+ pluck(:name).sort)
531
+ end
532
+
533
+ # ActiveRecord's from only takes a table name as a string.
534
+ def test_from_symbol
535
+ assert_sql(/FROM "tree"/, Node.from(:tree).to_sql)
536
+ end
537
+
538
+ def test_from_symbol_with_alias
539
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from(:tree, as: :nodes).to_sql)
540
+ end
541
+
542
+ def test_from_string_still_delegates
543
+ assert_sql(/FROM subq/, Node.from('subq').to_sql)
544
+ end
545
+
546
+ def test_from_alias_needs_a_symbol
547
+ assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
548
+ end
549
+
550
+ # A CTE is joined by name like any other table, so the recursive member's
551
+ # ON clause is a block rather than the string join Rails' own docs use.
552
+ def test_recursive_cte
553
+ Node.delete_all
554
+ root = Node.create!(name: 'root')
555
+ child = Node.create!(name: 'child', parent_id: root.id)
556
+ Node.create!(name: 'grandchild', parent_id: child.id)
557
+ Node.create!(name: 'unrelated', parent_id: nil)
558
+ descendants = Node.with_recursive(
559
+ tree: [
560
+ Node.where { :id == root.id },
561
+ Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] },
562
+ ]
563
+ ).from(:tree, as: :nodes)
564
+ assert_equal(%w[child grandchild root], descendants.pluck(:name).sort)
565
+ end
566
+
567
+ def test_cte_joined_by_name
568
+ Node.delete_all
569
+ root = Node.create!(name: 'root')
570
+ Node.create!(name: 'child', parent_id: root.id)
571
+ Node.create!(name: 'orphan', parent_id: nil)
572
+ q = Node.with(roots: Node.where { :parent_id.null? }).
573
+ joins(:roots) { :roots[:id] == :nodes[:parent_id] }
574
+ assert_equal(['child'], q.pluck(:name))
575
+ end
576
+
376
577
  def test_select_aggregate
377
578
  assert_sql(/SELECT SUM\("users"."age"\)/,
378
579
  User.select { :age.sum }.to_sql)
@@ -424,6 +625,33 @@ class TestBlockSyntax < Minitest::Test
424
625
  User.select { count(:*).as(:cnt) }.to_sql)
425
626
  end
426
627
 
628
+ def test_count_distinct
629
+ assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\)/,
630
+ User.select { count(:name, distinct: true) }.to_sql)
631
+ end
632
+
633
+ def test_count_distinct_as_method
634
+ assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS n/,
635
+ User.select { :name.count(distinct: true).as(:n) }.to_sql)
636
+ end
637
+
638
+ def test_count_distinct_in_having
639
+ assert_sql(/HAVING COUNT\(DISTINCT "users"."name"\) > 1/,
640
+ User.group(:age).having { count(:name, distinct: true) > 1 }.to_sql)
641
+ end
642
+
643
+ # DISTINCT is Arel's only aggregate modifier, and COUNT(DISTINCT *) is not
644
+ # valid SQL.
645
+ def test_distinct_is_rejected_for_other_aggregates
646
+ assert_raises(ArgumentError) do
647
+ ActiveRecord::Refined::AST::Aggregate.new(:age, :sum, distinct: true)
648
+ end
649
+ end
650
+
651
+ def test_count_star_distinct_is_rejected
652
+ assert_raises(ArgumentError) { User.select { count(:*, distinct: true) } }
653
+ end
654
+
427
655
  def test_having_count_star
428
656
  sql = Author.joins(:posts) { :posts[:author_id] == :authors[:id] }.
429
657
  group { :authors[:id] }.
@@ -482,6 +710,331 @@ class TestBlockSyntax < Minitest::Test
482
710
  User.where { length(:name) > 3 }.to_sql)
483
711
  end
484
712
 
713
+ # fn emits the name as written, so a case-sensitive one can be spelled
714
+ # exactly.
715
+ def test_fn
716
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\)/,
717
+ User.select { fn(:date_trunc, 'day', :name) }.to_sql)
718
+ end
719
+
720
+ def test_fn_is_comparable
721
+ assert_sql(/WHERE char_length\("users"."name"\) > 3/,
722
+ User.where { fn(:char_length, :name) > 3 }.to_sql)
723
+ end
724
+
725
+ def test_fn_alias
726
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS d/,
727
+ User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
728
+ end
729
+
730
+ # Aliases and function names are written into the SQL where a value would
731
+ # have been quoted, so a name that is not plain is refused rather than
732
+ # given the chance to close the identifier and carry on.
733
+ INJECTION = %q{a" AS x, (SELECT 1) AS "y}
734
+
735
+ def test_alias_rejects_an_injected_name
736
+ assert_raises(ArgumentError) { User.select { :name.as(INJECTION.to_sym) } }
737
+ assert_raises(ArgumentError) { User.select { count(:*).as(INJECTION.to_sym) } }
738
+ assert_raises(ArgumentError) { User.select { :name.as(:'a; DROP TABLE users') } }
739
+ end
740
+
741
+ def test_fn_rejects_an_injected_name
742
+ assert_raises(ArgumentError) { User.select { fn(INJECTION.to_sym, :name) } }
743
+ end
744
+
745
+ def test_plain_names_are_still_accepted
746
+ assert_sql(/AS post_count/, User.select { :name.as(:post_count) }.to_sql)
747
+ assert_sql(/AS 名前/, User.select { :name.as(:名前) }.to_sql)
748
+ assert_sql(/SELECT myFunc\(/, User.select { fn(:myFunc, :name) }.to_sql)
749
+ assert_sql(/SELECT pg_catalog.upper\(/,
750
+ User.select { fn(:'pg_catalog.upper', :name) }.to_sql)
751
+ end
752
+
753
+ # Values go through the adapter's quoting, which each spells its own way,
754
+ # so what is asserted is that the payload stays a value: it matches no row
755
+ # rather than opening the condition up.
756
+ def test_values_are_quoted
757
+ User.delete_all
758
+ User.create!(name: 'alice')
759
+ User.create!(name: 'bob')
760
+ payload = "x' OR 1=1 --"
761
+ assert_empty(User.where { :name == payload }.pluck(:name))
762
+ assert_empty(User.where { :name.like?(payload) }.pluck(:name))
763
+ assert_empty(User.where { :name.in?([payload]) }.pluck(:name))
764
+ assert_empty(User.where { :name.include?(payload) }.pluck(:name))
765
+ end
766
+
767
+ # Likewise for column names: the payload becomes one identifier, so the
768
+ # database rejects it as an unknown column instead of running it.
769
+ def test_column_names_are_quoted
770
+ assert_raises(ActiveRecord::StatementInvalid) do
771
+ User.where { :users[INJECTION.to_sym] == 1 }.to_a
772
+ end
773
+ end
774
+
775
+ def test_scalar_functions_shared_by_every_adapter
776
+ assert_sql(/SELECT CONCAT\(UPPER\("users"."name"\), 'x'\)/,
777
+ User.select { concat(upper(:name), 'x') }.to_sql)
778
+ assert_sql(/WHERE MOD\("users"."age", 7\) = 0/,
779
+ User.where { mod(:age, 7) == 0 }.to_sql)
780
+ end
781
+
782
+ # SQLite has no CHAR_LENGTH, GREATEST or LEAST, but LENGTH, MAX and MIN
783
+ # mean the same thing there.
784
+ def test_scalar_functions_spelled_differently_on_sqlite
785
+ expected = ADAPTER == 'sqlite3' ? %w[LENGTH MAX MIN] : %w[CHAR_LENGTH GREATEST LEAST]
786
+ assert_sql(/SELECT #{expected[0]}\("users"."name"\)/,
787
+ User.select { char_length(:name) }.to_sql)
788
+ assert_sql(/SELECT #{expected[1]}\("users"."age", 18\)/,
789
+ User.select { greatest(:age, 18) }.to_sql)
790
+ assert_sql(/SELECT #{expected[2]}\("users"."age", 99\)/,
791
+ User.select { least(:age, 99) }.to_sql)
792
+ end
793
+
794
+ def test_scalar_functions_run
795
+ User.delete_all
796
+ User.create!(name: 'alice', age: 60)
797
+ assert_equal(['ALICE-x'], User.select { concat(upper(:name), '-x').as(:v) }.map(&:v))
798
+ assert_equal([5], User.select { char_length(:name).as(:v) }.map(&:v))
799
+ assert_equal([60], User.select { greatest(:age, 18).as(:v) }.map(&:v))
800
+ end
801
+
802
+ # rand takes the name back from Kernel#rand, which would otherwise answer
803
+ # inside the block and never reach the database.
804
+ def test_rand
805
+ expected = ADAPTER == 'mysql2' ? 'RAND' : 'RANDOM'
806
+ assert_sql(/ORDER BY #{expected}\(\)/, User.order { rand }.to_sql)
807
+ end
808
+
809
+ # Where an adapter has no equivalent, the block raises instead of leaving
810
+ # the database to reject the SQL.
811
+ def test_unsupported_function_raises
812
+ if ADAPTER == 'postgresql'
813
+ assert_sql(/SELECT DATE_TRUNC\('day', "users"."name"\)/,
814
+ User.select { date_trunc('day', :name) }.to_sql)
815
+ else
816
+ e = assert_raises(NotImplementedError) { User.select { date_trunc('day', :name) } }
817
+ assert_match(/date_trunc/, e.message)
818
+ end
819
+ end
820
+
821
+ # MySQL's FORMAT is a different function that happens to share the name,
822
+ # and reads a printf template as the number zero rather than complaining,
823
+ # so the name carries the printf one and MySQL raises.
824
+ def test_format_is_printf_and_unsupported_on_mysql
825
+ if ADAPTER == 'mysql2'
826
+ assert_raises(NotImplementedError) { User.select { format('%s!', :name) } }
827
+ else
828
+ User.delete_all
829
+ User.create!(name: 'alice')
830
+ assert_equal(['alice!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
831
+ end
832
+ end
833
+
834
+ # MySQL's own is still reachable, spelled as the different thing it is.
835
+ def test_mysql_format_through_fn
836
+ assert_sql(/SELECT format\(1234.5678, 2\)/,
837
+ User.select { fn(:format, 1234.5678, 2) }.to_sql)
838
+ end
839
+
840
+ def test_now_is_unsupported_on_sqlite
841
+ if ADAPTER == 'sqlite3'
842
+ assert_raises(NotImplementedError) { User.select { now } }
843
+ else
844
+ assert_sql(/SELECT NOW\(\)/, User.select { now }.to_sql)
845
+ end
846
+ end
847
+
848
+ # CURRENT_TIMESTAMP and its relatives are grammar rather than calls, so
849
+ # they come out without the parentheses PostgreSQL and SQLite reject.
850
+ def test_datetime_value_functions_are_emitted_bare
851
+ assert_sql(/SELECT CURRENT_TIMESTAMP FROM/,
852
+ User.select { current_timestamp }.to_sql)
853
+ assert_sql(/SELECT CURRENT_DATE FROM/, User.select { current_date }.to_sql)
854
+ assert_sql(/SELECT CURRENT_TIME FROM/, User.select { current_time }.to_sql)
855
+ end
856
+
857
+ def test_datetime_value_function_in_comparison_and_alias
858
+ assert_sql(/WHERE "users"."name" < CURRENT_TIMESTAMP/,
859
+ User.where { :name < current_timestamp }.to_sql)
860
+ assert_sql(/SELECT CURRENT_TIMESTAMP AS ts FROM/,
861
+ User.select { current_timestamp.as(:ts) }.to_sql)
862
+ end
863
+
864
+ def test_current_timestamp_runs
865
+ User.delete_all
866
+ User.create!(name: 'alice')
867
+ refute_nil(User.select { current_timestamp.as(:v) }.sole.v)
868
+ end
869
+
870
+ # The one thing that does go into the parentheses is a precision, which
871
+ # current_date never takes and SQLite never accepts.
872
+ def test_datetime_value_function_with_precision
873
+ if ADAPTER == 'sqlite3'
874
+ e = assert_raises(NotImplementedError) { User.select { current_timestamp(3) } }
875
+ assert_match(/precision/, e.message)
876
+ else
877
+ assert_sql(/SELECT CURRENT_TIMESTAMP\(3\) FROM/,
878
+ User.select { current_timestamp(3) }.to_sql)
879
+ assert_sql(/SELECT CURRENT_TIME\(0\) FROM/,
880
+ User.select { current_time(0) }.to_sql)
881
+ User.delete_all
882
+ User.create!(name: 'alice')
883
+ refute_nil(User.select { current_timestamp(0).as(:v) }.sole.v)
884
+ end
885
+ end
886
+
887
+ def test_current_date_takes_no_precision
888
+ assert_raises(ArgumentError) { User.select { current_date(0) } }
889
+ end
890
+
891
+ # The precision is written into the SQL as given, so only an Integer is
892
+ # accepted there.
893
+ def test_precision_must_be_an_integer
894
+ assert_raises(ArgumentError) do
895
+ User.select { current_timestamp(:'3); DROP TABLE users --') }
896
+ end
897
+ end
898
+
899
+ def test_localtime_is_unsupported_on_sqlite
900
+ if ADAPTER == 'sqlite3'
901
+ assert_raises(NotImplementedError) { User.select { localtime } }
902
+ assert_raises(NotImplementedError) { User.select { localtimestamp } }
903
+ else
904
+ assert_sql(/SELECT LOCALTIME FROM/, User.select { localtime }.to_sql)
905
+ assert_sql(/SELECT LOCALTIMESTAMP FROM/,
906
+ User.select { localtimestamp }.to_sql)
907
+ end
908
+ end
909
+
910
+ def test_math_functions
911
+ assert_sql(/SELECT SIGN\("users"."age"\)/, User.select { sign(:age) }.to_sql)
912
+ assert_sql(/SELECT ATAN2\("users"."age", 2\)/,
913
+ User.select { atan2(:age, 2) }.to_sql)
914
+ assert_sql(/SELECT PI\(\)/, User.select { pi }.to_sql)
915
+ assert_sql(/SELECT DEGREES\(RADIANS\("users"."age"\)\)/,
916
+ User.select { degrees(radians(:age)) }.to_sql)
917
+ end
918
+
919
+ def test_math_functions_run
920
+ User.delete_all
921
+ User.create!(name: 'alice', age: 60)
922
+ assert_equal(1, User.select { sign(:age).as(:v) }.sole.v.to_i)
923
+ assert_equal(60,
924
+ User.select { round(degrees(radians(:age))).as(:v) }.sole.v.to_i)
925
+ end
926
+
927
+ # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
928
+ def test_log2_is_unsupported_on_postgresql
929
+ if ADAPTER == 'postgresql'
930
+ assert_raises(NotImplementedError) { User.select { log2(:age) } }
931
+ else
932
+ assert_sql(/SELECT LOG2\("users"."age"\)/, User.select { log2(:age) }.to_sql)
933
+ end
934
+ end
935
+
936
+ # MySQL spells trunc TRUNCATE, and insists on the second argument the
937
+ # others default to zero.
938
+ def test_trunc
939
+ expected = ADAPTER == 'mysql2' ? 'TRUNCATE' : 'TRUNC'
940
+ assert_sql(/SELECT #{expected}\("users"."age", 0\)/,
941
+ User.select { trunc(:age, 0) }.to_sql)
942
+ end
943
+
944
+ # EXTRACT(field FROM expr): the field is a keyword rather than a value.
945
+ # SQLite spells all of this as strftime formats, which no renaming
946
+ # carries.
947
+ def test_extract
948
+ if ADAPTER == 'sqlite3'
949
+ e = assert_raises(NotImplementedError) { User.select { extract(:year, :name) } }
950
+ assert_match(/extract/, e.message)
951
+ else
952
+ assert_sql(/SELECT EXTRACT\(YEAR FROM "users"."name"\)/,
953
+ User.select { extract(:year, :name) }.to_sql)
954
+ assert_sql(/WHERE EXTRACT\(YEAR FROM "users"."name"\) = 2026/,
955
+ User.where { extract(:year, :name) == 2026 }.to_sql)
956
+ end
957
+ end
958
+
959
+ # A bad field is an ArgumentError on every adapter, before SQLite gets to
960
+ # say it has no extract at all.
961
+ def test_extract_rejects_an_injected_field
962
+ assert_raises(ArgumentError) { User.select { extract(INJECTION.to_sym, :name) } }
963
+ end
964
+
965
+ def test_extract_runs
966
+ skip "#{ADAPTER} has no extract" if ADAPTER == 'sqlite3'
967
+ User.delete_all
968
+ User.create!(name: 'alice')
969
+ assert_equal(2026,
970
+ User.select { extract(:year, cast('2026-01-05', :date)).as(:v) }.sole.v.to_i)
971
+ end
972
+
973
+ def test_cast
974
+ assert_sql(/SELECT CAST\("users"."age" AS text\)/,
975
+ User.select { cast(:age, :text) }.to_sql)
976
+ end
977
+
978
+ def test_cast_runs
979
+ User.delete_all
980
+ User.create!(name: 'alice')
981
+ assert_equal(12.5,
982
+ User.select { cast('12.5', 'decimal(10,2)').as(:v) }.sole.v.to_f)
983
+ end
984
+
985
+ # The type is written into the SQL as given, so it has to look like one:
986
+ # a plain name, at most parenthesized with lengths. The adapters' own
987
+ # spellings with a space in them pass too.
988
+ def test_cast_type_names
989
+ assert_sql(/AS double precision\)/,
990
+ User.select { cast(:age, 'double precision') }.to_sql)
991
+ assert_sql(/AS decimal\(10,2\)\)/,
992
+ User.select { cast(:age, 'decimal(10,2)') }.to_sql)
993
+ assert_raises(ArgumentError) { User.select { cast(:age, INJECTION.to_sym) } }
994
+ assert_raises(ArgumentError) do
995
+ User.select { cast(:age, 'integer); DROP TABLE users --') }
996
+ end
997
+ end
998
+
999
+ # A name with no method of its own is still a NoMethodError, not a
1000
+ # function call the database has to reject.
1001
+ def test_unknown_function_is_a_no_method_error
1002
+ assert_raises(NoMethodError) { User.select { uppr(:name) } }
1003
+ end
1004
+
1005
+ def test_arithmetic_multiplication
1006
+ assert_sql(/SELECT "users"."age" \* 2 AS dbl/,
1007
+ User.select { (:age * 2).as(:dbl) }.to_sql)
1008
+ end
1009
+
1010
+ # Ruby puts * above >, so the expression groups the way it reads.
1011
+ def test_arithmetic_in_where_without_parentheses
1012
+ assert_sql(/WHERE "users"."age" \* 2 > 100/,
1013
+ User.where { :age * 2 > 100 }.to_sql)
1014
+ end
1015
+
1016
+ def test_arithmetic_between_columns
1017
+ assert_sql(/WHERE \("users"."age" \+ "users"."id"\) \/ 2 <= 30/,
1018
+ User.where { (:age + :id) / 2 <= 30 }.to_sql)
1019
+ end
1020
+
1021
+ def test_arithmetic_inside_aggregate
1022
+ assert_sql(/SELECT SUM\("users"."age" \* 2\)/,
1023
+ User.select { sum(:age * 2) }.to_sql)
1024
+ end
1025
+
1026
+ def test_aggregate_on_arithmetic
1027
+ assert_sql(/SELECT SUM\(\("users"."age" \+ 1\)\) AS s/,
1028
+ User.select { (:age + 1).sum.as(:s) }.to_sql)
1029
+ end
1030
+
1031
+ # Arel groups + and - but not * and /, which is how SQL precedence works
1032
+ # out anyway.
1033
+ def test_arithmetic_on_qualified_column
1034
+ assert_sql(/SELECT \("users"."age" - 1\)/,
1035
+ User.select { :users[:age] - 1 }.to_sql)
1036
+ end
1037
+
485
1038
  def test_coalesce_function_with_literal
486
1039
  assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
487
1040
  User.select { coalesce(:name, 'unknown') }.to_sql)
@@ -572,6 +1125,30 @@ class TestBlockSyntax < Minitest::Test
572
1125
  User.order { [:age.desc, :name.asc] }.to_sql)
573
1126
  end
574
1127
 
1128
+ def test_order_nulls_first
1129
+ skip_without_nulls_ordering_syntax
1130
+ assert_sql(/ORDER BY "users"."age" ASC NULLS FIRST/,
1131
+ User.order { :age.asc.nulls_first }.to_sql)
1132
+ end
1133
+
1134
+ def test_order_nulls_last
1135
+ skip_without_nulls_ordering_syntax
1136
+ assert_sql(/ORDER BY "users"."age" DESC NULLS LAST, "users"."name" ASC/,
1137
+ User.order { [:age.desc.nulls_last, :name.asc] }.to_sql)
1138
+ end
1139
+
1140
+ # The order itself is portable even where the syntax is not.
1141
+ def test_order_nulls_execution
1142
+ User.delete_all
1143
+ User.create!(name: 'null_age', age: nil)
1144
+ User.create!(name: 'young', age: 20)
1145
+ User.create!(name: 'old', age: 60)
1146
+ assert_equal(%w[null_age young old],
1147
+ User.order { :age.asc.nulls_first }.pluck(:name))
1148
+ assert_equal(%w[young old null_age],
1149
+ User.order { :age.asc.nulls_last }.pluck(:name))
1150
+ end
1151
+
575
1152
  def test_order_qualified_column
576
1153
  assert_sql(/ORDER BY "users"."name" DESC/,
577
1154
  User.order { :users[:name].desc }.to_sql)