quo 1.0.0.beta2 → 2.0.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/.devcontainer/Dockerfile +17 -0
- data/.devcontainer/compose.yml +10 -0
- data/.devcontainer/devcontainer.json +12 -0
- data/Appraisals +4 -12
- data/CHANGELOG.md +112 -1
- data/CLAUDE.md +19 -0
- data/Gemfile +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +496 -203
- data/Rakefile +66 -6
- data/UPGRADING.md +216 -0
- data/badges/coverage_badge_total.svg +35 -0
- data/badges/rubycritic_badge_score.svg +35 -0
- data/claude-skill/README.md +100 -0
- data/claude-skill/SKILL.md +442 -0
- data/claude-skill/references/API_REFERENCE.md +462 -0
- data/claude-skill/references/COMPOSITION.md +396 -0
- data/claude-skill/references/PAGINATION.md +396 -0
- data/claude-skill/references/QUERY_TYPES.md +297 -0
- data/claude-skill/references/TRANSFORMERS.md +282 -0
- data/context/01-core-architecture.md +247 -0
- data/context/02-query-types-implementation.md +355 -0
- data/context/03-composition-transformation.md +441 -0
- data/context/04-pagination-results.md +485 -0
- data/context/05-testing-configuration.md +491 -0
- data/context/06-advanced-patterns-examples.md +153 -0
- data/gemfiles/rails_8.0.gemfile +10 -5
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/lib/generators/quo/install/USAGE +21 -0
- data/lib/generators/quo/install/install_generator.rb +63 -0
- data/lib/quo/collection_backed_query.rb +21 -15
- data/lib/quo/collection_results.rb +1 -0
- data/lib/quo/composed_collection_backed_query.rb +42 -0
- data/lib/quo/composed_instance.rb +144 -0
- data/lib/quo/composed_query.rb +43 -178
- data/lib/quo/composed_relation_backed_query.rb +42 -0
- data/lib/quo/composing/base_strategy.rb +22 -0
- data/lib/quo/composing/class_strategy.rb +86 -0
- data/lib/quo/composing/class_strategy_registry.rb +31 -0
- data/lib/quo/composing/query_classes_strategy.rb +38 -0
- data/lib/quo/composing.rb +81 -0
- data/lib/quo/engine.rb +1 -0
- data/lib/quo/minitest/helpers.rb +14 -24
- data/lib/quo/preloadable.rb +1 -0
- data/lib/quo/query.rb +22 -5
- data/lib/quo/relation_backed_query.rb +24 -18
- data/lib/quo/relation_backed_query_specification.rb +44 -25
- data/lib/quo/relation_results.rb +1 -0
- data/lib/quo/results.rb +31 -2
- data/lib/quo/rspec/helpers.rb +15 -26
- data/lib/quo/testing/collection_backed_fake.rb +1 -0
- data/lib/quo/testing/fake_helpers.rb +30 -0
- data/lib/quo/testing/relation_backed_fake.rb +1 -0
- data/lib/quo/version.rb +1 -1
- data/lib/quo/wrapped_collection_backed_query.rb +21 -0
- data/lib/quo/wrapped_relation_backed_query.rb +21 -0
- data/lib/quo.rb +8 -0
- data/quo.png +0 -0
- data/sig/generated/quo/collection_backed_query.rbs +10 -4
- data/sig/generated/quo/collection_results.rbs +1 -0
- data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
- data/sig/generated/quo/composed_instance.rbs +61 -0
- data/sig/generated/quo/composed_query.rbs +23 -56
- data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
- data/sig/generated/quo/composing/base_strategy.rbs +16 -0
- data/sig/generated/quo/composing/class_strategy.rbs +38 -0
- data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
- data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
- data/sig/generated/quo/composing.rbs +40 -0
- data/sig/generated/quo/engine.rbs +1 -0
- data/sig/generated/quo/minitest/helpers.rbs +12 -0
- data/sig/generated/quo/preloadable.rbs +1 -0
- data/sig/generated/quo/query.rbs +15 -4
- data/sig/generated/quo/relation_backed_query.rbs +15 -5
- data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
- data/sig/generated/quo/relation_results.rbs +1 -0
- data/sig/generated/quo/results.rbs +11 -0
- data/sig/generated/quo/rspec/helpers.rbs +12 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
- data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
- data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
- data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
- data/sig/generated/quo.rbs +1 -0
- data/website/.gitignore +6 -0
- data/website/.nojekyll +0 -0
- data/website/404.html +26 -0
- data/website/Gemfile +24 -0
- data/website/_config.yml +50 -0
- data/website/_data/navigation.yml +8 -0
- data/website/_data/sidebar.yml +2 -0
- data/website/_data/social_links.yml +3 -0
- data/website/_docs/api.md +261 -0
- data/website/_docs/get-started.md +289 -0
- data/website/assets/quo.png +0 -0
- data/website/index.md +141 -0
- metadata +70 -13
- data/gemfiles/rails_7.0.gemfile +0 -15
- data/gemfiles/rails_7.1.gemfile +0 -15
- data/gemfiles/rails_7.2.gemfile +0 -15
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "base_strategy"
|
|
6
|
+
|
|
7
|
+
module Quo
|
|
8
|
+
module Composing
|
|
9
|
+
# Base class for class composition strategies
|
|
10
|
+
class ClassStrategy < BaseStrategy
|
|
11
|
+
# @rbs left_query_class: Class
|
|
12
|
+
# @rbs right_query_class: Class
|
|
13
|
+
# @rbs return: void
|
|
14
|
+
def validate_query_classes(left_query_class, right_query_class)
|
|
15
|
+
unless left_query_class.respond_to?(:<) && right_query_class.respond_to?(:<)
|
|
16
|
+
raise ArgumentError, "Cannot compose #{left_query_class} and #{right_query_class}, are they both classes? If you want to use instances use `.merge_instances`"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Collect properties that need to be (re)defined on the composed class.
|
|
21
|
+
# Properties already declared on `chosen_superclass` are inherited via the
|
|
22
|
+
# normal Ruby/Literal class hierarchy and do not need to be re-registered;
|
|
23
|
+
# skipping them avoids the per-prop Literal::Property allocation, schema
|
|
24
|
+
# dup, and `module_eval` of reader/writer source on every Class.new.
|
|
25
|
+
# @rbs chosen_superclass: Class
|
|
26
|
+
# @rbs left_query_class: Class
|
|
27
|
+
# @rbs right_query_class: Class
|
|
28
|
+
# @rbs return: Hash[Symbol, Literal::Property]
|
|
29
|
+
def collect_properties(chosen_superclass, left_query_class, right_query_class)
|
|
30
|
+
existing = chosen_superclass.literal_properties.properties_index
|
|
31
|
+
props = {}
|
|
32
|
+
if left_query_class < Quo::Query
|
|
33
|
+
left_query_class.literal_properties.properties_index.each do |name, property|
|
|
34
|
+
props[name] = property unless existing.key?(name)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
if right_query_class < Quo::Query
|
|
38
|
+
right_query_class.literal_properties.properties_index.each do |name, property|
|
|
39
|
+
props[name] = property unless existing.key?(name)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
props
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs chosen_superclass: Class
|
|
46
|
+
# @rbs props: Hash[Symbol, Literal::Property]
|
|
47
|
+
# @rbs return: Class & Quo::ComposedQuery
|
|
48
|
+
def create_composed_class(chosen_superclass, props)
|
|
49
|
+
Class.new(chosen_superclass) do
|
|
50
|
+
include Quo::ComposedQuery
|
|
51
|
+
props.each do |name, property|
|
|
52
|
+
prop(
|
|
53
|
+
name,
|
|
54
|
+
property.type,
|
|
55
|
+
property.kind,
|
|
56
|
+
reader: property.reader,
|
|
57
|
+
writer: property.writer,
|
|
58
|
+
default: property.default
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @rbs klass: Class
|
|
65
|
+
# @rbs left_query_class: Class
|
|
66
|
+
# @rbs right_query_class: Class
|
|
67
|
+
# @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]?
|
|
68
|
+
# @rbs left_spec: Quo::RelationBackedQuerySpecification?
|
|
69
|
+
# @rbs right_spec: Quo::RelationBackedQuerySpecification?
|
|
70
|
+
# @rbs return: void
|
|
71
|
+
def assign_query_metadata(klass, left_query_class, right_query_class, joins, left_spec, right_spec)
|
|
72
|
+
# merge spec and joins
|
|
73
|
+
left_joins = left_spec ? left_spec[:joins] : []
|
|
74
|
+
left_joins = left_joins.is_a?(Array) ? left_joins : [left_joins]
|
|
75
|
+
joins = joins.is_a?(Array) ? joins : [joins] if joins
|
|
76
|
+
merge_left_joins = joins ? joins + left_joins : left_joins
|
|
77
|
+
|
|
78
|
+
klass.instance_variable_set(:@_composing_joins, merge_left_joins)
|
|
79
|
+
klass.instance_variable_set(:@_left_specification, left_spec)
|
|
80
|
+
klass.instance_variable_set(:@_right_specification, right_spec)
|
|
81
|
+
klass.instance_variable_set(:@_left_query, left_query_class)
|
|
82
|
+
klass.instance_variable_set(:@_right_query, right_query_class)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "query_classes_strategy"
|
|
6
|
+
|
|
7
|
+
module Quo
|
|
8
|
+
module Composing
|
|
9
|
+
# Registry for class composition strategies
|
|
10
|
+
class ClassStrategyRegistry
|
|
11
|
+
# @rbs return: Array[Quo::Composing::BaseStrategy]
|
|
12
|
+
def strategies
|
|
13
|
+
@strategies ||= [
|
|
14
|
+
QueryClassesStrategy.new
|
|
15
|
+
# Add more class strategies as needed
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs left: Class
|
|
20
|
+
# @rbs right: Class
|
|
21
|
+
# @rbs return: Quo::Composing::BaseStrategy
|
|
22
|
+
def find_strategy(left, right)
|
|
23
|
+
strategy = strategies.find { |s| s.applicable?(left, right) }
|
|
24
|
+
unless strategy
|
|
25
|
+
raise ArgumentError, "No class composition strategy found for #{left.class} and #{right.class}"
|
|
26
|
+
end
|
|
27
|
+
strategy
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "class_strategy"
|
|
6
|
+
|
|
7
|
+
module Quo
|
|
8
|
+
module Composing
|
|
9
|
+
# Strategy for composing two Query classes
|
|
10
|
+
class QueryClassesStrategy < ClassStrategy
|
|
11
|
+
# @rbs override
|
|
12
|
+
# @rbs left: Class
|
|
13
|
+
# @rbs right: Class
|
|
14
|
+
# @rbs return: bool
|
|
15
|
+
def applicable?(left, right)
|
|
16
|
+
left.respond_to?(:<) && right.respond_to?(:<) &&
|
|
17
|
+
(left < Quo::Query || left.is_a?(::ActiveRecord::Relation)) &&
|
|
18
|
+
(right < Quo::Query || right.is_a?(::ActiveRecord::Relation))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs override
|
|
22
|
+
# @rbs chosen_superclass: Class
|
|
23
|
+
# @rbs left_query_class: Class
|
|
24
|
+
# @rbs right_query_class: Class
|
|
25
|
+
# @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]?
|
|
26
|
+
# @rbs left_spec: Quo::RelationBackedQuerySpecification?
|
|
27
|
+
# @rbs right_spec: Quo::RelationBackedQuerySpecification?
|
|
28
|
+
# @rbs return: Class & Quo::ComposedQuery
|
|
29
|
+
def compose(chosen_superclass, left_query_class, right_query_class, joins: nil, left_spec: nil, right_spec: nil)
|
|
30
|
+
validate_query_classes(left_query_class, right_query_class)
|
|
31
|
+
props = collect_properties(chosen_superclass, left_query_class, right_query_class)
|
|
32
|
+
klass = create_composed_class(chosen_superclass, props)
|
|
33
|
+
assign_query_metadata(klass, left_query_class, right_query_class, joins, left_spec, right_spec)
|
|
34
|
+
klass
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "composing/class_strategy_registry"
|
|
6
|
+
|
|
7
|
+
module Quo
|
|
8
|
+
module Composing
|
|
9
|
+
CLASS_STRATEGY_REGISTRY = ClassStrategyRegistry.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# @rbs chosen_superclass: Class
|
|
13
|
+
# @rbs left_query_class: Class
|
|
14
|
+
# @rbs right_query_class: Class
|
|
15
|
+
# @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]?
|
|
16
|
+
# @rbs left_spec: Quo::RelationBackedQuerySpecification?
|
|
17
|
+
# @rbs right_spec: Quo::RelationBackedQuerySpecification?
|
|
18
|
+
# @rbs return: Class & Quo::ComposedQuery
|
|
19
|
+
def composer(chosen_superclass, left_query_class, right_query_class, joins: nil, left_spec: nil, right_spec: nil)
|
|
20
|
+
strategy = CLASS_STRATEGY_REGISTRY.find_strategy(left_query_class, right_query_class)
|
|
21
|
+
strategy.compose(chosen_superclass, left_query_class, right_query_class, joins: joins, left_spec: left_spec, right_spec: right_spec)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs left_instance: Quo::Query
|
|
25
|
+
# @rbs right_instance: Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped]
|
|
26
|
+
# @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]?
|
|
27
|
+
# @rbs return: Quo::Query
|
|
28
|
+
def merge_instances(left_instance, right_instance, joins: nil)
|
|
29
|
+
page, page_size = inherited_pagination(left_instance, right_instance)
|
|
30
|
+
transformer = inherited_transformer(left_instance, right_instance)
|
|
31
|
+
|
|
32
|
+
opts = {left: left_instance, right: right_instance, merge_joins: joins}
|
|
33
|
+
opts[:page] = page if page
|
|
34
|
+
opts[:page_size] = page_size if page_size
|
|
35
|
+
|
|
36
|
+
composed = if relation_backed?(left_instance) || relation_backed?(right_instance)
|
|
37
|
+
Quo::ComposedRelationBackedQuery.new(**opts)
|
|
38
|
+
else
|
|
39
|
+
Quo::ComposedCollectionBackedQuery.new(**opts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
composed.transform(&transformer) if transformer
|
|
43
|
+
composed
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# @rbs operand: untyped
|
|
49
|
+
# @rbs return: bool
|
|
50
|
+
def relation_backed?(operand)
|
|
51
|
+
operand.is_a?(Quo::RelationBackedQuery) || operand.is_a?(::ActiveRecord::Relation)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs left: untyped
|
|
55
|
+
# @rbs right: untyped
|
|
56
|
+
# @rbs return: Array[Integer?]
|
|
57
|
+
def inherited_pagination(left, right)
|
|
58
|
+
operand = pagination_source(right) || pagination_source(left)
|
|
59
|
+
return [nil, nil] unless operand
|
|
60
|
+
[operand.page, operand.page_size]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @rbs operand: untyped
|
|
64
|
+
# @rbs return: untyped
|
|
65
|
+
def pagination_source(operand)
|
|
66
|
+
return nil unless operand.respond_to?(:page) && operand.respond_to?(:page_size)
|
|
67
|
+
operand.page ? operand : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs left: untyped
|
|
71
|
+
# @rbs right: untyped
|
|
72
|
+
# @rbs return: Proc?
|
|
73
|
+
def inherited_transformer(left, right)
|
|
74
|
+
right_transformer = left_transformer = nil
|
|
75
|
+
right_transformer = right.send(:transformer) if right.is_a?(Quo::Query) && right.transform?
|
|
76
|
+
left_transformer = left.send(:transformer) if left.is_a?(Quo::Query) && left.transform?
|
|
77
|
+
right_transformer || left_transformer
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/quo/engine.rb
CHANGED
data/lib/quo/minitest/helpers.rb
CHANGED
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
3
5
|
require "minitest/mock"
|
|
4
6
|
|
|
5
7
|
require_relative "../testing/collection_backed_fake"
|
|
6
8
|
require_relative "../testing/relation_backed_fake"
|
|
9
|
+
require_relative "../testing/fake_helpers"
|
|
7
10
|
|
|
8
11
|
module Quo
|
|
9
12
|
module Minitest
|
|
13
|
+
# Test helpers for stubbing query objects in Minitest
|
|
10
14
|
module Helpers
|
|
11
|
-
|
|
12
|
-
# make it so that results of instances of this class return a fake Result object
|
|
13
|
-
# of the right type which returns the results passed in
|
|
14
|
-
if query_class < Quo::CollectionBackedQuery
|
|
15
|
-
klass = Class.new(Quo::Testing::CollectionBackedFake) do
|
|
16
|
-
if query_class < Quo::Preloadable
|
|
17
|
-
include Quo::Preloadable
|
|
15
|
+
include Quo::Testing::FakeHelpers
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
query_class.stub(:new, ->(**kwargs) {
|
|
25
|
-
klass.new(results: results, total_count: total_count, page_count: page_count)
|
|
26
|
-
}) do
|
|
27
|
-
yield
|
|
28
|
-
end
|
|
29
|
-
elsif query_class < Quo::RelationBackedQuery
|
|
30
|
-
query_class.stub(:new, ->(**kwargs) {
|
|
31
|
-
Quo::Testing::RelationBackedFake.new(results: results, total_count: total_count, page_count: page_count)
|
|
32
|
-
}) do
|
|
33
|
-
yield
|
|
34
|
-
end
|
|
35
|
-
else
|
|
17
|
+
def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
|
|
18
|
+
unless query_class < Quo::Query
|
|
36
19
|
raise ArgumentError, "Not a Query class: #{query_class}"
|
|
37
20
|
end
|
|
21
|
+
|
|
22
|
+
klass = build_fake_class(query_class)
|
|
23
|
+
query_class.stub(:new, ->(**kwargs) {
|
|
24
|
+
klass.new(results: results, total_count: total_count, page_count: page_count)
|
|
25
|
+
}) do
|
|
26
|
+
yield
|
|
27
|
+
end
|
|
38
28
|
end
|
|
39
29
|
end
|
|
40
30
|
end
|
data/lib/quo/preloadable.rb
CHANGED
data/lib/quo/query.rb
CHANGED
|
@@ -2,24 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# rbs_inline: enabled
|
|
4
4
|
|
|
5
|
-
require "literal"
|
|
6
|
-
|
|
7
5
|
module Quo
|
|
6
|
+
# Base class for query objects with pagination and composition support
|
|
8
7
|
class Query < Literal::Struct
|
|
9
8
|
include Literal::Types
|
|
10
9
|
|
|
10
|
+
# @rbs override
|
|
11
11
|
def self.inspect
|
|
12
12
|
"#{name || "(anonymous)"}<#{superclass}>"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# @rbs override
|
|
15
16
|
def self.to_s
|
|
16
17
|
inspect
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# @rbs override
|
|
19
21
|
def inspect
|
|
20
22
|
"#{self.class.name || "(anonymous)"}<#{self.class.superclass} #{paged? ? "" : "not "}paginated>#{super}"
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# @rbs override
|
|
23
26
|
def to_s
|
|
24
27
|
inspect
|
|
25
28
|
end
|
|
@@ -35,12 +38,26 @@ module Quo
|
|
|
35
38
|
else
|
|
36
39
|
Quo.relation_backed_query_base_class
|
|
37
40
|
end
|
|
38
|
-
|
|
41
|
+
Composing.composer(super_class, self, right, joins: joins)
|
|
39
42
|
end
|
|
40
43
|
singleton_class.alias_method :+, :compose
|
|
41
44
|
|
|
45
|
+
# Helper method to define properties on a dynamically created class
|
|
46
|
+
# @rbs klass: Class
|
|
47
|
+
# @rbs props: Hash[Symbol, untyped]
|
|
48
|
+
# @rbs return: void
|
|
49
|
+
def self.define_props_on_class(klass, props)
|
|
50
|
+
props.each do |name, property|
|
|
51
|
+
if property.is_a?(Literal::Property)
|
|
52
|
+
klass.prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default
|
|
53
|
+
else
|
|
54
|
+
klass.prop name, property
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
COERCE_TO_INT = ->(value) do #: (untyped value) -> Integer?
|
|
43
|
-
return if value
|
|
60
|
+
return if value.equal?(Literal::Undefined)
|
|
44
61
|
value&.to_i
|
|
45
62
|
end
|
|
46
63
|
|
|
@@ -87,7 +104,7 @@ module Quo
|
|
|
87
104
|
# @rbs joins: untyped
|
|
88
105
|
# @rbs return: Quo::ComposedQuery
|
|
89
106
|
def merge(right, joins: nil)
|
|
90
|
-
|
|
107
|
+
Composing.merge_instances(self, right, joins: joins)
|
|
91
108
|
end
|
|
92
109
|
alias_method :+, :merge
|
|
93
110
|
|
|
@@ -2,26 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
# rbs_inline: enabled
|
|
4
4
|
|
|
5
|
-
require "literal"
|
|
6
|
-
|
|
7
5
|
module Quo
|
|
6
|
+
# Query object backed by ActiveRecord relations
|
|
8
7
|
class RelationBackedQuery < Query
|
|
9
|
-
# @rbs query: ActiveRecord::Relation | Quo::
|
|
8
|
+
# @rbs query: ActiveRecord::Relation | Quo::RelationBackedQuery
|
|
10
9
|
# @rbs props: Hash[Symbol, untyped]
|
|
11
|
-
# @rbs &block: () -> ActiveRecord::Relation | Quo::Query
|
|
10
|
+
# @rbs &block: ? () -> (ActiveRecord::Relation | Quo::Query)
|
|
12
11
|
# @rbs return: Quo::RelationBackedQuery
|
|
13
12
|
def self.wrap(query = nil, props: {}, &block)
|
|
14
13
|
raise ArgumentError, "either a query or a block must be provided" unless query || block
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
else
|
|
21
|
-
prop name, property
|
|
22
|
-
end
|
|
23
|
-
end
|
|
15
|
+
if query && !(query.is_a?(::ActiveRecord::Relation) || query.is_a?(Quo::RelationBackedQuery))
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"Quo::RelationBackedQuery.wrap requires an ActiveRecord::Relation or a Quo::RelationBackedQuery instance; got #{query.class}. " \
|
|
18
|
+
"Use Quo::CollectionBackedQuery.wrap or Quo::CollectionBackedQuery.from for in-memory collections."
|
|
24
19
|
end
|
|
20
|
+
|
|
21
|
+
klass = Class.new(self)
|
|
22
|
+
define_props_on_class(klass, props)
|
|
25
23
|
if block
|
|
26
24
|
klass.define_method(:query, &block)
|
|
27
25
|
else
|
|
@@ -30,6 +28,12 @@ module Quo
|
|
|
30
28
|
klass
|
|
31
29
|
end
|
|
32
30
|
|
|
31
|
+
# @rbs relation: ActiveRecord::Relation
|
|
32
|
+
# @rbs return: Quo::WrappedRelationBackedQuery
|
|
33
|
+
def self.from(relation)
|
|
34
|
+
Quo::WrappedRelationBackedQuery.new(wrapped: relation)
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
# @rbs conditions: untyped?
|
|
34
38
|
# @rbs return: String
|
|
35
39
|
def self.sanitize_sql_for_conditions(conditions)
|
|
@@ -53,7 +57,6 @@ module Quo
|
|
|
53
57
|
# @_specification: Quo::RelationBackedQuerySpecification?
|
|
54
58
|
prop :_specification, _Nilable(Quo::RelationBackedQuerySpecification),
|
|
55
59
|
default: -> { RelationBackedQuerySpecification.blank },
|
|
56
|
-
reader: false,
|
|
57
60
|
writer: false
|
|
58
61
|
|
|
59
62
|
# Apply a query specification to this query
|
|
@@ -72,10 +75,12 @@ module Quo
|
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
# Delegate methods that let us get the model class (available on AR relations)
|
|
75
|
-
# @rbs
|
|
76
|
-
#
|
|
78
|
+
# @rbs!
|
|
79
|
+
# def model: () -> (untyped | nil)
|
|
80
|
+
# def klass: () -> (untyped | nil)
|
|
77
81
|
delegate :model, :klass, to: :underlying_query
|
|
78
82
|
|
|
83
|
+
# @rbs total_count: Integer?
|
|
79
84
|
# @rbs return: Quo::CollectionBackedQuery
|
|
80
85
|
def to_collection(total_count: nil)
|
|
81
86
|
Quo.collection_backed_query_base_class.wrap(results.to_a).new(total_count:)
|
|
@@ -86,7 +91,7 @@ module Quo
|
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
# Return the SQL string for this query if its a relation type query object
|
|
89
|
-
def to_sql #: String
|
|
94
|
+
def to_sql #: String?
|
|
90
95
|
configured_query.to_sql if relation?
|
|
91
96
|
end
|
|
92
97
|
|
|
@@ -115,8 +120,9 @@ module Quo
|
|
|
115
120
|
# @rbs include_private: bool
|
|
116
121
|
# @rbs return: bool
|
|
117
122
|
def respond_to_missing?(method_name, include_private = false)
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
# Reuse the memoized blank specification rather than allocating a fresh
|
|
124
|
+
# instance per probe — ActiveRecord's delegation hits respond_to? heavily.
|
|
125
|
+
RelationBackedQuerySpecification.blank.respond_to?(method_name, include_private) || super
|
|
120
126
|
end
|
|
121
127
|
|
|
122
128
|
private
|
|
@@ -17,9 +17,10 @@ module Quo
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Creates a new specification with merged options
|
|
20
|
-
# @rbs new_options: Hash[Symbol, untyped]
|
|
21
|
-
# @rbs return: Quo::
|
|
20
|
+
# @rbs new_options: Hash[Symbol, untyped] | RelationBackedQuerySpecification
|
|
21
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
22
22
|
def merge(new_options)
|
|
23
|
+
new_options = new_options.options if new_options.is_a?(self.class)
|
|
23
24
|
self.class.new(options.merge(new_options))
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -34,8 +35,12 @@ module Quo
|
|
|
34
35
|
rel = rel.group(*options[:group]) if options[:group]
|
|
35
36
|
rel = rel.limit(options[:limit]) if options[:limit]
|
|
36
37
|
rel = rel.offset(options[:offset]) if options[:offset]
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if (joins = options[:joins])
|
|
39
|
+
rel = joins.is_a?(Array) ? rel.joins(*joins) : rel.joins(joins)
|
|
40
|
+
end
|
|
41
|
+
if (left_outer_joins = options[:left_outer_joins])
|
|
42
|
+
rel = left_outer_joins.is_a?(Array) ? rel.left_outer_joins(*left_outer_joins) : rel.left_outer_joins(left_outer_joins)
|
|
43
|
+
end
|
|
39
44
|
rel = rel.includes(*options[:includes]) if options[:includes]
|
|
40
45
|
rel = rel.preload(*options[:preload]) if options[:preload]
|
|
41
46
|
rel = rel.eager_load(*options[:eager_load]) if options[:eager_load]
|
|
@@ -46,107 +51,121 @@ module Quo
|
|
|
46
51
|
rel
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
# Introspection
|
|
55
|
+
|
|
56
|
+
# @rbs key: Symbol
|
|
57
|
+
# @rbs return: bool
|
|
58
|
+
def has?(key)
|
|
59
|
+
options.key?(key)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @rbs key: Symbol
|
|
63
|
+
# @rbs return: untyped
|
|
64
|
+
def [](key)
|
|
65
|
+
options[key]
|
|
66
|
+
end
|
|
67
|
+
|
|
49
68
|
# Create helpers for each query option
|
|
50
69
|
|
|
51
70
|
# @rbs *fields: untyped
|
|
52
|
-
# @rbs return: Quo::
|
|
71
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
53
72
|
def select(*fields)
|
|
54
73
|
merge(select: fields)
|
|
55
74
|
end
|
|
56
75
|
|
|
57
76
|
# @rbs conditions: untyped
|
|
58
|
-
# @rbs return: Quo::
|
|
77
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
59
78
|
def where(conditions)
|
|
60
79
|
merge(where: conditions)
|
|
61
80
|
end
|
|
62
81
|
|
|
63
82
|
# @rbs order_clause: untyped
|
|
64
|
-
# @rbs return: Quo::
|
|
83
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
65
84
|
def order(order_clause)
|
|
66
85
|
merge(order: order_clause)
|
|
67
86
|
end
|
|
68
87
|
|
|
69
88
|
# @rbs *columns: untyped
|
|
70
|
-
# @rbs return: Quo::
|
|
89
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
71
90
|
def group(*columns)
|
|
72
91
|
merge(group: columns)
|
|
73
92
|
end
|
|
74
93
|
|
|
75
94
|
# @rbs value: Integer
|
|
76
|
-
# @rbs return: Quo::
|
|
95
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
77
96
|
def limit(value)
|
|
78
97
|
merge(limit: value)
|
|
79
98
|
end
|
|
80
99
|
|
|
81
100
|
# @rbs value: Integer
|
|
82
|
-
# @rbs return: Quo::
|
|
101
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
83
102
|
def offset(value)
|
|
84
103
|
merge(offset: value)
|
|
85
104
|
end
|
|
86
105
|
|
|
87
|
-
# @rbs tables: untyped
|
|
88
|
-
# @rbs return: Quo::
|
|
89
|
-
def joins(tables)
|
|
106
|
+
# @rbs *tables: untyped
|
|
107
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
108
|
+
def joins(*tables)
|
|
90
109
|
merge(joins: tables)
|
|
91
110
|
end
|
|
92
111
|
|
|
93
|
-
# @rbs tables: untyped
|
|
94
|
-
# @rbs return: Quo::
|
|
95
|
-
def left_outer_joins(tables)
|
|
112
|
+
# @rbs *tables: untyped
|
|
113
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
114
|
+
def left_outer_joins(*tables)
|
|
96
115
|
merge(left_outer_joins: tables)
|
|
97
116
|
end
|
|
98
117
|
|
|
99
118
|
# @rbs *associations: untyped
|
|
100
|
-
# @rbs return: Quo::
|
|
119
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
101
120
|
def includes(*associations)
|
|
102
121
|
merge(includes: associations)
|
|
103
122
|
end
|
|
104
123
|
|
|
105
124
|
# @rbs *associations: untyped
|
|
106
|
-
# @rbs return: Quo::
|
|
125
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
107
126
|
def preload(*associations)
|
|
108
127
|
merge(preload: associations)
|
|
109
128
|
end
|
|
110
129
|
|
|
111
130
|
# @rbs *associations: untyped
|
|
112
|
-
# @rbs return: Quo::
|
|
131
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
113
132
|
def eager_load(*associations)
|
|
114
133
|
merge(eager_load: associations)
|
|
115
134
|
end
|
|
116
135
|
|
|
117
136
|
# @rbs enabled: bool
|
|
118
|
-
# @rbs return: Quo::
|
|
137
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
119
138
|
def distinct(enabled = true)
|
|
120
139
|
merge(distinct: enabled)
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
# @rbs order_clause: untyped
|
|
124
|
-
# @rbs return: Quo::
|
|
143
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
125
144
|
def reorder(order_clause)
|
|
126
145
|
merge(reorder: order_clause)
|
|
127
146
|
end
|
|
128
147
|
|
|
129
148
|
# @rbs *modules: untyped
|
|
130
|
-
# @rbs return: Quo::
|
|
149
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
131
150
|
def extending(*modules)
|
|
132
151
|
merge(extending: modules)
|
|
133
152
|
end
|
|
134
153
|
|
|
135
154
|
# @rbs *args: untyped
|
|
136
|
-
# @rbs return: Quo::
|
|
155
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
137
156
|
def unscope(*args)
|
|
138
157
|
merge(unscope: args)
|
|
139
158
|
end
|
|
140
159
|
|
|
141
160
|
# Builds a new specification from a hash of options
|
|
142
161
|
# @rbs options: Hash[Symbol, untyped]
|
|
143
|
-
# @rbs return: Quo::
|
|
162
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
144
163
|
def self.build(options = {})
|
|
145
164
|
new(options)
|
|
146
165
|
end
|
|
147
166
|
|
|
148
167
|
# Returns a blank specification
|
|
149
|
-
# @rbs return: Quo::
|
|
168
|
+
# @rbs return: Quo::RelationBackedQuerySpecification
|
|
150
169
|
def self.blank
|
|
151
170
|
@blank ||= new
|
|
152
171
|
end
|
data/lib/quo/relation_results.rb
CHANGED