@rip-lang/db 0.10.0 → 1.0.1
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/INTERNALS.md +324 -0
- package/README.md +93 -237
- package/bin/rip-db +3 -3
- package/build.zig +88 -0
- package/db.rip +66 -80
- package/lib/darwin-arm64/duckdb.node +0 -0
- package/lib/duckdb.mjs +246 -333
- package/package.json +11 -5
- package/src/duckdb.zig +1156 -0
- package/PROTOCOL.md +0 -258
- package/db.html +0 -122
- package/lib/duckdb-binary.rip +0 -525
package/db.rip
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
#
|
|
15
15
|
# Endpoints:
|
|
16
16
|
# GET / - Official DuckDB UI (proxied from ui.duckdb.org)
|
|
17
|
-
# GET /ui - Built-in SQL console (simple, no proxy)
|
|
18
17
|
# POST /sql - Execute SQL (JSON API)
|
|
19
18
|
# POST /ddb/run - DuckDB UI binary protocol
|
|
20
19
|
# GET /health - Health check
|
|
@@ -26,25 +25,18 @@
|
|
|
26
25
|
|
|
27
26
|
import { get, post, read, start, use } from '@rip-lang/api'
|
|
28
27
|
import { cors } from '@rip-lang/api/middleware'
|
|
29
|
-
import { open } from './lib/duckdb.mjs'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
serializeSuccessResult
|
|
33
|
-
serializeErrorResult
|
|
34
|
-
serializeEmptyResult
|
|
35
|
-
serializeTokenizeResult
|
|
36
|
-
tokenizeSQL
|
|
37
|
-
inferType
|
|
38
|
-
} from './lib/duckdb-binary.rip'
|
|
28
|
+
import { open, queryBinary, tokenize, emptyResult, errorResult } from './lib/duckdb.mjs'
|
|
29
|
+
|
|
30
|
+
VERSION = '1.0.0'
|
|
39
31
|
|
|
40
32
|
# Enable CORS for duck-ui and other clients
|
|
41
33
|
use cors preflight: true
|
|
42
34
|
|
|
43
35
|
# Log all requests
|
|
44
36
|
use (c, next) ->
|
|
45
|
-
|
|
37
|
+
t0 = Date.now()
|
|
46
38
|
result = next!
|
|
47
|
-
ms = Date.now() -
|
|
39
|
+
ms = Date.now() - t0
|
|
48
40
|
console.log "#{c.req.method} #{c.req.path} #{ms}ms"
|
|
49
41
|
result
|
|
50
42
|
|
|
@@ -99,8 +91,9 @@ if '--version' in args or '-v' in args
|
|
|
99
91
|
path = process.env.DB_PATH or args.find((a) -> not a.startsWith('-')) or ':memory:'
|
|
100
92
|
port = parseInt(process.env.DB_PORT or (args.find((a) -> a.startsWith('--port=')))?.split('=')[1]) or 4213
|
|
101
93
|
|
|
102
|
-
# Open database
|
|
94
|
+
# Open database and create persistent connection for binary queries
|
|
103
95
|
db = open(path)
|
|
96
|
+
binaryConn = db.connect() # Keep one connection for binary protocol queries
|
|
104
97
|
console.log "rip-db: database=#{path} (bun-native)"
|
|
105
98
|
|
|
106
99
|
# ==============================================================================
|
|
@@ -109,8 +102,7 @@ console.log "rip-db: database=#{path} (bun-native)"
|
|
|
109
102
|
|
|
110
103
|
# Extract column info from result
|
|
111
104
|
getColumnInfo = (rows) ->
|
|
112
|
-
|
|
113
|
-
return { columns: [], types: [] }
|
|
105
|
+
return { columns: [], types: [] } unless rows?.length
|
|
114
106
|
first = rows[0]
|
|
115
107
|
columns = Object.keys(first)
|
|
116
108
|
types = columns.map (col) ->
|
|
@@ -136,16 +128,12 @@ isSelectQuery = (sql) ->
|
|
|
136
128
|
upper.startsWith('PRAGMA')
|
|
137
129
|
|
|
138
130
|
# Execute SQL and return JSONCompact result
|
|
139
|
-
executeSQL = (sql
|
|
131
|
+
executeSQL = (sql) ->
|
|
140
132
|
startTime = Date.now()
|
|
141
133
|
conn = db.connect()
|
|
142
134
|
|
|
143
135
|
try
|
|
144
|
-
rows =
|
|
145
|
-
stmt = conn.prepare(sql)
|
|
146
|
-
stmt.query(...params)
|
|
147
|
-
else
|
|
148
|
-
conn.query(sql)
|
|
136
|
+
rows = conn.query(sql)
|
|
149
137
|
|
|
150
138
|
if isSelectQuery(sql)
|
|
151
139
|
{ columns, types } = getColumnInfo(rows)
|
|
@@ -169,17 +157,20 @@ executeSQL = (sql, params = []) ->
|
|
|
169
157
|
|
|
170
158
|
# POST / — duck-ui compatible (raw SQL in body)
|
|
171
159
|
post '/' ->
|
|
172
|
-
sql = read 'body', 'string'
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
sql = read 'body', 'string' or return { error: 'Empty query' }
|
|
161
|
+
# Log the SQL (truncate long queries)
|
|
162
|
+
preview = sql.replace(/\s+/g, ' ').trim()
|
|
163
|
+
preview = "#{preview.slice(0, 120)}..." if preview.length > 120
|
|
164
|
+
console.log "SQL: #{preview}"
|
|
175
165
|
executeSQL sql
|
|
176
166
|
|
|
177
|
-
# POST /sql — JSON body with
|
|
167
|
+
# POST /sql — JSON body with SQL query
|
|
178
168
|
post '/sql' ->
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
sql = read 'sql', 'string' or return { error: 'Missing required field: sql' }
|
|
170
|
+
preview = sql.replace(/\s+/g, ' ').trim()
|
|
171
|
+
preview = "#{preview.slice(0, 120)}..." if preview.length > 120
|
|
172
|
+
console.log "SQL: #{preview}"
|
|
173
|
+
executeSQL sql
|
|
183
174
|
|
|
184
175
|
# GET /health — Simple health check (no DB query)
|
|
185
176
|
get '/health', ->
|
|
@@ -217,8 +208,7 @@ get '/schema/:table', ->
|
|
|
217
208
|
table = read 'table', 'string!'
|
|
218
209
|
conn = db.connect()
|
|
219
210
|
try
|
|
220
|
-
|
|
221
|
-
columns = stmt.query(table)
|
|
211
|
+
columns = conn.query("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '#{table.replace(/'/g, "''")}' ORDER BY ordinal_position")
|
|
222
212
|
|
|
223
213
|
if columns.length is 0
|
|
224
214
|
{ ok: false, error: "Table '#{table}' not found" }
|
|
@@ -229,9 +219,6 @@ get '/schema/:table', ->
|
|
|
229
219
|
finally
|
|
230
220
|
conn.close()
|
|
231
221
|
|
|
232
|
-
# GET /ui — Built-in SQL console
|
|
233
|
-
get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
|
|
234
|
-
|
|
235
222
|
# ==============================================================================
|
|
236
223
|
# DuckDB UI Protocol — Binary endpoints for official UI compatibility
|
|
237
224
|
# ==============================================================================
|
|
@@ -254,53 +241,35 @@ get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
|
|
|
254
241
|
# POST /ddb/run — Execute SQL and return binary result (DuckDB UI protocol)
|
|
255
242
|
post '/ddb/run' ->
|
|
256
243
|
try
|
|
257
|
-
sql = read 'body', 'string'
|
|
258
|
-
if not sql
|
|
259
|
-
return binaryResponse serializeErrorResult 'Empty query'
|
|
244
|
+
sql = read 'body', 'string' or return binaryResponse errorResult 'Empty query'
|
|
260
245
|
|
|
261
246
|
# Parse row limit from DuckDB UI header
|
|
262
247
|
rowLimit = parseInt(@req.header('x-duckdb-ui-result-row-limit') or '10000')
|
|
248
|
+
rowLimit = 10000 if isNaN(rowLimit) or rowLimit <= 0
|
|
263
249
|
|
|
264
|
-
# Execute query
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# Build column metadata from first row
|
|
270
|
-
columns = []
|
|
271
|
-
if rows?.length > 0
|
|
272
|
-
first = rows[0]
|
|
273
|
-
for key of first
|
|
274
|
-
columns.push { name: key, type: inferType first[key] }
|
|
275
|
-
|
|
276
|
-
# Limit rows and convert to array format
|
|
277
|
-
limitedRows = rows?.slice(0, rowLimit) or []
|
|
278
|
-
arrayRows = limitedRows.map (row) ->
|
|
279
|
-
columns.map (col) -> row[col.name]
|
|
280
|
-
|
|
281
|
-
binaryResponse serializeSuccessResult columns, arrayRows
|
|
282
|
-
finally
|
|
283
|
-
conn.close()
|
|
250
|
+
# Execute query and get binary result directly from Zig!
|
|
251
|
+
# (No JS serialization needed - Zig does it all)
|
|
252
|
+
result = queryBinary binaryConn.handle, sql, rowLimit
|
|
253
|
+
binaryResponse result
|
|
284
254
|
|
|
285
255
|
catch err
|
|
286
256
|
console.error "POST /ddb/run error:", err?.message
|
|
287
|
-
binaryResponse
|
|
257
|
+
binaryResponse errorResult err?.message or 'Unknown error'
|
|
288
258
|
|
|
289
259
|
# POST /ddb/interrupt — Cancel running query
|
|
290
260
|
post '/ddb/interrupt', ->
|
|
291
261
|
# In a real implementation, this would cancel the running query
|
|
292
262
|
# For now, just return empty result
|
|
293
|
-
binaryResponse
|
|
263
|
+
binaryResponse emptyResult()
|
|
294
264
|
|
|
295
265
|
# POST /ddb/tokenize — Tokenize SQL for syntax highlighting
|
|
296
266
|
post '/ddb/tokenize' ->
|
|
297
267
|
sql = read 'body', 'string'
|
|
298
|
-
|
|
299
|
-
binaryResponse serializeTokenizeResult tokens
|
|
268
|
+
binaryResponse tokenize (sql or '')
|
|
300
269
|
|
|
301
|
-
# GET /version — Tell UI we're running in
|
|
270
|
+
# GET /version — Tell UI we're running in host/HTTP mode (not WASM)
|
|
302
271
|
get '/version' ->
|
|
303
|
-
{ origin: '
|
|
272
|
+
{ origin: 'host', version: '139-944c08a214' }
|
|
304
273
|
|
|
305
274
|
# GET /localToken — Return empty token for local mode (no MotherDuck)
|
|
306
275
|
get '/localToken' ->
|
|
@@ -335,27 +304,45 @@ binaryResponse = (buffer) ->
|
|
|
335
304
|
|
|
336
305
|
UI_REMOTE_URL = 'https://ui.duckdb.org'
|
|
337
306
|
|
|
307
|
+
# GET /config — Local mode configuration (prevents MotherDuck redirect)
|
|
308
|
+
get '/config' ->
|
|
309
|
+
# Return minimal config with required fields for local mode
|
|
310
|
+
config =
|
|
311
|
+
logging:
|
|
312
|
+
enabled: false
|
|
313
|
+
datadog:
|
|
314
|
+
applicationId: ''
|
|
315
|
+
clientToken: ''
|
|
316
|
+
site: ''
|
|
317
|
+
env: 'local'
|
|
318
|
+
allowedTracingOrigins: []
|
|
319
|
+
defaultFeatureValues: {}
|
|
320
|
+
|
|
321
|
+
# Headers tell UI this is a local DuckDB instance
|
|
322
|
+
new Response JSON.stringify(config),
|
|
323
|
+
headers:
|
|
324
|
+
'Content-Type': 'application/json'
|
|
325
|
+
'X-DuckDB-Version': '1.4.1'
|
|
326
|
+
'X-DuckDB-Platform': 'osx_arm64'
|
|
327
|
+
'X-DuckDB-UI-Extension-Version': '139-944c08a214'
|
|
328
|
+
|
|
338
329
|
# GET /localEvents — Server-sent events for catalog updates
|
|
339
330
|
get '/localEvents' ->
|
|
340
|
-
#
|
|
331
|
+
# SSE endpoint for DuckDB UI - must stay open
|
|
341
332
|
encoder = new TextEncoder()
|
|
342
|
-
intervalId = null
|
|
343
|
-
|
|
344
333
|
stream = new ReadableStream
|
|
345
334
|
start: (controller) ->
|
|
346
|
-
# Send initial
|
|
347
|
-
controller.enqueue encoder.encode "event:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
intervalId = setInterval ->
|
|
335
|
+
# Send initial connected event
|
|
336
|
+
controller.enqueue encoder.encode "event: ConnectedEvent\ndata: \n\n"
|
|
337
|
+
# Keep-alive with pings every 5 seconds (Bun has 10s idle timeout)
|
|
338
|
+
@interval = setInterval =>
|
|
351
339
|
try
|
|
352
|
-
controller.enqueue encoder.encode ":
|
|
340
|
+
controller.enqueue encoder.encode ": ping\n\n"
|
|
353
341
|
catch
|
|
354
|
-
clearInterval
|
|
355
|
-
,
|
|
356
|
-
|
|
342
|
+
clearInterval @interval
|
|
343
|
+
, 5000
|
|
357
344
|
cancel: ->
|
|
358
|
-
clearInterval
|
|
345
|
+
clearInterval @interval if @interval
|
|
359
346
|
|
|
360
347
|
new Response stream,
|
|
361
348
|
headers:
|
|
@@ -368,7 +355,7 @@ get '/*' ->
|
|
|
368
355
|
path = @req.path
|
|
369
356
|
|
|
370
357
|
# Skip if this is one of our API endpoints (shouldn't reach here, but safety)
|
|
371
|
-
return { error: 'Not found' } if path in ['/health', '/status', '/tables', '/info'
|
|
358
|
+
return { error: 'Not found' } if path in ['/health', '/status', '/tables', '/info']
|
|
372
359
|
|
|
373
360
|
try
|
|
374
361
|
# Fetch from remote UI server
|
|
@@ -404,5 +391,4 @@ get '/*' ->
|
|
|
404
391
|
start port: port
|
|
405
392
|
|
|
406
393
|
console.log "rip-db: listening on http://localhost:#{port}"
|
|
407
|
-
console.log "rip-db:
|
|
408
|
-
console.log "rip-db: Built-in console at http://localhost:#{port}/ui"
|
|
394
|
+
console.log "rip-db: DuckDB UI available at http://localhost:#{port}/"
|
|
Binary file
|