NAME

DBIO::MySQL::Storage::Async - future_io async MySQL transport over the DBD driver's native async binding

VERSION

version 0.900001

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 (core karr #70), 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

This class carries the DBD::mysql binding and all the shared transport control flow. Its convention subclass DBIO::MySQL::Storage::MariaDB::Async (resolved for a dbi:MariaDB: connection) overrides only the five DBD-specific primitives below with their mariadb_* equivalents. So whichever DBD the sync connection used, the matching async binding is driven -- mirroring the sync split, where DBIO::MySQL::Storage::MariaDB reads mariadb_insertid where DBIO::MySQL::Storage reads mysql_insertid. 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:

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).

Transport capabilities

This transport inherits transport_capabilities from DBIO::Async::Storage, which advertises exactly on_connect_replay (the pool replays the owning sync storage's on_connect_do / on_connect_call on every freshly-spawned connection, core karr #68). It declares nothing extra.

MySQL and MariaDB have no LISTEN/NOTIFY and no COPY (those are PostgreSQL wire features), so unlike the PostgreSQL transport 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 the inherited scaffold croaks if pipeline is called. The native ev backend (DBIO::MySQL::EV::Storage, dist dbio-mysql-ev, over EV::MariaDB) is the transport that carries pipelining. 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.

METHODS

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.

_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 was captured on $self->{_last_insert_id} by "_collect_result" from the per-statement insertid ("_async_insertid") 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 no id was generated (an explicit PK was supplied, or the table has no auto-increment) the supplied data is returned untouched. Overrides the inherited RETURNING-row default.

Genuinely-concurrent inserts on one storage instance (outside a transaction) are not supported by this last_insert_id path -- issue them sequentially or inside a txn_do_async. This mirrors MySQL's own "one async query per connection" limit; pipelining is not available on this backend.

last_insert_id

my $id = $storage->last_insert_id;

The auto-increment value from the last INSERT this storage finalised, captured from the driver's per-statement insertid ("_async_insertid"). Valid after an insert_async / insert succeeds; provided for legacy callers (scripts, tests). The CRUD path uses "_insert_returned_columns" instead.

_normalize_conninfo

my $info = $storage->_normalize_conninfo([ 'dbi:mysql:...', $user, $pass, \%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. A pool_size attribute, if present, is lifted into the conninfo hash (the inherited normaliser strips it back out to size the pool). 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.

_shutdown_pool_connection

Disconnect the DBD handle held by $conn.

_async_prepare_attrs

The prepare-time attributes that arm a non-blocking query: { async => 1 } for DBD::mysql. execute then returns immediately.

_conn_socket_fd

my $fd = $storage->_conn_socket_fd($dbh);

The connection socket file descriptor for the Future::IO readable watcher -- $dbh->mysql_fd for DBD::mysql (the MySQL analogue of DBD::Pg's pg_socket).

_async_ready

my $bool = $storage->_async_ready($sth);

True once "_async_result" will not block -- DBD::mysql mysql_async_ready.

_async_result

my $rv = $storage->_async_result($sth);

Finalise the in-flight async statement and return what execute would have -- the affected-row count (DBD::mysql mysql_async_result). Dies (RaiseError) on a server-side error.

_async_insertid

my $id = $storage->_async_insertid($sth);

The per-statement AUTO_INCREMENT id generated by the finalised INSERT, read from $sth->{mysql_insertid} -- the per-sth form DBD::mysql recommends over the shared-handle $dbh->{mysql_insertid}. Zero / undef for non-INSERTs.

_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).

_collect_result

my $future = $storage->_collect_result($conn, $sql, $bind);

Called once the socket is readable. If the async result has not fully arrived ("_async_ready" is false) it waits for the socket again 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 ->all yields (ADR 0031 §3);

  • a statement with no result set (INSERT / UPDATE / DELETE, BEGIN / COMMIT / ...) -> the affected-row count from "_async_result".

For an INSERT it also captures the per-statement insertid ("_async_insertid") onto $self->{_last_insert_id} so "_insert_returned_columns" can fold the auto-increment id into the returned-columns hashref (MySQL has no RETURNING). The id is captured only when non-zero, so a following non-INSERT statement does not clobber a pending insert's id.

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).

_txn_context_class

The pinned-connection transaction context handed to a txn_do_async coderef: the future_io DBIO::Async::TransactionContext.

_txn_conn_accessor

The constructor key the pinned connection is passed under -- txn_conn, matching DBIO::Storage::Async::TransactionContext.

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.