activerecord-refined 0.5.1 → 0.6.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.
@@ -346,19 +346,220 @@ class TestBlockSyntax < Minitest::Test
346
346
  User.where { :users[:name].not_null? }.to_sql)
347
347
  end
348
348
 
349
+ # CASE has two shapes, and so does the block: an operand to compare each
350
+ # `when` against, or a condition on every `when`.
351
+ def test_case_with_an_operand
352
+ assert_sql(/SELECT CASE "users"."age" WHEN 10 THEN 'ten' ELSE 'other' END AS "v"/,
353
+ User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql)
354
+ end
355
+
356
+ def test_when_on_a_column_is_the_same_case
357
+ assert_equal(
358
+ User.select { self.case(:age).when(10).then('ten').else('other').as(:v) }.to_sql,
359
+ User.select { :age.when(10).then('ten').else('other').as(:v) }.to_sql)
360
+ end
361
+
362
+ def test_searched_case
363
+ assert_sql(/SELECT CASE WHEN "users"."age" >= 60 THEN 'senior' ELSE 'other' END AS "v"/,
364
+ User.select { case_when { :age >= 60 }.then('senior').else('other').as(:v) }.to_sql)
365
+ end
366
+
367
+ def test_case_when_is_the_same_as_case_with_no_operand
368
+ assert_equal(
369
+ User.select { self.case.when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql,
370
+ User.select { case_when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql)
371
+ end
372
+
373
+ # A value and a block say the same thing; the block is there to read like
374
+ # the blocks around it.
375
+ def test_a_condition_reads_the_same_either_way
376
+ assert_equal(
377
+ User.select { case_when(:age >= 60).then(1).else(0).as(:v) }.to_sql,
378
+ User.select { case_when { :age >= 60 }.then(1).else(0).as(:v) }.to_sql)
379
+ end
380
+
381
+ def test_case_with_several_whens
382
+ assert_sql(
383
+ /CASE WHEN "users"."age" < 18 THEN 'minor' WHEN "users"."age" >= 60 THEN 'senior' ELSE 'adult' END/,
384
+ User.select {
385
+ case_when { :age < 18 }.then('minor').
386
+ when { :age >= 60 }.then('senior').
387
+ else('adult').as(:v)
388
+ }.to_sql)
389
+ end
390
+
391
+ # Leaving the ELSE off is SQL's own default rather than an omission.
392
+ def test_case_without_an_else
393
+ sql = User.select { case_when { :age >= 60 }.then('senior').as(:v) }.to_sql
394
+ assert_sql(/CASE WHEN "users"."age" >= 60 THEN 'senior' END/, sql)
395
+ refute_match(/ELSE/, sql)
396
+ end
397
+
398
+ def test_case_takes_expressions_and_columns
399
+ assert_sql(/THEN \("users"."age" - 60\)/,
400
+ User.select { case_when { :age >= 60 }.then { :age - 60 }.else(0).as(:v) }.to_sql)
401
+ assert_sql(/THEN "users"."name"/,
402
+ User.select { case_when { :age >= 60 }.then(:name).else('x').as(:v) }.to_sql)
403
+ end
404
+
405
+ def test_case_is_an_expression_like_any_other
406
+ assert_sql(/SUM\(CASE WHEN/,
407
+ User.select { sum(case_when { :age >= 60 }.then(1).else(0)).as(:v) }.to_sql)
408
+ assert_sql(/WHERE CASE "users"."age" WHEN 10 THEN 1 ELSE 2 END = 1/,
409
+ User.where { self.case(:age).when(10).then(1).else(2) == 1 }.to_sql)
410
+ end
411
+
412
+ def test_case_execution
413
+ User.delete_all
414
+ User.create!(name: 'senior', age: 70)
415
+ User.create!(name: 'adult', age: 30)
416
+ User.create!(name: 'minor', age: 10)
417
+ assert_equal(%w[adult minor senior],
418
+ User.select {
419
+ case_when { :age < 18 }.then('minor').
420
+ when { :age >= 60 }.then('senior').
421
+ else('adult').as(:v)
422
+ }.map(&:v).sort)
423
+ end
424
+
425
+ # One case finished two ways: the methods return new nodes rather than
426
+ # adding to the one they were called on.
427
+ def test_a_case_is_not_added_to_in_place
428
+ sql = User.select {
429
+ started = case_when { :age >= 60 }.then(1)
430
+ [started.else(0).as(:a), started.else(9).as(:b)]
431
+ }.to_sql
432
+ assert_sql(/THEN 1 ELSE 0 END AS "a"/, sql)
433
+ assert_sql(/THEN 1 ELSE 9 END AS "b"/, sql)
434
+ end
435
+
436
+ def test_when_needs_a_value_or_a_block
437
+ assert_raises(ArgumentError) { User.select { case_when.then(1) } }
438
+ e = assert_raises(ArgumentError) { User.select { case_when(1) { 2 }.then(1) } }
439
+ assert_match(/not both/, e.message)
440
+ end
441
+
442
+ def test_when_needs_a_matching_then
443
+ e = assert_raises(ArgumentError) { User.select { :age.when(10) }.to_sql }
444
+ assert_match(/matching then/, e.message)
445
+ end
446
+
447
+ # Kernel#then would otherwise answer this one, with no block and no noise.
448
+ def test_then_without_a_when_says_so
449
+ e = assert_raises(ArgumentError) { User.select { self.case(:age).then(1) } }
450
+ assert_match(/follows a when/, e.message)
451
+ end
452
+
453
+ # A window is built by chaining, the way Arel's own is.
454
+ def test_over_with_no_window
455
+ assert_sql(/SELECT AVG\("users"."age"\) OVER \(\) AS "v"/,
456
+ User.select { avg(:age).over.as(:v) }.to_sql)
457
+ end
458
+
459
+ def test_over_partition_and_order
460
+ assert_sql(
461
+ /AVG\("users"."age"\) OVER \(PARTITION BY "users"."name" ORDER BY "users"."age" DESC\)/,
462
+ User.select { avg(:age).over.partition(:name).order(:age.desc).as(:v) }.to_sql)
463
+ end
464
+
465
+ def test_over_takes_several_expressions
466
+ assert_sql(/PARTITION BY "users"."name", "users"."age"/,
467
+ User.select { count(:*).over.partition(:name, :age).as(:v) }.to_sql)
468
+ end
469
+
470
+ # The window-only functions, which the adapters that have them at all spell
471
+ # the same way.
472
+ def test_window_functions
473
+ assert_sql(/ROW_NUMBER\(\) OVER \(ORDER BY "users"."age"\)/,
474
+ User.select { row_number.over.order(:age).as(:v) }.to_sql)
475
+ assert_sql(/RANK\(\) OVER/, User.select { rank.over.order(:age).as(:v) }.to_sql)
476
+ assert_sql(/NTILE\(2\) OVER/, User.select { ntile(2).over.order(:age).as(:v) }.to_sql)
477
+ assert_sql(/LAG\("users"."age", 1\) OVER/,
478
+ User.select { lag(:age).over.order(:age).as(:v) }.to_sql)
479
+ assert_sql(/LAG\("users"."age", 2, 0\) OVER/,
480
+ User.select { lag(:age, 2, 0).over.order(:age).as(:v) }.to_sql)
481
+ end
482
+
483
+ # A frame is a range of rows counted from the current one: negative before
484
+ # it, positive after, 0 the row itself, an open end for unbounded.
485
+ def test_window_frames
486
+ assert_sql(/ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW/,
487
+ User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.to_sql)
488
+ assert_sql(/ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING/,
489
+ User.select { sum(:age).over.order(:age).rows(-1..1).as(:v) }.to_sql)
490
+ assert_sql(/ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING/,
491
+ User.select { sum(:age).over.order(:age).rows(0..).as(:v) }.to_sql)
492
+ assert_sql(/RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW/,
493
+ User.select { sum(:age).over.order(:age).range(..0).as(:v) }.to_sql)
494
+ end
495
+
496
+ def test_over_is_an_expression_like_any_other
497
+ assert_sql(/\(RANK\(\) OVER \(ORDER BY "users"."age"\) \+ 1\) AS "v"/,
498
+ User.select { (rank.over.order(:age) + 1).as(:v) }.to_sql)
499
+ end
500
+
501
+ def test_window_execution
502
+ User.delete_all
503
+ User.create!(name: 'a', age: 20)
504
+ User.create!(name: 'b', age: 30)
505
+ User.create!(name: 'c', age: 40)
506
+ assert_equal([1, 2, 3],
507
+ User.select { row_number.over.order(:age).as(:v) }.map {|u| u.v.to_i })
508
+ assert_equal([20, 50, 90],
509
+ User.select { sum(:age).over.order(:age).rows(..0).as(:v) }.map {|u| u.v.to_i })
510
+ end
511
+
512
+ # One window finished two ways: the methods return new nodes.
513
+ def test_a_window_is_not_added_to_in_place
514
+ sql = User.select {
515
+ started = sum(:age).over.order(:age)
516
+ [started.partition(:name).as(:a), started.as(:b)]
517
+ }.to_sql
518
+ assert_sql(/PARTITION BY "users"."name" ORDER BY "users"."age"\) AS "a"/, sql)
519
+ assert_sql(/SUM\("users"."age"\) OVER \(ORDER BY "users"."age"\) AS "b"/, sql)
520
+ end
521
+
522
+ def test_a_window_function_needs_over
523
+ e = assert_raises(ArgumentError) { User.select { row_number.as(:v) }.to_sql }
524
+ assert_match(/needs over/, e.message)
525
+ end
526
+
527
+ def test_a_window_has_one_frame
528
+ assert_raises(ArgumentError) { User.select { sum(:age).over.rows(..0).range(..0) } }
529
+ end
530
+
531
+ def test_a_frame_is_a_range_of_rows
532
+ assert_raises(ArgumentError) { User.select { sum(:age).over.rows(3) } }
533
+ assert_raises(ArgumentError) { User.select { sum(:age).over.rows('a'..'b') } }
534
+ e = assert_raises(ArgumentError) { User.select { sum(:age).over.rows(-2...0) } }
535
+ assert_match(/ends on a row/, e.message)
536
+ end
537
+
538
+ def test_partition_needs_an_expression
539
+ assert_raises(ArgumentError) { User.select { sum(:age).over.partition } }
540
+ assert_raises(ArgumentError) { User.select { sum(:age).over.order } }
541
+ end
542
+
543
+ def test_case_needs_a_when
544
+ e = assert_raises(ArgumentError) { User.select { self.case(:age).else(1) }.to_sql }
545
+ assert_match(/needs a when/, e.message)
546
+ end
547
+
349
548
  # The claim these methods rest on: the direct spelling is the same rows as
350
549
  # negating the positive one, which is where a NULL would show a difference
351
550
  # if there were one.
352
551
  def test_the_negations_match_what_bang_selects
353
552
  User.delete_all
354
- User.create!(name: 'alice', age: 60)
355
- User.create!(name: 'bob', age: 20)
553
+ User.create!(name: 'alice', age: 60, active: true)
554
+ User.create!(name: 'bob', age: 20, active: false)
356
555
  User.create!(name: nil, age: 40)
357
556
  [
358
557
  [-> { :name.not_null? }, -> { !:name.null? }],
359
558
  [-> { :age.not_in?([20, 30]) }, -> { !:age.in?([20, 30]) }],
360
559
  [-> { :age.not_between?(20, 30) }, -> { !:age.between?(20, 30) }],
361
560
  [-> { :name.not_like?('a%') }, -> { !:name.like?('a%') }],
561
+ [-> { :active.not_true? }, -> { !:active.true? }],
562
+ [-> { :active.not_false? }, -> { !:active.false? }],
362
563
  ].each do |direct, negated|
363
564
  assert_equal(User.where(&negated).pluck(:id).sort,
364
565
  User.where(&direct).pluck(:id).sort,
@@ -376,6 +577,44 @@ class TestBlockSyntax < Minitest::Test
376
577
  assert_match(/null\?/, e.message)
377
578
  end
378
579
 
580
+ def test_is_true
581
+ assert_sql(/WHERE "users"."active" IS TRUE/, User.where { :active.true? }.to_sql)
582
+ end
583
+
584
+ def test_is_not_true
585
+ assert_sql(/WHERE "users"."active" IS NOT TRUE/, User.where { :active.not_true? }.to_sql)
586
+ end
587
+
588
+ def test_is_false
589
+ assert_sql(/WHERE "users"."active" IS FALSE/, User.where { :active.false? }.to_sql)
590
+ end
591
+
592
+ def test_is_not_false
593
+ assert_sql(/WHERE "users"."active" IS NOT FALSE/, User.where { :active.not_false? }.to_sql)
594
+ end
595
+
596
+ # The four are spelled and answered the same way by every adapter, NULL
597
+ # included, which is what makes them worth having over = TRUE.
598
+ def test_truth_values_execution
599
+ User.delete_all
600
+ User.create!([{name: 'yes', active: true}, {name: 'no', active: false},
601
+ {name: 'unset', active: nil}])
602
+ order = ->(relation) { relation.order(:name).pluck(:name) }
603
+ assert_equal(['yes'], order.(User.where { :active.true? }))
604
+ assert_equal(%w[no unset], order.(User.where { :active.not_true? }))
605
+ assert_equal(['no'], order.(User.where { :active.false? }))
606
+ assert_equal(%w[unset yes], order.(User.where { :active.not_false? }))
607
+ end
608
+
609
+ # Where the difference from a comparison against the literal shows: = TRUE
610
+ # is NULL for a NULL row, and negating it leaves that row out.
611
+ def test_not_true_keeps_the_nulls_equality_drops
612
+ User.delete_all
613
+ User.create!([{name: 'no', active: false}, {name: 'unset', active: nil}])
614
+ assert_equal(%w[no unset], User.where { :active.not_true? }.order(:name).pluck(:name))
615
+ assert_equal(['no'], User.where { !(:active == true) }.order(:name).pluck(:name))
616
+ end
617
+
379
618
  def test_in
380
619
  assert_sql(/WHERE "users"."age" IN \(1, 2, 3\)/,
381
620
  User.where { :age.in?([1, 2, 3]) }.to_sql)
@@ -469,6 +708,66 @@ class TestBlockSyntax < Minitest::Test
469
708
  Author.where { !:id.in?(Post.select(:author_id)) }.to_sql)
470
709
  end
471
710
 
711
+ def test_any_subquery
712
+ skip_without_quantifiers
713
+ assert_sql(
714
+ /WHERE "users"."age" > ANY\(SELECT "users"."age" FROM "users" WHERE "users"."name" = 'alice'\)/,
715
+ User.where { :age > any(User.where(name: 'alice').select(:age)) }.to_sql)
716
+ end
717
+
718
+ def test_all_subquery
719
+ skip_without_quantifiers
720
+ assert_sql(/WHERE "users"."age" >= ALL\(SELECT "users"."age" FROM "users"\)/,
721
+ User.where { :age >= all(User.select(:age)) }.to_sql)
722
+ end
723
+
724
+ # The same default in? has, since both take the relation for a set of rows.
725
+ def test_quantifier_selects_primary_key_by_default
726
+ skip_without_quantifiers
727
+ assert_sql(/WHERE "authors"."id" > ANY\(SELECT "posts"."id" FROM "posts"\)/,
728
+ Author.where { :id > any(Post.all) }.to_sql)
729
+ end
730
+
731
+ # A list is what in? takes; ANY of one is what a plain comparison says.
732
+ def test_quantifier_without_a_relation_is_rejected
733
+ skip_without_quantifiers
734
+ e = assert_raises(ArgumentError) { User.where { :age > any([20, 30]) } }
735
+ assert_match(/relation/, e.message)
736
+ end
737
+
738
+ def test_quantifier_is_unsupported_on_sqlite
739
+ if ADAPTER == 'sqlite3'
740
+ e = assert_raises(NotImplementedError) { User.where { :age > any(User.select(:age)) } }
741
+ assert_match(/ANY/, e.message)
742
+ else
743
+ assert_sql(/> ANY\(SELECT/, User.where { :age > any(User.select(:age)) }.to_sql)
744
+ end
745
+ end
746
+
747
+ # ANY is satisfied by one row of the subquery and ALL by every row, so the
748
+ # two pick out the ends of the range the subquery covers.
749
+ def test_quantifier_execution
750
+ skip_without_quantifiers
751
+ User.delete_all
752
+ User.create!([{name: 'young', age: 20}, {name: 'middle', age: 40},
753
+ {name: 'old', age: 60}])
754
+ ages = -> { User.select(:age) }
755
+ assert_equal(%w[middle old], User.where { :age > any(ages.call) }.order(:age).pluck(:name))
756
+ assert_equal(['old'], User.where { :age >= all(ages.call) }.pluck(:name))
757
+ assert_equal(['young'], User.where { :age <= all(ages.call) }.pluck(:name))
758
+ end
759
+
760
+ # = ANY is IN and != ALL is NOT IN, which is worth a test because it is the
761
+ # part of the quantifiers the gem already had another spelling for.
762
+ def test_quantifier_equality_execution
763
+ skip_without_quantifiers
764
+ User.delete_all
765
+ User.create!([{name: 'young', age: 20}, {name: 'old', age: 60}])
766
+ young = -> { User.where(name: 'young').select(:age) }
767
+ assert_equal(['young'], User.where { :age == any(young.call) }.pluck(:name))
768
+ assert_equal(['old'], User.where { :age != all(young.call) }.pluck(:name))
769
+ end
770
+
472
771
  # The subquery correlates with the outer table through qualified columns,
473
772
  # and its own where block goes through the DSL too.
474
773
  def test_exists
@@ -520,21 +819,21 @@ class TestBlockSyntax < Minitest::Test
520
819
  # literal depends on the column type, so assert on the Arel node instead.
521
820
  def test_equal_range_is_an_equality
522
821
  node = ActiveRecord::Refined::AST::Comparison.new(:period, :==, 18..65).
523
- to_arel(User.arel_table)
822
+ to_arel(User.arel_table, User)
524
823
  assert_instance_of(Arel::Nodes::Equality, node)
525
824
  assert_equal(18..65, node.right.value)
526
825
  end
527
826
 
528
827
  def test_equal_array_is_an_equality
529
828
  node = ActiveRecord::Refined::AST::Comparison.new(:tags, :==, [1, 2, 3]).
530
- to_arel(User.arel_table)
829
+ to_arel(User.arel_table, User)
531
830
  assert_instance_of(Arel::Nodes::Equality, node)
532
831
  assert_equal([1, 2, 3], node.right.value)
533
832
  end
534
833
 
535
834
  def test_not_equal_array_is_an_inequality
536
835
  node = ActiveRecord::Refined::AST::Comparison.new(:tags, :!=, [1, 2, 3]).
537
- to_arel(User.arel_table)
836
+ to_arel(User.arel_table, User)
538
837
  assert_instance_of(Arel::Nodes::NotEqual, node)
539
838
  assert_equal([1, 2, 3], node.right.value)
540
839
  end
@@ -611,14 +910,43 @@ class TestBlockSyntax < Minitest::Test
611
910
  end
612
911
 
613
912
  def test_from_cte_takes_the_alias_from_the_model
614
- assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from_cte(:tree).to_sql)
615
- assert_equal(Node.from(:tree, as: :nodes).to_sql, Node.from_cte(:tree).to_sql)
913
+ declared = Node.with(tree: Node.all)
914
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/, declared.from_cte(:tree).to_sql)
915
+ assert_equal(declared.from(:tree, as: :nodes).to_sql,
916
+ declared.from_cte(:tree).to_sql)
616
917
  end
617
918
 
618
919
  def test_from_cte_needs_a_symbol
619
920
  assert_raises(ArgumentError) { Node.from_cte('tree') }
620
921
  end
621
922
 
923
+ # The name has to be one `with` declares, or the query is against a table
924
+ # nobody has -- which the database would say much later and less clearly.
925
+ def test_from_cte_needs_a_cte_of_that_name
926
+ e = assert_raises(ArgumentError) do
927
+ Node.with(tree: Node.all).from_cte(:tre).to_sql
928
+ end
929
+ assert_match(/names no CTE/, e.message)
930
+ assert_match(/:tree/, e.message)
931
+
932
+ e = assert_raises(ArgumentError) { Node.from_cte(:tree).to_sql }
933
+ assert_match(/declares none/, e.message)
934
+ end
935
+
936
+ # Checked when the SQL is built, so where the CTE is declared in the chain
937
+ # does not matter.
938
+ def test_from_cte_takes_a_cte_declared_later
939
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/,
940
+ Node.from_cte(:tree).with(tree: Node.all).to_sql)
941
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/,
942
+ Node.from_cte(:tree).merge(Node.with(tree: Node.all)).to_sql)
943
+ end
944
+
945
+ # from itself says nothing about CTEs and goes on taking any table.
946
+ def test_from_with_an_alias_is_not_checked
947
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from(:tree, as: :nodes).to_sql)
948
+ end
949
+
622
950
  # The alias is what lets a where find its column, which is the whole reason
623
951
  # from_cte exists; without it the SQL names a table the query does not have.
624
952
  def test_from_cte_leaves_where_able_to_qualify
@@ -667,33 +995,29 @@ class TestBlockSyntax < Minitest::Test
667
995
  assert_equal(['child'], q.pluck(:name))
668
996
  end
669
997
 
670
- def test_select_aggregate
671
- assert_sql(/SELECT SUM\("users"."age"\)/,
672
- User.select { :age.sum }.to_sql)
998
+ def test_select_sum
999
+ assert_sql(/SELECT SUM\("users"."age"\)/, User.select { sum(:age) }.to_sql)
673
1000
  end
674
1001
 
675
- def test_select_aggregate_qualified
1002
+ def test_select_aggregate_of_qualified_column
676
1003
  assert_sql(/SELECT COUNT\("users"."id"\)/,
677
- User.select { :users[:id].count }.to_sql)
678
- end
679
-
680
- def test_select_average
681
- assert_sql(/SELECT AVG\("users"."age"\)/,
682
- User.select { :age.average }.to_sql)
1004
+ User.select { count(:users[:id]) }.to_sql)
683
1005
  end
684
1006
 
685
- def test_select_maximum
686
- assert_sql(/SELECT MAX\("users"."age"\)/,
687
- User.select { :age.maximum }.to_sql)
1007
+ def test_select_max_and_min
1008
+ assert_sql(/SELECT MAX\("users"."age"\)/, User.select { max(:age) }.to_sql)
1009
+ assert_sql(/SELECT MIN\("users"."age"\)/, User.select { min(:age) }.to_sql)
688
1010
  end
689
1011
 
690
- def test_select_minimum
691
- assert_sql(/SELECT MIN\("users"."age"\)/,
692
- User.select { :age.minimum }.to_sql)
1012
+ # An aggregate is written as a call, the way SQL writes it; a column has no
1013
+ # method of its own for one.
1014
+ def test_aggregates_have_no_postfix_form
1015
+ assert_raises(NoMethodError) { User.select { :age.sum } }
1016
+ assert_raises(NoMethodError) { User.select { :age.average } }
693
1017
  end
694
1018
 
695
1019
  def test_having_aggregate
696
- sql = User.group(:name).having { :age.sum > 100 }.to_sql
1020
+ sql = User.group(:name).having { sum(:age) > 100 }.to_sql
697
1021
  assert_sql(/GROUP BY "users"."name"/, sql)
698
1022
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
699
1023
  end
@@ -714,7 +1038,7 @@ class TestBlockSyntax < Minitest::Test
714
1038
  end
715
1039
 
716
1040
  def test_select_count_star_alias
717
- assert_sql(/SELECT COUNT\(\*\) AS cnt/,
1041
+ assert_sql(/SELECT COUNT\(\*\) AS "cnt"/,
718
1042
  User.select { count(:*).as(:cnt) }.to_sql)
719
1043
  end
720
1044
 
@@ -723,9 +1047,9 @@ class TestBlockSyntax < Minitest::Test
723
1047
  User.select { count(:name, distinct: true) }.to_sql)
724
1048
  end
725
1049
 
726
- def test_count_distinct_as_method
727
- assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS n/,
728
- User.select { :name.count(distinct: true).as(:n) }.to_sql)
1050
+ def test_count_distinct_aliased
1051
+ assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS "n"/,
1052
+ User.select { count(:name, distinct: true).as(:n) }.to_sql)
729
1053
  end
730
1054
 
731
1055
  def test_count_distinct_in_having
@@ -783,11 +1107,6 @@ class TestBlockSyntax < Minitest::Test
783
1107
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
784
1108
  end
785
1109
 
786
- def test_function_and_method_syntax_match
787
- assert_equal User.select { :age.average }.to_sql,
788
- User.select { avg(:age) }.to_sql
789
- end
790
-
791
1110
  def test_upper_function
792
1111
  assert_sql(/SELECT UPPER\("users"."name"\)/,
793
1112
  User.select { upper(:name) }.to_sql)
@@ -816,7 +1135,7 @@ class TestBlockSyntax < Minitest::Test
816
1135
  end
817
1136
 
818
1137
  def test_fn_alias
819
- assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS d/,
1138
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS "d"/,
820
1139
  User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
821
1140
  end
822
1141
 
@@ -825,10 +1144,46 @@ class TestBlockSyntax < Minitest::Test
825
1144
  # given the chance to close the identifier and carry on.
826
1145
  INJECTION = %q{a" AS x, (SELECT 1) AS "y}
827
1146
 
828
- def test_alias_rejects_an_injected_name
829
- assert_raises(ArgumentError) { User.select { :name.as(INJECTION.to_sym) } }
830
- assert_raises(ArgumentError) { User.select { count(:*).as(INJECTION.to_sym) } }
831
- assert_raises(ArgumentError) { User.select { :name.as(:'a; DROP TABLE users') } }
1147
+ # An alias that is not a plain name is quoted by the adapter rather than
1148
+ # refused, so an injected one becomes an alias with a strange name and
1149
+ # nothing else. Each spells the quoting its own way, so what is asserted is
1150
+ # that the payload arrived as the name of the column it labelled.
1151
+ def test_an_injected_alias_is_quoted_rather_than_refused
1152
+ User.delete_all
1153
+ User.create!(name: 'alice')
1154
+ payload = 'a" FROM users; --'
1155
+ row = User.select { :name.as(payload.to_sym) }.first
1156
+ assert_equal('alice', row[payload])
1157
+ assert_equal(1, User.count)
1158
+ end
1159
+
1160
+ def test_an_alias_that_needs_quoting_gets_it
1161
+ assert_sql(/AS "total sales"/, User.select { :name.as(:'total sales') }.to_sql)
1162
+ assert_sql(/AS "select"/, User.select { :name.as(:select, quote: true) }.to_sql)
1163
+ assert_sql(/AS "up per"/, User.select { upper(:name).as(:'up per') }.to_sql)
1164
+ assert_sql(/AS "d epth"/, User.select { 0.as(:'d epth') }.to_sql)
1165
+ end
1166
+
1167
+ # Quoted, the name asked for is the name that comes back. Unquoted,
1168
+ # PostgreSQL would fold the capital away and the other two would keep it.
1169
+ def test_an_alias_keeps_the_name_as_written
1170
+ assert_sql(/AS "postCount"/, User.select { :name.as(:postCount) }.to_sql)
1171
+ User.delete_all
1172
+ User.create!(name: 'alice')
1173
+ assert_equal('alice', User.select { :name.as(:postCount) }.first['postCount'])
1174
+ end
1175
+
1176
+ def test_quote_false_asks_for_the_name_as_it_is
1177
+ assert_sql(/AS post_count/, User.select { :name.as(:post_count, quote: false) }.to_sql)
1178
+ refute_match(/"post_count"/,
1179
+ normalize_sql(User.select { :name.as(:post_count, quote: false) }.to_sql))
1180
+ end
1181
+
1182
+ # Nothing quotes it, so a name that would be SQL has to be refused.
1183
+ def test_quote_false_refuses_a_name_that_is_not_plain
1184
+ e = assert_raises(ArgumentError) { User.select { :name.as(:'total sales', quote: false) } }
1185
+ assert_match(/plain column alias/, e.message)
1186
+ assert_raises(ArgumentError) { User.select { :name.as(INJECTION.to_sym, quote: false) } }
832
1187
  end
833
1188
 
834
1189
  def test_fn_rejects_an_injected_name
@@ -836,8 +1191,8 @@ class TestBlockSyntax < Minitest::Test
836
1191
  end
837
1192
 
838
1193
  def test_plain_names_are_still_accepted
839
- assert_sql(/AS post_count/, User.select { :name.as(:post_count) }.to_sql)
840
- assert_sql(/AS 名前/, User.select { :name.as(:名前) }.to_sql)
1194
+ assert_sql(/AS "post_count"/, User.select { :name.as(:post_count) }.to_sql)
1195
+ assert_sql(/AS "名前"/, User.select { :name.as(:名前) }.to_sql)
841
1196
  assert_sql(/SELECT myFunc\(/, User.select { fn(:myFunc, :name) }.to_sql)
842
1197
  assert_sql(/SELECT pg_catalog.upper\(/,
843
1198
  User.select { fn(:'pg_catalog.upper', :name) }.to_sql)
@@ -950,7 +1305,7 @@ class TestBlockSyntax < Minitest::Test
950
1305
  def test_datetime_value_function_in_comparison_and_alias
951
1306
  assert_sql(/WHERE "users"."name" < CURRENT_TIMESTAMP/,
952
1307
  User.where { :name < current_timestamp }.to_sql)
953
- assert_sql(/SELECT CURRENT_TIMESTAMP AS ts FROM/,
1308
+ assert_sql(/SELECT CURRENT_TIMESTAMP AS "ts" FROM/,
954
1309
  User.select { current_timestamp.as(:ts) }.to_sql)
955
1310
  end
956
1311
 
@@ -1096,7 +1451,7 @@ class TestBlockSyntax < Minitest::Test
1096
1451
  end
1097
1452
 
1098
1453
  def test_arithmetic_multiplication
1099
- assert_sql(/SELECT "users"."age" \* 2 AS dbl/,
1454
+ assert_sql(/SELECT "users"."age" \* 2 AS "dbl"/,
1100
1455
  User.select { (:age * 2).as(:dbl) }.to_sql)
1101
1456
  end
1102
1457
 
@@ -1116,11 +1471,6 @@ class TestBlockSyntax < Minitest::Test
1116
1471
  User.select { sum(:age * 2) }.to_sql)
1117
1472
  end
1118
1473
 
1119
- def test_aggregate_on_arithmetic
1120
- assert_sql(/SELECT SUM\(\("users"."age" \+ 1\)\) AS s/,
1121
- User.select { (:age + 1).sum.as(:s) }.to_sql)
1122
- end
1123
-
1124
1474
  # Arel groups + and - but not * and /, which is how SQL precedence works
1125
1475
  # out anyway.
1126
1476
  def test_arithmetic_on_qualified_column
@@ -1128,6 +1478,110 @@ class TestBlockSyntax < Minitest::Test
1128
1478
  User.select { :users[:age] - 1 }.to_sql)
1129
1479
  end
1130
1480
 
1481
+ def test_bitwise_and_or
1482
+ assert_sql(/SELECT \("users"."flags" & 4\) AS "masked"/,
1483
+ User.select { (:flags & 4).as(:masked) }.to_sql)
1484
+ assert_sql(/SELECT \("users"."flags" \| 4\) AS "set"/,
1485
+ User.select { (:flags | 4).as(:set) }.to_sql)
1486
+ end
1487
+
1488
+ # Ruby puts & above >, so this groups the way it reads, and the node
1489
+ # parenthesises itself so that the adapter's own precedence cannot regroup
1490
+ # it -- PostgreSQL gives & and | the same one.
1491
+ def test_bitwise_in_where_without_parentheses
1492
+ assert_sql(/WHERE \("users"."flags" & 4\) > 0/,
1493
+ User.where { :flags & 4 > 0 }.to_sql)
1494
+ end
1495
+
1496
+ def test_bitwise_shifts
1497
+ assert_sql(/SELECT \("users"."flags" << 2\)/, User.select { :flags << 2 }.to_sql)
1498
+ assert_sql(/SELECT \("users"."flags" >> 1\)/, User.select { :flags >> 1 }.to_sql)
1499
+ end
1500
+
1501
+ def test_bitwise_not
1502
+ assert_sql(/SELECT \( ~ "users"."flags"\)/, User.select { ~:flags }.to_sql)
1503
+ end
1504
+
1505
+ # The one operator the three do not share: PostgreSQL's # is where a comment
1506
+ # starts on MySQL, MySQL's ^ is exponentiation to PostgreSQL, and SQLite has
1507
+ # neither, so it gets the two operations XOR is made of.
1508
+ def test_bitwise_xor_is_spelled_per_adapter
1509
+ sql = User.select { :flags ^ 10 }.to_sql
1510
+ case ADAPTER
1511
+ when 'postgresql' then assert_sql(/SELECT \("users"."flags" # 10\)/, sql)
1512
+ when 'mysql2' then assert_sql(/SELECT \("users"."flags" \^ 10\)/, sql)
1513
+ else assert_sql(
1514
+ /SELECT \(\("users"."flags" \| 10\) - \("users"."flags" & 10\)\)/, sql)
1515
+ end
1516
+ end
1517
+
1518
+ # Whatever the spelling, the answers agree.
1519
+ def test_bitwise_execution
1520
+ User.delete_all
1521
+ User.create!(name: 'a', flags: 12)
1522
+ assert_equal(8, User.select { (:flags & 10).as(:v) }.take.v.to_i)
1523
+ assert_equal(14, User.select { (:flags | 10).as(:v) }.take.v.to_i)
1524
+ assert_equal(6, User.select { (:flags ^ 10).as(:v) }.take.v.to_i)
1525
+ assert_equal(48, User.select { (:flags << 2).as(:v) }.take.v.to_i)
1526
+ assert_equal(6, User.select { (:flags >> 1).as(:v) }.take.v.to_i)
1527
+ # MariaDB reads ~ back as the unsigned 64-bit number where the others give
1528
+ # a negative one, so the assertion is on the bits rather than the value.
1529
+ assert_equal(243, User.select { (~:flags & 255).as(:v) }.take.v.to_i)
1530
+ end
1531
+
1532
+ # AND and OR are the conditions' own & and |, and an operand that is a
1533
+ # condition means one of the two was meant.
1534
+ def test_bitwise_refuses_a_condition
1535
+ e = assert_raises(ArgumentError) { User.where { :flags & (:age == 1) } }
1536
+ assert_match(/AND and OR/, e.message)
1537
+ end
1538
+
1539
+ # MySQL and SQLite would take a boolean for the bit it is stored as and
1540
+ # quietly answer as AND would; PostgreSQL has no such operator.
1541
+ def test_bitwise_refuses_a_boolean_column
1542
+ e = assert_raises(ArgumentError) { User.where { (:active & :active) > 0 }.to_sql }
1543
+ assert_match(/true\?/, e.message)
1544
+ assert_raises(ArgumentError) { User.select { ~:active }.to_sql }
1545
+ end
1546
+
1547
+ def test_conditions_still_and_with_the_same_operators
1548
+ assert_sql(/WHERE "users"."age" = 1 AND "users"."name" = 'a'/,
1549
+ User.where { (:age == 1) & (:name == 'a') }.to_sql)
1550
+ end
1551
+
1552
+ def test_bit_aggregates
1553
+ skip_without_bit_aggregates
1554
+ User.delete_all
1555
+ User.create!([{name: 'a', flags: 12}, {name: 'b', flags: 10}, {name: 'c', flags: 3}])
1556
+ assert_equal(0, User.select { bit_and(:flags).as(:v) }.take.v.to_i)
1557
+ assert_equal(15, User.select { bit_or(:flags).as(:v) }.take.v.to_i)
1558
+ assert_equal(5, User.select { bit_xor(:flags).as(:v) }.take.v.to_i)
1559
+ end
1560
+
1561
+ # PostgreSQL counts the bits of a bit string rather than of a number, so the
1562
+ # argument is cast there; bit(64) is what makes a negative answer alike.
1563
+ def test_bit_count
1564
+ if ADAPTER == 'sqlite3'
1565
+ assert_raises(NotImplementedError) { User.select { bit_count(:flags) } }
1566
+ return
1567
+ end
1568
+ User.delete_all
1569
+ User.create!(name: 'a', flags: 12)
1570
+ User.create!(name: 'b', flags: -1)
1571
+ assert_equal([2, 64], User.select { bit_count(:flags).as(:v) }.order(:name).map {|u| u.v.to_i })
1572
+ assert_sql(/BIT_COUNT\(CAST\("users"."flags" AS bit\(64\)\)\)/,
1573
+ User.select { bit_count(:flags) }.to_sql) if ADAPTER == 'postgresql'
1574
+ end
1575
+
1576
+ def test_bit_aggregates_are_unsupported_on_sqlite
1577
+ if ADAPTER == 'sqlite3'
1578
+ e = assert_raises(NotImplementedError) { User.select { bit_or(:flags) } }
1579
+ assert_match(/bit_or/, e.message)
1580
+ else
1581
+ assert_sql(/BIT_OR\("users"."flags"\)/, User.select { bit_or(:flags) }.to_sql)
1582
+ end
1583
+ end
1584
+
1131
1585
  def test_coalesce_function_with_literal
1132
1586
  assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
1133
1587
  User.select { coalesce(:name, 'unknown') }.to_sql)
@@ -1150,7 +1604,7 @@ class TestBlockSyntax < Minitest::Test
1150
1604
 
1151
1605
  def test_aggregate_in
1152
1606
  assert_sql(/HAVING SUM\("users"."age"\) BETWEEN 1 AND 10/,
1153
- User.group(:name).having { :age.sum.in?(1..10) }.to_sql)
1607
+ User.group(:name).having { sum(:age).in?(1..10) }.to_sql)
1154
1608
  end
1155
1609
 
1156
1610
  def test_nested_function
@@ -1179,22 +1633,22 @@ class TestBlockSyntax < Minitest::Test
1179
1633
  end
1180
1634
 
1181
1635
  def test_select_function_with_alias
1182
- assert_sql(/SELECT UPPER\("users"."name"\) AS upper_name, "users"."age"/,
1636
+ assert_sql(/SELECT UPPER\("users"."name"\) AS "upper_name", "users"."age"/,
1183
1637
  User.select { [upper(:name).as(:upper_name), :age] }.to_sql)
1184
1638
  end
1185
1639
 
1186
1640
  def test_select_column_alias
1187
- assert_sql(/SELECT "users"."name" AS n/,
1641
+ assert_sql(/SELECT "users"."name" AS "n"/,
1188
1642
  User.select { :name.as(:n) }.to_sql)
1189
1643
  end
1190
1644
 
1191
1645
  def test_select_qualified_column_alias
1192
- assert_sql(/SELECT "users"."name" AS n/,
1646
+ assert_sql(/SELECT "users"."name" AS "n"/,
1193
1647
  User.select { :users[:name].as(:n) }.to_sql)
1194
1648
  end
1195
1649
 
1196
1650
  def test_select_aggregate_alias
1197
- assert_sql(/SELECT COUNT\("users"."id"\) AS cnt/,
1651
+ assert_sql(/SELECT COUNT\("users"."id"\) AS "cnt"/,
1198
1652
  User.select { count(:id).as(:cnt) }.to_sql)
1199
1653
  end
1200
1654
 
@@ -1268,13 +1722,442 @@ class TestBlockSyntax < Minitest::Test
1268
1722
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
1269
1723
  end
1270
1724
 
1725
+ # update_all's hash reads a symbol as the value it is; the block reads it as
1726
+ # the column it names, which is what lets the new value be built from the old.
1727
+ def test_update_all_from_the_column
1728
+ Tally.delete_all
1729
+ Tally.create!(page: '/a', hits: 1)
1730
+ Tally.create!(page: '/b', hits: 2)
1731
+ Tally.update_all { { hits: :hits + 1 } }
1732
+ assert_equal([2, 3], Tally.order(:page).pluck(:hits))
1733
+ end
1734
+
1735
+ def test_update_all_takes_any_expression
1736
+ Tally.delete_all
1737
+ Tally.create!(page: '/a', hits: 5)
1738
+ Tally.update_all { { hits: case_when { :hits > 4 }.then(0).else(:hits), page: upper(:page) } }
1739
+ assert_equal([['/A', 0]], Tally.pluck(:page, :hits))
1740
+ end
1741
+
1742
+ def test_update_all_within_a_scope
1743
+ Tally.delete_all
1744
+ Tally.create!(page: '/a', hits: 1)
1745
+ Tally.create!(page: '/b', hits: 1)
1746
+ Tally.where { :page == '/a' }.update_all { { hits: 9 } }
1747
+ assert_equal([9, 1], Tally.order(:page).pluck(:hits))
1748
+ end
1749
+
1750
+ def test_update_all_without_a_block_is_unchanged
1751
+ Tally.delete_all
1752
+ Tally.create!(page: '/a', hits: 1)
1753
+ Tally.update_all(hits: 4)
1754
+ assert_equal([4], Tally.pluck(:hits))
1755
+ end
1756
+
1757
+ def test_update_all_takes_updates_or_a_block
1758
+ assert_raises(ArgumentError) { Tally.update_all({hits: 1}) { { hits: 2 } } }
1759
+ e = assert_raises(ArgumentError) { Tally.update_all { :hits + 1 } }
1760
+ assert_match(/hash of column/, e.message)
1761
+ end
1762
+
1763
+ # on_duplicate takes SQL text and nothing else, so the block is compiled to
1764
+ # some. `excluded` is the row that could not be inserted.
1765
+ def test_upsert_all_adds_to_what_is_there
1766
+ Tally.delete_all
1767
+ Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1768
+ Tally.upsert_all([{page: '/a', hits: 10}], **upsert_target) {
1769
+ { hits: :hits + excluded(:hits) }
1770
+ }
1771
+ assert_equal([11], Tally.pluck(:hits))
1772
+ end
1773
+
1774
+ def test_upsert_all_inserts_when_there_is_no_conflict
1775
+ Tally.delete_all
1776
+ Tally.upsert_all([{page: '/new', hits: 3}], **upsert_target) {
1777
+ { hits: :hits + excluded(:hits) }
1778
+ }
1779
+ assert_equal([3], Tally.pluck(:hits))
1780
+ end
1781
+
1782
+ def test_upsert_all_takes_any_expression
1783
+ Tally.delete_all
1784
+ Tally.upsert_all([{page: '/a', hits: 7}], **upsert_target)
1785
+ Tally.upsert_all([{page: '/a', hits: 2}], **upsert_target) {
1786
+ { hits: greatest(:hits, excluded(:hits)) }
1787
+ }
1788
+ assert_equal([7], Tally.pluck(:hits))
1789
+ end
1790
+
1791
+ def test_upsert_all_without_a_block_is_unchanged
1792
+ Tally.delete_all
1793
+ Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target)
1794
+ Tally.upsert_all([{page: '/a', hits: 6}], **upsert_target)
1795
+ assert_equal([6], Tally.pluck(:hits))
1796
+ end
1797
+
1798
+ def test_upsert_all_takes_on_duplicate_or_a_block
1799
+ assert_raises(ArgumentError) do
1800
+ Tally.upsert_all([{page: '/a', hits: 1}],
1801
+ on_duplicate: Arel.sql('hits = 1'), **upsert_target) { { hits: 2 } }
1802
+ end
1803
+ e = assert_raises(ArgumentError) do
1804
+ Tally.upsert_all([{page: '/a', hits: 1}], **upsert_target) { {} }
1805
+ end
1806
+ assert_match(/at least one column/, e.message)
1807
+ end
1808
+
1809
+ # Reading inside a JSON document, by the name of what Hash does. No two
1810
+ # adapters spell it alike, so what the tests assert is the value that comes
1811
+ # back rather than the SQL.
1812
+ def seed_docs
1813
+ Doc.delete_all
1814
+ Doc.create!(name: 'one',
1815
+ meta: json_document({ 'a' => { 'b' => 'deep' }, 'n' => 5,
1816
+ 'tags' => %w[x y], 'odd key' => 1 }))
1817
+ Doc.create!(name: 'two', meta: json_document({ 'n' => 9 }))
1818
+ end
1819
+
1820
+ def test_dig_a_key
1821
+ seed_docs
1822
+ assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig(:n).as(:v) }.map(&:v))
1823
+ end
1824
+
1825
+ def test_dig_a_path
1826
+ seed_docs
1827
+ assert_equal(['deep', nil],
1828
+ Doc.order(:name).select { :meta.dig(:a, :b).as(:v) }.map(&:v))
1829
+ end
1830
+
1831
+ def test_dig_an_array_index
1832
+ seed_docs
1833
+ assert_equal(['x', nil],
1834
+ Doc.order(:name).select { :meta.dig(:tags, 0).as(:v) }.map(&:v))
1835
+ end
1836
+
1837
+ # A key that is not a plain name travels as itself rather than being refused.
1838
+ def test_dig_a_key_that_needs_quoting
1839
+ seed_docs
1840
+ assert_equal(['1', nil],
1841
+ Doc.order(:name).select { :meta.dig(:'odd key').as(:v) }.map(&:v))
1842
+ end
1843
+
1844
+ # dig gives text on every adapter -- SQLite's ->> would otherwise give the
1845
+ # value with its type -- so a number is compared through a cast.
1846
+ def test_dig_is_text_everywhere
1847
+ seed_docs
1848
+ assert_equal(['one'], Doc.where { :meta.dig(:n) == '5' }.pluck(:name))
1849
+ type = integer_type
1850
+ assert_equal(['two'], Doc.where { cast(:meta.dig(:n), type) > 6 }.pluck(:name))
1851
+ end
1852
+
1853
+ def test_dig_json_keeps_the_json
1854
+ seed_docs
1855
+ value = Doc.where { :name == 'one' }.select { :meta.dig_json(:tags).as(:v) }.first.v
1856
+ assert_equal(%w[x y], value.is_a?(String) ? JSON.parse(value) : value)
1857
+ end
1858
+
1859
+ def test_dig_from_a_qualified_column
1860
+ seed_docs
1861
+ assert_equal(['one'], Doc.where { :docs[:meta].dig(:a, :b) == 'deep' }.pluck(:name))
1862
+ end
1863
+
1864
+ def test_key
1865
+ seed_docs
1866
+ assert_equal(['one'], Doc.where { :meta.key?(:tags) }.pluck(:name))
1867
+ assert_equal(%w[one two], Doc.where { :meta.key?(:n) }.order(:name).pluck(:name))
1868
+ end
1869
+
1870
+ def test_contains
1871
+ skip_without_json_containment
1872
+ seed_docs
1873
+ assert_equal(['one'], Doc.where { :meta.contains?(n: 5) }.pluck(:name))
1874
+ assert_equal([], Doc.where { :meta.contains?(n: 1) }.pluck(:name))
1875
+ end
1876
+
1877
+ def test_contains_says_where_it_cannot_go
1878
+ skip "#{ADAPTER} has JSON containment" unless ADAPTER == 'sqlite3'
1879
+ assert_raises(NotImplementedError) { Doc.where { :meta.contains?(n: 5) }.to_sql }
1880
+ end
1881
+
1882
+ def test_dig_needs_a_path
1883
+ assert_raises(ArgumentError) { Doc.select { :meta.dig } }
1884
+ e = assert_raises(ArgumentError) { Doc.select { :meta.dig(1.5) } }
1885
+ assert_match(/key or an array index/, e.message)
1886
+ end
1887
+
1888
+ # FILTER takes the aggregate over the rows a condition holds for. MySQL has
1889
+ # no such clause, so what is asserted across adapters is the number that
1890
+ # comes back rather than the SQL.
1891
+ def seed_for_filter
1892
+ User.delete_all
1893
+ User.create!(name: 'a', age: 10)
1894
+ User.create!(name: 'a', age: 20)
1895
+ User.create!(name: 'b', age: 100)
1896
+ end
1897
+
1898
+ def aggregate(&block)
1899
+ User.select(&block).to_a.first.v
1900
+ end
1901
+
1902
+ def test_filter_a_count
1903
+ seed_for_filter
1904
+ assert_equal(2, aggregate { count(:*).filter { :age < 50 }.as(:v) }.to_i)
1905
+ end
1906
+
1907
+ def test_filter_takes_a_value_as_well_as_a_block
1908
+ seed_for_filter
1909
+ assert_equal(2, aggregate { count(:*).filter(:age < 50).as(:v) }.to_i)
1910
+ end
1911
+
1912
+ def test_filter_a_sum_and_an_average
1913
+ seed_for_filter
1914
+ assert_equal(30, aggregate { sum(:age).filter { :age < 50 }.as(:v) }.to_i)
1915
+ assert_equal(15, aggregate { avg(:age).filter { :age < 50 }.as(:v) }.to_i)
1916
+ end
1917
+
1918
+ def test_filter_a_distinct_count
1919
+ seed_for_filter
1920
+ assert_equal(1, aggregate { count(:name, distinct: true).filter { :age < 50 }.as(:v) }.to_i)
1921
+ end
1922
+
1923
+ def test_filter_is_a_clause_where_there_is_one
1924
+ skip "#{ADAPTER} has no FILTER" if ADAPTER == 'mysql2'
1925
+ assert_sql(/COUNT\(\*\) FILTER \(WHERE "users"."age" < 50\)/,
1926
+ User.select { count(:*).filter { :age < 50 } }.to_sql)
1927
+ end
1928
+
1929
+ # Where there is not, the same rows are reached through a case: an aggregate
1930
+ # passes over a NULL, so a row the condition misses is a row it does not see.
1931
+ def test_filter_becomes_a_case_where_there_is_no_clause
1932
+ skip "#{ADAPTER} has FILTER" unless ADAPTER == 'mysql2'
1933
+ assert_sql(/COUNT\(CASE WHEN "users"."age" < 50 THEN 1 END\)/,
1934
+ User.select { count(:*).filter { :age < 50 } }.to_sql)
1935
+ assert_sql(/SUM\(CASE WHEN "users"."age" < 50 THEN "users"."age" END\)/,
1936
+ User.select { sum(:age).filter { :age < 50 } }.to_sql)
1937
+ end
1938
+
1939
+ def test_filter_needs_a_value_or_a_block
1940
+ assert_raises(ArgumentError) { User.select { count(:*).filter } }
1941
+ e = assert_raises(ArgumentError) { User.select { count(:*).filter(1) { 2 } } }
1942
+ assert_match(/not both/, e.message)
1943
+ end
1944
+
1945
+ # DISTINCT ON keeps the first row of each group the order brings up.
1946
+ def seed_for_distinct_on
1947
+ Author.delete_all
1948
+ Author.create!(name: 'a')
1949
+ Author.create!(name: 'a')
1950
+ Author.create!(name: 'b')
1951
+ end
1952
+
1953
+ def test_distinct_on
1954
+ skip_without_distinct_on
1955
+ seed_for_distinct_on
1956
+ assert_sql(/SELECT DISTINCT ON \( "authors"."name" \)/,
1957
+ Author.distinct_on { :name }.to_sql)
1958
+ assert_equal(%w[a b], Author.distinct_on { :name }.order { :name }.pluck(:name))
1959
+ end
1960
+
1961
+ def test_distinct_on_takes_columns_as_well_as_a_block
1962
+ skip_without_distinct_on
1963
+ assert_equal(Author.distinct_on { :name }.to_sql, Author.distinct_on(:name).to_sql)
1964
+ end
1965
+
1966
+ def test_distinct_on_takes_several
1967
+ skip_without_distinct_on
1968
+ assert_sql(/DISTINCT ON \( "authors"."id", "authors"."name" \)/,
1969
+ Author.distinct_on { [:id, :name] }.to_sql)
1970
+ end
1971
+
1972
+ def test_distinct_on_takes_an_expression
1973
+ skip_without_distinct_on
1974
+ assert_sql(/DISTINCT ON \( UPPER\("authors"."name"\) \)/,
1975
+ Author.distinct_on { upper(:name) }.to_sql)
1976
+ end
1977
+
1978
+ # Arel carries the node and refuses to write it elsewhere, as it does a
1979
+ # regexp, so the gem has nothing of its own to say.
1980
+ def test_distinct_on_says_where_it_cannot_go
1981
+ skip "#{ADAPTER} has DISTINCT ON" if ADAPTER == 'postgresql'
1982
+ assert_raises(NotImplementedError) { Author.distinct_on { :name }.to_sql }
1983
+ end
1984
+
1985
+ def test_distinct_on_needs_a_column
1986
+ assert_raises(ArgumentError) { Author.distinct_on }
1987
+ end
1988
+
1989
+ def test_distinct_on_spawns
1990
+ refute_match(/DISTINCT ON/, Author.all.to_sql)
1991
+ Author.distinct_on { :name }
1992
+ refute_match(/DISTINCT ON/, Author.all.to_sql)
1993
+ end
1994
+
1995
+ # A lateral join lets the relation joined see the row being joined to, which
1996
+ # is what makes the top row of each group reachable in one query.
1997
+ def top_post
1998
+ Post.select { :title }.
1999
+ where { :posts[:author_id] == :authors[:id] }.
2000
+ order { :title.desc }.limit(1)
2001
+ end
2002
+
2003
+ def seed_for_lateral
2004
+ Author.delete_all
2005
+ Post.delete_all
2006
+ author = Author.create!(name: 'writes')
2007
+ Author.create!(name: 'does not')
2008
+ Post.create!(author_id: author.id, title: 'a')
2009
+ Post.create!(author_id: author.id, title: 'b')
2010
+ end
2011
+
2012
+ def test_lateral_join
2013
+ skip_without_lateral
2014
+ seed_for_lateral
2015
+ rows = Author.joins(top_post, as: :top, lateral: true).
2016
+ select { [:name, :top[:title].as(:v)] }.map {|r| [r.name, r.v] }
2017
+ assert_equal([['writes', 'b']], rows)
2018
+ end
2019
+
2020
+ # Left, so that a row with nothing to join to is kept.
2021
+ def test_left_outer_lateral_join
2022
+ skip_without_lateral
2023
+ seed_for_lateral
2024
+ rows = Author.left_outer_joins(top_post, as: :top, lateral: true).
2025
+ select { [:name, :top[:title].as(:v)] }.order { :name }.map {|r| [r.name, r.v] }
2026
+ assert_equal([['does not', nil], ['writes', 'b']], rows)
2027
+ end
2028
+
2029
+ # Without a block the join is ON TRUE; what the subquery may see is said
2030
+ # inside it.
2031
+ def test_lateral_join_takes_an_on_clause
2032
+ skip_without_lateral
2033
+ seed_for_lateral
2034
+ assert_equal(0, Author.joins(top_post, as: :top, lateral: true) {
2035
+ :top[:title] == 'nothing'
2036
+ }.count)
2037
+ assert_sql(/ON TRUE/, Author.joins(top_post, as: :top, lateral: true).to_sql)
2038
+ end
2039
+
2040
+ def test_lateral_join_needs_a_relation_and_a_name
2041
+ e = assert_raises(ArgumentError) { Author.joins(:posts, as: :top, lateral: true) }
2042
+ assert_match(/takes a relation/, e.message)
2043
+ e = assert_raises(ArgumentError) { Author.joins(top_post, lateral: true) }
2044
+ assert_match(/needs a name/, e.message)
2045
+ end
2046
+
2047
+ def test_lateral_join_says_where_it_cannot_go
2048
+ skip 'this one has LATERAL' if ADAPTER == 'postgresql' || (ADAPTER == 'mysql2' && !mariadb?)
2049
+ e = assert_raises(NotImplementedError) { Author.joins(top_post, as: :top, lateral: true) }
2050
+ assert_match(/lateral join has no equivalent/, e.message)
2051
+ end
2052
+
2053
+ # Several groupings asked for at once, the totals of each coming back beside
2054
+ # the rows. What is asserted is the rows, since the point is which totals
2055
+ # arrive rather than how the clause is spelled.
2056
+ def seed_for_grouping
2057
+ Post.delete_all
2058
+ Author.delete_all
2059
+ a = Author.create!(name: 'a')
2060
+ b = Author.create!(name: 'b')
2061
+ Post.create!(author_id: a.id, title: 'x')
2062
+ Post.create!(author_id: a.id, title: 'y')
2063
+ Post.create!(author_id: b.id, title: 'x')
2064
+ end
2065
+
2066
+ def grouped(&block)
2067
+ Post.group(&block).select { [:author_id, :title, count(:*).as(:n)] }.
2068
+ map {|r| [r.author_id, r.title, r.n.to_i] }.sort_by(&:to_s)
2069
+ end
2070
+
2071
+ def test_grouping_sets
2072
+ skip_without_grouping_sets
2073
+ seed_for_grouping
2074
+ rows = grouped { grouping_sets([:author_id], [:title], []) }
2075
+ assert_equal(3, rows.count {|_, title, _| title.nil? }) # by author
2076
+ assert_includes(rows, [nil, 'x', 2]) # by title
2077
+ assert_includes(rows, [nil, nil, 3]) # the whole
2078
+ end
2079
+
2080
+ def test_rollup
2081
+ skip_without_grouping_sets
2082
+ seed_for_grouping
2083
+ rows = grouped { rollup(:author_id, :title) }
2084
+ assert_includes(rows, [nil, nil, 3])
2085
+ assert_sql(/GROUP BY ROLLUP\( "posts"."author_id", "posts"."title" \)/,
2086
+ Post.group { rollup(:author_id, :title) }.to_sql)
2087
+ end
2088
+
2089
+ def test_cube
2090
+ skip_without_grouping_sets
2091
+ seed_for_grouping
2092
+ assert_sql(/GROUP BY CUBE\( "posts"."author_id", "posts"."title" \)/,
2093
+ Post.group { cube(:author_id, :title) }.to_sql)
2094
+ # Every combination: by both, by each, and the whole.
2095
+ assert_equal(8, Post.group { cube(:author_id, :title) }.select { count(:*).as(:n) }.to_a.size)
2096
+ end
2097
+
2098
+ def test_grouping_sets_say_where_they_cannot_go
2099
+ skip 'PostgreSQL has them' if ADAPTER == 'postgresql'
2100
+ assert_raises(NotImplementedError) { Post.group { grouping_sets([:title]) } }
2101
+ assert_raises(NotImplementedError) { Post.group { rollup(:title) } }
2102
+ assert_raises(NotImplementedError) { Post.group { cube(:title) } }
2103
+ end
2104
+
2105
+ def test_grouping_sets_need_something_to_group_by
2106
+ assert_raises(ArgumentError) { Post.group { rollup } }
2107
+ assert_raises(ArgumentError) { Post.group { grouping_sets } }
2108
+ end
2109
+
2110
+ # bury sets what dig reads. The document comes back changed rather than
2111
+ # being written anywhere, so update_all is what makes it stick.
2112
+ def buried(&block)
2113
+ seed_docs
2114
+ Doc.where { :name == 'one' }.update_all(&block)
2115
+ value = Doc.find_by(name: 'one').meta
2116
+ value.is_a?(String) ? JSON.parse(value) : value
2117
+ end
2118
+
2119
+ def test_bury_a_nested_key
2120
+ assert_equal('new', buried { { meta: :meta.bury(:a, :b, 'new') } }.dig('a', 'b'))
2121
+ end
2122
+
2123
+ def test_bury_a_key_that_is_not_there_yet
2124
+ assert_equal(9, buried { { meta: :meta.bury(:fresh, 9) } }['fresh'])
2125
+ end
2126
+
2127
+ # A whole document, which each adapter takes its own way round.
2128
+ def test_bury_an_object_and_an_array
2129
+ assert_equal({ 'x' => 1 }, buried { { meta: :meta.bury(:obj, { 'x' => 1 }) } }['obj'])
2130
+ assert_equal([1, 2], buried { { meta: :meta.bury(:arr, [1, 2]) } }['arr'])
2131
+ end
2132
+
2133
+ def test_bury_an_array_index
2134
+ assert_equal(%w[7 y], buried { { meta: :meta.bury(:tags, 0, '7') } }['tags'])
2135
+ end
2136
+
2137
+ # The value can be read out of the document it is going into.
2138
+ def test_bury_an_expression
2139
+ assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig(:n)) } }['copy'].to_s)
2140
+ end
2141
+
2142
+ # It is an expression, so it does not have to be written anywhere.
2143
+ def test_bury_in_a_select
2144
+ seed_docs
2145
+ value = Doc.where { :name == 'one' }.select { :meta.bury(:a, :b, 'x').as(:v) }.first.v
2146
+ assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value).dig('a', 'b'))
2147
+ end
2148
+
2149
+ def test_bury_needs_a_path
2150
+ assert_raises(ArgumentError) { Doc.select { :meta.bury('v') } }
2151
+ assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5, 'v') } }
2152
+ end
2153
+
1271
2154
  def test_default_where_syntax
1272
2155
  assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
1273
2156
  User.where(name: 'Ruby', age: 19).to_sql)
1274
2157
  end
1275
2158
 
1276
2159
  def test_value_in_a_select_list
1277
- assert_sql(/SELECT "users"."name", 0 AS depth/,
2160
+ assert_sql(/SELECT "users"."name", 0 AS "depth"/,
1278
2161
  User.select { [:name, value(0).as(:depth)] }.to_sql)
1279
2162
  end
1280
2163
 
@@ -1284,7 +2167,7 @@ class TestBlockSyntax < Minitest::Test
1284
2167
  User.delete_all
1285
2168
  User.create!(name: 'alice')
1286
2169
  payload = "it's a value"
1287
- assert_sql(/SELECT 'draft' AS state/,
2170
+ assert_sql(/SELECT 'draft' AS "state"/,
1288
2171
  User.select { value('draft').as(:state) }.to_sql)
1289
2172
  assert_equal([payload],
1290
2173
  User.select { value(payload).as(:note) }.map(&:note))
@@ -1301,7 +2184,7 @@ class TestBlockSyntax < Minitest::Test
1301
2184
  end
1302
2185
 
1303
2186
  def test_value_takes_the_arithmetics
1304
- assert_sql(/SELECT \(1 \+ "users"."age"\) AS next_year/,
2187
+ assert_sql(/SELECT \(1 \+ "users"."age"\) AS "next_year"/,
1305
2188
  User.select { (value(1) + :age).as(:next_year) }.to_sql)
1306
2189
  end
1307
2190
 
@@ -1311,12 +2194,12 @@ class TestBlockSyntax < Minitest::Test
1311
2194
  end
1312
2195
 
1313
2196
  def test_integer_shorthand_for_value
1314
- assert_sql(/SELECT "users"."name", 0 AS depth/,
2197
+ assert_sql(/SELECT "users"."name", 0 AS "depth"/,
1315
2198
  User.select { [:name, 0.as(:depth)] }.to_sql)
1316
2199
  end
1317
2200
 
1318
2201
  def test_float_shorthand_for_value
1319
- assert_sql(/SELECT 1\.5 AS rate/, User.select { 1.5.as(:rate) }.to_sql)
2202
+ assert_sql(/SELECT 1\.5 AS "rate"/, User.select { 1.5.as(:rate) }.to_sql)
1320
2203
  end
1321
2204
 
1322
2205
  def test_numeric_shorthand_has_no_orderings
@@ -1324,9 +2207,15 @@ class TestBlockSyntax < Minitest::Test
1324
2207
  assert_raises(NoMethodError) { User.order { 1.desc } }
1325
2208
  end
1326
2209
 
1327
- def test_value_alias_rejects_an_injected_name
1328
- assert_raises(ArgumentError) { User.select { value(0).as(INJECTION.to_sym) } }
1329
- assert_raises(ArgumentError) { User.select { 0.as(INJECTION.to_sym) } }
2210
+ # The alias on a literal is quoted like any other, so a name that is not a
2211
+ # plain one arrives as itself rather than as SQL.
2212
+ def test_a_value_alias_is_quoted_rather_than_refused
2213
+ User.delete_all
2214
+ User.create!(name: 'alice')
2215
+ payload = 'a" FROM users; --'
2216
+ assert_equal(0, User.select { value(0).as(payload.to_sym) }.first[payload].to_i)
2217
+ assert_equal(0, User.select { 0.as(payload.to_sym) }.first[payload].to_i)
2218
+ assert_equal(1, User.count)
1330
2219
  end
1331
2220
 
1332
2221
  def test_a_value_selected_reaches_the_row
@@ -1339,4 +2228,31 @@ class TestBlockSyntax < Minitest::Test
1339
2228
  def test_numeric_shorthand_is_confined_to_the_block
1340
2229
  assert_raises(NoMethodError) { 0.as(:depth) }
1341
2230
  end
2231
+
2232
+ # pglite is PostgreSQL compiled to WebAssembly, which the sandbox runs in the
2233
+ # browser. Its adapter answers to a name of its own, so without this the
2234
+ # spellings would fall back to the standard ones and the browser would be
2235
+ # told PostgreSQL's JSON operators do not exist.
2236
+ def test_adapter_families
2237
+ model = Class.new do
2238
+ def self.with_adapter(name)
2239
+ config = Struct.new(:adapter).new(name)
2240
+ Class.new { define_singleton_method(:connection_db_config) { config } }
2241
+ end
2242
+ end
2243
+
2244
+ {
2245
+ 'sqlite3' => :sqlite,
2246
+ 'postgresql' => :postgresql,
2247
+ 'postgis' => :postgresql,
2248
+ 'pglite' => :postgresql,
2249
+ 'mysql2' => :mysql,
2250
+ 'trilogy' => :mysql,
2251
+ 'nothing_of_the_sort' => :unknown,
2252
+ }.each do |adapter, family|
2253
+ assert_equal(family,
2254
+ ActiveRecord::Refined::AST.adapter_family(model.with_adapter(adapter)),
2255
+ adapter)
2256
+ end
2257
+ end
1342
2258
  end