quo 0.6.0 → 1.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.standard.yml +4 -1
- data/Appraisals +11 -0
- data/CHANGELOG.md +78 -0
- data/Gemfile +6 -4
- data/LICENSE.txt +1 -1
- data/README.md +37 -36
- data/Steepfile +0 -2
- data/gemfiles/rails_7.0.gemfile +15 -0
- data/gemfiles/rails_7.1.gemfile +15 -0
- data/gemfiles/rails_7.2.gemfile +15 -0
- data/lib/quo/collection_backed_query.rb +87 -0
- data/lib/quo/collection_results.rb +44 -0
- data/lib/quo/composed_query.rb +168 -0
- data/lib/quo/engine.rb +11 -0
- data/lib/quo/minitest/helpers.rb +41 -0
- data/lib/quo/preloadable.rb +46 -0
- data/lib/quo/query.rb +97 -214
- data/lib/quo/relation_backed_query.rb +177 -0
- data/lib/quo/relation_results.rb +58 -0
- data/lib/quo/results.rb +48 -44
- data/lib/quo/rspec/helpers.rb +31 -9
- data/lib/quo/testing/collection_backed_fake.rb +29 -0
- data/lib/quo/testing/relation_backed_fake.rb +52 -0
- data/lib/quo/version.rb +3 -1
- data/lib/quo.rb +22 -30
- data/rbs_collection.yaml +0 -2
- data/sig/generated/quo/collection_backed_query.rbs +39 -0
- data/sig/generated/quo/collection_results.rbs +30 -0
- data/sig/generated/quo/composed_query.rbs +83 -0
- data/sig/generated/quo/engine.rbs +6 -0
- data/sig/generated/quo/preloadable.rbs +29 -0
- data/sig/generated/quo/query.rbs +98 -0
- data/sig/generated/quo/relation_backed_query.rbs +90 -0
- data/sig/generated/quo/relation_results.rbs +38 -0
- data/sig/generated/quo/results.rbs +39 -0
- data/sig/generated/quo/version.rbs +5 -0
- data/sig/generated/quo.rbs +9 -0
- metadata +67 -30
- data/lib/quo/eager_query.rb +0 -51
- data/lib/quo/loaded_query.rb +0 -18
- data/lib/quo/merged_query.rb +0 -36
- data/lib/quo/query_composer.rb +0 -78
- data/lib/quo/railtie.rb +0 -7
- data/lib/quo/utilities/callstack.rb +0 -21
- data/lib/quo/utilities/compose.rb +0 -18
- data/lib/quo/utilities/sanitize.rb +0 -19
- data/lib/quo/utilities/wrap.rb +0 -23
- data/lib/quo/wrapped_query.rb +0 -18
- data/sig/quo/eager_query.rbs +0 -15
- data/sig/quo/loaded_query.rbs +0 -7
- data/sig/quo/merged_query.rbs +0 -19
- data/sig/quo/query.rbs +0 -83
- data/sig/quo/query_composer.rbs +0 -32
- data/sig/quo/results.rbs +0 -22
- data/sig/quo/utilities/callstack.rbs +0 -7
- data/sig/quo/utilities/compose.rbs +0 -8
- data/sig/quo/utilities/sanitize.rbs +0 -9
- data/sig/quo/utilities/wrap.rbs +0 -11
- data/sig/quo/wrapped_query.rbs +0 -11
- data/sig/quo.rbs +0 -41
data/lib/quo/query_composer.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
class QueryComposer
|
5
|
-
def initialize(left, right, joins = nil)
|
6
|
-
@left = left
|
7
|
-
@right = right
|
8
|
-
@unwrapped_left = unwrap_relation(left)
|
9
|
-
@unwrapped_right = unwrap_relation(right)
|
10
|
-
@left_relation = @unwrapped_left.is_a?(::ActiveRecord::Relation)
|
11
|
-
@right_relation = @unwrapped_right.is_a?(::ActiveRecord::Relation)
|
12
|
-
@joins = joins
|
13
|
-
end
|
14
|
-
|
15
|
-
def compose
|
16
|
-
Quo::MergedQuery.new(
|
17
|
-
merge_left_and_right,
|
18
|
-
left,
|
19
|
-
right,
|
20
|
-
**merged_options
|
21
|
-
)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
attr_reader :left, :right, :joins, :unwrapped_left, :unwrapped_right
|
27
|
-
|
28
|
-
def left_relation?
|
29
|
-
@left_relation
|
30
|
-
end
|
31
|
-
|
32
|
-
def right_relation?
|
33
|
-
@right_relation
|
34
|
-
end
|
35
|
-
|
36
|
-
def merge_left_and_right
|
37
|
-
# FIXME: Skipping type checks here, as not sure how to make this type check with RBS
|
38
|
-
__skip__ = if both_relations?
|
39
|
-
apply_joins(unwrapped_left, joins).merge(unwrapped_right)
|
40
|
-
elsif left_relation_right_enumerable?
|
41
|
-
unwrapped_left.to_a + unwrapped_right
|
42
|
-
elsif left_enumerable_right_relation? && unwrapped_left.respond_to?(:+)
|
43
|
-
unwrapped_left + unwrapped_right.to_a
|
44
|
-
elsif unwrapped_left.respond_to?(:+)
|
45
|
-
unwrapped_left + unwrapped_right
|
46
|
-
else
|
47
|
-
raise ArgumentError, "Cannot merge #{left.class} with #{right.class}"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def merged_options
|
52
|
-
return left.options.merge(right.options) if left.is_a?(Quo::Query) && right.is_a?(Quo::Query)
|
53
|
-
return left.options if left.is_a?(Quo::Query)
|
54
|
-
return right.options if right.is_a?(Quo::Query)
|
55
|
-
{}
|
56
|
-
end
|
57
|
-
|
58
|
-
def unwrap_relation(query)
|
59
|
-
query.is_a?(Quo::Query) ? query.unwrap : query
|
60
|
-
end
|
61
|
-
|
62
|
-
def apply_joins(left_rel, joins)
|
63
|
-
joins.present? ? left_rel.joins(joins) : left_rel
|
64
|
-
end
|
65
|
-
|
66
|
-
def both_relations?
|
67
|
-
left_relation? && right_relation?
|
68
|
-
end
|
69
|
-
|
70
|
-
def left_relation_right_enumerable?
|
71
|
-
left_relation? && !right_relation?
|
72
|
-
end
|
73
|
-
|
74
|
-
def left_enumerable_right_relation?
|
75
|
-
!left_relation? && right_relation?
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
data/lib/quo/railtie.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
module Utilities
|
5
|
-
module Callstack
|
6
|
-
def debug_callstack
|
7
|
-
return unless Rails.env.development?
|
8
|
-
callstack_size = Quo.configuration.query_show_callstack_size
|
9
|
-
return unless callstack_size&.positive?
|
10
|
-
working_dir = Dir.pwd
|
11
|
-
exclude = %r{/(gems/|rubies/|query\.rb)}
|
12
|
-
stack = Kernel.caller.grep_v(exclude).map { |l| l.gsub(working_dir + "/", "") }
|
13
|
-
stack_to_display = stack[0..callstack_size]
|
14
|
-
message = "\n[Query stack]: -> #{stack_to_display&.join("\n &> ")}\n"
|
15
|
-
message += " (truncated to #{callstack_size} most recent)" if callstack_size && stack.size > callstack_size
|
16
|
-
logger = Quo.configuration.logger&.call
|
17
|
-
logger.info(message) if logger
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
module Utilities
|
5
|
-
# Combine two query-like or composeable entities:
|
6
|
-
# These can be Quo::Query, Quo::MergedQuery, Quo::EagerQuery and ActiveRecord::Relations.
|
7
|
-
# See the `README.md` docs for more details.
|
8
|
-
module Compose
|
9
|
-
def compose(query1, query2, joins: nil)
|
10
|
-
Quo::QueryComposer.new(query1, query2, joins).compose
|
11
|
-
end
|
12
|
-
|
13
|
-
def composable_with?(query)
|
14
|
-
query.is_a?(Quo::Query) || query.is_a?(ActiveRecord::Relation)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
module Utilities
|
5
|
-
module Sanitize
|
6
|
-
def sanitize_sql_for_conditions(conditions)
|
7
|
-
ActiveRecord::Base.sanitize_sql_for_conditions(conditions)
|
8
|
-
end
|
9
|
-
|
10
|
-
def sanitize_sql_string(string)
|
11
|
-
sanitize_sql_for_conditions(["'%s'", string])
|
12
|
-
end
|
13
|
-
|
14
|
-
def sanitize_sql_parameter(value)
|
15
|
-
sanitize_sql_for_conditions(["?", value])
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
data/lib/quo/utilities/wrap.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
module Utilities
|
5
|
-
# Wrap a ActiveRecord::Relation or data collection in a Query.
|
6
|
-
#
|
7
|
-
# If the passed in object is already a Query object then just return it or copy it if new options are passed in.
|
8
|
-
# Otherwise if a Relation wrap it in a new Query object or else in an EagerQuery .
|
9
|
-
module Wrap
|
10
|
-
def wrap(query_rel_or_data, **options)
|
11
|
-
if query_rel_or_data.is_a? Quo::Query
|
12
|
-
return options.present? ? query_rel_or_data.copy(**options) : query_rel_or_data
|
13
|
-
end
|
14
|
-
|
15
|
-
if query_rel_or_data.is_a? ActiveRecord::Relation
|
16
|
-
Quo::WrappedQuery.new(query_rel_or_data, **options)
|
17
|
-
else
|
18
|
-
Quo::LoadedQuery.new(query_rel_or_data, **options)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
data/lib/quo/wrapped_query.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Quo
|
4
|
-
class WrappedQuery < Quo::Query
|
5
|
-
def initialize(wrapped_query, **options)
|
6
|
-
@wrapped_query = wrapped_query
|
7
|
-
super(**options)
|
8
|
-
end
|
9
|
-
|
10
|
-
def copy(**options)
|
11
|
-
self.class.new(query, **@options.merge(options))
|
12
|
-
end
|
13
|
-
|
14
|
-
def query
|
15
|
-
@wrapped_query
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
data/sig/quo/eager_query.rbs
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class EagerQuery < Quo::Query
|
3
|
-
def collection: () -> loadedQueryOrEnumerable
|
4
|
-
def query: () -> loadedQueryOrEnumerable
|
5
|
-
|
6
|
-
def relation?: () -> false
|
7
|
-
def eager?: () -> true
|
8
|
-
|
9
|
-
private
|
10
|
-
|
11
|
-
def preload_includes: (untyped records, ?untyped? preload) -> untyped
|
12
|
-
def underlying_query: () -> enumerable
|
13
|
-
def unwrap_relation: (loadedQueryOrEnumerable collection) -> enumerable
|
14
|
-
end
|
15
|
-
end
|
data/sig/quo/loaded_query.rbs
DELETED
data/sig/quo/merged_query.rbs
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class MergedQuery < Quo::Query
|
3
|
-
def self.build_from_options: (queryOptions) -> MergedQuery
|
4
|
-
|
5
|
-
def initialize: (relOrEnumerable merged, composable left, composable right, **untyped options) -> void
|
6
|
-
|
7
|
-
@merged_query: relOrEnumerable
|
8
|
-
|
9
|
-
def query: () -> relOrEnumerable
|
10
|
-
|
11
|
-
def inspect: () -> ::String
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
attr_reader left: composable
|
16
|
-
attr_reader right: composable
|
17
|
-
def operand_desc: (composable operand) -> String
|
18
|
-
end
|
19
|
-
end
|
data/sig/quo/query.rbs
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class Query
|
3
|
-
include Quo::Utilities::Callstack
|
4
|
-
extend Quo::Utilities::Compose
|
5
|
-
extend Quo::Utilities::Sanitize
|
6
|
-
extend Quo::Utilities::Wrap
|
7
|
-
|
8
|
-
@underlying_query: ActiveRecord::Relation
|
9
|
-
|
10
|
-
def self.call: (**untyped options) -> untyped
|
11
|
-
def self.call!: (**untyped options) -> untyped
|
12
|
-
|
13
|
-
@scope: ActiveRecord::Relation | nil
|
14
|
-
|
15
|
-
attr_reader current_page: Integer?
|
16
|
-
attr_reader page_size: Integer?
|
17
|
-
attr_reader options: Hash[untyped, untyped]
|
18
|
-
|
19
|
-
def initialize: (**untyped options) -> void
|
20
|
-
def query: () -> queryOrRel
|
21
|
-
def compose: (composable right, ?joins: untyped?) -> Quo::MergedQuery
|
22
|
-
alias + compose
|
23
|
-
|
24
|
-
def copy: (**untyped options) -> Quo::Query
|
25
|
-
|
26
|
-
def limit: (untyped limit) -> Quo::Query
|
27
|
-
def order: (untyped options) -> Quo::Query
|
28
|
-
def group: (*untyped options) -> Quo::Query
|
29
|
-
def includes: (*untyped options) -> Quo::Query
|
30
|
-
def preload: (*untyped options) -> Quo::Query
|
31
|
-
def select: (*untyped options) -> Quo::Query
|
32
|
-
|
33
|
-
def sum: (?untyped column_name) -> Numeric
|
34
|
-
def average: (untyped column_name) -> Numeric
|
35
|
-
def minimum: (untyped column_name) -> Numeric
|
36
|
-
def maximum: (untyped column_name) -> Numeric
|
37
|
-
def count: () -> Integer
|
38
|
-
|
39
|
-
alias total_count count
|
40
|
-
|
41
|
-
alias size count
|
42
|
-
def page_count: () -> Integer
|
43
|
-
def first: (?Integer? limit) -> untyped
|
44
|
-
def first!: (?Integer? limit) -> untyped
|
45
|
-
def last: (?Integer? limit) -> untyped
|
46
|
-
def to_a: () -> Array[untyped]
|
47
|
-
def to_eager: (?::Hash[untyped, untyped] more_opts) -> Quo::EagerQuery
|
48
|
-
alias load to_eager
|
49
|
-
def results: () -> Quo::Results
|
50
|
-
|
51
|
-
# Set a block used to transform data after query fetching
|
52
|
-
def transform: () ?{ () -> untyped } -> self
|
53
|
-
|
54
|
-
def exists?: () -> bool
|
55
|
-
def none?: () -> bool
|
56
|
-
alias empty? none?
|
57
|
-
def relation?: () -> bool
|
58
|
-
def eager?: () -> bool
|
59
|
-
def paged?: () -> bool
|
60
|
-
|
61
|
-
def model: () -> (untyped | nil)
|
62
|
-
def klass: () -> (untyped | nil)
|
63
|
-
|
64
|
-
def transform?: () -> bool
|
65
|
-
def to_sql: () -> (String | nil)
|
66
|
-
def unwrap: () -> ActiveRecord::Relation
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def formatted_queries?: () -> bool
|
71
|
-
def trim_query: (String sql) -> String
|
72
|
-
def format_query: (String sql_str) -> String
|
73
|
-
def transformer: () -> (nil | ^(untyped, ?Integer) -> untyped)
|
74
|
-
def offset: () -> Integer
|
75
|
-
def configured_query: () -> ActiveRecord::Relation
|
76
|
-
def sanitised_page_size: () -> Integer
|
77
|
-
def query_with_logging: () -> ActiveRecord::Relation
|
78
|
-
def underlying_query: () -> ActiveRecord::Relation
|
79
|
-
def unwrap_relation: (queryOrRel query) -> ActiveRecord::Relation
|
80
|
-
def test_eager: (composable rel) -> bool
|
81
|
-
def test_relation: (composable rel) -> bool
|
82
|
-
end
|
83
|
-
end
|
data/sig/quo/query_composer.rbs
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class QueryComposer
|
3
|
-
@left_relation: bool
|
4
|
-
@right_relation: bool
|
5
|
-
|
6
|
-
def initialize: (composable left, composable right, ?untyped? joins) -> void
|
7
|
-
def compose: () -> Quo::MergedQuery
|
8
|
-
|
9
|
-
private
|
10
|
-
|
11
|
-
attr_reader left: composable
|
12
|
-
attr_reader right: composable
|
13
|
-
attr_reader joins: untyped
|
14
|
-
|
15
|
-
attr_reader unwrapped_left: relOrEnumerable
|
16
|
-
attr_reader unwrapped_right: relOrEnumerable
|
17
|
-
|
18
|
-
def left_relation?: -> bool
|
19
|
-
|
20
|
-
def merge_left_and_right: () -> relOrEnumerable
|
21
|
-
def merged_options: () -> ::Hash[untyped, untyped]
|
22
|
-
|
23
|
-
def right_relation?: -> bool
|
24
|
-
|
25
|
-
def unwrap_relation: (composable) -> relOrEnumerable
|
26
|
-
def relation_type?: (relOrEnumerable) -> bool
|
27
|
-
def apply_joins: (ActiveRecord::Relation left_rel, untyped joins) -> ActiveRecord::Relation
|
28
|
-
def both_relations?: () -> bool
|
29
|
-
def left_relation_right_enumerable?: () -> bool
|
30
|
-
def left_enumerable_right_relation?: () -> bool
|
31
|
-
end
|
32
|
-
end
|
data/sig/quo/results.rbs
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class Results
|
3
|
-
extend Forwardable
|
4
|
-
|
5
|
-
include Quo::Utilities::Callstack
|
6
|
-
|
7
|
-
def initialize: (Quo::Query query, ?transformer: (^(untyped, ?Integer) -> untyped)?) -> void
|
8
|
-
|
9
|
-
@query: Quo::Query
|
10
|
-
|
11
|
-
def group_by: () { (untyped, *untyped) -> untyped } -> Hash[untyped, Array[untyped]]
|
12
|
-
|
13
|
-
def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
attr_reader transformer: (^(untyped, ?Integer) -> untyped)?
|
18
|
-
attr_reader unwrapped: relOrEnumerable
|
19
|
-
|
20
|
-
def transform_results: (untyped) -> untyped
|
21
|
-
end
|
22
|
-
end
|
data/sig/quo/utilities/wrap.rbs
DELETED
data/sig/quo/wrapped_query.rbs
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
module Quo
|
2
|
-
class WrappedQuery < Quo::Query
|
3
|
-
def self.build_from_options: (**untyped options) -> WrappedQuery
|
4
|
-
|
5
|
-
@wrapped_query: ActiveRecord::Relation
|
6
|
-
|
7
|
-
def initialize: (ActiveRecord::Relation query, **untyped options) -> void
|
8
|
-
|
9
|
-
def query: () -> ActiveRecord::Relation
|
10
|
-
end
|
11
|
-
end
|
data/sig/quo.rbs
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
module ActiveRecord
|
2
|
-
module Associations
|
3
|
-
class Preloader
|
4
|
-
def initialize: (records: untyped, associations: untyped, ?scope: untyped, ?available_records: Array[untyped], ?associate_by_default: bool) -> void
|
5
|
-
end
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
module Quo
|
10
|
-
VERSION: String
|
11
|
-
|
12
|
-
type query = Quo::Query
|
13
|
-
type queryOrRel = query | ActiveRecord::Relation
|
14
|
-
type enumerable = Object & Enumerable[untyped]
|
15
|
-
type relOrEnumerable = ActiveRecord::Relation | enumerable
|
16
|
-
type loadedQueryOrEnumerable = LoadedQuery | EagerQuery | enumerable
|
17
|
-
type composable = query | relOrEnumerable
|
18
|
-
|
19
|
-
# TODO: how can we do the known options, eg `page` and then allow anything else?
|
20
|
-
# Maybe we should separate out the known options from the unknown options
|
21
|
-
type queryOptions = Hash[Symbol, untyped]
|
22
|
-
|
23
|
-
interface _Logger
|
24
|
-
def info: (String) -> void
|
25
|
-
def error: (String) -> void
|
26
|
-
def debug: (String) -> void
|
27
|
-
end
|
28
|
-
|
29
|
-
class Configuration
|
30
|
-
attr_accessor formatted_query_log: bool?
|
31
|
-
attr_accessor query_show_callstack_size: Integer?
|
32
|
-
attr_accessor logger: _Logger?
|
33
|
-
attr_accessor max_page_size: Integer?
|
34
|
-
attr_accessor default_page_size: Integer?
|
35
|
-
|
36
|
-
def initialize: () -> void
|
37
|
-
end
|
38
|
-
attr_reader self.configuration: Configuration
|
39
|
-
|
40
|
-
def self.configure: () { (Configuration config) -> void } -> void
|
41
|
-
end
|