NAME
DBIO::Test::Storage - Fake storage for testing SQL generation without a database
VERSION
version 0.900004
SYNOPSIS
my $schema = DBIO::Test::Schema->connect('DBIO::Test::Storage', '');
my $rs = $schema->resultset('Artist')->search({ name => 'foo' });
# .as_query works without a database
my ($sql, @bind) = @{ ${$rs->as_query} };
# or execute and inspect the captured query log
my $storage = $schema->storage;
$storage->reset_captured;
$rs->all; # generates SQL, returns empty results
my @queries = $storage->captured_queries;
See t/test/04_query_capture.t for a runnable example.
DESCRIPTION
A storage backend that generates SQL via DBIO::SQLMaker but never executes it against a real database. Every query is captured and can be inspected through "captured_queries".
This is useful for:
Testing SQL generation (SELECT, INSERT, UPDATE, DELETE)
Verifying ResultSet chaining produces expected queries
Schema metadata tests (columns, relationships, constraints)
Any test that only cares about what SQL would be generated
Transactions are part of that log: BEGIN, COMMIT, ROLLBACK and the savepoint verbs are captured like any statement, and that includes the implicit rollback a DBIO::Storage::TxnScopeGuard performs when it leaves scope without a commit. The guard weakens a reference to $storage->_dbh and declines to act when that reference is empty, so the fake storage carries a stand-in handle for it -- otherwise an assertion that a failing code path rolled back would pass without a rollback ever having been attempted.
METHODS
connected
Always returns true. There is no real connection to check.
ensure_connected
No-op. We are always "connected".
disconnect
Sets connected state to false.
_execute
Overrides "_execute" in DBIO::Storage::DBI to capture the generated SQL and bind values instead of executing them.
The statement handle it returns yields the rows of a matching mock (see "mock"), or no rows when none matches.
_execute_rendered
Overrides "_execute_rendered" in DBIO::Storage::DBI the same way "_execute" overrides the generated path: the already-rendered statement is captured verbatim instead of executed, and answered from the mock results. The SQL that lands in "captured_queries" is byte-for-byte the SQL a real storage would have handed to $dbh->prepare.
_prepare_rendered
Overrides "_prepare_rendered" in DBIO::Storage::DBI, the prepare-without-execute seam a driver whose answer is an attribute of the prepared statement uses (ADR 0040 section 6; Firebird's query plan is the case). The statement is captured and traced exactly as "_execute_rendered" captures and traces one -- which is what lets a driver assert against the mock that its _explain_query went through the storage instead of reaching for the handle -- and a fake statement handle is returned.
It is distinguishable in "captured_queries": the entry's op is 'prepare', not 'select'. That is the one observable separating this seam from "_execute_rendered" on a storage that never executes anything anyway.
mock_affected_rows
$storage->mock_affected_rows('0E0'); # what DBI returns for "no rows"
$storage->mock_affected_rows(2); # MySQL's "row was updated"
$storage->mock_affected_rows(undef); # back to the default of 1
What "_execute" hands back as the statement return value for insert/update/delete. The default is 1; select is unaffected. Real DBI reports the string '0E0' -- zero but true -- for a DML statement that affected no rows, which is a shape no fake storage produces on its own and which callers of $rs->upsert(..., { result => 'count' }) or $rs->update can legitimately see.
captured_queries
Returns all captured queries as a list of hashrefs, each containing op, sql, and bind keys.
my @queries = $storage->captured_queries;
# ( { op => 'select', sql => 'SELECT ...', bind => [...] }, ... )
op is the statement's operation -- select, insert, update, delete, or prepare for a statement that went through "_prepare_rendered" and was never executed. Transaction and savepoint statements are logged with op txn_begin, txn_commit, txn_rollback, svp_begin, svp_release or svp_rollback.
captured_sql_bind
Returns captured queries as arrayrefs of [$sql, @bind] pairs, compatible with "is_same_sql_bind" in SQL::Abstract::Test.
reset_captured
Clears the captured query log.
select
Returns a fake cursor that yields the rows of a matching mock (see "mock"), or no rows when none matches. The SQL is captured either way.
mock
$storage->mock($sql_pattern, \@rows);
$storage->mock($sql_pattern, \@rows, \@columns);
$storage->mock(qr/SELECT.*FROM artist/i, [
[1, 'Caterwauler McCrae'],
[2, 'Random Boy Band'],
]);
Registers a mock result. When a query matches $sql_pattern (string or regexp), the given rows are returned. Mocks are checked in LIFO order -- later mocks override earlier ones. Each mock is consumed once unless registered with "mock_persistent".
mock_persistent
Like "mock" but the mock is not consumed after matching -- it keeps returning the same rows for every matching query.
clear_mocks
Removes all registered mocks, including the "mock_func" answers.
mock_func
$storage->mock_func( ib_plan => "PLAN (ARTIST NATURAL)\n" );
$storage->mock_func( ib_plan => sub {
my ($sth, $name, @args) = @_;
$calls++;
return $plan;
});
Registers what the fake statement handle answers for a DBD-private statement-handle function, i.e. DBI's $sth->func($name, ...). Engines answer a question about a PREPARED statement through that dispatcher rather than through an attribute -- Firebird's query plan is $sth->func('ib_plan'), read off a handle that was never executed -- which is why the answer is registered on the STORAGE (through "_prepare_rendered" / "_execute_rendered" every fake statement handle is built here) and not on some handle the driver never sees.
$answer is returned for any $sth->func($name, ...) call. It can be a plain value, or a coderef, which is called with the fake handle, the function name and any extra arguments, and whose return value is the answer -- the coderef form is what a driver test uses to count calls. Registering undef explicitly is different from not registering the function: the call then ANSWERS undef (the way a DBD that failed to produce a value does) instead of falling through to the default.
Unregistered functions still answer undef. Registrations are per-storage and survive across calls; "clear_mocks" removes them along with the row mocks. There is no glob on core internals involved, and no dependence on when DBIO::Test::Storage was loaded -- the answer travels with the statement, not with the class.
set_auto_increment
$storage->set_auto_increment('Artist', 10);
Sets the auto-increment counter for a source so the next insert returns the given ID.
AUTHOR
DBIO & DBIx::Class Authors
COPYRIGHT AND LICENSE
Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.