@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/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
- import { version as VERSION } from './package.json'
31
- import {
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
- start = Date.now()
37
+ t0 = Date.now()
46
38
  result = next!
47
- ms = Date.now() - start
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
- if not rows?.length > 0
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, params = []) ->
131
+ executeSQL = (sql) ->
140
132
  startTime = Date.now()
141
133
  conn = db.connect()
142
134
 
143
135
  try
144
- rows = if params.length > 0
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
- if not sql
174
- return { error: 'Empty query' }
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 optional params
167
+ # POST /sql — JSON body with SQL query
178
168
  post '/sql' ->
179
- { sql, params } = read()
180
- if not sql
181
- return { error: 'Missing required field: sql' }
182
- executeSQL sql, (params or [])
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
- stmt = conn.prepare("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position")
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
- conn = db.connect()
266
- try
267
- rows = conn.query sql
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 serializeErrorResult err?.message or 'Unknown error'
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 serializeEmptyResult()
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
- tokens = tokenizeSQL sql
299
- binaryResponse serializeTokenizeResult tokens
268
+ binaryResponse tokenize (sql or '')
300
269
 
301
- # GET /version — Tell UI we're running in local/HTTP mode (not WASM)
270
+ # GET /version — Tell UI we're running in host/HTTP mode (not WASM)
302
271
  get '/version' ->
303
- { origin: 'local', version: '139-944c08a214' }
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
- # Create SSE response with keep-alive
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 connection event
347
- controller.enqueue encoder.encode "event: connected\ndata: {}\n\n"
348
-
349
- # Keep connection alive with periodic heartbeats
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 ": heartbeat\n\n"
340
+ controller.enqueue encoder.encode ": ping\n\n"
353
341
  catch
354
- clearInterval intervalId
355
- , 30000
356
-
342
+ clearInterval @interval
343
+ , 5000
357
344
  cancel: ->
358
- clearInterval intervalId if intervalId
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', '/ui']
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: Official DuckDB UI available at http://localhost:#{port}/"
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