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

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.

Examples:

ApiSetting.count
# => 42
ApiSetting.count({ verified: 1 })
# => 7

Parameters:

  • conditions (Hash{Symbol => Object}) (defaults to: {}) —

    the rows to count; an empty hash (or no argument) counts the whole table

Returns:

  • (Integer) —

    the number of matching rows — 0 when none match

Raises:

  • (NotImplementedError) —

    the model has not declared table_name (the declaration stub raises)



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.

Examples:

Post.create!({ title: "hello", body: "world" })

Parameters:

  • attrs (Hash{Symbol => Object}) —

    column => value. Raw Ruby values bind through write (nil binds NULL). A declared created_at/updated_at the caller did not pass is filled with the current UTC time — one value shared by both columns, so an insert's two stamps agree; a timestamp the caller did pass wins over the stamp.

Returns:

  • (Object) —

    the persisted record, re-read by its id — never nil (the row was just written)

Raises:

  • (ArgumentError) —

    on empty attrs — there is no row and no columns to insert

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)

  • (Sqlx::RecordNotUnique) —

    a UNIQUE constraint rejects the insert



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.

Examples:

Post.delete!(42)
# => 1

Parameters:

Returns:

  • (Integer) —

    the number of rows deleted — 0 when no row had that id

Raises:

  • (ArgumentError) —

    the id is neither an Integer nor a String — an Array is the shape delete_all!({ id: ids }) owns, and anything else would bind as text and miss silently on one adapter while the driver errors on the other

  • (NotImplementedError) —

    the model has not declared table_name



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

Examples:

Comment.delete_all!({ post_id: 42 })
# => 3
Comment.delete_all!(nil)
# => every row

Parameters:

  • conditions (Hash{Symbol => Object}, nil) —

    the find_by conditions contract, unchanged (a nil VALUE inside a condition means IS NULL, an operator hash carries comparisons and the null test ({ is_not: nil } → IS NOT NULL), an Array means IN (...) membership, anything else escaped equality — keys are code, not input, and an unknown column surfaces as a loud SQL error from the driver). nil CONDITIONS (the argument itself) mean the whole table: no WHERE clause at all.

Returns:

  • (Integer) —

    the number of rows deleted — 0 when no row matched

Raises:

  • (ArgumentError) —

    an empty conditions hash — the whole-table DELETE is spelled nil, so an empty hash means the condition builder broke; or the where_clause value contract — an empty Array value or an Array carrying nil

  • (NotImplementedError) —

    the model has not declared table_name



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.

Examples:

ApiSetting.exists?({ key: "flag" })  # conditions hash
User.exists?(user_id)                # id shorthand
Invoice.exists?                      # any row?

Parameters:

  • conditions (Hash{Symbol => Object}, Integer, String, nil) (defaults to: {}) —

    the conditions as above — a Hash is code-as-input on the find_by contract, a scalar matches the id column

Returns:

  • (Boolean) —

    true when one matching row exists

Raises:

  • (NotImplementedError) —

    the model has not declared table_name



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.

Examples:

typed association reader — declaration and walk agree

# rbs: def invoices: () -> Array[Invoice]
def invoices = Invoice.fetch_many({ user_id: @id }, order: :desc)

the whole table, sorted — nil is the explicit "every row"

User.fetch_many(nil, order: :desc)

a page — both the window and the total stay explicit

total = Invoice.count({ user_id: id })
page = Invoice.fetch_many({ user_id: id }, order: :desc, limit: 20, offset: 40)

Parameters:

  • conditions (Hash{Symbol => Object}, nil) —

    the find_by conditions contract, unchanged (a nil VALUE inside a condition means IS NULL, an operator hash carries comparisons and the null test ({ is_not: nil } → IS NOT NULL), an Array means IN (...) membership, anything else escaped equality — keys are code, not input, and an unknown column surfaces as a loud SQL error from the driver). nil CONDITIONS (the argument itself) mean the whole table: no WHERE clause at all.

  • order (Symbol, nil) (defaults to: nil) —

    :desc or :asc — the id sort direction of the returned rows; nil leaves the order implementation-defined

  • limit (Integer, nil) (defaults to: nil) —

    the maximum number of rows to return; nil means no limit. Must be positive.

  • offset (Integer, nil) (defaults to: nil) —

    how many matching rows to skip before collecting the window; nil means zero. Requires both limit: and order:.

Returns:

  • (Array) —

    one record per matching row in the window, mapped by the receiver's from_row — an empty array on no match (never nil); which row comes first without order: is implementation-defined, so a caller that needs order passes it

Raises:

  • (ArgumentError) —

    an empty conditions hash — the whole table is spelled nil, so an empty hash means the condition builder broke; an order: other than :asc/:desc (a typo must fail loud, not silently return unordered rows); a limit: that is not a positive Integer; an offset: that is not a non-negative Integer, or that arrives without limit: or order:; or the where_clause value contract — an empty Array value or an Array carrying nil

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)



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.

Examples:

User.find(42)

Parameters:

  • id (Integer, String) —

    the id column value to match — treated as a condition value: it is escaped through where_clause like any other

Returns:

  • (Object) —

    the matching record — never nil

Raises:

  • (Sqlx::RecordNotFound) —

    no row has that id

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (through find_by)



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.

Examples:

call

Model.find_by({ email: "a@example.com", deleted_at: nil })
InvoiceItem.find_by({ invoice_id: 42 }, order: :desc)
Subscription.find_by({ user_id: 7, status: [0, 1] }, order: :desc)
Plan.find_by({}, order: :desc)  # the table's last row

declaration — the model owns table and columns, the

inherited method owns the assembly
class Widget < Sqlx::Base
  def self.table_name = "widgets"
  def self.only_columns = [:id, :email, :created_at]
end

Parameters:

  • conditions (Hash{Symbol => Object}) —

    column Symbol => value. nil means IS NULL on that column; an operator hash carries comparisons ({ lt: cutoff }) and the null test ({ is_not: nil } → IS NOT NULL); an Array means membership on it (IN (...) — the where_clause contract); anything else means equality on it, rendered through Sqlx.quote — values are data (they arrive from request params) and are never interpolated raw. Keys are code, not input, and core does not validate them: an unknown column surfaces as a loud SQL error, never as nil — a typo'd key must not read as a miss. An empty hash is allowed only together with order: (the table's first/last row as above).

  • order (Symbol, nil) (defaults to: nil) —

    :desc or :asc — the id direction that picks the row; nil leaves the row implementation-defined (the pre-order contract)

Returns:

  • (Object, nil) —

    the matching record, or nil on no match — at most one row; with order: the row is the highest/lowest id among the matches

Raises:

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)

  • (ArgumentError) —

    an empty conditions hash with no order: — there is no clause to build and no row question to ask; or an order: other than :asc/:desc (a typo must fail loud, not silently pick a row); or the where_clause value contract — an empty Array value or an Array carrying nil



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.

Examples:

Model.find_by!({ email: "a@example.com" })

Parameters:

  • conditions (Hash{Symbol => Object}) —

    the find_by conditions contract, unchanged.

Returns:

  • (Object) —

    the matching record — never nil

Raises:

  • (Sqlx::RecordNotFound) —

    no row matches the conditions

  • (NotImplementedError) —

    the model has not declared table_name / only_columns



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.

Examples:

typed association reader — declaration and walk agree

# rbs: def subscriptions: () -> Array[Subscription]
def subscriptions
  Subscription.find_by_sql("SELECT " + Subscription.projection +
    " FROM subscriptions WHERE user_id = ?", [ @id ])
end

Parameters:

  • sql (String) —

    the caller's SELECT, with a ? for each value

  • values (Array, nil) (defaults to: nil) —

    one value per ? — nil for a statement that takes none

Returns:

  • (Array) —

    one record per row, mapped by the receiver's from_row

Raises:

  • (ArgumentError) —

    the values and the statement's placeholders do not agree



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.

Examples:

InvoiceItem.find_each(nil, batch_size: 500) { |item| item.reindex }

resume after a recorded id — the conditions carry the start

Job.find_each({ id: { gt: last_seen_id } }, batch_size: 500) { |job| ... }

Parameters:

  • conditions (Hash{Symbol => Object}, nil) (defaults to: nil) —

    the fetch_many conditions contract, unchanged; nil means the whole table and an empty hash raises (a broken condition builder must not quietly widen into a full walk)

  • batch_size (Integer) (defaults to: 1000) —

    how many rows one query reads; must be positive (the default is a page size, not a total cap)

Raises:

  • (ArgumentError) —

    the block is missing; the conditions hash is empty; batch_size is not a positive Integer; or only_columns omits id (the cursor column)

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)



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!.

Examples:

one bulk update per batch

Model.find_in_batches({ status: 0 }, batch_size: 500) do |records|
  Model.update_all!({ id: records.map { |record| record.id } }, { status: 1 })
end

Parameters:

  • conditions (Hash{Symbol => Object}, nil) (defaults to: nil) —

    the fetch_many conditions contract, unchanged; nil means the whole table and an empty hash raises (a broken condition builder must not quietly widen into a full walk)

  • batch_size (Integer) (defaults to: 1000) —

    how many rows one query reads; must be positive (the default is a page size, not a total cap)

Raises:

  • (ArgumentError) —

    the block is missing; the conditions hash is empty; batch_size is not a positive Integer; or only_columns omits id (the cursor column)

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)



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.

Examples:

Widget.projection
# => "\"widgets\".\"id\", \"widgets\".\"email\""

Returns:

  • (String) —

    the qualified column list, ready to splice after SELECT

Raises:

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)



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.

Examples:

Sqlx::Base.row_by_sql("SELECT id, name FROM widgets LIMIT 1")
# => { "id" => 7, "name" => "alpha" }
Sqlx::Base.row_by_sql("SELECT id, name FROM widgets WHERE id = ?", [ 7 ])

Parameters:

  • sql (String) —

    the caller's SELECT, with a ? for each value

  • values (Array, nil) (defaults to: nil) —

    one value per ? — nil for a statement that takes none

Returns:

  • (Hash{String => Object}) —

    the first row, keyed by column name — nil when the statement yielded no row

Raises:

  • (ArgumentError) —

    the values and the statement's placeholders do not agree



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.

Examples:

view read — no model fits the row shape

Sqlx::Base.rows_by_sql("SELECT service, host FROM latest_samples")

a read with a value — bound, not spliced into the text

Sqlx::Base.rows_by_sql("SELECT id FROM widgets WHERE name = ?", [ "alpha" ])

Parameters:

  • sql (String) —

    the caller's SELECT, with a ? for each value

  • values (Array, nil) (defaults to: nil) —

    one value per ? — nil for a statement that takes none. Within the list, nil binds NULL and a Time binds in the runtime's stored form (UTC)

Returns:

  • (Array) —

    one Hash per row, keyed by column name — an empty array when the statement yields no rows (never nil). Values are the adapter's native types (Integer/Float/String, nil for NULL)

Raises:

  • (ArgumentError) —

    the values and the statement's placeholders do not agree



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.

Examples:

Post.update!(42, { title: "revised" })

Parameters:

  • id (Integer, String) —

    the id of the row to rewrite — the same id column find looks up

  • attrs (Hash{Symbol => Object}) —

    column => value, same binding contract as create!. A declared updated_at the caller did not pass is filled with the current UTC time; a value the caller did pass wins.

Returns:

  • (Object) —

    the re-read record — never nil on success

Raises:

  • (ArgumentError) —

    on empty attrs — there is nothing to rewrite (stamp-only writes belong to a dedicated entry, not this one)

  • (Sqlx::RecordNotFound) —

    no row has that id

  • (NotImplementedError) —

    the model has not declared table_name / only_columns (the declaration stubs raise)



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

Examples:

Post.update_all!({ status: "stale", created_at: { lt: cutoff } }, { status: "archived" })
# => 7
Post.update_all!(nil, { status: "archived" })
# => every row

Parameters:

  • conditions (Hash{Symbol => Object}, nil) —

    the find_by conditions contract, unchanged (a nil VALUE inside a condition means IS NULL, an operator hash carries comparisons and the null test ({ is_not: nil } → IS NOT NULL), an Array means IN (...) membership, a Hash means a comparison like { lt: cutoff }, anything else escaped equality — keys are code, not input, and an unknown column surfaces as a loud SQL error from the driver). nil CONDITIONS (the argument itself) mean the whole table: no WHERE clause at all.

  • attrs (Hash{Symbol => Object}) —

    column => value, the update! binding contract for the SET half: keys are code (an unknown column is a loud SQL error), values bind through ? (nil binds NULL, a Time binds in the runtime's stored form)

Returns:

  • (Integer) —

    the number of rows changed — 0 when no row matched

Raises:

  • (ArgumentError) —

    on an empty conditions hash — the whole-table rewrite is spelled nil, so an empty hash means the condition builder broke — or on empty attrs (nothing to rewrite)

  • (NotImplementedError) —

    the model has not declared table_name



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.

Examples:

Widget.upsert_all!([{ name: "a", value: 1.0 }, { name: "b", value: 2.0 }], unique_by: [ :name ])
# => 2

Parameters:

  • rows (Array[Hash{Symbol => Object}]) —

    one hash per row to insert. Every row must have the same keys — a batch is one column list. Values bind through ? (nil binds NULL, a Time binds in the runtime's stored form)

  • unique_by (Array[Symbol, String]) —

    the columns of the unique index that defines the same row — code, not input; every column must be among the row keys (a conflict target outside the insert list would never fire)

Returns:

  • (Integer) —

    the number of rows inserted or rewritten

Raises:

  • (ArgumentError) —

    on empty rows, an empty unique_by, rows with differing keys, a unique_by column outside the row keys, or no column left to update (every key part of unique_by — a skip-duplicates statement is not this entry)

  • (Sqlx::RecordNotUnique) —

    another unique index on the table rejects a row

  • (NotImplementedError) —

    the model has not declared table_name



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.

Examples:

where_clause({ email: "a@example.com", deleted_at: nil })
# => "email = 'a@example.com' AND deleted_at IS NULL"
where_clause({ user_id: 7, status: [0, 1] })
# => "user_id = 7 AND status IN (0, 1)"
where_clause({ deleted_at: { is_not: nil } })
# => "deleted_at IS NOT NULL"
where_clause({ started_at: { lt: Time.utc(2026, 10, 6, 8, 0, 0) } })
# => "started_at < '2026-10-06 08:00:00.000000'"

Parameters:

  • conditions (Hash{Symbol => Object}) —

    column Symbol => value. nil means IS NULL on that column; an Array means membership on it — one IN (...) list, each element translated by the same per-type rules as equality below; a Hash means predicates on it — one per whitelisted operator: comparisons (:lt <, :lte <=, :gt >, :gte >=) with a value bound rendered like equality, and the null test (:is_not => nil, rendered IS NOT NULL); anything else means equality on it, rendered through Sqlx.quote — values are data (they arrive from request params) and are never interpolated raw. Keys are code, not input, and are not validated: an unknown column surfaces as a loud SQL error from the driver.

Returns:

  • (String) —

    the clause body — no WHERE keyword, ready to splice into the caller's SELECT

Raises:

  • (ArgumentError) —

    on an empty hash — there is no clause to build, so no WHERE-less SELECT either; on an empty Array value — IN () is not SQL, and an empty set deserves the same loud treatment as an empty hash; on an Array carrying nil — IN (NULL, ...) answers UNKNOWN for every row, so the null test must be a key of its own; on an Array carrying a Hash — IN (...) takes values, not predicates; on an operator hash that is empty (no predicate to build — the condition would vanish silently), names an operator outside the whitelist (a typo must fail loud, not render a predicate nobody meant), carries a nil or structured comparison bound (< NULL never matches, a nested Hash would stringify into the comparison), or gives is_not a bound other than nil (IS NOT <value> is not SQL — the operator is the null test)



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

Examples:

Model.write("INSERT INTO t (a) VALUES (?)", [ "x" ])
# => []
Model.write("UPDATE t SET a = ? WHERE id = ? RETURNING id", [ "x", 7 ])
# => [ { "id" => 7 } ]
Model.write("DELETE FROM t")
# => []

Parameters:

  • sql (String) —

    the caller's INSERT/UPDATE/DELETE, with a ? for each value

  • values (Array, nil) (defaults to: nil) —

    one value per ? — nil for a statement that takes none. nil binds NULL, a Time binds in the runtime's stored form (UTC), everything else by its runtime type

Returns:

  • (Array) —

    one name-keyed row per row the statement returned — an empty array when it returned none

Raises:

  • (ArgumentError) —

    the values and the statement's placeholders do not agree



151
# File 'runtime/sqlx/query.rb', line 151

def write: (String sql, ?Array[untyped]? values) -> Array[Hash[String, untyped]]