DBIO-PostgreSQL-Age
view release on metacpan or search on metacpan
t/22-cypher-async.t view on Meta::CPAN
isa_ok $f, 'Future', 'cypher_async returns a Future';
my $rows = $f->get;
is_deeply $rows, [ { name => '"alice"' }, { name => '"bob"' } ],
'resolves an arrayref of hashrefs keyed by column (sync {Slice=>{}} shape), raw by default';
# The captured SQL+bind must be EXACTLY the shared sync builder's output --
# RAW, un-transformed. Same sub => identical strings by construction; a
# copied/forked builder would drift and fail here.
my ($sync_sql, $sync_bind)
= DBIO::PostgreSQL::Age::Storage::_cypher_sql_bind($a, @args);
is $a->{cap}[0]{sql}, $sync_sql,
'cypher_async hands the transport the RAW sync _cypher_sql_bind SQL (no shaping in the layer)';
is_deeply $a->{cap}[0]{bind}, $sync_bind,
'cypher_async bind == sync _cypher_sql_bind bind (JSON-encoded params)';
like $a->{cap}[0]{sql}, qr/\$\$, \?\) AS \(name agtype\)/,
'param slot is a raw "?" placeholder ($N shaping is the transport concern); column declared agtype';
# '?'-seam body-survival: the Cypher body ($$...$$) and the $name reference are
# NOT SQL placeholders and must reach the transport intact.
like $a->{cap}[0]{sql}, qr/\$\$\nMATCH \(n \{name: \$name\}\) RETURN n\.name\n\$\$/,
q{the '$$...$$' cypher body and the '$name' reference survive untouched into the transport};
}
# --- cypher_async: no-params path -------------------------------------------
{
my $a = FakeAgeAsync->new;
$a->{rows} = [ [ '"x"', '"y"' ] ];
my @args = ('g', 'RETURN a, b', [qw(person friend)]);
my $rows = $a->cypher_async(@args)->get;
is_deeply $rows, [ { person => '"x"', friend => '"y"' } ],
'multi-column no-param query zips columns in declared order';
my ($sync_sql, $sync_bind)
= DBIO::PostgreSQL::Age::Storage::_cypher_sql_bind($a, @args);
is $a->{cap}[0]{sql}, $sync_sql, 'no-param SQL matches the raw sync builder';
is_deeply $a->{cap}[0]{bind}, [], 'no-param query carries no bind';
unlike $a->{cap}[0]{sql}, qr/\?/, 'no "?" placeholder emitted without params';
unlike $a->{cap}[0]{sql}, qr/\$\d/, 'no $N appears -- the layer never shapes placeholders';
}
# --- cypher_async: auto_decode parity ---------------------------------------
{
my $a = FakeAgeAsync->new;
my @raw = ( [ '"alice"', '30' ], [ '{"id":1,"label":"Person"}::vertex', 'null' ] );
$a->{rows} = [ map { [ @$_ ] } @raw ];
my $rows = $a->cypher_async(
'g', 'MATCH (n) RETURN n.name, n.age', [qw(name age)], undef, { auto_decode => 1 },
)->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';
$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 0.825 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )