NAME
DBIO::MySQL::Storage::Async - future_io async MySQL transport over the DBD driver's native async binding
VERSION
version 0.900004
DESCRIPTION
The concrete future_io transport adapter for MySQL / MariaDB (core ADR 0030 / 0031), the MySQL analogue of DBIO::PostgreSQL::Storage::Async. It is resolved by convention off the sync driver storage: a connection opened with
MyApp::Schema->connect($dsn, $user, $pass, { async => 'future_io' });
on a DBIO::MySQL::Storage instance derives ref($storage) . '::Async' == DBIO::MySQL::Storage::Async. No register_async_mode call is needed; the name itself is the registration.
Storage extensions ride on top by composition
Under the storage-layer composition model, a MySQL / MariaDB driver extension is not a storage_type subclass and does not ship a shadow register_async_mode / ::Storage::Async of its own. It ships a plain storage layer registered with $schema->register_storage_layer('DBIO::MySQL::Ext::Storage') and, for async behaviour, a sibling async mirror DBIO::MySQL::Ext::Storage::Async (a plain method package, not a transport). When a layered schema connects { async => 'future_io' }, core resolves this transport by convention off the composition base (the driver, not the layers) and then DBIO::Storage::Composed->composes each registered layer's async mirror on top of it. So this class stays the single per-driver future_io transport BASE; extensions add behaviour above it via C3, exactly as their sync layers ride the sync driver storage. MySQL ships no such extension today, but this reference driver honours the mechanism (the offline structural proof is t/56-storage-layer-composition.t, the live one t/57-*-live.t).
One dist, two DBD drivers
MySQL is served by two DBI drivers, and DBIO models them as two storage classes: DBIO::MySQL::Storage for a dbi:mysql: DSN (DBD::mysql) and DBIO::MySQL::Storage::MariaDB for a dbi:MariaDB: DSN (DBD::MariaDB). The user picks the DBD by the DSN, so the async transport must drive that DBD's binding, not a fixed one -- the two are really two drivers that happen to ship in one dist.
Both bindings share the same shape -- a single async query per connection plus a socket fd for the event loop -- but differ only in the attribute / method names:
DBD::mysql async mysql_fd mysql_async_ready
mysql_async_result mysql_insertid
DBD::MariaDB mariadb_async mariadb_sockfd mariadb_async_ready
mariadb_async_result mariadb_insertid
Which of the two a connection gets is decided per connection, by its DBD (this dist's ADR 0007 with its 2026-10-04 addendum), never by the class of this transport:
Before the connect there is no handle, so the DBD is the one the connection's DSN names ("_mysql_auto_reconnect_attr"). That DSN is the one this very connection is opened with: with an AccessBroker it is the broker's answer for this connection, which may differ from the last one's.
From then on it is the handle's own
$dbh->{Driver}{Name}, read once and kept on the connection.
The class cannot decide it. The class is found by convention off the sync storage class, and that is pinned by the schema component for every DSN: load_components('MySQL') over a dbi:MariaDB: DSN runs DBIO::MySQL::Storage and so this class, on DBD::MariaDB handles; the MariaDB component pins its classes for a dbi:mysql: DSN the same way. The names themselves come from the one table the sync storage reads too ("_dbd_attribute" in DBIO::MySQL::Storage), which asks the handle for the same reason. A DBD the table does not know is refused, before the connect, naming it.
So this class carries all of the transport, both bindings included, and its convention subclass DBIO::MySQL::Storage::MariaDB::Async adds nothing: it exists as the name the convention resolves for the MariaDB storage class. Neither DBD is loaded at compile time here; DBI->connect pulls the one the DSN names.
The Model-B orchestration -- the CRUD runner ("_run_crud" in DBIO::Storage::Async with its pooled / pinned runners), INSERT returned-columns mapping, "txn_do_async" in DBIO::Storage::Async bracketing, the pipeline scaffold, and the sync ->get fallbacks -- is inherited unchanged from DBIO::Storage::Async. The Future::IO transport (query execution over the socket-readable watcher, the Future class and the DBIO::Async::Pool) is inherited from DBIO::Async::Storage. This class fills only the DB-specific transport seams over the driver's asynchronous binding:
"sql_maker_class" / "_sql_maker_args" -- the MySQL SQLMaker
"_transform_sql" -- identity (MySQL keeps
?placeholders)"_post_insert_sql" -- empty (MySQL has no
RETURNING)"_insert_returned_columns" -- assemble the returned-columns hashref from the auto-increment id (there is no
RETURNING), the key divergence from PostgreSQL"_normalize_conninfo" -- DBI-form connect info into the pool's shape
"_create_pool_connection" / "_shutdown_pool_connection" -- DBD handle lifecycle
"_conn_ready" / "_conn_fileno" -- readiness + the socket fd for the Future::IO watcher
"_submit_query" / "_collect_result" -- send non-blocking, then gather once the wire is readable
"_conn_error_is_fatal" -- whether a failed statement took its connection with it, so the pool retires it
"_idle_connection_alive" -- whether an idle pool connection's session is still there, asked before the pool hands it out
"_async_prepare_attrs" / "_conn_socket_fd" / "_async_ready" / "_async_result" / "_async_insertid" -- the five DBD-specific async primitives the seams above call, each answering for the DBD of the connection it is handed
"_ddl_class" -- the install-DDL generator (DBIO::MySQL::DDL) core's generic
deploy_asyncresolves, so$storage->deploy_async($schema)runs the same DDL the sync deploy pipeline emits
The transaction-context seams are not among them: DBIO::Async::Storage answers _txn_context_class / _txn_conn_accessor with the future_io defaults this transport wants anyway (DBIO::Async::TransactionContext under the txn_conn constructor key), so repeating them here would only be a second place for the two to drift.
A connection is represented as a small hashref { dbh => $dbh, sth => $sth }: the driver's database handle plus the statement handle of the in-flight async query (there is at most one per connection at a time -- MySQL allows a single async query per connection -- guaranteed by the pool / transaction pinning).
For LISTEN/NOTIFY this class fills the listen seams with an emulation over a notify table (see "LISTEN / NOTIFY (emulated)"):
"_listen_open" / "_listen_close" -- the dedicated listen connection, carrying its DBIO::MySQL::NotifyTable state
"_listen_subscribe" / "_listen_unsubscribe" -- take a channel's mark on the listen connection / forget the channel
"_listen_poll_interval" -- the timer mode of the dbio-async watch loop
"_listen_drain" -- one poll of the notify table, never blocking
"_listen_probe" -- the liveness ping
"_notify_sql" -- the notify
INSERT"_mysql_auto_reconnect_attr" -- a sixth DBD-specific primitive, the auto-reconnect attribute the pool and the listen connection switch off, named for the DBD of the DSN being connected
Transport capabilities
"transport_capabilities" declares what DBIO::Async::Storage declares -- on_connect_replay: the pool replays the owning sync storage's on_connect_do / on_connect_call on every freshly-spawned connection -- plus listen and notify, which MySQL and MariaDB get emulated ("LISTEN / NOTIFY (emulated)"), under the capability names every transport uses for the same contract, and broker_connect_options: the connect options given beside an AccessBroker reach every connection this transport opens (ADR 0030 addendum II).
my $schema = MyApp::Schema->connect($broker, {
async => 'future_io',
pool_size => 3,
mysql_enable_utf8mb4 => 1, # mariadb_* names on a dbi:MariaDB: DSN
on_connect_do => [ q{SET time_zone = '+00:00'} ],
});
Of that hash the owning sync storage applies its own part -- the mode, the storage and SQL maker options, the connection actions it replays on every pooled connection. The rest is the transport's, and without the declaration core refuses the connect at the first async use rather than let the pooled connections run without settings the owner's connection has:
pool_sizesizes the pool ("connect_info" in DBIO::Storage::Async); it never reachesDBI->connect.The DBD attributes are merged into the attributes of every broker answer -- each pool spawn and each open of the listen connection -- by "_async_broker_conninfo" in DBIO::Async::Storage, which this transport inherits, and handed to
DBI->connectby "_create_pool_connection" and "_listen_open". A setting given both beside the broker and in its answer must have the same value; a different one is refused, naming the setting -- by the owning storage atconnect, and again for each of these connections, since the broker answers anew for every one. The DBD's auto-reconnect given there is the caller's choice on pool connections as it is in any other connect attributes ("_create_pool_connection"), and forced off on the listen connection.A broker that answers this transport's own shape -- a hashref with a
dsn, or a[ \%conninfo, \%opts ]pair -- cannot take DBD attributes from beside it: that connection throws, naming them, instead of being opened without them. Give the attributes in the answer'sattrsthen.
MySQL and MariaDB have no COPY (a PostgreSQL wire feature), so there is nothing of that kind to carry here. Pipelining ("pipeline" in DBIO::Storage::Async) is not implemented on this future_io transport either -- neither DBD driver's async binding exposes a batch/pipeline mode -- so pipeline returns a failed Future naming this class ("_pipeline_refusal" in DBIO::Storage::Async), before it touches the pool. The native ev backend (DBIO::MySQL::EV::Storage, dist dbio-mysql-ev, over EV::MariaDB) declines pipelining too: EV::MariaDB batches consecutive queries on its own and has no mode to bracket. An async storage layer that declares required_transport_capabilities for a feature this transport does not advertise will therefore fail loud (core capability gate) when composed over this future_io transport, naming the missing capability -- never a silent feature loss.
LISTEN / NOTIFY (emulated)
listen, unlisten, notify, listen_alive and listen_reconnect are core async API (core ADR 0056), the same contract on every async transport that declares listen / notify, documented once in "LISTEN / NOTIFY" in DBIO::Storage::Async. Core owns the channel registry, the one dedicated listen connection, reconnect with backoff, subscribing every channel again and the fork guard; DBIO::Async::Storage owns the Future::IO watch loop, the timer and the pooled notify (dbio-async ADR 0005).
MySQL and MariaDB have no LISTEN/NOTIFY, so this transport emulates it with a notify table and polling, through DBIO::MySQL::NotifyTable -- the part it shares with the ev transport of dbio-mysql-ev. notify inserts a row; the listen connection polls the table on a timer for rows new to it. The design and its reasons are this dist's ADR 0008.
my $schema = MyApp::Schema->connect($dsn, $user, $pass, { async => 'future_io' });
my $async = $schema->storage->async;
$async->deploy_notify_table->get; # once -- never created implicitly
$async->on_listen_error(sub {
my ($storage, $error) = @_; # every error; without a hook it is warned
$log->warn("listen: $error");
});
$async->on_listen_reconnect(sub {
my ($storage) = @_; # rebuilt, every channel subscribed again
$log->info('listen connection rebuilt -- re-read what may have been missed');
});
$async->listen(jobs => sub {
my ($channel, $payload, $sender_pid) = @_; # $sender_pid: CONNECTION_ID()
...
})->get; # resolves once the mark is taken
$async->notify(jobs => 'wake up')->get; # an INSERT on a pooled connection
$async->unlisten('jobs')->get;
What the emulation delivers, and where it differs from a server push:
Latency is the poll interval ("notify_poll_interval", default 1 second): a notification arrives with the first poll after it became visible.
Delivery on commit. A notification becomes visible when the transaction that inserted it commits -- PostgreSQL's moment too.
notifyruns itsINSERTon a pooled connection in autocommit, so it is visible at once; to notify transactionally, run "notify_sql" in DBIO::MySQL::NotifyTable inside your own transaction.No history, no replay.
listentakes a mark before its Future resolves, solisten(...)->get; notify(...)->getdelivers and nothing committed earlier does. A notification whose transaction was still open at that moment is delivered when it commits. Each listen connection takes new marks, so a reconnect -- like PostgreSQL's -- loses what was committed while the connection was down;on_listen_reconnectis where to re-read state.Out-of-order commits are caught, within the lookback. AUTO_INCREMENT ids are handed out at insert, not at commit; each poll re-reads a window of "notify_lookback" seconds (default 60) and delivers what it had not seen, each notification once. The limit: a notification is lost when its transaction commits more than the lookback after its
INSERTand a later notification on the same channel was delivered meanwhile (details in "How the out-of-order commit is caught" in DBIO::MySQL::NotifyTable).Retention. The listen connection deletes rows older than "notify_retention" seconds (default 3600) -- one bounded
DELETE ... LIMITat most once a minute, back to back only while it clears a backlog. The listening account needsSELECTandDELETEon the table, a notifying oneINSERT. A cleanup that fails is reported throughon_listen_errorand tried again a minute later; it does not end listening.notify_retention(0)turns it off for a deployment that prunes the table itself.The table is "THE TABLE" in DBIO::MySQL::NotifyTable, named by "notify_table" (default
dbio_notify, a plain identifier). It is never created implicitly: deploy it with "deploy_notify_table", or run "create_table_sql" in DBIO::MySQL::NotifyTable from a migration. Until it exists,listenfails with a message saying so, andnotifywith the server's.Channels and payloads. A channel is matched exactly and case-sensitively, at most 255 characters -- a longer one is refused, never truncated, whatever the
sql_mode; the payload isTEXT, at most 65535 bytes in UTF-8, refused beyond. Anundefpayload arrives as''. Both only ever travel as bind values. Non-ASCII channel names need a Unicode-aware connection (DBD::MariaDB always; DBD::mysql withmysql_enable_utf8mb4).The listen connection. A dedicated DBD handle outside the pool, opened from the connect info a pool spawn would get (fresh AccessBroker credentials included), with
AutoInactiveDestroy => 1and the driver's auto-reconnect forced off, since a silently rebuilt session would hide the death core must see. It is not given the pool'son_connect_do/on_connect_callreplay. Its polls are non-blocking; its connect, at the firstlistenand at every reconnect, is a blockingDBI->connectlike every pool connection's, as is the ping behindlisten_alive. One statement runs on it at a time: alistenarriving while a poll is in flight waits for that poll, andlisten_alivereports a poll in flight as alive without touching the connection -- the poll itself reports a dead one.Fork. Core never touches the listen connection or the pool outside the process that opened them, and every connection of this transport, pooled or listening, is opened with
AutoInactiveDestroy => 1("_create_pool_connection"). So a forked child that drops its copies -- by disconnecting the storage it inherited -- leaves the parent's sessions intact, and with DBD::mysql so does a child that simply exits. DBD::MariaDB (1.24) would not: DBI'sENDin the exiting child callsdisconnect_all, which DBD::MariaDB carries out on every connection the child inherited,AutoInactiveDestroynotwithstanding, or dies in a DBIactive kidspanic (exit status 22). So the first connect over DBD::MariaDB installs DBIO::MySQL::MariaDBForkGuard, which makes that call a no-op in every process but the installing one: the parent's listen and pool connections survive the child's exit, with no rebuild (this dist's ADR 0009). It covers every DBD::MariaDB connection of the process, not only this transport's, and it must come before the fork. A child that ends withPOSIX::_exitruns noENDeither way.
The same IO::Async::Loop advice as for the PostgreSQL transport applies: with Future::IO::Impl::IOAsync, build the program's loop through IO::Async::Loop->new (choosing an implementation with IO_ASYNC_LOOP), so the listen timer runs on the loop the program runs.
ATTRIBUTES
notify_table
The notify table's name, a plain identifier (validated by "new" in DBIO::MySQL::NotifyTable). Default dbio_notify.
notify_poll_interval
Seconds between two polls of the listen connection: the latency of a notification. Default 1.
notify_lookback
Seconds a notification's transaction may stay open between its INSERT and its COMMIT and still be delivered after a later notification on the same channel ("LISTEN / NOTIFY (emulated)"). Default 60.
notify_retention
Seconds a row is kept before the listen connection's cleanup deletes it; 0 turns the cleanup off. Must exceed "notify_lookback" plus "notify_poll_interval". Default 3600.
All four are inherited class data seeded from "defaults" in DBIO::MySQL::NotifyTable: set them on the class for every connection, or on the async backend for one. They are read when the listen connection is opened and whenever a statement is built, so a change applies from the next listen connection ("listen_reconnect" in DBIO::Storage::Async makes one).
METHODS
transport_capabilities
my @caps = DBIO::MySQL::Storage::Async->transport_capabilities;
# ('on_connect_replay', 'listen', 'notify', 'broker_connect_options')
Class method ("transport_capabilities" in DBIO::Storage::Async): the inherited list plus listen and notify, the capability names core's LISTEN/NOTIFY gate asks for (core ADR 0056 section 2) -- declared for an emulation, which core ADR 0056 section 6 allows when the POD states latency and on-commit delivery, as "LISTEN / NOTIFY (emulated)" does -- and broker_connect_options, which core reads before it hands this transport the connect options given beside an AccessBroker ("Transport capabilities"). Inherited unchanged by DBIO::MySQL::Storage::MariaDB::Async.
sql_maker_class
The DBIO::MySQL::SQLMaker subclass used to generate SQL.
_sql_maker_args
MySQL SQLMaker construction args: backtick identifier quoting and . name separator. The MySQL LIMIT offset, rows dialect is built into "apply_limit" in DBIO::MySQL::SQLMaker, so no limit_dialect is passed. Matches the sync driver and the ev backend.
_run_crud
my $future = $storage->_run_crud($op, $runner, @args);
Core's CRUD builder ("_run_crud" in DBIO::Storage::Async) with the type of each bind kept. Core hands the runner bare values, so the column a value is bound for is gone by the time the statement is sent -- and a value of a BLOB or BINARY column must be bound as SQL_BINARY, which the synchronous storage knows from the column ("bind_attribute_by_data_type" in DBIO::MySQL::Storage). This builds the statement with a maker that names the column of each bind, resolves its type from $args[0] (the result source) the way the synchronous storage does, and hands the runner the values: bare, except those of a binary column, which are DBIO::MySQL::Storage::Async::Binary objects that stringify as the value. A bind whose column cannot be resolved (a literal, an expression) stays a bare value.
_transform_sql
Identity: MySQL uses standard ? placeholders, so the maker's output is sent to the DBD driver unchanged (unlike PostgreSQL's ?>$N rewrite).
_post_insert_sql
Empty string: MySQL has no RETURNING clause, so nothing is appended to an INSERT. The returned-columns hashref is assembled from the auto-increment id instead (see "_insert_returned_columns").
_insert_returned_columns
$hashref = $storage->_insert_returned_columns($source, \%to_insert, $affected);
Assemble the returned-columns hashref (ADR 0031 §3) from the auto-increment id MySQL generated for the INSERT. MySQL has no RETURNING, so the inherited runner hands this method the INSERT's affected-row count ($affected, ignored here); the id itself is the statement's own insertid ("_async_insertid"), which "_collect_result" left on $self->{_statement_insert_id} at the moment the INSERT finalised. The hashref is { %$to_insert, $col => $id }, where $col is the source's first is_auto_increment column (falling back to the first PK column whose value was not supplied, matching the sync "insert" in DBIO::Storage::DBI heuristic). When the statement generated no id (the table has no auto-increment column, say) the supplied data is returned untouched -- whatever an earlier insert on this storage generated. Both DBDs report 0 for such a statement; for one that supplies the AUTO_INCREMENT column itself they report the supplied value, so the hashref carries the key the caller gave (both measured, see below). Overrides the inherited RETURNING-row default.
The id is not read from the slot behind "last_insert_id", which keeps the last id a statement generated. An insert that generated none would take the previous insert's: into the key it had not supplied, or -- on a column flagged is_auto_increment -- over the key it had. The Row built from that hashref would name another row, and its next update_async or delete_async would go there.
The id reaches this method through the storage object, not through its arguments: core hands over the first value the statement resolved with and nothing of the connection it ran on. That holds because nothing is collected in between. This method runs in the callback chain of the INSERT's own result Future, which "_collect_result" settles right after it stored the id; a statement that was waiting for the connection may be sent in between, but no other statement's result is read before this method has run -- as long as waiting for a socket ("_await_readable" in DBIO::Async::Storage) hands back a pending Future, which it does under Future::IO's built-in poll and under Future::IO::Impl::IOAsync, the two implementations this was run under. So inserts that overlap on a pool of several connections each resolve with their own id, in whatever order their results arrive, and so do the inserts of a txn_do_async, which run one after the other on its connection.
Exercised offline over a fake DBD, on the built-in poll (t/70-future-io-insert-id-per-statement.t), and measured against servers on Future::IO::Impl::IOAsync (2026-10-05, t/78-future-io-insert-id-live.t; MySQL 8.0.30 and MariaDB 11.8.8, DBD::mysql 4.055 and DBD::MariaDB 1.24). Six inserts started at once on a pool of two finished in another order than they were sent, with the server handing out the ids in another order too, and two of them generating none; each resolved with what its own statement had generated, checked by reading every row back under the id its insert resolved with. The same held for the unchained inserts of a transaction while pooled inserts ran beside it on the other connection. A pool of more than two connections, and another Future::IO implementation, were not run.
last_insert_id
my $id = $storage->last_insert_id;
The last auto-increment value a statement on this storage generated, captured from the driver's per-statement insertid ("_async_insertid"). Valid after an insert_async / insert succeeds; provided for legacy callers (scripts, tests). A statement that generated no id leaves it as it was, so it is not "the id of the last insert": after an insert that generated none it still names an earlier one, and with inserts overlapping on several pool connections it names whichever finalised last. "Generated" is the DBD's word for it: an insert that supplies the AUTO_INCREMENT column itself reports the supplied value, and that is then what this returns (measured, both DBDs). The CRUD path does not read it -- it takes the id of its own statement ("_insert_returned_columns").
_normalize_conninfo
my $info = $storage->_normalize_conninfo(
[ 'dbi:mysql:...', $user, $pass, \%attrs, \%extra_attrs ],
);
Convert the sync storage's DBI-form connect info into the [ \%conninfo, \%opts ] pair the pool consumes, carrying the DSN / user / password / attrs straight through for "_create_pool_connection" to hand to DBI->connect.
The attributes are both hashes of the DBI form -- the fourth element and the fifth, \%extra_attrs -- merged with the fifth winning, exactly as the owning sync storage merges them ("connect_info" in DBIO::Storage::DBI), so a pooled connection is opened with the values the owner's own connection has. Either may be missing. The DBIO-private keys (async, the storage and SQL maker options) are gone from both before this seam runs: core strips them ("connect_info" in DBIO::Storage::Async).
The single-hash form [ { dsn => ..., user => ..., password => ..., mysql_connect_timeout => 7 } ] is read as the owning sync storage reads it: every key but dsn, user, password and pool_size is a DBD attribute, so the pooled and listen connections are opened with the flat attributes the owner has. A nested attrs hash is read too and wins over a flat key of the same name. A hash with a dbh_maker, or the transport's own [ \%conninfo, \%opts ] pair (it has a second element), is not reshaped.
A pool_size among the merged attributes is lifted into the conninfo hash (the inherited normaliser strips it back out to size the pool), so it never reaches the DBD. Broker-style or already-normalised info is passed through untouched.
_create_pool_connection
my $conn = $storage->_create_pool_connection(\%conninfo);
Open one DBD handle via DBI->connect and wrap it as { dbh => $dbh }. AutoCommit is on: the inherited orchestration drives transactions with explicit BEGIN / COMMIT / ROLLBACK on the pinned connection.
Two attributes make the handle safe to hold:
AutoInactiveDestroy => 1, always: a forked child that drops its copy of the handle -- by disconnecting the storage it inherited, or at its exit -- does not end the parent's session. The pool itself never tears connections down outside the process that opened them ("shutdown" in DBIO::Storage::PoolBase); the handle's ownDESTROYis this transport's half. With DBD::MariaDB a child that runs DBI'sENDstill closes them (the Fork caveat under "LISTEN / NOTIFY (emulated)").The DBD's auto-reconnect ("_mysql_auto_reconnect_attr") off, unless the connect attributes set it: a session silently rebuilt under a pinned transaction loses that transaction without an error, and DBD::mysql switches
mysql_auto_reconnecton by itself underMOD_PERLorGATEWAY_INTERFACE. The same default as the sync storage, and like there an explicit value is the caller's informed choice (this dist's ADR 0005). The listen connection forces it off even then ("_listen_open").
A connect that fails is reported without its password: the message goes through core's DBIO::Util::mask_connect_secrets with the connect info it is about, as core's own connect does (DBIO::Storage::DBI). Otherwise it is DBI's message -- DBI connect('...','user',...) failed: and the DBD's reason, the DSN as given, a password= in it included -- or, when DBI->connect answered false because a HandleError among the connect attributes took the error, DBI connect failed: and the DBD's reason. The listen connection ("_listen_open") is opened the same way, so the same holds for what its failed connect hands the listen Future and on_listen_error, on every reconnect attempt again.
_shutdown_pool_connection
Disconnect the DBD handle held by $conn.
A connection known to be dead ("_conn_error_is_fatal", read from memory, no round trip) is disconnected quietly, as "_listen_close" does the listen connection: the handle's Warn is switched off, since DBD::mysql reports the statement handle a lost connection leaves active ("disconnect invalidates 1 active statement handle"), and the last statement handle is silenced (RaiseError and PrintError off), since its DESTROY would only report the loss again. A healthy connection is disconnected as it was, warnings untouched.
_async_prepare_attrs
my $attrs = $storage->_async_prepare_attrs($conn);
The prepare-time attributes that arm a non-blocking query on $conn: { async => 1 } for DBD::mysql, { mariadb_async => 1 } for DBD::MariaDB. execute then returns immediately.
_conn_socket_fd
my $fd = $storage->_conn_socket_fd($conn);
The connection socket file descriptor for the Future::IO readable watcher -- $dbh->mysql_fd for DBD::mysql, $dbh->mariadb_sockfd for DBD::MariaDB (the MySQL analogue of DBD::Pg's pg_socket).
_async_ready
my $bool = $storage->_async_ready($conn);
True once "_async_result" will not block: mysql_async_ready or mariadb_async_ready on the connection's statement in flight.
_async_result
my $rv = $storage->_async_result($conn);
Finalise the connection's statement in flight and return what execute would have -- the affected-row count (mysql_async_result or mariadb_async_result). Dies (RaiseError) on a server-side error.
_async_insertid
my $id = $storage->_async_insertid($conn);
The per-statement AUTO_INCREMENT id generated by the finalised INSERT, read from $sth->{mysql_insertid} or $sth->{mariadb_insertid} -- the per-sth form DBD::mysql recommends over the shared-handle $dbh->{mysql_insertid}. Zero / undef for non-INSERTs.
_mysql_auto_reconnect_attr
my $attr = $storage->_mysql_auto_reconnect_attr($dsn);
# 'mysql_auto_reconnect' for dbi:mysql:..., 'mariadb_auto_reconnect' for
# dbi:MariaDB:...
The name of the auto-reconnect attribute of the DBD that $dsn names. Pool connections are opened with it switched off unless the connect attributes set it ("_create_pool_connection"), the listen connection always ("_listen_open"). DBD::mysql turns mysql_auto_reconnect on by itself when GATEWAY_INTERFACE or MOD_PERL is set; DBD::MariaDB leaves mariadb_auto_reconnect off, and the transport says so explicitly.
The sixth DBD-specific primitive, and the one that is needed before a handle exists: it goes into the connect attributes. So it reads the DBD off the DSN, the way DBI and DBIO::Storage::DBI do -- the driver part of dbi:DRIVER:. A DSN that names none is refused (core refuses it; DBI_DRIVER does not stand in). The name is not spelled here: it comes from the table the sync storage reads too ("_dbd_attribute" in DBIO::MySQL::Storage), which throws for a DBD it has no names for.
_conn_ready
True once the connection can accept queries. DBI->connect is a blocking connect, so a handed-out connection is ready immediately -- we only confirm the handle is live.
_conn_fileno
The connection socket fd for the Future::IO readable watcher, obtained from the DBD-specific "_conn_socket_fd" primitive. The transport base ("_await_readable" in DBIO::Async::Storage) dups this integer fd into a filehandle for Future::IO->poll.
_submit_query
$storage->_submit_query($conn, $sql, $bind);
Send $sql non-blocking via the DBD driver's async binding: prepare with "_async_prepare_attrs" and execute the bind values. execute returns immediately; the result is gathered later by "_collect_result". The statement handle is stashed on $conn so the collector can finalise and fetch it.
The previous (already-collected) async statement handle is released before starting the new async query. MySQL allows a single async query per connection; clearing the spent sth first means the new execute owns the connection's async slot uncontested (mirrors the DBD::Pg adapter's fix).
On a connection already known to be dead ("_conn_error_is_fatal", from memory -- the ROLLBACK core sends after a lost connection) the spent statement handle is silenced before it is dropped, and so is the new one when its execute fails on a connection that turns out dead: their DESTROY could only report the loss as (in cleanup) ... st DESTROY failed. A live connection is never touched; the healthy path pays nothing for this.
_bind_and_execute
$storage->_bind_and_execute($dbh, $sth, \@bind);
Bind the values of \@bind and execute: the way a value reaches the server is the synchronous storage's. A value that is a DBIO::MySQL::Storage::Async::Binary (a value of a binary column, see "_run_crud") is bound as SQL_BINARY, every other value goes through "text_binds" in DBIO::MySQL::Storage -- under DBD::mysql with the utf8 attribute a string in Perl's 8-bit form is bound as characters.
_collect_result
my $future = $storage->_collect_result($conn);
Called once the socket is readable. The seam takes the connection and nothing else (ADR 0044): the bind values were serialised onto the wire by "_submit_query" at dispatch, so the re-poll recursion below carries $conn alone and not the caller's bind arrayref. What it needs to take the values out of an error text is on $conn: plain copies of the values and the statement, which "_submit_query" leaves there and this method removes when the result is finalised, whether that succeeded or not. An error the DBD raises while the result is read passes through "_mask_error_values" before it is rethrown.
If the async result has not fully arrived ("_async_ready" is false) it waits for the socket again ("_await_readable" in DBIO::Async::Storage) and re-checks -- so a large result spanning several reads is handled correctly. Once ready it finalises with "_async_result" and resolves:
a query that produced a result set (SELECT) -> the list of raw row arrayrefs, exactly the shape the sync cursor's
->allyields (ADR 0031 §3);a statement with no result set (INSERT / UPDATE / DELETE, BEGIN / COMMIT / ...) -> the affected-row count from "_async_result".
It also takes the statement's insertid ("_async_insertid") off the handle, to two places. $self->{_statement_insert_id} gets it for every statement, zero included, so that "_insert_returned_columns" folds into the returned-columns hashref the id of the INSERT that just finalised and never an earlier one's (MySQL has no RETURNING). The slot behind "last_insert_id", $self->{_last_insert_id}, gets it only when non-zero, so a statement that generated no id does not clear the last one that was.
The full row list is carried in an explicit future_class->done(@rows) so it survives the surrounding ->then chain (a bare list return would collapse to its last element).
_drain_more_results
$storage->_drain_more_results($conn);
Reads and discards the result sets that follow the first one of the statement on $conn->{sth} (more_results), so the pool connection stays usable: with DBD::MariaDB 1.24 a statement that left one unread made the library refuse every statement after it on that connection (2000, Calling a synchronous function on an asynchronous handle). A statement with a single result set asks more_results once, which answers from what the client already knows and sends nothing, so it cannot block.
When reading them fails the connection's state is unknown. The connection is flagged on $conn, the error is rethrown, and the failed Future makes the base ask "_conn_error_is_fatal", which answers true for a flagged connection: the pool retires it.
_mask_error_values
my $text = $storage->_mask_error_values($text, \@values, $statement);
The seam of the synchronous storage ("_mask_error_values" in DBIO::MySQL::Storage) for the transport: the text an engine writes into an error with the offending value taken out, *** in its place, and the type, the column and the key left. \@values are the values bound to the statement as plain values, $statement the statement in placeholder form or undef. It never throws; a text it cannot treat is answered unchanged, and anything that is no text (an exception object) as it is.
Duplicate entry 'geheim42' for key 'users.PRIMARY'
# becomes
Duplicate entry '***' for key 'users.PRIMARY'
Every error the DBD raises while a statement is sent ("_submit_query") or collected ("_collect_result", with the reading of further result sets) passes through it before it reaches the failed Future. The statements of the listen emulation travel the same two ways.
_conn_error_is_fatal
my $dead = $storage->_conn_error_is_fatal($conn, $error);
Driver seam of "_conn_error_is_fatal" in DBIO::Async::Storage: did the statement that just failed on $conn take the connection with it? The base asks for every failed query Future of "_query_async" in DBIO::Async::Storage and "_query_async_pinned" in DBIO::Async::Storage -- whether "_submit_query" died sending the statement or "_collect_result" died reading its result -- and on a true answer reports $conn to the pool ("_mark_connection_dead" in DBIO::Storage::PoolBase) before the connection is handed back, so the release retires it instead of putting it back. The pool cannot see a connection die by itself: without the report a dead handle goes back on the idle list, and every caller it is handed to fails the same way. A transaction's pinned connection is covered alike, since core hands it back through the same release once the transaction has ended.
Dead means the client library has lost the server: the error number on the handle, $conn->{dbh}->err, is one of
2006CR_SERVER_GONE_ERROR-- server has gone away: the client library found the connection closed as it sent the statement.2013CR_SERVER_LOST-- lost connection during query: it closed while the result was awaited. Also what a connection that died while it sat idle in the pool answers its next statement with: that statement is sent without an error and fails as its result is read (measured; see below).2026CR_SSL_CONNECTION_ERROR-- TLS/SSL error: unexpected eof while reading: the same loss on an encrypted connection, where it is this number the handle holds and not 2013 (measured; see below). A TLS error on a statement means the encrypted channel is broken, whatever broke it.2055CR_SERVER_LOST_EXTENDED-- lost connection at ..., system error .... Listed for what it says; MySQL's client library and MariaDB Connector/C both report that text under number 2013.
or $conn holds no handle at all. These are numbers of the client library (errmsg.h), the same in MySQL's and in MariaDB Connector/C, and both DBDs hand the library's number through as err unchanged -- two of them (2006, 2013) are also what each DBD's own auto-reconnect takes for "the server has disconnected". So one implementation serves both DBDs and DBIO::MySQL::Storage::MariaDB::Async adds nothing.
It is a list, not "any error", because a statement error leaves the session where it was: after a duplicate key (1062), a syntax error (1064), a deadlock (1213), a lock wait timeout (1205) or an interrupted query (1317) the connection runs the next statement, and retiring it would cost a reconnect per failed statement -- and inside a transaction it would have the base refuse every statement queued behind the failed one. Any number not listed answers false, as does a failure the DBD did not report at all (no number on the handle). A statement the server timed out is among them, and it is not 1317: MySQL reports 3024 for MAX_EXECUTION_TIME, MariaDB 1969 for max_statement_time. After either, as after 1317 from KILL QUERY, the session is there and a transaction it had open is still open (measured inside txn_do_async, t/79-future-io-txn-kill-query-live.t).
The answer is also what the quiet drop of a dead connection's handles asks ("_shutdown_pool_connection", "_submit_query"), through an internal sibling, so the seam itself is still asked by the base alone.
No ping, no round trip. The seam runs in the failure path of a query, from inside the event loop, so it must not block or send anything; a ping is a synchronous round trip, and would be paid on every ordinary statement error. The number is already there. It is read from the database handle rather than the statement handle, since a statement that fails as it is sent leaves no statement handle on $conn; DBI keeps one error for a statement handle and its database handle. DBI clears it only on the next method call on the handle, and the base asks from the failed Future's own callbacks, before the release. It is not always the number the statement died with, though: a statement that fails as it is sent dies with 2006, its statement handle is dropped as "_submit_query" unwinds, and that handle's DESTROY, failing on the dead connection, leaves 2013 behind -- which is what this seam then reads, on both DBDs (measured). Both are on the list. $error is not read: the exception text carries no number, and its wording is the client library's and, for a server error, the server's lc_messages.
What was measured (2026-10-05, t/77-future-io-conn-error-is-fatal-live.t; MySQL 8.0.30 and MariaDB 11.8.8, DBD::mysql 4.055 and DBD::MariaDB 1.24, each plain and over TLS 1.3). On that host both DBDs were built on MariaDB Connector/C 3.4.9, so MySQL's own client library is not among the measurements. The number is what the handle held when this seam was asked; "dead" means a ping on it right afterwards failed.
what happened plain TLS connection
KILL CONNECTION while a statement runs 2013 2026 dead
the ROLLBACK core then sends, when
that was in a transaction 2013 2013 dead
statement on a connection killed
while it sat idle 2013 2026 dead
statement after wait_timeout, MariaDB 2013 2026 dead
statement after wait_timeout, MySQL 4031 4031 dead
KILL QUERY while a statement runs 1317 1317 alive
duplicate key, syntax error 1062, 1064 alive
Every row was the same on both DBDs. KILL CONNECTION never arrived as a server error: what the client library reported was the loss of the connection.
Not listed (or listed by product):
4031is two errors, and is listed for one product. On MySQL 8.0.24 and later it isER_CLIENT_INTERACTION_TIMEOUT, the client was disconnected by the server because of inactivity: MySQL 8.0.30 sends it as an error packet ahead of the close atwait_timeout, the client library hands it out as the answer to the next statement, and the connection is dead (measured, table above). On MariaDB the same number isER_REFERENCED_TRG_DOES_NOT_EXIST, a statement error on a healthy session. The number alone cannot tell, so the seam answers 4031 from the server product (maintainer's decision): fatal on MySQL 8.0.24 and later, not on MariaDB, and not when the product is unknown. The product is the version string of the server's handshake (mysql_serverinfo/mariadb_serverinfo, named by the handle's DBD), read once when the pool connection is opened and kept on it: no statement, no round trip, nothing sent on a connection that may be dead. It is the same stringSELECT VERSION()answers, which "_is_mariadb" in DBIO::MySQL::Storage reads on the sync side.1053ER_SERVER_SHUTDOWNand MariaDB's1927ER_CONNECTION_KILLEDare what the servers set for a statement on a session being killed. AKILLdid not deliver either of them (table above). Whether a server that shuts down does, and in which state the connection then is, has not been measured -- it needs a server that may be stopped.2014CR_COMMANDS_OUT_OF_SYNCis the client library refusing a statement while a result is unread. It was looked for with aCALLthat returns two result sets, and did not arrive: with DBD::mysql the next statement simply runs, and DBD::MariaDB 1.24 refuses it -- and every one after it on that connection -- with its own2000, Calling a synchronous function on an asynchronous handle, a number that says nothing about the server. That cannot happen any more: "_collect_result" reads the remaining result sets out ("_drain_more_results"), so the connection stays usable. When that reading fails, the connection is flagged, and this seam answers true for it whatever the handle's error: the pool retires it. 2000 itself stays a kept error -- it is the DBD's number for a misuse.
A connection that dies while it sits idle is "_idle_connection_alive"'s to catch, before the pool hands it out. What that check cannot see -- a session that ended without the server saying so, or after the check -- is handed out, its statement fails as a lost connection, and this seam retires it: the call after that gets a fresh one.
The listen connection is asked about too: its statements run through "_query_async_pinned" in DBIO::Async::Storage ("_listen_drain"). The pool does not hold it, so a true answer changes nothing there; core's listen machinery sees the failed poll and rebuilds the connection itself.
_idle_connection_alive
my $alive = $storage->_idle_connection_alive($conn);
Driver seam of "_idle_connection_alive" in DBIO::Async::Storage: is the server session behind the idle pool connection $conn still there? Core's pool asks it every time it is about to hand out an idle connection ("acquire" in DBIO::Storage::PoolBase); on a false answer it retires $conn and takes the next idle connection or a fresh one. So a session the server ended while its connection sat idle -- KILL, a server restart, wait_timeout -- no longer costs a caller a failed statement first.
Three things are read, in this order, and nothing else:
- 1. No handle, or one whose
Activeis false: dead.Activeis DBI's own flag on the handle, read from memory; both DBDs clear it only when the handle is disconnected on this side. It does not notice the server going away -- that is what step 3 is for -- but it keeps the next step from asking a closed handle for its socket. - 2. No socket fd ("_conn_socket_fd"): dead. Both DBDs answer
undefonce the client library holds no socket;mysql_fdandmariadb_sockfdeach return a number the library or the DBD keeps in memory. - 3. The socket is readable, by one
selectwith a zero timeout on that fd: dead. Not readable: alive -- the common case, and all a borrow costs.
Why readable means dead. MySQL's client/server protocol is request and response: the server sends a packet only to answer a command, on MySQL and on MariaDB alike. An idle connection has no command outstanding -- the pool takes a connection back only once its statement's result was collected or the statement failed, and one cancelled in flight is retired, not idled ("_query_async" in DBIO::Async::Storage). So nothing is due on it, and whatever makes its socket readable is not an answer to anything:
The server closed it. End of file leaves a socket readable for good. This is what
KILL, a shutdown or an idle timeout come to. AKILLsends nothing ahead of the close, on MySQL 8.0.30 and on MariaDB 11.8.8, and neither does MariaDB atwait_timeout(measured).The server said why, then closed it. MySQL 8.0.24 and later send an error packet (4031, the client was disconnected by the server because of inactivity) ahead of the close at
wait_timeout-- seen on the socket of an idle connection on 8.0.30 -- and a server shutting down may send one too. The packet can arrive before the close does; either way the session is over.A previous statement left something unread -- a further result set of a statement that returned more than one. "_collect_result" reads those out, so none reaches the idle list. A
CALLwith two result sets never made this check see anything, though: the socket was quiet, on both DBDs (measured), which is why the unread result was a defect of the collector and not something this check could have caught.A TLS record that carries no MySQL data -- a session ticket or a key update of TLS 1.3. This is the one case where a healthy connection would be taken for dead. A server's session tickets follow the handshake at once and are read with the answer to the authentication, so none is expected on a connection that has gone idle; if one arrives anyway the cost is one reconnect, never an error. Over TLS 1.3 to both servers none did: the idle socket was quiet at every borrow, from the first after the connect (measured).
It only looks. The select reports; it does not read, so what the server sent stays on the socket, and no recv, not even a peeking one, is needed to tell the cases apart: all of them are dead. It never waits (the timeout is zero; a select interrupted by a signal is repeated) and never sends: no ping, no statement -- both are round trips, and this runs on every borrow. A select that fails for any other reason -- the fd is not open -- is dead: the transport waits for every result on this same fd ("_await_readable" in DBIO::Async::Storage) and could not run a statement on it either. Perl's select takes a bit vector of any length, so an fd above 1023 is checked like any other.
One implementation serves both DBDs: the fd comes from the connection's own DBD ("_conn_socket_fd"), the rest is DBI's and the kernel's, and DBIO::MySQL::Storage::MariaDB::Async adds nothing.
Read from the protocol and the DBD sources (DBD::mysql 4.055, DBD::MariaDB 1.24), exercised offline against real sockets, and measured against servers (2026-10-05, t/76-future-io-idle-connection-alive-live.t; MySQL 8.0.30 and MariaDB 11.8.8, both DBDs -- on that host both built on MariaDB Connector/C 3.4.9 -- each plain and over TLS 1.3): a healthy idle connection's socket stays quiet and the connection is handed out again; after a KILL and after wait_timeout it is readable, and the next call runs on a new session without an error. What a server that shuts down leaves on the socket has not been measured.
It narrows the window; it does not close it. A session can end after the check, or without the server saying so -- a network partition, a host gone, a firewall that dropped the idle flow: the socket stays quiet, the connection is handed out, and its statement fails as a lost connection. "_conn_error_is_fatal" then retires it, as before. The two are siblings: that one reports a connection dead after a statement failed on it, this one before one is sent. Together a connection the server ended while it sat idle costs no caller a call.
The listen connection is not asked about here: the pool does not hold it, and core's listen machinery probes it through "_listen_probe".
_listen_open
my $f = $storage->_listen_open; # done with { dbh => $dbh, ... }
Core listen seam ("_listen_open" in DBIO::Storage::Async): open the dedicated listen connection -- a new DBD handle, never one from the pool. Its connect info is what a pool spawn would get at this moment: fresh credentials through "_conninfo_provider" in DBIO::Storage::Async when an AccessBroker is attached, otherwise the static conninfo. A provider whose broker has been detached ("clear_access_broker" in DBIO::Storage) opens nothing: "_async_broker_conninfo" in DBIO::Async::Storage, which this transport inherits, throws saying that no broker is attached, and core reports that like any other failed open. The connect shape is the pool's ("_create_pool_connection"): AutoInactiveDestroy => 1, so a forked child's copy of the handle does not end the parent's session when it is destroyed (DBD::MariaDB closes it anyway at the child's exit -- the Fork caveat under "LISTEN / NOTIFY (emulated)"), and the DBD's auto-reconnect ("_mysql_auto_reconnect_attr") off -- here forced off even against the connect attributes, so a dead session is reported instead of silently replaced.
The connection carries a fresh DBIO::MySQL::NotifyTable built from the "notify_table" settings: the marks of this connection, which a reconnect therefore takes anew. Invalid settings throw here, before anything connects; core reports that and keeps retrying with backoff. Resolves with the pool's connection shape, so the pinned query path of DBIO::Async::Storage runs on it unchanged.
_listen_close
$storage->_listen_close($conn);
Core listen seam ("_listen_close" in DBIO::Storage::Async): disconnect the listen handle and take it off $conn. A dead connection is fine, and so is one with a poll still in flight -- core has stopped the watch first, which cancelled the poll. The handle is disconnected before the last statement handle is released: releasing an in-flight asynchronous statement handle waits for its result, disconnecting first does not. That statement handle is released with its errors silenced, since on a dead connection its DESTROY would only report the loss core has already reported.
_listen_subscribe
my $f = $storage->_listen_subscribe($conn, $channel);
Core listen seam ("_listen_subscribe" in DBIO::Storage::Async): take the channel's mark ("subscribe_sql" in DBIO::MySQL::NotifyTable) on the listen connection and resolve once it is in effect -- so a notify after listen(...)->get is delivered. A poll in flight is waited for first. The channel is validated at this seam and travels as a bind value; a channel it refuses makes the seam throw, and a missing notify table fails it with a message naming the table and how to deploy it. Either way core finds the connection alive, drops the channel and fails its listen Future with the reason.
_listen_unsubscribe
my $f = $storage->_listen_unsubscribe($conn, $channel);
Core listen seam ("_listen_unsubscribe" in DBIO::Storage::Async): stop polling the channel. Local bookkeeping only ("unsubscribe" in DBIO::MySQL::NotifyTable); rows for it that a poll already in flight brings back are dropped.
_listen_poll_interval
Seam of the DBIO::Async::Storage watch loop ("_listen_poll_interval" in DBIO::Async::Storage): "notify_poll_interval" seconds, which selects its timer mode -- it polls on that interval and never watches the socket.
_listen_drain
my $f = $storage->_listen_drain($conn);
# Future of [ [ $channel, $payload, $sender_pid ], ... ]
Driver seam of the DBIO::Async::Storage watch loop ("_listen_drain" in DBIO::Async::Storage), called on every tick: one poll of the notify table ("poll_sql" in DBIO::MySQL::NotifyTable) as a non-blocking query on the listen connection, resolving with what is new in the contract order. When a cleanup is due ("notify_retention") it runs right after, on the same connection, before the Future resolves; its failure goes to core's "_listen_error" in DBIO::Storage::Async, which reports it through on_listen_error, and does not fail the drain.
The Future fails when the poll fails -- a dead connection, for the loop's _listen_lost. An empty arrayref, without a statement, when no channel is subscribed or a subscribe's statement is running on the connection (the next tick polls). Dies on a connection "_listen_close" closed or whose statement a cancelled watch abandoned.
_listen_probe
my $alive = $storage->_listen_probe($conn);
Core listen seam ("_listen_probe" in DBIO::Storage::Async), asked by "listen_alive" in DBIO::Storage::Async and after a failed subscribe: the DBD's ping, a synchronous round trip, false on a dead connection. While a statement is in flight on the connection it answers true without pinging -- a ping then would die in the DBD and be taken for a death -- because that statement reports a dead connection itself.
_notify_sql
my ($sql, $bind) = $storage->_notify_sql($channel, $payload);
Driver seam of "_notify_send" in DBIO::Async::Storage, which runs the statement on a pooled connection: "notify_sql" in DBIO::MySQL::NotifyTable, an INSERT with channel and payload as bind values, CONNECTION_ID() as the sender. Throws on an invalid channel, payload or "notify_table", which fails the Future of "notify" in DBIO::Storage::Async.
deploy_notify_table
$storage->deploy_notify_table->get;
Create the notify table ("create_table_sql" in DBIO::MySQL::NotifyTable, CREATE TABLE IF NOT EXISTS) on a pooled connection; a Future that resolves once it exists. Nothing creates it implicitly: it is DDL, which MySQL commits implicitly, and a schema change is the deployer's call. Fails for an invalid "notify_table".
_ddl_class
The install-DDL generator the inherited "deploy_async" in DBIO::Storage::Async resolves through "_install_ddl" in DBIO::Storage::Async: DBIO::MySQL::DDL -- the very class the sync MySQL deploy pipeline uses ("_ddl_class" in DBIO::MySQL::Deploy), so async and sync deploy emit byte-identical MySQL DDL. Filling this seam is all the transport needs: core's generic deploy_async then runs every generated statement over the "_query_async_pinned" in DBIO::Async::Storage path this class already serves for CRUD, through "_submit_query" / "_collect_result". Inherited unchanged by DBIO::MySQL::Storage::MariaDB::Async -- which DDL the server speaks is a property of the product, not of the DBD a connection runs on (this dist's ADR 0001).
The DDL batch is deliberately not wrapped in a transaction. MySQL implicitly COMMITs each DDL statement -- DBIO::MySQL::Storage declares _use_transactional_ddl(0) for both products -- so the inherited deploy_async takes its non-transactional branch (ADR 0026): statement at a time on one pooled connection, with a one-shot warning naming this class. A deploy that fails half way therefore leaves the already-executed statements behind; re-run with add_drop_table => 1 to get back to a known state.
_ddl_quote_ident
my $quoted = $storage->_ddl_quote_ident($name);
The identifier of every statement in the "deploy_async" in DBIO::Storage::Async add_drop_table => 1 pre-pass (DROP VIEW IF EXISTS for the declared views, DROP TABLE IF EXISTS ... CASCADE for the tables), quoted the way the install DDL quotes it: "quote_ident" in DBIO::MySQL::Identifier, always backticks. The seam of core quotes ANSI (a plain name bare, any other in double quotes), which on MySQL and MariaDB is a syntax error (1064) for a table named order or my-tab, and a string literal for the second. Which objects are dropped, in which order, and the shape of the result are the seam's ("_drop_statements_for" in DBIO::Storage::Async); this hook is the only part that differs here.
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.