NAME
DBIO::Test::StorageLayer - Structural contract check for DBIO storage-extension components and layers
VERSION
version 0.900004
SYNOPSIS
In an extension distribution's t/00-load.t:
use strict;
use warnings;
use Test::More;
use DBIO::Test::StorageLayer qw( storage_layer_contract_ok );
use_ok 'DBIO::PostgreSQL::Age';
use_ok 'DBIO::PostgreSQL::Age::Storage';
# component + layer
storage_layer_contract_ok(
'DBIO::PostgreSQL::Age' => 'DBIO::PostgreSQL::Age::Storage',
);
done_testing;
An inert marker component that registers no layer at all (DBIO::MySQL::EV, DBIO::PostgreSQL::EV -- they only tag the schema so the storage MRO can resolve the ev mode, ADR 0030) passes the layer class as undef, i.e. just omits it:
storage_layer_contract_ok('DBIO::MySQL::EV');
The arguments are positional, so a custom test name without a layer spells the layer undef:
storage_layer_contract_ok('DBIO::MySQL::EV', undef, 'EV marker stays inert');
DESCRIPTION
A DBIO storage extension does not pick a driver. It registers a storage layer ("register_storage_layer" in DBIO::Schema), the layer is composed over whatever base storage the DSN resolved to (DBIO::Storage::Composed, "_determine_driver" in DBIO::Storage::DBI), and the extension therefore rides every driver and every transport it is composed onto. That is the hard cut recorded in ADR 0032 section 7 ("Hard cut of the extension storage_type-override pattern").
This module makes that rule mechanical. Prose did not hold it: the rule was broken twice in the family, each time with a different mechanism, and each time it was only caught afterwards by review.
dbio-mysql-ev --
DBIO::MySQL::EVoverrodeconnection()and forcedstorage_type('+DBIO::MySQL::EV::Storage')on every connect, so the component decided the storage class instead of the DSN.dbio-postgresql-age --
DBIO::PostgreSQL::Agegaineduse base 'DBIO::PostgreSQL'to turn one red test assertion green. That welds the extension to one driver through inheritance rather than throughstorage_type, and it produces a genuine C3 regression as soon as the schema class loads both (Inconsistent hierarchy during C3 merge).
ADR 0032 section 7 names only storage_type, so the inheritance route was formally uncovered. The checks here are deliberately mechanism-independent: they ask what the class structurally is and what its connect hook structurally does, not which spelling was used to get there.
METHODS
storage_layer_contract_ok
storage_layer_contract_ok($component_class);
storage_layer_contract_ok($component_class, $layer_class);
storage_layer_contract_ok($component_class, $layer_class, $test_name);
One test: passes when $component_class (and, when given, $layer_class) satisfy the ADR 0032 section 7 contract described above. On failure the diagnostic names every violation, its cause, and points at the ADR.
$layer_class is optional -- an inert marker component that registers no layer (DBIO::MySQL::EV, DBIO::PostgreSQL::EV) is checked on its own. Both classes are loaded if they are not loaded already.
Returns the boolean the underlying assertion returned.
storage_layer_contract_violations
my @violations = storage_layer_contract_violations($component_class, $layer_class);
The assertion-free half of "storage_layer_contract_ok": returns the list of violation descriptions (empty when the contract holds), each a self-contained sentence naming the offending class and the rule it breaks. Useful when a repo wants to shape its own assertion around the result, and it is what core's own suite drives the negative fixtures through.
WHAT IS CHECKED, AND HOW
1. The component hangs off no driver
Every class in mro::get_linear_isa($component) except the component itself must be driver-neutral. An ancestor is a violation when it either
defines its own
connectionmethod, oris a storage class (
->isa('DBIO::Storage')).
connection() is the one seam where a component can decide the storage class, and it is exactly how every driver component does it -- DBIO::PostgreSQL, DBIO::MySQL and DBIO::SQLite each override connection to pin storage_type('+...::Storage'). A component that inherits a connection override has inherited somebody else's storage decision, which is the dbio-postgresql-age shape precisely. "Defines its own" is resolved through DBIO::Storage::Composed's own-method scan (compiled body in that package), so an imported or inherited sub never counts.
The obvious alternative -- "anything outside DBIO::Base and what core brings under it is suspicious" -- was tried and rejected on evidence: a well-behaved component reaches core classes far outside that chain through ordinary load_components. DBIO::PostgreSQL::PostGIS linearises as PostGIS, DBIO::InflateColumn, DBIO::Row, DBIO::Base, ... because it loads the InflateColumn component. A namespace/baseline rule flags that; the connect-hook rule does not, because neither DBIO::InflateColumn nor DBIO::Row touches the connect path. The sharper question ("does this ancestor carry a storage decision?") is the one worth asking.
For the same reason this check does not require the component to derive from DBIO::Base. load_components only unshifts a component into the consuming class's @ISA; a component with an empty @ISA composes and resolves exactly like one that inherits DBIO::Base. Requiring a base here would fail a component that pins nothing and welds to nothing, which is not what ADR 0032 section 7 is about.
2. The component pins no storage_type
Checked by running the connect hook, not by reading source. A throwaway probe class is synthesised with
@ISA = ( <recorder>, $component, <terminal> )
under the c3 MRO. The recorder is the most specific rung, so every $self->storage_type(...) and $self->register_storage_layer(...) made anywhere in the chain resolves to it and is recorded; the terminal is the least specific rung and supplies the final connection that the component's $self->next::method(@_) lands on. Then $probe->connection($dsn) is called and the recording is inspected: any storage_type setter call is the violation.
This is a structural probe rather than a source grep, and it catches both incidents through the one mechanism that matters -- dbio-mysql-ev directly (the component's own hook pins), and dbio-postgresql-age too (the inherited driver's hook pins, reached through the probe's MRO). It is safe to run: connection() in DBIO never opens a database handle, it only resolves and stores the storage object, and the probe's recorder/terminal keep even that from happening.
Two limits, stated rather than hidden:
only pinning reachable from
connection()is seen. A component that setstorage_typefrom some other hook (import, a class-level statement) would pass this check. No incident took that route, and both known ones are covered.if the probe call dies, that is reported as a violation naming the exception. A component's connect hook is a thin hook -- register a layer, chain with
next::method-- and one that cannot be called as such is a finding, not something to swallow silently.
3. The layer is a real layer
Only when a layer class is given. These are the rules DBIO::Storage::Composed states under Layer rules:
no
@ISApointing at a storage -- no ancestor may satisfy->isa('DBIO::Storage')(which coversDBIO::Storage::DBI, every driver storage, andDBIO::Storage::Async). A layer is mixed in above the base, not beside it.no constructor -- the layer must not define its own
new. The synthesised class inherits the base storage's constructor; a layer that brings one is a storage class wearing a layer's name.
The own-method scan reuses DBIO::Storage::Composed->_own_methods, the same routine the compose-time collision check uses. It stays a core-internal call -- nothing of DBIO::Storage::Composed's internals is re-exported from here.
NAMES A LAYER WRITES
A layer that issues statements about a schema, a database extension or a library (_set_connection_schema, _activate_extensions, an extension's install and load) handles every such name as an identifier (ADR 0077). It writes the name into a statement only through the driver's identifier quoting (or as a bind) and refuses with a DBIO::Exception a name it cannot write that way. It never joins a name into the statement text by string concatenation. The checks above are structural and do not see this; the Names list of DBIO::Test::Conformance does, in the driver's conformance run.
SEE ALSO
DBIO::Storage::Composed, "register_storage_layer" in DBIO::Schema, DBIO::Storage::DBI.
ADR 0032 in the DBIO core distribution's docs/adr/.
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.