activerecord-refined 0.4.0 → 0.5.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.
@@ -2,11 +2,11 @@ require_relative 'test_helper'
2
2
 
3
3
  class TestBlockSyntax < Minitest::Test
4
4
  def test_equal
5
- assert_sql(/WHERE "users"."name" = 'matz'/, User.where { :name == 'matz' }.to_sql)
5
+ assert_sql(/WHERE "users"."name" = 'alice'/, User.where { :name == 'alice' }.to_sql)
6
6
  end
7
7
 
8
8
  def test_not_equal
9
- assert_sql(/WHERE "users"."name" != 'nobu'/, User.where { :name != 'nobu' }.to_sql)
9
+ assert_sql(/WHERE "users"."name" != 'bob'/, User.where { :name != 'bob' }.to_sql)
10
10
  end
11
11
 
12
12
  def test_greater_than
@@ -34,23 +34,23 @@ class TestBlockSyntax < Minitest::Test
34
34
  end
35
35
 
36
36
  def test_and
37
- assert_sql(/WHERE "users"."name" = 'matz' AND "users"."age" > 18/,
38
- User.where { (:name == 'matz') & (:age > 18) }.to_sql)
37
+ assert_sql(/WHERE "users"."name" = 'alice' AND "users"."age" > 18/,
38
+ User.where { (:name == 'alice') & (:age > 18) }.to_sql)
39
39
  end
40
40
 
41
41
  def test_or
42
- assert_sql(/WHERE \(?\"users\".\"name\" = 'matz' OR \"users\".\"name\" = 'nobu'\)?/,
43
- User.where { (:name == 'matz') | (:name == 'nobu') }.to_sql)
42
+ assert_sql(/WHERE \(?\"users\".\"name\" = 'alice' OR \"users\".\"name\" = 'bob'\)?/,
43
+ User.where { (:name == 'alice') | (:name == 'bob') }.to_sql)
44
44
  end
45
45
 
46
46
  def test_not
47
- assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'matz'\)?/,
48
- User.where { !(:name == 'matz') }.to_sql)
47
+ assert_sql(/WHERE NOT \(?\"users\".\"name\" = 'alice'\)?/,
48
+ User.where { !(:name == 'alice') }.to_sql)
49
49
  end
50
50
 
51
51
  def test_complex_combination
52
- sql = User.where { ((:name == 'matz') & (:age > 18)) | !(:name == 'nobu') }.to_sql
53
- assert_sql(/"users"."name" = 'matz'/, sql)
52
+ sql = User.where { ((:name == 'alice') & (:age > 18)) | !(:name == 'bob') }.to_sql
53
+ assert_sql(/"users"."name" = 'alice'/, sql)
54
54
  assert_sql(/"users"."age" > 18/, sql)
55
55
  assert_sql(/NOT/, sql)
56
56
  assert_sql(/OR/, sql)
@@ -70,13 +70,13 @@ class TestBlockSyntax < Minitest::Test
70
70
  end
71
71
 
72
72
  def test_casecmp
73
- assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Matz'\)/,
74
- User.where { :name.casecmp?('Matz') }.to_sql)
73
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
74
+ User.where { :name.casecmp?('Alice') }.to_sql)
75
75
  end
76
76
 
77
77
  def test_casecmp_qualified
78
- assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Matz'\)/,
79
- User.where { :users[:name].casecmp?('Matz') }.to_sql)
78
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Alice'\)/,
79
+ User.where { :users[:name].casecmp?('Alice') }.to_sql)
80
80
  end
81
81
 
82
82
  def test_casecmp_nil_is_rejected
@@ -86,16 +86,27 @@ class TestBlockSyntax < Minitest::Test
86
86
 
87
87
  def test_casecmp_execution
88
88
  User.delete_all
89
- User.create!(name: 'Matz')
90
- User.create!(name: 'nobu')
91
- assert_equal(['Matz'], User.where { :name.casecmp?('mAtZ') }.pluck(:name))
89
+ User.create!(name: 'Alice')
90
+ User.create!(name: 'bob')
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)
@@ -115,8 +126,8 @@ class TestBlockSyntax < Minitest::Test
115
126
  # of literals; matching any one of them is enough.
116
127
  def test_start_with_multiple
117
128
  assert_sql(
118
- /WHERE \("users"."name" LIKE 'ma%' ESCAPE '\\' OR "users"."name" LIKE 'no%' ESCAPE '\\'\)/,
119
- User.where { :name.start_with?('ma', 'no') }.to_sql)
129
+ /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\)/,
130
+ User.where { :name.start_with?('al', 'bo') }.to_sql)
120
131
  end
121
132
 
122
133
  def test_end_with_multiple
@@ -128,8 +139,8 @@ class TestBlockSyntax < Minitest::Test
128
139
  # The OR arrives grouped, so a following & applies to the whole list.
129
140
  def test_start_with_multiple_combined
130
141
  assert_sql(
131
- /WHERE \("users"."name" LIKE 'ma%' ESCAPE '\\' OR "users"."name" LIKE 'no%' ESCAPE '\\'\) AND "users"."age" > 18/,
132
- User.where { :name.start_with?('ma', 'no') & (:age > 18) }.to_sql)
142
+ /WHERE \("users"."name" LIKE 'al%' ESCAPE '\\' OR "users"."name" LIKE 'bo%' ESCAPE '\\'\) AND "users"."age" > 18/,
143
+ User.where { :name.start_with?('al', 'bo') & (:age > 18) }.to_sql)
133
144
  end
134
145
 
135
146
  def test_start_with_no_arguments
@@ -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,36 @@ 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
+ # The claim these methods rest on: the direct spelling is the same rows as
350
+ # negating the positive one, which is where a NULL would show a difference
351
+ # if there were one.
352
+ def test_the_negations_match_what_bang_selects
353
+ User.delete_all
354
+ User.create!(name: 'alice', age: 60)
355
+ User.create!(name: 'bob', age: 20)
356
+ User.create!(name: nil, age: 40)
357
+ [
358
+ [-> { :name.not_null? }, -> { !:name.null? }],
359
+ [-> { :age.not_in?([20, 30]) }, -> { !:age.in?([20, 30]) }],
360
+ [-> { :age.not_between?(20, 30) }, -> { !:age.between?(20, 30) }],
361
+ [-> { :name.not_like?('a%') }, -> { !:name.like?('a%') }],
362
+ ].each do |direct, negated|
363
+ assert_equal(User.where(&negated).pluck(:id).sort,
364
+ User.where(&direct).pluck(:id).sort,
365
+ "#{direct.source_location} did not match the ! form")
366
+ end
367
+ end
368
+
316
369
  def test_equal_nil_is_rejected
317
370
  e = assert_raises(ArgumentError) { User.where { :name == nil } }
318
371
  assert_match(/null\?/, e.message)
@@ -333,11 +386,21 @@ class TestBlockSyntax < Minitest::Test
333
386
  User.where { :users[:age].in?([1, 2, 3]) }.to_sql)
334
387
  end
335
388
 
336
- def test_not_in
389
+ def test_bang_negates_in
337
390
  assert_sql(/WHERE NOT \("users"."age" IN \(1, 2, 3\)\)/,
338
391
  User.where { !:age.in?([1, 2, 3]) }.to_sql)
339
392
  end
340
393
 
394
+ def test_not_in
395
+ assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
396
+ User.where { :age.not_in?([1, 2, 3]) }.to_sql)
397
+ end
398
+
399
+ def test_not_in_qualified
400
+ assert_sql(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
401
+ User.where { :users[:age].not_in?([1, 2, 3]) }.to_sql)
402
+ end
403
+
341
404
  # Spelled IS [NOT] DISTINCT FROM on PostgreSQL, IS / IS NOT on SQLite and
342
405
  # <=> on MySQL, so only the resulting rows are portable.
343
406
  def test_not_distinct_from_execution
@@ -350,11 +413,11 @@ class TestBlockSyntax < Minitest::Test
350
413
 
351
414
  def test_not_distinct_from_a_value_execution
352
415
  User.delete_all
353
- User.create!(name: 'matz')
416
+ User.create!(name: 'alice')
354
417
  User.create!(name: nil)
355
- assert_equal(['matz'], User.where { :name.not_distinct_from?('matz') }.pluck(:name))
418
+ assert_equal(['alice'], User.where { :name.not_distinct_from?('alice') }.pluck(:name))
356
419
  # Unlike !=, this keeps the NULL row.
357
- assert_equal([nil], User.where { :name.distinct_from?('matz') }.pluck(:name))
420
+ assert_equal([nil], User.where { :name.distinct_from?('alice') }.pluck(:name))
358
421
  end
359
422
 
360
423
  def test_distinct_from_postgresql_syntax
@@ -421,8 +484,8 @@ class TestBlockSyntax < Minitest::Test
421
484
 
422
485
  def test_exists_combined
423
486
  assert_sql(
424
- /WHERE "authors"."name" = 'matz' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
425
- Author.where { (:name == 'matz') & exists?(Post.where(title: 'pub')) }.to_sql)
487
+ /WHERE "authors"."name" = 'alice' AND EXISTS \(SELECT "posts"\.\* FROM "posts" WHERE "posts"."title" = 'pub'\)/,
488
+ Author.where { (:name == 'alice') & exists?(Post.where(title: 'pub')) }.to_sql)
426
489
  end
427
490
 
428
491
  def test_exists_execution
@@ -477,8 +540,8 @@ class TestBlockSyntax < Minitest::Test
477
540
  end
478
541
 
479
542
  def test_qualified_column
480
- assert_sql(/WHERE "users"."name" = 'matz'/,
481
- User.where { :users[:name] == 'matz' }.to_sql)
543
+ assert_sql(/WHERE "users"."name" = 'alice'/,
544
+ User.where { :users[:name] == 'alice' }.to_sql)
482
545
  end
483
546
 
484
547
  def test_column_to_column_comparison
@@ -547,6 +610,36 @@ class TestBlockSyntax < Minitest::Test
547
610
  assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
548
611
  end
549
612
 
613
+ def test_from_cte_takes_the_alias_from_the_model
614
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from_cte(:tree).to_sql)
615
+ assert_equal(Node.from(:tree, as: :nodes).to_sql, Node.from_cte(:tree).to_sql)
616
+ end
617
+
618
+ def test_from_cte_needs_a_symbol
619
+ assert_raises(ArgumentError) { Node.from_cte('tree') }
620
+ end
621
+
622
+ # The alias is what lets a where find its column, which is the whole reason
623
+ # from_cte exists; without it the SQL names a table the query does not have.
624
+ def test_from_cte_leaves_where_able_to_qualify
625
+ Node.delete_all
626
+ root = Node.create!(name: 'root')
627
+ Node.create!(name: 'child', parent_id: root.id)
628
+ other = Node.create!(name: 'other root')
629
+ Node.create!(name: 'other child', parent_id: other.id)
630
+ forest = Node.with_recursive(
631
+ tree: [
632
+ Node.where { :parent_id.null? }.
633
+ select { [:id, :name, :parent_id, :id.as(:root_id)] },
634
+ Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] }.
635
+ select { [:nodes[:id], :nodes[:name], :nodes[:parent_id],
636
+ :tree[:root_id]] },
637
+ ]
638
+ ).from_cte(:tree)
639
+ assert_equal(%w[child root],
640
+ forest.where { :root_id == root.id }.pluck(:name).sort)
641
+ end
642
+
550
643
  # A CTE is joined by name like any other table, so the recursive member's
551
644
  # ON clause is a block rather than the string join Rails' own docs use.
552
645
  def test_recursive_cte
@@ -755,8 +848,8 @@ class TestBlockSyntax < Minitest::Test
755
848
  # rather than opening the condition up.
756
849
  def test_values_are_quoted
757
850
  User.delete_all
758
- User.create!(name: 'matz')
759
- User.create!(name: 'nobu')
851
+ User.create!(name: 'alice')
852
+ User.create!(name: 'bob')
760
853
  payload = "x' OR 1=1 --"
761
854
  assert_empty(User.where { :name == payload }.pluck(:name))
762
855
  assert_empty(User.where { :name.like?(payload) }.pluck(:name))
@@ -793,9 +886,9 @@ class TestBlockSyntax < Minitest::Test
793
886
 
794
887
  def test_scalar_functions_run
795
888
  User.delete_all
796
- User.create!(name: 'matz', age: 60)
797
- assert_equal(['MATZ-x'], User.select { concat(upper(:name), '-x').as(:v) }.map(&:v))
798
- assert_equal([4], User.select { char_length(:name).as(:v) }.map(&:v))
889
+ User.create!(name: 'alice', age: 60)
890
+ assert_equal(['ALICE-x'], User.select { concat(upper(:name), '-x').as(:v) }.map(&:v))
891
+ assert_equal([5], User.select { char_length(:name).as(:v) }.map(&:v))
799
892
  assert_equal([60], User.select { greatest(:age, 18).as(:v) }.map(&:v))
800
893
  end
801
894
 
@@ -826,8 +919,8 @@ class TestBlockSyntax < Minitest::Test
826
919
  assert_raises(NotImplementedError) { User.select { format('%s!', :name) } }
827
920
  else
828
921
  User.delete_all
829
- User.create!(name: 'matz')
830
- assert_equal(['matz!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
922
+ User.create!(name: 'alice')
923
+ assert_equal(['alice!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
831
924
  end
832
925
  end
833
926
 
@@ -845,6 +938,157 @@ class TestBlockSyntax < Minitest::Test
845
938
  end
846
939
  end
847
940
 
941
+ # CURRENT_TIMESTAMP and its relatives are grammar rather than calls, so
942
+ # they come out without the parentheses PostgreSQL and SQLite reject.
943
+ def test_datetime_value_functions_are_emitted_bare
944
+ assert_sql(/SELECT CURRENT_TIMESTAMP FROM/,
945
+ User.select { current_timestamp }.to_sql)
946
+ assert_sql(/SELECT CURRENT_DATE FROM/, User.select { current_date }.to_sql)
947
+ assert_sql(/SELECT CURRENT_TIME FROM/, User.select { current_time }.to_sql)
948
+ end
949
+
950
+ def test_datetime_value_function_in_comparison_and_alias
951
+ assert_sql(/WHERE "users"."name" < CURRENT_TIMESTAMP/,
952
+ User.where { :name < current_timestamp }.to_sql)
953
+ assert_sql(/SELECT CURRENT_TIMESTAMP AS ts FROM/,
954
+ User.select { current_timestamp.as(:ts) }.to_sql)
955
+ end
956
+
957
+ def test_current_timestamp_runs
958
+ User.delete_all
959
+ User.create!(name: 'alice')
960
+ refute_nil(User.select { current_timestamp.as(:v) }.sole.v)
961
+ end
962
+
963
+ # The one thing that does go into the parentheses is a precision, which
964
+ # current_date never takes and SQLite never accepts.
965
+ def test_datetime_value_function_with_precision
966
+ if ADAPTER == 'sqlite3'
967
+ e = assert_raises(NotImplementedError) { User.select { current_timestamp(3) } }
968
+ assert_match(/precision/, e.message)
969
+ else
970
+ assert_sql(/SELECT CURRENT_TIMESTAMP\(3\) FROM/,
971
+ User.select { current_timestamp(3) }.to_sql)
972
+ assert_sql(/SELECT CURRENT_TIME\(0\) FROM/,
973
+ User.select { current_time(0) }.to_sql)
974
+ User.delete_all
975
+ User.create!(name: 'alice')
976
+ refute_nil(User.select { current_timestamp(0).as(:v) }.sole.v)
977
+ end
978
+ end
979
+
980
+ def test_current_date_takes_no_precision
981
+ assert_raises(ArgumentError) { User.select { current_date(0) } }
982
+ end
983
+
984
+ # The precision is written into the SQL as given, so only an Integer is
985
+ # accepted there.
986
+ def test_precision_must_be_an_integer
987
+ assert_raises(ArgumentError) do
988
+ User.select { current_timestamp(:'3); DROP TABLE users --') }
989
+ end
990
+ end
991
+
992
+ def test_localtime_is_unsupported_on_sqlite
993
+ if ADAPTER == 'sqlite3'
994
+ assert_raises(NotImplementedError) { User.select { localtime } }
995
+ assert_raises(NotImplementedError) { User.select { localtimestamp } }
996
+ else
997
+ assert_sql(/SELECT LOCALTIME FROM/, User.select { localtime }.to_sql)
998
+ assert_sql(/SELECT LOCALTIMESTAMP FROM/,
999
+ User.select { localtimestamp }.to_sql)
1000
+ end
1001
+ end
1002
+
1003
+ def test_math_functions
1004
+ assert_sql(/SELECT SIGN\("users"."age"\)/, User.select { sign(:age) }.to_sql)
1005
+ assert_sql(/SELECT ATAN2\("users"."age", 2\)/,
1006
+ User.select { atan2(:age, 2) }.to_sql)
1007
+ assert_sql(/SELECT PI\(\)/, User.select { pi }.to_sql)
1008
+ assert_sql(/SELECT DEGREES\(RADIANS\("users"."age"\)\)/,
1009
+ User.select { degrees(radians(:age)) }.to_sql)
1010
+ end
1011
+
1012
+ def test_math_functions_run
1013
+ User.delete_all
1014
+ User.create!(name: 'alice', age: 60)
1015
+ assert_equal(1, User.select { sign(:age).as(:v) }.sole.v.to_i)
1016
+ assert_equal(60,
1017
+ User.select { round(degrees(radians(:age))).as(:v) }.sole.v.to_i)
1018
+ end
1019
+
1020
+ # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
1021
+ def test_log2_is_unsupported_on_postgresql
1022
+ if ADAPTER == 'postgresql'
1023
+ assert_raises(NotImplementedError) { User.select { log2(:age) } }
1024
+ else
1025
+ assert_sql(/SELECT LOG2\("users"."age"\)/, User.select { log2(:age) }.to_sql)
1026
+ end
1027
+ end
1028
+
1029
+ # MySQL spells trunc TRUNCATE, and insists on the second argument the
1030
+ # others default to zero.
1031
+ def test_trunc
1032
+ expected = ADAPTER == 'mysql2' ? 'TRUNCATE' : 'TRUNC'
1033
+ assert_sql(/SELECT #{expected}\("users"."age", 0\)/,
1034
+ User.select { trunc(:age, 0) }.to_sql)
1035
+ end
1036
+
1037
+ # EXTRACT(field FROM expr): the field is a keyword rather than a value.
1038
+ # SQLite spells all of this as strftime formats, which no renaming
1039
+ # carries.
1040
+ def test_extract
1041
+ if ADAPTER == 'sqlite3'
1042
+ e = assert_raises(NotImplementedError) { User.select { extract(:year, :name) } }
1043
+ assert_match(/extract/, e.message)
1044
+ else
1045
+ assert_sql(/SELECT EXTRACT\(YEAR FROM "users"."name"\)/,
1046
+ User.select { extract(:year, :name) }.to_sql)
1047
+ assert_sql(/WHERE EXTRACT\(YEAR FROM "users"."name"\) = 2026/,
1048
+ User.where { extract(:year, :name) == 2026 }.to_sql)
1049
+ end
1050
+ end
1051
+
1052
+ # A bad field is an ArgumentError on every adapter, before SQLite gets to
1053
+ # say it has no extract at all.
1054
+ def test_extract_rejects_an_injected_field
1055
+ assert_raises(ArgumentError) { User.select { extract(INJECTION.to_sym, :name) } }
1056
+ end
1057
+
1058
+ def test_extract_runs
1059
+ skip "#{ADAPTER} has no extract" if ADAPTER == 'sqlite3'
1060
+ User.delete_all
1061
+ User.create!(name: 'alice')
1062
+ assert_equal(2026,
1063
+ User.select { extract(:year, cast('2026-01-05', :date)).as(:v) }.sole.v.to_i)
1064
+ end
1065
+
1066
+ def test_cast
1067
+ assert_sql(/SELECT CAST\("users"."age" AS text\)/,
1068
+ User.select { cast(:age, :text) }.to_sql)
1069
+ end
1070
+
1071
+ def test_cast_runs
1072
+ User.delete_all
1073
+ User.create!(name: 'alice')
1074
+ assert_equal(12.5,
1075
+ User.select { cast('12.5', 'decimal(10,2)').as(:v) }.sole.v.to_f)
1076
+ end
1077
+
1078
+ # The type is written into the SQL as given, so it has to look like one:
1079
+ # a plain name, at most parenthesized with lengths. The adapters' own
1080
+ # spellings with a space in them pass too.
1081
+ def test_cast_type_names
1082
+ assert_sql(/AS double precision\)/,
1083
+ User.select { cast(:age, 'double precision') }.to_sql)
1084
+ assert_sql(/AS decimal\(10,2\)\)/,
1085
+ User.select { cast(:age, 'decimal(10,2)') }.to_sql)
1086
+ assert_raises(ArgumentError) { User.select { cast(:age, INJECTION.to_sym) } }
1087
+ assert_raises(ArgumentError) do
1088
+ User.select { cast(:age, 'integer); DROP TABLE users --') }
1089
+ end
1090
+ end
1091
+
848
1092
  # A name with no method of its own is still a NoMethodError, not a
849
1093
  # function call the database has to reject.
850
1094
  def test_unknown_function_is_a_no_method_error
@@ -1028,4 +1272,71 @@ class TestBlockSyntax < Minitest::Test
1028
1272
  assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
1029
1273
  User.where(name: 'Ruby', age: 19).to_sql)
1030
1274
  end
1275
+
1276
+ def test_value_in_a_select_list
1277
+ assert_sql(/SELECT "users"."name", 0 AS depth/,
1278
+ User.select { [:name, value(0).as(:depth)] }.to_sql)
1279
+ end
1280
+
1281
+ # Each adapter escapes the apostrophe its own way, so what is asserted is
1282
+ # that the string stays a value rather than reaching the SQL as written.
1283
+ def test_value_is_quoted
1284
+ User.delete_all
1285
+ User.create!(name: 'alice')
1286
+ payload = "it's a value"
1287
+ assert_sql(/SELECT 'draft' AS state/,
1288
+ User.select { value('draft').as(:state) }.to_sql)
1289
+ assert_equal([payload],
1290
+ User.select { value(payload).as(:note) }.map(&:note))
1291
+ end
1292
+
1293
+ def test_a_bare_string_is_still_sql
1294
+ assert_sql(/SELECT "users"."name", 1 \+ 1 AS two/,
1295
+ User.select { [:name, '1 + 1 AS two'] }.to_sql)
1296
+ end
1297
+
1298
+ def test_value_takes_the_predications
1299
+ assert_sql(/WHERE 1 = "users"."age"/, User.where { value(1) == :users[:age] }.to_sql)
1300
+ assert_sql(/WHERE 1 IS NULL/, User.where { value(1).null? }.to_sql)
1301
+ end
1302
+
1303
+ def test_value_takes_the_arithmetics
1304
+ assert_sql(/SELECT \(1 \+ "users"."age"\) AS next_year/,
1305
+ User.select { (value(1) + :age).as(:next_year) }.to_sql)
1306
+ end
1307
+
1308
+ def test_value_as_a_function_argument
1309
+ assert_sql(/SELECT COALESCE\("users"."age", 0\)/,
1310
+ User.select { coalesce(:age, value(0)) }.to_sql)
1311
+ end
1312
+
1313
+ def test_integer_shorthand_for_value
1314
+ assert_sql(/SELECT "users"."name", 0 AS depth/,
1315
+ User.select { [:name, 0.as(:depth)] }.to_sql)
1316
+ end
1317
+
1318
+ def test_float_shorthand_for_value
1319
+ assert_sql(/SELECT 1\.5 AS rate/, User.select { 1.5.as(:rate) }.to_sql)
1320
+ end
1321
+
1322
+ def test_numeric_shorthand_has_no_orderings
1323
+ assert_raises(NoMethodError) { User.order { 1.asc } }
1324
+ assert_raises(NoMethodError) { User.order { 1.desc } }
1325
+ end
1326
+
1327
+ def test_value_alias_rejects_an_injected_name
1328
+ assert_raises(ArgumentError) { User.select { value(0).as(INJECTION.to_sym) } }
1329
+ assert_raises(ArgumentError) { User.select { 0.as(INJECTION.to_sym) } }
1330
+ end
1331
+
1332
+ def test_a_value_selected_reaches_the_row
1333
+ User.delete_all
1334
+ User.create!(name: 'alice', age: 60)
1335
+ assert_equal([['alice', 0]],
1336
+ User.select { [:name, 0.as(:depth)] }.map {|u| [u.name, u.depth] })
1337
+ end
1338
+
1339
+ def test_numeric_shorthand_is_confined_to_the_block
1340
+ assert_raises(NoMethodError) { 0.as(:depth) }
1341
+ end
1031
1342
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-refined
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -81,6 +81,7 @@ extensions: []
81
81
  extra_rdoc_files: []
82
82
  files:
83
83
  - .github/workflows/push_gem.yml
84
+ - .github/workflows/sandbox.yml
84
85
  - .github/workflows/test.yml
85
86
  - .gitignore
86
87
  - Gemfile