Revision history for DBIO-PostgreSQL-Age

0.900004  2026-11-08
    * Deploy
        - New DBIO::PostgreSQL::Age::Extension: a schema that declares
          pg_extensions('age') has Apache AGE as an entry of its extension
          list. The graph schemas created by create_graph belong to the
          deploy model, so an unrestricted DBIO::PostgreSQL deploy never
          diffs them into DROP SCHEMA, DROP TABLE or DROP TRIGGER.
        - The extension names the activation (LOAD 'age', ag_catalog on the
          search_path) and the storage layer. AGE beside PostGIS on one
          schema works by the extension protocol.
        - DBIO::PostgreSQL::Age::Deploy and DBIO::PostgreSQL::Age::Introspect
          are stubs; their rules live in the extension class.

    * Connection
        - connect_call_load_age puts ag_catalog in front of the search_path;
          with a tenant namespace (connect key `schema`) it goes behind the
          namespace, so unqualified DDL lands there. A path that already
          holds ag_catalog, quoted or not, stays as it is.
        - An async pool connection replays LOAD 'age' and the ag_catalog
          search_path after the pool's session reset (DISCARD ALL), so
          cypher() runs in every borrow without a schema prefix.

    * agtype
        - decode_agtype strips every ::vertex, ::edge, ::path and ::numeric
          annotation outside string literals, also inside lists and maps, so
          a path or a collect(n) list decodes with auto_decode. A numeric a
          double cannot hold exactly (over 15 significant digits) stays the
          exact string.
        - decode_agtype(undef) returns undef without a warning.
        - decode_agtype reads a value in linear time, decodes a value with
          the UTF8 flag as characters and a byte string as UTF-8, and throws
          a DBIO::Exception naming map or list and the annotation, without
          the value, for a structure it cannot decode.
        - Cypher parameters are encoded as a character string, so non-ASCII
          values reach DBD::Pg once-encoded and read back as written.

    * Storage
        - cypher() and cypher_async() dollar-quote the Cypher text with a tag
          that does not occur in it ($dbio_age$, counted up on a collision),
          so a "$$" in a string literal is safe. Result column names must be
          plain identifiers, otherwise "Invalid AGE column name" is thrown.
          Values belong in the $params argument.
        - The Age component derives from DBIO::Base, like the other extension
          components.

    * Documentation
        - cypher_async, create_graph_async and drop_graph_async are
          documented as pool-only: inside a txn_do_async body they run on a
          sibling connection, outside the transaction.
        - SYNOPSIS, README and the ADRs follow the code; Cypher bodies in the
          POD are written as q{ ... }, and every example schema declares its
          driver (use DBIO 'Schema', -pg).

    * Dependencies
        - Requires DBIO 0.900004, DBIO::PostgreSQL 0.900004 and JSON::MaybeXS;
          develop recommends DBIO::PostgreSQL::EV 0.900004 and
          DBIO::PostgreSQL::PostGIS 0.900004.
        - Every module under lib/ carries its own literal $VERSION.

0.900001  2026-07-12
    * Storage
        - AGE now composes onto any schema as a storage LAYER
          (register_storage_layer) instead of setting storage_type
          directly, so it stacks with sibling extensions (e.g. PostGIS)
          on one schema and runs on every async transport that can
          replay LOAD 'age' -- future_io and ev today. Age::Storage and
          Age::Storage::Async are now plain layers (no more `use base`
          on the PostgreSQL sync/async storage classes); the async
          layer is composed by core over whatever transport the mode
          resolves (future_io -> PostgreSQL::Storage::Async, ev ->
          EV::Storage) instead of hard-inheriting the future_io
          adapter. cypher_async / create_graph_async / drop_graph_async
          are dispatched from the sync side via core's _run_async (like
          core CRUD *_async: route to the backend on an async
          connection, degrade in-process under { async => 'immediate' },
          croak on a sync connection); _cypher_sql_bind and
          decode_agtype are shared by composition, never by inheriting
          the sync DBI storage. The '?' seam is transport-internal
          (core #70/ADR 0032): no caller-side _transform_sql call
          remains, and the $$...$$ cypher body / $name refs survive
          unshaped. required_transport_capabilities => (on_connect_replay)
          is declared and enforced by core's capability gate. LOAD 'age'
          + search_path setup rides the core pool on_connect-replay seam
          (karr #68) on every freshly spawned pool connection, on every
          transport -- no adapter-local LOAD hack. karr #5, #6, core #70.
        - New decode_agtype() decodes agtype scalars, maps, lists,
          vertices (id/label/properties), edges (id/label/start_id/
          end_id/properties) and paths, including older-AGE-version
          fallback when the ::vertex/::edge/::path type annotations are
          absent. cypher() (and cypher_async) accept an { auto_decode =>
          1 } option that runs it over every cell of every row; 4-arg
          calls are unaffected.

    * Fixes
        - DBIO::PostgreSQL::Age::Storage no longer registers itself as the Pg
          driver. The registration clobbered DBIO::PostgreSQL's entry, so any
          plain dbi:Pg: connection made after the AGE module was loaded would
          rebless into AGE storage and run graph SQL against a non-AGE database.
          AGE is activated explicitly via the component (which registers the
          Age storage layer), so the global registration was never needed.
          karr #2.

    * Tests
        - t/30-registry.t pins that loading AGE storage leaves the Pg
          driver resolving to the plain PostgreSQL storage
        - Expanded live/offline coverage to 141 tests: agtype decoding
          across every shape, an idempotent create_graph + MERGE deploy
          pattern, WHERE/ORDER BY/SET/REMOVE/MERGE/OPTIONAL MATCH/WITH +
          count()/variable-length-path Cypher coverage, layer stacking
          with PostGIS on one schema, dual-mode coexistence (ev +
          future_io instances on one schema class resolving distinct
          composed backends, ADR 0030), and immediate-mode smoke
          coverage. docker-compose and k8s test paths moved off the
          retired private src.ci/srv/postgres image onto the public
          apache/age image (postgres 18 + AGE 1.7); k8s/test.sh now
          bails early on a port already in use or a port-forward that
          never comes up, instead of reporting a false PASS against a
          dead socket.

    * Dependencies
        - Requires core DBIO and DBIO::PostgreSQL >= 0.900001
          (composition-carrying versions); DBIO::PostgreSQL::EV and
          DBIO::PostgreSQL::PostGIS are develop-recommends only (the
          offline suite gates on them and skips cleanly when absent)

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

0.900000  2026-06-23
    First release. Apache AGE graph database support layered on DBIO::PostgreSQL.

    * Schema component
        - DBIO::PostgreSQL::Age, loaded via load_components('PostgreSQL::Age'),
          sets storage_type to DBIO::PostgreSQL::Age::Storage

    * Storage
        - DBIO::PostgreSQL::Age::Storage extends DBIO::PostgreSQL::Storage and
          registers for the Pg driver
        - connect_call_load_age connection callback: LOAD 'age' and set
          search_path to include ag_catalog on each connection

    * Graph lifecycle
        - create_graph($name) creates a named graph
        - drop_graph($name, $cascade) drops a graph, optionally cascading to
          all vertices and edges

    * Cypher queries
        - cypher($graph, $query, \@columns, \%params) executes openCypher and
          returns arrayrefs of hashrefs; result columns are declared as agtype
        - graph name validated as a plain identifier and inlined as a SQL
          literal, since Apache AGE requires the first cypher() argument to be
          a name constant rather than a placeholder
        - optional params hashref JSON-encoded and passed as AGE's third
          cypher() argument for parameterized queries
        - SQL/bind generation split into a pure _cypher_sql_bind helper for
          offline unit testing

    * Tests
        - t/00-load.t module load, t/20-cypher.t offline cypher SQL/bind
          coverage, t/10-age-live.t live graph create/drop and CREATE/MATCH
          queries (skips without a configured AGE-enabled PostgreSQL)
