module PgORM::Query

Overview

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)

Defined in:

pg-orm/query.cr
pg-orm/query/builder.cr
pg-orm/query/cache.cr
pg-orm/query/iterator.cr
pg-orm/query/methods.cr

Instance Method Summary

Instance Method Detail

def all : Collection(self) #

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

[View source]
def 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)

[View source]
def 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

[View source]
def 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)

[View source]
def 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

[View source]
def 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.


[View source]
def 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

[View source]
def 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"

[View source]
def 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)

[View source]
def 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},
])

[View source]
def 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.


[View source]
def find_by(**args) : self #

Finds the first record matching the given conditions.

Example

user = User.find_by(email: "[email protected]")
user = User.find_by(name: "John", active: true)

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


[View source]
def find_by?(**args) : self | Nil #

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

Example

user = User.find_by?(email: "[email protected]")
if user
  puts "Found: #{user.name}"
end

[View source]
def 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.


[View source]
def 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

[View source]
def first : self #

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.


[View source]
def first? : self | Nil #

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


[View source]
def 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)

[View source]
def ids : Array #

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}]

[View source]
def 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)

[View source]
def 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")

[View source]
def last : self #

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.


[View source]
def last? : self | Nil #

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


[View source]
def 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)

[View source]
def 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)

[View source]
def 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)

[View source]
def none : Collection(self) #

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)

[View source]
def 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)

[View source]
def 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

[View source]
def order(*columns : Symbol | String) : Collection(self) #

Orders results by column names (ascending by default).

Example

User.order(:name, :age)

[View source]
def order(**columns) : Collection(self) #

Orders results using keyword arguments.

Example

User.order(name: :asc, created_at: :desc)

[View source]
def paginate(page : Int32 = 1, limit : Int32 = 25) : PaginatedResult(self) #

Paginate results using page number (1-indexed)


[View source]
def paginate_by_offset(offset : Int32 = 0, limit : Int32 = 25) : PaginatedResult(self) #

Paginate results using offset and limit directly


[View source]
def 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


[View source]
def 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)

[View source]
def 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)

[View source]
def reorder(*columns : Symbol | String) : Collection(self) #

Replaces any existing ORDER BY clause with new columns.

Example

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

[View source]
def reorder(**columns) : Collection(self) #

Replaces any existing ORDER BY clause using keyword arguments.

Example

User.order(:name).reorder(age: :desc, created_at: :asc)

[View source]
def 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)

[View source]
def 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)

[View source]
def 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)

[View source]
def take : self #

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.


[View source]
def take? : self | Nil #

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

Example

user = User.where(active: true).take?
puts user ? user.name : "No active users"

[View source]
def 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)

[View source]
def 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")

[View source]
def 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})

[View source]
def 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)

[View source]
def where(**conditions) : Collection(self) #

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])

[View source]
def where_between(column : Symbol | String, min : Value, max : Value) : Collection(self) #

BETWEEN range comparison


[View source]
def where_gt(column : Symbol | String, value : Value) : Collection(self) #

Greater than comparison


[View source]
def where_gte(column : Symbol | String, value : Value) : Collection(self) #

Greater than or equal comparison


[View source]
def where_ilike(column : Symbol | String, pattern : String) : Collection(self) #

Pattern matching with ILIKE operator


[View source]
def where_like(column : Symbol | String, pattern : String) : Collection(self) #

Pattern matching with LIKE operator


[View source]
def where_lt(column : Symbol | String, value : Value) : Collection(self) #

Less than comparison


[View source]
def where_lte(column : Symbol | String, value : Value) : Collection(self) #

Less than or equal comparison


[View source]
def 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]})

[View source]
def where_not(**conditions) : Collection(self) #

Adds WHERE NOT conditions using keyword arguments.

Example

User.where_not(status: "deleted", banned: true)

[View source]
def where_not_between(column : Symbol | String, min : Value, max : Value) : Collection(self) #

NOT BETWEEN range comparison


[View source]
def where_not_ilike(column : Symbol | String, pattern : String) : Collection(self) #

Negated pattern matching with NOT ILIKE


[View source]
def where_not_like(column : Symbol | String, pattern : String) : Collection(self) #

Negated pattern matching with NOT LIKE


[View source]