Revision history for DBIO-MySQL-EV

0.900004  2026-11-08
    * Async
        - Statements run concurrently over EV::MariaDB on a pool, chosen per
          connection with connect(..., { async => 'ev' }). The blocking
          methods (select, insert, txn_do, deploy ...) wait on the EV loop,
          also inside a callback of the transport's own connections, and
          refuse a wait that nothing could end (DBIO::MySQL::EV::Future).
        - deploy_async writes the same DDL as the sync pipeline, quoting
          every name with backticks in the drop pre-pass.
        - The statement trace is core's: one trace line per statement,
          encoded binds shown as ***, query_end when it is over.
        - The session of a pooled connection is reset when its borrow ends
          (COM_RESET_CONNECTION, character set and collation again, connect
          actions again); a failing or unanswered reset retires the
          connection (ADR 0011).
        - ev over TLS is concurrent: every handle's socket is set
          non-blocking, so a statement leaves the event loop free; the
          connect and the TLS handshake block.
        - The ROLLBACK of a pinned connection waits for the statements in
          flight at most rollback_wait_timeout seconds (default 30, 0 =
          unlimited); a statement queued behind a prepare or execute in
          flight waits for it in order.
        - The transaction context, the connection readiness wiring and the
          live test harness are core's; txn_mdb stays as an alias of
          txn_conn.

    * Storage
        - An error text names keys, columns and options, never a value: the
          value MySQL and MariaDB write into it ("Duplicate entry '***' for
          key ...", "Incorrect integer value: '***' for column ...", the
          quoted part of a syntax error) is taken out before the exception
          is built, bound or literal. The SQLSTATE stays. The notify
          statements of the listen connection are masked the same way.

    * LISTEN/NOTIFY
        - listen and notify are emulated over a notify table, shared with
          DBIO::MySQL (DBIO::MySQL::NotifyTable; ADRs 0008 and 0010). Latency
          is notify_poll_interval, a late commit is caught within
          notify_lookback, old rows go after notify_retention; there is no
          history. deploy_notify_table creates the table.

    * Pool
        - A connection whose session is gone (KILL, server restart,
          wait_timeout) is retired, whether a statement failed on it or it
          sits idle: the caller gets a live or a new connection, no call
          fails (ADR 0010).
        - A forked child that resumes the EV loop finishes its copy of the
          parent's handles and leaves the parent's sessions alone.
        - Every bound statement is closed on the server before its
          connection is released, so the pool stays within
          max_prepared_stmt_count; a CALL is read to its end.
        - Cancelling a statement keeps its connection checked out until the
          answer arrives; a transaction ends with its ROLLBACK queued behind
          the statement in flight, and a failing ROLLBACK or COMMIT closes
          the connection.
        - A connection goes back to the pool it was borrowed from, also
          after a die before the statement had a Future and after a
          disconnect. Shutdown finishes the handles, and a statement waiting
          on a torn-down connection fails with a DBIO::Exception.
        - A call waiting for the pool is kept alive until it is done.

    * Connect info
        - Connect info as a DBI DSN is read, including the positional form
          and the single-hash form; mysql_* and mariadb_* settings that
          EV::MariaDB has a parameter for are translated and any other is
          refused naming the key, never the value. mysql_enable_utf8mb4 and
          mysql_enable_utf8 select the character set, CLIENT_FOUND_ROWS is
          on (mysql_client_found_rows=0 turns it off), and a false
          auto_reconnect is let through.
        - Connect info without host and user is refused naming keys only,
          by the pool and by the listen connection; connect_info closes a
          listen-only connection and rotates the ownership token without a
          pool.
        - Passwords are masked in every connection message
          (DBIO::Util::mask_connect_secrets).
        - pool_size in the fifth element of DBI-form connect info sizes the
          pool.

    * AccessBroker
        - Every answer shape of a built-in broker (DBI arrayref, hash,
          pair) becomes EV::MariaDB named parameters, fresh for every
          connection; DBIO::AccessBroker::Vault connects. An answer without
          host and user or with a whole DSN as dbname is refused.
        - connect($broker, { async => 'ev', pool_size => N }) takes options
          beside a broker (broker_connect_options). A connection parameter
          there (charset, connect_timeout, host ...) is refused, naming it,
          never its value; give it in the broker's answer.
        - A detached broker opens no connection and leaves no connect info,
          password included, behind.

    * SQL
        - An upsert resolves the id of the row it touched on the ON
          DUPLICATE KEY UPDATE branch, on MariaDB 10.5 and up too, and
          evaluates the owning storage's whole plan: where the row cannot
          be named the key is withheld and upsert resolves undef. A supplied
          autoinc value is kept, and 0 means generate unless the session has
          NO_AUTO_VALUE_ON_ZERO.
        - The conflict-target rule of the sync path holds here too: an
          explicit target on a table with several unique keys is refused.
        - A server refusal fails its Future with DBIO::Exception::Server
          carrying the SQLSTATE, as the sync driver throws it.
        - insert_async resolves with the id of its own statement.

    * Testing
        - DBIO::MySQL::EV::TestHarness declares the MySQL driver by default
          and offers count_session_status and session_status for live tests
          that count Com_* of their own session across the reset.

    * Documentation
        - This driver has no pipeline mode: $storage->pipeline returns a
          failed Future without taking a connection, several *_async calls
          gathered with needs_all are the alternative (ADR 0009).
        - POD, README, the shipped dbio-mysql-ev skill and the ADRs describe
          the code as it behaves; SYNOPSIS and README use
          load_components('MySQL') with { async => 'ev' }, and the ADRs
          say which parts core owns (ADR 0006 superseded in part).

    * Dependencies
        - Requires DBIO and DBIO::MySQL 0.900004, EV 4.15 and EV::MariaDB
          0.08.

0.900001  2026-07-12

    * Storage
        - DBIO::MySQL::EV::Storage becomes a thin transport over the shared
          core Model-B machinery (720 -> 623 lines, ~208 code lines / 22
          subs): select_async/…/delete_async, _run_crud, txn_do_async, the
          sync fallbacks, _generate_sql, sql_maker, new, schema/debug/
          connected/disconnect/DESTROY, pipeline and the prepared-statement
          cache are all deleted and now inherited from core
          DBIO::Storage::Async (coderef-identity asserted in tests). What's
          kept is genuine MySQL value-add: _transform_sql as identity
          (MySQL speaks '?' natively, no ?->$N rewrite; applied first in
          the query seams to keep the core '?'-seam contract uniform),
          insert_async/_run_crud_pinned(insert) (no RETURNING, so INSERT +
          SELECT LAST_INSERT_ID() ride the same pinned session and the LII
          folds onto the auto-increment column), and
          _normalize_async_connect_info (dbname -> database). pool()
          now passes storage => $self so core's _setup_pool_connection
          fires on_connect_do/on_connect_call replay for every spawned EV
          pool connection (_run_pool_connect_statement drives it
          synchronously on the EV::MariaDB handle). transport_capabilities
          is (on_connect_replay) only -- no LISTEN/NOTIFY/COPY; pipeline is
          not claimed as a capability (EV::MariaDB pipelines automatically
          at the wire with no explicit-mode API to map onto the base
          seams). karr #18, #19, core #70.

    * Bugfixes
        - AccessBroker::Credentials was removed from core (split into
          ::Static for single-identity and DBIO::Replicated for routing); a
          stale local copy had masked the break, so a fresh install/smoker
          hit "Can't locate DBIO/AccessBroker/Credentials.pm". Switched the
          test to ::Static, mirroring the dbio-postgresql fix. karr #10.
        - The async path retained bind values on the in-flight bind
          arrayref after a query's Future completed (the sync path clears
          them once execute() returns), causing a re-issued query on the
          same handle to see stale binds while a prior future was pending,
          and memory growth proportional to total binds ever issued rather
          than outstanding queries. Bind values are now cleared in the
          completion handler (fires on both done and fail), never on the
          issuing side (which would corrupt a still-pending query's
          binds). Per CurtisPoe architecture review #5, karr #11.
        - Three driver bugs surfaced by the first end-to-end test driving
          the real DBIO facade through EV::MariaDB + EV::run (the
          mock-based suite only ever proved the Future API shape, never
          that the driver works against a live DB): Storage.pm's pool()
          was missing a lazy require for QueryExecutor (fired "Can't
          locate object method 'new'" on first use); txn_do_async held its
          coderef Future only weakly via a bare ->then->catch chain, so it
          could be GC'd before BEGIN's callback fired, silently hanging any
          EV::run-based wait loop -- fixed with ->retain, mirroring the
          dbio-postgresql-async fix; and QueryExecutor's execute() passed a
          bind arrayref to $mdb->query, which silently ignores it, so
          bound queries either threw a syntax error or ran with stale
          binds from the previous prepared statement -- now routed through
          prepare+execute whenever binds are non-empty. txn_do_async's
          on_fail also now surfaces a ROLLBACK failure as a wrapped error
          instead of dropping it silently.
        - Fix PRODUCTION BUG: the first bound async query on a cold pool
          connection died 'not connected' (karr #20). DBIO::MySQL::EV::Pool
          wired on_connect => sub {} -- a pure no-op -- so it never tracked
          connection readiness; core DBIO::Storage::PoolBase's
          connection-readiness gate (karr #75) had nothing to wait on.
          _create_connection now registers a real per-connection readiness
          Future (core _register_connection_ready) resolved from
          EV::MariaDB's on_connect/on_error, and a new
          _connection_ready_future override looks it up -- mirroring
          dbio-postgresql-ev's karr #9 fix. New offline regression test
          t/08-pool-readiness.t (mirrors PG's t/04). Closes karr #19's
          leftover gap.
    * Async opt-in via { async => 'ev' } (ADR 0030, karr #14)
        - Dropped the connection()/storage_type hijack in DBIO::MySQL::EV.
          Loading the MySQL::EV component is now an inert marker; the EV
          backend is reached explicitly through
          MyApp::Schema->connect($dsn, $u, $p, { async => 'ev' }), which
          resolves the 'ev' mode via DBIO::MySQL::Storage's mode registry
          (the registration is its own karr ticket on the dbio-mysql
          board). PostgreSQL's DBIO::PostgreSQL::EV has no such override
          either; this brings MySQL to parity.
    * insert_async resolves with the returned-columns HASHREF (ADR 0031 §3)
        - MySQL has no RETURNING clause; the EV storage reads
          SELECT LAST_INSERT_ID() on the pinned connection and assembles
          the hashref = { %to_insert, $col => $lii_id } via the new
          _insert_returned_columns hook (overrides the inherited
          RETURNING-row -> hashref default). $col is the source's first
          is_auto_increment column, falling back to the first PK column
          not supplied in %to_insert. The legacy ->last_insert_id
          side-effect is preserved. create_async / Row::insert_async
          fold the hashref back via _store_inserted_columns.
    * select_async / select_single_async shapes (ADR 0031 §3)
        - select_async resolves with the raw row arrayrefs (cursor ->all
          shape) and select_single_async with a single row arrayref,
          matching the sync cursor shape and what _inflate_fetched_rows /
          _construct_results consume.
    * Future then auto-wrap (ADR 0031 §4)
        - The backend Future is plain Future (perl-Future), whose ->then
          natively auto-wraps a plain return into a resolved Future --
          compliance is transitive. Documented in POD and verified by
          t/storage-facade-mock.t.

    * Packaging
        - Renamed distribution DBIO-MySQL-Async to DBIO-MySQL-EV and namespace
          DBIO::MySQL::Async to DBIO::MySQL::EV. The EV::MariaDB driver is
          hard-wired to the EV event loop; the DBIO::MySQL::Async namespace is
          freed for a future loop-agnostic driver built on DBIO::Async::Storage.
          CPAN namespace migration — the old distribution stays on CPAN. No
          behaviour change.

    * Tests
        - Add maint/k8s/mysql-pod.yaml (mysql:8.0, mysql_native_password
          forced so the libmariadb-based clients authenticate over a plain
          port-forward) for live integration testing, and
          DBIO::MySQL::EV::TestHarness (installable, gated on
          DBIO_TEST_MYSQL_DSN, mirrors the PostgreSQL-EV harness API).
        - Fixed test/harness-only bugs surfaced on the first live run
          (no production runtime change): PoolBase::acquire can hand off
          an EV::MariaDB handle before its async connect completes when no
          on_connect action is configured, so pooled-connection tests now
          wait for readiness explicitly (_await_connected) or warm the
          pool first; a live SELECT used an invalid bare '*' after a named
          select-expression and a plain-string column alias that got
          backtick-quoted into an unknown column; and a broker test now
          asserts the achievable truth (broker attaches to the sync
          storage; a schema-level connect() cannot select an async mode
          and attach a broker at the same time). karr #19, #21.

    * Dependencies
        - Requires core DBIO >= 0.900001 (family-wide version alignment)

    * Project
        - Added SECURITY.md (CPAN Security Group guidelines v1.5.0)

0.900000  2026-06-23
    First release. Future-based async MySQL/MariaDB storage for DBIO via EV::MariaDB.

    * Storage
        - DBIO::MySQL::Async::Storage on DBIO::Storage::Async, speaking the
          MariaDB C client directly through EV::MariaDB (no DBI / DBD::mysql)
        - Async select/insert/update/delete returning Future objects
        - Sync fallback (->all, ->first, ...) via blocking ->get on the Future

    * Transactions
        - txn_do_async driving the BEGIN/COMMIT/ROLLBACK chain
        - TransactionContext pinning one pooled connection for the txn duration

    * Pipelining
        - Pipeline mode batching queries into a single network round-trip,
          up to 64 in-flight

    * Connection pool
        - Connection pool with transaction pinning, built on
          DBIO::Storage::PoolBase

    * AccessBroker
        - Accept AccessBroker objects via Schema->connect($broker)
        - Refresh async conninfo through the broker for newly created
          pooled connections
