Revision history for DBIO-PostgreSQL-EV

0.900004  2026-11-08
    * Connect info
        - Connect info is read in one place, in every form the owning sync
          storage reads: a DBI-form list, a hash with a dsn key, DBI
          attributes in front of the DSN, a postgresql:// URI in the DSN, an
          AccessBroker's answer. The pool connects where the dbh does, as
          the same user.
        - Every built-in AccessBroker shape is turned into libpq conninfo,
          fetched fresh for each pool spawn and each listen open, so rotated
          credentials apply (see ADR 0009). connect($broker, { async =>
          'ev', pool_size => N }) sizes the pool; no conninfo is kept from a
          broker's answer.
        - Connect info the transport cannot connect from is refused naming
          its form, never a value: a libpq parameter beside a broker or among
          DBI attributes (also spelled with a capital), an empty broker
          answer, a static dbi:Pg: DSN that names no target, a NUL byte in a
          parameter value, a code reference.
        - Every connection has a connect_timeout of 10 seconds unless the
          conninfo sets one; the POD recommends sslmode=verify-full.
        - Passwords are masked as *** in every message about a connection.

    * Pool
        - A borrow has a session of its own: DISCARD ALL runs when it ends,
          then the connect actions again, before the connection is lent on.
          A reset that fails or exceeds reset_timeout retires the connection.
        - Connections are fork-safe: a forked child never ends or reads the
          sessions of its parent. Dead connections, also idle ones whose
          server end closed, are retired and replaced on demand.
        - A pooled statement, notify or copy_in gives its connection back to
          its own pool on every path (a refused dispatch, a dying callback,
          a disconnect in a callback), and a stale release takes nothing
          from the connection's next holder.
        - A Future that waits for the pool holds itself until it is ready.
          A process that ends with statements in flight prints no warnings.
        - The connect-action runner returns the statement's rows for a true
          dbio_rows attribute.

    * Blocking calls
        - select, insert, txn_do, deploy and the other blocking methods work:
          the Future of this transport waits by running the EV loop. A wait
          that could never end, or would spin inside a delivering callback,
          dies with "Blocking wait refused" and leaves the pool usable.

    * Transactions and pipelines
        - Pinned statements run one at a time per connection, so a coderef
          that dies mid-statement rolls back cleanly. A COMMIT that
          PostgreSQL answers with ROLLBACK fails the transaction. A
          statement cancelled in the queue is not sent.
        - txn_do refuses a coderef that returns a storage call queued for
          the transaction's own connection, and rolls back.
        - pipeline hands the batch a pinned context; a connection lost
          before the sync point fails with EV::Pg's error, and a transaction
          ended inside a pipeline rolls back after the sync.
        - A pinned async context of an earlier connection generation is
          refused once connect_info or disconnect has run.
        - TransactionContext is core's, with txn_pg as an alias of txn_conn.

    * Cancel
        - Cancelling the Future of a statement in flight asks the server to
          stop it (EV::Pg cancel_async, libpq 17 or later). The connection
          returns to the pool once the statement and the request are
          answered.

    * LISTEN/NOTIFY
        - listen, unlisten and notify are core's, with listen_alive,
          listen_reconnect and the error and reconnect hooks. The listen
          connection reconnects with backoff and listens again, takes the
          broker's current answer, and survives a forked child.
        - Channel names follow DBIO::PostgreSQL::Util::quote_channel;
          listen_alive notices a session the server has ended.

    * COPY
        - copy_in escapes backslash, TAB, LF and CR by the COPY text-format
          rules, writes table and column names as identifiers, and ends the
          COPY once. A stream that cannot be ended retires its connection;
          a row libpq does not take fails the COPY.

    * Errors
        - A refused statement fails its Future with a
          DBIO::Exception::Server (sqlstate, constraint, table, detail);
          BEGIN, COMMIT and ROLLBACK keep it with their context. Values in
          DETAIL lines are masked, as on the synchronous storage.
        - An error names no value: every text of the server's that reaches an
          exception -- a pooled, pinned or pipelined statement, notify,
          LISTEN, copy_in, the session reset, a connect action -- loses the
          values bound to the statement (four characters or more, wherever
          they stand, as in 'relation "..." does not exist') and the values
          the server quotes in its messages (invalid input syntax, out of
          range, enum, date/time, array literal, byte sequence, time zone,
          the bound parameter and COPY line in CONTEXT). A text without a
          known form stays as it is, also from an EV::Pg without
          error_fields.
        - Refusals of the transport are DBIO::Exception objects at the
          caller's file and line; a dispatch EV::Pg refuses fails with its
          own text.

    * SQL
        - A ? is numbered into $N only where PostgreSQL reads SQL, by
          DBIO::PostgreSQL::Util::number_placeholders, as on the other
          transport: not in dollar quoting, comments or E'' strings. \? is a
          literal question mark, mixing $N and ? is refused, and a ? after @
          is a placeholder.
        - Identifier quoting follows the owning connection: quote_names => 0
          and an explicit quote_char or name_sep are used as given.
        - Upserts with ON CONFLICT ... DO UPDATE and RETURNING work through
          the transport; SET values arrive as plain values.
        - deploy_async runs core's generic deploy over the DDL class of the
          sync storage, and asks the server which schema the control file
          of an extension fixes.

    * Tracing
        - The statement trace is core's: query_start and query_end on the
          storage's debug object, binds from DBIO_TRACE=2 on, encode_column
          binds shown as ***, redact_bind_value applied, values cut at 32
          characters.

    * Documentation
        - DBIO::PostgreSQL::EV carries the overview of the distribution, and
          its SYNOPSIS uses a plain schema that declares its driver.
        - Setup instructions, README and ADRs 0001-0010 describe
          the code; the shipped demo and skill use { async => 'ev' }.

    * Dependencies
        - Requires DBIO 0.900004, DBIO::PostgreSQL 0.900004, EV 4.11 and
          Sub::Util 1.70.

0.900001  2026-07-12
    * Documentation
        - DBIO::PostgreSQL::EV POD: replaced the stale async_backend() opt-in
          story with the actual { async => 'ev' } per-connection opt-in
          (ADR 0030); SYNOPSIS now shows loading the PostgreSQL::EV component
          and connecting with the async option instead of implying async runs
          without either. (karr #23, flagged during #22)

    * Pool
        - DBIO::PostgreSQL::EV::Pool adopts core DBIO::Storage::PoolBase's new
          connection-readiness seam (karr #75, hoisted from this pool's own
          karr #9 fix). Dropped the local acquire() override -- the base now
          gates every acquire() through _connection_ready_future for all
          three paths (spawn/idle-reuse/waiter) -- and the private {_ready}
          refaddr bookkeeping, replaced with core's
          _register_connection_ready/_connection_ready_lookup helpers;
          _shutdown_connection no longer has to clean the table up itself
          (the base's shutdown() does it centrally). Behaviour unchanged;
          this mirrors the reference fix dbio-mysql-ev's karr #20 was
          missing and just adopted the same way.

    * Packaging
        - Renamed dist DBIO-PostgreSQL-Async -> DBIO-PostgreSQL-EV and the
          DBIO::PostgreSQL::Async::* namespace -> DBIO::PostgreSQL::EV::*. The
          EV::Pg/libpq-async driver is now named for its event loop; the ::Async
          namespace is reserved for a future loop-agnostic driver. The old dist
          stays on CPAN — this is a namespace migration, not a behaviour change.

    * Storage
        - DBIO::PostgreSQL::EV::Storage is now a thin transport over core
          DBIO::Storage::Async instead of reimplementing the whole async surface
          (Storage.pm 1109 -> 938 lines). It inherits the shared machinery —
          select/insert/update/delete_async, _run_crud, txn_do_async, the
          insert-RETURNING overlay, pipeline, the sync fallbacks and SQL
          generation — and keeps only the EV/libpq wire seams plus EV value-add
          (listen/notify, copy_in, deploy_async). (karr #22, core #70)
        - Async is opt-in per connection via { async => 'ev' } (ADR 0030):
          loading the PostgreSQL::EV component is an inert marker, and the EV
          backend is reached explicitly through
          MyApp::Schema->connect($dsn, $u, $p, { async => 'ev' }). The async
          storage is embedded as the async backend of the sync storage rather
          than hijacking storage_type; it weakens its schema ref to break the
          embed cycle and accepts the sync driver's DBI-form connect_info. The
          legacy async_backend() instance method and async_fallback chain are
          gone — a *_async call on a sync instance now croaks explicitly rather
          than silently degrading. (ADR 0028, ADR 0030, karr #59)
        - insert_async resolves with a returned-columns HASHREF, not a positional
          row (ADR 0031 §3): _run_crud appends RETURNING * to the INSERT and folds
          the returned row onto the source's declared column order, so
          create_async / Row::insert_async see the autoinc PK on the row.
          select_async resolves with row arrayrefs (cursor ->all shape),
          select_single_async with a single row arrayref / undef; documented in
          POD. The backend Future is plain perl-Future, whose ->then auto-wraps a
          plain return, so no explicit Future->done is needed in *_async
          callbacks (ADR 0031 §4). (karr #18)
        - transport_capabilities advertises on_connect_replay, listen, notify,
          copy and pipeline — each backed by a real implementation, none
          claimed-but-absent. (karr #22)

    * SQL
        - Async CRUD translates the SQL maker's '?' placeholders into PostgreSQL
          positional '$N' before handing SQL to libpq (EV::Pg->query_params only
          understands '$N') via the _transform_sql seam. The shared
          DBIO::PostgreSQL::SQLMaker keeps emitting '?' for the sync DBI driver;
          the translation is async-only and preserves the '@?' jsonpath operator
          and '?' inside quoted literals. Without it a bound WHERE/SET/VALUES
          reached Postgres as '... = ?' and died with a syntax error.
          (karr #7, core #70, ADR 0032)

    * Deploy
        - deploy_async: deploy a schema through DBIO's native deploy pipeline,
          executing every generated DDL statement on EV::Pg inside a single
          async transaction (COMMIT on success, ROLLBACK on the first DDL
          error). The add_drop_table option prepends DROP TABLE IF EXISTS ...
          CASCADE for every regular table (views and non-plain-identifier names
          skipped) so a re-run on a populated database stays idempotent. A
          blocking deploy() wrapper drives it via ->get for scripts. Covered by
          t/21-deploy-async-live.t (live).

    * Connection pool
        - on_connect_do / on_connect_call now replay on every EV pooled
          connection (e.g. AGE's load_age): pool() passes storage => $self so
          core's pool setup fires, and each on_connect statement is driven to
          completion on the EV loop. (karr #22, karr #69)
        - Pooled acquire waits for the connection to finish its async connect
          before resolving, so the first CRUD call on a cold pool no longer dies
          with "not connected"; idle reuse stays immediate and a failed connect
          fails the dependent query instead of hanging. (karr #9)
        - Pool acquire order is FIFO (core PoolBase shift/push), verified by a
          pg_backend_pid() rotation test. (karr #20)
        - Guarded _shutdown_connection against undef at global destruction — no
          more "Use of uninitialized value" from Pool.pm on teardown.

    * Bug fixes
        - txn_do_async no longer hangs. Its COMMIT/ROLLBACK chain was built off
          the coderef's Future, which Future.pm holds only weakly, so the chain
          was garbage-collected ("lost a sequence Future") before COMMIT/ROLLBACK
          could run — the transaction Future never resolved and the event loop
          busy-spun at ~100% CPU. The chain now completes the outer Future
          directly via on_done/on_fail and retains the coderef Future until it
          settles. (karr #10)
        - copy_in no longer races the pooled-connection release: the connection
          is released and the Future completed only after libpq finishes the COPY
          stream (EV::Pg fires the COPY callback twice), so a follow-up query on
          the same storage no longer hits "another command is already in
          progress". A second listen() issued while the first LISTEN was still in
          flight is no longer silently lost — LISTEN/UNLISTEN serialise through a
          single dispatch queue. (karr #13, karr #15)

    * Security
        - Added SECURITY.md following the CPAN Security Group author guidelines
          (v1.5.0): email-first private reporting to the maintainer with CPANSec
          escalation, and an explicit note that Codeberg has no confidential
          issue channel.

    * Dependencies
        - Loosened the EV::Pg pin to >= 0.02, < 0.08. EV::Pg 0.07 added the
          libpq-16+ symbols (PQconnectionUsedGSSAPI, PGRES_TUPLES_CHUNK); the
          previous < 0.03 upper bound locked out the Docker test image (libpq 17
          + EV::Pg 0.07). libpq 16+ is the new client floor; libpq-15 hosts
          (Debian 12 default) should use maint/docker/Dockerfile.test. README and
          maint/docker/README.md updated. (karr #16)
        - Bumped DBIO-family requirements to 0.900001 to keep the whole family on
          one released version.

    * Tests
        - Added DBIO::PostgreSQL::EV::TestHarness — an installable, reusable
          live-PG async harness (skip_all_unless_live,
          run_on_each_pooled_connection, await, query_on) that the EV live suite
          dogfoods and other dists (e.g. AGE) consume instead of duplicating
          scaffolding. (karr #22)
        - t/02-access-broker.t: swap the removed DBIO::AccessBroker::Credentials
          for DBIO::AccessBroker::Static (user -> username) so the suite passes
          on fresh installs instead of dying with "Can't locate
          DBIO/AccessBroker/Credentials.pm". (karr #8, Codeberg #1)
        - Added offline structural/contract guards (thin-transport
          inherited-vs-overridden identity, positional placeholders, pool
          readiness, bind-value release, insert RETURNING shapes) and live
          regression suites for pipeline, listen/notify, concurrency, copy_in,
          cold pool, transactions and the composed async layer.

0.900000  2026-06-23
    First release. Future-based async PostgreSQL storage for DBIO, speaking
    libpq's async protocol via EV::Pg with no DBI in the path.

    * Storage
        - DBIO::PostgreSQL::Async::Storage built on DBIO::Storage::Async
        - Non-blocking select/insert/update/delete returning Future objects
        - Prepared statement caching
        - Sync fallback (->all, ->first, etc. via blocking ->get on the Future)

    * Transactions
        - txn_do_async with BEGIN/COMMIT/ROLLBACK chaining
        - Transaction context with connection pinning

    * Pipelining
        - Pipeline mode batching multiple queries in a single round-trip

    * LISTEN/NOTIFY
        - Real-time event streaming via ->listen and ->notify

    * COPY
        - COPY IN for bulk data loading

    * Connection pool
        - Async connection pool with transaction pinning

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