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.
- checksums.yaml +4 -4
- data/.github/workflows/sandbox.yml +162 -25
- data/.github/workflows/test.yml +15 -2
- data/README.md +414 -51
- data/examples/ctes.rb +3 -3
- data/examples/expressions.rb +71 -5
- data/examples/json.rb +94 -0
- data/examples/postgresql.rb +89 -6
- data/examples/predicates.rb +32 -5
- data/examples/windows.rb +96 -0
- data/examples/writes.rb +84 -0
- data/lib/active_record/refined/ast.rb +780 -92
- data/lib/active_record/refined.rb +317 -26
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +5 -2
- data/test/test_block_syntax.rb +976 -60
- data/test/test_helper.rb +82 -0
- metadata +4 -1
data/test/test_block_syntax.rb
CHANGED
|
@@ -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
|
-
|
|
615
|
-
|
|
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
|
|
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
|
|
1002
|
+
def test_select_aggregate_of_qualified_column
|
|
676
1003
|
assert_sql(/SELECT COUNT\("users"."id"\)/,
|
|
677
|
-
User.select { :users[:id]
|
|
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
|
|
686
|
-
assert_sql(/SELECT MAX\("users"."age"\)/,
|
|
687
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
|
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
|
|
727
|
-
assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS n/,
|
|
728
|
-
User.select { :name
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|