Skip to content

feat(database): add @bunny.net/database-client, a fetch-only SQL client - #154

Open
jamie-at-bunny wants to merge 5 commits into
mainfrom
database-client
Open

feat(database): add @bunny.net/database-client, a fetch-only SQL client#154
jamie-at-bunny wants to merge 5 commits into
mainfrom
database-client

Conversation

@jamie-at-bunny

@jamie-at-bunny jamie-at-bunny commented Aug 13, 2026

Copy link
Copy Markdown
Member

Adds @bunny.net/database-client, a small SQL client for Bunny Database aimed at server-side application code rather than the CLI.

It speaks hrana-over-HTTP (POST /v3/pipeline) using only fetch, so the same source runs on Edge Scripting (Deno), Bun, and Node. No dependencies.

Why not just use @libsql/client

Bunny Database runs sqld, which serves hrana v2/v3 over plain HTTP. That is JSON over fetch, so a client needs no libSQL dependency at all. Dropping it means one fewer thing to keep in step and a surface we can shape for Bunny rather than inherit.

Shape

Modelled on Cloudflare D1 rather than libSQL, since there is no backwards compatibility to preserve here:

import { connect } from "@bunny.net/database-client";

const db = connect(); // reads BUNNY_DATABASE_URL / BUNNY_DATABASE_AUTH_TOKEN

await db.prepare("SELECT * FROM users WHERE id = ?").bind(1).first();
await db.prepare("SELECT name FROM users WHERE id = ?").bind(1).first("name");
await db.prepare("SELECT id, name FROM users").all();
await db.prepare("SELECT id, name FROM users").raw();
await db.prepare("INSERT INTO users (name) VALUES (?)").bind("Carol").run();

await db.batch([...]);    // one transaction, one round trip
await db.exec("...;...")  // multi-statement script

Deliberately stateless

baton is always null and every request closes its own session, so there is no connection pool, no session pinning, and nothing to tear down when an edge isolate is discarded.

That rules out interactive transactions and cross-call TEMP tables. batch() covers atomicity instead, wrapping its steps in BEGIN / COMMIT / ROLLBACK with per-step conditions. There is also no cursor streaming and no automatic retries: a failed write cannot be retried safely unless the caller knows whether it landed.

Server-side only

An auth token authorizes the connection rather than the query, and this client sends raw SQL. Read-only tokens narrow the damage but still expose every row of every table, and SQLite has no row-level security to fall back on. The README leads with that and shows the Edge Script proxy pattern instead of documenting the client as browser-compatible.

Types

Integers decode to number while exactly representable and widen to bigint past 2^53 rather than silently rounding. Values SQLite cannot store are rejected at bind time instead of being coerced, and Date gets its own message pointing at .toISOString() or .getTime().

Verification

Release

release.yml gains a publish-database-client job, gated on a version bump detected via npm view. The package has no workspace dependencies, so it publishes with plain npm publish.

@bunnynet-devops

Copy link
Copy Markdown

@codex review

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9790c82

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bunny.net/database-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds and publishes @bunny.net/database-client, a zero-dependency, fetch-based SQL client for server-side Bun, Node, and Deno environments.

  • Adds statement binding, result decoding, batching, script execution, and structured database errors.
  • Adds environment-based configuration, documentation, tests, and a live smoke example.
  • Extends the release workflow to detect, build, and publish the package independently.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/database-client/src/client.ts Implements the public database, statement, batch, and execution APIs over the stateless pipeline transport.
packages/database-client/src/protocol.ts Implements URL normalization, Hrana value encoding and decoding, HTTP transport, and response validation.
packages/database-client/src/env.ts Adds guarded environment-variable access for runtime configuration.
packages/database-client/package.json Defines the compiled package exports, build scripts, publish contents, and public npm configuration.
.github/workflows/release.yml Adds independent version detection, build, and npm publication for the database client.
packages/database-client/examples/smoke.ts Exercises the client against a live database across its documented runtime path.

Sequence Diagram

sequenceDiagram
  participant App as Server-side application
  participant Client as database-client
  participant DB as Bunny Database
  App->>Client: connect(config)
  App->>Client: prepare(sql).bind(values)
  Client->>DB: POST /v2/pipeline
  DB-->>Client: Hrana result
  Client-->>App: rows and write metadata
Loading

Reviews (5): Last reviewed commit: "refactor(database-client): read env thro..." | Re-trigger Greptile

Comment thread packages/database-client/package.json
Comment thread packages/database-client/examples/smoke.ts
Comment thread packages/database-client/src/protocol.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c238a154b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/database-client/package.json
Comment thread packages/database-client/src/protocol.ts Outdated
Comment thread packages/database-client/src/client.ts
@jamie-at-bunny

Copy link
Copy Markdown
Member Author

Package isn't hooked up to releases just yet. I want to replace @libsql/client in the CLI and Database Studio with this package to quiet test it first @amir-at-bunny.

…ithout --allow-env (#155)

* docs(database-client): tidy README and comments, drop the D1 shorthand

- remove the truncated sentence at the end of the security section
- fold the intro's dangling dependency claim into the sentence
- explain the unsafe-integer rejection in active voice
- collapse a stacked comment in env.ts to one line
- describe the API surface in AGENTS.md without the D1 comparison

* fix(database-client): treat unreadable env vars as unset under Deno without --allow-env

Deno 2's node-compat process.env throws NotCapable on read just like
Deno.env.get, but only the latter was guarded, so readEnv crashed instead
of falling through to connect()'s clearer missing-URL error. One module-scope
sniff type and one try now cover both globals, and env.test.ts locks in the
degrade-to-unset behavior with throwing stubs.
Every runtime this client targets exposes process.env, Deno included, so
the Deno.env.get branch and the globalThis runtime sniff were carrying no
weight. readEnv() now reads process.env directly, and the examples use
process.env instead of importing readEnv.

The permission tolerance from #155 stays: reading can throw rather than
return undefined when Deno runs without --allow-env, so readEnv still
catches and reports the variable as unset. Verified that connect() with
no arguments under `deno run --allow-net` still raises URL_MISSING with
its usual message rather than a NotCapable crash.

--allow-env is still required for Deno to read process.env, so the
example's invocation line is unchanged.

Behaviour is unchanged for consumers, so the existing changeset still
covers it. Live smoke passes on Bun and Deno.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants