Skip to content

neuralforgeone/bunnydb-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bunnydb-rs

crates.io docs.rs CI WASM

Async Rust client for the Bunny.net Database SQL pipeline API — works on native (tokio) and WebAssembly (wasm32-unknown-unknown, Bunny Edge Scripts).

Target endpoint format:

https://<db-id>.lite.bunnydb.net/v2/pipeline

Highlights

  • Async API with query, execute, batch
  • Positional (?) and named (:name) parameters
  • Typed values: null, integer, float, text, blob base64
  • Structured error model: transport, HTTP, pipeline, decode
  • Configurable timeout and retry/backoff for 429 and 5xx
  • Query telemetry fields (rows_read, rows_written, query_duration_ms)
  • wasm32-unknown-unknown — runs inside Bunny Edge Scripts via the browser fetch API

Installation

Native (server, Docker, Magic Container)

[dependencies]
bunnydb-rs = "0.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

WebAssembly (Bunny Edge Script)

[lib]
crate-type = ["cdylib"]

[dependencies]
bunnydb-rs = "0.3"       # reqwest uses fetch API automatically on wasm32
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

No extra feature flags — the crate detects wasm32-unknown-unknown at compile time and swaps tokio for the browser runtime automatically.

Client Construction

Choose the constructor that fits your deployment:

Constructor When to use
BunnyDbClient::from_env() 12-factor apps, Docker, CI: reads BUNNYDB_PIPELINE_URL + BUNNYDB_TOKEN
BunnyDbClient::from_env_db_id() Edge scripts / containers: reads BUNNYDB_ID + BUNNYDB_TOKEN
BunnyDbClient::from_db_id(id, tok) Known DB ID, token from config
BunnyDbClient::new_bearer(url, tok) Full URL + bearer token
BunnyDbClient::new_raw_auth(url, auth) Full URL + custom auth header
# Recommended defaults for production
BUNNYDB_PIPELINE_URL=https://<db-id>.lite.bunnydb.net/v2/pipeline
BUNNYDB_TOKEN=<your-token>

Quick Start

Option A — environment variables (recommended)

The most autonomous setup: set env vars once, no URL construction in code.

use bunnydb_http::BunnyDbClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Reads BUNNYDB_PIPELINE_URL + BUNNYDB_TOKEN automatically
    let db = BunnyDbClient::from_env().expect("missing BUNNYDB_* env vars");

    db.execute(
        "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
        (),
    ).await?;

    let result = db
        .query(
            "SELECT id, name FROM users WHERE name = :name",
            bunnydb_http::Params::named([("name", bunnydb_http::Value::text("Kit"))]),
        )
        .await?;

    println!("rows={}", result.rows.len());
    Ok(())
}

Option B — database ID + token

use bunnydb_http::BunnyDbClient;

// URL is derived automatically from the ID
let db = BunnyDbClient::from_db_id("my-db-abc123", "my-token");

Option C — explicit URL

use bunnydb_http::{BunnyDbClient, Params, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pipeline_url = std::env::var("BUNNYDB_PIPELINE_URL")?;
    let token = std::env::var("BUNNYDB_TOKEN")?;

    let db = BunnyDbClient::new_bearer(pipeline_url, token);

    db.execute(
        "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
        (),
    )
    .await?;

    db.execute("INSERT INTO users (name) VALUES (?)", [Value::text("Kit")])
        .await?;

    let result = db
        .query(
            "SELECT id, name FROM users WHERE name = :name",
            Params::named([("name", Value::text("Kit"))]),
        )
        .await?;

    println!(
        "rows={}, rows_read={:?}, rows_written={:?}, duration_ms={:?}",
        result.rows.len(),
        result.rows_read,
        result.rows_written,
        result.query_duration_ms
    );

    Ok(())
}

Authentication and Endpoint

  • BunnyDbClient::from_env():
    Reads BUNNYDB_PIPELINE_URL and BUNNYDB_TOKEN from environment. Ideal for 12-factor apps, Docker, CI.
  • BunnyDbClient::from_env_db_id():
    Reads BUNNYDB_ID and BUNNYDB_TOKEN. URL constructed automatically.
  • BunnyDbClient::from_db_id(db_id, token):
    Provide a database ID; URL constructed as https://<db_id>.lite.bunnydb.net/v2/pipeline.
  • BunnyDbClient::new_bearer(url, token):
    Pass the full pipeline URL and token. Bearer prefix added automatically.
  • BunnyDbClient::new_raw_auth(url, authorization):
    Pass full authorization value directly.
  • BunnyDbClient::new(url, token):
    Backward-compatible raw constructor.

url must point to the pipeline endpoint (.../v2/pipeline).

Parameters

Positional:

db.query("SELECT * FROM users WHERE id = ?", [Value::integer(1)]).await?;

Named:

db.query(
    "SELECT * FROM users WHERE name = :name",
    Params::named([("name", Value::text("Kit"))]),
)
.await?;

Batch Semantics

batch returns per-statement outcomes and does not fail the full request for SQL-level statement errors.

use bunnydb_http::{Statement, StatementOutcome, Value};

let outcomes = db.batch([
    Statement::execute("INSERT INTO users(name) VALUES (?)", [Value::text("A")]),
    Statement::execute("INSER INTO users(name) VALUES (?)", [Value::text("B")]),
    Statement::query("SELECT COUNT(*) FROM users", ()),
]).await?;

for outcome in outcomes {
    match outcome {
        StatementOutcome::Exec(exec) => println!("affected={}", exec.affected_row_count),
        StatementOutcome::Query(query) => println!("rows={}", query.rows.len()),
        StatementOutcome::SqlError { request_index, message, .. } => {
            eprintln!("sql error at {request_index}: {message}");
        }
    }
}

Timeout and Retry

use bunnydb_http::{BunnyDbClient, ClientOptions};

let db = BunnyDbClient::new_bearer(pipeline_url, token).with_options(ClientOptions {
    timeout_ms: 10_000,
    max_retries: 2,
    retry_backoff_ms: 250,
});

Defaults:

  • timeout_ms = 10_000
  • max_retries = 0
  • retry_backoff_ms = 250

Error Model

  • BunnyDbError::Transport(reqwest::Error)
  • BunnyDbError::Http { status, body }
  • BunnyDbError::Pipeline { request_index, message, code }
  • BunnyDbError::Decode(String)

Optional Features

Feature Description
tracing retry/debug tracing hooks
raw-mode experimental raw response types
row-map experimental row mapping helpers
baton-experimental experimental baton/session types

Platform Support

Target Status Notes
x86_64-unknown-linux-gnu Primary target, full tokio
aarch64-unknown-linux-gnu ARM64, Docker, Magic Containers
x86_64-apple-darwin macOS native
wasm32-unknown-unknown Bunny Edge Scripts, browser, Deno

On wasm32-unknown-unknown:

  • reqwest uses the browser fetch API (no TLS layer needed)
  • tokio is not linked — the WASM runtime drives the event loop
  • from_env() / from_env_db_id() are not available (no std::env in browsers)
  • Retry backoff sleep is a no-op — edge functions prefer fast failures
  • BunnyDbClient::new_bearer(), from_db_id(), query, execute, batch work identically

Bunny Edge Scripting & Magic Containers

Option 1 — Magic Container (pure Rust, native binary)

Bunny Magic Containers run a Docker workload co-located with the database — full Rust ecosystem, no WASM needed.

  1. Open the Bunny dashboard → Database → your DB → Access → generate a token.
  2. In your Magic Container environment variables:
BUNNYDB_PIPELINE_URL = https://<your-db-id>.lite.bunnydb.net/v2/pipeline
BUNNYDB_TOKEN        = <your-token>
  1. In your Rust code:
let db = BunnyDbClient::from_env().expect("missing BUNNYDB_* env vars");

Option 2 — Edge Script (Rust → WASM) 🆕

Compile your Rust logic to wasm32-unknown-unknown and deploy it as a Bunny Edge Script. The same BunnyDbClient API, same type safety — running at the CDN edge PoP nearest to your users.

Bunny CDN edge PoP
  └── edge/main.ts           tiny TypeScript host (~30 lines)
        ↕ wasm-bindgen
  └── src/lib.rs             your Rust logic compiled to .wasm
        └── bunnydb-rs       reqwest → browser fetch API
              └── BunnyDB /v2/pipeline

Rust side (src/lib.rs)

use bunnydb_http::{BunnyDbClient, Value};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct EdgeHandler {
    db: BunnyDbClient,
}

#[wasm_bindgen]
impl EdgeHandler {
    #[wasm_bindgen(constructor)]
    pub fn new(pipeline_url: &str, token: &str) -> Self {
        Self { db: BunnyDbClient::new_bearer(pipeline_url, token) }
    }

    /// Query users and return JSON string.
    pub async fn get_users(&self) -> Result<String, String> {
        let result = self.db
            .query("SELECT id, name FROM users ORDER BY id DESC LIMIT 50", ())
            .await
            .map_err(|e| e.to_string())?;

        // Build a JSON array of rows
        let rows: Vec<String> = result.rows.iter().map(|row| {
            let id   = match &row[0] { bunnydb_http::Value::Integer(n) => n.to_string(), v => format!("{v:?}") };
            let name = match &row[1] { bunnydb_http::Value::Text(s) => s.clone(), v => format!("{v:?}") };
            format!(r#"{{"id":{id},"name":"{name}"}}"#)
        }).collect();

        Ok(format!("[{}]", rows.join(",")))
    }

    /// Insert a user and return affected row count.
    pub async fn create_user(&self, name: String, email: String) -> Result<String, String> {
        let result = self.db
            .execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                [Value::text(name), Value::text(email)],
            )
            .await
            .map_err(|e| e.to_string())?;

        Ok(format!(r#"{{"affected":{},"id":{:?}}}"#,
            result.affected_row_count, result.last_insert_rowid))
    }
}

Edge Script host (edge/main.ts)

import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.12.0";
import process from "node:process";
import init, { EdgeHandler } from "./pkg/my_handler.js";  // wasm-pack output

// Load the .wasm binary once at cold start
await init(fetch(process.env.WASM_URL!));

// Create Rust handler — credentials from Bunny env vars
const handler = new EdgeHandler(process.env.DB_URL!, process.env.DB_TOKEN!);

BunnySDK.net.http.serve(async (req: Request): Promise<Response> => {
  const url = new URL(req.url);

  if (req.method === "GET" && url.pathname === "/users") {
    const json = await handler.get_users();
    return new Response(json, { headers: { "Content-Type": "application/json" } });
  }

  if (req.method === "POST" && url.pathname === "/users") {
    const { name, email } = await req.json();
    const result = await handler.create_user(name, email);
    return new Response(result, { status: 201, headers: { "Content-Type": "application/json" } });
  }

  return new Response("not found", { status: 404 });
});

Build & deploy

# 1. Install wasm-pack
cargo install wasm-pack

# 2. Compile Rust → WASM
wasm-pack build --target bundler --release
# → pkg/my_handler_bg.wasm  (~150–250 KB optimized)
# → pkg/my_handler.js       (wasm-bindgen glue)

# 3. Upload .wasm to Bunny Storage
curl -X PUT "https://storage.bunnycdn.com/<zone>/my_handler_bg.wasm" \
  -H "AccessKey: <key>" --data-binary @pkg/my_handler_bg.wasm

# 4. Set env vars in Edge Script dashboard:
#    WASM_URL  = https://your-cdn.b-cdn.net/my_handler_bg.wasm
#    DB_URL    = https://<db-id>.lite.bunnydb.net/v2/pipeline
#    DB_TOKEN  = <your-token>

A complete, ready-to-deploy example is in examples/wasm-edge/.

See docs/edge-scripting.md for the full wire protocol reference, authentication details, and replication notes.

GUI Client (Example)

This repo includes a desktop GUI example built with eframe/egui.

Run it:

cargo run --example gui

The GUI supports:

  • Query / Execute / Batch modes
  • Bearer or raw authorization mode
  • JSON params: [] for positional, {} for named
  • Batch JSON format:
[
  { "kind": "execute", "sql": "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)" },
  { "kind": "execute", "sql": "INSERT INTO users (name) VALUES (?)", "params": ["Kit"] },
  { "kind": "query", "sql": "SELECT id, name FROM users", "params": [] }
]

Testing

Run all tests:

cargo test

Live integration test reads credentials in this order:

  • Environment: BUNNYDB_PIPELINE_URL and BUNNYDB_TOKEN
  • Local file fallback: secrets.json with either BUNNYDB_PIPELINE_URL + BUNNYDB_TOKEN or BUNNY_DATABASE_URL + BUNNY_DATABASE_AUTH_TOKEN

secrets.json is excluded from packaging.

Documentation

Document Description
docs/architecture.md Module map, data flow, design decisions
docs/edge-scripting.md Edge Scripting, Magic Containers, wire protocol reference

MSRV

Rust 1.75

License

MIT

About

Async Rust HTTP client for Bunny.net Database — A fully async Rust client for interacting with the Bunny.net Database SQL Pipeline API. Works both natively (via tokio) and in WebAssembly (e.g., Bunny Edge Scripts), supporting query, execute, batch operations, typed parameters, structured errors, retries, and telemetry.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages