module

PgORM::Query

Query module provides the primary interface for building and executing database queries. Most methods are delegated to a Collection(T) object which provides a chainable, immutable query builder interface.

Basic Usage

# Find all users
User.all.to_a

# Find with conditions
User.where(active: true).order(:name).limit(10).to_a

# Find by ID
user = User.find(123)

# Aggregate queries
User.where(active: true).count
User.sum(:age)

Instance methods

all

Returns a collection representing all records in the table.

This doesn't execute a query immediately - it returns a lazy collection that will query the database when you call a terminating method like .to_a, .each, .first, etc.

Example

users = User.all # No query executed yet
users.to_a       # Now the query executes
Source
average(column_name : Symbol | String) : Float64

Calculates the average of a numeric column.

Always returns Float64.

Example

# Average age
avg_age = User.average(:age) # => 32.5

# Average with conditions
avg_salary = User.where(department: "Engineering").average(:salary)
Source
count(column_name : Symbol | String = "*", distinct = false) : Int64

Counts the number of records matching the current query scope.

Can count all records, a specific column, or distinct values.

Example

# Count all users
User.count # => 150

# Count with conditions
User.where(active: true).count # => 120

# Count specific column
User.count(:email) # => 150

# Count distinct values
User.count(:city, distinct: true) # => 25
Source
distinct(value = true) : Collection(self)

Adds DISTINCT to the query to remove duplicate rows.

Example

# Get unique cities
cities = User.select(:city).distinct.pluck(:city)

# Disable distinct
User.distinct(false)
Source
exists?(id) : Bool

Checks if a record with the given primary key exists.

More efficient than loading the full record when you only need to check existence.

Example

if User.exists?(123)
  puts "User exists"
end

# With scope
if User.where(active: true).exists?(123)
  puts "Active user exists"
end
Source
find(id) : self

Finds a record by its primary key value.

For single primary keys, pass the ID directly. For composite primary keys, pass a tuple of values.

Example

# Single primary key
user = User.find(123)

# Composite primary key
record = CompositeModel.find({"key1", "key2"})

Raises Error::RecordNotFound if the record doesn't exist.

Source
find!(id) : self

Same as #find but with explicit exception raising.

Useful when you want to be explicit about the error handling.

Example

user = User.find!(123) # Raises Error::RecordNotFound if not found
Source
find?(id) : self | Nil

Same as #find but returns nil if the record doesn't exist instead of raising an exception.

Example

user = User.find?(123)
puts user ? user.name : "Not found"
Source
find_all(ids : Enumerable(Value)) : Collection(self)

Finds multiple records by an array of primary key values.

Returns a collection (not an array) so you can chain additional query methods. For models with single primary keys only.

Example

users = User.find_all([1, 2, 3, 4, 5])
active_users = User.find_all([1, 2, 3]).where(active: true)
Source
find_all(ids : Enumerable(Enumerable)) : Collection(self)

Finds multiple records by composite primary key values.

Pass an array of tuples/arrays, where each tuple contains the values for all components of the composite key.

Example

# For a model with primary_key :tenant_id, :user_id
records = CompositeModel.find_all([
  {1, 100},
  {1, 101},
  {2, 200},
])
Source
find_all_by_sql(sql : String, *args_, args : Array | Nil = nil) : Array(self)

Loads records by raw SQL query with parameter binding.

Use $1, $2, etc. for parameter placeholders (PostgreSQL style). Parameters are passed as additional arguments after the SQL string.

Example

# Single parameter
users = User.find_all_by_sql("SELECT * FROM users WHERE username = $1", "john")

# Multiple parameters
users = User.find_all_by_sql(
  "SELECT * FROM users WHERE age > $1 AND city = $2",
  18,
  "NYC"
)

# With array of parameters
params = ["john", "doe"]
users = User.find_all_by_sql("SELECT * FROM users WHERE first_name = $1 OR last_name = $2", args: params)

Returns an array of model instances.

Source
find_by

Finds the first record matching the given conditions.

Example

user = User.find_by(email: "john@example.com")
user = User.find_by(name: "John", active: true)

Raises Error::RecordNotFound if no matching record is found.

Source
find_by?

Same as #find_by but returns nil if no record is found.

Example

user = User.find_by?(email: "john@example.com")
if user
  puts "Found: #{user.name}"
end
Source
find_one_by_sql(sql : String, *args_, args : Array | Nil = nil) : self

Loads one record by raw SQL query with parameter binding.

Use $1, $2, etc. for parameter placeholders (PostgreSQL style). Always include LIMIT 1 in your SQL for performance.

Example

user = User.find_one_by_sql(
  "SELECT * FROM users WHERE username = $1 LIMIT 1",
  "john"
)

Raises Error::RecordNotFound if no record is found.

Source
find_one_by_sql?(sql : String, *args_, args : Array | Nil = nil) : self | Nil

Same as #find_one_by_sql but returns nil when no record is found instead of raising an exception.

Example

user = User.find_one_by_sql?(
  "SELECT * FROM users WHERE username = $1 LIMIT 1",
  "john"
)

if user
  puts "Found: #{user.name}"
else
  puts "Not found"
end
Source
first

Returns the first record, ordered by primary key ascending.

If an order is already specified, uses that ordering instead.

Example

user = User.first                          # ORDER BY id ASC
user = User.order(:name).first             # ORDER BY name ASC
user = User.order(created_at: :desc).first # Uses specified order

Raises Error::RecordNotFound if no records exist.

Source
first?

Same as #first but returns nil if no records exist.

Source
group_by(*columns : Symbol | String) : Collection(self)

Groups results by one or more columns.

Typically used with aggregate functions like COUNT, SUM, etc.

Example

# Count users by city
User.select("city, COUNT(*) as count").group_by(:city)

# Multiple columns
User.select("city, state, COUNT(*)").group_by(:city, :state)
Source
ids

Loads all primary key values matching the current query scope.

This is more efficient than loading full records when you only need IDs. For composite primary keys, returns tuples.

Example

# Simple primary key
user_ids = User.where(active: true).ids
# => [1, 2, 3, 4, 5]

# Composite primary key
keys = CompositeModel.where(status: "active").ids
# => [{key1: "a", key2: 1}, {key1: "b", key2: 2}]
Source
join(type : JoinType, model : Base.class, fk : Symbol, pk : Base.class | Nil = nil) : Collection(self)

Performs a SQL JOIN with another table.

Supports LEFT, RIGHT, INNER, and FULL joins.

Example

# Join with foreign key
User.join(:left, Group, :group_id)

# Join with explicit primary key
User.join(:inner, Group, :group_id, Group)
Source
join(type : JoinType, model : Base.class, on : String) : Collection(self)

Performs a SQL JOIN with a custom ON clause.

Example

# Custom join condition
User.join(:left, Group, "groups.id = users.group_id AND groups.active = true")
Source
last

Returns the last record, ordered by primary key descending.

If an order is already specified, reverses that ordering.

Example

user = User.last                          # ORDER BY id DESC
user = User.order(:name).last             # ORDER BY name DESC
user = User.order(created_at: :desc).last # ORDER BY created_at ASC

Raises Error::RecordNotFound if no records exist.

Source
last?

Same as #last but returns nil if no records exist.

Source
limit(value : Int32) : Collection(self)

Limits the number of records returned.

Example

# Get first 10 users
User.limit(10).to_a

# Combine with other methods
User.where(active: true).order(:name).limit(20)
Source
maximum(column_name : Symbol | String)

Finds the maximum value in a column.

Example

# Oldest user
max_age = User.maximum(:age) # => 75

# Most recent record
last_created = User.maximum(:created_at)
Source
minimum(column_name : Symbol | String)

Finds the minimum value in a column.

Example

# Youngest user
min_age = User.minimum(:age) # => 18

# Earliest created record
first_created = User.minimum(:created_at)
Source
none

Returns a collection that will never return any records.

Useful for conditional query building where you want to ensure no results are returned in certain cases.

Example

scope = user.admin? ? User.all : User.none
scope.to_a # => [] (if not admin)
Source
offset(value : Int32) : Collection(self)

Skips the specified number of records.

Useful for pagination when combined with limit.

Example

# Skip first 20 records
User.offset(20).limit(10).to_a

# Page 3 (20 per page)
page = 3
per_page = 20
User.offset((page - 1) * per_page).limit(per_page)
Source
order(columns : Hash(Symbol, Symbol)) : Collection(self)

Orders results by one or more columns.

Can specify direction (:asc or :desc) for each column. Multiple calls to order are cumulative.

Example

# Single column ascending (default)
User.order(:name)

# Single column descending
User.order(created_at: :desc)

# Multiple columns
User.order({name: :asc, created_at: :desc})
User.order(name: :asc, age: :desc)

# Cumulative ordering
User.order(:name).order(:age) # ORDER BY name, age
Source
order(*columns : Symbol | String) : Collection(self)

Orders results by column names (ascending by default).

Example

User.order(:name, :age)
Source
order

Orders results using keyword arguments.

Example

User.order(name: :asc, created_at: :desc)
Source
paginate(page : Int32 = 1, limit : Int32 = 25) : PaginatedResult(self)

Paginate results using page number (1-indexed)

Source
paginate_by_offset(offset : Int32 = 0, limit : Int32 = 25) : PaginatedResult(self)

Paginate results using offset and limit directly

Source
paginate_cursor(after : String | Nil = nil, before : String | Nil = nil, limit : Int32 = 25, cursor_column : Symbol = :id) : CursorPaginatedResult(self)

Cursor-based pagination for efficient large dataset traversal

Source
pluck(column_name : Symbol | String) : Array(Value)

Extracts values from a single column as an array.

More efficient than loading full records when you only need one column. Can also be used with raw SQL expressions.

Example

# Column name
names = User.where(active: true).pluck(:name)
# => ["John", "Jane", "Bob"]

# Multiple queries
emails = User.pluck(:email)
ages = User.pluck(:age)

# Works with scopes
admin_names = User.where(role: "admin").pluck(:name)
Source
reorder(columns : Hash(Symbol, Symbol)) : Collection(self)

Replaces any existing ORDER BY clause.

Unlike order which is cumulative, reorder discards previous ordering.

Example

# Original order is discarded
User.order(:name).reorder(:age) # ORDER BY age (not name, age)

# Useful for overriding default scopes
User.order(:name).reorder(created_at: :desc)
Source
reorder(*columns : Symbol | String) : Collection(self)

Replaces any existing ORDER BY clause with new columns.

Example

User.order(:name).reorder(:age, :created_at)
Source
reorder

Replaces any existing ORDER BY clause using keyword arguments.

Example

User.order(:name).reorder(age: :desc, created_at: :asc)
Source
select(sql : String) : Collection(self)

Specifies a raw SQL SELECT clause.

Useful for complex selections, aggregations, or PostgreSQL-specific functions.

Example

# With SQL functions
User.select("id, UPPER(name) as name, LENGTH(email) as email_length")

# With aggregations
User.select("department, COUNT(*) as employee_count").group_by(:department)
Source
select(*columns : Symbol) : Collection(self)

Specifies which columns to SELECT in the query.

By default, all columns are selected (SELECT *). Use this to optimize queries by only loading the columns you need.

Example

# Select specific columns
users = User.select(:id, :name, :email).to_a

# Chain with other methods
User.select(:id, :name).where(active: true).order(:name)
Source
sum(column_name : Symbol | String) : Int64 | Float64

Calculates the sum of a numeric column.

Returns Int64 for integer columns, Float64 for decimal columns.

Example

# Sum of all salaries
total = User.sum(:salary) # => 1500000

# Sum with conditions
active_total = User.where(active: true).sum(:salary)
Source
take

Returns one record without any specific ordering.

Faster than first when you don't care about order. Useful for checking if any records exist.

Example

user = User.where(active: true).take

Raises Error::RecordNotFound if no records exist.

Source
take?

Same as #take but returns nil if no records exist.

Example

user = User.where(active: true).take?
puts user ? user.name : "No active users"
Source
where(sql : String, args : Enumerable) : Collection(self)

Adds WHERE clause with raw SQL and an array of parameters.

Example

params = [18, "NYC"]
User.where("age > ? AND city = ?", params)
Source
where(sql : String) : Collection(self)

Adds a WHERE clause with raw SQL.

Use this for complex conditions that can't be expressed with the hash syntax.

Example

# Simple raw SQL
User.where("age > 18")

# With PostgreSQL functions
User.where("LENGTH(name) > 10")
Source
where(conditions : Hash(Symbol, Value | Array(Value)) | NamedTuple) : Collection(self)

Adds WHERE conditions using a hash or named tuple.

Supports equality, NULL checks, and IN queries with arrays.

Example

# Equality
User.where({name: "John", active: true})

# NULL check
User.where({deleted_at: nil})

# IN query
User.where({id: [1, 2, 3, 4, 5]})

# Named tuple
User.where({name: "John", age: 30})
Source
where(sql : String, *splat : Value) : Collection(self)

Adds WHERE clause with raw SQL and parameter binding.

Use ? as placeholders for parameters (converted to $1, $2, etc.).

Example

# Single parameter
User.where("age > ?", 18)

# Multiple parameters
User.where("age > ? AND city = ?", 18, "NYC")

# With PostgreSQL functions
User.where("LENGTH(name) > ?", 10)
Source
where

Adds WHERE conditions using keyword arguments.

Syntactic sugar for the hash-based where method.

Example

User.where(name: "John", active: true)
User.where(age: 30, city: "NYC")
User.where(id: [1, 2, 3])
Source
where_between(column : Symbol | String, min : Value, max : Value) : Collection(self)

BETWEEN range comparison

Source
where_gt(column : Symbol | String, value : Value) : Collection(self)

Greater than comparison

Source
where_gte(column : Symbol | String, value : Value) : Collection(self)

Greater than or equal comparison

Source
where_ilike(column : Symbol | String, pattern : String) : Collection(self)

Pattern matching with ILIKE operator

Source
where_like(column : Symbol | String, pattern : String) : Collection(self)

Pattern matching with LIKE operator

Source
where_lt(column : Symbol | String, value : Value) : Collection(self)

Less than comparison

Source
where_lte(column : Symbol | String, value : Value) : Collection(self)

Less than or equal comparison

Source
where_not(conditions : Hash(Symbol, Value | Array(Value)) | NamedTuple) : Collection(self)

Adds WHERE NOT conditions using a hash or named tuple.

Negates the conditions - finds records that DON'T match.

Example

# Not equal
User.where_not({status: "deleted"})

# Not NULL
User.where_not({deleted_at: nil})

# NOT IN
User.where_not({id: [1, 2, 3]})
Source
where_not

Adds WHERE NOT conditions using keyword arguments.

Example

User.where_not(status: "deleted", banned: true)
Source
where_not_between(column : Symbol | String, min : Value, max : Value) : Collection(self)

NOT BETWEEN range comparison

Source
where_not_ilike(column : Symbol | String, pattern : String) : Collection(self)

Negated pattern matching with NOT ILIKE

Source
where_not_like(column : Symbol | String, pattern : String) : Collection(self)

Negated pattern matching with NOT LIKE

Source

Nested types