Revision history for DBIO-MySQL

0.900004  2026-11-08
    * Storage
        - The SQL dialect follows the connected server, not the DBD driver:
          upsert row alias, locking-read clause and replica status. A modifier
          the server lacks is refused locally.
        - Capabilities declared: supports_exists_query, insert_returning from
          MariaDB 10.5, transactional_ddl 0, supports_upsert_where 0;
          INTERSECT and EXCEPT are refused for an older declared server.
        - The session runs with STRICT_ALL_TABLES unless a strict mode is set,
          so an overlong value is refused, not cut.
        - Binary columns bind as SQL_BINARY; under DBD::mysql with
          mysql_enable_utf8mb4 text binds as characters, also in populate in
          void context. Pass characters for text and bytes for binary columns.
        - mysql_auto_reconnect is off from connect on, named per DBD (mysql_*
          or mariadb_*); an explicit value is kept. load_components('MySQL')
          works over dbi:MariaDB: and the other way round.
        - DBD::MariaDB: a forked child's exit leaves the parent's connections
          alone (DBIO::MySQL::MariaDBForkGuard).
        - The storage declares the mysql SQL dialect, so a backslash in a
          literal is an escape for the DDL splitter. A HostBound view over a
          positional DSN binds its host and port.
        - deployment_statements asks no server, and a schema always declares
          its driver.
        - An error text carries the type, the column, the key or constraint and
          the number of binds, never a bound value: the server's own quotes
          of a value (Duplicate entry, Incorrect ... value, the excerpt after
          "near") are masked as well as the values themselves, short ones
          included. Both DBDs, client and server side prepare, and the
          future_io transport.

    * SQL
        - $rs->upsert renders INSERT ... ON DUPLICATE KEY UPDATE. A demanded
          conflict target on a table with further unique keys throws unless
          conflict_target => 'any'.
        - order_by with -nulls => 'first' or 'last' is emulated by a leading
          IS NULL sort, in the PostgreSQL row order.

    * Types
        - uniqueidentifier is VARCHAR(36), clob is LONGTEXT and money is
          DECIMAL(19,4); an enum or set column writes its members from extra
          => { list => [...] } and a bit column keeps its size.
        - The decimal, character, varchar, varbinary and binary aliases take
          an explicit size, a precision-only numeric renders DECIMAL(p), and a
          column without is_nullable deploys as NOT NULL.

    * DDL
        - Every identifier is written in backticks, so reserved words and
          names like my-col install and upgrade.
        - mysql_index renders FULLTEXT and SPATIAL as index classes and BTREE
          and HASH as USING; invalid values are refused. install_ddl also
          reads idx and indices.
        - Tables are written in foreign key order, cycles close with ALTER
          TABLE ... ADD FOREIGN KEY, and the key of an AUTO_INCREMENT column
          outside the primary key stands in the CREATE TABLE.
        - Foreign keys carry ON UPDATE and ON DELETE from the belongs_to
          attributes; SET DEFAULT and unknown words throw. A relationship to a
          view writes no foreign key.
        - A foreign key has the fixed name <table>_<columns>_fk, the same in
          install and upgrade; duplicates become one constraint and clashing
          names get a hash suffix.
        - Views are written after the views they read; a cycle is refused,
          naming the views.
        - DEFAULT values come from one helper (DBIO::MySQL::DefaultValue):
          literals are escaped, expressions from the catalog are not quoted. A
          refused column declaration reaches the caller.
        - A table named database.table is made in that database; the driver
          creates no database. A view or foreign key with a dot is refused.

    * Deploy
        - DBIO::MySQL::Deploy takes apply and upgrade from DBIO::Deploy::Base:
          a missing database and rows a new NOT NULL column cannot fill are
          refused before the first statement; dry_run asks the server nothing.
        - A column change that can alter stored values (narrower type, signed
          to unsigned, another character set, enum or set losing a member)
          needs allow_destructive; lossless type pairs and wider sizes do not.
        - add_drop_table drops the declared views before the tables, sync and
          async, so a second deploy over a filled schema runs.

    * Introspect
        - is_pk is read from the index PRIMARY; defaults are literals or
          expressions (default_is_expression), and ON UPDATE CURRENT_TIMESTAMP
          is read as on_update.
        - A nullable column without a default has none on MariaDB too, and an
          explicit NO ACTION foreign key of a MariaDB reads as RESTRICT.

    * Diff
        - An upgrade gives an existing table the primary key the target names,
          drops the key of a target without one, and handles the foreign keys
          and AUTO_INCREMENT columns around the change.
        - Unique indexes are diffed like every other index; an index is
          dropped before the column changes and created after them, and an
          index a foreign key needs is replaced before its drop.
        - Foreign keys stand around everything else: dropped first, added
          last. The last foreign key of a table is dropped when it leaves the
          schema.
        - The foreign key of a new table follows an index the same upgrade
          creates over its columns, and ADD COLUMN follows the declaration
          order of the schema (contributed by Olaf Alders).
        - An upgrade from an empty database creates tables in foreign key
          order. Tables of other databases (database.table) are created,
          compared and altered there.
        - Views are diffed as views (DBIO::MySQL::Diff::View); views that read
          each other are dropped and created in dependency order, and a
          changed definition counts as changing, not destructive.
        - A table whose collation differs is converted with CONVERT TO where
          the data provably survives; a narrowing, a TEXT, ENUM or SET column
          of a widening and a foreign key over text are refused.
        - CREATE TABLE names the collation install_ddl writes, a column off
          its table's character set or collation is written explicitly, and ON
          UPDATE CURRENT_TIMESTAMP is compared and written.
        - DECIMAL(p) compares as decimal(p,0), and the default of a column the
          schema leaves without one is removed, so an upgraded schema diffs
          empty.
        - A FULLTEXT or SPATIAL index added through a diff is rendered as such
          (DBIO::MySQL::IndexType); the POD names the MariaDB FULLTEXT rebuild
          behaviour.

    * Async
        - The future_io transport fills the deploy seam with the generator of
          the sync pipeline, so deploy_async writes the same DDL;
          add_drop_table quotes tables with backticks.
        - LISTEN/NOTIFY is emulated over a notify table (ADR 0008): listen,
          unlisten and notify work on { async => 'future_io' };
          deploy_notify_table creates the table.
        - Pool connections are fork-safe and do not auto-reconnect unless the
          connect attributes say so.
        - The DBD binding (mysql_* or mariadb_*) is chosen per connection, so
          every component and DSN combination connects; a DSN without a driver
          part is refused.
        - connect($broker, { ... }) takes DBD attributes and pool_size beside
          an AccessBroker, and attributes in the fifth element or the
          single-hash form reach every pool and listen connection.
        - A dead or idle-expired pool connection is retired before it is
          handed out, and _collect_result reads the further result sets of a
          CALL.
        - insert_async resolves with the id of its own statement, also when
          inserts overlap.
        - The connections get the session setup of the sync storage: strict
          sql_mode, SQL_BINARY binds, text as characters.

    * Security
        - Connect errors of the future_io transport read *** for the password.

    * Tests
        - DBIO::MySQL::Test::Conformance answers the shared DDL, Diff and Bind
          case tables of core against MySQL and MariaDB, each case in a
          database of its own.
        - A fresh maint/docker stack runs the whole live suite as dbio with no
          manual step (contributed by Olaf Alders); the MySQL test image is
          pinned to 8.0.30.
        - Live tests skip, naming the missing GRANT, when the account cannot
          create a diff's temporary database, and a DBD that is not installed
          is skipped (contributed by Olaf Alders).

    * Documentation
        - POD, README and ADRs describe the code as it behaves, and the README
          states Perl 5.20 as the requirement.

    * Dependencies
        - Requires DBIO 0.900004.

0.900001  2026-07-12
    * Storage
        - Add DBIO::MySQL::Storage::Async and DBIO::MySQL::Storage::MariaDB::Async,
          the real future_io async adapter. It drives whichever DBD the DSN
          named: the base class carries DBD::mysql's async binding (async /
          mysql_fd / mysql_async_ready / mysql_async_result / mysql_insertid),
          and the MariaDB subclass overrides five DBD-specific primitives with
          DBD::MariaDB's mariadb_* binding -- mirroring the sync mysql_insertid /
          mariadb_insertid split, so a dbi:mysql: connection uses DBD::mysql's
          async and a dbi:MariaDB: connection uses DBD::MariaDB's. Subclasses
          DBIO::Async::Storage and fills only the DB-specific seams
          (_submit_query / _collect_result, socket fd, insertid folded onto the
          autoinc PK in place of RETURNING); Model-B orchestration is inherited.
          Convention-resolved off the leaf driver storage (ADR 0030) — no
          registration. Pipeline is unsupported (one async query per connection).
        - Register the 'ev' async mode (core ADR 0030): connect(..., { async => 'ev' })
          now resolves DBIO::MySQL::EV::Storage via the core mode registry;
          this shadows the generic modes registered on DBIO::Storage::DBI
          and replaces the obsolete async_backend() / load_components('MySQL::EV')
          opt-ins. Requires the optional dbio-mysql-ev dist at first use;
          absent → the canonical "install DBIO::MySQL::EV::Storage" croak (ADR 0030).
        - Reference-driver compliance for the storage-layer composition model
          (karr #22, core #70): storage_type is written at exactly two sites,
          both driver Schema components (DBIO::MySQL -> DBIO::MySQL::Storage,
          DBIO::MySQL::MariaDB -> DBIO::MySQL::Storage::MariaDB) -- the
          driver's job; extensions register plain storage LAYERS instead of
          subclassing storage_type. Inherits transport_capabilities =>
          (on_connect_replay); MySQL/MariaDB have no LISTEN/NOTIFY/COPY
          (documented), pipelining stays on the ev backend. The driver was
          already future_io-compliant -- MySQL keeps '?' natively, so
          _transform_sql is identity, run once inside the inherited
          DBIO::Async::Storage transport (no double-shaping). No behavioural
          change -- POD/comments + driver-level structural tests only.

    * Documentation
        - Add docs/adr/0030-async-mode-registration.md — per-repo operationalisation
          of the core async mode registry mechanism for the MySQL driver
        - Add docs/adr/0007-async-transport-mirrors-the-sync-dbd-split.md —
          records why the future_io adapter mirrors the sync mysql_*/mariadb_*
          DBD split (one base class plus a MariaDB-flavoured subclass) instead
          of a single generic transport

    * Security
        - Add SECURITY.md following the CPAN Security Group's Guidelines for
          Adding a Security Policy to Perl Distributions (v1.5.0): email-first
          private reporting to the maintainer

    * Bugfixes
        - Diff: suppress the standalone DROP INDEX that collided with DROP TABLE
          in a full-schema sync (karr #23). When a whole table is removed in the
          same diff pass, DROP TABLE already drops that table's own indexes;
          as_sql runs tables before indexes, so the later DROP INDEX ... ON
          <table> targeted an already-gone table and failed on a real server.
          DBIO::MySQL::Diff now threads the source/target tables sections into
          Diff::Index->diff (as it already does for Diff::Column /
          Diff::ForeignKey), and Diff::Index skips the drop for any index whose
          owning table is itself being dropped. Indexes removed from a surviving
          table are unaffected. Backward compatible: absent table args -> no
          suppression.
        - Fix the native introspect->diff round-trip against a real MySQL 8
          server, which produced phantom diffs (every column reported as a
          spurious add) and a zero-date write failure: information_schema
          result columns come back UPPERCASE on MySQL 8, so Columns/
          ForeignKeys/Indexes now force NAME_lc on their fetches; MySQL
          8.0.17+ drops the integer display width in information_schema
          (bigint(20) -> bigint), now normalised on both sides in
          Diff::Column (tinyint(1) and the semantic decimal/char widths stay
          untouched); target_from_compiled no longer synthesises a literal
          'NULL' default for nullable no-default columns (information_schema
          reports SQL NULL as undef, not the string); and a new
          connect_call_set_zero_date_permissive on_connect seam relaxes
          strict sql_mode on every (re)connect, not just the deploy-time
          handle, so zero-date writes survive reconnects
        - Remove t/30-versioning.t and its DBIOVersion_v1/v2/v3 fixtures; the
          test exercised DBIO::Schema::Versioned, which core retired (its
          upgrade path depended on the hard-deprecated create_ddl_dir, with
          no native replacement -- core karr #74)
        - Remove t/01-access-broker-api.t, which required the undeclared and
          since-removed DBIO::AccessBroker::Credentials and broke the test
          suite on a clean install (Codeberg #1)
        - Make the table-name assertions in t/50-introspect-contract.t
          quote-agnostic; the code generator emits double-quoted strings via
          B::perlstring

    * Dependencies
        - Require DBIO 0.900001 (central private-attr strip in
          DBIO::Storage::Async::connect_info, on which both the sync and the
          future_io path rely)
        - DBIO::Async, the shared future_io backend base the async adapter
          subclasses, is deliberately NOT declared in cpanfile: it is
          optional and convention-resolved at runtime (ADR 0030), only
          pulled when { async => 'future_io' } is used, so a sync-only
          install never drags in the async stack. Core's future_io resolver
          now names the missing module and the distribution that provides
          it when the adapter can't load (core karr #78), so a separate
          recommends pointer is no longer needed.
        - Add a runtime suggests on DBD::mysql alongside the recommended
          DBD::MariaDB, so both supported DBD drivers are advertised in the
          metadata (DBD::MariaDB stays the default, DBD::mysql the alternative)
        - Drop a stray test-phase recommends on Kubernetes::REST, an unused
          copy-paste leftover from another distribution

    * Tests
        - Add maint/docker/ (karr #21): a Docker test image for a buildable,
          live DBD::mysql -- ubuntu:24.04's libmysqlclient-dev is still the
          genuine Oracle MySQL client (Debian switched its default provider to
          MariaDB, and the MySQL APT repo's own signing key has expired),
          sidestepping both the wrong-flavour and wrong-signature roadblocks.
          Live-verified the mysql_* future_io transport (t/55) against a real
          mysql:8.0 server -- non-blocking select/insert/select_single,
          mysql_insertid folded onto the autoinc PK, txn_do_async commit and
          rollback, and the high-level create_async/all_async API -- mirroring
          the already-live mariadb_* subclass

0.900000  2026-06-23
    First release. MySQL and MariaDB driver for DBIO, with native schema
    deployment, introspection, diffing and DDL generation.

    * Driver
        - MySQL/MariaDB storage with autodetection of dbi:mysql: DSNs via
          the DBIO driver registry
        - DBIO::MySQL schema component and -mysql shortcut
        - DBIO::MySQL::Storage and DBIO::MySQL::Storage::MariaDB
        - DateTime::Format::MySQL datetime parsing

    * SQLMaker
        - DBIO::MySQL::SQLMaker with apply_limit() (LIMIT ?, ? syntax) and
          FOR SHARE locking
        - DBIO::MySQL::SQLMaker::MariaDB maps FOR SHARE to LOCK IN SHARE
          MODE for MariaDB

    * Schema management
        - DBIO::MySQL::Introspect (Tables, Columns, Indexes, ForeignKeys)
          via information_schema
        - DBIO::MySQL::DDL generator from Result classes with
          ENGINE/CHARSET/COLLATE handling, view and shared-table handling
        - DBIO::MySQL::Diff (Table, Column, Index, ForeignKey) with inlined
          columns on CREATE TABLE and full MODIFY COLUMN support
        - DBIO::MySQL::Deploy: install/diff/apply/upgrade via test-deploy-
          and-compare against a temporary database
        - Storage routes DBIO::Schema->deploy() through DBIO::MySQL::Deploy

    * Result component
        - DBIO::MySQL::Result with mysql_engine, mysql_charset,
          mysql_collate and mysql_index declarations consumed by the DDL
          generator
