NAME

DBIO::Storage::DBI - DBI storage handler

VERSION

version 0.900004

SYNOPSIS

my $schema = MySchema->connect('dbi:SQLite:my.db');

$schema->storage->debug(1);

my @stuff = $schema->storage->dbh_do(
  sub {
    my ($storage, $dbh, @args) = @_;
    $dbh->do("DROP TABLE authors");
  },
  @column_list
);

my $rs = $schema->resultset('Book')->search({
   written_on => $schema->storage->datetime_parser->format_datetime(DateTime->now)
});

See t/test/04_query_capture.t for a runnable example.

DESCRIPTION

This is DBIO's common storage backend for DBI-based databases. It owns the live database handle, transaction state, SQL maker integration, and driver determination.

A schema declares its driver ("storage_type" in DBIO::Schema, pinned by the driver's component or shortcut), so its storage is the driver's base class from the start -- DBIO::PostgreSQL::Storage, DBIO::MySQL::Storage, DBIO::SQLite::Storage. At the first use that needs the driver it adapts itself to the access path of the connection ("_adapt_to_access_path") and stays within its driver; what does not fit is reported through "_driver_mismatch". This generic class itself has no driver: a schema whose storage would be of it is refused at connect, and a storage of it built by hand is refused at the first use that needs the driver (ADR 0067). Which driver a DSN leads to is answered by "driver_for" in DBIO.

See DBIO::Storage for the storage API shared across all backends. This POD focuses on the DBI-specific layer, including connection handling, capability checks, and the hooks that driver subclasses build on.

METHODS

connect_info

This method is normally called by "connection" in DBIO::Schema, which encapsulates its argument list in an arrayref before passing them here.

The argument list may contain:

  • The same 4-element argument set one would normally pass to "connect" in DBI, optionally followed by extra attributes recognized by DBIO:

    $connect_info_args = [ $dsn, $user, $password, \%dbi_attributes?, \%extra_attributes? ];
  • A single code reference which returns a connected DBI database handle optionally followed by extra attributes recognized by DBIO:

    $connect_info_args = [ sub { DBI->connect (...) }, \%extra_attributes? ];
  • A single hashref with all the attributes and the dsn/user/password mixed together:

    $connect_info_args = [{
      dsn => $dsn,
      user => $user,
      password => $pass,
      %dbi_attributes,
      %extra_attributes,
    }];
    
    $connect_info_args = [{
      dbh_maker => sub { DBI->connect (...) },
      %dbi_attributes,
      %extra_attributes,
    }];

    This is particularly useful for Catalyst based applications, allowing the following config (Config::General style):

    <Model::DB>
      schema_class   App::DB
      <connect_info>
        dsn          dbi:mysql:database=test
        user         testuser
        password     TestPass
        AutoCommit   1
      </connect_info>
    </Model::DB>

    The dsn/user/password combination can be substituted by the dbh_maker key whose value is a coderef that returns a connected DBI database handle

    In place of dsn the hash may name the database by its parts -- a dbname key, optionally with host and port:

    $connect_info_args = [{
      dbname => $dbname,
      host => $host,
      port => $port,
      user => $user,
      password => $pass,
      %dbi_attributes,
      %extra_attributes,
    }];

    The DSN is then built for the DBD the storage class is registered for ("_connect_params_dsn"), the driver the schema declares: the generic storage, which has no driver, throws, and so does a storage whose DBD has no DSN form these parts can fill (DBIO::Storage::DBI::ODBC). It is the dbname key that selects this form. A hash with host or port and neither a dsn nor a dbname key throws: there is no DSN to build them into, and they are never passed on as DBI attributes.

  • A DBIO::AccessBroker, which answers the connection details -- asked again for every new connection -- optionally followed by the connect options of this connection:

    $connect_info_args = [ $broker ];
    $connect_info_args = [ $broker, \%dbi_attributes_and_extra_attributes ];
    
    $connect_info_args = [ $broker, {
      async         => 'future_io',
      quote_names   => 0,
      on_connect_do => [ 'SET search_path TO app' ],
      pg_enable_utf8 => 1,
    } ];

    The hash beside the broker takes what the attribute hash of every other form takes: DBI attributes, the "DBIO specific connection attributes", the async mode and pool_size. It does not take the connection itself -- dsn, user, password, dbh_maker, dbname, host, port throw -- which database, and as whom, is the broker's answer (ADR 0013). This is how a broker that answers a hash of connection parameters (DBIO::AccessBroker::Static) gets them at all.

    An answer that names no database -- no DSN, and no dbname to build one from -- throws, naming the broker class. With a broker the storage never falls back to $ENV{DBI_DSN}: the broker's credentials go to the database the broker names ("connect_info_for_storage" in DBIO::AccessBroker).

    The storage does not open a connection without a parameter it was given and would not apply. Of a hash answer only dbname, host and port are built into the DSN, and what stands beside the broker joins the DBI attributes, so a parameter of the database client (sslmode, connect_timeout, ...) in either place would reach DBI->connect as an attribute the DBD does not read. What passes are the connect options, and DBI attributes by their name -- an uppercase first letter, private_, dbi_, dbd_, or the prefix of the DBD the answer's DSN names ("_dbd_attribute_prefixes"). With any other key set to a value, in a hash answer or beside the broker, the storage throws when it is to connect, naming the broker class, the keys and which of the two places they stand in. That is at the first use of its dbh and at every reconnect, not here: connect_info configures the storage with such an answer, since it may be for an async backend alone -- a transport that takes the hash opens its pooled connections with the parameter, and this storage's own connection throws only once something asks for it. A parameter of the database client is the broker's to answer, never a connect option beside it: inside the DSN of the DBI form, or by the storage that asks ("connect_info_for_storage" in DBIO::AccessBroker).

    A broker answering a code reference (dbh_maker) opens the connection by that code, and the storage applies no DBI attribute and no client parameter to it. So beside such a broker every key the storage does not consume itself -- a DBI attribute such as LongReadLen as much as sslmode -- throws when the storage is to connect; the connect options, the SQL maker options, async, pool_size and ignore_version pass. The options are judged before they are set against the broker's answer.

    A broker answering the DBI form may carry some of the same settings among its attributes (DBIO::AccessBroker::Vault's dbi_attrs). A setting given in both places must have the same value in both, or the connect throws naming it -- when the storage is configured, and again whenever the broker is asked for a new connection.

    With an async mode, the settings a backend applies itself -- the DBI attributes and pool_size -- are handed to the async backend beside the broker. pool_size sizes its pool. The DBI attributes are treated as that transport treats the same keys in DBI-form connect info: a transport that opens its pooled connections through a DBD (the future_io adapters) opens them with these attributes too; one whose client is no DBD (the ev transports) drops them, so there they apply to this storage's own connection only -- a parameter of that client (sslmode, charset, ...) reaches a pooled connection only as part of the broker's answer. A transport that does not take the settings at all (it does not declare broker_connect_options, see "transport_capabilities" in DBIO::Storage::Async) throws at its first use rather than open its pool without them. Every other setting is this storage's own, exactly as when it comes with a DSN; the pooled connections replay on_connect_do/on_connect_call from it.

    A pool_size in the broker's answer sizes the backend's pool as well, and stays out of this storage's own DBI->connect. Given both there and beside the broker, the two must be the same value; the backend compares them when it is built ("_access_broker_pool_size" in DBIO::Storage::Async).

Static connect info -- the forms above without a broker -- carries its DSN: the DSN names the driver. Connect info without one throws here, naming its shape and never a value of it: no arguments at all, the DBI form with an undefined or an empty DSN, a hash whose dsn is undefined or empty, and a hash with neither a dsn nor a dbname key. $ENV{DBI_DSN} does not stand in for it -- the storage connects to the DSN it is given and to no other, and a storage that never got connect info has nothing to connect from. A hash with a dbname key has a DSN, the one built from its parts. The code reference forms open the connection themselves and need none; a broker's answer has the rule described with it above. A schema that is only to generate SQL names its DSN like any other: nothing is opened before the first use that needs a connection.

Static connect info is not judged for client parameters. sslmode (and any other parameter of the database client) in the hash form, or in the attribute hash of the DBI form, is passed on unchanged to DBI->connect as a DBI attribute, so it is also kept in the connect attributes. DBI and the DBD decide: a driver that knows the name applies it, another one rejects it or ignores it. The strict rule above is for a broker, where the storage knows which parameters it would leave unapplied.

A connection that cannot be opened throws DBI Connection failed: ..., with DBI's own error after it. DBI quotes the DSN there, and a driver may repeat more of what it was given. The password is not part of the message, in whichever form the connect info came: the password argument, a Password among the DBI attributes and a password inside the DSN -- the value of a password, passwd, pwd or pass key, or the password of a URI -- are shown as ***, while the key, the user name and the rest of the DSN stay. When DBI's error comes with a stack trace of Carp -- as every croak does under $Carp::Verbose -- they are masked among the arguments of its frames too, escaped or shortened as Carp shows an argument. The same holds wherever DBIO writes connect info into a message: the warning about a driver that cannot be determined, and the trace of DBIO::Replicated. It holds for the arguments of a stack trace as well ("stacktrace" in DBIO::Schema), where the connect info is an argument of "connect" in DBIO::Schema.

Please note that the DBI docs recommend that you always explicitly set AutoCommit to either 0 or 1. DBIO further recommends that it be set to 1, and that you perform transactions via our "txn_do" in DBIO::Schema method. DBIO will set it to 1 if you do not do explicitly set it to zero. This is the default for most DBDs. See "DBIO and AutoCommit" for details.

DBIO specific connection attributes

In addition to the standard DBI connection attributes, DBIO recognizes the following connection options. These options can be mixed in with your other DBI connection attributes, or placed in a separate hashref (\%extra_attributes) as shown above.

Unknown attributes are refused. Without a broker the whole connect info is known at connect_info, so a key that is neither one of the options below nor a DBI attribute by its name throws there, naming the key (never the value) -- { limit_dialect => 'Top' } used to be handed to DBI->connect and dropped without a trace. What passes: the options below, a DBI attribute by name (an uppercase first letter, private_, dbi_, dbd_, or the prefix of the DBD the DSN names, "_dbd_attribute_prefixes"), sslmode (handed to the DBD, which reads it or not), and a key with an undefined or empty value. A parameter of the database client belongs in the DSN. The code reference forms (dbh_maker) have no DSN and apply no attribute, and are not checked. With a broker the rules described with the AccessBroker form above stand.

Each connect_info call is a full reconfiguration of the storage, not a merge (ADR 0050). Options named in the call are applied; every other storage option -- including on_connect_do/on_connect_call and their disconnect counterparts, and any option a driver adds -- reverts to its default first. cursor_class is the one exception: it reverts to its class default rather than to undef. This diverges deliberately from upstream DBIx::Class, which only ever set options that were present in a given call and silently left an absent option (e.g. a previously-set on_connect_do) active on the next connect -- a fossil of how the option-picker was written in 2006, never a designed "options persist" behavior. If you rely on an option across repeated connect_info calls, restate it every time, or set it via its accessor afterwards.

schema

The namespace this connection works in, and the one a deploy over it installs into: a single name, nothing else (ADR 0065). Core defines the word and the contract; what a namespace is, is the driver's -- PostgreSQL's search_path, Oracle's CURRENT_SCHEMA, DB2's SET SCHEMA, DuckDB's USE. A driver gives it meaning by overriding _set_connection_schema($name), which runs on every new connection (sync, and each async pool connection) before the on_connect_* actions. A driver without namespaces keeps the default, which throws a DBIO::Exception naming the storage class. The seam also gets the activation of the declared database extensions as its second argument (_set_connection_schema($name, $activation)), or undef.

The name is an identifier (ADR 0077). Core refuses a name with ;, a line break, CR or NUL (a DBIO::Exception that names the key and never the value); the rest of what it holds -- a quote, a blank, a backslash -- is the driver's. The driver sets the name only through its identifier quoting (or as a bind), or refuses it with a DBIO::Exception; it never builds the statement by joining the name into it. The same holds for the extension names and libraries of _activate_extensions. The Names list of DBIO::Test::Conformance checks it per driver.

Before the seam, core asks the driver _extension_activation($name) -- undef by default, else { load => [ ... ], search_path => [ ... ] } (ADR 0066) -- and, when there is one, hands it to _activate_extensions($activation, $name), which writes the driver's statements (PostgreSQL: LOAD, and without a namespace the appending to the search_path). Both default to nothing. The same sequence runs for the sync connection and for every async pool connection. The key never selects or changes the driver, and is kept on the storage as connection_schema (the schema accessor is the back-reference to the schema class).

on_connect_do

Specifies things to do immediately after connecting or re-connecting to the database. Its value may contain:

a scalar

This contains one SQL statement to execute.

an array reference

This contains SQL statements to execute in order. Each element is a string, an array reference [ $sql, \%dbi_attrs, @bind ] holding the arguments to "do" in DBI, or a code reference. A code reference is called as $code->($storage) and returns one SQL string, an array reference of such elements, or nothing.

a code reference

This contains some code to execute. Unlike code references within an array reference, its return value is ignored.

An error about a statement that fails does not quote what the statement carries as a secret: the value of a key, token, secret or password (PRAGMA key = '...', ATTACH '...?token=...') is shown as ***, also where the database repeats a part of it in its own error text ("mask_statement_secrets" in DBIO::Util).

on_disconnect_do

Takes arguments in the same form as "on_connect_do" and executes them immediately before disconnecting from the database.

Note, this only runs if you explicitly call "disconnect" on the storage object.

on_connect_call

A more generalized form of "on_connect_do" that calls the specified connect_call_METHOD methods in your storage driver.

on_connect_do => 'select 1'

is equivalent to:

on_connect_call => [ [ do_sql => 'select 1' ] ]

Its values may contain:

a scalar

Will call the connect_call_METHOD method.

a code reference

Will execute $code->($storage)

an array reference

Each value can be a method name or code reference.

an array of arrays

For each array, the first item is taken to be the connect_call_ method name or code reference, and the rest are parameters to it.

Some predefined storage methods you may use:

do_sql

Executes a SQL string, or the statements a code reference returns. This is what "on_connect_do" and "on_disconnect_do" use.

It can take:

a scalar

Will execute the scalar as SQL.

an arrayref

Taken to be arguments to "do" in DBI, the SQL string optionally followed by the attributes hashref and bind values.

a code reference

Will execute $code->($storage), which returns one SQL string (a single statement), an arrayref of statements -- each a scalar or an arrayref as above -- or nothing, and execute them in order.

datetime_setup

Execute any statements necessary to initialize the database session to return and accept datetime/timestamp values used with DBIO::InflateColumn::DateTime.

Only necessary for some databases, see your specific storage driver for implementation details.

on_disconnect_call

Takes arguments in the same form as "on_connect_call" and executes them immediately before disconnecting from the database.

Calls the disconnect_call_METHOD methods as opposed to the connect_call_METHOD methods called by "on_connect_call".

Note, this only runs if you explicitly call "disconnect" on the storage object.

disable_sth_caching

If set to a true value, this option will disable the caching of statement handles via "prepare_cached" in DBI.

quote_names

On by default in DBIO (unlike upstream DBIx::Class, where it defaults off). Automatically sets "quote_char" and "name_sep" to the characters appropriate for your particular RDBMS, so generated SQL always quotes table and column identifiers. This is a security-hardening default: it makes reserved words and unusual identifiers safe and removes a class of SQL-injection footguns. This option is preferred over specifying "quote_char" directly.

Pass quote_names => 0 in your connect_info to opt out and emit unquoted identifiers. An explicitly supplied "quote_char" / "name_sep" is always respected.

quote_char

Specifies what characters to use to quote table and column names.

quote_char expects either a single character, in which case is it is placed on either side of the table/column name, or an arrayref of length 2 in which case the table/column name is placed between the elements.

For example under MySQL you should use quote_char => '`', and for SQL Server you should use quote_char => [qw/[ ]/].

name_sep

This parameter is only useful in conjunction with quote_char, and is used to specify the character that separates elements (schemas, tables, columns) from each other. If unspecified it defaults to the most commonly used ..

unsafe

This Storage driver normally installs its own HandleError and makes sure RaiseError is on, on all database handles, including those supplied by a coderef. On the handles it opens itself it also defaults PrintError to off; your connect_info DBI options override that, and a handle supplied by a coderef keeps it as the coderef set it. It does this so that it can have consistent and useful error behavior. ShowErrorStatement is always off, whatever connect_info says: an error text carries the statement and the number of its bind values, never the values themselves (see "Error texts carry no bind values" in DBIO::Exception).

If you set this option to a true value, Storage will not do its usual modifications to the database handle's attributes, and instead relies on the settings in your connect_info DBI options (or the values you set in your connection coderef, in the case that you are connecting via coderef).

Note that your custom settings can cause Storage to malfunction, especially if you set a HandleError handler that suppresses exceptions and/or disable RaiseError.

auto_savepoint

If this option is true, DBIO will use savepoints when nesting transactions, making it possible to recover from failure in the inner transaction without having to abort all outer transactions.

cursor_class

Use this argument to supply a cursor class other than the default DBIO::Storage::DBI::Cursor.

async

The async mode of this connection, by name: immediate, future_io, or a mode an add-on registered -- forked, ev, ... (see "register_async_mode" and DBIO::Manual::AsyncStorage). It decides how the *_async methods of this storage are answered; a mode that is not available throws at its first use. Without it the connection is synchronous and the *_async methods throw. The mode is DBIO's own and never reaches DBI->connect.

pool_size

With an async mode, the size of the async backend's connection pool -- a positive integer, default 5 -- kept out of this storage's own DBI->connect. A mode without a pool (immediate, forked) has nothing to size. Without an async mode it is no DBIO option: it is passed to the DBD like any other attribute.

acquire_timeout, max_waiters, txn_timeout, reset_timeout

The other async options. With an async mode they go to the async backend and stay out of this storage's own DBI->connect: acquire_timeout is the seconds a caller waits for a pool connection (default 30, 0 for no timeout), max_waiters the number of callers that may be queued for one, txn_timeout the seconds a txn_do_async may take, reset_timeout the seconds the reset of a released connection may take (default 10, 0 for no bound; ADR 0079). The list of async options is declared, not open: the four above and pool_size are the core's, and a mode's backend adds the names it takes ("async_option_names" in DBIO::Storage::Async). Any other name that is no DBI attribute is refused like every unknown attribute. Without an async mode they are no DBIO options and the DBD gives its verdict, like for pool_size. DBIO::Manual::AsyncStorage describes them.

Some real-life examples of arguments to "connect_info" and "connect" in DBIO::Schema

# Simple SQLite connection
->connect_info([ 'dbi:SQLite:./foo.db' ]);

# Connect via subref
->connect_info([ sub { DBI->connect(...) } ]);

# Connect via subref in hashref
->connect_info([{
  dbh_maker => sub { DBI->connect(...) },
  on_connect_do => 'alter session ...',
}]);

# A bit more complicated
->connect_info(
  [
    'dbi:Pg:dbname=foo',
    'postgres',
    'my_pg_password',
    { AutoCommit => 1 },
    { quote_char => q{"} },
  ]
);

# Equivalent to the previous example
->connect_info(
  [
    'dbi:Pg:dbname=foo',
    'postgres',
    'my_pg_password',
    { AutoCommit => 1, quote_char => q{"}, name_sep => q{.} },
  ]
);

# Same, but with hashref as argument
->connect_info(
  [{
    dsn         => 'dbi:Pg:dbname=foo',
    user        => 'postgres',
    password    => 'my_pg_password',
    AutoCommit  => 1,
    quote_char  => q{"},
    name_sep    => q{.},
  }]
);

# Subref + DBIO-specific connection options
->connect_info(
  [
    sub { DBI->connect(...) },
    {
        quote_char => q{`},
        name_sep => q{@},
        on_connect_do => ['SET search_path TO myschema,otherschema,public'],
        disable_sth_caching => 1,
    },
  ]
);

_connect_params_dsn

sub _connect_params_dsn {
  my ($self, $driver, $params) = @_;
  ...
  return $dsn;
}

The per-driver seam for the form of the DSN built from connection parameters that carry none (ADR 0057) -- dbname, host and port, as DBIO::AccessBroker::Static answers them, or a connect-info hash that has a dbname key and no dsn. Override it in the driver's storage class when its DBD does not take the default form; everything else stays core's.

The dbname key decides, not its value: Static answers it as undef when no dbname was given, and the DSN is then built from the other parts. An empty dbname ('') is no such case: it names no database and is refused. A hash with neither a dsn nor a dbname key never reaches this seam: with host or port in it, it is refused before -- in static connect info and as a broker's answer alike -- so they do not become DBI attributes of a connection that has no DSN.

It receives $driver, the DBD name the storage class is registered for ("register_driver") -- the default puts it into the DSN; an override may name another DBD when its engine is reached through one (MSSQL through DBD::Sybase). And it receives $params, a fresh hashref holding only the parts given: dbname, host, port -- each key present only when its value is defined and not empty, and no value contains ;. It returns the DSN string, and nothing else. The default:

dbi:<driver>:dbname=<dbname>;host=<host>;port=<port>

in that order, leaving out the parts not given -- a form DBD::Pg, DBD::mysql, DBD::MariaDB, DBD::SQLite, DBD::Firebird and DBD::DuckDB take.

An override does not remove the parts from the connect info, check for ;, resolve the driver or handle a dbname that is a whole DSN (taken as the DSN before this seam is asked): core does all of that first, the same for every driver, so the dbname, host and port of such a hash never reach DBI->connect as attributes. It is asked only on a storage registered for exactly one DBD driver: a storage that is not -- the generic class, which has no driver -- refuses the parameters before it is asked.

A driver whose DBD has no form these parts can fill throws here, naming what its DBD needs instead (DBIO::Storage::DBI::ODBC does). It is called at "connect_info" and again for every new connection, since a broker is asked anew each time.

_bind_host_into_dsn

sub _bind_host_into_dsn {
  my ($self, $driver, $dsn, $host, $port) = @_;
  ...
  return $bound_dsn;                 # bound
  return (undef, $form);             # declined
}

The per-driver seam for putting a host (and port) into a DSN that is already written (ADR 0013, addendum 2026-10-09) -- what a DBIO::AccessBroker::HostBound view does with a DBI-form answer. How a DSN names its server is a question of the DBD; the view asks the driver's storage class, and core's default knows one form. Override it in the driver's storage class when its DBD names the server another way (libpq's hostaddr=, DB2's HOSTNAME=, Sybase's server=, Oracle Easy Connect, ...).

It is a class or an instance method and needs no instance data. $driver is the DBD name read off the DSN; $dsn is the whole DSN string, with its dbi:Driver: prefix; $host and $port are the view's, $port undef when the view has none, and neither contains ; -- the view refuses that before it asks. It returns the DSN with the host (and the port, when defined) bound, and nothing else, or declines by returning the list (undef, $form), with $form a short description of the form it cannot translate: a key name or a dialect, never a value of the DSN. The view refuses the connection with that form in its message, rather than let a replicant connect to the host the answer named.

The default translates the dbi:Driver: form (the prefix may carry (attr) parentheses) whose parts, separated by ;, are all key=value with a key of word characters: it replaces a host= or port= token where it stands, whatever its case, and puts a missing one in front of the others. It declines

  • a DSN without a dbi:Driver: prefix;

  • a non-empty part that is not key=value -- Oracle Easy Connect (//host:port/service), a TNS descriptor ((DESCRIPTION=...), an ODBC data source name, the positional DBD::mysql form;

  • a part whose key names a server another way, whatever its case: hostaddr, hostname, server, servername, address, addr, dsn;

  • a db, dbname or database part whose value begins with host: or host/port: (Firebird's db=host:path). A local path (dbname=/var/db/x) and a Windows drive (db=C:\data\x.fdb) stay local and translatable.

An override that does not know a form delegates to $self->next::method(@_), which declines it the same way. Called at every connection a view makes, since a broker is asked anew each time.

_dbd_attribute_prefixes

sub _dbd_attribute_prefixes {
  my ($self, $driver) = @_;
  ...
  return @prefixes;
}

The prefixes the private attributes of DBD $driver start with -- pg_ for Pg, mysql_ for mysql -- or nothing when they are not known. $driver is the DBD name read off the DSN of a connection, and is undefined when the DSN names none.

It is asked when the storage is to connect through an AccessBroker and a lowercase key it does not consume stands in the broker's hash answer or beside the broker: with one of these prefixes the key is an attribute of that DBD and goes to DBI->connect; without, it cannot be told from a parameter of the database client that DBI->connect would silently not apply, and the storage throws rather than connect ("_assert_access_broker_parameters_apply" in DBIO::Storage::DBI::AccessBroker, "_assert_access_broker_options_apply" in DBIO::Storage::DBI::AccessBroker).

The default asks DBI's own registry of driver prefixes (DBI->driver_prefix), which needs no DBD loaded and knows the DBDs registered with DBI. Override it in the storage class of a driver whose DBD is not among them (DBD::Firebird, DBD::DuckDB), or takes attributes under more than one prefix, and delegate every other $driver to $self->next::method($driver).

register_async_mode

DBIO::Storage::DBI->register_async_mode( forked    => 'DBIO::Forked::Storage' );
DBIO::PostgreSQL::Storage->register_async_mode( ev   => 'DBIO::PostgreSQL::EV::Storage' );

Registers an async mode name against the embedded backend class that answers the *_async methods for a connection opened with { async => $mode }. Generic modes register on a base storage class (inherited by every driver); native modes register on a concrete driver storage class, so the same logical mode name (e.g. ev) resolves to a DB-specific backend. After lazy loading, every target must be a DBIO::Storage::Async subclass except the literal immediate mode, whose DBIO::Future::Immediate target deliberately enables synchronous dispatch. A loaded wrong-type target for any other mode croaks rather than falling back. See ADR 0030.

The future_io mode is the exception: it is resolved by convention, not from this registry. Rather than a registered class, its transport adapter is found by walking the concrete storage's linearised ISA most-specific first and taking the first <class>::Async that loads (stopping before the generic DBIO::Storage::DBI base), so an extension storage_type subclass without its own adapter inherits the nearest parent driver's one. An explicit register_async_mode(future_io => ...) on a concrete class still wins over the walk; a same-named adapter that loads but is not a DBIO::Storage::Async croaks rather than degrading to a parent. See ADR 0030.

register_access_path_dbd

__PACKAGE__->register_access_path_dbd('mysql', 'MariaDB');

Arguments: @dbd_names. Return value: none.

Declares, on the storage class of a driver, the DBDs of other drivers that this driver takes as its own access path -- the transport it talks to its engine through. It is for a driver whose transport is the DBD of a foreign driver and which, deliberately, does not inherit that driver's storage (a ClickHouse driver speaking the MySQL protocol over DBD::mysql is the case this exists for).

The check a declared storage runs against the DBD of its DSN ("_adapt_to_access_path", ADR 0059) asks "register_driver" which driver a DBD belongs to and reports a DBD registered for a class that is neither an ancestor of the storage nor a subclass of the declared class. A DBD declared here is not judged at all: the check returns without a finding, under driver_strict too. Anything not declared is judged as before, so a mistaken DSN through a DBD the driver does not use is still reported.

The declaration belongs to the class it is made on and is inherited by its subclasses, as class inheritance goes -- never by a sibling, never by a parent. It is not global: another driver's declaration does not make a DBD acceptable to you. When the driver's storage does inherit the class the DBD is registered for, the inheritance already says so and nothing needs to be declared; that stays the normal way. Call this once at load time, beside "register_driver". The declaration does not change driver detection, the @ISA of any class or the "_adapt_to_access_path" hook.

_adapt_to_access_path

sub _adapt_to_access_path {
  my ($self, $dbd) = @_;

  if (defined $dbd and $dbd eq 'ODBC') {
    $self->ensure_class_loaded('DBIO::MyDB::Storage::ODBC');
    bless $self, 'DBIO::MyDB::Storage::ODBC';
  }
}

The hook through which a declared driver storage adapts itself to the access path of a connection -- the DBD and client library the DSN leads through (ADR 0059). A schema declares its driver: its driver component, or a driver shortcut ("apply_driver" in DBIO), pins the driver's base storage class as "storage_type" in DBIO::Schema. That class stands: it never becomes the class of another driver (ADR 0059, ADR 0067). What may still differ within one driver is the class for the access path -- the same engine over its native DBD, over ODBC, over another client library -- and that is the driver's own to decide, here.

Core calls it once per storage instance, during driver determination, on a storage whose class is not the generic one, before _init -- so _init runs on the class the hook chose. Driver determination is lazy: it runs at the first use that needs the driver, not at connect. The generic storage has no driver and is refused there instead (ADR 0067).

$dbd is the name of the DBD as far as core knows it without connecting: that of the live handle when there is one, else the one the DSN names; a DSN with an empty driver part is refused, and $ENV{DBI_DRIVER} stands in for nothing. It is undefined when the connect info is a code reference that has not been run, and when there is no DSN at all. Core opens no connection for the hook and asks the server nothing. A hook that needs more gets it itself -- $self->_get_dbh connects, and connection actions (on_connect_do, on_connect_call) then run once, after _init -- and that cost is the driver's decision.

The hook works on the instance: it reblesses $self, may call itself again on the new class, and may set options. The one rule is that it specialises within its own driver: afterwards the storage must still be an instance of the declared class. If it is not, core puts the storage back into the class it had and throws, naming both classes -- whatever "driver_strict" in DBIO::Schema and "driver_warnings" in DBIO::Schema say, since that is a defect of the driver and no mismatch. A server that turns out to be another product is reported through "_driver_mismatch"; the storage does not move into that product's class. Storage layers ("register_storage_layer" in DBIO::Schema) survive: when the hook reblesses a layered storage into a plain class, core composes the same layers over it. An exception from the hook, or from _init after it, leaves the storage in the class it had and without a connection -- no connection action has run on the one a connect or the hook had opened -- and determination is tried again at the next use.

After the hook core checks the DBD once more, if it is known: when "register_driver" maps it to a storage class that is no ancestor of the adapted storage and no subclass of the declared class, the DSN leads through the DBD of another driver, and core reports it through "_driver_mismatch". A DBD nothing is registered for is not judged, and neither is a declared class none of whose ancestors is registered for a DBD -- a private subclass of this generic storage, set as storage_type by hand, has no driver to compare the DBD with. A subclass of a registered driver class is judged. The inheritance of the adapted class is the whole declaration, and the normal one: there is no list of "my DBDs" to keep. The one exception is a driver whose transport is the DBD of a foreign driver and which does not inherit that driver's storage; it declares those DBDs with "register_access_path_dbd".

A class of another distribution that inherits the declared class passes that inheritance rule (DBIO::MSSQL::Storage::Sybase inherits DBIO::Sybase::Storage). Moving the storage there is the driver's own breach of its declaration, and nothing here catches it.

The default does nothing; a driver without access paths of its own needs no hook, and its storage stays the declared class as before.

_driver_mismatch

$self->_driver_mismatch(
  'the server answers as Microsoft SQL Server, not as Sybase ASE'
);

The one place a declared driver that does not fit what it meets is reported (ADR 0059). Core calls it when the DSN leads through the DBD of another driver (see "_adapt_to_access_path"). A driver calls it when it notices, from what it knows anyway, that the server is not its engine -- in its _adapt_to_access_path, or wherever that knowledge turns up. Neither asks the server a question for the sake of the check.

What happens is the schema's to say:

driver_strict wins over driver_warnings(0). The switches are those of the storage's schema. A storage without a schema -- a writer or bulk storage a driver builds with (ref $self)->new -- reads them off the schema of the nearest parent storage along _parent_storage, a plain accessor the driver sets to the storage it was built from (the chain is walked a bounded number of steps, and a cycle ends it). With no schema found it gets the default: not strict, warns. The message is prefixed with the declared storage class and, in a warning, followed by the two switches; pass the finding alone, and never connect info -- a DBD or class name says enough.

_finish_connect_dsn

sub _finish_connect_dsn {
  my ($self, $dsn) = @_;
  return "$dsn;maxConnect=$n";
}

The per-driver seam for the last word on the DSN of a connection. _connect asks it for the DSN string of every connect, after the connect info is formed -- from a broker's answer or from the stored connect info, the same place for both -- and connects with what it returns. Without it, whatever a driver appended to the stored DSN would be lost for a broker, whose answer forms the DSN anew for each connection.

It is not asked for a connection coderef or any other reference in place of the DSN. The stored connect info is not touched: the seam works on a copy, so it must be idempotent -- it is called again at every reconnect and has to return the same DSN for the same input, not append a second time. The default returns the DSN unchanged. It returns the DSN string, and nothing else.

_mask_error_values

my $text = $storage->_mask_error_values($text, \@values, $statement);

The one seam through which the text an engine writes into an error passes before it is thrown (ADR 0075). $text is that text, \@values the values bound to the statement that was running as plain values -- undef and a reference that is no plain value are left out, an object with a stringification counts as its string; the list may be empty -- and $statement the statement in placeholder form, or undef. It answers $text with every value it recognises replaced by ***. It never connects and never throws.

The base implementation hands the values to "mask_bound_values" in DBIO::Util: each value of at least four characters, as it was written and as the handle quotes it (when the storage has a connection). A driver whose engine writes values in a form of its own, or rules for the shorter ones, overrides the method and calls next::method:

sub _mask_error_values {
  my ($self, $text, $values, $statement) = @_;
  $text = $self->next::method($text, $values, $statement);
  $text =~ s/\(value\)=\(.*?\)/(value)=(***)/g;   # the engine's own form
  return $text;
}

Core calls it wherever engine text reaches an exception: the error handler of every handle (_dbi_handle_error), the statement run by _dbh_execute, the statements of the connection actions, and the errors of a bulk populate.

_dbi_error_fields

my $fields = $storage->_dbi_error_fields($handle);   # { sqlstate => ... }

The machine-readable part of a DBI error, as a hashref with the keys of DBIO::Exception::Server -- sqlstate, constraint, table, detail; one the handle did not report is left out. $handle is the handle DBI passed to HandleError. The base class answers sqlstate only, from $handle->state: DBI's own S1000, 00000 and the empty string mean "no SQLSTATE" and give no sqlstate. A driver whose DBD names more -- the constraint, the table -- overrides this, calls next::method and adds the keys. It invents nothing and never connects or queries.

_execute_sth

my ($rv, $messages) = $self->_execute_sth($dbh, $sth);

The execute verb of _dbh_execute, wrapped so a driver can collect the server messages a statement produced during its execution -- the channel engines answer on when the answer is not a result set (Sybase ASE's showplan, Transact-SQL print, PostgreSQL NOTICE). The base implementation is the bare execute:

my $rv = $sth->execute;
return ($rv, undef);

$dbh is the handle the statement actually runs on -- the one _dbh_execute received. On a DBIO::Replicated::Storage that is the handle of the backend the statement was routed to: the read handler's for the reading paths (_select, "_execute_rendered"), the master's for insert, update and delete. A driver that can collect server messages installs its handler on that $dbh, executes, and returns the collected messages as the second value; core stores them where the driver can read them back (_server_messages). The base returns undef for the messages, which is the "this engine does not collect" answer. The shape a driver (DBD::Sybase / syb_err_handler) writes:

sub _execute_sth {
  my ($self, $dbh, $sth) = @_;
  my $messages = [];
  my $old     = $dbh->{syb_err_handler};
  $dbh->{syb_err_handler} = sub { push @$messages, $_[3]; 0 };

  # restore the prior handler even if $sth->execute dies (DBIO has
  # RaiseError=1, so that is the normal error path, not an edge case)
  my $guard = DBIO::Util::scope_guard {
    $dbh->{syb_err_handler} = $old;
  };

  my $rv = $sth->execute;
  return ($rv, $messages);
}

The restore MUST run on every exit path -- success, exception, die. DBIO::Util::scope_guard (or CPAN Scope::Guard) is the standard pattern; the guard's DESTROY fires the restore ref as the sub unwinds, so a dying $sth-execute> still reaches it. Doing it by hand with eval { ... }; ... = $old; works but is easy to forget at the bottom of the sub, and $sth-execute> dying is the normal error case under RaiseError=1, not an edge case -- skip the guard and the next statement on the same handle inherits the dead handler.

On a failed _dbh_execute ($sth-execute> returns false, throw_exception runs before _server_messages($messages) is reached) the old value is kept: _server_messages only overwrites after the success check, so a driver reading it after a failed statement sees the messages from the last successful statement, not the most recent one. Drivers that read _server_messages must not assume it tracks the most recent statement -- only the most recent successful one. (ADR 0040 section 7.)

The seam keeps everything "_execute_rendered" keeps: the execute still happens inside _query_start / _query_end, so the trace and therefore "redact_bind_value" (ADR 0025) still see the statement; dbh_do still retries over a dropped connection, and each retry re-runs this method, so the handler is installed fresh on the handle the retry executes on; and the driver never reaches for $self->dbh, so the read handler of a replicated storage is preserved. ADR 0040 section 7 records the shape.

_adjust_bind_values

my $bind = $self->_adjust_bind_values($sth, $bind, $bind_attrs);

The one seam for a driver that changes the values it binds -- encoding a character string for a client library that wants bytes, turning a boolean or a number into the form the engine takes, and the like. $bind is the list of [ $meta, $value ] pairs of the statement ($meta carries sqlt_datatype, dbic_colname, ...), $bind_attrs the DBI attribute for each position as "_dbi_attrs_for_bind" resolved it. The answer is an arrayref of pairs of the same form and length. The base implementation returns $bind unchanged.

Both ways values reach the database run through it: _bind_sth_params, for every statement bound one by one (create, search, update), and _dbh_execute_for_fetch, for each row of a populate in void context. The two therefore bind the same values. A driver adjusts values here and overrides neither of those methods for that purpose.

A driver that changes a value returns a new list of new pairs: the values belong to the caller and are never written to. A scalar reference in the value slot is a bind_param_inout target; keep it the very same reference. The attributes are not adjusted here -- they stay as _dbi_attrs_for_bind resolved them.

_random_function

my $func = $storage->_random_function;

The function call this engine spells "give me a random number" with, for "rand" in DBIO::ResultSet, which drops it into an order_by literal.

The base implementation returns RANDOM() -- correct on PostgreSQL, SQLite and DuckDB. This is the declaration site: a driver whose engine spells it differently overrides this hook, and one that does not inherit a call its engine will reject. The drivers whose engine spells it differently override it -- NEWID() on SQL Server, RAND() on MySQL / MariaDB, DBMS_RANDOM.VALUE on Oracle, which has no RANDOM() at all (ORA-00904), among others.

Static, like "_exists_value_sql": string out, no $dbh, no version probe. It is on the driver-determination list at the top of this class, so a never-connected storage already answers with the spelling of the driver its DSN names, and DBIO::Replicated::Storage delegates it to the master backend next to sqlt_type.

There is no capability flag beside it. The failure mode is loud -- an engine that does not know the function names it in its own error -- and a caller who learned "unsupported" would have no second call to make: ordering rows at random has no portable fallback in the ORM. This is the same cut ADR 0037 section 1 made for set difference and ADR 0039 generalised to all six set operations in "_set_operation_keyword".

_explain_query

my $rows = $storage->_explain_query($sql, \@bind);

The whole operation of obtaining a query plan on this engine, for "explain" in DBIO::ResultSet. $sql is the query to be explained, already stripped of the parentheses "as_query" in DBIO::ResultSet wraps it in; \@bind are that query's bind values in the native [ \%attrs, $value ] pair format. The return value is the plan as an arrayref of row arrayrefs -- the shape $sth->fetchall_arrayref produces, whatever the engine had to do to get there. Its contents are engine-specific and always were: no two engines in this family name the same plan columns.

The base implementation is the prefix form:

my (undef, $sth) = $self->_execute_rendered("EXPLAIN $sql", $bind);
return $sth->fetchall_arrayref;

correct on PostgreSQL, MySQL / MariaDB, ClickHouse and DuckDB. This is the declaration site: a driver whose engine does it differently overrides this hook, and one that does not inherit an operation its engine will reject -- or, worse, accept and answer wrongly. Known so far:

  • SQLite accepts a bare EXPLAIN but answers with VM bytecode rather than a query plan -- the one silent wrong answer in this family. It wants EXPLAIN QUERY PLAN. (Measured, on libsqlite 3.51.1.)

  • Oracle needs EXPLAIN PLAN FOR, which returns no rows at all because it writes into PLAN_TABLE, so the plan comes from a second statement, SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY). (Vendor documentation.)

  • DB2 is the same two-statement shape into its EXPLAIN_* tables, spelled EXPLAIN PLAN SELECTION FOR. (Vendor documentation; no engine has confirmed it.)

  • SQL Server has no EXPLAIN statement at all and drives the plan through a session setting -- SET SHOWPLAN_ALL ON, which must stand alone in its batch and suppresses execution, so the query itself answers with the plan; then OFF, and from a guard, or the setting outlives the failure and every later query on that connection silently returns a plan instead of its rows. Three round-trips plus session state. (Vendor documentation.)

  • Sybase ASE is not the same case, despite both being Transact-SQL. Its setting is set showplan on, it does not suppress execution -- the query runs as well, so set noexec on / off has to bracket it, five statements in all -- and the plan comes back as server messages rather than rows, on the channel DBD::Sybase exposes only through syb_err_handler. Nothing reaches $sth->fetchall_arrayref, so the driver collects the messages through the "_execute_sth" seam instead: it installs its handler on the handle the statement runs on, executes, and reads the messages back from _server_messages, wrapping them as a single-column rowset. ADR 0040 section 7 records the shape. (SAP documentation plus DBD::Sybase's own POD.)

  • Firebird has no statement of its own: the plan is a property of the prepared statement (isc_info_sql_get_plan), read through the DBD's private $sth->func('ib_plan') -- see "_prepare_rendered". (DBD::Firebird XS and POD.)

This hook is a method rather than a string precisely so those shapes can be expressed (ADR 0040).

The parenthesised note on each entry is its evidence, and it is there because three of these were wrong when first written. Core runs no database: every engine claim above is vendor documentation, DBD documentation or a driver's own measurement, never core's. Check your engine before implementing against one of them, and report back if it does not hold -- that is how the ASE entry and the Firebird one came to be correct.

Every statement an override issues MUST go through the storage's statement path -- "_execute_rendered" when the statement is executed, "_prepare_rendered" when the engine's answer is an attribute of the prepared statement and nothing is executed at all. This is a requirement of the contract, not advice, and it has no engine-named exceptions. Reaching for $self->dbh directly loses _query_start / _query_end tracing and therefore "redact_bind_value" (ADR 0025), dbh_do retry over disconnects, DBIO::Storage::DBI::NoBindVars bind interpolation, and -- on a DBIO::Replicated::Storage -- the read handler, sending the EXPLAIN of a read query to the master's handle instead. That is exactly the set of regressions routing explain through this seam removed; owning the execution is what makes it possible to reintroduce them. A driver that needs a session setting or a second query issues each of them through "_execute_rendered" too; Firebird, which issues nothing at all, prepares through "_prepare_rendered" and keeps the same guarantees up to the execute that never happens (ADR 0040 section 6).

When the engine's plan is one opaque string rather than a table -- Firebird's $sth->func('ib_plan') is the case in this family -- the driver wraps it as a single row of a single column, [ [ $plan ] ], and does not invent columns to fill. That is a wrapping, not a fit, and it is fine: section 2 of ADR 0040 already says the contents of these rows are engine-specific and never were portable.

Unlike "_random_function" this hook is not static -- it connects and it executes. It is still on the driver-determination list at the top of this class, for the same reason as before: a never-connected storage must answer with the operation of the driver its DSN names. On DBIO::Replicated::Storage it is a reader, delegated to the read handler beside _select and "_execute_rendered", because it now performs the read rather than merely spelling it.

_exists_value_sql

my $stmt = $storage->_exists_value_sql($inner_sql);

Engine-specific rendering seam for "results_exist" in DBIO::ResultSet and its two siblings (ADR 0037). $inner_sql is the parenthesised subquery whose existence is being asked about; the return value is a complete statement that yields 1 or 0.

The base implementation is the standard-conforming spelling:

SELECT CASE WHEN EXISTS <inner> THEN 1 ELSE 0 END

which is correct on PostgreSQL, MySQL/MariaDB, SQLite, DuckDB, SQL Server and Sybase. Only the engines that also refuse a FROM-less SELECT override this hook, each appending its own dummy table -- Oracle FROM DUAL, DB2 FROM SYSIBM.SYSDUMMY1, Firebird FROM RDB$DATABASE. The driver renders the whole fragment: core never composes it from parts and holds no dummy-table knowledge.

Overriding the hook does not by itself enable the feature: the driver also declares the supports_exists_query capability (see DBIO::Storage::DBI::Capabilities), which is what "results_exist" in DBIO::ResultSet probes.

Static, like "_random_function": no $dbh, no version probe. It is on the driver-determination list at the top of this class, so a never-connected storage still answers with the rendering of the driver its DSN names.

_set_operation_keyword

my $keyword = $storage->_set_operation_keyword('except');

The keyword this engine spells one of the six SQL set operations with, for "union" in DBIO::ResultSet and its five siblings (ADR 0039; it replaces the single-operation _except_operator of ADR 0037 section 3). $op is the canonical operation name, which is the ResultSet method's own name: union, union_all, intersect, intersect_all, except, except_all. The base implementation answers with the ANSI spelling of each -- UNION, UNION ALL, INTERSECT, INTERSECT ALL, EXCEPT, EXCEPT ALL.

The hook has two jobs, deliberately in one place:

  • spell the operation for this engine. DBIO::Oracle::Storage answers MINUS for except.

  • refuse it, for an engine that does not have the operation at all -- by throwing, from the driver, with a message naming the engine and the version (via $self->throw_exception, F21 -- never a bare croak). Known gaps, as reported by the driver repos rather than verified here: Oracle has no MINUS ALL / EXCEPT ALL before 21c, and SQLite and SQL Server have no EXCEPT ALL / INTERSECT ALL.

Rendering and refusal stay together because the driver is the only place that knows both, and a refusal that has to name a version can only be written where that version is known. The shape of an override is therefore:

sub _set_operation_keyword {
  my ($self, $op) = @_;
  return $self->next::method($op) unless defined $op;   # let the base throw
  return 'MINUS' if $op eq 'except';
  $self->throw_exception("...") if $op eq 'except_all';
  return $self->next::method($op);
}

The defined guard on the first line is not decoration. The base below handles an undefined $op correctly -- that is the core programming error it throws for -- but it only ever sees one if the override passes it down, and an override that opens with $op eq 'except' emits an uninitialized-value warning before it gets there. Reported from the dbio-firebird rollout, where the example was copied literally, which is what an example is for.

An override handles the operations it has an opinion about and hands the rest to $self->next::method($op), so it inherits every base keyword it did not name -- and an unknown $op reaching the base is a core programming error, not an engine fact, so the base throws and lists the six valid names rather than letting a bare token through into the SQL.

There is no capability flag beside it, deliberately: there is nothing to branch on (no set operation has a portable fallback in the ORM, so a caller who learned "unsupported" would have no second call to make); the hook is already the refusal seam and the driver writes a better message than core could; and a gate defaulting to 0 would regress every engine that has these operations today. ADR 0037 section 3 gave a fourth reason -- a boolean cannot carry a keyword -- which applies to except but not to the ... ALL family, where availability really is a yes/no question; ADR 0039 section 3 records why a capability is still the wrong shape there.

Must be a static class fact: no $dbh, no version probe. That binds the refusal too, and every known gap is version-bounded (Oracle gained MINUS ALL in 21c), so a driver cannot ask which version it is talking to here. It refuses unconditionally and says so in the message, or reads a class-level opt-in a deployment sets when it knows its server -- ADR 0039 section 7. DBIO::ResultSet::_set_operation bakes the keyword into a literal from clause at construction time, so a runtime-resolved keyword is not expressible at all -- and asking the database which word to use is what made $rs->except($other)->as_query open a connection before this hook's predecessor existed. It is on the driver-determination list at the top of this class for the same reason "_exists_value_sql" is, and DBIO::Replicated::Storage delegates it to the master backend.

_execute_rendered

my ($rv, $sth) = $storage->_execute_rendered($sql, \@bind);

Run an already-rendered statement and its (already bind-attribute-resolved) binds. Everything _execute does below the SQL-generation step happens here too -- dbh_do retry over disconnects, _query_start / _query_end tracing and therefore "redact_bind_value" -- but no sql_maker op is invoked, because there is nothing left to generate.

One caller is "results_exist" in DBIO::ResultSet, whose statement comes out of "_exists_value_sql" as a complete statement (ADR 0037 section 2): its base form has no FROM at all, so it cannot be expressed as an sql_maker->select call, and wrapping it in one (SELECT * FROM ( ... ) AS ...) is exactly the derived-table decoration that ADR made illegal. The other is "_explain_query": its base implementation, and each statement a driver override executes, as that hook's contract requires (ADR 0040).

_prepare_rendered

my $sth = $storage->_prepare_rendered($sql, \@bind);

"_execute_rendered" stopped one verb short: the same already-rendered statement is reshaped, the connection is established, the handle is prepared and the binds are bound -- and then the live $sth is handed back without being executed. Nothing is sent to the engine beyond the prepare itself.

This exists because on some engines the answer to a question is a property of the prepared statement rather than the result of running it. Firebird's query plan is the case that forced it (isc_info_sql_get_plan, reached through DBD::Firebird's private statement-handle function once the statement is prepared), so its "_explain_query" reads:

my $sth  = $self->_prepare_rendered($sql, $bind);
my $plan = $sth->func('ib_plan');
return [ [ $plan ] ];

func and not $sth->{ib_plan}: ib_plan is a private XSUB of the statement handle in both DBD::Firebird and DBD::InterBase, and neither serves it as a fetchable attribute. Asking for the attribute does not throw -- DBI answers undef -- so the wrong spelling returns a plan row nothing can tell from a real one. Ask the DBD's own documentation which of the two forms a given value has before reading it off a handle here; that is the one thing this seam cannot check for a driver.

Without this seam that driver would have to reach for $self->dbh, which is exactly the bypass "_explain_query" forbids -- committed by the one engine that could not otherwise obey (ADR 0040 section 6).

What the seam still buys, all of it the same code "_execute_rendered" runs:

  • _query_start / _query_end tracing, and therefore the "redact_bind_value" hook (ADR 0025). The bind values of the query being prepared are the caller's real data and must not reach a trace sink raw.

  • dbh_do retry over a dropped connection -- and a prepare is a far better citizen there than an execute, because re-running it leaves nothing behind.

  • "_prep_rendered_for_execute", hence DBIO::Storage::DBI::NoBindVars interpolation: on an engine with no usable placeholders the statement is prepared with its values already substituted and an empty bind list, instead of arriving with its ? intact.

  • _dbi_attrs_for_bind plus _bind_sth_params -- resolved DBI bind attributes, bind_param_inout for scalar-ref binds, and the silent-failure check in _prepare_sth for a $dbh->prepare that returns false without raising.

  • on a DBIO::Replicated::Storage, the read handler rather than the master's handle: it is on @reader_methods beside "_execute_rendered", with the matching forwarder on DBIO::Replicated::Balancer.

What it does not buy, stated rather than assumed:

  • No execute. Attributes some DBDs only populate after execute (row counts, and on several engines $sth->{NAME} and friends) are not there yet. Only ask the handle for what your engine answers at prepare time.

  • The retry window closes when the handle is returned. dbh_do covers the prepare; whatever the caller then does with the $sth happens outside it. That is not new -- "_execute_rendered" hands back a $sth that is fetched from after dbh_do has returned in exactly the same way -- but a prepare-only caller does more with its handle, so it is worth saying.

  • The handle may come out of DBI's statement cache. _prepare_sth uses prepare_cached unless "disable_sth_caching" is set. Use the handle and let it go; do not hold it across other statements on the same connection.

  • The trace does not distinguish a prepare from an execute. The statement appears in debugcb exactly as an executed one would. It did go to the server; it did not run.

_prep_rendered_for_execute

my ($sql, $bind) = $self->_prep_rendered_for_execute($sql, $bind);

The pre-rendered counterpart of the post-generation half of _prep_for_execute: the last chance a driver has to reshape a statement core rendered itself, on the way into "_execute_rendered" or "_prepare_rendered". The identity by default; DBIO::Storage::DBI::NoBindVars overrides it to interpolate the bind values into the SQL, which is the whole reason that class exists -- without the seam, an engine with no usable placeholders would receive the one statement core does not build through sql_maker with its ? intact.

deploy_defaults

Returns a hash of default arguments merged into every $schema->deploy() call made by DBIO::Test. The base implementation returns an empty list; drivers override this to declare their requirements without hard-coding driver names in test infrastructure.

# DBIO::MySQL::Storage
sub deploy_defaults { return (add_drop_table => 1) }

deploy_setup

$storage->deploy_setup($schema);

Called by DBIO::Test immediately before $schema->deploy(). The default implementation is a no-op. Drivers override this to perform any one-time database-level setup that must happen before the schema is installed (for example, stripping incompatible sql_mode flags on MySQL).

on_connect_do

This method is deprecated in favour of setting via "connect_info".

on_disconnect_do

This method is deprecated in favour of setting via "connect_info".

dbh_do

Arguments: ($subref | $method_name), @extra_coderef_args?

Execute the given $subref or $method_name using the new exception-based connection management.

The first two arguments will be the storage object that dbh_do was called on and a database handle to use. Any additional arguments will be passed verbatim to the called subref as arguments 2 and onwards.

Using this (instead of $self->_dbh or $self->dbh) ensures correct exception handling and reconnection (or failover in future subclasses).

Your subref should have no side-effects outside of the database, as there is the potential for your subref to be partially double-executed if the database connection was stale/dysfunctional.

Example:

my @stuff = $schema->storage->dbh_do(
  sub {
    my ($storage, $dbh, @cols) = @_;
    my $cols = join(q{, }, @cols);
    $dbh->selectrow_array("SELECT $cols FROM foo");
  },
  @column_list
);

async

Returns the embedded async backend storage (a DBIO::Storage::Async) for an instance connected with an event-loop async mode ({ async => 'forked' }, 'future_io', 'ev', ...), or undef for a sync or immediate-mode instance. It is a thin public alias over the lazy _async_storage resolver that also feeds the six CRUD *_async methods, so it shares their backend discovery and caching. An unavailable mode, or a loaded non- DBIO::Storage::Async target other than literal immediate, croaks (see "register_async_mode" and ADR 0030). For future_io the backend class is resolved by convention: the concrete storage's linearised ISA is walked most-specific first for a <class>::Async adapter, stopping before the generic DBIO::Storage::DBI base, so extension storage_type subclasses (AGE, PostGIS) reach async CRUD through the nearest parent driver's adapter.

Use it to reach async API that the CRUD *_async methods do not cover -- listen/notify (core LISTEN/NOTIFY, ADR 0056, on every transport that declares it) and driver features such as PostgreSQL's pipeline and copy_in -- on the very same schema that serves your synchronous queries:

$schema->storage->async->listen(channel => sub {
  my ($channel, $payload, $sender_pid) = @_;
  ...
});

disconnect

Our disconnect method also performs a rollback first if the database is not in AutoCommit mode.

with_deferred_fk_checks

Arguments: $coderef
Return Value: The return value of $coderef

Storage specific method to run the code ref with FK checks deferred or in MySQL's case disabled entirely.

connected

Arguments: none
Return Value: 1|0

Verifies that the current database handle is active and ready to execute an SQL statement (e.g. the connection did not get stale, server is still answering, etc.) This method is used internally by "dbh".

dbh

Returns a $dbh - a data base handle of class DBI. The returned handle is guaranteed to be healthy by implicitly calling "connected", and if necessary performing a reconnection before returning. Keep in mind that this is very expensive on some database engines. Consider using "dbh_do" instead.

register_driver

Arguments: $dbd_driver_name, $storage_class
Return value: none

Registers a mapping from a DBI driver name (for example Pg, mysql, SQLite) to a DBIO storage class.

Called by driver distributions at load time. The registry says which driver a DBD belongs to, in two places: "driver_for" in DBIO answers the driver of a connection with it (the <Name> of the DBIO::<Name>::Storage class the DBD is registered for), and a declared storage is checked against the DBD of its DSN with it ("_adapt_to_access_path"). A driver that takes another driver's DBD as its own transport, without inheriting that driver's storage, says so with "register_access_path_dbd". The registry moves no storage into a class: a storage is its driver's from the start (ADR 0067).

Example:

__PACKAGE__->register_driver('Pg' => 'DBIO::PostgreSQL::Storage');

register_connector_driver

Retired (ADR 0067). It filled a registry from which a storage connected through ODBC was moved into the class of whatever engine the server named. A storage is never moved into another driver's class: a schema declares its driver, and a driver that runs over ODBC adapts itself to it ("_adapt_to_access_path"). Calling it throws.

connect_call_datetime_setup

A no-op stub method, provided so that one can always safely supply the connection option

on_connect_call => 'datetime_setup'

This way one does not need to know in advance whether the underlying storage requires any sort of hand-holding when dealing with calendar data.

connect_call_rebase_sqlmaker

This on-connect call takes as a single argument the name of a class to "rebase" the SQLMaker inheritance hierarchy upon. For this to work properly the target class MUST inherit from DBIO::SQLMaker::ClassicExtensions and either SQL::Abstract or SQL::Abstract::Classic as shown below.

This infrastructure is provided to aid recent activity around experimental new aproaches to SQL generation within DBIO. You can (and are encouraged to) mix and match old and new within the same codebase as follows:

package DBIO::Awesomer::SQLMaker;
# you MUST inherit in this order to get the composition right
# you are free to override-without-next::method any part you need
use base qw(
  DBIO::SQLMaker::ClassicExtensions
  << OPTIONAL::AWESOME::Class::Implementing::ExtraRainbowSauce >>
  SQL::Abstract
);
<< your new code goes here >>


... and then ...


my $experimental_schema = $original_schema->connect(
  sub {
    $original_schema->storage->dbh
  },
  {
    # the nested arrayref is important, as per
    # https://metacpan.org/pod/DBIO::Storage::DBI#on_connect_call
    on_connect_call => [ [ rebase_sqlmaker => 'DBIO::Awesomer::SQLMaker' ] ],
  },
);

redact_bind_value

Class-level accessor for a coderef that decides how each bind value appears in the trace output. The coderef is invoked as

$redactor->($column_name, $value, $bindattrs)

where $column_name is the column name attached to the bind slot by the SQL generator (or undef for binds that carry no column metadata, such as positional placeholders in literal SQL such as \[ 'name = ?', $value ]) and $bindattrs is the bind attribute hash of the slot (or undef). The coderef returns the value to interpolate into the trace string. The raw bind value sent to the database is not modified - only its representation in the trace sink is.

The default redactor shows the bind of every column that has encode_column (DBIO::EncodedColumn) as *** -- the bind attributes of such a column carry redact => 1 -- and every other value as it is. Install a custom redactor to scrub further credential or PII columns, e.g.:

DBIO::Storage::DBI->redact_bind_value(sub {
    my ($colname, $value) = @_;
    return defined $colname && $colname eq 'password' ? '***' : $value;
});

An undef bind value is rendered as NULL without the redactor being called. What the redactor returns is put between single quotes as it is -- DBIO does not validate it: return a defined string without line breaks (undef renders as an empty string and warns).

The redactor is asked for every bind value from trace level 2 on (DBIO_TRACE=2, debug(2)); level 1 traces the statement alone. What it returns is cut to 32 characters, with ... marking the cut.

The redactor is a single class-level slot; setting it on a subclass does not affect the parent or sibling classes. If you need per-instance behavior, install a closure that consults $self on the storage object (via a dispatcher closure captured at connect time).

select

Arguments: $ident, $select, $condition, $attrs

Handle a SQL select statement.

last_insert_id

Return the row id of the last insert. Returns the autoincrement values for the columns requested by the insert codepaths.

sqlt_type

Returns the database driver name.

bind_attribute_by_data_type

Given a datatype from column info, returns a database specific bind attribute for $dbh->bind_param($val,$attribute) or nothing if we will let the database planner just handle it.

This method is always called after the driver has been determined and a DBI connection has been established. Therefore you can refer to DBI::$constant and/or DBD::$driver::$constant directly, without worrying about loading the correct modules.

is_datatype_numeric

Given a datatype from column_info, returns a boolean value indicating if the current RDBMS considers it a numeric value. This controls how "set_column" in DBIO::Row decides whether to mark the column as dirty - when the datatype is deemed numeric a != comparison will be performed instead of the usual eq.

create_ddl_dir

Arguments: $schema, \@databases, $version, $directory, $preversion, \%sqlt_args

DEPRECATED: This method is deprecated and will throw an exception if called. Use the native Deploy class on your storage instead (see "deploy" in DBIO::Schema).

Historically this created SQL DDL files for each requested database engine, plus ALTER TABLE upgrade scripts between schema versions, via SQL::Translator. Core no longer generates SQL on the fly; the native Deploy classes (e.g. DBIO::PostgreSQL::Deploy) handle introspection, diff and deployment. The original signature is retained only for the deprecation throw.

deployment_statements

Arguments: $schema, $type, $version, $directory, $sqlt_args

Returns the SQL statements for deploying the schema.

If the DDL file "ddl_filename" in DBIO::Schema names for these arguments exists in the directory, reads and returns its contents. Otherwise, throws an exception. There is no on-the-fly SQL generation here, and core does not write that file either: "create_ddl_dir" only throws.

For deployments, use "deploy" in DBIO::Schema instead, which routes to the storage's native Deploy class if available.

deploy

Arguments: $schema, $type, \%sqlt_args, $directory

Installs the schema. A storage that names a native Deploy class through dbio_deploy_class hands the schema to it (->new(schema => $schema, %args)->install); %args come from \%sqlt_args ("deploy_args" in DBIO::Deploy::Base): the option add_drop_table drops the declared views and tables before the install, every other key, version included, throws; $type and $directory are ignored on this route; this is the one place every driver, and DBIO::Replicated::Storage through its master, takes that route, and "deploy" in DBIO::Schema ends here the same way. A DDL file in the directory is not executed in its place. The driver storage owns dbio_deploy_class; a storage layer does not define it ("The Deploy class belongs to the driver" in DBIO::Storage::Composed).

A storage without a native Deploy class runs the statements of an existing DDL file (see "deployment_statements") and throws when there is none. The native route needs a DBIO::Schema; any other first argument takes the DDL-file route.

datetime_parser

Returns the datetime parser class

datetime_parser_type

Defines the datetime parser class - currently defaults to DateTime::Format::MySQL

build_datetime_parser

See "datetime_parser"

is_replicating

A boolean that reports if a particular DBIO::Storage::DBI is set to replicate from a master database. Default is undef, which is the result returned by databases that don't support replication.

lag_behind_master

Returns a number that represents a certain amount of lag behind a master db when a given storage is replicating. The number is database dependent, but starts at zero and increases with the amount of lag. Default in undef

relname_to_table_alias

Arguments: $relname, $join_count
Return Value: $alias

DBIO uses DBIO::Relationship names as table aliases in queries.

This hook is to allow specific DBIO::Storage drivers to change the way these aliases are named.

The default behavior is "$relname_$join_count" if $join_count > 1, otherwise "$relname".

USAGE NOTES

DBIO and AutoCommit

DBIO can do some wonderful magic with handling exceptions, disconnections, and transactions when you use AutoCommit => 1 (the default) combined with txn_do for transaction support.

If you set AutoCommit => 0 in your connect info, then you are always in an assumed transaction between commits, and you're telling us you'd like to manage that manually. A lot of the magic protections offered by this module will go away. We can't protect you from exceptions due to database disconnects because we don't know anything about how to restart your transactions. You're on your own for handling all sorts of exceptional cases if you choose the AutoCommit => 0 path, just as you would be with raw DBI.

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.