DBIO-PostgreSQL-Age

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

          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

README.md  view on Meta::CPAN

      [qw( person friend )],
    );

DBIO core autodetects `dbi:Pg:` DSNs with the PostgreSQL driver, and
[DBIO::PostgreSQL::Age](https://metacpan.org/pod/DBIO::PostgreSQL::Age) is loaded via `load_components`.

## Apache AGE Features

**Graph Operations**
- `create_graph($name)` - create a named graph
- `drop_graph($name, $cascade)` - drop a graph (cascade drops vertices and edges too)
- `cypher($graph, $query, \@columns, \%params, \%opts)` - execute openCypher query
- `decode_agtype($value)` - decode a single agtype text value into native Perl data
- `connect_call_load_age` - connection callback: `LOAD 'age'` + `SET search_path = ag_catalog, ...`

By default `cypher()` returns each result cell as the raw agtype text
that PostgreSQL hands back over the wire — strings are quoted (`"alice"`),
maps and vertices are JSON, and so on. Pass `{ auto_decode => 1 }` as the
fifth argument to apply [`decode_agtype`](https://metacpan.org/pod/DBIO::PostgreSQL::Age::Storage#decode_agtype)
to every cell of every row, so you get back plain Perl strings, numbers,
hashrefs, arrayrefs, vertex hashrefs (`{ id, label, properties }`), and

docs/adr/0001-native-graph-surface-on-storage.md  view on Meta::CPAN

connection), never through the relational ORM API and never via a second
transport object:

- `cypher($graph, $query, \@columns, \%params)` — executes one openCypher query
  against the named graph (`Storage.pm:97-106`). It builds `SELECT * FROM
  cypher('graph', $$ ... $$ [, ?]) AS (col agtype, ...)` and runs it through the
  core `dbh_do` wrapper. **Every** result column is declared `agtype`
  (`Storage.pm:119`) — AGE's only column type for `cypher()` output — and rows
  come back as an arrayref of hashrefs of `agtype` strings for the caller to
  decode (JSON for projected maps; the SYNOPSIS shows `JSON::MaybeXS`).
- `create_graph($name)` / `drop_graph($name, $cascade)` — graph lifecycle, thin
  wrappers over `ag_catalog.create_graph` / `ag_catalog.drop_graph`
  (`Storage.pm:66-85`).

The SQL/bind construction is split into a **pure** helper `_cypher_sql_bind`
(`Storage.pm:110-129`) so the generated SQL and binds are unit-tested with no
database (`t/20-cypher.t`); `cypher()` is the thin wrapper that adds execution.
This is the offline-testability seam for the one method that builds SQL by hand.

### Divergence from ADR 0017 point 2 (recorded, not hidden)

lib/DBIO/PostgreSQL/Age/Storage.pm  view on Meta::CPAN

sub create_graph {
  my ($self, $name) = @_;
  $self->dbh_do(sub {
    my (undef, $dbh) = @_;
    $dbh->do('SELECT * FROM ag_catalog.create_graph(?)', undef, $name);
  });
}


sub drop_graph {
  my ($self, $name, $cascade) = @_;
  $self->dbh_do(sub {
    my (undef, $dbh) = @_;
    $dbh->do(
      'SELECT * FROM ag_catalog.drop_graph(?, ?)',
      undef, $name, $cascade ? 1 : 0,
    );
  });
}


sub cypher {
  my ($self, $graph, $query, $columns, $params, $opts) = @_;

  my ($sql, $bind) = $self->_cypher_sql_bind($graph, $query, $columns, $params);

lib/DBIO/PostgreSQL/Age/Storage.pm  view on Meta::CPAN


  $storage->create_graph('social');

  my $rows = $storage->cypher(
    'social',
    $$ MATCH (a:Person {name: $name})-[:KNOWS]->(b) RETURN b.name $$,
    ['friend'],
    { name => 'Alice' },
  );

  $storage->drop_graph('social', 1);  # cascade

=head1 DESCRIPTION

A storage B<layer> that adds Apache AGE graph database support -- connection
initialization, graph lifecycle management, and Cypher query execution -- to a
PostgreSQL storage. It is B<not> a storage subclass: it is a plain method
package composed over the resolved driver storage at connection time (see
L<DBIO::Storage::Composed>). L<DBIO::PostgreSQL::Age> registers it via
L<DBIO::Schema/register_storage_layer>, so on C<< $schema->connect >> the live
storage isa B<both> this layer and L<DBIO::PostgreSQL::Storage>, and the methods

lib/DBIO/PostgreSQL/Age/Storage.pm  view on Meta::CPAN


=head2 create_graph

  $storage->create_graph('social');

Creates a new Apache AGE graph with the given name.

=head2 drop_graph

  $storage->drop_graph('social');
  $storage->drop_graph('social', 1);  # cascade

Drops the named graph. Pass a true second argument to cascade the drop to all
vertices and edges within the graph.

=head2 cypher_async

=head2 create_graph_async

=head2 drop_graph_async

The async counterparts of L</cypher>, L</create_graph> and L</drop_graph>,
reachable on the live (composed) storage. Each dispatches through core's

lib/DBIO/PostgreSQL/Age/Storage/Async.pm  view on Meta::CPAN


sub create_graph_async {
  my ($self, $name) = @_;
  return $self->_query_async(
    'SELECT * FROM ag_catalog.create_graph(?)', [ $name ],
  );
}


sub drop_graph_async {
  my ($self, $name, $cascade) = @_;
  return $self->_query_async(
    'SELECT * FROM ag_catalog.drop_graph(?, ?)', [ $name, $cascade ? 1 : 0 ],
  );
}


1;

__END__

=pod

lib/DBIO/PostgreSQL/Age/Storage/Async.pm  view on Meta::CPAN


  my $future = $async->create_graph_async('social');

Async counterpart of L<DBIO::PostgreSQL::Age::Storage/create_graph>: a thin
wrapper that runs C<ag_catalog.create_graph(?)> over the composed transport.
Returns a L<Future>.

=head2 drop_graph_async

  my $future = $async->drop_graph_async('social');
  my $future = $async->drop_graph_async('social', 1);   # cascade

Async counterpart of L<DBIO::PostgreSQL::Age::Storage/drop_graph>: a thin
wrapper that runs C<ag_catalog.drop_graph(?, ?)> over the composed transport.
Pass a true second argument to cascade the drop. Returns a L<Future>.

=seealso

=over 4

=item * L<DBIO::PostgreSQL::Age::Storage> - the sync storage layer with C<cypher()>

=item * L<DBIO::PostgreSQL::Storage::Async> - the C<future_io> transport this layer composes over

=item * L<DBIO::PostgreSQL::EV::Storage> - the C<ev> transport this layer composes over

share/skills/dbio-postgresql-age/SKILL.md  view on Meta::CPAN

If extension not installed:

```perl
$storage->dbh->do('CREATE EXTENSION IF NOT EXISTS age');
```

## Graph Lifecycle

```perl
$storage->create_graph('social');
$storage->drop_graph('social', 1);   # 1 = cascade
```

Graph names must be plain PG identifiers (validated by `cypher()`):

```
valid:   social, app_graph_1
invalid: app-graph, public.social, "graph name"
```

## Running Cypher

t/10-age-live.t  view on Meta::CPAN

# --- backward-compat: cypher() without auto_decode still returns strings ---
my $raw = $schema->storage->cypher(
  $graph,
  q[ MATCH (p:Person {name: $name}) RETURN p.age ],
  ['age'],
  { name => 'Alice' },
);
like($raw->[0]{age}, qr/^"?30"?$/,
  'cypher() without auto_decode still returns agtype strings');

# --- drop_graph (without cascade should fail if non-empty, in newer AGE) ---
# Skip the non-cascade case — depends on AGE version. Just drop with cascade.
lives_ok { $schema->storage->drop_graph($graph, 1) } 'drop_graph cascade lives';

my ($still_there) = $schema->storage->dbh->selectrow_array(
  'SELECT 1 FROM ag_catalog.ag_graph WHERE name = ?',
  undef, $graph,
);
ok(!$still_there, 'graph removed from ag_catalog.ag_graph');

done_testing;

t/22-cypher-async.t  view on Meta::CPAN


  $a->create_graph_async('social')->get;
  is $a->{cap}[0]{sql}, 'SELECT * FROM ag_catalog.create_graph(?)',
    'create_graph_async runs ag_catalog.create_graph with a raw "?" (transport shapes it)';
  is_deeply $a->{cap}[0]{bind}, ['social'], 'create_graph_async binds the graph name';

  $a->{cap} = [];
  $a->drop_graph_async('social')->get;
  is $a->{cap}[0]{sql}, 'SELECT * FROM ag_catalog.drop_graph(?, ?)',
    'drop_graph_async runs ag_catalog.drop_graph with raw "?" placeholders';
  is_deeply $a->{cap}[0]{bind}, ['social', 0], 'drop_graph_async defaults cascade to 0';

  $a->{cap} = [];
  $a->drop_graph_async('social', 1)->get;
  is_deeply $a->{cap}[0]{bind}, ['social', 1], 'drop_graph_async passes a true cascade as 1';
}

done_testing;



( run in 1.257 second using v1.01-cache-2.11-cpan-e7c6538aa59 )