class
CQL::Query
- CQL::Query
- Reference
- Object
Overview
The Query class is responsible for building SQL queries in a structured manner.
It holds various components like selected columns, tables, conditions, and more.
It provides methods to execute the query and return results.
Performance Monitoring
All query execution methods (all, #first, get, each) are tracked by the
CQL Performance module once monitoring is enabled. This provides:
- Query execution time tracking
- Slow query detection
- N+1 query pattern detection
- Performance report generation
Monitoring is opt-in. To enable it:
CQL::Performance.enable_monitoring
To track queries with context:
CQL::Performance.with_context("UserController#index") do
AcmeDB.query.from(:users).all(User)
end
Example Creating a new query
query = CQL::Query.new(AcmeDB) # same as AcmeDB.query
users = query.from(:users).where(name: "John").all(User)
Example Executing a query and iterating over results
AcmeDB.query.from(:users).where(name: "John").each(User) do |user|
puts user.name
end
Included Modules
Defined in:
query.crConstructors
-
.new(schema : Schema)
Initializes the
Queryobject with the provided schema.
Macro Summary
Instance Method Summary
-
#avg(column : Symbol, as as_name : Symbol | Nil = nil)
Adds an AVG aggregate function to the query.
-
#build(limit : Int32 | Nil = @limit) : Expression::Query
Builds the final query expression.
-
#clone
Clone the query object
-
#column_aliases : Hash(CQL::BaseColumn, String)
Store column aliases: column => alias
-
#column_aliases? : Bool
Store column aliases: column => alias
-
#column_expression(name : Symbol | String) : Expression::Column
The column as an expression qualified by the alias of its table in this query.
- #columns : Array(CQL::BaseColumn)
-
#count(column : Symbol = :*, as as_name : Symbol | Nil = nil, distinct : Bool = false)
Adds a COUNT aggregate function to the query.
-
#cross(table_or_alias : Symbol) : self
Adds a CROSS JOIN (every row paired with every row of table).
-
#ctes : Array(Expression::Cte)
CTEs declared on this query, and the tables they expose by name.
-
#ctes? : Bool
CTEs declared on this query, and the tables they expose by name.
- #derived_tables : Hash(Symbol, CQL::Table)
- #derived_tables? : Bool
-
#distinct
Sets the distinct flag to true.
- #distinct=(distinct : Bool)
- #distinct? : Bool
- #distinct_on : Array(Expression::BaseColumn)
-
#distinct_on(*columns : Symbol) : self
PostgreSQL / CockroachDB: keep the first row of each group of equal columns (the order is decided by
#order). - #distinct_on? : Bool
-
#first(as kind)
The first row mapped to kind, rendered with
LIMIT 1instead of on a clone. - #first!(as kind)
-
#from(source : Executable, as alias_name : Symbol) : self
Uses a subquery as the FROM source:
FROM (SELECT ...) AS alias. -
#from(*tbls_or_aliases : Symbol | Hash(Symbol, Symbol))
Specifies the tables to select from.
-
#from(**tables_with_aliases)
Accept keyword arguments for table/alias pairs
-
#full(table_or_alias : Symbol, &block : Expression::FilterBuilder -> _)
Adds a FULL OUTER JOIN using a block for the condition (not supported by MySQL).
-
#group(*columns : Symbol | String)
Specifies the columns to group by.
- #group_by : Array(CQL::BaseColumn)
- #group_by? : Bool
- #having : Expression::Having | Nil
-
#having(&)
Adds a HAVING condition to the grouped results.
- #having=(having : Expression::Having | Nil)
-
#join(table_or_alias : Symbol)
Adds a JOIN to the query with automatic relationship detection.
-
#join(table_or_alias : Symbol, &block : Expression::FilterBuilder -> _)
Adds a JOIN to the query using a block for the condition.
-
#join(**tables_with_aliases)
Adds JOINs to the query using named arguments for table aliasing.
-
#join_on(table : Symbol, type : Expression::JoinType, &block : Expression::FilterBuilder -> _) : self
Joins table with the given join type and an explicit ON condition built by the block.
- #joins : Array(Expression::Join)
- #joins? : Bool
-
#left(table_or_alias : Symbol)
Adds a LEFT JOIN to the query with automatic relationship detection.
-
#left(table_or_alias : Symbol, &block : Expression::FilterBuilder -> _)
Adds a LEFT JOIN to the query using a block for the condition.
-
#left(**tables_with_aliases)
Adds LEFT JOINs to the query using named arguments for table aliasing.
-
#limit(value : Int32)
Sets the limit for the number of records to return.
- #limit : Int32 | Nil
- #limit=(limit : Int32 | Nil)
-
#lock(mode : Symbol = :update, skip_locked : Bool = false, nowait : Bool = false) : self
Appends
FOR UPDATE(orFOR SHARE), optionallySKIP LOCKED/NOWAIT. -
#max(column : Symbol, as as_name : Symbol | Nil = nil)
Adds a MAX aggregate function to the query.
-
#merge(other_query : Query) : Query
Merges the properties of another Query object into this one.
-
#min(column : Symbol, as as_name : Symbol | Nil = nil)
Adds a MIN aggregate function to the query.
-
#nested : Query
A fresh query that can reference this query's CTEs, for subqueries and CTE bodies.
-
#offset(value : Int32)
Sets the offset for the query.
- #offset : Int32 | Nil
- #offset=(offset : Int32 | Nil)
-
#order(*fields : Symbol | String)
Specifies the columns to order by.
-
#order(**fields)
Specifies the columns to order by with direction.
- #order_by : Hash(CQL::BaseColumn, Expression::OrderDirection)
- #order_by? : Bool
- #query_tables : Hash(String, QueryTableInfo)
- #recursive=(recursive : Bool)
- #recursive? : Bool
-
#reorder(*fields : Symbol | String)
Replaces the existing order clause with new ordering.
-
#reorder(**fields)
Replaces the existing order clause with new ordering using hash syntax.
-
#reverse_order
Reverses the order of the query.
-
#right(table_or_alias : Symbol)
Adds a RIGHT JOIN to the query with automatic relationship detection.
-
#right(table_or_alias : Symbol, &block : Expression::FilterBuilder -> _)
Adds a RIGHT JOIN to the query using a block for the condition.
-
#right(**tables_with_aliases)
Adds RIGHT JOINs to the query using named arguments for table aliasing.
- #row_lock : Expression::RowLock | Nil
- #row_lock=(row_lock : Expression::RowLock | Nil)
- #schema : Schema
-
#select(*columns : Symbol | String)
Specifies the columns to select.
-
#select(**fields : Hash(String | Symbol, Array(Symbol) | Symbol))
Specifies the columns to select, grouped by table name or alias.
- #select(**fields)
-
#select(*cols : Symbol | String, **fields : Hash(String | Symbol, Array(Symbol) | Symbol))
Allow mixing String/Symbol args and Hash
- #select(*cols, **columns)
- #select_expressions : Array(Expression::SelectExpression)
- #select_expressions? : Bool
-
#sum(column : Symbol, as as_name : Symbol | Nil = nil)
Adds a SUM aggregate function to the query.
-
#unscope(*scopes : Symbol)
Removes specific scopes from the query.
-
#where(hash : Hash(String | Symbol, DB::Any | Array(DB::Any)))
Accept Hash(Symbol, DB::Any) for backward compatibility
-
#where(condition : Expression::ConditionBuilder) : self
ANDs an already built condition (typed DSL,
CQL.raw) with the existing WHERE. -
#where(__sql sql : String, *positional, **named)
A raw SQL fragment with bound
?or:namevalues (not both in one fragment), ANDed with other conditions. - #where : Expression::Where | Nil
-
#where(&)
Correct implementation for where(&)
- #where(**fields)
- #where=(where : Expression::Where | Nil)
-
#where_gt(field : Symbol | String, value : DB::Any)
Adds a greater-than condition for the specified field.
-
#where_like(field : Symbol | String, pattern : String)
Support for LIKE conditions
-
#where_lt(field : Symbol | String, value : DB::Any)
Adds a less-than condition for the specified field.
-
#where_not_null(field : Symbol | String)
Adds an IS NOT NULL condition for the specified field.
-
#where_null(field : Symbol | String)
Adds an IS NULL condition for the specified field.
-
#window(function : Symbol, column : Symbol | Nil = nil, *, as name : Symbol, offset : Int32 | Nil = nil, buckets : Int32 | Nil = nil, partition_by : Symbol | Array(Symbol) | Nil = nil, order_by : Symbol | Array(Symbol) | Hash(Symbol, Symbol) | NamedTuple | Nil = nil, rows : Tuple(Int32 | Symbol, Int32 | Symbol) | Nil = nil, range : Tuple(Int32 | Symbol, Int32 | Symbol) | Nil = nil) : self
Adds `FUNCTION(...) OVER (PARTITION BY ...
- #with(name : Symbol, source : Executable) : self
-
#with(name : Symbol, &block : Query -> Executable) : self
Like
#with, building the source from a query that can already see the CTEs declared so far, so one CTE can read another. -
#with_performance_context(context : String, &)
Executes the query within a performance monitoring context.
-
#with_recursive(name : Symbol, anchor : Query, all : Bool = true, &block : Query -> Query) : self
Declares
WITH RECURSIVE name AS (anchor UNION [ALL] step).
Instance methods inherited from module CQL::SetOperations
except(other : Executable) : CompoundQuery
except,
intersect(other : Executable) : CompoundQuery
intersect,
union(other : Executable) : CompoundQuery
union,
union_all(other : Executable) : CompoundQuery
union_all
Instance methods inherited from module CQL::Executable
all(as kind)
all,
all!(as kind)
all!,
build : Expression::Statement
build,
each(as kind : U.class, & : U -> ) : Nil forall U
each,
explain(analyze : Bool = false) : String
explain,
get(as kind)
get,
get!(as kind)
get!,
returns_rows?(count : Int32) : Bool
returns_rows?,
to_sql(gen : Expression::Generator | Nil = nil)to_sql(for adapter : Adapter) to_sql
Instance methods inherited from module CQL::Pinnable
executor : Executor
executor,
schema : Schema
schema,
using(connection : DB::Connection) : selfusing(executor : Executor) : self using
Constructor Detail
Initializes the Query object with the provided schema.
- @param schema [Schema] The schema object to use for the query
- @return [Query] The query object
Example Creating a new query
query = CQL::Query.new(AcmeDB)
Macro Detail
A clause collection most queries never use: allocated on first access through its getter,
so .new and #clone pay nothing for it. name? tells whether it holds anything without
allocating.
Instance Method Detail
Adds an AVG aggregate function to the query.
- @param column [Symbol] The column to average
- @return [Query] The query object
Example
AcmeDB.query.from(:users).avg(:age).to_sql
# => {"SELECT AVG(users.age) FROM users", []}
Builds the final query expression.
- @param limit [Int32?] Overrides the query's limit for this build (the query is unchanged)
- @return [Expression::Query] The query expression
Example
AcmeDB.query.from(:users).build # an Expression::Query, ready for a generator
The column as an expression qualified by the alias of its table in this query.
Adds a COUNT aggregate function to the query.
- @param column [Symbol] The column to count
- @return [Query] The query object
Example
AcmeDB.query.from(:users).count(:id).to_sql
# => {"SELECT COUNT(users.id) FROM users", []}
Adds a CROSS JOIN (every row paired with every row of table).
CTEs declared on this query, and the tables they expose by name.
Sets the distinct flag to true.
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).distinct.to_sql
# => {"SELECT DISTINCT users.name FROM users", []}
PostgreSQL / CockroachDB: keep the first row of each group of equal columns
(the order is decided by #order). Other dialects raise when SQL is generated.
Uses a subquery as the FROM source: FROM (SELECT ...) AS alias. Its selected
columns can be referenced through alias.
Specifies the tables to select from.
- @param tbls_or_aliases [Symbol* | Hash(Symbol, Symbol)*] The tables to select from
- @return [Query] The query object
Example
AcmeDB.query.from(:users, :posts) # selects every column of both tables
Adds a FULL OUTER JOIN using a block for the condition (not supported by MySQL).
Specifies the columns to group by. Handles qualified columns or aliases.
- @param columns [Symbol* | String*] The columns to group by
- @return [Query] The query object
Adds a HAVING condition to the grouped results. The block uses a HavingBuilder which needs alias awareness.
- @yield [HavingBuilder] Block to build the condition.
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:age).group(:age).having { count(:id) > 5 }.to_sql
# => {"SELECT users.age FROM users GROUP BY users.age HAVING COUNT(users.id) > ?", [5]}
Adds a JOIN to the query with automatic relationship detection.
- @param table_or_alias [Symbol] The table to join
- @return [Query] The query object
Adds a JOIN to the query using a block for the condition.
- @param table_or_alias [Symbol] The table to join
- @yield [FilterBuilder] The block to build the ON condition
- @return [Query] The query object
Adds JOINs to the query using named arguments for table aliasing.
- @param tables_with_aliases [NamedTuple] Table name => alias
- @return [Query] The query object
Joins table with the given join type and an explicit ON condition built by the block.
Adds a LEFT JOIN to the query with automatic relationship detection.
- @param table_or_alias [Symbol] The table to join
- @return [Query] The query object
Adds a LEFT JOIN to the query using a block for the condition.
- @param table_or_alias [Symbol] The table to join
- @yield [FilterBuilder] The block to build the ON condition
- @return [Query] The query object
Adds LEFT JOINs to the query using named arguments for table aliasing.
- @param tables_with_aliases [NamedTuple] Table name => alias
- @return [Query] The query object
Sets the limit for the number of records to return.
- @param value [Int32] The limit value
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).limit(10).to_sql
# => {"SELECT users.name FROM users LIMIT ?", [10]}
Appends FOR UPDATE (or FOR SHARE), optionally SKIP LOCKED / NOWAIT.
SQLite has no row locks and raises when SQL is generated.
Adds a MAX aggregate function to the query.
- @param column [Symbol] The column to find the maximum value of
- @return [Query] The query object
Example
AcmeDB.query.from(:users).max(:age).to_sql
# => {"SELECT MAX(users.age) FROM users", []}
Merges the properties of another Query object into this one. The current query is modified in place.
- @param other_query [Query] The query object to merge from.
- @return [Query] The current query object, modified.
Behavior:
- Schema: Must be the same for both queries.
- Distinct: Becomes
trueif either query is distinct. - Select Columns (
@columns): Concatenated and uniqued by object identity. - Aggregate Columns (
@select_expressions): Concatenated and uniqued by object identity. - Query Tables (
@query_tables): Merged. Conflicts on alias pointing to different tables raise an error. - Joins (
@joins): Concatenated and uniqued by object identity. - Where (
@where): Conditions are combined usingAND. - Group By (
@group_by): Concatenated and uniqued by object identity. - Having (
@having): Conditions are combined usingAND. - Order By (
@order_by): Entries fromother_querytake precedence. - Limit (
@limit): The minimum of the two limits is taken if both are set; otherwise, the set limit is used. - Offset (
@offset): Theother_query's offset takes precedence if set.
ameba/disable Metrics/CyclomaticComplexity
Adds a MIN aggregate function to the query.
- @param column [Symbol] The column to find the minimum value of
- @return [Query] The query object
Example
AcmeDB.query.from(:users).min(:age).to_sql
# => {"SELECT MIN(users.age) FROM users", []}
A fresh query that can reference this query's CTEs, for subqueries and CTE bodies.
Sets the offset for the query.
- @param value [Int32] The offset value
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).limit(10).offset(20).to_sql
# => {"SELECT users.name FROM users LIMIT ? OFFSET ?", [10, 20]}
Specifies the columns to order by.
Handles qualified columns like #order("users.name") or aliases #order("u.name").
- @param fields [Symbol* | String*] The columns to order by (Symbol or "alias.column")
- @return [Query] The query object
Specifies the columns to order by with direction. Handles qualified columns or aliases in keys.
- @param fields [Hash(Symbol | String, Symbol)] Column => :asc/:desc
- @return [Query] The query object
Replaces the existing order clause with new ordering.
- @param fields [Symbol*] The fields to order by
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).order(:name).reorder(:age).to_sql
# => {"SELECT users.name FROM users ORDER BY users.age ASC", []}
Replaces the existing order clause with new ordering using hash syntax.
- @param fields [Hash] The fields and directions to order by
- @return [Query] The query object
Reverses the order of the query.
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).order(:name).reverse_order.to_sql
# => {"SELECT users.name FROM users ORDER BY users.name DESC", []}
Adds a RIGHT JOIN to the query with automatic relationship detection.
- @param table_or_alias [Symbol] The table to join
- @return [Query] The query object
Adds a RIGHT JOIN to the query using a block for the condition.
- @param table_or_alias [Symbol] The table to join
- @yield [FilterBuilder] The block to build the ON condition
- @return [Query] The query object
Adds RIGHT JOINs to the query using named arguments for table aliasing.
- @param tables_with_aliases [NamedTuple] Table name => alias
- @return [Query] The query object
Specifies the columns to select.
- @param columns [Symbol*] The columns to select
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name, :age).to_sql
# => {"SELECT users.name, users.age FROM users", []}
Specifies the columns to select, grouped by table name or alias.
- @param fields [Hash(Symbol, Array(Symbol) | Symbol)] The columns to select
- @return [Query] The query object
Example
AcmeDB.query.from(:users, :posts).select(users: [:name, :age], posts: [:title]).to_sql
# => {"SELECT users.name, users.age, posts.title FROM users, posts", []}
Example with aggregates
AcmeDB.query.from(:users).select(count: :id).to_sql
# => {"SELECT COUNT(users.id) FROM users", []}
Allow mixing String/Symbol args and Hash
Adds a SUM aggregate function to the query.
- @param column [Symbol] The column to sum
- @return [Query] The query object
Example
AcmeDB.query.from(:users).sum(:age).to_sql
# => {"SELECT SUM(users.age) FROM users", []}
Removes specific scopes from the query.
- @param scopes [Symbol*] The scopes to remove
- @return [Query] The query object
Example
AcmeDB.query.from(:users).select(:name).where(active: true).unscope(:where).to_sql
# => {"SELECT users.name FROM users", []}
Accept Hash(Symbol, DB::Any) for backward compatibility
ANDs an already built condition (typed DSL, CQL.raw) with the existing WHERE.
A raw SQL fragment with bound ? or :name values (not both in one fragment),
ANDed with other conditions.
AcmeDB.query.from(:users).where("lower(name) = ? AND age > ?", "ada", 18)
AcmeDB.query.from(:users).where("age > :age", age: 18)
Adds a greater-than condition for the specified field. Useful for cursor-based pagination and range queries.
- @param field [Symbol | String] The column to compare
- @param value [DB::Any] The value to compare against
- @return [Query] The query object
Example
# Find records with id greater than 100
AcmeDB.query.from(:users).where_gt(:id, 100).all(User)
Adds a less-than condition for the specified field. Useful for cursor-based pagination and range queries.
- @param field [Symbol | String] The column to compare
- @param value [DB::Any] The value to compare against
- @return [Query] The query object
Example
# Find records with id less than 100
AcmeDB.query.from(:users).where_lt(:id, 100).all(User)
Adds an IS NOT NULL condition for the specified field. Provides explicit non-null checking for better code clarity.
- @param field [Symbol | String] The column to check for not null
- @return [Query] The query object
Example
# Find posts with a body
AcmeDB.query.from(:posts).where_not_null(:body).all(Post)
Adds an IS NULL condition for the specified field. Provides explicit null checking for better code clarity.
- @param field [Symbol | String] The column to check for null
- @return [Query] The query object
Example
# Find posts without a body
AcmeDB.query.from(:posts).where_null(:body).all(Post)
Adds FUNCTION(...) OVER (PARTITION BY ... ORDER BY ... frame) AS name to the select list.
query = AcmeDB.query.from(:users)
query.window(:row_number, as: :age_rank, partition_by: :active, order_by: {age: :desc})
query.window(:sum, :age, as: :running_age, order_by: :id, rows: {:unbounded_preceding, 0})
query.window(:lag, :age, offset: 1, as: :previous_age, order_by: :id)
query.window(:ntile, buckets: 4, as: :quartile, order_by: :age)
Functions: row_number, rank, dense_rank, percent_rank, cume_dist, ntile,
lag, lead, first_value, last_value, count, sum, avg, min, max. Frame
bounds are offsets (negative = preceding) or :unbounded_preceding /
:unbounded_following. Without a preceding #select(...) only the window column
is selected; windows read plain table columns (not aggregates).
Declares WITH name AS (source); name can then be used in #from/#join
like a table, with the columns source selects.
adults = AcmeDB.query.from(:users).select(:name).where_gt(:age, 17)
AcmeDB.query.with(:adults, adults).from(:adults).to_sql
# => {"WITH adults AS (SELECT users.name FROM users WHERE users.age > ?) SELECT adults.name FROM adults", [17]}
Like #with, building the source from a query that can already see the CTEs
declared so far, so one CTE can read another.
Executes the query within a performance monitoring context. This is useful for grouping related queries together in performance reports.
- @param context [String] The context name (e.g., "UserController#index")
- @yield Block that executes the query
- @return The result of the block
Example
query = AcmeDB.query.from(:users).where(active: true)
users = query.with_performance_context("UserController#index") do
query.all(User)
end
Declares WITH RECURSIVE name AS (anchor UNION [ALL] step). The block receives
an empty query that already knows name, and returns the recursive step
(which cannot have ORDER BY / LIMIT; put those on the outer query).