DBIO-PostgreSQL-Age
view release on metacpan or search on metacpan
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
[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.086 second using v1.01-cache-2.11-cpan-e7c6538aa59 )