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.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +1 -1
- data/.github/workflows/ruby.yml +24 -8
- data/.gitignore +2 -2
- data/.rubocop.yml +4 -0
- data/Appraisals +24 -4
- data/CHANGELOG.md +38 -0
- data/Gemfile +17 -1
- data/Gemfile.lock +125 -0
- data/LICENSE +1 -1
- data/MIGRATION.md +260 -0
- data/README.md +14 -8
- data/RUBY_VERSION +1 -1
- data/Rakefile +4 -11
- data/VERSION +1 -1
- data/doc/Inquery/Exceptions/Base.html +4 -4
- data/doc/Inquery/Exceptions/InvalidRelation.html +4 -4
- data/doc/Inquery/Exceptions/UnknownCallSignature.html +4 -4
- data/doc/Inquery/Exceptions.html +4 -4
- data/doc/Inquery/MethodAccessibleHash.html +431 -0
- data/doc/Inquery/Mixins/RawSqlUtils.html +17 -4
- data/doc/Inquery/Mixins/RelationValidation/ClassMethods.html +8 -7
- data/doc/Inquery/Mixins/RelationValidation.html +9 -8
- data/doc/Inquery/Mixins/SchemaValidation/ClassMethods.html +4 -4
- data/doc/Inquery/Mixins/SchemaValidation.html +4 -4
- data/doc/Inquery/Mixins.html +4 -4
- data/doc/Inquery/Query/Chainable.html +207 -90
- data/doc/Inquery/Query.html +401 -73
- data/doc/Inquery.html +12 -9
- data/doc/_index.html +12 -5
- data/doc/class_list.html +6 -3
- data/doc/css/full_list.css +3 -3
- data/doc/css/style.css +6 -0
- data/doc/file.README.html +102 -16
- data/doc/file_list.html +5 -2
- data/doc/frames.html +10 -5
- data/doc/index.html +102 -16
- data/doc/js/app.js +294 -264
- data/doc/js/full_list.js +30 -4
- data/doc/method_list.html +48 -21
- data/doc/top-level-namespace.html +4 -4
- data/gemfiles/rails_5.2.gemfile +1 -0
- data/gemfiles/rails_6.0.gemfile +1 -0
- data/gemfiles/rails_6.1.gemfile +1 -0
- data/gemfiles/rails_7.0.gemfile +1 -0
- data/gemfiles/{rails_5.1.gemfile → rails_7.1.gemfile} +2 -1
- data/gemfiles/rails_7.2.gemfile +8 -0
- data/gemfiles/rails_8.0.gemfile +8 -0
- data/gemfiles/rails_8.1.gemfile +8 -0
- data/inquery.gemspec +11 -34
- data/lib/inquery/method_accessible_hash.rb +45 -0
- data/lib/inquery/mixins/raw_sql_utils.rb +32 -6
- data/lib/inquery/mixins/relation_validation.rb +1 -1
- data/lib/inquery/query/chainable.rb +69 -27
- data/lib/inquery/query.rb +67 -14
- data/lib/inquery.rb +2 -0
- data/test/inquery/error_handling_test.rb +117 -0
- data/test/inquery/method_accessible_hash_test.rb +85 -0
- data/test/inquery/mixins/raw_sql_utils_test.rb +67 -0
- data/test/inquery/query/chainable_test.rb +78 -0
- data/test/inquery/query_test.rb +86 -0
- data/test/test_helper.rb +11 -0
- metadata +30 -129
- 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
|
-
#
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
#
|
|
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
|
|
15
|
-
#
|
|
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
|
-
#
|
|
21
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
46
|
-
#
|
|
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 ||=
|
|
97
|
+
@osparams ||= MethodAccessibleHash.new(params)
|
|
49
98
|
end
|
|
50
99
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
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
|