DBIO-PostgreSQL-Age
view release on metacpan or search on metacpan
lib/DBIO/PostgreSQL/Age/Storage.pm view on Meta::CPAN
# newer versions do not. Be defensive about both.
my $raw = $value;
$raw =~ s/::(?:vertex|edge|path)\s*\z//;
return $raw unless defined $raw && length $raw;
my $first = substr($raw, 0, 1);
# Map or list: hand off to JSON.
if ($first eq '{' || $first eq '[') {
my $decoded = eval { $JSON->decode($raw) };
return $decoded unless $@;
return $raw;
}
# Quoted string scalar: "foo" -> foo. Decode any JSON-escaped chars.
if ($first eq '"') {
my $decoded = eval { $JSON->decode($raw) };
return $decoded unless $@;
# Fallback: naive strip of outer quotes.
my $inner = substr($raw, 1, length($raw) - 2);
return $inner;
}
# Unquoted scalar. true/false/null/number.
if ($raw eq 'true') { return $JSON->true; }
if ($raw eq 'false') { return $JSON->false; }
if ($raw eq 'null') { return undef; }
lib/DBIO/PostgreSQL/Age/Storage.pm view on Meta::CPAN
C<connect_call_do_sql> uses, and it is what routes the C<LOAD 'age'> replay onto
each freshly-spawned async pool connection (core karr #68); a bare
C<< $self->dbh->do >> would run against the sync dbh instead and defeat the
replay. The pure helpers C<_cypher_sql_bind> and C<decode_agtype> are DB-free
class-level helpers, shared by composition with the async layer
(L<DBIO::PostgreSQL::Age::Storage::Async>) so sync and async build identical SQL
and decode identically.
All result columns from C<cypher()> are declared as C<agtype> â Apache AGE's
JSON-superset type that represents vertices, edges, paths, and scalar values.
Values are returned as strings and can be decoded with a JSON parser.
=head1 METHODS
=head2 connect_call_load_age
{ on_connect_call => 'load_age' }
Connection callback that loads the Apache AGE shared library into the session
and sets C<search_path> to include C<ag_catalog>. Must be called before any
graph operations.
lib/DBIO/PostgreSQL/Age/Storage.pm view on Meta::CPAN
=item * Null (C<null>) â C<undef>
=item * Map / list (e.g. C<{"name": "alice"}>, C<[1, 2, 3]>) â hashref / arrayref
=item * Vertex / edge â same as the underlying map; a trailing C<::vertex> /
C<::edge> cast annotation (only emitted by older AGE versions) is stripped
before decoding. C<id>, C<label>, C<start_id>, C<end_id>, C<properties> are
preserved as JSON keys.
=item * Path â arrayref of decoded vertices and edges (structure preserved,
not unwrapped)
=item * Anything else â returned as-is so the caller can post-process it
=back
=head2 cypher
my $rows = $storage->cypher(
'social',
lib/DBIO/PostgreSQL/Age/Storage/Async.pm view on Meta::CPAN
$async->create_graph_async('social')->then(sub {
$async->cypher_async(
'social',
$$ MATCH (n:Person {name: $name}) RETURN n $$,
['node'],
{ name => 'Alice' },
{ auto_decode => 1 },
);
})->then(sub {
my ($rows) = @_; # arrayref of hashrefs, one key per column, already decoded
...
});
=head1 DESCRIPTION
The B<floating async storage layer> for L<Apache AGE|https://age.apache.org/>.
It is a plain method package -- B<not> a transport and B<not> a subclass of any
transport. Core's storage-layer composition (karr #70) mirrors the registered
sync Age layer onto its async sibling by convention
(C<< DBIO::PostgreSQL::Age::Storage >> -> C<< ...::Async >>) and composes B<this>
lib/DBIO/PostgreSQL/Age/Storage/Async.pm view on Meta::CPAN
The async counterpart of L<DBIO::PostgreSQL::Age::Storage/cypher>. Builds the
same SQL and binds via the shared C<_cypher_sql_bind> and executes them over the
composed transport, returning a L<Future> that resolves to an arrayref of
hashrefs (one key per C<$columns> entry) -- exactly the shape sync C<cypher()>
returns.
C<$params>, if given, is JSON-encoded and passed as AGE's third C<cypher()>
argument. With C<< { auto_decode => 1 } >> every cell is passed through
L<DBIO::PostgreSQL::Age::Storage/decode_agtype> B<inside the Future chain>, so
the resolved arrayref is already decoded -- identical semantics to sync
C<auto_decode>, just async. Without it every cell is a raw agtype string and
decoding is the caller's responsibility.
=head2 create_graph_async
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>.
t/10-age-live.t view on Meta::CPAN
q{
MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person)
RETURN a.name, b.name, r.since
},
[qw(src dst since)],
);
is(scalar @$with_edge, 2, 'Alice has two KNOWS edges');
like($with_edge->[0]{since}, qr/^"?20\d\d"?$/, 'edge property since is a year');
# --- auto_decode: cypher() option returns structured Perl data ---
my $decoded = $schema->storage->cypher(
$graph,
q[ MATCH (p:Person {name: $name}) RETURN p.age, p.name ],
[qw(age name)],
{ name => 'Alice' },
{ auto_decode => 1 },
);
is($decoded->[0]{name}, 'Alice',
'auto_decode: string scalar has quotes stripped');
is($decoded->[0]{age}, 30,
'auto_decode: integer scalar comes back as Perl number');
# --- auto_decode: vertex object decoded into hashref ---
my $vertex_rows = $schema->storage->cypher(
$graph,
q[ MATCH (p:Person {name: $name}) RETURN p ],
['p'],
{ name => 'Alice' },
{ auto_decode => 1 },
);
my $v = $vertex_rows->[0]{p};
is(ref($v), 'HASH', 'auto_decode: vertex decodes to hashref');
is($v->{label}, 'Person', 'auto_decode: vertex label preserved');
is_deeply(
{ name => 'Alice', age => 30 },
$v->{properties},
'auto_decode: vertex properties decoded into nested hashref'
);
# --- 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"?$/,
t/11-age-deploy.t view on Meta::CPAN
my $rows = $schema->storage->cypher(
'deploy_test',
q[ MATCH (p:Person {name: $name})-[:KNOWS]->(friend) RETURN friend.name, friend.age ],
[qw(name age)],
{ name => 'Alice' },
{ auto_decode => 1 },
);
is(scalar @$rows, 2, 'Alice has two outgoing KNOWS edges');
my @friends = sort map { $_->{name} } @$rows;
is_deeply(\@friends, [qw(Bob Carol)],
'auto_decode returns friend names as decoded strings');
};
done_testing;
t/21-agtype.t view on Meta::CPAN
}
{
# Newer AGE (1.4+) returns the same JSON object WITHOUT the cast annotation.
# The decoder must produce the same shape in either case.
my $raw_no_cast = q[{"id": 844424930131969, "label": "Person", "properties": {"name": "alice"}}];
my $raw_with_cast = $raw_no_cast . '::vertex';
my $a = $s->decode_agtype($raw_no_cast);
my $b = $s->decode_agtype($raw_with_cast);
is_deeply $a, $b,
'vertex decoded identically with or without ::vertex annotation';
}
# --- nested / tricky cases ---
{
# Vertex where properties contain a nested object.
my $raw = q[{"id": 1, "label": "Person", "properties": {"name": "alice", "addr": {"city": "Berlin", "zip": "10115"}}}::vertex];
my $v = $s->decode_agtype($raw);
is $v->{properties}{addr}{city}, 'Berlin',
'nested map (vertex.properties.addr.city) is decoded recursively';
is $v->{properties}{addr}{zip}, '10115',
'nested string inside properties is decoded (quotes stripped)';
}
{
# The string "true" (with quotes) MUST decode to the literal string 'true',
# not to JSON true. This is the classic gotcha.
my $v = $s->decode_agtype('"true"');
is $v, 'true', 'quoted "true" decodes to the string "true" (not boolean)';
}
{
t/22-cypher-async.t view on Meta::CPAN
)->get;
# Expected = the SAME pure decode_agtype applied cell-by-cell.
my @expected = map {
{ name => DBIO::PostgreSQL::Age::Storage::decode_agtype($a, $_->[0]),
age => DBIO::PostgreSQL::Age::Storage::decode_agtype($a, $_->[1]) }
} @raw;
is_deeply $rows, \@expected,
'auto_decode applies decode_agtype to every cell, inside the Future chain';
is $rows->[0]{name}, 'alice', '... quoted string decoded';
is $rows->[0]{age}, 30, '... integer decoded to a number';
is ref($rows->[1]{name}), 'HASH', '... vertex decoded to a hashref';
is $rows->[1]{age}, undef, '... null decoded to undef';
}
# --- create_graph_async / drop_graph_async: mirror the sync graph SQL --------
{
my $a = FakeAgeAsync->new;
$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';
( run in 1.740 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )