Quo
Quo
A ROM-inspired query builder for Crystal with explicit, unambiguous syntax, immutable queries, and composable relations.
Features
- Explicit Syntax: All columns are table-qualified (e.g.,
contracts[:status]), eliminating ambiguity in complex queries - Immutable Queries: Every method returns a new query instance, enabling safe composition and branching
- Type-Safe: Runtime column validation and type checking catch errors before they reach the database
- ROM-Inspired Architecture: Relations, scopes, and repositories for clean data access patterns
- Multiple Database Support: PostgreSQL, MySQL, and SQLite adapters with connection pooling
- Rich Query Building: Complex WHERE expressions, joins, subqueries, aggregations, and more
- Repository Pattern: Clean data access layer with entity mapping and CRUD shortcuts
- Comprehensive Logging: Query instrumentation with timing breakdowns and slow query detection
- Query Caching: TTL-based caching with tag-based invalidation
- Transaction Support: ACID transactions with savepoints and isolation levels
Installation
Add this to your application's shard.yml:
dependencies:
quo:
github: alexandrelairan/quo
version: ~> 0.1.0
# Choose your database driver
pg:
github: will/crystal-pg
version: ~> 0.28.0
# OR
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.21.0
# OR
mysql:
github: crystal-lang/crystal-mysql
Quick Start
Define a Relation
require "quo"
require "pg"
class UsersRelation < Quo::Relation
schema :users do
primary_key :id, Int64
column :email, String
column :name, String
column :active, Bool
column :created_at, Time
has_many :posts, :user_id, :posts
end
scope :active do
query.where(users: {active: true})
end
scope :created_after do |date|
query.where { |e| e[:users][:created_at] > date }
end
end
Query with Explicit Syntax
# Create an adapter
adapter = Quo::Adapters::Postgres.new("postgres://localhost/mydb")
# Build and execute queries
users = UsersRelation.new(adapter)
.active
.created_after(1.year.ago)
.order(users: {name: :asc})
.limit(10)
.to_a
users.each do |row|
puts "#{row["name"]} - #{row["email"]}"
end
Complex Queries with Joins
class PostsRelation < Quo::Relation
schema :posts do
primary_key :id, Int64
column :title, String
column :user_id, Int64
belongs_to :user, :user_id, :users
end
end
# Join using defined associations
posts = PostsRelation.new(adapter)
.select(posts: [:id, :title], users: [:name])
.join(:user)
.where { |e| e[:users][:active] == true }
.to_a
Repository Pattern
class UserRepository < Quo::Repository
relation :users, UsersRelation
entity :user, User do
map :id, Int64
map :email, String
map :name, String
end
def find_by_email(email : String) : User?
users
.where(users: {email: email})
.first
.try { |row| user_from_row(row) }
end
def create(email : String, name : String) : User?
insert_returning(:users, [:id, :email, :name],
email: email,
name: name,
active: true
).try { |row| user_from_row(row) }
end
end
repo = UserRepository.new(adapter)
user = repo.find_by_email("user@example.com")
Mutations
# INSERT
relation
.insert(users: {email: "new@example.com", name: "John"})
.execute
# INSERT with RETURNING (PostgreSQL)
row = relation
.insert(users: {email: "new@example.com", name: "John"})
.returning(users: [:id, :email])
.execute
# UPDATE
relation
.update(users: {active: false})
.where(users: {email: "old@example.com"})
.execute
# DELETE
relation
.delete
.where { |e| e[:users][:created_at] < 1.year.ago }
.execute
Expression Building
relation.where { |e|
(e[:users][:active] == true) &
(
(e[:users][:email].like("%@example.com")) |
(e[:users][:name].in(["Alice", "Bob"]))
)
}
Logging and Instrumentation
Quo::Logging.log_level = Quo::LogLevel::Info
Quo::Logging.slow_query_threshold = 50.milliseconds
Quo::Logging.subscribe do |event|
puts "SQL: #{event.sql}"
puts "Params: #{event.params.inspect}"
puts "Duration: #{event.duration.total_milliseconds}ms"
puts " - Compile: #{event.compile_time.total_milliseconds}ms"
puts " - Execute: #{event.execute_time.total_milliseconds}ms"
end
Connection Pooling
# Use pooled adapters for multi-threaded applications
adapter = Quo::Adapters::PooledPostgres.new(
"postgres://localhost/mydb",
pool_size: 10,
pool_timeout: 5.seconds
)
Why Quo?
Explicit Over Implicit
Many query builders allow bare column names, leading to ambiguity:
# Ambiguous - which table's 'name' column?
query.where(name: "John")
Quo requires table qualification:
# Clear and unambiguous
query.where(users: {name: "John"})
Immutable Queries Enable Safe Composition
base = UsersRelation.new(adapter).active
# Branch without affecting the original
admins = base.where(users: {role: "admin"})
moderators = base.where(users: {role: "moderator"})
# base remains unchanged
Type Safety Catches Errors Early
# Typo in column name caught at runtime validation
relation.where(users: {emal: "test@example.com"})
# => Quo::ColumnNotFoundError: Column 'emal' not found in table 'users'
# Type mismatch caught before query execution
relation.where(users: {active: "yes"})
# => Quo::TypeMismatchError: Expected Bool for column 'active', got String
Documentation
- Getting Started
- Queries
- Expressions
- Joins
- Relations & Schemas
- Scopes
- Repositories
- Mutations (INSERT/UPDATE/DELETE)
- Transactions
- Connection Pooling
- Logging & Instrumentation
- Query Caching
- Database Adapters
- Multi-Database
- Introspection
- Validation
Examples
Check out the examples directory for complete working examples:
- demo.cr - Basic usage with relations and scopes
- demo_sqlite.cr - SQLite-specific examples
- example_advanced_pg.cr - Advanced PostgreSQL features
- example_introspection_pg.cr - Database introspection examples
- logging_example.cr - Query logging and instrumentation
Requirements
- Crystal >= 1.10.0
- Database driver (pg, sqlite3, or mysql)
Contributing
- Fork it (https://github.com/alexandrelairan/quo/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT License. See LICENSE for details.
Author
Alexandre Lairan
Acknowledgments
Quo is inspired by ROM-rb, the Ruby data mapping and persistence toolkit.
Repository
Quo
Owner
Statistic
- 0
- 0
- 0
- 0
- 3
- about 5 hours ago
- January 26, 2026
License
Links
Synced at
Mon, 26 Jan 2026 17:44:54 GMT
Languages