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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -1
  3. data/Appraisals +11 -0
  4. data/CHANGELOG.md +78 -0
  5. data/Gemfile +6 -4
  6. data/LICENSE.txt +1 -1
  7. data/README.md +37 -36
  8. data/Steepfile +0 -2
  9. data/gemfiles/rails_7.0.gemfile +15 -0
  10. data/gemfiles/rails_7.1.gemfile +15 -0
  11. data/gemfiles/rails_7.2.gemfile +15 -0
  12. data/lib/quo/collection_backed_query.rb +87 -0
  13. data/lib/quo/collection_results.rb +44 -0
  14. data/lib/quo/composed_query.rb +168 -0
  15. data/lib/quo/engine.rb +11 -0
  16. data/lib/quo/minitest/helpers.rb +41 -0
  17. data/lib/quo/preloadable.rb +46 -0
  18. data/lib/quo/query.rb +97 -214
  19. data/lib/quo/relation_backed_query.rb +177 -0
  20. data/lib/quo/relation_results.rb +58 -0
  21. data/lib/quo/results.rb +48 -44
  22. data/lib/quo/rspec/helpers.rb +31 -9
  23. data/lib/quo/testing/collection_backed_fake.rb +29 -0
  24. data/lib/quo/testing/relation_backed_fake.rb +52 -0
  25. data/lib/quo/version.rb +3 -1
  26. data/lib/quo.rb +22 -30
  27. data/rbs_collection.yaml +0 -2
  28. data/sig/generated/quo/collection_backed_query.rbs +39 -0
  29. data/sig/generated/quo/collection_results.rbs +30 -0
  30. data/sig/generated/quo/composed_query.rbs +83 -0
  31. data/sig/generated/quo/engine.rbs +6 -0
  32. data/sig/generated/quo/preloadable.rbs +29 -0
  33. data/sig/generated/quo/query.rbs +98 -0
  34. data/sig/generated/quo/relation_backed_query.rbs +90 -0
  35. data/sig/generated/quo/relation_results.rbs +38 -0
  36. data/sig/generated/quo/results.rbs +39 -0
  37. data/sig/generated/quo/version.rbs +5 -0
  38. data/sig/generated/quo.rbs +9 -0
  39. metadata +67 -30
  40. data/lib/quo/eager_query.rb +0 -51
  41. data/lib/quo/loaded_query.rb +0 -18
  42. data/lib/quo/merged_query.rb +0 -36
  43. data/lib/quo/query_composer.rb +0 -78
  44. data/lib/quo/railtie.rb +0 -7
  45. data/lib/quo/utilities/callstack.rb +0 -21
  46. data/lib/quo/utilities/compose.rb +0 -18
  47. data/lib/quo/utilities/sanitize.rb +0 -19
  48. data/lib/quo/utilities/wrap.rb +0 -23
  49. data/lib/quo/wrapped_query.rb +0 -18
  50. data/sig/quo/eager_query.rbs +0 -15
  51. data/sig/quo/loaded_query.rbs +0 -7
  52. data/sig/quo/merged_query.rbs +0 -19
  53. data/sig/quo/query.rbs +0 -83
  54. data/sig/quo/query_composer.rbs +0 -32
  55. data/sig/quo/results.rbs +0 -22
  56. data/sig/quo/utilities/callstack.rbs +0 -7
  57. data/sig/quo/utilities/compose.rbs +0 -8
  58. data/sig/quo/utilities/sanitize.rbs +0 -9
  59. data/sig/quo/utilities/wrap.rbs +0 -11
  60. data/sig/quo/wrapped_query.rbs +0 -11
  61. data/sig/quo.rbs +0 -41
@@ -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,7 +0,0 @@
1
- module Quo
2
- class Railtie < ::Rails::Railtie
3
- rake_tasks do
4
- load "tasks/quo.rake"
5
- end
6
- end
7
- end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,7 +0,0 @@
1
- module Quo
2
- class LoadedQuery < Quo::EagerQuery
3
- @collection: enumerable
4
-
5
- def initialize: (enumerable, **untyped options) -> void
6
- end
7
- end
@@ -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
@@ -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
@@ -1,7 +0,0 @@
1
- module Quo
2
- module Utilities
3
- module Callstack
4
- def debug_callstack: () -> void
5
- end
6
- end
7
- end
@@ -1,8 +0,0 @@
1
- module Quo
2
- module Utilities
3
- module Compose
4
- def compose: (composable query1, composable query2, ?joins: untyped?) -> Quo::MergedQuery
5
- def composable_with?: (queryOrRel query) -> bool
6
- end
7
- end
8
- end
@@ -1,9 +0,0 @@
1
- module Quo
2
- module Utilities
3
- module Sanitize
4
- def sanitize_sql_for_conditions: (untyped conditions) -> untyped?
5
- def sanitize_sql_string: (untyped string) -> untyped?
6
- def sanitize_sql_parameter: (untyped value) -> untyped?
7
- end
8
- end
9
- end
@@ -1,11 +0,0 @@
1
- module Quo
2
- module Utilities
3
- interface _Wrapable
4
- def new: (**untyped options) -> query
5
- end
6
-
7
- module Wrap : _Wrapable
8
- def wrap: (composable query_rel_or_data, **untyped options) -> query
9
- end
10
- end
11
- end
@@ -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