@rip-lang/db 0.7.0
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 +293 -0
- package/bin/rip-db +17 -0
- package/db.html +76 -0
- package/db.rip +223 -0
- package/lib/darwin-arm64/duckdb.node +0 -0
- package/lib/duckdb.mjs +412 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
|
|
2
|
+
|
|
3
|
+
# Rip DB - @rip-lang/db
|
|
4
|
+
|
|
5
|
+
> **Simple RestFUL HTTP server for concurrent DuckDB read and write queries**
|
|
6
|
+
|
|
7
|
+
Uses custom Zig-based DuckDB bindings (`lib/`) for fast, type-safe database access.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install globally
|
|
13
|
+
bun add -g @rip-lang/db
|
|
14
|
+
|
|
15
|
+
# Run with in-memory database
|
|
16
|
+
rip-db
|
|
17
|
+
|
|
18
|
+
# Run with file-based database
|
|
19
|
+
rip-db mydb.duckdb
|
|
20
|
+
|
|
21
|
+
# Specify port
|
|
22
|
+
rip-db mydb.duckdb --port=8080
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or run directly with Rip:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rip db.rip :memory: --port=4000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API Endpoints
|
|
32
|
+
|
|
33
|
+
### POST /sql
|
|
34
|
+
|
|
35
|
+
Execute any SQL statement (queries or mutations) with JSON body.
|
|
36
|
+
|
|
37
|
+
**Request:**
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"sql": "SELECT * FROM users WHERE id = ?",
|
|
41
|
+
"params": [1]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Response (JSONCompact format):**
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"meta": [
|
|
49
|
+
{"name": "id", "type": "INTEGER"},
|
|
50
|
+
{"name": "name", "type": "VARCHAR"},
|
|
51
|
+
{"name": "email", "type": "VARCHAR"}
|
|
52
|
+
],
|
|
53
|
+
"data": [
|
|
54
|
+
[1, "Alice", "alice@example.com"]
|
|
55
|
+
],
|
|
56
|
+
"rows": 1,
|
|
57
|
+
"time": 0.001
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### POST /
|
|
62
|
+
|
|
63
|
+
Execute SQL with raw body (duck-ui compatible). Used by [duck-ui](https://demo.duckui.com).
|
|
64
|
+
|
|
65
|
+
**Request:**
|
|
66
|
+
```
|
|
67
|
+
POST / HTTP/1.1
|
|
68
|
+
Content-Type: application/x-www-form-urlencoded
|
|
69
|
+
|
|
70
|
+
SELECT * FROM users WHERE id = 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Response:** Same JSONCompact format as `/sql`.
|
|
74
|
+
|
|
75
|
+
### GET /health
|
|
76
|
+
|
|
77
|
+
Simple health check (no database query).
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "ok": true }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### GET /status
|
|
84
|
+
|
|
85
|
+
Database info including table list.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"ok": true,
|
|
90
|
+
"database": "mydb.duckdb",
|
|
91
|
+
"tables": ["users", "orders", "products"],
|
|
92
|
+
"time": "2026-02-02T14:30:00.000Z"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### GET /tables
|
|
97
|
+
|
|
98
|
+
List all tables in the database.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"ok": true,
|
|
103
|
+
"tables": ["users", "orders", "products"]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### GET /schema/:table
|
|
108
|
+
|
|
109
|
+
Get schema for a specific table.
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"ok": true,
|
|
114
|
+
"table": "users",
|
|
115
|
+
"columns": [
|
|
116
|
+
{ "column_name": "id", "data_type": "INTEGER", "is_nullable": "NO" },
|
|
117
|
+
{ "column_name": "name", "data_type": "VARCHAR", "is_nullable": "YES" },
|
|
118
|
+
{ "column_name": "email", "data_type": "VARCHAR", "is_nullable": "YES" }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### GET /ui
|
|
124
|
+
|
|
125
|
+
Built-in SQL console. Open in browser:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
http://localhost:4000/ui
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
### Create a table
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
curl -X POST http://localhost:4000/sql \
|
|
137
|
+
-H "Content-Type: application/json" \
|
|
138
|
+
-d '{"sql": "CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR, email VARCHAR)"}'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Insert data
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
curl -X POST http://localhost:4000/sql \
|
|
145
|
+
-H "Content-Type: application/json" \
|
|
146
|
+
-d '{"sql": "INSERT INTO users VALUES (?, ?, ?)", "params": [1, "Alice", "alice@example.com"]}'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Query with parameters
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
curl -X POST http://localhost:4000/sql \
|
|
153
|
+
-H "Content-Type: application/json" \
|
|
154
|
+
-d '{"sql": "SELECT * FROM users WHERE name LIKE ?", "params": ["%Ali%"]}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Aggregations
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
curl -X POST http://localhost:4000/sql \
|
|
161
|
+
-H "Content-Type: application/json" \
|
|
162
|
+
-d '{"sql": "SELECT COUNT(*) as total, AVG(age) as avg_age FROM users"}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Response Format
|
|
166
|
+
|
|
167
|
+
All responses use JSONCompact format (compatible with DuckDB HTTP Server and duck-ui):
|
|
168
|
+
|
|
169
|
+
| Field | Type | Description |
|
|
170
|
+
|-------|------|-------------|
|
|
171
|
+
| `meta` | object[] | Column metadata: `[{name, type}, ...]` |
|
|
172
|
+
| `data` | any[][] | Row data as arrays |
|
|
173
|
+
| `rows` | number | Number of rows returned |
|
|
174
|
+
| `time` | number | Execution time in seconds |
|
|
175
|
+
| `error` | string | Error message (on failure) |
|
|
176
|
+
|
|
177
|
+
**Success response:**
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"meta": [{"name": "id", "type": "INTEGER"}, {"name": "name", "type": "VARCHAR"}],
|
|
181
|
+
"data": [[1, "Alice"], [2, "Bob"]],
|
|
182
|
+
"rows": 2,
|
|
183
|
+
"time": 0.001
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Error response:**
|
|
188
|
+
```json
|
|
189
|
+
{"error": "Table 'foo' does not exist"}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This format is compatible with [duck-ui](https://demo.duckui.com) and other DuckDB HTTP clients.
|
|
193
|
+
|
|
194
|
+
## Performance
|
|
195
|
+
|
|
196
|
+
Our custom Zig-based DuckDB bindings deliver exceptional performance:
|
|
197
|
+
|
|
198
|
+
| Operation | Latency | Throughput |
|
|
199
|
+
|-----------|---------|------------|
|
|
200
|
+
| Point lookup (WHERE id=?) | 0.09ms | 11,000 qps |
|
|
201
|
+
| Range scan (LIMIT 100) | 0.20ms | 5,000 qps |
|
|
202
|
+
| Aggregation (COUNT/AVG/MAX) | 0.12ms | 8,400 qps |
|
|
203
|
+
| JOIN + GROUP BY | 0.25ms | 4,000 qps |
|
|
204
|
+
| INSERT (single row) | 0.13ms | 7,700 qps |
|
|
205
|
+
|
|
206
|
+
### Comparison to MySQL/PostgreSQL
|
|
207
|
+
|
|
208
|
+
| Operation | Our DuckDB | MySQL/PG (localhost) | Speedup |
|
|
209
|
+
|-----------|------------|----------------------|---------|
|
|
210
|
+
| Point lookup | **0.09ms** | 0.3-1ms | 3-10x faster |
|
|
211
|
+
| Range scan | **0.20ms** | 1-5ms | 5-25x faster |
|
|
212
|
+
| Aggregation | **0.12ms** | 2-20ms | 17-170x faster |
|
|
213
|
+
| JOIN + GROUP BY | **0.25ms** | 5-50ms | 20-200x faster |
|
|
214
|
+
| INSERT | **0.13ms** | 0.5-2ms | 4-15x faster |
|
|
215
|
+
|
|
216
|
+
**Why so fast?**
|
|
217
|
+
- **Zero network latency** — DuckDB runs in-process
|
|
218
|
+
- **No connection overhead** — No auth, handshake, or protocol parsing
|
|
219
|
+
- **Columnar engine** — DuckDB is optimized for analytical queries
|
|
220
|
+
- **Direct FFI** — Zig bindings call DuckDB's C API directly
|
|
221
|
+
|
|
222
|
+
## Architecture & Concurrency
|
|
223
|
+
|
|
224
|
+
### Single Process, High Concurrency
|
|
225
|
+
|
|
226
|
+
DuckDB only allows **one process** to access a database file at a time (for write access). This means rip-db runs as a **single process** — it does NOT use rip-server's multi-worker model.
|
|
227
|
+
|
|
228
|
+
However, rip-db handles **high concurrency** through two mechanisms:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
┌─────────────────────────────────────────────────────────┐
|
|
232
|
+
│ rip-db (single process) │
|
|
233
|
+
├─────────────────────────────────────────────────────────┤
|
|
234
|
+
│ Bun HTTP Server (async I/O) │
|
|
235
|
+
│ ├─ Request 1 ──┐ │
|
|
236
|
+
│ ├─ Request 2 ──┼── thousands of concurrent │
|
|
237
|
+
│ ├─ Request 3 ──┤ connections via event loop │
|
|
238
|
+
│ └─ Request N ──┘ │
|
|
239
|
+
├─────────────────────────────────────────────────────────┤
|
|
240
|
+
│ rip-api (routes, middleware) │
|
|
241
|
+
├─────────────────────────────────────────────────────────┤
|
|
242
|
+
│ DuckDB (multi-threaded query engine) │
|
|
243
|
+
│ └─ Queries parallelized across CPU cores │
|
|
244
|
+
└─────────────────────────────────────────────────────────┘
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Layer 1: Bun's Async I/O**
|
|
248
|
+
- Single JavaScript thread with event loop
|
|
249
|
+
- Handles thousands of concurrent HTTP connections
|
|
250
|
+
- Non-blocking I/O — while one request waits on DuckDB, others are processed
|
|
251
|
+
- No thread-per-request overhead
|
|
252
|
+
|
|
253
|
+
**Layer 2: DuckDB's Multi-Threading**
|
|
254
|
+
- Queries are parallelized across all CPU cores internally
|
|
255
|
+
- MVCC for concurrent reads
|
|
256
|
+
- Writes serialized automatically by DuckDB
|
|
257
|
+
|
|
258
|
+
### Why Not Multi-Process?
|
|
259
|
+
|
|
260
|
+
| Approach | Works with DuckDB? | Why |
|
|
261
|
+
|----------|-------------------|-----|
|
|
262
|
+
| Multi-process (rip-server) | ❌ No | DuckDB only allows one process with write access |
|
|
263
|
+
| Multi-threaded | ✅ Yes | DuckDB handles this internally |
|
|
264
|
+
| Async I/O | ✅ Yes | Bun's event loop handles concurrent connections |
|
|
265
|
+
|
|
266
|
+
### The Result
|
|
267
|
+
|
|
268
|
+
Even as a single process, rip-db can handle:
|
|
269
|
+
- **Thousands of concurrent connections** (Bun async I/O)
|
|
270
|
+
- **Parallel query execution** (DuckDB multi-threading)
|
|
271
|
+
- **High throughput** (11,000+ queries/sec on typical hardware)
|
|
272
|
+
|
|
273
|
+
This is the correct architecture for DuckDB — single process, high concurrency.
|
|
274
|
+
|
|
275
|
+
## Requirements
|
|
276
|
+
|
|
277
|
+
- Bun 1.0+
|
|
278
|
+
- rip-lang 2.0+
|
|
279
|
+
- @rip-lang/api 0.5+
|
|
280
|
+
|
|
281
|
+
## Building from Source
|
|
282
|
+
|
|
283
|
+
To rebuild the native DuckDB bindings (requires Zig 0.15+ and DuckDB):
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
./src/build.sh
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
This creates `lib/{platform}-{arch}/duckdb.node` for your platform.
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
package/bin/rip-db
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const dbRip = join(__dirname, '..', 'db.rip');
|
|
11
|
+
const args = process.argv.slice(2).join(' ');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
execSync(`rip ${dbRip} ${args}`, { stdio: 'inherit' });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
process.exit(error.status || 1);
|
|
17
|
+
}
|
package/db.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>DuckDB Console</title>
|
|
6
|
+
<style>
|
|
7
|
+
* { box-sizing: border-box; }
|
|
8
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #1a1a2e; color: #eee; }
|
|
9
|
+
h1 { margin: 0 0 20px; font-size: 24px; color: #fff; }
|
|
10
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
11
|
+
textarea { width: 100%; height: 120px; padding: 12px; font-family: 'Monaco', 'Menlo', monospace; font-size: 14px; background: #16213e; color: #0f0; border: 1px solid #0f4c75; border-radius: 6px; resize: vertical; }
|
|
12
|
+
button { padding: 10px 24px; font-size: 14px; background: #0f4c75; color: #fff; border: none; border-radius: 6px; cursor: pointer; margin: 10px 10px 10px 0; }
|
|
13
|
+
button:hover { background: #1b6ca8; }
|
|
14
|
+
.info { font-size: 12px; color: #888; margin-bottom: 10px; }
|
|
15
|
+
.results { margin-top: 20px; }
|
|
16
|
+
table { width: 100%; border-collapse: collapse; background: #16213e; border-radius: 6px; overflow: hidden; }
|
|
17
|
+
th { background: #0f4c75; padding: 10px; text-align: left; font-weight: 600; }
|
|
18
|
+
td { padding: 8px 10px; border-bottom: 1px solid #1b2838; }
|
|
19
|
+
tr:hover td { background: #1b2838; }
|
|
20
|
+
.error { color: #ff6b6b; background: #2d1f1f; padding: 12px; border-radius: 6px; }
|
|
21
|
+
.meta { color: #888; font-size: 12px; margin-top: 10px; }
|
|
22
|
+
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; }
|
|
23
|
+
.status.ok { background: #1d4d1d; color: #4ade80; }
|
|
24
|
+
.status.err { background: #4d1d1d; color: #ff6b6b; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div class="container">
|
|
29
|
+
<h1>🦆 DuckDB Console</h1>
|
|
30
|
+
<div class="info">Connected to: <span id="db">loading...</span></div>
|
|
31
|
+
<textarea id="sql" placeholder="SELECT * FROM users LIMIT 10;">SELECT * FROM users;</textarea>
|
|
32
|
+
<div>
|
|
33
|
+
<button onclick="runQuery()">▶ Run (Cmd+Enter)</button>
|
|
34
|
+
<button onclick="showTables()">📋 Tables</button>
|
|
35
|
+
<button onclick="showStatus()">ℹ️ Status</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="results" class="results"></div>
|
|
38
|
+
</div>
|
|
39
|
+
<script>
|
|
40
|
+
fetch('/status').then(r => r.json()).then(d => document.getElementById('db').textContent = d.database);
|
|
41
|
+
document.getElementById('sql').addEventListener('keydown', e => {
|
|
42
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') runQuery();
|
|
43
|
+
});
|
|
44
|
+
async function runQuery() {
|
|
45
|
+
const sql = document.getElementById('sql').value.trim();
|
|
46
|
+
if (!sql) return;
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/sql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sql }) });
|
|
49
|
+
renderResult(await res.json());
|
|
50
|
+
} catch (e) { renderError(e.message); }
|
|
51
|
+
}
|
|
52
|
+
async function showTables() {
|
|
53
|
+
document.getElementById('sql').value = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main';";
|
|
54
|
+
runQuery();
|
|
55
|
+
}
|
|
56
|
+
async function showStatus() {
|
|
57
|
+
try {
|
|
58
|
+
const data = await (await fetch('/status')).json();
|
|
59
|
+
document.getElementById('results').innerHTML = '<div style="background:#16213e;padding:16px;border-radius:6px;"><div><strong>Database:</strong> ' + data.database + '</div><div><strong>Tables:</strong> ' + (data.tables?.join(', ') || 'none') + '</div><div><strong>Time:</strong> ' + data.time + '</div></div>';
|
|
60
|
+
} catch (e) { renderError(e.message); }
|
|
61
|
+
}
|
|
62
|
+
function renderResult(data) {
|
|
63
|
+
if (data.error) { renderError(data.error); return; }
|
|
64
|
+
if (!data.meta?.length) { document.getElementById('results').innerHTML = '<div class="meta"><span class="status ok">✓ OK</span> Query executed in ' + data.time + 's</div>'; return; }
|
|
65
|
+
let html = '<table><tr>';
|
|
66
|
+
data.meta.forEach(col => { html += '<th>' + col.name + '<br><span style="font-weight:normal;font-size:11px;color:#888">' + col.type + '</span></th>'; });
|
|
67
|
+
html += '</tr>';
|
|
68
|
+
data.data.forEach(row => { html += '<tr>'; row.forEach(val => { html += '<td>' + (val === null ? '<span style="color:#666">NULL</span>' : val) + '</td>'; }); html += '</tr>'; });
|
|
69
|
+
html += '</table><div class="meta"><span class="status ok">✓ OK</span> ' + data.rows + ' row' + (data.rows !== 1 ? 's' : '') + ' in ' + data.time + 's</div>';
|
|
70
|
+
document.getElementById('results').innerHTML = html;
|
|
71
|
+
}
|
|
72
|
+
function renderError(msg) { document.getElementById('results').innerHTML = '<div class="error"><span class="status err">✗ Error</span> ' + msg + '</div>'; }
|
|
73
|
+
runQuery();
|
|
74
|
+
</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
package/db.rip
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# @rip-lang/db — DuckDB Server
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
#
|
|
5
|
+
# A simple HTTP server for DuckDB queries. One server per database.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# rip db.rip <database.duckdb> [--port 4000]
|
|
9
|
+
# rip db.rip :memory: --port 4000
|
|
10
|
+
#
|
|
11
|
+
# Endpoints:
|
|
12
|
+
# POST /sql - Execute SQL (query or mutation)
|
|
13
|
+
# GET /health - Health check (always ok)
|
|
14
|
+
# GET /status - Database info and table list
|
|
15
|
+
# GET /tables - List all tables
|
|
16
|
+
# GET /schema/:table - Get table schema
|
|
17
|
+
#
|
|
18
|
+
# ==============================================================================
|
|
19
|
+
|
|
20
|
+
import { get, post, raw, read, start, use } from '@rip-lang/api'
|
|
21
|
+
import { cors } from '@rip-lang/api/middleware'
|
|
22
|
+
import { open } from './lib/duckdb.mjs'
|
|
23
|
+
import { version as VERSION } from './package.json'
|
|
24
|
+
|
|
25
|
+
# Enable CORS for duck-ui and other clients
|
|
26
|
+
use cors preflight: true
|
|
27
|
+
|
|
28
|
+
# Fix duck-ui's wrong content-type (sends raw SQL as form-urlencoded)
|
|
29
|
+
raw (req) ->
|
|
30
|
+
if req.headers.get('format') is 'JSONCompact'
|
|
31
|
+
req.headers.set 'content-type', 'text/plain'
|
|
32
|
+
|
|
33
|
+
# ==============================================================================
|
|
34
|
+
# Configuration
|
|
35
|
+
# ==============================================================================
|
|
36
|
+
|
|
37
|
+
# Parse command line arguments
|
|
38
|
+
args = process.argv.slice(2)
|
|
39
|
+
|
|
40
|
+
# Handle --help and --version
|
|
41
|
+
if '--help' in args or '-h' in args
|
|
42
|
+
console.log """
|
|
43
|
+
rip-db v#{VERSION} — DuckDB Server (Bun-native)
|
|
44
|
+
|
|
45
|
+
Usage: rip db.rip [database] [options]
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
database Path to DuckDB file (default: :memory:)
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--port=N Port to listen on (default: 4000)
|
|
52
|
+
--help Show this help
|
|
53
|
+
--version Show version
|
|
54
|
+
|
|
55
|
+
Environment variables (for rip-server):
|
|
56
|
+
DB_PATH Path to DuckDB file
|
|
57
|
+
DB_PORT Port to listen on
|
|
58
|
+
|
|
59
|
+
Endpoints:
|
|
60
|
+
POST /sql Execute SQL query or statement
|
|
61
|
+
GET /health Health check (always ok)
|
|
62
|
+
GET /status Database info and table list
|
|
63
|
+
GET /tables List all tables
|
|
64
|
+
GET /schema/:t Get schema for table t
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
rip db.rip # In-memory database on port 4000
|
|
68
|
+
rip db.rip mydb.duckdb # File-based database
|
|
69
|
+
rip db.rip :memory: --port=8080
|
|
70
|
+
|
|
71
|
+
# With rip-server (hot-reloading):
|
|
72
|
+
DB_PATH=mydb.duckdb rip-server http db.rip
|
|
73
|
+
"""
|
|
74
|
+
process.exit(0)
|
|
75
|
+
|
|
76
|
+
if '--version' in args or '-v' in args
|
|
77
|
+
console.log "rip-db v#{VERSION}"
|
|
78
|
+
process.exit(0)
|
|
79
|
+
|
|
80
|
+
# Support both env vars (for rip-server) and CLI args (for rip db.rip)
|
|
81
|
+
path = process.env.DB_PATH or args.find((a) -> not a.startsWith('-')) or ':memory:'
|
|
82
|
+
port = parseInt(process.env.DB_PORT or (args.find((a) -> a.startsWith('--port=')))?.split('=')[1]) or 4000
|
|
83
|
+
|
|
84
|
+
# Open database
|
|
85
|
+
db = open(path)
|
|
86
|
+
console.log "rip-db: database=#{path} (bun-native)"
|
|
87
|
+
|
|
88
|
+
# ==============================================================================
|
|
89
|
+
# Helpers
|
|
90
|
+
# ==============================================================================
|
|
91
|
+
|
|
92
|
+
# Extract column info from result
|
|
93
|
+
getColumnInfo = (rows) ->
|
|
94
|
+
return { columns: [], types: [] } unless rows?.length > 0
|
|
95
|
+
first = rows[0]
|
|
96
|
+
columns = Object.keys(first)
|
|
97
|
+
types = columns.map (col) ->
|
|
98
|
+
switch typeof val = first[col]
|
|
99
|
+
when 'number' then Number.isInteger(val) ? 'INTEGER' : 'DOUBLE'
|
|
100
|
+
when 'string' then 'VARCHAR'
|
|
101
|
+
when 'boolean' then 'BOOLEAN'
|
|
102
|
+
when 'bigint' then 'BIGINT'
|
|
103
|
+
else val is null ? 'NULL' : 'UNKNOWN'
|
|
104
|
+
{ columns, types }
|
|
105
|
+
|
|
106
|
+
# Convert row objects to arrays for compact format
|
|
107
|
+
rowsToArrays = (rows, columns) ->
|
|
108
|
+
rows.map (row) -> columns.map (col) -> row[col]
|
|
109
|
+
|
|
110
|
+
# Check if SQL is a SELECT-type query
|
|
111
|
+
isSelectQuery = (sql) ->
|
|
112
|
+
upper = sql.trim().toUpperCase()
|
|
113
|
+
upper.startsWith('SELECT') or
|
|
114
|
+
upper.startsWith('WITH') or
|
|
115
|
+
upper.startsWith('SHOW') or
|
|
116
|
+
upper.startsWith('DESCRIBE') or
|
|
117
|
+
upper.startsWith('PRAGMA')
|
|
118
|
+
|
|
119
|
+
# Execute SQL and return JSONCompact result
|
|
120
|
+
executeSQL = (sql, params = []) ->
|
|
121
|
+
startTime = Date.now()
|
|
122
|
+
conn = db.connect()
|
|
123
|
+
|
|
124
|
+
try
|
|
125
|
+
rows = if params.length > 0
|
|
126
|
+
stmt = conn.prepare(sql)
|
|
127
|
+
stmt.query(...params)
|
|
128
|
+
else
|
|
129
|
+
conn.query(sql)
|
|
130
|
+
|
|
131
|
+
if isSelectQuery(sql)
|
|
132
|
+
{ columns, types } = getColumnInfo(rows)
|
|
133
|
+
data = rowsToArrays(rows, columns)
|
|
134
|
+
{
|
|
135
|
+
meta: columns.map((name, i) -> { name, type: types[i] })
|
|
136
|
+
data: data
|
|
137
|
+
rows: data.length
|
|
138
|
+
time: (Date.now() - startTime) / 1000
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
{ meta: [], data: [], rows: 0, time: (Date.now() - startTime) / 1000 }
|
|
142
|
+
catch err
|
|
143
|
+
{ error: err.message }
|
|
144
|
+
finally
|
|
145
|
+
conn.close()
|
|
146
|
+
|
|
147
|
+
# ==============================================================================
|
|
148
|
+
# Endpoints
|
|
149
|
+
# ==============================================================================
|
|
150
|
+
|
|
151
|
+
# POST / — duck-ui compatible (raw SQL in body)
|
|
152
|
+
post '/', ->
|
|
153
|
+
sql = read().body
|
|
154
|
+
console.log "POST /", sql
|
|
155
|
+
return { error: 'Empty query' } unless sql?.trim()
|
|
156
|
+
executeSQL sql
|
|
157
|
+
|
|
158
|
+
# POST /sql — JSON body with optional params
|
|
159
|
+
post '/sql', ->
|
|
160
|
+
{ sql, params } = read()
|
|
161
|
+
console.log "POST /sql", sql, params or []
|
|
162
|
+
return { error: 'Missing required field: sql' } unless sql
|
|
163
|
+
executeSQL sql, params or []
|
|
164
|
+
|
|
165
|
+
# GET /health — Simple health check (no DB query)
|
|
166
|
+
get '/health', ->
|
|
167
|
+
{ ok: true }
|
|
168
|
+
|
|
169
|
+
# GET /status — Database info and table list
|
|
170
|
+
get '/status', ->
|
|
171
|
+
conn = db.connect()
|
|
172
|
+
try
|
|
173
|
+
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'")
|
|
174
|
+
{
|
|
175
|
+
ok: true
|
|
176
|
+
database: path
|
|
177
|
+
tables: tables.map((t) -> t.table_name)
|
|
178
|
+
time: new Date().toISOString()
|
|
179
|
+
}
|
|
180
|
+
catch err
|
|
181
|
+
{ ok: false, error: err.message }
|
|
182
|
+
finally
|
|
183
|
+
conn.close()
|
|
184
|
+
|
|
185
|
+
# GET /tables — List all tables
|
|
186
|
+
get '/tables', ->
|
|
187
|
+
conn = db.connect()
|
|
188
|
+
try
|
|
189
|
+
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name")
|
|
190
|
+
{ ok: true, tables: tables.map((t) -> t.table_name) }
|
|
191
|
+
catch err
|
|
192
|
+
{ ok: false, error: err.message }
|
|
193
|
+
finally
|
|
194
|
+
conn.close()
|
|
195
|
+
|
|
196
|
+
# GET /schema/:table — Get table schema
|
|
197
|
+
get '/schema/:table', ->
|
|
198
|
+
table = read 'table', 'string!'
|
|
199
|
+
conn = db.connect()
|
|
200
|
+
try
|
|
201
|
+
stmt = conn.prepare("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position")
|
|
202
|
+
columns = stmt.query(table)
|
|
203
|
+
|
|
204
|
+
if columns.length is 0
|
|
205
|
+
{ ok: false, error: "Table '#{table}' not found" }
|
|
206
|
+
else
|
|
207
|
+
{ ok: true, table: table, columns: columns }
|
|
208
|
+
catch err
|
|
209
|
+
{ ok: false, error: err.message }
|
|
210
|
+
finally
|
|
211
|
+
conn.close()
|
|
212
|
+
|
|
213
|
+
# GET /ui — Built-in SQL console
|
|
214
|
+
get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
|
|
215
|
+
|
|
216
|
+
# ==============================================================================
|
|
217
|
+
# Start Server
|
|
218
|
+
# ==============================================================================
|
|
219
|
+
|
|
220
|
+
start port: port
|
|
221
|
+
|
|
222
|
+
console.log "rip-db: listening on http://localhost:#{port}"
|
|
223
|
+
console.log "rip-db: POST /sql, GET /health, GET /status, GET /tables, GET /schema/:table, GET /ui"
|
|
Binary file
|
package/lib/duckdb.mjs
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDB bindings for Bun using Zig FFI
|
|
3
|
+
*
|
|
4
|
+
* Clean, type-safe wrapper with full type support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dlopen, ptr, CString } from 'bun:ffi';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { platform, arch } from 'process';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Map Node.js arch names
|
|
15
|
+
const archMap = { 'arm64': 'arm64', 'x64': 'x64', 'x86_64': 'x64' };
|
|
16
|
+
const target = `${platform}-${archMap[arch] || arch}`;
|
|
17
|
+
|
|
18
|
+
// Load the platform-specific native module
|
|
19
|
+
const libPath = join(__dirname, `${target}/duckdb.node`);
|
|
20
|
+
|
|
21
|
+
const duck = dlopen(libPath, {
|
|
22
|
+
// Database operations (usize = pointer-sized integer)
|
|
23
|
+
duck_open: { args: ['ptr'], returns: 'usize' },
|
|
24
|
+
duck_close: { args: ['usize'], returns: 'void' },
|
|
25
|
+
duck_connect: { args: ['usize'], returns: 'usize' },
|
|
26
|
+
duck_disconnect: { args: ['usize'], returns: 'void' },
|
|
27
|
+
|
|
28
|
+
// Query operations
|
|
29
|
+
duck_query: { args: ['usize', 'ptr'], returns: 'usize' },
|
|
30
|
+
duck_free_result: { args: ['usize'], returns: 'void' },
|
|
31
|
+
duck_result_error: { args: ['usize'], returns: 'ptr' },
|
|
32
|
+
duck_row_count: { args: ['usize'], returns: 'u64' },
|
|
33
|
+
duck_column_count: { args: ['usize'], returns: 'u64' },
|
|
34
|
+
duck_column_name: { args: ['usize', 'u64'], returns: 'ptr' },
|
|
35
|
+
duck_column_type: { args: ['usize', 'u64'], returns: 'u32' },
|
|
36
|
+
|
|
37
|
+
// Value extraction
|
|
38
|
+
duck_value_is_null: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
|
|
39
|
+
duck_value_bool: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
|
|
40
|
+
duck_value_i8: { args: ['usize', 'u64', 'u64'], returns: 'i8' },
|
|
41
|
+
duck_value_i16: { args: ['usize', 'u64', 'u64'], returns: 'i16' },
|
|
42
|
+
duck_value_i32: { args: ['usize', 'u64', 'u64'], returns: 'i32' },
|
|
43
|
+
duck_value_i64: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
|
|
44
|
+
duck_value_u8: { args: ['usize', 'u64', 'u64'], returns: 'u8' },
|
|
45
|
+
duck_value_u16: { args: ['usize', 'u64', 'u64'], returns: 'u16' },
|
|
46
|
+
duck_value_u32: { args: ['usize', 'u64', 'u64'], returns: 'u32' },
|
|
47
|
+
duck_value_u64: { args: ['usize', 'u64', 'u64'], returns: 'u64' },
|
|
48
|
+
duck_value_f32: { args: ['usize', 'u64', 'u64'], returns: 'f32' },
|
|
49
|
+
duck_value_f64: { args: ['usize', 'u64', 'u64'], returns: 'f64' },
|
|
50
|
+
duck_value_string_internal: { args: ['usize', 'u64', 'u64'], returns: 'ptr' },
|
|
51
|
+
duck_value_date_days: { args: ['usize', 'u64', 'u64'], returns: 'i32' },
|
|
52
|
+
duck_value_time_micros: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
|
|
53
|
+
duck_value_timestamp_micros: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
|
|
54
|
+
|
|
55
|
+
// Prepared statements
|
|
56
|
+
duck_prepare: { args: ['usize', 'ptr'], returns: 'usize' },
|
|
57
|
+
duck_prepare_error: { args: ['usize'], returns: 'ptr' },
|
|
58
|
+
duck_free_prepare: { args: ['usize'], returns: 'void' },
|
|
59
|
+
duck_nparams: { args: ['usize'], returns: 'u64' },
|
|
60
|
+
duck_param_type: { args: ['usize', 'u64'], returns: 'u32' },
|
|
61
|
+
duck_execute_prepared: { args: ['usize'], returns: 'usize' },
|
|
62
|
+
|
|
63
|
+
// Parameter binding
|
|
64
|
+
duck_bind_null: { args: ['usize', 'u64'], returns: 'bool' },
|
|
65
|
+
duck_bind_bool: { args: ['usize', 'u64', 'bool'], returns: 'bool' },
|
|
66
|
+
duck_bind_i8: { args: ['usize', 'u64', 'i8'], returns: 'bool' },
|
|
67
|
+
duck_bind_i16: { args: ['usize', 'u64', 'i16'], returns: 'bool' },
|
|
68
|
+
duck_bind_i32: { args: ['usize', 'u64', 'i32'], returns: 'bool' },
|
|
69
|
+
duck_bind_i64: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
|
|
70
|
+
duck_bind_u8: { args: ['usize', 'u64', 'u8'], returns: 'bool' },
|
|
71
|
+
duck_bind_u16: { args: ['usize', 'u64', 'u16'], returns: 'bool' },
|
|
72
|
+
duck_bind_u32: { args: ['usize', 'u64', 'u32'], returns: 'bool' },
|
|
73
|
+
duck_bind_u64: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
|
|
74
|
+
duck_bind_f32: { args: ['usize', 'u64', 'f32'], returns: 'bool' },
|
|
75
|
+
duck_bind_f64: { args: ['usize', 'u64', 'f64'], returns: 'bool' },
|
|
76
|
+
duck_bind_string: { args: ['usize', 'u64', 'ptr', 'u64'], returns: 'bool' },
|
|
77
|
+
duck_bind_blob: { args: ['usize', 'u64', 'ptr', 'u64'], returns: 'bool' },
|
|
78
|
+
duck_bind_timestamp: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
|
|
79
|
+
duck_bind_date: { args: ['usize', 'u64', 'i32'], returns: 'bool' },
|
|
80
|
+
duck_bind_time: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
|
|
81
|
+
|
|
82
|
+
// Utility
|
|
83
|
+
duck_free: { args: ['usize'], returns: 'void' },
|
|
84
|
+
}).symbols;
|
|
85
|
+
|
|
86
|
+
// Note: Don't use .native versions as they convert BigInts to Numbers,
|
|
87
|
+
// which can cause precision loss for pointer values on 64-bit systems.
|
|
88
|
+
|
|
89
|
+
const utf8 = new TextEncoder();
|
|
90
|
+
|
|
91
|
+
// DuckDB type enum
|
|
92
|
+
const Type = {
|
|
93
|
+
INVALID: 0,
|
|
94
|
+
BOOLEAN: 1,
|
|
95
|
+
TINYINT: 2,
|
|
96
|
+
SMALLINT: 3,
|
|
97
|
+
INTEGER: 4,
|
|
98
|
+
BIGINT: 5,
|
|
99
|
+
UTINYINT: 6,
|
|
100
|
+
USMALLINT: 7,
|
|
101
|
+
UINTEGER: 8,
|
|
102
|
+
UBIGINT: 9,
|
|
103
|
+
FLOAT: 10,
|
|
104
|
+
DOUBLE: 11,
|
|
105
|
+
TIMESTAMP: 12,
|
|
106
|
+
DATE: 13,
|
|
107
|
+
TIME: 14,
|
|
108
|
+
INTERVAL: 15,
|
|
109
|
+
HUGEINT: 16,
|
|
110
|
+
VARCHAR: 17,
|
|
111
|
+
BLOB: 18,
|
|
112
|
+
DECIMAL: 19,
|
|
113
|
+
TIMESTAMP_S: 20,
|
|
114
|
+
TIMESTAMP_MS: 21,
|
|
115
|
+
TIMESTAMP_NS: 22,
|
|
116
|
+
ENUM: 23,
|
|
117
|
+
LIST: 24,
|
|
118
|
+
STRUCT: 25,
|
|
119
|
+
MAP: 26,
|
|
120
|
+
UUID: 27,
|
|
121
|
+
JSON: 28,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Value extractors by type
|
|
125
|
+
const extractors = {
|
|
126
|
+
[Type.BOOLEAN]: (r, c, row) => duck.duck_value_bool(r, c, row),
|
|
127
|
+
[Type.TINYINT]: (r, c, row) => duck.duck_value_i8(r, c, row),
|
|
128
|
+
[Type.SMALLINT]: (r, c, row) => duck.duck_value_i16(r, c, row),
|
|
129
|
+
[Type.INTEGER]: (r, c, row) => duck.duck_value_i32(r, c, row),
|
|
130
|
+
[Type.BIGINT]: (r, c, row) => Number(duck.duck_value_i64(r, c, row)), // Convert to Number for JSON compatibility
|
|
131
|
+
[Type.UTINYINT]: (r, c, row) => duck.duck_value_u8(r, c, row),
|
|
132
|
+
[Type.USMALLINT]: (r, c, row) => duck.duck_value_u16(r, c, row),
|
|
133
|
+
[Type.UINTEGER]: (r, c, row) => duck.duck_value_u32(r, c, row),
|
|
134
|
+
[Type.UBIGINT]: (r, c, row) => Number(duck.duck_value_u64(r, c, row)), // Convert to Number for JSON compatibility
|
|
135
|
+
[Type.FLOAT]: (r, c, row) => duck.duck_value_f32(r, c, row),
|
|
136
|
+
[Type.DOUBLE]: (r, c, row) => duck.duck_value_f64(r, c, row),
|
|
137
|
+
[Type.DECIMAL]: (r, c, row) => duck.duck_value_f64(r, c, row), // Treat as double
|
|
138
|
+
[Type.VARCHAR]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
|
|
139
|
+
[Type.JSON]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
|
|
140
|
+
[Type.UUID]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
|
|
141
|
+
[Type.DATE]: (r, c, row) => duck.duck_value_date_days(r, c, row) * 86400000, // ms since epoch
|
|
142
|
+
[Type.TIME]: (r, c, row) => Number(duck.duck_value_time_micros(r, c, row)) / 1000, // ms
|
|
143
|
+
[Type.TIMESTAMP]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
|
|
144
|
+
[Type.TIMESTAMP_S]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
|
|
145
|
+
[Type.TIMESTAMP_MS]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
|
|
146
|
+
[Type.TIMESTAMP_NS]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
|
|
147
|
+
[Type.HUGEINT]: (r, c, row) => Number(duck.duck_value_i64(r, c, row)), // Simplified, Number for JSON
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Parameter binders by type
|
|
151
|
+
const binders = {
|
|
152
|
+
[Type.BOOLEAN]: (stmt, idx, val) => duck.duck_bind_bool(stmt, idx, val),
|
|
153
|
+
[Type.TINYINT]: (stmt, idx, val) => duck.duck_bind_i8(stmt, idx, val | 0),
|
|
154
|
+
[Type.SMALLINT]: (stmt, idx, val) => duck.duck_bind_i16(stmt, idx, val | 0),
|
|
155
|
+
[Type.INTEGER]: (stmt, idx, val) => duck.duck_bind_i32(stmt, idx, val | 0),
|
|
156
|
+
[Type.BIGINT]: (stmt, idx, val) => duck.duck_bind_i64(stmt, idx, BigInt(val)),
|
|
157
|
+
[Type.UTINYINT]: (stmt, idx, val) => duck.duck_bind_u8(stmt, idx, val >>> 0),
|
|
158
|
+
[Type.USMALLINT]: (stmt, idx, val) => duck.duck_bind_u16(stmt, idx, val >>> 0),
|
|
159
|
+
[Type.UINTEGER]: (stmt, idx, val) => duck.duck_bind_u32(stmt, idx, val >>> 0),
|
|
160
|
+
[Type.UBIGINT]: (stmt, idx, val) => duck.duck_bind_u64(stmt, idx, BigInt(val)),
|
|
161
|
+
[Type.FLOAT]: (stmt, idx, val) => duck.duck_bind_f32(stmt, idx, val),
|
|
162
|
+
[Type.DOUBLE]: (stmt, idx, val) => duck.duck_bind_f64(stmt, idx, val),
|
|
163
|
+
[Type.DECIMAL]: (stmt, idx, val) => duck.duck_bind_f64(stmt, idx, val),
|
|
164
|
+
[Type.VARCHAR]: (stmt, idx, val) => {
|
|
165
|
+
const bytes = utf8.encode(String(val));
|
|
166
|
+
return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
|
|
167
|
+
},
|
|
168
|
+
[Type.JSON]: (stmt, idx, val) => {
|
|
169
|
+
const str = typeof val === 'string' ? val : JSON.stringify(val);
|
|
170
|
+
const bytes = utf8.encode(str);
|
|
171
|
+
return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
|
|
172
|
+
},
|
|
173
|
+
[Type.UUID]: (stmt, idx, val) => {
|
|
174
|
+
const bytes = utf8.encode(String(val));
|
|
175
|
+
return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
|
|
176
|
+
},
|
|
177
|
+
[Type.TIMESTAMP]: (stmt, idx, val) => duck.duck_bind_timestamp(stmt, idx, BigInt(val) * 1000n),
|
|
178
|
+
[Type.DATE]: (stmt, idx, val) => duck.duck_bind_date(stmt, idx, Math.floor(val / 86400000)),
|
|
179
|
+
[Type.TIME]: (stmt, idx, val) => duck.duck_bind_time(stmt, idx, BigInt(val) * 1000n),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Default extractor for unknown types
|
|
183
|
+
function defaultExtractor(r, c, row) {
|
|
184
|
+
const p = duck.duck_value_string_internal(r, c, row);
|
|
185
|
+
return p ? new CString(p) : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Bind a value based on its JS type and param type
|
|
189
|
+
function bindValue(stmt, idx, val, paramType) {
|
|
190
|
+
if (val === null || val === undefined) {
|
|
191
|
+
return duck.duck_bind_null(stmt, idx);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const binder = binders[paramType];
|
|
195
|
+
if (binder) {
|
|
196
|
+
return binder(stmt, idx, val);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fallback: infer from JS type
|
|
200
|
+
const t = typeof val;
|
|
201
|
+
if (t === 'boolean') return duck.duck_bind_bool(stmt, idx, val);
|
|
202
|
+
if (t === 'number') {
|
|
203
|
+
if (Number.isInteger(val)) {
|
|
204
|
+
return duck.duck_bind_i64(stmt, idx, BigInt(val));
|
|
205
|
+
}
|
|
206
|
+
return duck.duck_bind_f64(stmt, idx, val);
|
|
207
|
+
}
|
|
208
|
+
if (t === 'bigint') return duck.duck_bind_i64(stmt, idx, val);
|
|
209
|
+
if (t === 'string') {
|
|
210
|
+
const bytes = utf8.encode(val);
|
|
211
|
+
return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Object -> JSON
|
|
215
|
+
const bytes = utf8.encode(JSON.stringify(val));
|
|
216
|
+
return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Open a DuckDB database
|
|
221
|
+
* @param {string|null} path - Path to database file, or null/:memory: for in-memory
|
|
222
|
+
*/
|
|
223
|
+
export function open(path) {
|
|
224
|
+
return new Database(path);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class Database {
|
|
228
|
+
#ptr;
|
|
229
|
+
|
|
230
|
+
constructor(path) {
|
|
231
|
+
const p = path === null || path === ':memory:'
|
|
232
|
+
? 0
|
|
233
|
+
: ptr(utf8.encode(path + '\0'));
|
|
234
|
+
this.#ptr = duck.duck_open(p);
|
|
235
|
+
if (this.#ptr === 0) throw new Error('Failed to open database');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
connect() {
|
|
239
|
+
return new Connection(this.#ptr);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
close() {
|
|
243
|
+
if (this.#ptr) {
|
|
244
|
+
duck.duck_close(this.#ptr);
|
|
245
|
+
this.#ptr = 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
class Connection {
|
|
251
|
+
#ptr;
|
|
252
|
+
|
|
253
|
+
constructor(db) {
|
|
254
|
+
this.#ptr = duck.duck_connect(db);
|
|
255
|
+
if (this.#ptr === 0) throw new Error('Failed to connect to database');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
query(sql) {
|
|
259
|
+
const sqlPtr = ptr(utf8.encode(sql + '\0'));
|
|
260
|
+
const result = duck.duck_query(this.#ptr, sqlPtr);
|
|
261
|
+
|
|
262
|
+
// Check for error
|
|
263
|
+
const errPtr = duck.duck_result_error(result);
|
|
264
|
+
if (errPtr) {
|
|
265
|
+
const err = new CString(errPtr);
|
|
266
|
+
duck.duck_free_result(result);
|
|
267
|
+
throw new Error(err.toString());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Extract rows
|
|
271
|
+
const rowCount = Number(duck.duck_row_count(result));
|
|
272
|
+
const colCount = Number(duck.duck_column_count(result));
|
|
273
|
+
|
|
274
|
+
if (rowCount === 0) {
|
|
275
|
+
duck.duck_free_result(result);
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Get column info
|
|
280
|
+
const columns = [];
|
|
281
|
+
const types = [];
|
|
282
|
+
const extract = [];
|
|
283
|
+
|
|
284
|
+
for (let c = 0; c < colCount; c++) {
|
|
285
|
+
columns.push(new CString(duck.duck_column_name(result, c)).toString());
|
|
286
|
+
const type = duck.duck_column_type(result, c);
|
|
287
|
+
types.push(type);
|
|
288
|
+
extract.push(extractors[type] || defaultExtractor);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract all rows
|
|
292
|
+
const rows = new Array(rowCount);
|
|
293
|
+
for (let r = 0; r < rowCount; r++) {
|
|
294
|
+
const row = {};
|
|
295
|
+
for (let c = 0; c < colCount; c++) {
|
|
296
|
+
if (duck.duck_value_is_null(result, c, r)) {
|
|
297
|
+
row[columns[c]] = null;
|
|
298
|
+
} else {
|
|
299
|
+
row[columns[c]] = extract[c](result, c, r);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
rows[r] = row;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
duck.duck_free_result(result);
|
|
306
|
+
return rows;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
prepare(sql) {
|
|
310
|
+
const sqlPtr = ptr(utf8.encode(sql + '\0'));
|
|
311
|
+
const stmt = duck.duck_prepare(this.#ptr, sqlPtr);
|
|
312
|
+
|
|
313
|
+
const errPtr = duck.duck_prepare_error(stmt);
|
|
314
|
+
if (errPtr) {
|
|
315
|
+
const err = new CString(errPtr);
|
|
316
|
+
duck.duck_free_prepare(stmt);
|
|
317
|
+
throw new Error(err.toString());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return new PreparedStatement(stmt);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
close() {
|
|
324
|
+
if (this.#ptr) {
|
|
325
|
+
duck.duck_disconnect(this.#ptr);
|
|
326
|
+
this.#ptr = 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class PreparedStatement {
|
|
332
|
+
#ptr;
|
|
333
|
+
#paramCount;
|
|
334
|
+
#paramTypes;
|
|
335
|
+
|
|
336
|
+
constructor(ptr) {
|
|
337
|
+
this.#ptr = ptr;
|
|
338
|
+
this.#paramCount = Number(duck.duck_nparams(ptr));
|
|
339
|
+
this.#paramTypes = [];
|
|
340
|
+
for (let i = 0; i < this.#paramCount; i++) {
|
|
341
|
+
this.#paramTypes.push(duck.duck_param_type(ptr, i + 1));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
query(...params) {
|
|
346
|
+
// Bind parameters
|
|
347
|
+
for (let i = 0; i < params.length; i++) {
|
|
348
|
+
const paramType = this.#paramTypes[i] || Type.VARCHAR;
|
|
349
|
+
if (!bindValue(this.#ptr, i + 1, params[i], paramType)) {
|
|
350
|
+
throw new Error(`Failed to bind parameter ${i + 1}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Execute
|
|
355
|
+
const result = duck.duck_execute_prepared(this.#ptr);
|
|
356
|
+
|
|
357
|
+
// Check for error
|
|
358
|
+
const errPtr = duck.duck_result_error(result);
|
|
359
|
+
if (errPtr) {
|
|
360
|
+
const err = new CString(errPtr);
|
|
361
|
+
duck.duck_free_result(result);
|
|
362
|
+
throw new Error(err.toString());
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Extract rows
|
|
366
|
+
const rowCount = Number(duck.duck_row_count(result));
|
|
367
|
+
const colCount = Number(duck.duck_column_count(result));
|
|
368
|
+
|
|
369
|
+
if (rowCount === 0) {
|
|
370
|
+
duck.duck_free_result(result);
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Get column info
|
|
375
|
+
const columns = [];
|
|
376
|
+
const types = [];
|
|
377
|
+
const extract = [];
|
|
378
|
+
|
|
379
|
+
for (let c = 0; c < colCount; c++) {
|
|
380
|
+
columns.push(new CString(duck.duck_column_name(result, c)).toString());
|
|
381
|
+
const type = duck.duck_column_type(result, c);
|
|
382
|
+
types.push(type);
|
|
383
|
+
extract.push(extractors[type] || defaultExtractor);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Extract all rows
|
|
387
|
+
const rows = new Array(rowCount);
|
|
388
|
+
for (let r = 0; r < rowCount; r++) {
|
|
389
|
+
const row = {};
|
|
390
|
+
for (let c = 0; c < colCount; c++) {
|
|
391
|
+
if (duck.duck_value_is_null(result, c, r)) {
|
|
392
|
+
row[columns[c]] = null;
|
|
393
|
+
} else {
|
|
394
|
+
row[columns[c]] = extract[c](result, c, r);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
rows[r] = row;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
duck.duck_free_result(result);
|
|
401
|
+
return rows;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
close() {
|
|
405
|
+
if (this.#ptr) {
|
|
406
|
+
duck.duck_free_prepare(this.#ptr);
|
|
407
|
+
this.#ptr = 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export { Type };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rip-lang/db",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "DuckDB Server — Simple HTTP API for DuckDB queries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "db.rip",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rip-db": "./bin/rip-db"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "rip db.rip",
|
|
12
|
+
"dev": "rip db.rip :memory:",
|
|
13
|
+
"test": "bun test"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"db",
|
|
17
|
+
"database",
|
|
18
|
+
"duckdb",
|
|
19
|
+
"sql",
|
|
20
|
+
"http",
|
|
21
|
+
"api",
|
|
22
|
+
"rip"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/shreeve/rip-lang.git",
|
|
27
|
+
"directory": "packages/db"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/db#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/shreeve/rip-lang/issues"
|
|
32
|
+
},
|
|
33
|
+
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"files": [
|
|
36
|
+
"db.rip",
|
|
37
|
+
"db.html",
|
|
38
|
+
"lib/",
|
|
39
|
+
"bin/",
|
|
40
|
+
"README.md"
|
|
41
|
+
]
|
|
42
|
+
}
|