bunnydb-rs 0.3.0

Async HTTP client for Bunny.net Database SQL pipeline API — native and WASM (Bunny Edge Scripts)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# bunnydb-rs

[![crates.io](https://img.shields.io/crates/v/bunnydb-rs.svg)](https://crates.io/crates/bunnydb-rs)
[![docs.rs](https://docs.rs/bunnydb-rs/badge.svg)](https://docs.rs/bunnydb-rs)
[![CI](https://github.com/neuralforgeone/bunnydb-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/neuralforgeone/bunnydb-rs/actions/workflows/ci.yml)
[![WASM](https://img.shields.io/badge/target-wasm32--unknown--unknown-blueviolet)](https://webassembly.org)

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)

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

### WebAssembly (Bunny Edge Script)

```toml
[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 |

```toml
# 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.

```rust
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

```rust
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

```rust
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:

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

Named:

```rust
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.

```rust
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

```rust
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](https://docs.bunny.net/database/connect/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>
```

3. In your Rust code:

```rust
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`)

```rust
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`)

```typescript
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

```bash
# 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/`](examples/wasm-edge/).

See [docs/edge-scripting.md](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:

```bash
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:

```json
[
  { "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:

```bash
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]docs/architecture.md | Module map, data flow, design decisions |
| [docs/edge-scripting.md]docs/edge-scripting.md | Edge Scripting, Magic Containers, wire protocol reference |

## MSRV

Rust `1.75`

## License

MIT