activerecord-refined 0.3.3 → 0.4.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.
@@ -61,6 +61,36 @@ class TestBlockSyntax < Minitest::Test
61
61
  User.where { :users[:name].like?('tender%') }.to_sql)
62
62
  end
63
63
 
64
+ # ILIKE is PostgreSQL's; elsewhere Arel emits LIKE, which those adapters
65
+ # already match case-insensitively by default.
66
+ def test_ilike
67
+ expected = ADAPTER == 'postgresql' ? 'ILIKE' : 'LIKE'
68
+ assert_sql(/WHERE "users"."name" #{expected} 'ma%'/,
69
+ User.where { :name.ilike?('ma%') }.to_sql)
70
+ end
71
+
72
+ def test_casecmp
73
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Matz'\)/,
74
+ User.where { :name.casecmp?('Matz') }.to_sql)
75
+ end
76
+
77
+ def test_casecmp_qualified
78
+ assert_sql(/WHERE LOWER\("users"."name"\) = LOWER\('Matz'\)/,
79
+ User.where { :users[:name].casecmp?('Matz') }.to_sql)
80
+ end
81
+
82
+ def test_casecmp_nil_is_rejected
83
+ e = assert_raises(ArgumentError) { User.where { :name.casecmp?(nil) } }
84
+ assert_match(/null\?/, e.message)
85
+ end
86
+
87
+ def test_casecmp_execution
88
+ User.delete_all
89
+ User.create!(name: 'Matz')
90
+ User.create!(name: 'nobu')
91
+ assert_equal(['Matz'], User.where { :name.casecmp?('mAtZ') }.pluck(:name))
92
+ end
93
+
64
94
  def test_not_like
65
95
  assert_sql(/WHERE NOT \("users"."name" LIKE 'tender%'\)/,
66
96
  User.where { !:name.like?('tender%') }.to_sql)
@@ -135,9 +165,52 @@ class TestBlockSyntax < Minitest::Test
135
165
  User.where { !:tags.member?('ruby') }.to_sql)
136
166
  end
137
167
 
138
- def test_member_multiple_elements
168
+ # Ruby's [1, 2].member?([1]) is false: member? tests one element, and an
169
+ # Array argument would have to mean something the namesake does not.
170
+ def test_member_array_is_rejected
171
+ e = assert_raises(ArgumentError) { User.where { :tags.member?(%w[ruby rails]) } }
172
+ assert_match(/superset\?/, e.message)
173
+ end
174
+
175
+ def test_superset
176
+ assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
177
+ User.where { :tags.superset?(%w[ruby rails]) }.to_sql)
178
+ end
179
+
180
+ def test_superset_takes_a_set
139
181
  assert_sql(/WHERE "users"."tags" @> '\{ruby,rails\}'/,
140
- User.where { :tags.member?(%w[ruby rails]) }.to_sql)
182
+ User.where { :tags.superset?(Set['ruby', 'rails']) }.to_sql)
183
+ end
184
+
185
+ def test_superset_rejects_a_scalar
186
+ assert_raises(ArgumentError) { User.where { :tags.superset?('ruby') } }
187
+ end
188
+
189
+ def test_subset
190
+ assert_sql(/WHERE "users"."tags" <@ '\{ruby,rails,go\}'/,
191
+ User.where { :tags.subset?(%w[ruby rails go]) }.to_sql)
192
+ end
193
+
194
+ def test_intersect
195
+ assert_sql(/WHERE "users"."tags" && '\{ruby,go\}'/,
196
+ User.where { :tags.intersect?(%w[ruby go]) }.to_sql)
197
+ end
198
+
199
+ def test_intersect_negated
200
+ assert_sql(/WHERE NOT \("users"."tags" && '\{ruby,go\}'\)/,
201
+ User.where { !:tags.intersect?(%w[ruby go]) }.to_sql)
202
+ end
203
+
204
+ def test_array_comparisons_execution
205
+ skip_without_array_columns
206
+ User.delete_all
207
+ User.create!(name: 'both', tags: %w[ruby rails])
208
+ User.create!(name: 'one', tags: %w[ruby go])
209
+ User.create!(name: 'neither', tags: %w[python])
210
+ assert_equal(['both'], User.where { :tags.superset?(%w[ruby rails]) }.pluck(:name))
211
+ assert_equal(['neither'], User.where { :tags.subset?(%w[python js]) }.pluck(:name))
212
+ assert_equal(%w[both one],
213
+ User.where { :tags.intersect?(%w[ruby js]) }.pluck(:name).sort)
141
214
  end
142
215
 
143
216
  # MySQL additionally escapes the double quotes inside its string literal,
@@ -265,6 +338,56 @@ class TestBlockSyntax < Minitest::Test
265
338
  User.where { !:age.in?([1, 2, 3]) }.to_sql)
266
339
  end
267
340
 
341
+ # Spelled IS [NOT] DISTINCT FROM on PostgreSQL, IS / IS NOT on SQLite and
342
+ # <=> on MySQL, so only the resulting rows are portable.
343
+ def test_not_distinct_from_execution
344
+ User.delete_all
345
+ User.create!(name: 'named')
346
+ User.create!(name: nil)
347
+ assert_equal([nil], User.where { :name.not_distinct_from?(nil) }.pluck(:name))
348
+ assert_equal(['named'], User.where { :name.distinct_from?(nil) }.pluck(:name))
349
+ end
350
+
351
+ def test_not_distinct_from_a_value_execution
352
+ User.delete_all
353
+ User.create!(name: 'matz')
354
+ User.create!(name: nil)
355
+ assert_equal(['matz'], User.where { :name.not_distinct_from?('matz') }.pluck(:name))
356
+ # Unlike !=, this keeps the NULL row.
357
+ assert_equal([nil], User.where { :name.distinct_from?('matz') }.pluck(:name))
358
+ end
359
+
360
+ def test_distinct_from_postgresql_syntax
361
+ skip "#{ADAPTER} spells it differently" unless ADAPTER == 'postgresql'
362
+ assert_sql(/WHERE "users"."name" IS NOT DISTINCT FROM 'x'/,
363
+ User.where { :name.not_distinct_from?('x') }.to_sql)
364
+ assert_sql(/WHERE "users"."name" IS DISTINCT FROM 'x'/,
365
+ User.where { :name.distinct_from?('x') }.to_sql)
366
+ end
367
+
368
+ def test_comparison_with_scalar_subquery
369
+ assert_sql(/WHERE "users"."age" >= \(SELECT AVG\("users"."age"\) FROM "users"\)/,
370
+ User.where { :age >= User.select { avg(:age) } }.to_sql)
371
+ end
372
+
373
+ def test_equality_with_scalar_subquery
374
+ assert_sql(/WHERE "users"."age" = \(SELECT MAX\("users"."age"\) FROM "users"\)/,
375
+ User.where { :age == User.select { max(:age) } }.to_sql)
376
+ end
377
+
378
+ # A scalar comparison has no sensible default select list, unlike in?.
379
+ def test_scalar_subquery_without_select_is_rejected
380
+ e = assert_raises(ArgumentError) { User.where { :age >= User.all } }
381
+ assert_match(/select/, e.message)
382
+ end
383
+
384
+ def test_scalar_subquery_execution
385
+ User.delete_all
386
+ User.create!(name: 'young', age: 20)
387
+ User.create!(name: 'old', age: 60)
388
+ assert_equal(['old'], User.where { :age >= User.select { avg(:age) } }.pluck(:name))
389
+ end
390
+
268
391
  def test_in_subquery
269
392
  assert_sql(
270
393
  /WHERE "authors"."id" IN \(SELECT "posts"."author_id" FROM "posts" WHERE "posts"."title" = 'pub'\)/,
@@ -373,6 +496,84 @@ class TestBlockSyntax < Minitest::Test
373
496
  assert_sql(/LEFT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
374
497
  end
375
498
 
499
+ # The alias is what the block's qualified columns name, which is what makes
500
+ # a self join expressible at all. Adapters differ on writing the AS
501
+ # keyword, so the assertions allow either.
502
+ def test_joins_with_alias
503
+ assert_sql(
504
+ /INNER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
505
+ Author.joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
506
+ end
507
+
508
+ def test_left_outer_joins_with_alias
509
+ assert_sql(
510
+ /LEFT OUTER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
511
+ Author.left_outer_joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
512
+ end
513
+
514
+ def test_joins_alias_needs_a_block
515
+ assert_raises(ArgumentError) { Author.joins(:posts, as: :p) }
516
+ assert_raises(ArgumentError) { Author.left_outer_joins(:posts, as: :p) }
517
+ end
518
+
519
+ def test_joins_without_alias_still_delegates
520
+ assert_sql(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
521
+ Author.joins(:posts).to_sql)
522
+ end
523
+
524
+ def test_self_join_execution
525
+ Author.delete_all
526
+ Author.create!(name: 'shared')
527
+ Author.create!(name: 'other')
528
+ assert_equal(%w[other shared],
529
+ Author.joins(:authors, as: :mentors) { :mentors[:name] == :authors[:name] }.
530
+ pluck(:name).sort)
531
+ end
532
+
533
+ # ActiveRecord's from only takes a table name as a string.
534
+ def test_from_symbol
535
+ assert_sql(/FROM "tree"/, Node.from(:tree).to_sql)
536
+ end
537
+
538
+ def test_from_symbol_with_alias
539
+ assert_sql(/FROM "tree" (?:AS )?"nodes"/, Node.from(:tree, as: :nodes).to_sql)
540
+ end
541
+
542
+ def test_from_string_still_delegates
543
+ assert_sql(/FROM subq/, Node.from('subq').to_sql)
544
+ end
545
+
546
+ def test_from_alias_needs_a_symbol
547
+ assert_raises(ArgumentError) { Node.from('tree', as: :nodes) }
548
+ end
549
+
550
+ # A CTE is joined by name like any other table, so the recursive member's
551
+ # ON clause is a block rather than the string join Rails' own docs use.
552
+ def test_recursive_cte
553
+ Node.delete_all
554
+ root = Node.create!(name: 'root')
555
+ child = Node.create!(name: 'child', parent_id: root.id)
556
+ Node.create!(name: 'grandchild', parent_id: child.id)
557
+ Node.create!(name: 'unrelated', parent_id: nil)
558
+ descendants = Node.with_recursive(
559
+ tree: [
560
+ Node.where { :id == root.id },
561
+ Node.joins(:tree) { :nodes[:parent_id] == :tree[:id] },
562
+ ]
563
+ ).from(:tree, as: :nodes)
564
+ assert_equal(%w[child grandchild root], descendants.pluck(:name).sort)
565
+ end
566
+
567
+ def test_cte_joined_by_name
568
+ Node.delete_all
569
+ root = Node.create!(name: 'root')
570
+ Node.create!(name: 'child', parent_id: root.id)
571
+ Node.create!(name: 'orphan', parent_id: nil)
572
+ q = Node.with(roots: Node.where { :parent_id.null? }).
573
+ joins(:roots) { :roots[:id] == :nodes[:parent_id] }
574
+ assert_equal(['child'], q.pluck(:name))
575
+ end
576
+
376
577
  def test_select_aggregate
377
578
  assert_sql(/SELECT SUM\("users"."age"\)/,
378
579
  User.select { :age.sum }.to_sql)
@@ -424,6 +625,33 @@ class TestBlockSyntax < Minitest::Test
424
625
  User.select { count(:*).as(:cnt) }.to_sql)
425
626
  end
426
627
 
628
+ def test_count_distinct
629
+ assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\)/,
630
+ User.select { count(:name, distinct: true) }.to_sql)
631
+ end
632
+
633
+ def test_count_distinct_as_method
634
+ assert_sql(/SELECT COUNT\(DISTINCT "users"."name"\) AS n/,
635
+ User.select { :name.count(distinct: true).as(:n) }.to_sql)
636
+ end
637
+
638
+ def test_count_distinct_in_having
639
+ assert_sql(/HAVING COUNT\(DISTINCT "users"."name"\) > 1/,
640
+ User.group(:age).having { count(:name, distinct: true) > 1 }.to_sql)
641
+ end
642
+
643
+ # DISTINCT is Arel's only aggregate modifier, and COUNT(DISTINCT *) is not
644
+ # valid SQL.
645
+ def test_distinct_is_rejected_for_other_aggregates
646
+ assert_raises(ArgumentError) do
647
+ ActiveRecord::Refined::AST::Aggregate.new(:age, :sum, distinct: true)
648
+ end
649
+ end
650
+
651
+ def test_count_star_distinct_is_rejected
652
+ assert_raises(ArgumentError) { User.select { count(:*, distinct: true) } }
653
+ end
654
+
427
655
  def test_having_count_star
428
656
  sql = Author.joins(:posts) { :posts[:author_id] == :authors[:id] }.
429
657
  group { :authors[:id] }.
@@ -482,6 +710,180 @@ class TestBlockSyntax < Minitest::Test
482
710
  User.where { length(:name) > 3 }.to_sql)
483
711
  end
484
712
 
713
+ # fn emits the name as written, so a case-sensitive one can be spelled
714
+ # exactly.
715
+ def test_fn
716
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\)/,
717
+ User.select { fn(:date_trunc, 'day', :name) }.to_sql)
718
+ end
719
+
720
+ def test_fn_is_comparable
721
+ assert_sql(/WHERE char_length\("users"."name"\) > 3/,
722
+ User.where { fn(:char_length, :name) > 3 }.to_sql)
723
+ end
724
+
725
+ def test_fn_alias
726
+ assert_sql(/SELECT date_trunc\('day', "users"."name"\) AS d/,
727
+ User.select { fn(:date_trunc, 'day', :name).as(:d) }.to_sql)
728
+ end
729
+
730
+ # Aliases and function names are written into the SQL where a value would
731
+ # have been quoted, so a name that is not plain is refused rather than
732
+ # given the chance to close the identifier and carry on.
733
+ INJECTION = %q{a" AS x, (SELECT 1) AS "y}
734
+
735
+ def test_alias_rejects_an_injected_name
736
+ assert_raises(ArgumentError) { User.select { :name.as(INJECTION.to_sym) } }
737
+ assert_raises(ArgumentError) { User.select { count(:*).as(INJECTION.to_sym) } }
738
+ assert_raises(ArgumentError) { User.select { :name.as(:'a; DROP TABLE users') } }
739
+ end
740
+
741
+ def test_fn_rejects_an_injected_name
742
+ assert_raises(ArgumentError) { User.select { fn(INJECTION.to_sym, :name) } }
743
+ end
744
+
745
+ def test_plain_names_are_still_accepted
746
+ assert_sql(/AS post_count/, User.select { :name.as(:post_count) }.to_sql)
747
+ assert_sql(/AS 名前/, User.select { :name.as(:名前) }.to_sql)
748
+ assert_sql(/SELECT myFunc\(/, User.select { fn(:myFunc, :name) }.to_sql)
749
+ assert_sql(/SELECT pg_catalog.upper\(/,
750
+ User.select { fn(:'pg_catalog.upper', :name) }.to_sql)
751
+ end
752
+
753
+ # Values go through the adapter's quoting, which each spells its own way,
754
+ # so what is asserted is that the payload stays a value: it matches no row
755
+ # rather than opening the condition up.
756
+ def test_values_are_quoted
757
+ User.delete_all
758
+ User.create!(name: 'matz')
759
+ User.create!(name: 'nobu')
760
+ payload = "x' OR 1=1 --"
761
+ assert_empty(User.where { :name == payload }.pluck(:name))
762
+ assert_empty(User.where { :name.like?(payload) }.pluck(:name))
763
+ assert_empty(User.where { :name.in?([payload]) }.pluck(:name))
764
+ assert_empty(User.where { :name.include?(payload) }.pluck(:name))
765
+ end
766
+
767
+ # Likewise for column names: the payload becomes one identifier, so the
768
+ # database rejects it as an unknown column instead of running it.
769
+ def test_column_names_are_quoted
770
+ assert_raises(ActiveRecord::StatementInvalid) do
771
+ User.where { :users[INJECTION.to_sym] == 1 }.to_a
772
+ end
773
+ end
774
+
775
+ def test_scalar_functions_shared_by_every_adapter
776
+ assert_sql(/SELECT CONCAT\(UPPER\("users"."name"\), 'x'\)/,
777
+ User.select { concat(upper(:name), 'x') }.to_sql)
778
+ assert_sql(/WHERE MOD\("users"."age", 7\) = 0/,
779
+ User.where { mod(:age, 7) == 0 }.to_sql)
780
+ end
781
+
782
+ # SQLite has no CHAR_LENGTH, GREATEST or LEAST, but LENGTH, MAX and MIN
783
+ # mean the same thing there.
784
+ def test_scalar_functions_spelled_differently_on_sqlite
785
+ expected = ADAPTER == 'sqlite3' ? %w[LENGTH MAX MIN] : %w[CHAR_LENGTH GREATEST LEAST]
786
+ assert_sql(/SELECT #{expected[0]}\("users"."name"\)/,
787
+ User.select { char_length(:name) }.to_sql)
788
+ assert_sql(/SELECT #{expected[1]}\("users"."age", 18\)/,
789
+ User.select { greatest(:age, 18) }.to_sql)
790
+ assert_sql(/SELECT #{expected[2]}\("users"."age", 99\)/,
791
+ User.select { least(:age, 99) }.to_sql)
792
+ end
793
+
794
+ def test_scalar_functions_run
795
+ User.delete_all
796
+ User.create!(name: '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))
799
+ assert_equal([60], User.select { greatest(:age, 18).as(:v) }.map(&:v))
800
+ end
801
+
802
+ # rand takes the name back from Kernel#rand, which would otherwise answer
803
+ # inside the block and never reach the database.
804
+ def test_rand
805
+ expected = ADAPTER == 'mysql2' ? 'RAND' : 'RANDOM'
806
+ assert_sql(/ORDER BY #{expected}\(\)/, User.order { rand }.to_sql)
807
+ end
808
+
809
+ # Where an adapter has no equivalent, the block raises instead of leaving
810
+ # the database to reject the SQL.
811
+ def test_unsupported_function_raises
812
+ if ADAPTER == 'postgresql'
813
+ assert_sql(/SELECT DATE_TRUNC\('day', "users"."name"\)/,
814
+ User.select { date_trunc('day', :name) }.to_sql)
815
+ else
816
+ e = assert_raises(NotImplementedError) { User.select { date_trunc('day', :name) } }
817
+ assert_match(/date_trunc/, e.message)
818
+ end
819
+ end
820
+
821
+ # MySQL's FORMAT is a different function that happens to share the name,
822
+ # and reads a printf template as the number zero rather than complaining,
823
+ # so the name carries the printf one and MySQL raises.
824
+ def test_format_is_printf_and_unsupported_on_mysql
825
+ if ADAPTER == 'mysql2'
826
+ assert_raises(NotImplementedError) { User.select { format('%s!', :name) } }
827
+ else
828
+ User.delete_all
829
+ User.create!(name: 'matz')
830
+ assert_equal(['matz!'], User.select { format('%s!', :name).as(:v) }.map(&:v))
831
+ end
832
+ end
833
+
834
+ # MySQL's own is still reachable, spelled as the different thing it is.
835
+ def test_mysql_format_through_fn
836
+ assert_sql(/SELECT format\(1234.5678, 2\)/,
837
+ User.select { fn(:format, 1234.5678, 2) }.to_sql)
838
+ end
839
+
840
+ def test_now_is_unsupported_on_sqlite
841
+ if ADAPTER == 'sqlite3'
842
+ assert_raises(NotImplementedError) { User.select { now } }
843
+ else
844
+ assert_sql(/SELECT NOW\(\)/, User.select { now }.to_sql)
845
+ end
846
+ end
847
+
848
+ # A name with no method of its own is still a NoMethodError, not a
849
+ # function call the database has to reject.
850
+ def test_unknown_function_is_a_no_method_error
851
+ assert_raises(NoMethodError) { User.select { uppr(:name) } }
852
+ end
853
+
854
+ def test_arithmetic_multiplication
855
+ assert_sql(/SELECT "users"."age" \* 2 AS dbl/,
856
+ User.select { (:age * 2).as(:dbl) }.to_sql)
857
+ end
858
+
859
+ # Ruby puts * above >, so the expression groups the way it reads.
860
+ def test_arithmetic_in_where_without_parentheses
861
+ assert_sql(/WHERE "users"."age" \* 2 > 100/,
862
+ User.where { :age * 2 > 100 }.to_sql)
863
+ end
864
+
865
+ def test_arithmetic_between_columns
866
+ assert_sql(/WHERE \("users"."age" \+ "users"."id"\) \/ 2 <= 30/,
867
+ User.where { (:age + :id) / 2 <= 30 }.to_sql)
868
+ end
869
+
870
+ def test_arithmetic_inside_aggregate
871
+ assert_sql(/SELECT SUM\("users"."age" \* 2\)/,
872
+ User.select { sum(:age * 2) }.to_sql)
873
+ end
874
+
875
+ def test_aggregate_on_arithmetic
876
+ assert_sql(/SELECT SUM\(\("users"."age" \+ 1\)\) AS s/,
877
+ User.select { (:age + 1).sum.as(:s) }.to_sql)
878
+ end
879
+
880
+ # Arel groups + and - but not * and /, which is how SQL precedence works
881
+ # out anyway.
882
+ def test_arithmetic_on_qualified_column
883
+ assert_sql(/SELECT \("users"."age" - 1\)/,
884
+ User.select { :users[:age] - 1 }.to_sql)
885
+ end
886
+
485
887
  def test_coalesce_function_with_literal
486
888
  assert_sql(/SELECT COALESCE\("users"."name", 'unknown'\)/,
487
889
  User.select { coalesce(:name, 'unknown') }.to_sql)
@@ -572,6 +974,30 @@ class TestBlockSyntax < Minitest::Test
572
974
  User.order { [:age.desc, :name.asc] }.to_sql)
573
975
  end
574
976
 
977
+ def test_order_nulls_first
978
+ skip_without_nulls_ordering_syntax
979
+ assert_sql(/ORDER BY "users"."age" ASC NULLS FIRST/,
980
+ User.order { :age.asc.nulls_first }.to_sql)
981
+ end
982
+
983
+ def test_order_nulls_last
984
+ skip_without_nulls_ordering_syntax
985
+ assert_sql(/ORDER BY "users"."age" DESC NULLS LAST, "users"."name" ASC/,
986
+ User.order { [:age.desc.nulls_last, :name.asc] }.to_sql)
987
+ end
988
+
989
+ # The order itself is portable even where the syntax is not.
990
+ def test_order_nulls_execution
991
+ User.delete_all
992
+ User.create!(name: 'null_age', age: nil)
993
+ User.create!(name: 'young', age: 20)
994
+ User.create!(name: 'old', age: 60)
995
+ assert_equal(%w[null_age young old],
996
+ User.order { :age.asc.nulls_first }.pluck(:name))
997
+ assert_equal(%w[young old null_age],
998
+ User.order { :age.asc.nulls_last }.pluck(:name))
999
+ end
1000
+
575
1001
  def test_order_qualified_column
576
1002
  assert_sql(/ORDER BY "users"."name" DESC/,
577
1003
  User.order { :users[:name].desc }.to_sql)
data/test/test_helper.rb CHANGED
@@ -66,6 +66,12 @@ module SqlAssertions
66
66
  skip "#{ADAPTER} has no array columns" unless ADAPTER == 'postgresql'
67
67
  end
68
68
 
69
+ # MySQL has no NULLS FIRST/LAST; Arel emulates it with a leading IS NULL
70
+ # ordering, so only the resulting order is portable, not the SQL.
71
+ def skip_without_nulls_ordering_syntax
72
+ skip "#{ADAPTER} emulates NULLS FIRST/LAST" if ADAPTER == 'mysql2'
73
+ end
74
+
69
75
  def regexp_operator
70
76
  Regexp.escape(REGEXP_OPERATORS.fetch(ADAPTER).first)
71
77
  end
@@ -104,11 +110,16 @@ class Post < ActiveRecord::Base
104
110
  belongs_to :author
105
111
  end
106
112
 
113
+ # Self-referencing, for the recursive CTE tests.
114
+ class Node < ActiveRecord::Base
115
+ end
116
+
107
117
  class CreateAllTables < ActiveRecord::Migration[8.1]
108
118
  def up
109
119
  drop_table(:users, if_exists: true)
110
120
  drop_table(:authors, if_exists: true)
111
121
  drop_table(:posts, if_exists: true)
122
+ drop_table(:nodes, if_exists: true)
112
123
  create_table(:users) do |t|
113
124
  t.string :name
114
125
  t.integer :age
@@ -116,6 +127,7 @@ class CreateAllTables < ActiveRecord::Migration[8.1]
116
127
  end
117
128
  create_table(:authors) {|t| t.string :name}
118
129
  create_table(:posts) {|t| t.string :title; t.integer :author_id}
130
+ create_table(:nodes) {|t| t.string :name; t.integer :parent_id}
119
131
  end
120
132
  end
121
133
  ActiveRecord::Migration.verbose = false
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.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -88,8 +88,14 @@ files:
88
88
  - README.md
89
89
  - Rakefile
90
90
  - activerecord-refined.gemspec
91
+ - benchmark/query_building.rb
91
92
  - examples/aggregations.rb
92
93
  - examples/complex_joins.rb
94
+ - examples/ctes.rb
95
+ - examples/expressions.rb
96
+ - examples/postgresql.rb
97
+ - examples/predicates.rb
98
+ - examples/subqueries.rb
93
99
  - lib/active_record/refined.rb
94
100
  - lib/active_record/refined/ast.rb
95
101
  - lib/activerecord-refined.rb