@bunny.net/database-client is a small SQL client for Bunny Database. It has no dependencies, and fetch is the only runtime API it touches, so the same code runs on Bunny Edge Scripting (Deno), Bun, and Node.
Bunny Database is currently in Public
Preview. Features and APIs
may evolve during this period.
Install
Quickstart
connect() reads BUNNY_DATABASE_URL and BUNNY_DATABASE_AUTH_TOKEN from the environment. Edge Scripting already sets both, bunny db quickstart --lang typescript prints them for you, and bunny db create --token --save-env writes them straight to .env:
packages/database-client/examples.
API
connect(config?)
Returns a Database. Every option is optional.
The client rewrites a
libsql:// URL to https://. It rejects credentials in the URL, both user:pass@ and an authToken query parameter, so pass authToken instead. Dropping a token silently would leave you debugging an unexplained 401.
Every request carries User-Agent: bunny-database-client. The client matches header names case-insensitively, so your own User-Agent replaces that default and only one of the two goes out.
Without timeout a request waits as long as the runtime allows, which on an edge function means a hung fetch can burn the whole invocation. timeout and signal compose: whichever fires first aborts the request.
db.prepare(sql)
Returns a Statement. Statements are immutable, so you can keep one around and bind it as often as you like.
db.sql`...`
A template literal that binds every interpolated value, so the shortest way to write a query is also the parameterized one:
${...} becomes a ? placeholder, and the client binds the value without ever splicing it into the SQL string. It returns a Statement, so everything under Executing applies unchanged.
Values follow the same rules as bind(), with one exception: an interpolated object throws. Inside a template it is nearly always a mistake, so named parameters go through bind().
Pass a row type the same way as prepare(), as db.sql<User>`...` .
SQLite parameterizes values and nothing else, so a table or column name that has to vary belongs in the SQL string you build with prepare().
statement.bind(...values)
Binds parameters and returns a new statement. Accepts null, boolean, number, bigint, string, and Uint8Array.
Pass values in order for ? placeholders:
:name, @name, and $name:
{ id: 1 } and { ":id": 1 } both bind :id. One statement uses one style, and mixing positional values with an object in the same bind() call throws. The client will not pick a winner for you.
undefined throws. A mistyped property such as bind(user.nmae) surfaces at the call site, and nothing writes NULL on your behalf. Pass null when you mean NULL.
Any other value throws, because SQLite has nowhere to put it. Date gets its own message pointing at .toISOString() and .getTime(). The client will not choose for you, since each one puts something different in the column.
The client sends an integer number as INTEGER for as long as it fits exactly, up to 2^53. Past that every double is a whole number, so the client sends it as REAL and SQLite stores the value as is. Pass a bigint when you need an exact integer that large. Bigints must fit SQLite’s signed 64-bit range.
Executing
Four ways to run a statement:run() returns rows plus write metadata:
runRaw() returns the same metadata with rows as positional arrays. Reach for it when a result may contain two columns of the same name, since object rows keep only the last one:
prepare() and bind() are safe to pass around.
db.batch(statements, options?)
Runs every statement in one transaction and one round trip. All of them commit or none do.
Result per statement you passed, in order. batchRaw() does the same with positional rows. If any statement fails the transaction rolls back and batch() throws that statement’s error, with error.batchIndex set to the position of the statement that failed.
The batch is the transaction, so the client rejects a statement of your own that starts with BEGIN, COMMIT, END, or