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 )