Querying & Scopes

The Queryable module in CQL provides a powerful, type-safe, and chainable interface for constructing SQL queries using idiomatic Crystal code. It enables you to filter, sort, join, and aggregate records with a fluent API, abstracting away raw SQL while retaining full expressiveness and performance.

This guide covers how to use the query interface provided by CQL Active Record models and repositories, including all major query methods and best practices.


Basic Usage

You can build queries directly on your model or repository. Most query methods return a chainable query object, allowing you to compose complex queries step by step.

Example: Simple Filtering

# Find all users named "Alice"
alice_users = User.where(name: "Alice").all

# Find the first user with a specific email
user = User.where(email: "alice@example.com").first

# Using query builder directly
users = User.query.where { age > 18 }.order(:name).all(User)

Example: Using a Repository

user_repo = UserRepository.new
# Find all users with a given domain
gmail_users = user_repo.query.where { email.like("%@gmail.com") }.all(User)

Chainable Query Methods

CQL provides a rich set of chainable query methods that allow you to build complex queries in a type-safe and expressive way. Each method returns a query object that can be further chained with additional methods.

where

The where method is used to filter records based on conditions. It supports both simple key-value pairs and block-based conditions.

where_like

Pattern matching using SQL LIKE operator.

order

Sort records using the order method. You can specify multiple columns and sort directions.

reorder

Replace existing order clauses with new ones.

reverse_order

Reverse the current ordering direction.

limit and offset

Control the number of records returned and paginate through results.

select

Specify which columns to retrieve from the database.

group and group_by

Group records by one or more columns, often used with aggregate functions.

having

Add conditions to grouped records, typically used with group_by.

join, left, right

Perform SQL joins between tables. CQL supports various join types and provides a type-safe way to specify join conditions.

distinct

Remove duplicate records from the result set.

unscope

Remove specific scopes from the query.


Finding Records

find and find!

Find a record by its primary key.

find_by and find_by!

Find a record by attributes.


Terminal Operations

Terminal operations execute the query and return the results. These methods are typically used at the end of a query chain to fetch the data from the database.

all

Returns all records matching the query as an array.

first, first!, last, last!

Returns the first or last record matching the query.

count

Returns the number of records matching the query.

size

Alias for count - returns the number of records.

exists?

Checks if any records match the query.

any? and many?

Check existence and quantity.

empty?

Checks if no records match the query.

pluck

Returns an array of values for the specified columns.

pick

Extract a single column value from the first matching record.

ids

Get array of primary key values.

Aggregate Functions

Calculate statistics using built-in aggregate functions.


Batch Processing

Process large datasets efficiently using batch methods.

find_each

Process records one at a time in batches.

find_in_batches

Process records in batches.


Query Inspection and Debugging

to_sql

Get the generated SQL for debugging.

none

Create a query that returns no results (useful for conditional building).


Advanced Query Composition

CQL's query interface allows you to build complex queries by combining multiple query methods. This section demonstrates common patterns and best practices for composing sophisticated queries.

Advanced Filtering

Combine multiple conditions using logical operators and complex expressions.

Joins with Complex Conditions

Combine joins with filtering and ordering.

Aggregations with Grouping

Combine grouping with aggregations and having clauses.

Subqueries and Complex Filtering

Use subqueries for advanced filtering and data retrieval.

Query Scopes and Reusable Methods

Define reusable query scopes for common query patterns.

Conditional Query Building

Build queries dynamically based on conditions.


Performance Optimization

Efficient Querying

Query Analysis and Debugging


Best Practices Summary

Query Construction

  • Use meaningful method names and extract complex queries into scopes

  • Chain methods logically - filters first, then joins, then ordering

  • Use block syntax for complex conditions to improve readability

  • Leverage automatic relationship detection for simpler joins

Performance

  • Always use .select to limit columns when you don't need all data

  • Use .exists? instead of .count > 0 for existence checks

  • Use batch processing (.find_each, .find_in_batches) for large datasets

  • Use .pluck for extracting single column values efficiently

Maintainability

  • Define reusable scopes for common query patterns

  • Use conditional query building for dynamic filters

  • Extract complex queries into well-named methods

  • Document complex business logic in query methods

Security

  • Always use parameterized queries (CQL handles this automatically)

  • Validate user input before using in dynamic query construction

  • Use .none as a safe default for access control

  • Be careful with user-provided LIKE patterns

Testing

  • Test query scopes in isolation

  • Verify that complex queries return expected results

  • Test edge cases (empty results, large datasets)

  • Use .to_sql to verify generated SQL in tests


For more information on related query topics:

Last updated

Was this helpful?