activerecord-refined 0.6.1 → 0.8.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.
@@ -697,7 +697,7 @@ class TestBlockSyntax < Minitest::Test
697
697
  end
698
698
 
699
699
  # A relation without an explicit select list selects its primary key, the
700
- # same way ActiveRecord's own where(id: relation) does.
700
+ # same way Active Record's own where(id: relation) does.
701
701
  def test_in_subquery_selects_primary_key_by_default
702
702
  assert_sql(/WHERE "authors"."id" IN \(SELECT "posts"."id" FROM "posts"\)/,
703
703
  Author.where { :id.in?(Post.all) }.to_sql)
@@ -878,6 +878,80 @@ class TestBlockSyntax < Minitest::Test
878
878
  assert_raises(ArgumentError) { Author.left_outer_joins(:posts, as: :p) }
879
879
  end
880
880
 
881
+ # The other two outer joins, which Active Record has no method for. MySQL
882
+ # has no FULL OUTER JOIN either, and says so before the SQL is built.
883
+ def test_right_outer_joins_with_block
884
+ assert_sql(/RIGHT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
885
+ Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql)
886
+ end
887
+
888
+ def test_full_outer_joins_with_block
889
+ skip_without_full_outer_joins
890
+ assert_sql(/FULL OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
891
+ Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql)
892
+ end
893
+
894
+ def test_full_outer_joins_says_where_it_cannot_go
895
+ skip "#{ADAPTER} has FULL OUTER JOIN" unless ADAPTER == 'mysql2'
896
+ e = assert_raises(NotImplementedError) do
897
+ Author.full_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
898
+ end
899
+ assert_match(/no equivalent on MySQL/, e.message)
900
+ end
901
+
902
+ def test_right_outer_joins_with_alias
903
+ assert_sql(
904
+ /RIGHT OUTER JOIN "authors" (?:AS )?"mentors" ON "mentors"."id" = "authors"."id"/,
905
+ Author.right_outer_joins(:authors, as: :mentors) { :mentors[:id] == :authors[:id] }.to_sql)
906
+ end
907
+
908
+ # An association is what joins and left_outer_joins read; there is nothing
909
+ # for these two to read one as.
910
+ def test_the_other_outer_joins_need_a_block
911
+ e = assert_raises(ArgumentError) { Author.right_outer_joins(:posts) }
912
+ assert_match(/takes a table and the block/, e.message)
913
+ unless ADAPTER == 'mysql2'
914
+ assert_raises(ArgumentError) { Author.full_outer_joins(:posts) }
915
+ end
916
+ end
917
+
918
+ # CROSS JOIN: every row against every row, so there is no condition to give.
919
+ def test_cross_joins
920
+ assert_sql(/FROM "authors" CROSS JOIN "posts"/, Author.cross_joins(:posts).to_sql)
921
+ end
922
+
923
+ def test_cross_joins_with_alias
924
+ assert_sql(/CROSS JOIN "authors" "others"/,
925
+ Author.cross_joins(:authors, as: :others).to_sql)
926
+ end
927
+
928
+ def test_cross_joins_takes_no_block
929
+ e = assert_raises(ArgumentError) { Author.cross_joins(:posts) { :id == 1 } }
930
+ assert_match(/no condition/, e.message)
931
+ end
932
+
933
+ def test_cross_joins_execution
934
+ Author.delete_all
935
+ Post.delete_all
936
+ Author.create!(name: 'a')
937
+ Author.create!(name: 'b')
938
+ Post.create!(title: 'one')
939
+ Post.create!(title: 'two')
940
+ Post.create!(title: 'three')
941
+ assert_equal(6, Author.cross_joins(:posts).count)
942
+ end
943
+
944
+ def test_right_outer_joins_execution
945
+ Author.delete_all
946
+ Post.delete_all
947
+ author = Author.create!(name: 'a')
948
+ Post.create!(title: 'hers', author_id: author.id)
949
+ Post.create!(title: 'nobody\'s', author_id: nil)
950
+ assert_equal(['a', nil],
951
+ Author.right_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.
952
+ order { :posts[:title] }.pluck(:'authors.name'))
953
+ end
954
+
881
955
  def test_joins_without_alias_still_delegates
882
956
  assert_sql(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/,
883
957
  Author.joins(:posts).to_sql)
@@ -892,7 +966,7 @@ class TestBlockSyntax < Minitest::Test
892
966
  pluck(:name).sort)
893
967
  end
894
968
 
895
- # ActiveRecord's from only takes a table name as a string.
969
+ # Active Record's from only takes a table name as a string.
896
970
  def test_from_symbol
897
971
  assert_sql(/FROM "tree"/, Node.from(:tree).to_sql)
898
972
  end
@@ -1817,48 +1891,115 @@ class TestBlockSyntax < Minitest::Test
1817
1891
  Doc.create!(name: 'two', meta: json_document({ 'n' => 9 }))
1818
1892
  end
1819
1893
 
1820
- def test_dig_a_key
1894
+ def test_dig_text_a_key
1821
1895
  seed_docs
1822
- assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig(:n).as(:v) }.map(&:v))
1896
+ assert_equal(%w[5 9], Doc.order(:name).select { :meta.dig_text(:n).as(:v) }.map(&:v))
1823
1897
  end
1824
1898
 
1825
- def test_dig_a_path
1899
+ def test_dig_text_a_path
1826
1900
  seed_docs
1827
1901
  assert_equal(['deep', nil],
1828
- Doc.order(:name).select { :meta.dig(:a, :b).as(:v) }.map(&:v))
1902
+ Doc.order(:name).select { :meta.dig_text(:a, :b).as(:v) }.map(&:v))
1829
1903
  end
1830
1904
 
1831
- def test_dig_an_array_index
1905
+ def test_dig_text_an_array_index
1832
1906
  seed_docs
1833
1907
  assert_equal(['x', nil],
1834
- Doc.order(:name).select { :meta.dig(:tags, 0).as(:v) }.map(&:v))
1908
+ Doc.order(:name).select { :meta.dig_text(:tags, 0).as(:v) }.map(&:v))
1835
1909
  end
1836
1910
 
1837
1911
  # A key that is not a plain name travels as itself rather than being refused.
1838
- def test_dig_a_key_that_needs_quoting
1912
+ def test_dig_text_a_key_that_needs_quoting
1839
1913
  seed_docs
1840
1914
  assert_equal(['1', nil],
1841
- Doc.order(:name).select { :meta.dig(:'odd key').as(:v) }.map(&:v))
1915
+ Doc.order(:name).select { :meta.dig_text(:'odd key').as(:v) }.map(&:v))
1842
1916
  end
1843
1917
 
1844
- # dig gives text on every adapter -- SQLite's ->> would otherwise give the
1918
+ # dig_text gives text on every adapter -- SQLite's ->> would otherwise give the
1845
1919
  # value with its type -- so a number is compared through a cast.
1846
- def test_dig_is_text_everywhere
1920
+ def test_dig_text_is_text_everywhere
1847
1921
  seed_docs
1848
- assert_equal(['one'], Doc.where { :meta.dig(:n) == '5' }.pluck(:name))
1922
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1849
1923
  type = integer_type
1850
- assert_equal(['two'], Doc.where { cast(:meta.dig(:n), type) > 6 }.pluck(:name))
1924
+ assert_equal(['two'], Doc.where { cast(:meta.dig_text(:n), type) > 6 }.pluck(:name))
1851
1925
  end
1852
1926
 
1853
- def test_dig_json_keeps_the_json
1927
+ def test_dig_keeps_the_json
1854
1928
  seed_docs
1855
- value = Doc.where { :name == 'one' }.select { :meta.dig_json(:tags).as(:v) }.first.v
1929
+ value = Doc.where { :name == 'one' }.select { :meta.dig(:tags).as(:v) }.first.v
1856
1930
  assert_equal(%w[x y], value.is_a?(String) ? JSON.parse(value) : value)
1857
1931
  end
1858
1932
 
1859
- def test_dig_from_a_qualified_column
1933
+ # What text compared with a number means is a question the three adapters
1934
+ # answer three ways -- `dig_text(:n) == 5` is true on SQLite, an error on
1935
+ # PostgreSQL and true on MySQL, and `dig_text(:flag) == true` is true, an error
1936
+ # and false -- so the comparison is refused rather than left to them.
1937
+ def test_dig_text_refuses_a_comparison_with_anything_but_text
1938
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) == 5 } }
1939
+ assert_match(/cast/, e.message)
1940
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) != 5 } }
1941
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n) > 6 } }
1942
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:flag) == true } }
1943
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).in?([1, 2]) } }
1944
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:n).between?(1, 9) } }
1945
+ end
1946
+
1947
+ # A string is what a dug value compares to; so is anything the block built
1948
+ # rather than wrote as a literal, since that is nobody's guess to make.
1949
+ def test_dig_text_compares_with_text_and_with_expressions
1860
1950
  seed_docs
1861
- assert_equal(['one'], Doc.where { :docs[:meta].dig(:a, :b) == 'deep' }.pluck(:name))
1951
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == '5' }.pluck(:name))
1952
+ assert_equal([], Doc.where { :meta.dig_text(:n) == :name }.pluck(:name))
1953
+ assert_equal(['one'], Doc.where { :meta.dig_text(:n) == upper('5') }.pluck(:name))
1954
+ type = integer_type
1955
+ assert_equal(['one'], Doc.where { cast(:meta.dig_text(:n), type) == 5 }.pluck(:name))
1956
+ end
1957
+
1958
+ # The JSON for a string carries its quotes, so the same comparison is
1959
+ # refused the other way about: false on SQLite, an error on PostgreSQL and
1960
+ # true on MySQL.
1961
+ def test_dig_refuses_a_comparison_with_a_ruby_value
1962
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig(:a) == 'deep' } }
1963
+ assert_match(/dig_text gives the value/, e.message)
1964
+ assert_raises(ArgumentError) { Doc.where { :meta.dig(:n) == 5 } }
1965
+ end
1966
+
1967
+ # What dig gives is a document, so the JSON operations read it: the
1968
+ # same question asked of a part rather than of the whole.
1969
+ def test_the_json_operations_read_what_dig_kept
1970
+ seed_docs
1971
+ assert_equal(['one'], Doc.where { :meta.dig(:a).key?(:b) }.pluck(:name))
1972
+ assert_equal(%w[one two], Doc.where { :meta.dig(:n).not_null? }.order(:name).pluck(:name))
1973
+ assert_equal(['one'],
1974
+ Doc.where { :meta.dig(:a).dig_text(:b) == 'deep' }.pluck(:name))
1975
+ value = Doc.where { :name == 'one' }.
1976
+ select { :meta.dig(:a).bury(:b, 'x').as(:v) }.first.v
1977
+ assert_equal('x', (value.is_a?(String) ? JSON.parse(value) : value)['b'])
1978
+ end
1979
+
1980
+ def test_containment_reads_what_dig_kept
1981
+ skip_without_json_containment
1982
+ seed_docs
1983
+ assert_equal(['one'], Doc.where { :meta.dig(:tags).contains?(['x']) }.pluck(:name))
1984
+ assert_equal([], Doc.where { :meta.dig(:tags).contains?(['z']) }.pluck(:name))
1985
+ end
1986
+
1987
+ # Reading text back as a document is where the adapters part company:
1988
+ # SQLite parses it, MySQL takes it as written, PostgreSQL has no such
1989
+ # function for text at all.
1990
+ def test_the_json_operations_are_refused_on_a_dug_value
1991
+ e = assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).key?(:b) } }
1992
+ assert_match(/dig keeps it/, e.message)
1993
+ assert_raises(ArgumentError) { Doc.where { :meta.dig_text(:a).contains?(b: 1) } }
1994
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig_text(:b) } }
1995
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).dig(:b) } }
1996
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).bury(:b, 'x') } }
1997
+ assert_raises(ArgumentError) { Doc.select { :meta.dig_text(:a).except(:b) } }
1998
+ end
1999
+
2000
+ def test_dig_text_from_a_qualified_column
2001
+ seed_docs
2002
+ assert_equal(['one'], Doc.where { :docs[:meta].dig_text(:a, :b) == 'deep' }.pluck(:name))
1862
2003
  end
1863
2004
 
1864
2005
  def test_key
@@ -1881,7 +2022,7 @@ class TestBlockSyntax < Minitest::Test
1881
2022
 
1882
2023
  def test_dig_needs_a_path
1883
2024
  assert_raises(ArgumentError) { Doc.select { :meta.dig } }
1884
- e = assert_raises(ArgumentError) { Doc.select { :meta.dig(1.5) } }
2025
+ e = assert_raises(ArgumentError) { Doc.select { :meta.dig_text(1.5) } }
1885
2026
  assert_match(/key or an array index/, e.message)
1886
2027
  end
1887
2028
 
@@ -2012,7 +2153,7 @@ class TestBlockSyntax < Minitest::Test
2012
2153
  def test_lateral_join
2013
2154
  skip_without_lateral
2014
2155
  seed_for_lateral
2015
- rows = Author.joins(top_post, as: :top, lateral: true).
2156
+ rows = Author.joins(top_post.lateral, as: :top).
2016
2157
  select { [:name, :top[:title].as(:v)] }.map {|r| [r.name, r.v] }
2017
2158
  assert_equal([['writes', 'b']], rows)
2018
2159
  end
@@ -2021,7 +2162,7 @@ class TestBlockSyntax < Minitest::Test
2021
2162
  def test_left_outer_lateral_join
2022
2163
  skip_without_lateral
2023
2164
  seed_for_lateral
2024
- rows = Author.left_outer_joins(top_post, as: :top, lateral: true).
2165
+ rows = Author.left_outer_joins(top_post.lateral, as: :top).
2025
2166
  select { [:name, :top[:title].as(:v)] }.order { :name }.map {|r| [r.name, r.v] }
2026
2167
  assert_equal([['does not', nil], ['writes', 'b']], rows)
2027
2168
  end
@@ -2031,22 +2172,28 @@ class TestBlockSyntax < Minitest::Test
2031
2172
  def test_lateral_join_takes_an_on_clause
2032
2173
  skip_without_lateral
2033
2174
  seed_for_lateral
2034
- assert_equal(0, Author.joins(top_post, as: :top, lateral: true) {
2175
+ assert_equal(0, Author.joins(top_post.lateral, as: :top) {
2035
2176
  :top[:title] == 'nothing'
2036
2177
  }.count)
2037
- assert_sql(/ON TRUE/, Author.joins(top_post, as: :top, lateral: true).to_sql)
2178
+ assert_sql(/ON TRUE/, Author.joins(top_post.lateral, as: :top).to_sql)
2038
2179
  end
2039
2180
 
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) }
2181
+ def test_lateral_join_needs_the_mark_and_a_name
2182
+ e = assert_raises(ArgumentError) { Author.joins(top_post, as: :top) }
2183
+ assert_match(/mark it/, e.message)
2184
+ e = assert_raises(ArgumentError) { Author.joins(top_post.lateral) }
2044
2185
  assert_match(/needs a name/, e.message)
2045
2186
  end
2046
2187
 
2188
+ def test_lateral_spawns
2189
+ relation = top_post
2190
+ assert(relation.lateral.lateral_value)
2191
+ refute(relation.lateral_value)
2192
+ end
2193
+
2047
2194
  def test_lateral_join_says_where_it_cannot_go
2048
2195
  skip 'this one has LATERAL' if ADAPTER == 'postgresql' || (ADAPTER == 'mysql2' && !mariadb?)
2049
- e = assert_raises(NotImplementedError) { Author.joins(top_post, as: :top, lateral: true) }
2196
+ e = assert_raises(NotImplementedError) { Author.joins(top_post.lateral, as: :top) }
2050
2197
  assert_match(/lateral join has no equivalent/, e.message)
2051
2198
  end
2052
2199
 
@@ -2136,7 +2283,7 @@ class TestBlockSyntax < Minitest::Test
2136
2283
 
2137
2284
  # The value can be read out of the document it is going into.
2138
2285
  def test_bury_an_expression
2139
- assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig(:n)) } }['copy'].to_s)
2286
+ assert_equal('5', buried { { meta: :meta.bury(:copy, :meta.dig_text(:n)) } }['copy'].to_s)
2140
2287
  end
2141
2288
 
2142
2289
  # It is an expression, so it does not have to be written anywhere.
@@ -2151,6 +2298,44 @@ class TestBlockSyntax < Minitest::Test
2151
2298
  assert_raises(ArgumentError) { Doc.select { :meta.bury(1.5, 'v') } }
2152
2299
  end
2153
2300
 
2301
+ # except takes keys out, by the name of what Hash does. PostgreSQL
2302
+ # subtracts them where the others remove a path apiece, and what comes back
2303
+ # is the same document on all three.
2304
+ def test_except_a_key
2305
+ assert_equal({ 'a' => { 'b' => 'deep' }, 'tags' => %w[x y], 'odd key' => 1 },
2306
+ buried { { meta: :meta.except(:n) } })
2307
+ end
2308
+
2309
+ def test_except_several_keys
2310
+ assert_equal({ 'a' => { 'b' => 'deep' } },
2311
+ buried { { meta: :meta.except(:n, :tags, :'odd key') } })
2312
+ end
2313
+
2314
+ # A key that is not there is not an error, as Hash#except has none for it.
2315
+ def test_except_a_key_that_is_not_there
2316
+ assert_equal(5, buried { { meta: :meta.except(:nothing) } }['n'])
2317
+ end
2318
+
2319
+ # The document a bury gives back is one to take keys out of.
2320
+ def test_except_after_bury
2321
+ document = buried { { meta: :meta.bury(:fresh, 9).except(:n) } }
2322
+ assert_equal(9, document['fresh'])
2323
+ assert_nil(document['n'])
2324
+ end
2325
+
2326
+ def test_except_in_a_select
2327
+ seed_docs
2328
+ value = Doc.where { :name == 'one' }.select { :meta.except(:n).as(:v) }.first.v
2329
+ assert_nil((value.is_a?(String) ? JSON.parse(value) : value)['n'])
2330
+ end
2331
+
2332
+ # An index is not what the name says anywhere, and a path is bury's.
2333
+ def test_except_takes_keys
2334
+ assert_raises(ArgumentError) { Doc.select { :meta.except } }
2335
+ e = assert_raises(ArgumentError) { Doc.select { :meta.except(0) } }
2336
+ assert_match(/keys of the document/, e.message)
2337
+ end
2338
+
2154
2339
  def test_default_where_syntax
2155
2340
  assert_sql(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
2156
2341
  User.where(name: 'Ruby', age: 19).to_sql)
data/test/test_helper.rb CHANGED
@@ -74,7 +74,12 @@ module SqlAssertions
74
74
  skip "#{ADAPTER} has no JSON containment" if ADAPTER == 'sqlite3'
75
75
  end
76
76
 
77
- # MariaDB's json is a checked longtext, which ActiveRecord sees as a string
77
+ # MySQL is the one without a FULL OUTER JOIN, and so is MariaDB.
78
+ def skip_without_full_outer_joins
79
+ skip "#{ADAPTER} has no full outer join" if ADAPTER == 'mysql2'
80
+ end
81
+
82
+ # MariaDB's json is a checked longtext, which Active Record sees as a string
78
83
  # and does not serialise a hash into -- what would go in is Ruby's inspect,
79
84
  # which the check refuses. MySQL's json is a type of its own and takes the
80
85
  # hash; handing that one a string would store the document as a JSON string
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.6.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -73,7 +73,23 @@ dependencies:
73
73
  - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: "0"
76
- description: Adding clean and powerful query syntax on AR using refinements
76
+ - !ruby/object:Gem::Dependency
77
+ name: bump
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ -
81
+ - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ -
89
+ - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: "0"
92
+ description: Adding clean and powerful query syntax on Active Record using refinements
77
93
  email:
78
94
  - shugo@ruby-lang.org
79
95
  executables: []
@@ -127,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
143
  requirements: []
128
144
  rubygems_version: 4.1.0.dev
129
145
  specification_version: 4
130
- summary: "ActiveRecord + Ruby 4.1 Proc#refined"
146
+ summary: Write Active Record queries as Ruby expressions
131
147
  test_files:
132
148
  - test/test_block_syntax.rb
133
149
  - test/test_helper.rb