API Reference
Query Object Classes
Quo::RelationBackedQuery
Base class for queries that work with ActiveRecord relations.
class MyQuery < Quo::RelationBackedQuery
prop :some_filter, String
def query
MyModel.where(column: some_filter)
end
end
Key Methods:
query- Must be implemented to return an ActiveRecord relationresults- Returns a Results object with the query resultsunwrap- Returns the underlying relation with pagination appliedunwrap_unpaginated- Returns the underlying relation without paginationto_sql- Returns the SQL representation of the query
Quo::CollectionBackedQuery
Base class for queries that work with any Enumerable collection.
class MyCollectionQuery < Quo::CollectionBackedQuery
prop :filter_value, String
def collection
[1, 2, 3, 4, 5].select { |n| n > filter_value.to_i }
end
end
Key Methods:
collection- Must be implemented to return an Enumerableresults- Returns a Results object with the collection resultsunwrap- Returns the underlying collection with pagination appliedunwrap_unpaginated- Returns the underlying collection without pagination
Common Query Methods
Both query types support these methods:
Property Definition
prop :property_name, Type, default: -> { default_value }
Define typed properties using Literal types. Common examples:
# Basic types
prop :name, String
prop :age, Integer
prop :active, _Boolean # Use _Boolean for true/false
# Optional types
prop :email, String | nil
# Collections
prop :tags, _Array(String)
prop :metadata, _Hash(Symbol, String)
Pagination
MyQuery.new(page: 1, page_size: 20)
query.next_page_query
query.previous_page_query
query.paged? # => true/false
Composition
# Instance-level: Merge query instances
query1 + query2 # Returns a new query instance
query1.merge(query2) # Returns a new query instance
query1.merge(query2, joins: :association) # Returns a new query instance
# Class-level: Compose query classes
ComposedQuery = Query1 + Query2 # Returns a new query CLASS
ComposedQuery = Query1.compose(Query2) # Returns a new query CLASS
ComposedQuery.new # Create an instance of composed class
Transformation
# Transform results with a block
query.transform { |item| ItemPresenter.new(item) }
query.transform { |item, index| ItemPresenter.new(item, position: index) }
query.transform? # => true/false
Fluent API (RelationBackedQuery)
Chain ActiveRecord methods to build complex queries:
# Filtering, ordering, and associations
query.where(column: value)
query.select(:column1, :column2)
query.order(created_at: :desc)
query.reorder(updated_at: :asc)
query.joins(:association)
query.left_outer_joins(:association)
query.includes(:profile, :posts) # Alias for preload (not ActiveRecord's includes)
query.preload(:comments)
query.eager_load(:tags)
# Limiting and grouping
query.limit(10).offset(5)
query.group(:category).distinct
# Advanced
query.extending(MyQueryExtension)
query.unscope(:order, :limit)
Query Helpers
# Check query type
query.relation? # => true if backed by ActiveRecord relation
query.collection? # => true if backed by a collection
# Create a query class from a relation or collection
# IMPORTANT: wrap returns a new query CLASS, not an instance!
MyQuery = Quo::RelationBackedQuery.wrap(User.active)
instance = MyQuery.new # Must call .new to create an instance
# With dynamic properties - still returns a CLASS
MyQuery = Quo::RelationBackedQuery.wrap(props: {role: String}) { User.where(role: role) }
instance = MyQuery.new(role: "admin") # Must call .new with props
# Convert a RelationBackedQuery to CollectionBackedQuery (executes the query)
collection_query = query.to_collection
collection_query = query.to_collection(total_count: 100) # Optional total count
Results Objects
RelationResults
Returned by RelationBackedQuery#results.
Methods:
each,map,select,reject- Enumerable methods with transformation supportfirst,last,first(n),last(n)- Access specific itemscount,total_count,size- Total count of ALL results (ignores pagination)page_count,page_size- Count of items on CURRENT page onlyexists?,empty?- Existence checksto_a- Convert to arrayfind(id),find_by(attributes)- ActiveRecord finder methods
Note: Transformations are applied to all items returned by result methods.
CollectionResults
Returned by CollectionBackedQuery#results.
Methods:
- Same as RelationResults, except ActiveRecord-specific methods like
findandfind_by
Configuration
module Quo
# Set custom base classes
self.relation_backed_query_base_class = "ApplicationQuery"
self.collection_backed_query_base_class = "ApplicationCollectionQuery"
# Configure pagination defaults
self.default_page_size = 25 # Default: 20
self.max_page_size = 100 # Default: 200
end
Preloading Associations (CollectionBackedQuery)
Preload associations for CollectionBackedQuery objects containing ActiveRecord models:
class FirstAndLastPosts < Quo::CollectionBackedQuery
include Quo::Preloadable
def collection
[Post.first, Post.last]
end
end
# Preload associations to avoid N+1 queries
query = FirstAndLastPosts.new.includes(:author, :comments)
# Access preloaded data without additional queries
query.results.each do |post|
puts "#{post.title} by #{post.author.name} (#{post.comments.count} comments)"
end
Note: Quo’s includes is an alias for preload (not ActiveRecord’s includes which uses eager loading). Both preload associations without joining.
Testing Helpers
Minitest
include Quo::Minitest::Helpers
# Basic usage
fake_query(MyQuery, results: [item1, item2]) do
result = MyQuery.new.results.to_a
assert_equal [item1, item2], result
end
# With pagination metadata
fake_query(MyQuery, results: [item1, item2], total_count: 100, page_count: 2) do
results = MyQuery.new.results
assert_equal 100, results.total_count # Total across all pages
assert_equal 2, results.page_count # Items on current page
end
RSpec
include Quo::Rspec::Helpers
# Basic usage
fake_query(MyQuery, results: [item1, item2]) do
result = MyQuery.new.results.to_a
expect(result).to eq([item1, item2])
end
# With specific argument expectations
fake_query(MyQuery, with: {role: "admin"}, results: [admin1, admin2]) do
result = MyQuery.new(role: "admin").results.to_a
expect(result).to eq([admin1, admin2])
end
# With pagination metadata
fake_query(MyQuery, results: [item1, item2], total_count: 100, page_count: 2) do
results = MyQuery.new.results
expect(results.total_count).to eq(100) # Total across all pages
expect(results.page_count).to eq(2) # Items on current page
end