inquery 1.0.11 → 1.1.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +1 -1
  3. data/.github/workflows/ruby.yml +24 -8
  4. data/.gitignore +2 -2
  5. data/.rubocop.yml +4 -0
  6. data/Appraisals +24 -4
  7. data/CHANGELOG.md +38 -0
  8. data/Gemfile +17 -1
  9. data/Gemfile.lock +125 -0
  10. data/LICENSE +1 -1
  11. data/MIGRATION.md +260 -0
  12. data/README.md +14 -8
  13. data/RUBY_VERSION +1 -1
  14. data/Rakefile +4 -11
  15. data/VERSION +1 -1
  16. data/doc/Inquery/Exceptions/Base.html +4 -4
  17. data/doc/Inquery/Exceptions/InvalidRelation.html +4 -4
  18. data/doc/Inquery/Exceptions/UnknownCallSignature.html +4 -4
  19. data/doc/Inquery/Exceptions.html +4 -4
  20. data/doc/Inquery/MethodAccessibleHash.html +431 -0
  21. data/doc/Inquery/Mixins/RawSqlUtils.html +17 -4
  22. data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +8 -7
  23. data/doc/Inquery/Mixins/RelationValidation.html +9 -8
  24. data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +4 -4
  25. data/doc/Inquery/Mixins/SchemaValidation.html +4 -4
  26. data/doc/Inquery/Mixins.html +4 -4
  27. data/doc/Inquery/Query/Chainable.html +207 -90
  28. data/doc/Inquery/Query.html +401 -73
  29. data/doc/Inquery.html +12 -9
  30. data/doc/_index.html +12 -5
  31. data/doc/class_list.html +6 -3
  32. data/doc/css/full_list.css +3 -3
  33. data/doc/css/style.css +6 -0
  34. data/doc/file.README.html +102 -16
  35. data/doc/file_list.html +5 -2
  36. data/doc/frames.html +10 -5
  37. data/doc/index.html +102 -16
  38. data/doc/js/app.js +294 -264
  39. data/doc/js/full_list.js +30 -4
  40. data/doc/method_list.html +48 -21
  41. data/doc/top-level-namespace.html +4 -4
  42. data/gemfiles/rails_5.2.gemfile +1 -0
  43. data/gemfiles/rails_6.0.gemfile +1 -0
  44. data/gemfiles/rails_6.1.gemfile +1 -0
  45. data/gemfiles/rails_7.0.gemfile +1 -0
  46. data/gemfiles/{rails_5.1.gemfile → rails_7.1.gemfile} +2 -1
  47. data/gemfiles/rails_7.2.gemfile +8 -0
  48. data/gemfiles/rails_8.0.gemfile +8 -0
  49. data/gemfiles/rails_8.1.gemfile +8 -0
  50. data/inquery.gemspec +11 -34
  51. data/lib/inquery/method_accessible_hash.rb +45 -0
  52. data/lib/inquery/mixins/raw_sql_utils.rb +32 -6
  53. data/lib/inquery/mixins/relation_validation.rb +1 -1
  54. data/lib/inquery/query/chainable.rb +69 -27
  55. data/lib/inquery/query.rb +67 -14
  56. data/lib/inquery.rb +2 -0
  57. data/test/inquery/error_handling_test.rb +117 -0
  58. data/test/inquery/method_accessible_hash_test.rb +85 -0
  59. data/test/inquery/mixins/raw_sql_utils_test.rb +67 -0
  60. data/test/inquery/query/chainable_test.rb +78 -0
  61. data/test/inquery/query_test.rb +86 -0
  62. data/test/test_helper.rb +11 -0
  63. metadata +30 -129
  64. data/.yardopts +0 -1
@@ -1,25 +1,66 @@
1
1
  module Inquery
2
+ # Chainable query class for queries that input and output ActiveRecord relations.
3
+ #
4
+ # Use this class when you want to build queries that can be chained together
5
+ # or used as ActiveRecord scopes. The query receives a relation, transforms
6
+ # it, and returns a new relation.
7
+ #
8
+ # Example:
9
+ # class FetchActive < Inquery::Query::Chainable
10
+ # relation class: 'User'
11
+ #
12
+ # def call
13
+ # relation.where(active: true)
14
+ # end
15
+ # end
16
+ #
17
+ # User.all.then { |rel| FetchActive.run(rel) }
18
+ # # Or as a scope:
19
+ # class User < ActiveRecord::Base
20
+ # scope :active, FetchActive
21
+ # end
2
22
  class Query::Chainable < Query
3
23
  include Inquery::Mixins::RelationValidation
4
24
 
5
- # Allows using this class as an AR scope.
25
+ # Instantiates the query and executes it, allowing use as an AR scope.
26
+ #
27
+ # This enables chainable queries to be used directly as ActiveRecord scopes:
28
+ # scope :active, FetchActive
29
+ #
30
+ # @param args [Array] Arguments passed to initialize (relation and/or params)
31
+ # @return [ActiveRecord::Relation] The transformed relation
6
32
  def self.call(*args)
7
33
  return new(*args).call
8
34
  end
9
35
 
10
- def call(*args)
11
- fail args.inspect
12
- end
13
-
36
+ # The input ActiveRecord relation that will be transformed by this query.
37
+ #
38
+ # @return [ActiveRecord::Relation]
14
39
  attr_reader :relation
15
40
 
41
+ # Initializes a chainable query with a relation and optional parameters.
42
+ #
43
+ # Supports multiple call signatures:
44
+ # new() - Uses default relation from 'relation' DSL
45
+ # new(relation) - Uses provided relation
46
+ # new(params) - Uses default relation with params
47
+ # new(relation, params) - Uses provided relation and params
48
+ #
49
+ # @param args [Array] Variable arguments for relation and/or params
50
+ # @raise [Inquery::Exceptions::UnknownCallSignature] for invalid arguments
51
+ # @raise [Inquery::Exceptions::InvalidRelation] if relation validation fails
16
52
  def initialize(*args)
17
53
  relation, params = parse_init_args(*args)
18
54
  @relation = validate_relation!(relation)
19
55
  super(params)
20
56
  end
21
57
 
22
- # Override the connection method to (re-)use the connection of the relation
58
+ # Returns the database connection from the relation.
59
+ #
60
+ # This ensures that the query uses the same connection as the input
61
+ # relation, which is important for connection pooling and transactions.
62
+ #
63
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
23
64
  def connection
24
65
  @relation.connection
25
66
  end
@@ -27,32 +68,33 @@ module Inquery
27
68
  private
28
69
 
29
70
  def parse_init_args(*args)
30
- # new(relation)
31
- if (args[0].is_a?(ActiveRecord::Relation) || args[0].class < ActiveRecord::Base) && args[1].nil?
32
- relation = args[0]
33
- params = {}
71
+ first_arg = args[0]
72
+ second_arg = args[1]
73
+
74
+ # new() - no arguments
75
+ return [nil, {}] if args.empty?
34
76
 
35
- # new(params)
36
- elsif args[0].is_a?(Hash) && args[1].nil?
37
- relation = nil
38
- params = args[0]
77
+ # new(relation) or new(params)
78
+ if second_arg.nil?
79
+ if relation_like?(first_arg)
80
+ return [first_arg, {}]
81
+ elsif first_arg.is_a?(Hash)
82
+ return [nil, first_arg]
83
+ end
84
+ end
39
85
 
40
86
  # new(relation, params)
41
- elsif (args[0].is_a?(ActiveRecord::Relation) || args[0].class < ActiveRecord::Base) && args[1].is_a?(Hash)
42
- relation = args[0]
43
- params = args[1]
44
-
45
- # new()
46
- elsif args.empty?
47
- relation = nil
48
- params = {}
49
-
50
- # Unknown
51
- else
52
- fail Inquery::Exceptions::UnknownCallSignature, "Unknown call signature for the query constructor: #{args.collect(&:class)}."
87
+ if relation_like?(first_arg) && second_arg.is_a?(Hash)
88
+ return [first_arg, second_arg]
53
89
  end
54
90
 
55
- return relation, params
91
+ # Unknown signature
92
+ fail Inquery::Exceptions::UnknownCallSignature,
93
+ "Unknown call signature for the query constructor: #{args.collect(&:class)}."
94
+ end
95
+
96
+ def relation_like?(obj)
97
+ obj.is_a?(ActiveRecord::Relation) || (obj.class < ActiveRecord::Base)
56
98
  end
57
99
  end
58
100
  end
data/lib/inquery/query.rb CHANGED
@@ -1,24 +1,54 @@
1
1
  module Inquery
2
+ # Base query class that encapsulates database queries in reusable classes.
3
+ #
4
+ # Subclasses must implement the 'call' method to define the query logic.
5
+ # Optionally, override 'process' to transform the query results.
6
+ #
7
+ # Example:
8
+ # class FetchActiveUsers < Inquery::Query
9
+ # def call
10
+ # User.where(active: true)
11
+ # end
12
+ # end
13
+ #
14
+ # FetchActiveUsers.run # => Returns active users
2
15
  class Query
3
16
  include Mixins::SchemaValidation
4
17
  include Mixins::RawSqlUtils
5
18
 
6
19
  attr_reader :params
7
20
 
8
- # Instantiates the query class using the given arguments
9
- # and runs `call` and `process` on it.
21
+ # Instantiates the query class with the given params and executes both
22
+ # 'call' and 'process' methods.
23
+ #
24
+ # This is the primary way to execute queries. It runs the query logic
25
+ # defined in 'call' and then passes the results through 'process' for
26
+ # any post-processing.
27
+ #
28
+ # @param args [Hash] Parameters to pass to the query
29
+ # @return [Object] The processed query results
30
+ # @raise [Schemacop::Exceptions::ValidationError] if params don't match schema
10
31
  def self.run(*args)
11
32
  new(*args).run
12
33
  end
13
34
 
14
- # Instantiates the query class using the given arguments
15
- # and runs `call` on it.
35
+ # Instantiates the query class with the given params and executes only
36
+ # the 'call' method, skipping post-processing.
37
+ #
38
+ # @param args [Hash] Parameters to pass to the query
39
+ # @return [Object] The raw query results
40
+ # @raise [Schemacop::Exceptions::ValidationError] if params don't match schema
16
41
  def self.call(*args)
17
42
  new(*args).call
18
43
  end
19
44
 
20
- # Instantiates the query class and validates the given params hash (if there
21
- # was a validation schema specified).
45
+ # Initializes a new query instance with the given parameters.
46
+ #
47
+ # If a schema is defined using 'schema' or 'schema3', the params will be
48
+ # validated against it before the query is executed.
49
+ #
50
+ # @param params [Hash] Parameters for the query
51
+ # @raise [Schemacop::Exceptions::ValidationError] if params don't match schema
22
52
  def initialize(params = {})
23
53
  @params = params
24
54
 
@@ -27,29 +57,52 @@ module Inquery
27
57
  end
28
58
  end
29
59
 
30
- # Runs both `call` and `process`.
60
+ # Executes the query by calling 'call' and then 'process'.
61
+ #
62
+ # @return [Object] The processed query results
31
63
  def run
32
64
  process(call)
33
65
  end
34
66
 
35
- # Override this method to perform the actual query.
67
+ # Override this method in subclasses to define the query logic.
68
+ #
69
+ # @return [Object] Query results (typically an ActiveRecord::Relation)
70
+ # @raise [NotImplementedError] if not overridden in subclass
36
71
  def call
37
72
  fail NotImplementedError
38
73
  end
39
74
 
40
- # Override this method to perform an optional result postprocessing.
75
+ # Override this method in subclasses to transform the query results.
76
+ #
77
+ # By default, returns the results unchanged. Common uses include
78
+ # converting to JSON, counting records, or extracting specific fields.
79
+ #
80
+ # @param results [Object] The results from the 'call' method
81
+ # @return [Object] The processed results
41
82
  def process(results)
42
83
  results
43
84
  end
44
85
 
45
- # Returns a copy of the query's params, wrapped in an OpenStruct object for
46
- # easyer access.
86
+ # Returns the query params wrapped in a MethodAccessibleHash for
87
+ # convenient access using dot notation.
88
+ #
89
+ # Example:
90
+ # schema3 { str! :name }
91
+ # def call
92
+ # User.where(name: osparams.name) # Access via dot notation
93
+ # end
94
+ #
95
+ # @return [MethodAccessibleHash] Params with method access
47
96
  def osparams
48
- @osparams ||= OpenStruct.new(params)
97
+ @osparams ||= MethodAccessibleHash.new(params)
49
98
  end
50
99
 
51
- # Provides a connection to the database. May be overridden if a different
52
- # connection is desired. Defaults to `ActiveRecord::Base.connection`.
100
+ # Returns the database connection to use for this query.
101
+ #
102
+ # Override this method if you need to use a different connection than
103
+ # the default ActiveRecord connection.
104
+ #
105
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
53
106
  def connection
54
107
  ActiveRecord::Base.connection
55
108
  end
data/lib/inquery.rb CHANGED
@@ -9,9 +9,11 @@ module Inquery
9
9
  end
10
10
 
11
11
  require 'uri'
12
+ require 'logger'
12
13
  require 'schemacop'
13
14
 
14
15
  require 'inquery/exceptions'
16
+ require 'inquery/method_accessible_hash'
15
17
  require 'inquery/mixins/schema_validation'
16
18
  require 'inquery/mixins/relation_validation'
17
19
  require 'inquery/mixins/raw_sql_utils'
@@ -0,0 +1,117 @@
1
+ require 'test_helper'
2
+
3
+ module Inquery
4
+ class ErrorHandlingTest < Minitest::Test
5
+ include TestHelper
6
+
7
+ def setup
8
+ self.class.setup_db
9
+ self.class.setup_base_data
10
+ end
11
+
12
+ def test_query_without_call_method_raises_not_implemented_error
13
+ query_class = Class.new(Inquery::Query)
14
+
15
+ assert_raises(NotImplementedError) do
16
+ query_class.run
17
+ end
18
+ end
19
+
20
+ def test_chainable_query_without_call_method_raises_not_implemented_error
21
+ query_class = Class.new(Inquery::Query::Chainable) do
22
+ relation class: 'User'
23
+ end
24
+
25
+ assert_raises(NotImplementedError) do
26
+ query_class.run(User.all)
27
+ end
28
+ end
29
+
30
+ def test_invalid_relation_class_raises_error
31
+ assert_raises(NameError) do
32
+ Class.new(Inquery::Query::Chainable) do
33
+ relation class: 'NonExistentClass'
34
+
35
+ def call
36
+ relation
37
+ end
38
+ end.run
39
+ end
40
+ end
41
+
42
+ def test_invalid_relation_type_raises_error
43
+ query_class = Class.new(Inquery::Query::Chainable) do
44
+ relation class: 'User'
45
+
46
+ def call
47
+ relation
48
+ end
49
+ end
50
+
51
+ assert_raises(Inquery::Exceptions::UnknownCallSignature) do
52
+ query_class.run('not a relation')
53
+ end
54
+ end
55
+
56
+ def test_schema_validation_failure_with_missing_required_param
57
+ query_class = Class.new(Inquery::Query) do
58
+ schema3 do
59
+ str! :name
60
+ int! :age
61
+ end
62
+
63
+ def call
64
+ User.all
65
+ end
66
+ end
67
+
68
+ assert_raises(Schemacop::Exceptions::ValidationError) do
69
+ query_class.run(name: 'Alice')
70
+ end
71
+ end
72
+
73
+ def test_schema_validation_failure_with_wrong_type
74
+ query_class = Class.new(Inquery::Query) do
75
+ schema3 do
76
+ int! :age
77
+ end
78
+
79
+ def call
80
+ User.all
81
+ end
82
+ end
83
+
84
+ assert_raises(Schemacop::Exceptions::ValidationError) do
85
+ query_class.run(age: 'not a number')
86
+ end
87
+ end
88
+
89
+ def test_invalid_relation_field_count
90
+ query_class = Class.new(Inquery::Query::Chainable) do
91
+ relation fields: 1
92
+
93
+ def call
94
+ relation
95
+ end
96
+ end
97
+
98
+ # Passing a relation with more than 1 field should raise an error
99
+ assert_raises(Inquery::Exceptions::InvalidRelation) do
100
+ query_class.run(User.select(:id, :name))
101
+ end
102
+ end
103
+
104
+ def test_unknown_call_signature_raises_error
105
+ query_class = Class.new(Inquery::Query::Chainable) do
106
+ def call
107
+ relation
108
+ end
109
+ end
110
+
111
+ # Passing invalid arguments should raise UnknownCallSignature
112
+ assert_raises(Inquery::Exceptions::UnknownCallSignature) do
113
+ query_class.new(123, 456, 789)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,85 @@
1
+ require 'test_helper'
2
+
3
+ module Inquery
4
+ class MethodAccessibleHashTest < Minitest::Test
5
+ def test_new
6
+ hash = MethodAccessibleHash.new(name: 'John Smith', age: 70, 'pension' => 300)
7
+
8
+ assert_equal 'John Smith', hash.name
9
+ assert_equal 70, hash.age
10
+ assert_equal 300, hash.pension
11
+ end
12
+
13
+ def test_new_no_arguments
14
+ assert_equal '{}', MethodAccessibleHash.new.to_s
15
+ end
16
+
17
+ def test_to_h
18
+ assert MethodAccessibleHash.new.merge(foo: :bar).to_h.instance_of?(::Hash)
19
+ end
20
+
21
+ def test_getter
22
+ hash = MethodAccessibleHash.new.merge(foo: :bar, bar: :baz)
23
+ assert_equal :bar, hash.foo
24
+ assert_equal :baz, hash.bar
25
+ end
26
+
27
+ def test_setter
28
+ hash = MethodAccessibleHash.new.merge(foo: :bar)
29
+ assert_equal :bar, hash.foo
30
+
31
+ hash.foo = :x
32
+ hash.bar = :y
33
+
34
+ assert_equal :x, hash.foo
35
+ assert_equal :y, hash.bar
36
+ end
37
+
38
+ def test_reference
39
+ hash = MethodAccessibleHash.new
40
+ hash.foo = 42
41
+ assert_equal 42, hash[:foo]
42
+ assert_equal 42, hash.foo
43
+ end
44
+
45
+ def test_comparison
46
+ assert_equal({ foo: :bar }, MethodAccessibleHash.new(foo: :bar))
47
+ refute_equal({ foo: :bar, bar: :baz }, MethodAccessibleHash.new(foo: :bar))
48
+ end
49
+
50
+ def test_frozen
51
+ hash = MethodAccessibleHash.new(name: 'John Smith', age: 70, pension: 300).freeze
52
+
53
+ assert_equal 70, hash.age
54
+ assert_equal 300, hash.pension
55
+ assert_equal 'John Smith', hash.name
56
+
57
+ assert_raises RuntimeError do
58
+ hash.age = 42
59
+ end
60
+
61
+ assert_raises RuntimeError do
62
+ hash.foo = 42
63
+ end
64
+
65
+ clone = hash.clone
66
+ assert clone.frozen?
67
+ assert_equal 70, clone.age
68
+
69
+ assert_raises RuntimeError do
70
+ clone.age = 42
71
+ end
72
+
73
+ assert_raises RuntimeError do
74
+ clone.foo = 42
75
+ end
76
+
77
+ duplicate = hash.dup
78
+ refute duplicate.frozen?
79
+
80
+ assert_equal 70, duplicate.age
81
+ assert_equal 300, duplicate.pension
82
+ assert_equal 'John Smith', duplicate.name
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,67 @@
1
+ require 'test_helper'
2
+
3
+ module Inquery
4
+ module Mixins
5
+ class RawSqlUtilsTest < Minitest::Test
6
+ include TestHelper
7
+
8
+ def setup
9
+ self.class.setup_db
10
+ self.class.setup_base_data
11
+ end
12
+
13
+ # Test query class
14
+ class TestQuery < Inquery::Query
15
+ def call
16
+ # Intentionally empty
17
+ end
18
+ end
19
+
20
+ def test_san_sanitizes_sql_with_variables
21
+ query = TestQuery.new
22
+ result = query.san('SELECT * FROM users WHERE id = ?', 1)
23
+
24
+ assert_kind_of String, result
25
+ assert_includes result, 'SELECT * FROM users WHERE id = 1'
26
+ end
27
+
28
+ def test_san_handles_multiple_variables
29
+ query = TestQuery.new
30
+ result = query.san('SELECT * FROM users WHERE name = ? AND id = ?', 'Alice', 1)
31
+
32
+ assert_kind_of String, result
33
+ assert_includes result, 'SELECT * FROM users WHERE'
34
+ assert_includes result, 'Alice'
35
+ assert_includes result, '1'
36
+ end
37
+
38
+ def test_san_handles_special_characters
39
+ query = TestQuery.new
40
+ result = query.san('SELECT * FROM users WHERE name = ?', "O'Brien")
41
+
42
+ assert_kind_of String, result
43
+ # ActiveRecord escapes quotes using SQL standard (doubled quotes)
44
+ assert_includes result, "O''Brien"
45
+ end
46
+
47
+ def test_exec_query_executes_sql
48
+ query = TestQuery.new
49
+ sql = query.san('SELECT * FROM users WHERE id = ?', 1)
50
+ result = query.exec_query(sql)
51
+
52
+ assert_kind_of ActiveRecord::Result, result
53
+ assert_equal 1, result.length
54
+ assert_equal 1, result.first['id']
55
+ end
56
+
57
+ def test_exec_query_returns_multiple_rows
58
+ query = TestQuery.new
59
+ sql = 'SELECT * FROM users ORDER BY id'
60
+ result = query.exec_query(sql)
61
+
62
+ assert_kind_of ActiveRecord::Result, result
63
+ assert_operator result.length, :>, 1
64
+ end
65
+ end
66
+ end
67
+ end
@@ -62,6 +62,84 @@ module Inquery
62
62
  result = Queries::Group::FilterWithColor.run(Group.where('id > 2'), color: 'green')
63
63
  assert_equal Group.find([3]), result.to_a
64
64
  end
65
+
66
+ def test_call_not_implemented_error
67
+ # Create an anonymous chainable query class that doesn't override call
68
+ query_class = Class.new(Inquery::Query::Chainable) do
69
+ relation class: 'Group'
70
+ end
71
+
72
+ # Should raise NotImplementedError when call is not overridden
73
+ assert_raises(NotImplementedError) do
74
+ query_class.run(Group.all)
75
+ end
76
+ end
77
+
78
+ def test_chainable_with_default_relation
79
+ query_class = Class.new(Inquery::Query::Chainable) do
80
+ relation class: 'Group'
81
+
82
+ def call
83
+ relation.where(color: 'red')
84
+ end
85
+ end
86
+
87
+ # Should use default relation when no relation is passed
88
+ result = query_class.run
89
+ assert_equal Group.find([1]), result.to_a
90
+ end
91
+
92
+ def test_chainable_with_params_only
93
+ result = Queries::Group::FilterWithColor.run(color: 'red')
94
+ assert_equal Group.find([1]), result.to_a
95
+ end
96
+
97
+ def test_chainable_with_empty_relation
98
+ query_class = Class.new(Inquery::Query::Chainable) do
99
+ relation class: 'Group'
100
+
101
+ def call
102
+ relation.where(color: 'blue')
103
+ end
104
+ end
105
+
106
+ result = query_class.run(Group.where('1=0'))
107
+ assert_empty result.to_a
108
+ end
109
+
110
+ def test_chainable_with_different_call_signatures
111
+ query_class = Class.new(Inquery::Query::Chainable) do
112
+ relation class: 'Group'
113
+ schema3 do
114
+ str? :color
115
+ end
116
+
117
+ def call
118
+ if osparams.color
119
+ relation.where(color: osparams.color)
120
+ else
121
+ relation
122
+ end
123
+ end
124
+ end
125
+
126
+ # Test with relation and params
127
+ result1 = query_class.run(Group.all, color: 'red')
128
+ assert_equal Group.find([1]), result1.to_a
129
+
130
+ # Test with params only
131
+ result2 = query_class.run(color: 'green')
132
+ assert_equal Group.find([2, 3]), result2.to_a
133
+
134
+ # Test with relation only (should return all)
135
+ result3 = query_class.run(Group.where('id > 1'))
136
+ assert_equal Group.find([2, 3]), result3.to_a
137
+ end
138
+
139
+ def test_relation_returns_active_record_relation
140
+ result = Queries::Group::FetchRed.run
141
+ assert_kind_of ActiveRecord::Relation, result
142
+ end
65
143
  end
66
144
  end
67
145
  end