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 relation
  • results - Returns a Results object with the query results
  • unwrap - Returns the underlying relation with pagination applied
  • unwrap_unpaginated - Returns the underlying relation without pagination
  • to_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 Enumerable
  • results - Returns a Results object with the collection results
  • unwrap - Returns the underlying collection with pagination applied
  • unwrap_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 support
  • first, last, first(n), last(n) - Access specific items
  • count, total_count, size - Total count of ALL results (ignores pagination)
  • page_count, page_size - Count of items on CURRENT page only
  • exists?, empty? - Existence checks
  • to_a - Convert to array
  • find(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 find and find_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

Quo | Copyright © 2025. Licensed under the MIT License.