NAME

DBIx::Fast::SQL - Query and CRUD methods of DBIx::Fast

SYNOPSIS

# These methods are composed into DBIx::Fast itself:
my $rows = $db->all('SELECT * FROM users WHERE status = ?', 1);
my $id   = $db->insert('users', { name => 'Alice' });

DESCRIPTION

Object::Pad role composed into DBIx::Fast. It owns the statement state (sql, p, results, last_id, last_sql - same accessors as always) and provides every query and CRUD method. You never use this module directly: all methods below are called on the DBIx::Fast object.

Every method below takes raw SQL; values are passed as ? placeholders (or :name for "execute") and bound, never interpolated. For a task-oriented overview see "RAW SQL" in DBIx::Fast.

QUERY METHODS

Lists: IN (?)

my $rows = $db->all('SELECT * FROM users WHERE id IN (?)', \@ids);
$db->exec('DELETE FROM sessions WHERE user_id IN (?) AND kind = ?', \@ids, 'web');
$db->execute('SELECT * FROM users WHERE id IN (:ids)', { ids => \@ids });

In every raw-SQL method (all, hash, val, flat, array, query, kv, exec, execute) an arrayref bound to a placeholder inside IN ( ... ) becomes one placeholder per element. Only IN (?) / NOT IN (?) placeholders are expanded - an arrayref elsewhere stays a single bind (PostgreSQL arrays, = ANY(?)) - and ? inside quoted literals is ignored. An empty list throws: IN () is invalid SQL.

all

$db->all('SELECT * FROM users WHERE id > ?', 10);
my $rows = $db->results;  # arrayref of hashrefs

Executes SQL and stores all rows in results.

hash

$db->hash('SELECT * FROM users WHERE id = ?', 1);
my $row = $db->results;  # hashref

Executes SQL and stores a single row in results.

val

my $name = $db->val('SELECT name FROM users WHERE id = ?', 1);

Executes SQL and returns a single scalar value: the first column of the first row. Intended for single-column SELECTs; with multiple columns the extra ones are ignored.

flat

my @names = $db->flat('SELECT name FROM users');

Executes SQL and returns a flat list of values.

array

$db->array('SELECT name FROM users');
my $names = $db->results;  # arrayref

Executes SQL and stores a single column as arrayref in results.

query

my $rs = $db->query('SELECT id, name FROM users WHERE active = ?', 1);
while (my $row = $rs->hash) {
    say "$row->{id}: $row->{name}";
}

Executes SQL with bind parameters and returns a DBIx::Fast::Result iterator. Unlike all, this does not load the entire result set into memory - rows are fetched one at a time via hash, array, all, or flat on the result object. The statement handle is automatically finished when the result object goes out of scope.

metrics

my $m = $db->metrics;

Statement counters when the metrics option is on (undef otherwise). See "MONITORING" in DBIx::Fast.

reset_metrics

Zeroes the metrics counters.

stream

my $rs = $db->stream('SELECT * FROM logs WHERE day = ?', $day);
while (my $row = $rs->hash) { ... }

Like "query", for result sets too big for memory: on MariaDB/MySQL the rows come from the server as they are read (mariadb_use_result / mysql_use_result) instead of being loaded into the client first - 200,000 rows of ~180 bytes: +45 MB with query, +0 MB with stream. While a stream is open, that connection cannot run other statements (MySQL protocol): drain or finish it first, or read through another connection. SQLite streams natively; DBD::Pg always buffers results, so on PostgreSQL stream behaves like query.

count

my $total = $db->count('users');
my $active = $db->count('users', { status => 1 });

Returns row count, optionally filtered by WHERE conditions.

exists

if ($db->exists('users', { email => $email })) { ... }

Returns 1 if at least one row matches (SELECT 1 ... LIMIT 1), else 0. Takes the same WHERE hashref (and operator whitelist) as count.

kv

my $names = $db->kv('SELECT id, name FROM users WHERE active = ?', 1);
# { 1 => 'Alice', 2 => 'Bob' }

First column to second column, as a hashref. A later duplicate key wins.

all_by

my $users = $db->all_by('id', 'SELECT * FROM users WHERE active = ?', 1);
# { 1 => { id => 1, name => 'Alice', ... }, ... }

Rows as a hashref keyed by the given column (DBI's fetchall_hashref).

where

my ($where, @binds) = $db->where({
    status   => 'active',
    country  => $form->{country},          # undef: filter skipped
    id       => \@ids,                     # IN (...)
    name     => { CONTAINS => $search },   # escaped LIKE
    created  => { '>=' => $since },
    deleted  => { IS => undef },           # explicit IS NULL
});
my $rows = $db->all("SELECT * FROM users $where ORDER BY id", @binds);

Builds ('WHERE ...', @binds) from a filter hashref (or ('') when no filter is left), for hand-written SQL. Columns are validated and quoted; operators are whitelisted: = != <> < <= > >= LIKE NOT BETWEEN IS IN, NOT IN, and CONTAINS / PREFIX / SUFFIX, which match user text literally (LIKE ? ESCAPE '!' with "like"). A filter whose value is undef is skipped (an empty form field means "do not filter"); use { IS => undef } for IS NULL. count and exists accept the same conditions, except that there an undef value means IS NULL.

like

$db->all(q{SELECT * FROM users WHERE name LIKE ? ESCAPE '!'},
         $db->like($search));               # contains (default)
$db->like($s, 'prefix');  $db->like($s, 'suffix');  $db->like($s, 'exact');

Turns user text into a LIKE pattern that matches it literally: %, _ and the escape character ! are escaped with !. Pair it with ESCAPE '!', which behaves the same on SQLite, MariaDB/MySQL and PostgreSQL (a backslash does not).

page

my $page = $db->page(
    'SELECT id, name, email FROM users WHERE status = ?', ['active'],
    page => $req->param('page'), per => 50,
    order => $req->param('sort'), allowed_order => [qw(id name email)],
);
# { rows => [...], total => 1234, page => 2, per => 50, pages => 25 }

One page of a query plus its total (COUNT(*) over the statement). order is a column from allowed_order ("name", "name DESC" or "-name"), or an arrayref of them; anything else throws, so it can come straight from a request. page and per must be positive integers, per at most max_per (default 1000). The statement must not carry its own ORDER BY or LIMIT.

in_chunks

my @rows = map { @$_ } $db->in_chunks(\@ids, 500, sub ($chunk) {
    $db->all('SELECT * FROM products WHERE id IN (?)', $chunk);
});

Calls the coderef once per slice of at most $size elements and returns the results in order - for lists too long for one statement.

rows

$db->delete('sessions', { user_id => $id });
my $n = $db->rows;

Rows affected by the last write (insert/update/up/delete/ upsert/*_many/exec): a plain number (0, never '0E0'), or undef when the driver does not know.

exec

$db->exec('CREATE TABLE foo (id INT)');
$db->exec('INSERT INTO foo VALUES (?)', 42);
$db->exec('UPDATE foo SET id = ? WHERE id = ?', 99, 42);
my $sth = $db->exec('SELECT * FROM foo WHERE id > ?', 0);

The general-purpose raw-SQL method: runs any statement (DDL, DML or vendor-specific SQL) with optional ? bind parameters and returns the statement handle. Records the query in the profiler when one is active. For a SELECT you usually want all/hash/val/query instead, which fetch the rows for you.

execute

$db->execute('SELECT * FROM users WHERE name = :name', { name => 'Alice' });
$db->execute('SELECT * FROM users WHERE id = :id', { id => 1 }, 'hash');

Executes SQL with named parameter substitution (:name syntax). Third argument selects result type: arrayref (default) or hash.

CRUD METHODS

insert

$db->insert('users', { name => 'Alice', status => 1 });
$db->insert('users', { name => 'Alice' }, time => 'created_at');
my $id = $db->last_id;

Inserts a row using SQL::Abstract. Sets last_id automatically based on the database driver. On PostgreSQL the id comes back in the same round trip (INSERT ... RETURNING the table's primary key, looked up once per table) - any key type: serial, identity, uuid... A table without a single-column primary key falls back to DBD::Pg's last_insert_id (a serial column), as insert_ignore does for an ignored row.

update

$db->update('users', {
    sen   => { name => 'Bob', status => 1 },
    where => { id => 1 },
});
$db->update('users', {
    sen   => { name => 'Bob' },
    where => { id => 1 },
}, time => 'updated_at');

Updates rows using SQL::Abstract. Pass time => 'column_name' to auto-set a timestamp column to now().

up

$db->up('users', { name => 'Bob' }, { id => 1 });
$db->up('users', { name => 'Bob' }, { id => 1 }, 'updated_at');

Shortcut for update. Arguments: table, data hashref, where hashref, optional time column name (positional).

delete

$db->delete('users', { id => 1 });

Deletes rows using SQL::Abstract.

upsert

$db->upsert('products',
    { sku => 'X', name => 'Widget', stock => 5, price => 9.99 },
    ['sku'],                  # conflict keys (UNIQUE / PRIMARY KEY)
    [qw(stock price)],        # optional: columns to update on conflict
);

Inserts the row, or updates the conflict-targeted columns if a row with matching conflict keys already exists. Translates to driver-native syntax (ON CONFLICT (...) DO UPDATE for PostgreSQL and SQLite 3.24+, ON DUPLICATE KEY UPDATE for MariaDB and MySQL).

If update_cols is omitted, every row column that is not in conflict_keys is updated. If after that the update set is empty, an exception is raised - use a regular insert if you do not want to update anything on conflict.

last_id is not updated by upsert because the operation may have been an insert or an update.

update_cols can also be a hashref saying how each column is updated:

$db->upsert('points', { user_id => $uid, balance => 50, visits => 1 },
    ['user_id'],
    {
        balance    => '+',                    # balance = balance + 50
        visits     => { '+' => 1 },           # visits  = visits + 1
        last_name  => 'new',                  # take the incoming value
        updated_at => \'CURRENT_TIMESTAMP',   # literal SQL (code only)
    });

'new' takes the incoming value, '+' adds the incoming value to the stored one (the column must be in the row), { '+' => N } adds a numeric constant. These are translated per driver (VALUES(col) on MariaDB, the new.col row alias on MySQL 8.0.19+, excluded.col on PostgreSQL/SQLite, with the stored row qualified where the dialect needs it). A scalarref is literal SQL - only ever from code; on PostgreSQL a literal that reads the stored row must name it dbix_t.col, so prefer the modes. upsert_many accepts the same forms.

insert_ignore

my $inserted = $db->insert_ignore('subscribers', { email => $email });

Inserts the row unless it would violate a UNIQUE / PRIMARY KEY, in which case nothing happens. Returns 1 if inserted, 0 if the row already existed (INSERT IGNORE on MariaDB/MySQL, INSERT OR IGNORE on SQLite, ON CONFLICT DO NOTHING on PostgreSQL). Note that on MariaDB/MySQL IGNORE also turns other errors (invalid values, missing NOT NULL columns) into warnings.

find_or_create

my $tag = $db->find_or_create('tags', { slug => $slug }, { name => $name });
my ($row, $created) = $db->find_or_create('tags', { slug => $slug });

Returns the row matching the key (which must be covered by a UNIQUE index), inserting { %defaults, %key } first when there is none. Safe against concurrent callers: the insert is an insert_ignore, so a racing duplicate is skipped and the existing row is read back. In list context also returns whether this call created the row.

insert_many

$db->insert_many('orders', [
    { customer_id => 1, total => 99 },
    { customer_id => 2, total => 45 },
]);

Inserts multiple rows in a single statement. All rows must share the same column set (same keys); rows whose keys differ from the first one raise an exception. Every row's values must be bindable: a reference (other than a blessed object, or an arrayref on Pg) raises an exception instead of being stored as its "HASH(0x...)" stringification. Returns the number of rows inserted.

last_id follows the driver's convention for a multi-row INSERT, which is not portable: MariaDB/MySQL report the first inserted id, SQLite and Pg the last. If you need every id, use INSERT ... RETURNING (Pg, SQLite 3.35+, MariaDB 10.5+) via all().

For very large arrays you should chunk the input manually to stay within the driver's max_allowed_packet / statement-size limits.

Options (after the rows): chunk => N splits the rows into statements of at most N rows (max_allowed_packet / bind-count limits) and returns the total; each chunk is its own statement, so wrap the call in txn when it must be all-or-nothing. ignore => 1 skips rows that violate a UNIQUE / PRIMARY KEY (as "insert_ignore") and returns the number actually inserted.

$db->insert_many('events', \@rows, chunk => 500);
my $new = $db->insert_many('subscribers', \@rows, ignore => 1);

upsert_many

$db->upsert_many('products', \@rows, ['sku'], [qw(stock price)]);

Bulk INSERT with per-row conflict resolution, combining insert_many and upsert. Same column-uniformity rule as insert_many. Returns the number of rows in the batch.

Takes the same update_cols forms as "upsert", and chunk => N after them: $db->upsert_many($t, \@rows, ['sku'], undef, chunk => 500).

make_sen

$db->sql('SELECT * FROM users WHERE name = :name AND age = :age');
$db->make_sen({ name => 'Alice', age => 30 });
# $db->sql is now 'SELECT * FROM users WHERE name = ? AND age = ?'
# $db->p is ['Alice', 30]

Replaces named placeholders (:name) with ? in order of appearance in the SQL string. Supports duplicate placeholders. Quoted string literals ('...' / "...") and PostgreSQL ::cast operators are left untouched.

Named parameters are intended for simple SQL: the literal-skipping is based on straight single/double quotes and does not understand backslash escapes ('a\'b'), PostgreSQL dollar-quoting ($$...$$), or SQL comments. For SQL using those constructs, use positional ? placeholders instead.

q

$db->q('SELECT * FROM users WHERE id = ?', 1);

Sets sql and p (bind parameters) for subsequent use.

execute_prepare

$db->sql('INSERT INTO users (name) VALUES (?)');
$db->execute_prepare('Alice');

Prepares and executes the current sql with bind parameters.

SEE ALSO

DBIx::Fast - constructor, accessors, subsystems and the SECURITY notes that apply to these methods.

AUTHOR

SeHarrys

LICENSE

This is free software under the Artistic License 2.0.