Module: Sqlx::Query
- Included in:
- Base
- Defined in:
- runtime/sqlx/query.rb,
runtime/sqlx/query.rbs
Overview
The query surface — every class-side entry point that runs or assembles SQL. Extended onto Base so each method arrives as a model class method; the bodies reach the declaration surface (Model) through the same receiver.
Instance Method Summary collapse
-
#count(conditions = {}) ⇒ Integer
how many rows of the model's table match.
-
#create!(attrs) ⇒ Object
insert one row from a column/value hash and hand back the persisted record.
-
#delete!(id) ⇒ Integer
remove the row with this
id. -
#delete_all!(conditions) ⇒ Integer
remove every row matching the conditions hash: the multi-row counterpart of
delete!, assembled from the model'stable_nameand the inheritedwhere_clause. -
#exists?(conditions = {}) ⇒ Boolean
whether a matching row exists, answered from `SELECT 1 ...
-
#fetch_many(conditions, order: nil, limit: nil, offset: nil) ⇒ Array
the multi-row counterpart of
find_by: every row matching the conditions hash, assembled from the model'stable_nameandonly_columnsdeclarations. -
#find(id) ⇒ Object
the lookup by the
idcolumn: the matching record, orRecordNotFound— never nil, so a missing row becomes a 404 at the call site instead of a nil that fails somewhere downstream. -
#find_by(conditions, order: nil) ⇒ Object?
the single-row lookup by a conditions hash, assembled from the model's
table_nameandonly_columnsdeclarations. -
#find_by!(conditions) ⇒ Object
the bang counterpart of
find_by: no match RAISES instead of returning nil, so an absent record becomes a 404 at the call site instead of a nil that fails somewhere downstream. -
#find_by_sql(sql, values = nil) ⇒ Array
the escape hatch for a caller-written SELECT: this walks the statement and hands back this model's records.
-
#find_each(conditions = nil, batch_size: 1000) ⇒ void
the whole-table per-record walk.
-
#find_in_batches(conditions = nil, batch_size: 1000) ⇒ void
the whole-table per-batch walk: the bulk counterpart of
find_each. -
#projection ⇒ String
the model's SELECT column list: every declared column, table-qualified and identifier-quoted, comma-joined.
-
#row_by_sql(sql, values = nil) ⇒ Hash{String => Object}
the single-row half of
rows_by_sql: walk the caller's SELECT and hand back the row it yields first, keyed by column name, or nil when it yields none. -
#rows_by_sql(sql, values = nil) ⇒ Array
the raw-read half of the read family: walk the caller's SELECT and hand back each row as a name→value Hash, without hydrating a record.
-
#update!(id, attrs) ⇒ Object
rewrite a row's columns by an id hash write and hand back the re-read record.
-
#update_all!(conditions, attrs) ⇒ Integer
rewrite every row matching a conditions hash and answer how many went: the conditions-side counterpart of
update!(which pins one row by id) and the update-side counterpart ofdelete_all!(the same conditions contract, the same count return). -
#upsert_all!(rows, unique_by:) ⇒ Integer
insert many rows in one statement, rewriting the row a conflict collides with instead of failing: the bulk counterpart of
create!for tables whose identity is a unique index, not a primary key. -
#where_clause(conditions) ⇒ String
the WHERE clause body for a conditions hash: one predicate per column, joined with AND.
-
#write(sql, values = nil) ⇒ Array
the parameterized-write half of the raw SQL surface: run the caller's INSERT/UPDATE/DELETE with raw Ruby values bound to each
?.
Instance Method Details
#count(conditions = {}) ⇒ Integer
how many rows of the model's table match. One of the declared-query group:
the model owns the table, core owns the assembly, so every model answers
with the same SELECT COUNT(*) FROM <quoted table> instead of a
hand-rolled prepare/step/finalize cursor. With no conditions it is the
whole table; with a conditions hash it is the matching rows — the same
conditions contract find_by·fetch_many· delete_all! use (nil →
IS NULL, { is_not: nil } → IS NOT NULL, an Array → IN (...),
anything else → equality, keys unvalidated and unknown columns fail loud
from the driver).
An empty table answers 0: a count is a number, so there is no miss path to raise on. The projection declaration is not needed here: COUNT(*) never reads columns.
651 |
# File 'runtime/sqlx/query.rb', line 651
def count: (?Hash[Symbol, untyped] conditions) -> Integer
|
#create!(attrs) ⇒ Object
insert one row from a column/value hash and hand back the persisted
record. Core owns the mechanical middle (attrs keys become the INSERT
columns, the declared timestamp columns fill themselves, the row arrives
back through find) because it never varies by model; the id comes from
the INSERT's own RETURNING row, not a session-scoped last-insert value,
so a concurrent session cannot answer with its own id. Input preparation —
type conversion, association objects reduced to ids, dumps — stays in the
caller. This is a partial INSERT: exactly the columns the caller passed
plus the declared timestamps it left out, so a table without timestamp
columns needs no opt-out. Keys are code, not input, and are not validated:
an unknown column surfaces as a loud SQL error from the driver. There are
no model validations to run — a schema constraint is the validation this
layer has, and it reaches the caller as an exception: a UNIQUE violation
raises Sqlx::RecordNotUnique, other constraint failures surface as the
adapter's driver error.
718 |
# File 'runtime/sqlx/query.rb', line 718
def create!: (Hash[Symbol, untyped] attrs) -> untyped
|
#delete!(id) ⇒ Integer
remove the row with this id. The low-level delete half of the write
family: no callbacks exist on this runtime's models, so the delete/destroy
split collapses to one entry — and the bang is family consistency with
create!/update!, not a promise of raise-on-miss. An unknown id is
silent (0 rows): the caller's lookup already decided whether a missing
record is a 404 before reaching here, so a second existence check inside
the delete would be redundant. The column is id by the same decision
find pins it: every table this entry point serves has an id primary
key. Returns the deleted-row count — the number a row delete answers —
so a caller that needs "did anything go" reads it off the return instead
of a separate peek at the adapter's counter.
968 |
# File 'runtime/sqlx/query.rb', line 968
def delete!: (untyped id) -> Integer
|
#delete_all!(conditions) ⇒ Integer
remove every row matching the conditions hash: the multi-row counterpart
of delete!, assembled from the model's table_name and the inherited
where_clause. It exists so an association cascade or a scope-shaped
cleanup carries only its conditions instead of hand-writing the same
DELETE FROM <table> WHERE <clause> — the assembly never varies by model.
nil conditions mean the whole table: "every row" is an explicit nil, never
an empty hash — an empty hash is the shape a broken condition builder
leaves behind, so it raises instead of quietly erasing every row (the same
rule update_all! keeps).
1006 |
# File 'runtime/sqlx/query.rb', line 1006
def delete_all!: (Hash[Symbol, untyped]? conditions) -> Integer
|
#exists?(conditions = {}) ⇒ Boolean
whether a matching row exists, answered from SELECT 1 ... LIMIT 1
without hydrating a record. Four input forms, one per question: no
argument (or an empty hash) asks whether the table has any row at all — an
empty question here is well-defined, unlike find_by({})'s "which row?";
a conditions hash is the find_by conditions contract through the
inherited where_clause; a scalar is shorthand for an id lookup — the
same id column find uses, so an exists?(user_id) call site needs no
rewrite; nil and false answer false without running a query — there is
no row to look for.
678 |
# File 'runtime/sqlx/query.rb', line 678
def exists?: (?untyped conditions) -> bool
|
#fetch_many(conditions, order: nil, limit: nil, offset: nil) ⇒ Array
the multi-row counterpart of find_by: every row matching the conditions
hash, assembled from the model's table_name and only_columns
declarations. It exists so an association reader or a scope-shaped class
method carries only its own conditions instead of hand-writing the same
SELECT <projection> FROM <table> WHERE <clause> the consumer models
repeated per reader — the assembly never varies by model, and the walk
rides find_by_sql, so the cross-class typed Array[<Model>] contract
holds at the call site.
Unlike find_by's order: (which picks one row among the
matches), the option here sorts the returned rows: :desc is
highest id first, :asc lowest first. nil conditions mean the
whole table: "every row" is an explicit nil the caller writes,
never an empty hash — an empty hash is the shape a broken
condition builder leaves behind, so it raises instead of quietly
widening the read to every row (the same rule update_all!·
delete_all! keep).
limit: and offset: bound the returned window at SQL. They are
for a read that wants at most N rows, or a numbered page — not for
walking a whole table: an offset page counts the skipped rows
again, and without order: the same page can repeat or skip rows
under writes. Both are why offset: requires limit: and
order: while limit: alone is a valid "at most N" read. A full
walk belongs to find_each.
417 |
# File 'runtime/sqlx/query.rb', line 417
def fetch_many: (Hash[Symbol, untyped]? conditions, ?order: Symbol, ?limit: Integer, ?offset: Integer) -> Array[untyped]
|
#find(id) ⇒ Object
the lookup by the id column: the matching record, or RecordNotFound —
never nil, so a missing row becomes a 404 at the call site instead of a
nil that fails somewhere downstream. The column is id by decision, not
by declaration: every table this entry point serves has an id primary
key (a table without one does not use find), and a different key would
be its own declaration decision if a model ever needs it. Built on
find_by, so the conditions contract (escaping, loud unknown columns) and
the declaration pair come along — and the message names the table and the
id that missed, which is the diagnostic a 404 log needs.
618 |
# File 'runtime/sqlx/query.rb', line 618
def find: (untyped id) -> untyped
|
#find_by(conditions, order: nil) ⇒ Object?
the single-row lookup by a conditions hash, assembled from the model's
table_name and only_columns declarations. Core owns this shape because
it never varies by model (the same
SELECT <quoted cols> FROM <quoted table> WHERE <clause> LIMIT 1 every
time) — the hand-rolled per-model copy is where the miss path and the
exception message drift. Core quotes the table and column identifiers as
it builds them, so reserved words are safe; the condition keys stay
unquoted (code, not input — the where_clause rule). A model whose lookup
needs more (a default scope, a join) overrides this method; a forgotten
declaration fails loud through the declaration stubs instead of quietly
reading as a miss.
The order: option decides which matching row arrives instead
of leaving it implementation-defined: :desc is the match with
the highest id, :asc the lowest. Order with no conditions
is a defined question too — find_by({}, order: :desc) is the
table's last row and :asc its first, the answers
Model.last/Model.first give — while an empty hash with no
order still has no answer and raises. The order column is id
by the same decision find pins the lookup column: every
table this entry point serves is ordered by its primary key,
and a different order would be its own declaration decision if
a model ever needs it.
319 |
# File 'runtime/sqlx/query.rb', line 319
def find_by: (Hash[Symbol, untyped] conditions, ?order: Symbol) -> untyped
|
#find_by!(conditions) ⇒ Object
the bang counterpart of find_by: no match RAISES instead of returning
nil, so an absent record becomes a 404 at the call site instead of a nil
that fails somewhere downstream. The message names the declared table —
the one identifier core has — rather than a class name it cannot see.
347 |
# File 'runtime/sqlx/query.rb', line 347
def find_by!: (Hash[Symbol, untyped] conditions) -> untyped
|
#find_by_sql(sql, values = nil) ⇒ Array
the escape hatch for a caller-written SELECT: this walks the statement and
hands back this model's records. It exists so an association reader on the
owning model carries only its own SQL (column list, join, foreign key)
instead of the prepare/step/finalize dance every reader repeated. The
per-column read stays in the model's from_row, which receives one
name-keyed row per statement row — the same shape the raw read
(rows_by_sql) answers with — so the caller's column order never binds
hydration: a reordered SELECT still lands every value on its own
attribute.
The walk specializes per concrete receiver — the receiver's
from_row fixes the element — so a caller may declare
-> Array[<Model>] across classes and the declaration holds.
No per-model hydration wrapper exists for typing.
A value enters the statement by binding, the way it enters a write: the values list is the one way in, and a value spliced into the statement's text is the caller's own escaping.
38 |
# File 'runtime/sqlx/query.rb', line 38
def find_by_sql: (String sql, ?Array[untyped]? values) -> Array[untyped]
|
#find_each(conditions = nil, batch_size: 1000) ⇒ void
the whole-table per-record walk. It uses the same keyset walk as
find_in_batches, but yields one record at a time: a caller with a
per-row side effect (send, enqueue, call) does not hold a batch of records
while it works. find_in_batches is the bulk counterpart that yields the
batch array.
The conditions contract is the same as fetch_many, but rows
arrive in id order through keyset pagination — each batch asks
for the rows after the last id it saw, so a static table is
visited exactly once and no skipped-row arithmetic (OFFSET) is
involved. The block is required: without one there is no work to
do, and returning an enumerator would hide that.
batch_size is the page size, not a total cap — a caller that
wants at most N rows uses fetch_many(..., limit: N). The walk is
not a snapshot: rows inserted with a higher id during the walk may
be seen, and changing a row's id while it is being traversed is
outside the contract. A break in the block stops the walk.
The id column is the cursor, read from the same projection the
record is built from; a model whose only_columns omits id
cannot use this entry (raise) rather than silently stopping after
the first batch. A table without an id column, a join, or a
descending full walk stays on raw find_by_sql.
488 |
# File 'runtime/sqlx/query.rb', line 488
def find_each: (?Hash[Symbol, untyped]? conditions, ?batch_size: Integer) { (untyped) -> void } -> void
|
#find_in_batches(conditions = nil, batch_size: 1000) ⇒ void
the whole-table per-batch walk: the bulk counterpart of find_each. It
uses the same keyset walk, but yields each batch's records as an Array, so
a caller that applies one bulk operation per batch (update_all!,
upsert_all!, a batch API) pays one block call and one write per batch
instead of one per record. It still hydrates the batch — there is no
Relation to hand a bulk statement to — so the memory bound is the batch
size, not the table.
The conditions, batch_size, id cursor, snapshot, and break
contracts are the same as find_each. Empty batches are never
yielded; the final shorter batch is. in_batches is not provided:
that name promises a Relation, and this runtime has none; the
portable bulk form is this array plus update_all!·
delete_all!·upsert_all!.
558 |
# File 'runtime/sqlx/query.rb', line 558
def find_in_batches: (?Hash[Symbol, untyped]? conditions, ?batch_size: Integer) { (Array[untyped]) -> void } -> void
|
#projection ⇒ String
the model's SELECT column list: every declared column, table-qualified and
identifier-quoted, comma-joined. Core owns this one assembly so find_by
and a model's own raw walk build the same spelling — the hand-rolled copy
is where quoting drops off and a reserved-word column breaks the parse. A
join reader calls another model's projection (User.projection) and
receives that model's table qualified, so there is no target-table
argument: the receiver is always the source of both the table and the
column list.
251 |
# File 'runtime/sqlx/query.rb', line 251
def projection: () -> String
|
#row_by_sql(sql, values = nil) ⇒ Hash{String => Object}
the single-row half of rows_by_sql: walk the caller's SELECT and hand
back the row it yields first, keyed by column name, or nil when it yields
none. It is the raw counterpart of the model-level single-row read
(find_by_sql(...).first): the same query, without the hydration: the row
shape is dynamic (a view, an aggregate, a computed column), so no model
stands behind it.
60 |
# File 'runtime/sqlx/query.rb', line 60
def row_by_sql: (String sql, ?Array[untyped]? values) -> Hash[String, untyped]?
|
#rows_by_sql(sql, values = nil) ⇒ Array
the raw-read half of the read family: walk the caller's SELECT and hand
back each row as a name→value Hash, without hydrating a record. It exists
for the statements no model can stand behind — a view, an aggregate, a
computed column — where the row shape is genuinely dynamic and a model
would be a lie. find_by_sql is the same walk with a model in front of
it; this one keeps the dynamic shape honest, and the shape is shared —
from_row receives exactly these name-keyed rows.
A value enters a read the way it enters a write: bound. The
statement's placeholders and the values must agree — a missing
value would bind NULL and a surplus one would be dropped, both
quiet — so this entry checks and says so, the way write does.
95 |
# File 'runtime/sqlx/query.rb', line 95
def rows_by_sql: (String sql, ?Array[untyped]? values) -> Array[Hash[String, untyped]]
|
#update!(id, attrs) ⇒ Object
rewrite a row's columns by an id hash write and hand back the re-read
record. The class-level shape is a decision: this runtime's models write
through class-level SQL (there is no instance save), so the entry reads
next to create! rather than as an instance method. The re-read is also
the loud half — an unknown id writes nothing (a 0-row UPDATE is silent)
and then find raises RecordNotFound instead of returning a record that
was never written. The declared updated_at fills itself when the caller
left it out; created_at is never touched. Same key rule as create!:
code, not input, an unknown column surfaces as a loud SQL error.
777 |
# File 'runtime/sqlx/query.rb', line 777
def update!: (untyped id, Hash[Symbol, untyped] attrs) -> untyped
|
#update_all!(conditions, attrs) ⇒ Integer
rewrite every row matching a conditions hash and answer how many went: the
conditions-side counterpart of update! (which pins one row by id) and
the update-side counterpart of delete_all! (the same conditions
contract, the same count return). It exists so a batch fixup carries only
its conditions and values instead of hand-writing the same
UPDATE <table> SET ... WHERE <clause> — the assembly never varies by
model: the table comes from the declaration, the WHERE body from the
inherited where_clause, the SET half from the attrs keys.
No timestamp is stamped: update! stamps because it hands back
a re-read record, and here the caller passes updated_at when
it wants it moved — a rewrite never moves a timestamp the
caller did not ask to move. nil conditions mean the whole
table: "every row" is an explicit nil the caller writes, never
an empty hash — an empty hash is the shape a broken condition
builder leaves behind, so it raises instead of quietly widening
the rewrite to every row (the same rule delete_all! keeps).
845 |
# File 'runtime/sqlx/query.rb', line 845
def update_all!: (Hash[Symbol, untyped]? conditions, Hash[Symbol, untyped] attrs) -> Integer
|
#upsert_all!(rows, unique_by:) ⇒ Integer
insert many rows in one statement, rewriting the row a conflict collides
with instead of failing: the bulk counterpart of create! for tables
whose identity is a unique index, not a primary key. It exists so an
import batch carries its rows and its uniqueness rule instead of
hand-writing the same INSERT ... ON CONFLICT ... DO UPDATE per row — the
assembly never varies by model.
The conflict target is required, not defaulted: this runtime has no schema reflection to read the table's unique indexes from, and the table this entry exists for has no primary key to fall back to — the caller names the columns that define the same row. A conflict on any OTHER unique index is not this statement's business and stays a loud constraint, so a typo in the target cannot quietly swallow the wrong rows.
The updated columns are the row keys minus unique_by — the
conflict columns are already the row being replaced. No
timestamp is stamped, as in update_all!: the caller passes
one when it wants one. A batch bigger than the driver's bound
value limit is the caller's to chunk — the entry builds one
statement per call.
904 |
# File 'runtime/sqlx/query.rb', line 904
def upsert_all!: (Array[Hash[Symbol, untyped]] rows, unique_by: Array[Symbol | String]) -> Integer
|
#where_clause(conditions) ⇒ String
the WHERE clause body for a conditions hash: one predicate per column,
joined with AND. The assembly half that find_by is built on. Core owns
it because the translation never varies by model (the table lives in the
caller's SELECT) — a hand-rolled private copy is where a nil condition
silently becomes = '' and stops matching IS NULL rows. An Array value
is membership on the column (IN (...)), so a status-set filter rides the
same conditions contract instead of escaping to raw SQL. An operator hash
value is a predicate bag on the column: a range comparison ({ lt: cutoff }) so a cutoff filter rides the same contract without a Range object
(which this runtime cannot build over a Time), and the null test ({ is_not: nil }) so "this column has a value" reads as the SQL it renders —
col IS NOT NULL, the has-value side of nil's IS NULL.
207 |
# File 'runtime/sqlx/query.rb', line 207
def where_clause: (Hash[Symbol, untyped] conditions) -> String
|
#write(sql, values = nil) ⇒ Array
the parameterized-write half of the raw SQL surface: run the caller's
INSERT/UPDATE/DELETE with raw Ruby values bound to each ?. The adapter
renders each value by its runtime type, so callers never inline literals.
"Write" is the DB-side counterpart of the read entries (reads SELECT,
writes change rows). A class-level entry so models call it where
find_by_sql lives; instance writers name the receiver explicitly.
Delegates to adapter-owned Sqlx::Database.exec_write.
The return is what the statement returned — the rows it
produced, keyed by column name. A statement that returns
nothing returns an empty array; a RETURNING statement returns
one row per row it changed, which is how a caller learns which
row a write took (a queue claim picks one and needs it back).
"How many rows changed?" is a question about a table, so it
belongs to the model layer (count, delete_all!) — the raw
write answers what the statement returned.
A Time is stored in the runtime's stored form — UTC, whatever zone the caller handed over — so a caller never formats a time to store one.
A value enters a statement only by binding: the values list is the one way in, and the adapter renders each value by its runtime type. A value spliced into the SQL text is the caller's own escaping — the thing this surface exists to remove.
The statement's placeholders and the values must agree: a missing value would bind NULL and a surplus one would be dropped, both quiet — so this entry checks and says so. The check is here, at the surface that promises the binding, and not in the adapter below (a caller that wants the adapter's own behavior still has it).
151 |
# File 'runtime/sqlx/query.rb', line 151
def write: (String sql, ?Array[untyped]? values) -> Array[Hash[String, untyped]]
|