quo 0.5.3 → 1.0.0.alpha1

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 (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 -69
  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 +101 -213
  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 -20
  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,20 +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
- Quo.configuration.logger&.info(message)
17
- end
18
- end
19
- end
20
- 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