WebDyne::Cloudflare::D1

Future-returning Cloudflare D1 facade.

Synopsis

my $db_or=WebDyne::Cloudflare::D1->new(
    scope => $self->r()->{'scope'}, binding => 'DB',
);
my $row_hr=$db_or->prepare('SELECT name FROM things WHERE id = ?1')
    ->bind($id)->first()->get();

Interface

SQL parameters accept scalars, undef (SQL NULL), JSON booleans and explicit BLOB wrappers. Unflagged non-ASCII text is decoded strictly as UTF-8. Returned BLOB columns become Perl bytes. Rows may safely contain columns named type and base64; envelopes are interpreted only at column-value positions. Zero, empty strings and NULL remain distinct.

Host failures fail the Future with a D1::Error. Missing capabilities and invalid constructor arguments throw immediately. Internal encoding/host-call helpers are not supported API. No implicit retries or automatic SQL interpolation is provided.

Atomic batches

my $insert_or=$db_or->prepare('INSERT INTO things(name) VALUES (?1)');
my $results_ar=$db_or->batch([
    $insert_or->bind('first'),
    $insert_or->bind('second'),
    $db_or->prepare('SELECT name FROM things ORDER BY name'),
])->get();
my $rows_ar=$results_ar->[2]{'results'};

D1 executes statements sequentially and rolls back the entire batch when one fails. The Future fails with a structured D1 error; partial results are not returned. Parameters and BLOB columns follow the ordinary query encoding. Statements remain reusable and binding does not change the original statement.

Supply all statements before execution; a batch cannot pause for Perl code to inspect a result or bind that result into a later statement. It is not an interactive transaction API. Statements from another database object (even one using the same binding) are rejected, as are empty batches and non-statement entries. Validation errors fail the Future before the host call. The adapter does not split or retry batches; Cloudflare's query and resource limits apply.

Text inputs, including nested metadata keys and values, are normalized from unflagged UTF-8 without changing caller data. Invalid UTF-8, cyclic containers and duplicate normalized keys are rejected before calling the host. See the usage guide.

Sessions and read replication

with_session($constraint_or_bookmark) returns a Future of a Session. Omit the argument for first-unconstrained, or pass first-primary or a non-empty bookmark from a previous session. Explicit undef, empty strings, references and extra arguments are rejected.

my $session_or=await $db_or->with_session('first-primary');
await $session_or->run('UPDATE things SET name = ?1 WHERE id = ?2', $name, $id);
my $row_hr=await $session_or->first('SELECT name FROM things WHERE id = ?1', $id);
my $bookmark=await $session_or->get_bookmark();

Enable read replication separately in the Cloudflare D1 database settings. Existing database objects keep their primary-only behavior. Sessions also work when replication is disabled; writes always go to the primary. A session does not guarantee the latest primary data on every read.

For HTTP continuity, read a per-database bookmark from an application header such as x-d1-bookmark, use it in with_session(), then return the new bookmark in the response header before sending the response. For example, with a PAGI scope and send callback:

my ($bookmark)=map { $_->[1] }
    grep { lc($_->[0]) eq 'x-d1-bookmark' } @{$scope_hr->{'headers'}};
my $session_or=await $db_or->with_session(
    (defined($bookmark)&&length($bookmark)) ? $bookmark : 'first-primary',
);
my $result_hr=await $session_or->all('SELECT name FROM things');
my $next=await $session_or->get_bookmark();
await $send_cr->({type => 'http.response.start', status => 200, headers => [
    ['content-type', 'application/json'], ['cache-control', 'no-store'],
    (defined($next) ? (['x-d1-bookmark', $next]) : ()),
]});
await $send_cr->({type => 'http.response.body',
    body => JSON::PP->new()->utf8()->encode(WebDyne::Cloudflare::json_value($result_hr)),
    more_body => 0});

Choose bookmark/header names separately for multiple databases. The library never automatically reads or writes HTTP headers. Finish database work before response headers when exporting the final bookmark, including streaming pages.

The protocol remains version 1 with an additive session_bindings allow-list. Older hosts without that advertisement reject session creation in Perl; hosts also verify native withSession support. Session IDs stay scoped to a request and binding. run, all and batch preserve provider metadata, including served_by_primary and served_by_region when supplied; first and raw retain their existing row-only results.

See Cloudflare read replication and the Sessions API.