activerecord-refined 0.5.0 → 0.6.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.
@@ -91,11 +91,22 @@ class TestBlockSyntax < Minitest::Test
91
91
  assert_equal(['Alice'], User.where { :name.casecmp?('aLiCe') }.pluck(:name))
92
92
  end
93
93
 
94
- def test_not_like
94
+ def test_bang_negates_like
95
95
  assert_sql(/WHERE NOT \("users"."name" LIKE 'tender%'\)/,
96
96
  User.where { !:name.like?('tender%') }.to_sql)
97
97
  end
98
98
 
99
+ def test_not_like
100
+ assert_sql(/WHERE "users"."name" NOT LIKE 'tender%'/,
101
+ User.where { :name.not_like?('tender%') }.to_sql)
102
+ end
103
+
104
+ def test_not_ilike
105
+ expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
106
+ assert_sql(/WHERE "users"."name" NOT #{expected} 'tender%'/,
107
+ User.where { :name.not_ilike?('tender%') }.to_sql)
108
+ end
109
+
99
110
  def test_start_with
100
111
  assert_sql(/WHERE "users"."name" LIKE 'tender%' ESCAPE '\\'/,
101
112
  User.where { :name.start_with?('tender') }.to_sql)
@@ -298,11 +309,23 @@ class TestBlockSyntax < Minitest::Test
298
309
  User.where { :age.in?(18...65) }.to_sql)
299
310
  end
300
311
 
301
- def test_not_between
312
+ def test_bang_negates_between
302
313
  assert_sql(/WHERE NOT \("users"."age" BETWEEN 18 AND 65\)/,
303
314
  User.where { !:age.between?(18, 65) }.to_sql)
304
315
  end
305
316
 
317
+ # Arel spells the negation as the two comparisons rather than NOT BETWEEN,
318
+ # which is the same set of rows, NULLs included.
319
+ def test_not_between
320
+ assert_sql(/WHERE \("users"."age" < 18 OR "users"."age" > 65\)/,
321
+ User.where { :age.not_between?(18, 65) }.to_sql)
322
+ end
323
+
324
+ def test_not_in_range
325
+ assert_sql(/WHERE \("users"."age" < 18 OR "users"."age" > 65\)/,
326
+ User.where { :age.not_in?(18..65) }.to_sql)
327
+ end
328
+
306
329
  def test_is_null
307
330
  assert_sql(/WHERE "users"."name" IS NULL/,
308
331
  User.where { :name.null? }.to_sql)
@@ -313,6 +336,237 @@ class TestBlockSyntax < Minitest::Test
313
336
  User.where { :users[:name].null? }.to_sql)
314
337
  end
315
338
 
339
+ def test_is_not_null
340
+ assert_sql(/WHERE "users"."name" IS NOT NULL/,
341
+ User.where { :name.not_null? }.to_sql)
342
+ end
343
+
344
+ def test_is_not_null_qualified
345
+ assert_sql(/WHERE "users"."name" IS NOT NULL/,
346
+ User.where { :users[:name].not_null? }.to_sql)
347
+ end
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
+
548
+ # The claim these methods rest on: the direct spelling is the same rows as
549
+ # negating the positive one, which is where a NULL would show a difference
550
+ # if there were one.
551
+ def test_the_negations_match_what_bang_selects
552
+ User.delete_all
553
+ User.create!(name: 'alice', age: 60, active: true)
554
+ User.create!(name: 'bob', age: 20, active: false)
555
+ User.create!(name: nil, age: 40)
556
+ [
557
+ [-> { :name.not_null? }, -> { !:name.null? }],
558
+ [-> { :age.not_in?([20, 30]) }, -> { !:age.in?([20, 30]) }],
559
+ [-> { :age.not_between?(20, 30) }, -> { !:age.between?(20, 30) }],
560
+ [-> { :name.not_like?('a%') }, -> { !:name.like?('a%') }],
561
+ [-> { :active.not_true? }, -> { !:active.true? }],
562
+ [-> { :active.not_false? }, -> { !:active.false? }],
563
+ ].each do |direct, negated|
564
+ assert_equal(User.where(&negated).pluck(:id).sort,
565
+ User.where(&direct).pluck(:id).sort,
566
+ "#{direct.source_location} did not match the ! form")
567
+ end
568
+ end
569
+
316
570
  def test_equal_nil_is_rejected
317
571
  e = assert_raises(ArgumentError) { User.where { :name == nil } }
318
572
  assert_match(/null\?/, e.message)
@@ -323,6 +577,44 @@ class TestBlockSyntax < Minitest::Test
323
577
  assert_match(/null\?/, e.message)
324
578
  end
325
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
+
326
618
  def test_in
327
619
  assert_sql(/WHERE "users"."age" IN \(1, 2, 3\)/,
328
620
  User.where { :age.in?([1, 2, 3]) }.to_sql)
@@ -333,11 +625,21 @@ class TestBlockSyntax < Minitest::Test
333
625
  User.where { :users[:age].in?([1, 2, 3]) }.to_sql)
334
626
  end
335
627
 
336
- def test_not_in
628
+ def test_bang_negates_in
337
629
  assert_sql(/WHERE NOT \("users"."age" IN \(1, 2, 3\)\)/,
338
630
  User.where { !:age.in?([1, 2, 3]) }.to_sql)
339
631
  end
340
632
 
633
+ def test_not_in
634
+ assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
635
+ User.where { :age.not_in?([1, 2, 3]) }.to_sql)
636
+ end
637
+
638
+ def test_not_in_qualified
639
+ assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
640
+ User.where { :users[:age].not_in?([1, 2, 3]) }.to_sql)
641
+ end
642
+
341
643
  # Spelled IS [NOT] DISTINCT FROM on PostgreSQL, IS / IS NOT on SQLite and
342
644
  # <=> on MySQL, so only the resulting rows are portable.
343
645
  def test_not_distinct_from_execution
@@ -406,6 +708,66 @@ class TestBlockSyntax < Minitest::Test
406
708
  Author.where { !:id.in?(Post.select(:author_id)) }.to_sql)
407
709
  end
408
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
+
409
771
  # The subquery correlates with the outer table through qualified columns,
410
772
  # and its own where block goes through the DSL too.
411
773
  def test_exists
@@ -457,21 +819,21 @@ class TestBlockSyntax < Minitest::Test
457
819
  # literal depends on the column type, so assert on the Arel node instead.
458
820
  def test_equal_range_is_an_equality
459
821
  node = ActiveRecord::Refined::AST::Comparison.new(:period, :==, 18..65).
460
- to_arel(User.arel_table)
822
+ to_arel(User.arel_table, User)
461
823
  assert_instance_of(Arel::Nodes::Equality, node)
462
824
  assert_equal(18..65, node.right.value)
463
825
  end
464
826
 
465
827
  def test_equal_array_is_an_equality
466
828
  node = ActiveRecord::Refined::AST::Comparison.new(:tags, :==, [1, 2, 3]).
467
- to_arel(User.arel_table)
829
+ to_arel(User.arel_table, User)
468
830
  assert_instance_of(Arel::Nodes::Equality, node)
469
831
  assert_equal([1, 2, 3], node.right.value)
470
832
  end
471
833
 
472
834
  def test_not_equal_array_is_an_inequality
473
835
  node = ActiveRecord::Refined::AST::Comparison.new(:tags, :!=, [1, 2, 3]).
474
- to_arel(User.arel_table)
836
+ to_arel(User.arel_table, User)
475
837
  assert_instance_of(Arel::Nodes::NotEqual, node)
476
838
  assert_equal([1, 2, 3], node.right.value)
477
839
  end
@@ -547,6 +909,65 @@ class TestBlockSyntax < Minitest::Test
547
909
  assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
548
910
  end
549
911
 
912
+ def test_from_cte_takes_the_alias_from_the_model
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)
917
+ end
918
+
919
+ def test_from_cte_needs_a_symbol
920
+ assert_raises(ArgumentError) { Node.from_cte('tree') }
921
+ end
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
+
950
+ # The alias is what lets a where find its column, which is the whole reason
951
+ # from_cte exists; without it the SQL names a table the query does not have.
952
+ def test_from_cte_leaves_where_able_to_qualify
953
+ Node.delete_all
954
+ root = Node.create!(name: 'root')
955
+ Node.create!(name: 'child', parent_id: root.id)
956
+ other = Node.create!(name: 'other root')
957
+ Node.create!(name: 'other child', parent_id: other.id)
958
+ forest = Node.with_recursive(
959
+ tree: [
960
+ Node.where { :parent_id.null? }.
961
+ select { [:id, :name, :parent_id, :id.as(:root_id)] },
962
+ Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }.
963
+ select { [:nodes[:id], :nodes[:name], :nodes[:parent_id],
964
+ :tree[:root_id]] },
965
+ ]
966
+ ).from_cte(:tree)
967
+ assert_equal(%w[child root],
968
+ forest.where { :root_id == root.id }.pluck(:name).sort)
969
+ end
970
+
550
971
  # A CTE is joined by name like any other table, so the recursive member's
551
972
  # ON clause is a block rather than the string join Rails' own docs use.
552
973
  def test_recursive_cte
@@ -574,33 +995,29 @@ class TestBlockSyntax < Minitest::Test
574
995
  assert_equal(['child'], q.pluck(:name))
575
996
  end
576
997
 
577
- def test_select_aggregate
578
- assert_sql(/SELECT SUM\("users"."age"\)/,
579
- User.select { :age.sum }.to_sql)
998
+ def test_select_sum
999
+ assert_sql(/SELECT SUM\("users"."age"\)/, User.select { sum(:age) }.to_sql)
580
1000
  end
581
1001
 
582
- def test_select_aggregate_qualified
1002
+ def test_select_aggregate_of_qualified_column
583
1003
  assert_sql(/SELECT COUNT\("users"."id"\)/,
584
- User.select { :users[:id].count }.to_sql)
1004
+ User.select { count(:users[:id]) }.to_sql)
585
1005
  end
586
1006
 
587
- def test_select_average
588
- assert_sql(/SELECT AVG\("users"."age"\)/,
589
- User.select { :age.average }.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)
590
1010
  end
591
1011
 
592
- def test_select_maximum
593
- assert_sql(/SELECT MAX\("users"."age"\)/,
594
- User.select { :age.maximum }.to_sql)
595
- end
596
-
597
- def test_select_minimum
598
- assert_sql(/SELECT MIN\("users"."age"\)/,
599
- 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 } }
600
1017
  end
601
1018
 
602
1019
  def test_having_aggregate
603
- sql = User.group(:name).having { :age.sum > 100 }.to_sql
1020
+ sql = User.group(:name).having { sum(:age) > 100 }.to_sql
604
1021
  assert_sql(/GROUP BY "users"."name"/, sql)
605
1022
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
606
1023
  end
@@ -621,7 +1038,7 @@ class TestBlockSyntax < Minitest::Test
621
1038
  end
622
1039
 
623
1040
  def test_select_count_star_alias
624
- assert_sql(/SELECT COUNT\(\*\) AS cnt/,
1041
+ assert_sql(/SELECT COUNT\(\*\) AS "cnt"/,
625
1042
  User.select { count(:*).as(:cnt) }.to_sql)
626
1043
  end
627
1044
 
@@ -630,9 +1047,9 @@ class TestBlockSyntax < Minitest::Test
630
1047
  User.select { count(:name, distinct: true) }.to_sql)
631
1048
  end
632
1049
 
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)
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)
636
1053
  end
637
1054
 
638
1055
  def test_count_distinct_in_having
@@ -690,11 +1107,6 @@ class TestBlockSyntax < Minitest::Test
690
1107
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
691
1108
  end
692
1109
 
693
- def test_function_and_method_syntax_match
694
- assert_equal User.select { :age.average }.to_sql,
695
- User.select { avg(:age) }.to_sql
696
- end
697
-
698
1110
  def test_upper_function
699
1111
  assert_sql(/SELECT UPPER\("users"."name"\)/,
700
1112
  User.select { upper(:name) }.to_sql)
@@ -723,7 +1135,7 @@ class TestBlockSyntax < Minitest::Test
723
1135
  end
724
1136
 
725
1137
  def test_fn_alias
726
- assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS d/,
1138
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS "d"/,
727
1139
  User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
728
1140
  end
729
1141
 
@@ -732,10 +1144,46 @@ class TestBlockSyntax < Minitest::Test
732
1144
  # given the chance to close the identifier and carry on.
733
1145
  INJECTION = %q{a" AS x, (SELECT 1) AS "y}
734
1146
 
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') } }
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) } }
739
1187
  end
740
1188
 
741
1189
  def test_fn_rejects_an_injected_name
@@ -743,8 +1191,8 @@ class TestBlockSyntax < Minitest::Test
743
1191
  end
744
1192
 
745
1193
  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)
1194
+ assert_sql(/AS "post_count"/, User.select { :name.as(:post_count) }.to_sql)
1195
+ assert_sql(/AS "名前"/, User.select { :name.as(:名前) }.to_sql)
748
1196
  assert_sql(/SELECT myFunc\(/, User.select { fn(:myFunc, :name) }.to_sql)
749
1197
  assert_sql(/SELECT pg_catalog.upper\(/,
750
1198
  User.select { fn(:'pg_catalog.upper', :name) }.to_sql)
@@ -857,7 +1305,7 @@ class TestBlockSyntax < Minitest::Test
857
1305
  def test_datetime_value_function_in_comparison_and_alias
858
1306
  assert_sql(/WHERE "users"."name" < CURRENT_TIMESTAMP/,
859
1307
  User.where { :name < current_timestamp }.to_sql)
860
- assert_sql(/SELECT CURRENT_TIMESTAMP AS ts FROM/,
1308
+ assert_sql(/SELECT CURRENT_TIMESTAMP AS "ts" FROM/,
861
1309
  User.select { current_timestamp.as(:ts) }.to_sql)
862
1310
  end
863
1311
 
@@ -1003,7 +1451,7 @@ class TestBlockSyntax < Minitest::Test
1003
1451
  end
1004
1452
 
1005
1453
  def test_arithmetic_multiplication
1006
- assert_sql(/SELECT "users"."age" \* 2 AS dbl/,
1454
+ assert_sql(/SELECT "users"."age" \* 2 AS "dbl"/,
1007
1455
  User.select { (:age * 2).as(:dbl) }.to_sql)
1008
1456
  end
1009
1457
 
@@ -1023,11 +1471,6 @@ class TestBlockSyntax < Minitest::Test
1023
1471
  User.select { sum(:age * 2) }.to_sql)
1024
1472
  end
1025
1473
 
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
1474
  # Arel groups + and - but not * and /, which is how SQL precedence works
1032
1475
  # out anyway.
1033
1476
  def test_arithmetic_on_qualified_column
@@ -1035,6 +1478,110 @@ class TestBlockSyntax < Minitest::Test
1035
1478
  User.select { :users[:age] - 1 }.to_sql)
1036
1479
  end
1037
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
+
1038
1585
  def test_coalesce_function_with_literal
1039
1586
  assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
1040
1587
  User.select { coalesce(:name, 'unknown') }.to_sql)
@@ -1057,7 +1604,7 @@ class TestBlockSyntax < Minitest::Test
1057
1604
 
1058
1605
  def test_aggregate_in
1059
1606
  assert_sql(/HAVING SUM\("users"."age"\) BETWEEN 1 AND 10/,
1060
- User.group(:name).having { :age.sum.in?(1..10) }.to_sql)
1607
+ User.group(:name).having { sum(:age).in?(1..10) }.to_sql)
1061
1608
  end
1062
1609
 
1063
1610
  def test_nested_function
@@ -1086,22 +1633,22 @@ class TestBlockSyntax < Minitest::Test
1086
1633
  end
1087
1634
 
1088
1635
  def test_select_function_with_alias
1089
- assert_sql(/SELECT UPPER\("users"."name"\) AS upper_name, "users"."age"/,
1636
+ assert_sql(/SELECT UPPER\("users"."name"\) AS "upper_name", "users"."age"/,
1090
1637
  User.select { [upper(:name).as(:upper_name), :age] }.to_sql)
1091
1638
  end
1092
1639
 
1093
1640
  def test_select_column_alias
1094
- assert_sql(/SELECT "users"."name" AS n/,
1641
+ assert_sql(/SELECT "users"."name" AS "n"/,
1095
1642
  User.select { :name.as(:n) }.to_sql)
1096
1643
  end
1097
1644
 
1098
1645
  def test_select_qualified_column_alias
1099
- assert_sql(/SELECT "users"."name" AS n/,
1646
+ assert_sql(/SELECT "users"."name" AS "n"/,
1100
1647
  User.select { :users[:name].as(:n) }.to_sql)
1101
1648
  end
1102
1649
 
1103
1650
  def test_select_aggregate_alias
1104
- assert_sql(/SELECT COUNT\("users"."id"\) AS cnt/,
1651
+ assert_sql(/SELECT COUNT\("users"."id"\) AS "cnt"/,
1105
1652
  User.select { count(:id).as(:cnt) }.to_sql)
1106
1653
  end
1107
1654
 
@@ -1175,8 +1722,510 @@ class TestBlockSyntax < Minitest::Test
1175
1722
  assert_sql(/HAVING SUM\("users"."age"\) > 100/, sql)
1176
1723
  end
1177
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
+
1178
2154
  def test_default_where_syntax
1179
2155
  assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
1180
2156
  User.where(name: 'Ruby', age: 19).to_sql)
1181
2157
  end
2158
+
2159
+ def test_value_in_a_select_list
2160
+ assert_sql(/SELECT "users"."name", 0 AS "depth"/,
2161
+ User.select { [:name, value(0).as(:depth)] }.to_sql)
2162
+ end
2163
+
2164
+ # Each adapter escapes the apostrophe its own way, so what is asserted is
2165
+ # that the string stays a value rather than reaching the SQL as written.
2166
+ def test_value_is_quoted
2167
+ User.delete_all
2168
+ User.create!(name: 'alice')
2169
+ payload = "it's a value"
2170
+ assert_sql(/SELECT 'draft' AS "state"/,
2171
+ User.select { value('draft').as(:state) }.to_sql)
2172
+ assert_equal([payload],
2173
+ User.select { value(payload).as(:note) }.map(&:note))
2174
+ end
2175
+
2176
+ def test_a_bare_string_is_still_sql
2177
+ assert_sql(/SELECT "users"."name", 1 \+ 1 AS two/,
2178
+ User.select { [:name, '1 + 1 AS two'] }.to_sql)
2179
+ end
2180
+
2181
+ def test_value_takes_the_predications
2182
+ assert_sql(/WHERE 1 = "users"."age"/, User.where { value(1) == :users[:age] }.to_sql)
2183
+ assert_sql(/WHERE 1 IS NULL/, User.where { value(1).null? }.to_sql)
2184
+ end
2185
+
2186
+ def test_value_takes_the_arithmetics
2187
+ assert_sql(/SELECT \(1 \+ "users"."age"\) AS "next_year"/,
2188
+ User.select { (value(1) + :age).as(:next_year) }.to_sql)
2189
+ end
2190
+
2191
+ def test_value_as_a_function_argument
2192
+ assert_sql(/SELECT COALESCE\("users"."age", 0\)/,
2193
+ User.select { coalesce(:age, value(0)) }.to_sql)
2194
+ end
2195
+
2196
+ def test_integer_shorthand_for_value
2197
+ assert_sql(/SELECT "users"."name", 0 AS "depth"/,
2198
+ User.select { [:name, 0.as(:depth)] }.to_sql)
2199
+ end
2200
+
2201
+ def test_float_shorthand_for_value
2202
+ assert_sql(/SELECT 1\.5 AS "rate"/, User.select { 1.5.as(:rate) }.to_sql)
2203
+ end
2204
+
2205
+ def test_numeric_shorthand_has_no_orderings
2206
+ assert_raises(NoMethodError) { User.order { 1.asc } }
2207
+ assert_raises(NoMethodError) { User.order { 1.desc } }
2208
+ end
2209
+
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)
2219
+ end
2220
+
2221
+ def test_a_value_selected_reaches_the_row
2222
+ User.delete_all
2223
+ User.create!(name: 'alice', age: 60)
2224
+ assert_equal([['alice', 0]],
2225
+ User.select { [:name, 0.as(:depth)] }.map {|u| [u.name, u.depth] })
2226
+ end
2227
+
2228
+ def test_numeric_shorthand_is_confined_to_the_block
2229
+ assert_raises(NoMethodError) { 0.as(:depth) }
2230
+ end
1182
2231
  end