class Lustra::Model::CollectionBase(T)

Overview

CollectionBase(T) is the base class for collection of model. Collection of model are a SQL SELECT query mapping & building system. They are Enumerable and are `Lustra::SQL::SelectBuilder` behavior; therefore, they can be used array-like and are working with low-level SQL Building.

The CollectionBase(T) is extended by each model. For example, generating the model MyModel will generate the class MyModel::Collection which inherits from CollectionBase(MyModel)

Collection are instantiated using Model.query method.

Included Modules

Direct Known Subclasses

Defined in:

lustra/model/collection.cr

Instance Method Summary

Instance methods inherited from module Lustra::SQL::SelectBuilder

before_query(&block : -> Nil) before_query, columns : Array(SQL::Column) columns, default_wildcard_table default_wildcard_table, dup : self dup, havings : Array(Lustra::Expression::Node) havings, is_distinct? is_distinct?, joins : Array(SQL::Join) joins, limit : Int64 | Nil limit, lock : String | Nil lock, offset : Int64 | Nil offset, order_bys : Array(Lustra::SQL::Query::OrderBy::Record) order_bys, to_delete to_delete, to_sql : String to_sql, to_update to_update, total_entries : Int64 | Nil total_entries, total_entries=(total_entries : Int64 | Nil) total_entries=, wheres : Array(Lustra::Expression::Node) wheres

Constructor methods inherited from module Lustra::SQL::SelectBuilder

new(distinct_value = nil, cte = {} of String => Lustra::SQL::SelectBuilder | String, columns = [] of SQL::Column, froms = [] of SQL::From, joins = [] of SQL::Join, wheres = [] of Lustra::Expression::Node, havings = [] of Lustra::Expression::Node, windows = [] of ::Tuple(String, String), group_bys = [] of Symbolic, order_bys = [] of Lustra::SQL::Query::OrderBy::Record, limit = nil, offset = nil, lock = nil, before_query_triggers = [] of (-> Nil)) new

Instance methods inherited from module Lustra::SQL::Query::WithPagination

current_page : Int32 | Int64 current_page, first_page? first_page?, last_page? last_page?, next_page next_page, out_of_bounds? out_of_bounds?, paginate(page : Int32 = DEFAULT_PAGE, per_page : Int32 = DEFAULT_LIMIT) paginate, per_page : Int32 | Int64 per_page, previous_page previous_page, total_pages : Int32 | Int64 total_pages

Instance methods inherited from module Lustra::SQL::Query::Change

change! : self change!

Instance methods inherited from module Lustra::SQL::Query::Connection

connection_name : String connection_name, use_connection(connection_name : String) use_connection

Instance methods inherited from module Lustra::SQL::Query::Pluck

pluck(fields : Tuple(*T)) forall T
pluck(*fields) : Array
pluck(**fields : **T) forall T
pluck
, pluck_col(field : Lustra::SQL::Symbolic, type : T.class) : Array(T) forall T
pluck_col(field : Lustra::SQL::Symbolic) : Array(Lustra::SQL::Any)
pluck_col

Instance methods inherited from module Lustra::SQL::Query::Fetch

fetch(fetch_all = false, & : Hash(String, Lustra::SQL::Any) -> Nil) fetch, fetch_first fetch_first, fetch_first! fetch_first!, fetch_with_cursor(count = 1000, & : Hash(String, Lustra::SQL::Any) -> Nil) fetch_with_cursor, first first, first! first!, scalar(type : T.class) forall T scalar, to_a : Array(Hash(String, Lustra::SQL::Any)) to_a

Instance methods inherited from module Lustra::SQL::Query::Execute

execute(connection_name : String | Nil = nil) execute, execute_and_count(connection_name : String | Nil = nil) : Int64 execute_and_count, explain(connection_name : String | Nil = nil) : String explain, explain_analyze(connection_name : String | Nil = nil) : String explain_analyze

Instance methods inherited from module Lustra::SQL::Query::Lock

with_lock(str : String = "FOR UPDATE") with_lock

Instance methods inherited from module Lustra::SQL::Query::Window

clear_windows clear_windows, print_windows print_windows, window(name, value)
window(windows : NamedTuple)
window
, windows : Array(WindowDeclaration) windows

Instance methods inherited from module Lustra::SQL::Query::CTE

cte : Hash(String, CTEAuthorized) cte, with_cte(name, request : CTEAuthorized)
with_cte(tuple : NamedTuple)
with_cte

Instance methods inherited from module Lustra::SQL::Query::Aggregate

agg(field, x : X.class) forall X agg, avg(field, x : X.class) forall X avg, count(type : X.class = Int64) forall X count, exists? : Bool exists?, max(field, x : X.class) forall X max, min(field, x : X.class) forall X min, sum(field) : Float64 sum

Instance methods inherited from module Lustra::SQL::Query::OffsetLimit

clear_limit clear_limit, clear_offset clear_offset, limit(x : Int | Nil) limit, offset(x : Int | Nil) offset

Instance methods inherited from module Lustra::SQL::Query::GroupBy

clear_group_bys clear_group_bys, group_by(column : Symbolic) group_by, group_bys : Array(Symbolic) group_bys

Instance methods inherited from module Lustra::SQL::Query::OrderBy

clear_order_bys clear_order_bys, order_by(tuple : NamedTuple)
order_by(expression : Symbol, direction : Symbol = :asc, nulls : Symbol | Nil = nil)
order_by(expression : String, direction : Symbol = :asc, nulls : Symbol | Nil = nil)
order_by(**tuple)
order_by
, reverse_order_by reverse_order_by

Instance methods inherited from module Lustra::SQL::Query::Having

clear_havings clear_havings, having(node : Lustra::Expression::Node)
having(&)
having(conditions : NamedTuple | Hash(String, Lustra::SQL::Any))
having(template : String, *args)
having(template : String, **tuple)
having(**tuple)
having
, or_having(node : Lustra::Expression::Node)
or_having(template : String, *args)
or_having(template : String, **named_tuple)
or_having(&)
or_having

Instance methods inherited from module Lustra::SQL::Query::Where

clear_wheres clear_wheres, not(&)
not(conditions : NamedTuple | Hash(String, Lustra::SQL::Any))
not(template : String)
not(template : String, *args)
not(template : String, **tuple)
not(**tuple)
not
, or(node : Lustra::Expression::Node)
or(&)
or(conditions : NamedTuple | Hash(String, Lustra::SQL::Any))
or(template : String, *args)
or(template : String, **tuple)
or(**tuple)
or
, where(node : Lustra::Expression::Node)
where(&)
where(conditions : NamedTuple | Hash(String, Lustra::SQL::Any))
where(template : String)
where(template : String, *args)
where(template : String, **tuple)
where(**tuple)
where

Instance methods inherited from module Lustra::SQL::Query::Join

cross_join(name : Selectable, lateral = false) cross_join, full_outer_join(name : Selectable, lateral = false, &)
full_outer_join(name : Selectable, condition : String = "true", lateral = false)
full_outer_join
, inner_join(name : Selectable, lateral = false, &)
inner_join(name : Selectable, condition : String = "true", lateral = false)
inner_join
, join(name : Selectable, type = :inner, lateral = false, &)
join(name : Selectable, type = :inner, condition : String = "true", lateral = false)
join(name : Selectable, type = :inner, lateral = false)
join
, left_join(name : Selectable, lateral = false, &)
left_join(name : Selectable, condition : String = "true", lateral = false)
left_join
, right_join(name : Selectable, lateral = false, &)
right_join(name : Selectable, condition : String = "true", lateral = false)
right_join

Instance methods inherited from module Lustra::SQL::Query::From

clear_from clear_from, from(*args)
from(**tuple)
from
, froms : Array(SQL::From) froms

Instance methods inherited from module Lustra::SQL::Query::Select

clear_distinct clear_distinct, clear_select clear_select, default_wildcard_table=(table : String | Nil = nil) default_wildcard_table=, distinct(on : String | Nil = "") distinct, distinct_value : String | Nil distinct_value, select(c : Column)
select(*args)
select

Instance Method Detail

def <<(item : T) #

Add an item to the current collection.

If the current collection is not originated from a has_many or has_many through: relation, calling << over the collection will raise a Lustra::SQL::OperationNotPermittedError

Returns self and therefore can be chained


[View source]
def [](range : Range(Number, Number), fetch_columns = false) : Array(T) #

Get a range of models


[View source]
def [](off, fetch_columns = false) : T #

Basically a fancy way to write OFFSET x LIMIT 1


[View source]
def []?(off, fetch_columns = false) : T | Nil #

Basically a fancy way to write OFFSET x LIMIT 1


[View source]
def add(item : T) #

Alias for Collection#<<


[View source]
def any? #

Check whether the query return any row.


[View source]
def association_name : String | Nil #

[View source]
def association_name=(association_name : String | Nil) #

[View source]
def autosave=(autosave : Bool) #

[View source]
def autosave? : Bool #

[View source]
def build(x : NamedTuple, &block : T -> Nil) : T #

Build a new collection; if the collection comes from a has_many relation (e.g. my_model.associations.build), the foreign column which store the primary key of my_model will be setup by default, preventing you to forget it. You can pass extra parameters using a named tuple: my_model.associations.build({a_column: "value"})


[View source]
def build(**tuple, & : T -> Nil) : T #

Build a new collection; if the collection comes from a has_many relation (e.g. my_model.associations.build), the foreign column which store the primary key of my_model will be setup by default, preventing you to forget it. You can pass extra parameters using a named tuple: my_model.associations.build({a_column: "value"})


[View source]
def build(x : NamedTuple) : T #

Build a new collection; if the collection comes from a has_many relation (e.g. my_model.associations.build), the foreign column which store the primary key of my_model will be setup by default, preventing you to forget it. You can pass extra parameters using a named tuple: my_model.associations.build({a_column: "value"})


[View source]
def build(**tuple) : T #

Build a new collection; if the collection comes from a has_many relation (e.g. my_model.associations.build), the foreign column which store the primary key of my_model will be setup by default, preventing you to forget it. You can pass extra parameters using a named tuple: my_model.associations.build({a_column: "value"})


[View source]
def count(type : X.class = Int64) forall X #

Use SQL COUNT over your query, and return this number as a Int64


[View source]
def create(x : NamedTuple, &block : T -> Nil) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def create(**tuple, & : T -> Nil) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def create(x : NamedTuple) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def create(**tuple) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def create!(x : NamedTuple, &block : T -> Nil) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object. But instead of returning self if validation failed, raise Lustra::Model::InvalidError exception


[View source]
def create!(**tuple, & : T -> Nil) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object. But instead of returning self if validation failed, raise Lustra::Model::InvalidError exception


[View source]
def create!(x : NamedTuple) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object. But instead of returning self if validation failed, raise Lustra::Model::InvalidError exception


[View source]
def create!(**tuple) : T #

Build a new object and setup the fields like setup in the condition tuple. Just after building, save the object. But instead of returning self if validation failed, raise Lustra::Model::InvalidError exception


[View source]
def delete_all : self #

Delete all the rows which would have been returned by this collection WITHOUT callbacks. This is a bulk operation that doesn't load models into memory. Is equivalent to collection.to_delete.execute

User.query.where { active == false }.delete_all

Returns self for chaining.


[View source]
def destroy_all : self #

Destroy all the rows which would have been returned by this collection WITH callbacks. This loads each model into memory and calls destroy on it, triggering all :delete callbacks. Use #delete_all if you don't need callbacks (much faster for large datasets).

# With callbacks - slower but safe
User.query.where { active == false }.destroy_all

# Without callbacks - faster
User.query.where { active == false }.delete_all

Returns self for chaining.


[View source]
def dup #
Description copied from module Lustra::SQL::SelectBuilder

Duplicate the query


[View source]
def each(fetch_columns = false, & : T -> ) : Nil #

Build the SQL, send the query then iterate through each models gathered by the request.


[View source]
def each_with_cursor(batch = 1000, fetch_columns = false, &block : T -> ) #

Build the SQL, send the query then iterate through each models gathered by the request. Use a postgres cursor to avoid memory bloating. Useful to fetch millions of rows at once.


[View source]
def empty? #

Inverse of #any?, return true if the request return no rows.


[View source]
def find(fetch_columns = false, &) : T | Nil #

A convenient way to write where { condition }.first(fetch_columns)


[View source]
def find(tuple : NamedTuple, fetch_columns = false) : T | Nil #

A convenient way to write where({any_column: "any_value"}).first(fetch_columns)


[View source]
def find(**tuple) : T | Nil #

A convenient way to write where({any_column: "any_value"}).first


[View source]
def find!(fetch_columns = false, &) : T #

A convenient way to write where { condition }.first!(fetch_columns)


[View source]
def find!(tuple : NamedTuple, fetch_columns = false) : T #

A convenient way to write where({any_column: "any_value"}).first!(fetch_columns)


[View source]
def find!(**tuple) : T #

A convenient way to write where({any_column: "any_value"}).first!


[View source]
def find_by(tuple : NamedTuple, fetch_columns = false) : T | Nil #

Find a model by column values. Returns nil if not found. This is an alias for #find(**tuple) with better naming.

user = User.query.find_by(email: "test@example.com")
user = User.query.where { active == true }.find_by(role: "admin")

[View source]
def find_by(**tuple) : T | Nil #

Find a model by column values. Returns nil if not found. This is an alias for #find(**tuple) with better naming.

user = User.query.find_by(email: "test@example.com")
user = User.query.where { active == true }.find_by(role: "admin")

[View source]
def find_by!(tuple : NamedTuple, fetch_columns = false) : T #

Find a model by column values. Raises error if not found. This is an alias for #find!(**tuple) with better naming.

user = User.query.find_by!(email: "test@example.com")

[View source]
def find_by!(**tuple) : T #

Find a model by column values. Raises error if not found. This is an alias for #find!(**tuple) with better naming.

user = User.query.find_by!(email: "test@example.com")

[View source]
def find_or_build(x : NamedTuple, &block : T -> Nil) : T #

[View source]
def find_or_build(**tuple, & : T -> Nil) : T #

Try to fetch a row. If not found, build a new object and setup the fields like setup in the condition tuple.


[View source]
def find_or_build(x : NamedTuple) : T #

[View source]
def find_or_build(**tuple) : T #

[View source]
def find_or_create(x : NamedTuple, &block : T -> Nil) : T #

Try to fetch a row. If not found, build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def find_or_create(**tuple, & : T -> Nil) : T #

Try to fetch a row. If not found, build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def find_or_create(x : NamedTuple) : T #

Try to fetch a row. If not found, build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def find_or_create(**tuple) : T #

Try to fetch a row. If not found, build a new object and setup the fields like setup in the condition tuple. Just after building, save the object.


[View source]
def first(fetch_columns = false) : T | Nil #

Get the first row from the collection query. if not found, return nil


[View source]
def first!(fetch_columns = false) : T #

Get the first row from the collection query. if not found, throw an error


[View source]
def full_outer_join(association : Lustra::SQL::Symbolic, lateral = false) #

FULL_OUTER JOIN using association name (auto-detects join conditions)


[View source]
def ids : Array(Lustra::SQL::Any) #

Convenient shortcut to get an array of primary key values. Equivalent to pluck_col(T.__pkey__) but more readable.

User.query.where { active == true }.ids
# => [1, 2, 3, 4, 5]

Post.query.where { published == true }.ids
# => [10, 25, 42, 100]

Returns an array of primary key values (typically Array(Int64) or Array(Int32)).


[View source]
def inner_join(association : Lustra::SQL::Symbolic, lateral = false) #

INNER JOIN using association name (auto-detects join conditions)


[View source]
def item_class #

Return the model class for this collection


[View source]
def join(association : Lustra::SQL::Symbolic, type = :inner, lateral = false) #

Join a relation using association name (auto-detects join conditions) Overrides the parent join to handle association names without blocks


[View source]
def last(fetch_columns = false) : T | Nil #

Get the last row from the collection query. if not found, return nil


[View source]
def last!(fetch_columns = false) : T #

Get the last row from the collection query. if not found, throw an error


[View source]
def left_join(association : Lustra::SQL::Symbolic, lateral = false) #

LEFT JOIN using association name (auto-detects join conditions)


[View source]
def map(fetch_columns = false, &block : T -> X) : Array(X) forall X #

Build the SQL, send the query then build and array by applying the block transformation over it.


[View source]
def parent_model : Lustra::Model | Nil #

Parent model context for autosave functionality


[View source]
def parent_model=(parent_model : Lustra::Model | Nil) #

Parent model context for autosave functionality


[View source]
def right_join(association : Lustra::SQL::Symbolic, lateral = false) #

RIGHT JOIN using association name (auto-detects join conditions)


[View source]
def save!(item : T) #

Save a model and handle append_operation for has_many through relationships This allows the build + save pattern to work


[View source]
def tags #

[View source]
def to_a(fetch_columns = false) : Array(T) #

Create an array from the query.


[View source]
def unlink(item : T) #

Unlink the model currently referenced through a relation has_many through

If the current colleciton doesn't come from a has_many through relation, this method will throw Lustra::SQL::OperationNotPermittedError

Returns true if unlinking is successful (e.g. one or more rows have been updated), or false otherwise


[View source]
def update_all(fields : NamedTuple) : Int64 #

Update all the rows which would have been returned by this collection without loading the models into memory. Bypasses validations and callbacks.

This is useful for bulk updates where you don't need to run validations or callbacks on each individual model.

# Update all inactive users to have a specific status
affected = User.query.where { active == false }.update_all(status: "inactive")
puts "Updated #{affected} users"

# Update multiple columns at once
Post.query.where { published == false }.update_all(published: true, published_at: Time.utc)

# With complex conditions
User.query.where { created_at < 1.year.ago }.update_all(archived: true)

Returns the number of rows affected.


[View source]
def update_all(fields : Hash(String, Lustra::SQL::Any)) : Int64 #

Update all the rows which would have been returned by this collection without loading the models into memory. Bypasses validations and callbacks.

This is useful for bulk updates where you don't need to run validations or callbacks on each individual model.

# Update all inactive users to have a specific status
affected = User.query.where { active == false }.update_all(status: "inactive")
puts "Updated #{affected} users"

# Update multiple columns at once
Post.query.where { published == false }.update_all(published: true, published_at: Time.utc)

# With complex conditions
User.query.where { created_at < 1.year.ago }.update_all(archived: true)

Returns the number of rows affected.


[View source]
def update_all(**fields) : Int64 #

Update all the rows which would have been returned by this collection without loading the models into memory. Bypasses validations and callbacks.

This is useful for bulk updates where you don't need to run validations or callbacks on each individual model.

# Update all inactive users to have a specific status
affected = User.query.where { active == false }.update_all(status: "inactive")
puts "Updated #{affected} users"

# Update multiple columns at once
Post.query.where { published == false }.update_all(published: true, published_at: Time.utc)

# With complex conditions
User.query.where { created_at < 1.year.ago }.update_all(archived: true)

Returns the number of rows affected.


[View source]