@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.
Files changed (4) hide show
  1. package/README.md +175 -60
  2. package/db.rip +5 -5
  3. package/lib/duckdb.mjs +15 -15
  4. 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, notebooks, and data
9
- exploration. It connects to DuckDB via pure Bun FFI (no npm packages, no
10
- native build step) and implements DuckDB's binary serialization protocol
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. Use it from curl, your application code, or any language that
55
- speaks HTTP.
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
- ## JSON API
80
+ ## Database Client
71
81
 
72
- For programmatic access from any HTTP client.
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
- ### POST /sql
85
+ ```coffee
86
+ import { query, findOne, findAll, Model } from '@rip-lang/db/client'
87
+ ```
75
88
 
76
- Execute SQL with optional parameters:
89
+ ### The Balance: Model vs Raw SQL
77
90
 
78
- ```bash
79
- curl -X POST http://localhost:4213/sql \
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
- ### POST /
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
- Execute raw SQL (body is the query):
98
+ ```coffee
99
+ User = Model 'users'
87
100
 
88
- ```bash
89
- curl -X POST http://localhost:4213/ -d "SELECT 42 as answer"
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
- Response format:
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
- ```json
95
- {
96
- "meta": [{"name": "answer", "type": "INTEGER"}],
97
- "data": [[42]],
98
- "rows": 1,
99
- "time": 0.001
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
- ### Other Endpoints
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
- | Endpoint | Method | Description |
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
- ## Database Client
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
- Rip DB includes an ActiveRecord-style database client for use in Rip
114
- applications. Import it from `@rip-lang/db/client` — it talks to a running
115
- `rip-db` server over HTTP with parameterized queries.
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
- import { connect, query, findOne, findAll, Model } from '@rip-lang/db/client'
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
- connect 'http://localhost:4213' # optional — defaults to DB_URL env or localhost:4213
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 and return new row |
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` | ~390 | HTTP server — routes, middleware, UI proxy |
303
- | `lib/duckdb.mjs` | ~800 | FFI driver — modern chunk-based DuckDB C API |
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
- No deprecated per-value functions, no intermediate copies. For complex types
309
- like DECIMAL, ENUM, LIST, and STRUCT, it uses DuckDB's logical type
310
- introspection to read values with full fidelity.
311
-
312
- The binary serializer implements the same wire protocol that DuckDB's official
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: database=#{path} (bun-ffi)"
106
- console.log "rip-db: DuckDB version=#{duckdbVersion()}"
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
- start port: port
419
+ console.log "rip-db: server http://localhost:#{port}"
419
420
 
420
- console.log "rip-db: listening on http://localhost:#{port}"
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: { 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' },
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: { args: ['ptr'], returns: 'u64' },
112
- duckdb_column_name: { args: ['ptr', 'u64'], returns: 'ptr' },
113
- duckdb_column_type: { args: ['ptr', 'u64'], returns: 'i32' },
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/db",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "DuckDB server with official DuckDB UI — pure Bun FFI",
5
5
  "type": "module",
6
6
  "main": "db.rip",