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.