@rip-lang/db 1.3.5 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -60
- package/db.rip +5 -5
- package/lib/duckdb.mjs +15 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# Rip DB - @rip-lang/db
|
|
4
4
|
|
|
5
|
-
> **A lightweight DuckDB HTTP server with the official DuckDB UI built in**
|
|
5
|
+
> **A lightweight DuckDB HTTP server with bulk inserts, an ActiveRecord-style client, and the official DuckDB UI built in**
|
|
6
6
|
|
|
7
7
|
Rip DB turns any DuckDB database into a full-featured HTTP server — complete
|
|
8
|
-
with the official DuckDB UI for interactive queries,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
to power the UI with native-speed data transfer.
|
|
8
|
+
with the official DuckDB UI for interactive queries, bulk insert via DuckDB's
|
|
9
|
+
Appender API (~200K rows/sec), and a clean Model interface that picks the
|
|
10
|
+
optimal strategy automatically. Pure Bun FFI, zero npm dependencies for DuckDB.
|
|
12
11
|
|
|
13
12
|
## Quick Start
|
|
14
13
|
|
|
@@ -23,8 +22,20 @@ rip-db mydata.duckdb # File-based database
|
|
|
23
22
|
rip-db mydata.duckdb --port 8080
|
|
24
23
|
```
|
|
25
24
|
|
|
25
|
+
```
|
|
26
|
+
rip-db: DuckDB v1.4.4
|
|
27
|
+
rip-db: rip-db v1.3.5
|
|
28
|
+
rip-db: source mydata.duckdb
|
|
29
|
+
rip-db: server http://localhost:4213
|
|
30
|
+
```
|
|
31
|
+
|
|
26
32
|
Open **http://localhost:4213** for the official DuckDB UI.
|
|
27
33
|
|
|
34
|
+
The `source` line shows the active data source — today that's a local DuckDB
|
|
35
|
+
file or `:memory:`, but the architecture supports any source DuckDB can attach:
|
|
36
|
+
S3 buckets, PostgreSQL, MySQL, SQLite, Parquet files, CSV, and more via
|
|
37
|
+
DuckDB's extension system.
|
|
38
|
+
|
|
28
39
|
## What It Does
|
|
29
40
|
|
|
30
41
|
Rip DB sits between your clients and DuckDB, providing two interfaces:
|
|
@@ -47,77 +58,119 @@ Rip DB sits between your clients and DuckDB, providing two interfaces:
|
|
|
47
58
|
**DuckDB UI** — The official DuckDB notebook interface loads instantly in your
|
|
48
59
|
browser. Rip DB proxies the UI assets from ui.duckdb.org and implements the
|
|
49
60
|
full binary serialization protocol that the UI uses to communicate with DuckDB.
|
|
50
|
-
This includes query execution, SQL tokenization for syntax highlighting, and
|
|
51
|
-
Server-Sent Events for real-time catalog updates.
|
|
52
61
|
|
|
53
62
|
**JSON API** — Any HTTP client can execute SQL queries and receive JSON
|
|
54
|
-
responses.
|
|
55
|
-
|
|
63
|
+
responses. Three execution strategies are selected automatically based on the
|
|
64
|
+
request shape — the caller never needs to think about it.
|
|
56
65
|
|
|
57
66
|
## Features
|
|
58
67
|
|
|
59
68
|
- **Official DuckDB UI** — Interactive notebooks, syntax highlighting, data exploration
|
|
69
|
+
- **Bulk insert via Appender API** — ~200K rows/sec, bypasses SQL parsing entirely
|
|
70
|
+
- **Batch prepared statements** — Prepare once, execute N times with different params
|
|
71
|
+
- **ActiveRecord-style Model** — `User.find!`, `User.insert!`, `User.where(...).all!`
|
|
72
|
+
- **Smart dispatch** — `Model.insert!` picks Appender for arrays, prepared statements for singles
|
|
60
73
|
- **Full binary protocol** — Native DuckDB UI serialization implemented in Rip
|
|
61
74
|
- **Pure Bun FFI** — Direct calls to DuckDB's C API using the modern chunk-based interface
|
|
62
75
|
- **Zero npm dependencies for DuckDB** — Uses the system-installed DuckDB library
|
|
63
76
|
- **Parameterized queries** — Prepared statements with type-safe parameter binding
|
|
64
77
|
- **Complete type support** — All DuckDB types handled natively, including UUID, DECIMAL, TIMESTAMP, LIST, STRUCT, MAP
|
|
65
|
-
- **DECIMAL precision preserved** — Exact string representation, never converted to floating point
|
|
66
|
-
- **Timestamps as UTC** — All timestamps returned as JavaScript Date objects (UTC)
|
|
67
|
-
- **Powered by @rip-lang/api** — Fast, lightweight HTTP server framework
|
|
68
78
|
- **Single binary** — One `rip-db` command, one process, one database
|
|
69
79
|
|
|
70
|
-
##
|
|
80
|
+
## Database Client
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
The real power of Rip DB is its client library. Import it from
|
|
83
|
+
`@rip-lang/db/client` — it talks to a running `rip-db` server over HTTP.
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
```coffee
|
|
86
|
+
import { query, findOne, findAll, Model } from '@rip-lang/db/client'
|
|
87
|
+
```
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
### The Balance: Model vs Raw SQL
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-H "Content-Type: application/json" \
|
|
81
|
-
-d '{"sql": "SELECT * FROM users WHERE id = $1", "params": [1]}'
|
|
82
|
-
```
|
|
91
|
+
Not every query needs an ORM, and not every query benefits from raw SQL.
|
|
92
|
+
Rip DB gives you both and lets you choose the right tool:
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
**Use the Model** for simple and medium queries — CRUD, where clauses,
|
|
95
|
+
counts, upserts. The Model is shorter, safer, and handles parameterization
|
|
96
|
+
automatically:
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
```coffee
|
|
99
|
+
User = Model 'users'
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
# These are cleaner than raw SQL
|
|
102
|
+
user = User.find! 42
|
|
103
|
+
count = User.count!
|
|
104
|
+
active = User.where(active: true).order('name').limit(10).all!
|
|
105
|
+
created = User.insert! { name: 'Alice', email: 'alice@example.com' }
|
|
106
|
+
User.upsert! { email: 'alice@example.com', name: 'Alice' }, on: 'email'
|
|
90
107
|
```
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
**Use raw SQL** for complex queries — JOINs, GROUP BY, aggregates, subqueries.
|
|
110
|
+
SQL is the most direct, readable expression for these. No ORM improves on it:
|
|
93
111
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
```coffee
|
|
113
|
+
users = findAll! """
|
|
114
|
+
SELECT u.id, u.name, count(o.id) as order_count
|
|
115
|
+
FROM users u
|
|
116
|
+
LEFT JOIN orders o ON o.user_id = u.id
|
|
117
|
+
WHERE u.active = true
|
|
118
|
+
GROUP BY u.id, u.name
|
|
119
|
+
ORDER BY order_count DESC
|
|
120
|
+
"""
|
|
101
121
|
```
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
This isn't a compromise — it's the optimal approach. Simple queries get
|
|
124
|
+
shorter with the Model. Complex queries stay clear with SQL. You never
|
|
125
|
+
fight the abstraction.
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|----------|--------|-------------|
|
|
107
|
-
| `/health` | GET | Health check |
|
|
108
|
-
| `/tables` | GET | List all tables |
|
|
109
|
-
| `/schema/:table` | GET | Table schema |
|
|
127
|
+
### Bulk Insert (Appender API)
|
|
110
128
|
|
|
111
|
-
|
|
129
|
+
Pass an array to `Model.insert!` and it automatically uses DuckDB's Appender
|
|
130
|
+
API — the fastest possible insert path (~200K rows/sec). The Appender bypasses
|
|
131
|
+
SQL parsing entirely, writing directly to DuckDB's columnar storage.
|
|
132
|
+
|
|
133
|
+
```coffee
|
|
134
|
+
# Single insert — uses prepared statement, returns the row
|
|
135
|
+
user = User.insert! { name: 'Alice', email: 'alice@example.com' }
|
|
136
|
+
|
|
137
|
+
# Bulk insert — uses Appender API, fastest path
|
|
138
|
+
User.insert! [
|
|
139
|
+
{ name: 'Alice', email: 'alice@example.com' }
|
|
140
|
+
{ name: 'Bob', email: 'bob@example.com' }
|
|
141
|
+
{ name: 'Charlie', email: 'charlie@example.com' }
|
|
142
|
+
]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The caller writes the same `insert!` — the Model detects the array and
|
|
146
|
+
picks the optimal strategy. Column subsets work too; missing columns get
|
|
147
|
+
their default values.
|
|
112
148
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
### Bulk Upsert (Multi-Row VALUES)
|
|
150
|
+
|
|
151
|
+
For upserts (INSERT ... ON CONFLICT), the Appender can't be used. The Model
|
|
152
|
+
builds a multi-row VALUES statement with proper parameterization:
|
|
116
153
|
|
|
117
154
|
```coffee
|
|
118
|
-
|
|
155
|
+
# Single upsert
|
|
156
|
+
Response.upsert! { email: 'alice@example.com', name: 'Alice' }, on: 'email'
|
|
157
|
+
|
|
158
|
+
# Bulk upsert — one SQL statement with N value tuples
|
|
159
|
+
Response.upsert! responses, on: 'email'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Batch Queries (Prepared Statement Reuse)
|
|
163
|
+
|
|
164
|
+
Pass an array of param arrays to `query!` and it reuses one prepared
|
|
165
|
+
statement for all executions — one prepare, N bind-and-execute cycles:
|
|
119
166
|
|
|
120
|
-
|
|
167
|
+
```coffee
|
|
168
|
+
# Execute the same UPDATE 3 times with different params
|
|
169
|
+
query! "UPDATE reviews SET completed_at = $1 WHERE id = $2", [
|
|
170
|
+
[now, 1]
|
|
171
|
+
[now, 2]
|
|
172
|
+
[now, 3]
|
|
173
|
+
]
|
|
121
174
|
```
|
|
122
175
|
|
|
123
176
|
### Low-Level Queries
|
|
@@ -177,11 +230,9 @@ User.where('age > $1', [21]).all!
|
|
|
177
230
|
|
|
178
231
|
# OR conditions
|
|
179
232
|
User.where(active: true).or(role: 'admin').all!
|
|
180
|
-
User.where(active: true).or('role = $1', ['admin']).all!
|
|
181
233
|
|
|
182
234
|
# NOT conditions
|
|
183
235
|
User.where(active: true).not(role: 'banned').all!
|
|
184
|
-
User.not(deleted_at: null).all! # WHERE "deleted_at" IS NOT NULL
|
|
185
236
|
```
|
|
186
237
|
|
|
187
238
|
#### Group & Having
|
|
@@ -196,13 +247,16 @@ User.group('role').having('count(*) > $1', [5]).select('role, count(*) as n').al
|
|
|
196
247
|
All mutations return the affected row(s) via `RETURNING *`.
|
|
197
248
|
|
|
198
249
|
```coffee
|
|
199
|
-
# Insert — returns the new record
|
|
250
|
+
# Insert single — returns the new record
|
|
200
251
|
user = User.insert! { first_name: 'Alice', email: 'alice@example.com' }
|
|
201
252
|
|
|
253
|
+
# Insert bulk — uses Appender API (~200K rows/sec)
|
|
254
|
+
User.insert! rows
|
|
255
|
+
|
|
202
256
|
# Update by id — returns the updated record
|
|
203
257
|
user = User.update! 42, { email: 'newemail@example.com' }
|
|
204
258
|
|
|
205
|
-
# Upsert — insert or update on conflict
|
|
259
|
+
# Upsert — insert or update on conflict (single or bulk)
|
|
206
260
|
user = User.upsert! { email: 'alice@example.com', name: 'Alice' }, on: 'email'
|
|
207
261
|
|
|
208
262
|
# Destroy by id — returns the deleted record
|
|
@@ -238,6 +292,20 @@ Archive = Model 'orders', 'archive_db'
|
|
|
238
292
|
order = Archive.find! 99 # SELECT * FROM "archive_db"."orders" WHERE id = $1
|
|
239
293
|
```
|
|
240
294
|
|
|
295
|
+
### Execution Strategy Summary
|
|
296
|
+
|
|
297
|
+
The client picks the optimal execution path automatically:
|
|
298
|
+
|
|
299
|
+
| Caller writes | Strategy | Speed |
|
|
300
|
+
|---------------|----------|-------|
|
|
301
|
+
| `Model.insert!(object)` | Prepared statement | Fast |
|
|
302
|
+
| `Model.insert!(array)` | DuckDB Appender API | ~200K rows/sec |
|
|
303
|
+
| `Model.upsert!(object)` | Prepared statement | Fast |
|
|
304
|
+
| `Model.upsert!(array)` | Multi-row VALUES SQL | Fast (batch) |
|
|
305
|
+
| `query!(sql, params)` | Prepared statement | Fast |
|
|
306
|
+
| `query!(sql, [params...])` | Prepared stmt reuse | Fast (batch) |
|
|
307
|
+
| `findOne!(sql)` / `findAll!(sql)` | Direct execution | Fast |
|
|
308
|
+
|
|
241
309
|
### Query Builder Reference
|
|
242
310
|
|
|
243
311
|
| Method | Description |
|
|
@@ -274,12 +342,60 @@ order = Archive.find! 99 # SELECT * FROM "archive_db"."orders" WHERE id = $1
|
|
|
274
342
|
| `Model.order(...)` | Start a chain with ORDER BY |
|
|
275
343
|
| `Model.group(...)` | Start a chain with GROUP BY |
|
|
276
344
|
| `Model.limit(n)` | Start a chain with LIMIT |
|
|
277
|
-
| `Model.insert!(data)` | Insert
|
|
345
|
+
| `Model.insert!(data)` | Insert single object or bulk array |
|
|
278
346
|
| `Model.update!(id, data)` | Update by id and return row |
|
|
279
|
-
| `Model.upsert!(data, on:)` | Insert or update on conflict |
|
|
347
|
+
| `Model.upsert!(data, on:)` | Insert or update on conflict (single or bulk) |
|
|
280
348
|
| `Model.destroy!(id)` | Delete by id and return row |
|
|
281
349
|
| `Model.query!(sql, params)` | Raw parameterized query |
|
|
282
350
|
|
|
351
|
+
## JSON API
|
|
352
|
+
|
|
353
|
+
For programmatic access from any HTTP client.
|
|
354
|
+
|
|
355
|
+
### POST /sql
|
|
356
|
+
|
|
357
|
+
The `/sql` endpoint accepts four shapes and dispatches automatically:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Standard query
|
|
361
|
+
curl -X POST http://localhost:4213/sql \
|
|
362
|
+
-H "Content-Type: application/json" \
|
|
363
|
+
-d '{"sql": "SELECT * FROM users WHERE id = $1", "params": [1]}'
|
|
364
|
+
|
|
365
|
+
# Bulk insert (Appender API)
|
|
366
|
+
curl -X POST http://localhost:4213/sql \
|
|
367
|
+
-H "Content-Type: application/json" \
|
|
368
|
+
-d '{"table": "users", "columns": ["name", "email"], "rows": [["Alice", "a@b.com"], ["Bob", "b@b.com"]]}'
|
|
369
|
+
|
|
370
|
+
# Batch prepared statement
|
|
371
|
+
curl -X POST http://localhost:4213/sql \
|
|
372
|
+
-H "Content-Type: application/json" \
|
|
373
|
+
-d '{"sql": "INSERT INTO t (a, b) VALUES ($1, $2)", "params": [[1, "x"], [2, "y"]]}'
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
| Shape | Dispatches to |
|
|
377
|
+
|-------|---------------|
|
|
378
|
+
| `{ sql }` | Raw execution |
|
|
379
|
+
| `{ sql, params: [...] }` | Prepared statement |
|
|
380
|
+
| `{ sql, params: [[...], ...] }` | Batch prepared (reuse stmt) |
|
|
381
|
+
| `{ table, columns, rows }` | Appender API (fastest insert) |
|
|
382
|
+
|
|
383
|
+
### POST /
|
|
384
|
+
|
|
385
|
+
Execute raw SQL (body is the query):
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
curl -X POST http://localhost:4213/ -d "SELECT 42 as answer"
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Other Endpoints
|
|
392
|
+
|
|
393
|
+
| Endpoint | Method | Description |
|
|
394
|
+
|----------|--------|-------------|
|
|
395
|
+
| `/health` | GET | Health check |
|
|
396
|
+
| `/tables` | GET | List all tables |
|
|
397
|
+
| `/schema/:table` | GET | Table schema |
|
|
398
|
+
|
|
283
399
|
## DuckDB UI
|
|
284
400
|
|
|
285
401
|
The official DuckDB UI is available at the root URL. It provides:
|
|
@@ -299,19 +415,18 @@ Rip DB is built from three files:
|
|
|
299
415
|
|
|
300
416
|
| File | Lines | Role |
|
|
301
417
|
|------|-------|------|
|
|
302
|
-
| `db.rip` | ~
|
|
303
|
-
| `lib/duckdb.mjs` | ~
|
|
418
|
+
| `db.rip` | ~430 | HTTP server — routes, middleware, UI proxy, bulk dispatch |
|
|
419
|
+
| `lib/duckdb.mjs` | ~960 | FFI driver — chunk-based API, Appender, batch prepared |
|
|
304
420
|
| `lib/duckdb-binary.rip` | ~550 | Binary serializer — DuckDB UI protocol |
|
|
421
|
+
| `client.rip` | ~320 | HTTP client — Model factory, query builder, bulk insert |
|
|
305
422
|
|
|
306
423
|
The FFI driver uses DuckDB's modern chunk-based API (`duckdb_fetch_chunk`,
|
|
307
424
|
`duckdb_vector_get_data`) to read query results directly from columnar memory.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
UI extension uses. It handles all DuckDB types including native 16-byte UUID
|
|
314
|
-
serialization, uint64-aligned validity bitmaps, and proper timestamp encoding.
|
|
425
|
+
For bulk inserts, it uses the Appender API (`duckdb_appender_create`,
|
|
426
|
+
`duckdb_append_*`) which writes directly to DuckDB's storage engine, bypassing
|
|
427
|
+
SQL parsing for maximum throughput. Prepared statement reuse
|
|
428
|
+
(`duckdb_prepare` once, `duckdb_execute_prepared` N times) handles batch
|
|
429
|
+
operations efficiently.
|
|
315
430
|
|
|
316
431
|
## Requirements
|
|
317
432
|
|
package/db.rip
CHANGED
|
@@ -102,8 +102,9 @@ port = parseInt(process.env.DB_PORT or portArg) or 4213
|
|
|
102
102
|
# Open database and create persistent connection
|
|
103
103
|
db = open(path)
|
|
104
104
|
conn = db.connect()
|
|
105
|
-
console.log "rip-db:
|
|
106
|
-
console.log "rip-db:
|
|
105
|
+
console.log "rip-db: DuckDB #{duckdbVersion()}"
|
|
106
|
+
console.log "rip-db: rip-db v#{VERSION}"
|
|
107
|
+
console.log "rip-db: source #{path}"
|
|
107
108
|
|
|
108
109
|
# ==============================================================================
|
|
109
110
|
# Helpers
|
|
@@ -415,7 +416,6 @@ get '/*' ->
|
|
|
415
416
|
# Start Server
|
|
416
417
|
# ==============================================================================
|
|
417
418
|
|
|
418
|
-
|
|
419
|
+
console.log "rip-db: server http://localhost:#{port}"
|
|
419
420
|
|
|
420
|
-
|
|
421
|
-
console.log "rip-db: DuckDB UI available at http://localhost:#{port}/"
|
|
421
|
+
start port: port, silent: true
|
package/lib/duckdb.mjs
CHANGED
|
@@ -92,25 +92,25 @@ const lib = dlopen(libPath, {
|
|
|
92
92
|
duckdb_clear_bindings: { args: ['ptr'], returns: 'i32' },
|
|
93
93
|
|
|
94
94
|
// Appender API
|
|
95
|
-
duckdb_appender_create:
|
|
96
|
-
duckdb_appender_error:
|
|
97
|
-
duckdb_appender_flush:
|
|
98
|
-
duckdb_appender_close:
|
|
99
|
-
duckdb_appender_destroy:
|
|
100
|
-
duckdb_appender_end_row:
|
|
101
|
-
duckdb_append_bool:
|
|
102
|
-
duckdb_append_int32:
|
|
103
|
-
duckdb_append_int64:
|
|
104
|
-
duckdb_append_double:
|
|
105
|
-
duckdb_append_varchar:
|
|
106
|
-
duckdb_append_null:
|
|
95
|
+
duckdb_appender_create: { args: ['ptr', 'ptr', 'ptr', 'ptr'], returns: 'i32' },
|
|
96
|
+
duckdb_appender_error: { args: ['ptr'], returns: 'ptr' },
|
|
97
|
+
duckdb_appender_flush: { args: ['ptr'], returns: 'i32' },
|
|
98
|
+
duckdb_appender_close: { args: ['ptr'], returns: 'i32' },
|
|
99
|
+
duckdb_appender_destroy: { args: ['ptr'], returns: 'i32' },
|
|
100
|
+
duckdb_appender_end_row: { args: ['ptr'], returns: 'i32' },
|
|
101
|
+
duckdb_append_bool: { args: ['ptr', 'bool'], returns: 'i32' },
|
|
102
|
+
duckdb_append_int32: { args: ['ptr', 'i32'], returns: 'i32' },
|
|
103
|
+
duckdb_append_int64: { args: ['ptr', 'i64'], returns: 'i32' },
|
|
104
|
+
duckdb_append_double: { args: ['ptr', 'f64'], returns: 'i32' },
|
|
105
|
+
duckdb_append_varchar: { args: ['ptr', 'ptr'], returns: 'i32' },
|
|
106
|
+
duckdb_append_null: { args: ['ptr'], returns: 'i32' },
|
|
107
107
|
duckdb_appender_add_column: { args: ['ptr', 'ptr'], returns: 'i32' },
|
|
108
108
|
duckdb_appender_clear_columns: { args: ['ptr'], returns: 'i32' },
|
|
109
109
|
|
|
110
110
|
// Result inspection
|
|
111
|
-
duckdb_column_count:
|
|
112
|
-
duckdb_column_name:
|
|
113
|
-
duckdb_column_type:
|
|
111
|
+
duckdb_column_count: { args: ['ptr'], returns: 'u64' },
|
|
112
|
+
duckdb_column_name: { args: ['ptr', 'u64'], returns: 'ptr' },
|
|
113
|
+
duckdb_column_type: { args: ['ptr', 'u64'], returns: 'i32' },
|
|
114
114
|
duckdb_result_error: { args: ['ptr'], returns: 'ptr' },
|
|
115
115
|
|
|
116
116
|
// Modern chunk-based API (non-deprecated)
|