@rip-lang/db 0.7.0 → 0.8.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/PROTOCOL.md ADDED
@@ -0,0 +1,258 @@
1
+ # DuckDB UI Binary Protocol Specification
2
+
3
+ This document describes the binary protocol used by DuckDB's built-in UI to communicate
4
+ with the database server. rip-db implements this protocol, allowing the official DuckDB UI
5
+ to connect transparently.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ ┌─────────────┐ HTTP POST ┌─────────────┐ SQL ┌─────────────┐
11
+ │ DuckDB UI │ ──────────────────▶│ rip-db │ ──────────────▶│ DuckDB │
12
+ │ (Browser) │◀────────────────── │ Server │◀────────────── │ Database │
13
+ └─────────────┘ Binary Response └─────────────┘ Results └─────────────┘
14
+ ```
15
+
16
+ ## Implemented Endpoints
17
+
18
+ | Endpoint | Method | Body | Response | Status |
19
+ |-------------------|--------|----------------|-------------------|----------|
20
+ | `/ddb/run` | POST | SQL (text) | Binary result | ✅ Done |
21
+ | `/ddb/interrupt` | POST | Empty | Empty result | ✅ Done |
22
+ | `/ddb/tokenize` | POST | SQL (text) | Binary tokens | ✅ Done |
23
+ | `/info` | GET | - | Empty + headers | ✅ Done |
24
+
25
+ ## Request Headers
26
+
27
+ All headers are optional unless noted.
28
+
29
+ | Header | Encoding | Purpose |
30
+ |-------------------------------------|----------|-----------------------------------|
31
+ | `X-DuckDB-UI-Connection-Name` | Plain | Named connection (for persistence) |
32
+ | `X-DuckDB-UI-Database-Name` | Base64 | Target database |
33
+ | `X-DuckDB-UI-Schema-Name` | Base64 | Target schema |
34
+ | `X-DuckDB-UI-Parameter-Count` | Plain | Number of prepared stmt params |
35
+ | `X-DuckDB-UI-Parameter-Value-{n}` | Base64 | Param value (0-indexed) |
36
+ | `X-DuckDB-UI-Result-Row-Limit` | Plain | Max rows to return |
37
+ | `X-DuckDB-UI-Result-Database-Name` | Base64 | Store results in this database |
38
+ | `X-DuckDB-UI-Result-Schema-Name` | Base64 | Store results in this schema |
39
+ | `X-DuckDB-UI-Result-Table-Name` | Base64 | Store results in this table |
40
+ | `X-DuckDB-UI-Result-Table-Row-Limit`| Plain | Max rows to store in table |
41
+ | `X-DuckDB-UI-Errors-As-JSON` | Plain | Return errors as JSON |
42
+ | `X-DuckDB-UI-Request-Description` | Plain | Human-readable description |
43
+
44
+ ## Binary Serialization Format
45
+
46
+ ### Primitives
47
+
48
+ #### varint (Variable-length Integer)
49
+ ```
50
+ while (byte & 0x80):
51
+ result |= (byte & 0x7F) << shift
52
+ shift += 7
53
+ ```
54
+
55
+ #### uint8
56
+ Single byte, unsigned.
57
+
58
+ #### uint16 (Field ID)
59
+ 2 bytes, little-endian.
60
+
61
+ #### string
62
+ ```
63
+ length: varint
64
+ data: UTF-8 bytes (length count)
65
+ ```
66
+
67
+ #### data (raw bytes)
68
+ ```
69
+ length: varint
70
+ data: raw bytes (length count)
71
+ ```
72
+
73
+ #### nullable<T>
74
+ ```
75
+ present: uint8 (0 = null, non-zero = present)
76
+ value: T (only if present)
77
+ ```
78
+
79
+ #### list<T>
80
+ ```
81
+ count: varint
82
+ items: T[] (count items)
83
+ ```
84
+
85
+ ### Object Structure
86
+
87
+ Objects use field IDs to identify properties:
88
+ ```
89
+ field_id: uint16 (little-endian)
90
+ value: <type depends on field>
91
+ ...
92
+ end: 0xFFFF (end marker)
93
+ ```
94
+
95
+ ### Response Types
96
+
97
+ #### SuccessResult
98
+ ```
99
+ field_100: boolean (true)
100
+ field_101: ColumnNamesAndTypes
101
+ field_102: list<DataChunk>
102
+ 0xFFFF
103
+ ```
104
+
105
+ #### ErrorResult
106
+ ```
107
+ field_100: boolean (false)
108
+ field_101: string (error message)
109
+ 0xFFFF
110
+ ```
111
+
112
+ #### EmptyResult
113
+ ```
114
+ (no fields)
115
+ ```
116
+
117
+ #### TokenizeResult
118
+ ```
119
+ field_100: list<varint> (offsets)
120
+ field_101: list<varint> (token types)
121
+ 0xFFFF
122
+ ```
123
+
124
+ ### ColumnNamesAndTypes
125
+ ```
126
+ field_100: list<string> (column names)
127
+ field_101: list<Type> (column types)
128
+ 0xFFFF
129
+ ```
130
+
131
+ ### Type
132
+ ```
133
+ field_100: uint8 (LogicalTypeId)
134
+ field_101: nullable<TypeInfo> (extra info for complex types)
135
+ 0xFFFF
136
+ ```
137
+
138
+ ### DataChunk
139
+ ```
140
+ field_100: varint (row count)
141
+ field_101: list<Vector> (vectors - one per column)
142
+ 0xFFFF
143
+ ```
144
+
145
+ ### Vector
146
+
147
+ All vectors start with:
148
+ ```
149
+ field_100: uint8 (allValid flag - 0 means some nulls)
150
+ field_101: data (validity bitmap - only if allValid=0)
151
+ ```
152
+
153
+ Then type-specific data:
154
+
155
+ #### Data Vector (numeric/temporal types)
156
+ ```
157
+ field_102: data (raw bytes - type-specific encoding)
158
+ 0xFFFF
159
+ ```
160
+
161
+ #### String Vector (CHAR, VARCHAR)
162
+ ```
163
+ field_102: list<string> (string values)
164
+ 0xFFFF
165
+ ```
166
+
167
+ ## LogicalTypeId Constants
168
+
169
+ ```javascript
170
+ const LogicalTypeId = {
171
+ BOOLEAN: 10,
172
+ TINYINT: 11,
173
+ SMALLINT: 12,
174
+ INTEGER: 13,
175
+ BIGINT: 14,
176
+ DATE: 15,
177
+ TIME: 16,
178
+ TIMESTAMP_SEC: 17,
179
+ TIMESTAMP_MS: 18,
180
+ TIMESTAMP: 19,
181
+ TIMESTAMP_NS: 20,
182
+ DECIMAL: 21,
183
+ FLOAT: 22,
184
+ DOUBLE: 23,
185
+ CHAR: 24,
186
+ VARCHAR: 25,
187
+ BLOB: 26,
188
+ INTERVAL: 27,
189
+ UTINYINT: 28,
190
+ USMALLINT: 29,
191
+ UINTEGER: 30,
192
+ UBIGINT: 31,
193
+ TIMESTAMP_TZ: 32,
194
+ TIME_TZ: 34,
195
+ BIT: 36,
196
+ BIGNUM: 39,
197
+ UHUGEINT: 49,
198
+ HUGEINT: 50,
199
+ UUID: 54,
200
+ STRUCT: 100,
201
+ LIST: 101,
202
+ MAP: 102,
203
+ ENUM: 104,
204
+ UNION: 107,
205
+ ARRAY: 108,
206
+ };
207
+ ```
208
+
209
+ ## Data Type Byte Sizes
210
+
211
+ | Type | Bytes | Format |
212
+ |---------------|-------|-------------------------------------|
213
+ | BOOLEAN | 1 | 0 or non-zero |
214
+ | TINYINT | 1 | int8 |
215
+ | UTINYINT | 1 | uint8 |
216
+ | SMALLINT | 2 | int16 LE |
217
+ | USMALLINT | 2 | uint16 LE |
218
+ | INTEGER | 4 | int32 LE |
219
+ | UINTEGER | 4 | uint32 LE |
220
+ | BIGINT | 8 | int64 LE |
221
+ | UBIGINT | 8 | uint64 LE |
222
+ | HUGEINT | 16 | int128 LE |
223
+ | UHUGEINT | 16 | uint128 LE |
224
+ | FLOAT | 4 | IEEE 754 float32 |
225
+ | DOUBLE | 8 | IEEE 754 float64 |
226
+ | DATE | 4 | int32 (days since 1970-01-01) |
227
+ | TIME | 8 | int64 (microseconds since midnight) |
228
+ | TIMESTAMP | 8 | int64 (microseconds since epoch) |
229
+ | TIMESTAMP_MS | 8 | int64 (milliseconds since epoch) |
230
+ | TIMESTAMP_SEC | 8 | int64 (seconds since epoch) |
231
+ | TIMESTAMP_NS | 8 | int64 (nanoseconds since epoch) |
232
+ | TIMESTAMP_TZ | 8 | same as TIMESTAMP |
233
+ | TIME_TZ | 8 | int64 (micros + offset encoded) |
234
+ | INTERVAL | 16 | months(4) + days(4) + micros(8) |
235
+ | UUID | 16 | 128-bit UUID |
236
+ | DECIMAL | varies| depends on width |
237
+ | ENUM | varies| depends on enum size |
238
+
239
+ ## Validity Bitmap
240
+
241
+ When `allValid` is 0, the validity bitmap indicates which values are NULL:
242
+ - Bit order: LSB first within each byte
243
+ - Bit meaning: 1 = valid, 0 = NULL
244
+ - Size: ceil(rowCount / 8) bytes
245
+
246
+ ```javascript
247
+ // Check if row i is valid
248
+ const byteIndex = Math.floor(i / 8);
249
+ const bitIndex = i % 8;
250
+ const isValid = (validity[byteIndex] >> bitIndex) & 1;
251
+ ```
252
+
253
+ ## References
254
+
255
+ - [DuckDB UI GitHub](https://github.com/duckdb/duckdb-ui)
256
+ - [BinaryDeserializer.ts](https://github.com/duckdb/duckdb-ui/blob/main/ts/pkgs/duckdb-ui-client/src/serialization/classes/BinaryDeserializer.ts)
257
+ - [DuckDB BinarySerializer](https://github.com/duckdb/duckdb/blob/main/src/include/duckdb/common/serializer/binary_serializer.hpp)
258
+ - [Vector::Serialize](https://github.com/duckdb/duckdb/blob/main/src/common/types/vector.cpp)
package/README.md CHANGED
@@ -128,6 +128,34 @@ Built-in SQL console. Open in browser:
128
128
  http://localhost:4000/ui
129
129
  ```
130
130
 
131
+ ## DuckDB UI Compatibility
132
+
133
+ rip-db implements the official DuckDB UI binary protocol, making it compatible with
134
+ DuckDB's built-in UI. This means you can use the beautiful DuckDB UI with rip-db!
135
+
136
+ ### Binary Protocol Endpoints
137
+
138
+ | Endpoint | Method | Purpose |
139
+ |-------------------|--------|----------------------------|
140
+ | `/ddb/run` | POST | Execute SQL (binary result)|
141
+ | `/ddb/interrupt` | POST | Cancel running query |
142
+ | `/ddb/tokenize` | POST | Syntax highlighting |
143
+ | `/info` | GET | Version info |
144
+
145
+ ### How It Works
146
+
147
+ ```
148
+ ┌─────────────┐ Binary Protocol ┌─────────────┐
149
+ │ DuckDB UI │ ◀─────────────────────▶│ rip-db │
150
+ │ (Browser) │ │ Server │
151
+ └─────────────┘ └─────────────┘
152
+ ```
153
+
154
+ The UI sends SQL to `/ddb/run`, rip-db executes it and returns results in DuckDB's
155
+ binary format. The UI has no idea it's talking to rip-db instead of native DuckDB!
156
+
157
+ For protocol details, see [PROTOCOL.md](./PROTOCOL.md).
158
+
131
159
  ## Examples
132
160
 
133
161
  ### Create a table
package/db.rip CHANGED
@@ -3,6 +3,7 @@
3
3
  # ==============================================================================
4
4
  #
5
5
  # A simple HTTP server for DuckDB queries. One server per database.
6
+ # Compatible with the official DuckDB UI (duckdb -ui) via binary protocol.
6
7
  #
7
8
  # Usage:
8
9
  # rip db.rip <database.duckdb> [--port 4000]
@@ -10,10 +11,12 @@
10
11
  #
11
12
  # Endpoints:
12
13
  # POST /sql - Execute SQL (query or mutation)
14
+ # POST /ddb/run - DuckDB UI binary protocol
13
15
  # GET /health - Health check (always ok)
14
16
  # GET /status - Database info and table list
15
17
  # GET /tables - List all tables
16
18
  # GET /schema/:table - Get table schema
19
+ # GET /ui - Built-in SQL console
17
20
  #
18
21
  # ==============================================================================
19
22
 
@@ -21,6 +24,14 @@ import { get, post, raw, read, start, use } from '@rip-lang/api'
21
24
  import { cors } from '@rip-lang/api/middleware'
22
25
  import { open } from './lib/duckdb.mjs'
23
26
  import { version as VERSION } from './package.json'
27
+ import {
28
+ serializeSuccessResult
29
+ serializeErrorResult
30
+ serializeEmptyResult
31
+ serializeTokenizeResult
32
+ tokenizeSQL
33
+ inferType
34
+ } from './lib/duckdb-binary.rip'
24
35
 
25
36
  # Enable CORS for duck-ui and other clients
26
37
  use cors preflight: true
@@ -213,6 +224,86 @@ get '/schema/:table', ->
213
224
  # GET /ui — Built-in SQL console
214
225
  get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
215
226
 
227
+ # ==============================================================================
228
+ # DuckDB UI Protocol — Binary endpoints for official UI compatibility
229
+ # ==============================================================================
230
+ #
231
+ # These endpoints implement the binary protocol used by DuckDB's official UI.
232
+ # This allows you to use the beautiful DuckDB UI with rip-db!
233
+ #
234
+ # The official UI expects these endpoints:
235
+ # POST /ddb/run - Execute SQL (binary response)
236
+ # POST /ddb/interrupt - Cancel running query
237
+ # POST /ddb/tokenize - Tokenize SQL for syntax highlighting
238
+ # GET /info - Server version info
239
+ #
240
+ # To use the DuckDB UI:
241
+ # 1. Start rip-db: rip-db mydb.duckdb --port 4000
242
+ # 2. Open: http://localhost:4000 (UI proxied from ui.duckdb.org)
243
+ #
244
+ # ==============================================================================
245
+
246
+ # POST /ddb/run — Execute SQL and return binary result (DuckDB UI protocol)
247
+ post '/ddb/run', (req) ->
248
+ try
249
+ sql = req.body! or req.text!
250
+ console.log "POST /ddb/run", sql?.slice(0, 100)
251
+
252
+ return binaryResponse serializeErrorResult 'Empty query' unless sql?.trim()
253
+
254
+ # Parse result row limit from headers
255
+ rowLimit = parseInt(req.headers.get('x-duckdb-ui-result-row-limit') or '10000')
256
+
257
+ # Execute query
258
+ conn = db.connect()
259
+ try
260
+ rows = conn.query sql
261
+
262
+ # Build column metadata from first row
263
+ columns = []
264
+ if rows?.length > 0
265
+ first = rows[0]
266
+ for key of first
267
+ columns.push { name: key, type: inferType first[key] }
268
+
269
+ # Limit rows and convert to array format
270
+ limitedRows = rows?.slice(0, rowLimit) or []
271
+ arrayRows = limitedRows.map (row) ->
272
+ columns.map (col) -> row[col.name]
273
+
274
+ binaryResponse serializeSuccessResult columns, arrayRows
275
+ finally
276
+ conn.close()
277
+
278
+ catch error
279
+ console.error "POST /ddb/run error:", error.message
280
+ binaryResponse serializeErrorResult error.message or String(error)
281
+
282
+ # POST /ddb/interrupt — Cancel running query
283
+ post '/ddb/interrupt', ->
284
+ # In a real implementation, this would cancel the running query
285
+ # For now, just return empty result
286
+ binaryResponse serializeEmptyResult()
287
+
288
+ # POST /ddb/tokenize — Tokenize SQL for syntax highlighting
289
+ post '/ddb/tokenize', (req) ->
290
+ sql = req.body! or req.text! or ''
291
+ tokens = tokenizeSQL sql
292
+ binaryResponse serializeTokenizeResult tokens
293
+
294
+ # GET /info — Server version info (DuckDB UI checks this)
295
+ get '/info', (req, res) ->
296
+ res.headers.set 'Access-Control-Allow-Origin', '*'
297
+ res.headers.set 'X-DuckDB-Version', '1.2.1' # Pretend to be DuckDB
298
+ res.headers.set 'X-DuckDB-Platform', 'rip-db'
299
+ res.headers.set 'X-DuckDB-UI-Extension-Version', VERSION
300
+ ''
301
+
302
+ # Helper to create binary response
303
+ binaryResponse = (buffer) ->
304
+ new Response buffer,
305
+ headers: { 'Content-Type': 'application/octet-stream' }
306
+
216
307
  # ==============================================================================
217
308
  # Start Server
218
309
  # ==============================================================================
@@ -220,4 +311,4 @@ get '/ui', -> new Response Bun.file(import.meta.dir + '/db.html')
220
311
  start port: port
221
312
 
222
313
  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"
314
+ console.log "rip-db: POST /sql, POST /ddb/run, GET /health, GET /status, GET /tables, GET /schema/:table, GET /ui"
@@ -0,0 +1,510 @@
1
+ # DuckDB Binary Protocol Serializer
2
+ #
3
+ # Implements the binary serialization format used by DuckDB's official UI.
4
+ # This allows rip-db to serve responses that the DuckDB UI can understand.
5
+ #
6
+ # Protocol spec: See PROTOCOL.md in this directory
7
+
8
+ # ==============================================================================
9
+ # LogicalTypeId - matches DuckDB's internal type IDs
10
+ # ==============================================================================
11
+
12
+ export LogicalTypeId =
13
+ BOOLEAN: 10
14
+ TINYINT: 11
15
+ SMALLINT: 12
16
+ INTEGER: 13
17
+ BIGINT: 14
18
+ DATE: 15
19
+ TIME: 16
20
+ TIMESTAMP_SEC: 17
21
+ TIMESTAMP_MS: 18
22
+ TIMESTAMP: 19
23
+ TIMESTAMP_NS: 20
24
+ DECIMAL: 21
25
+ FLOAT: 22
26
+ DOUBLE: 23
27
+ CHAR: 24
28
+ VARCHAR: 25
29
+ BLOB: 26
30
+ INTERVAL: 27
31
+ UTINYINT: 28
32
+ USMALLINT: 29
33
+ UINTEGER: 30
34
+ UBIGINT: 31
35
+ TIMESTAMP_TZ: 32
36
+ TIME_TZ: 34
37
+ BIT: 36
38
+ BIGNUM: 39
39
+ UHUGEINT: 49
40
+ HUGEINT: 50
41
+ UUID: 54
42
+ STRUCT: 100
43
+ LIST: 101
44
+ MAP: 102
45
+ ENUM: 104
46
+ UNION: 107
47
+ ARRAY: 108
48
+
49
+ # ==============================================================================
50
+ # BinarySerializer - writes the DuckDB binary format
51
+ # ==============================================================================
52
+
53
+ export class BinarySerializer
54
+ constructor: ->
55
+ @buffer = []
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Primitive writers
59
+ # ---------------------------------------------------------------------------
60
+
61
+ writeUint8: (value) ->
62
+ @buffer.push value & 0xFF
63
+
64
+ writeUint16LE: (value) ->
65
+ @buffer.push value & 0xFF
66
+ @buffer.push (value >> 8) & 0xFF
67
+
68
+ writeUint32LE: (value) ->
69
+ @buffer.push value & 0xFF
70
+ @buffer.push (value >> 8) & 0xFF
71
+ @buffer.push (value >> 16) & 0xFF
72
+ @buffer.push (value >> 24) & 0xFF
73
+
74
+ writeInt32LE: (value) ->
75
+ @writeUint32LE value >>> 0
76
+
77
+ writeUint64LE: (value) ->
78
+ big = BigInt value
79
+ for i in [0...8]
80
+ @buffer.push Number (big >> BigInt(i * 8)) & 0xFFn
81
+
82
+ writeInt64LE: (value) ->
83
+ @writeUint64LE value
84
+
85
+ writeFloat32: (value) ->
86
+ buf = new ArrayBuffer 4
87
+ new DataView(buf).setFloat32 0, value, true
88
+ bytes = new Uint8Array buf
89
+ @buffer.push ...bytes
90
+
91
+ writeFloat64: (value) ->
92
+ buf = new ArrayBuffer 8
93
+ new DataView(buf).setFloat64 0, value, true
94
+ bytes = new Uint8Array buf
95
+ @buffer.push ...bytes
96
+
97
+ writeVarInt: (value) ->
98
+ v = value >>> 0
99
+ loop
100
+ byte = v & 0x7F
101
+ v >>>= 7
102
+ if v isnt 0
103
+ @buffer.push byte | 0x80
104
+ else
105
+ @buffer.push byte
106
+ break
107
+
108
+ writeString: (str) ->
109
+ encoder = new TextEncoder()
110
+ bytes = encoder.encode str
111
+ @writeVarInt bytes.length
112
+ @buffer.push ...bytes
113
+
114
+ writeData: (data) ->
115
+ bytes = if data instanceof Uint8Array then data else new Uint8Array data
116
+ @writeVarInt bytes.length
117
+ @buffer.push ...bytes
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Object structure writers
121
+ # ---------------------------------------------------------------------------
122
+
123
+ writeFieldId: (id) ->
124
+ @writeUint16LE id
125
+
126
+ writeEndMarker: ->
127
+ @writeUint16LE 0xFFFF
128
+
129
+ writeBoolean: (fieldId, value) ->
130
+ @writeFieldId fieldId
131
+ @writeUint8 if value then 1 else 0
132
+
133
+ writePropertyString: (fieldId, value) ->
134
+ @writeFieldId fieldId
135
+ @writeString value
136
+
137
+ writePropertyVarInt: (fieldId, value) ->
138
+ @writeFieldId fieldId
139
+ @writeVarInt value
140
+
141
+ writeList: (fieldId, items, writer) ->
142
+ @writeFieldId fieldId
143
+ @writeVarInt items.length
144
+ for item, i in items
145
+ writer this, item, i
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Get final buffer
149
+ # ---------------------------------------------------------------------------
150
+
151
+ toArrayBuffer: ->
152
+ new Uint8Array(@buffer).buffer
153
+
154
+ toUint8Array: ->
155
+ new Uint8Array @buffer
156
+
157
+ # ==============================================================================
158
+ # Result serializers
159
+ # ==============================================================================
160
+
161
+ export serializeSuccessResult = (columns, rows) ->
162
+ s = new BinarySerializer()
163
+
164
+ # field_100: success = true
165
+ s.writeBoolean 100, true
166
+
167
+ # field_101: ColumnNamesAndTypes
168
+ s.writeFieldId 101
169
+ serializeColumnNamesAndTypes s, columns
170
+
171
+ # field_102: list<DataChunk> (one chunk with all rows)
172
+ s.writeFieldId 102
173
+ s.writeVarInt 1
174
+ serializeDataChunk s, columns, rows
175
+
176
+ s.writeEndMarker()
177
+ s.toArrayBuffer()
178
+
179
+ export serializeErrorResult = (message) ->
180
+ s = new BinarySerializer()
181
+ s.writeBoolean 100, false
182
+ s.writePropertyString 101, message
183
+ s.writeEndMarker()
184
+ s.toArrayBuffer()
185
+
186
+ export serializeEmptyResult = ->
187
+ new BinarySerializer().toArrayBuffer()
188
+
189
+ export serializeTokenizeResult = (tokens) ->
190
+ s = new BinarySerializer()
191
+ s.writeList 100, tokens.map((t) -> t.offset), (s, v) -> s.writeVarInt v
192
+ s.writeList 101, tokens.map((t) -> t.type), (s, v) -> s.writeVarInt v
193
+ s.writeEndMarker()
194
+ s.toArrayBuffer()
195
+
196
+ # ==============================================================================
197
+ # Internal serializers
198
+ # ==============================================================================
199
+
200
+ serializeColumnNamesAndTypes = (s, columns) ->
201
+ s.writeList 100, columns, (s, col) -> s.writeString col.name
202
+ s.writeList 101, columns, (s, col) -> serializeType s, col
203
+ s.writeEndMarker()
204
+
205
+ serializeType = (s, column) ->
206
+ typeId = mapDuckDBType column.type
207
+ s.writeFieldId 100
208
+ s.writeUint8 typeId
209
+ s.writeFieldId 101
210
+ s.writeUint8 0 # null (no extra type info for basic types)
211
+ s.writeEndMarker()
212
+
213
+ serializeDataChunk = (s, columns, rows) ->
214
+ s.writePropertyVarInt 100, rows.length
215
+ s.writeList 101, columns, (s, col, colIdx) ->
216
+ values = rows.map (row) -> row[colIdx] ? row[col.name]
217
+ serializeVector s, col, values
218
+ s.writeEndMarker()
219
+
220
+ serializeVector = (s, column, values) ->
221
+ typeId = mapDuckDBType column.type
222
+ hasNulls = values.some (v) -> v is null or v is undefined
223
+ allValid = if hasNulls then 0 else 1
224
+
225
+ s.writeFieldId 100
226
+ s.writeUint8 allValid
227
+
228
+ if not allValid
229
+ s.writeFieldId 101
230
+ s.writeData createValidityBitmap values
231
+
232
+ switch typeId
233
+ when LogicalTypeId.VARCHAR, LogicalTypeId.CHAR
234
+ s.writeList 102, values, (s, v) -> s.writeString String(v ? '')
235
+
236
+ when LogicalTypeId.BOOLEAN
237
+ s.writeFieldId 102
238
+ bytes = new Uint8Array values.length
239
+ bytes[i] = if v then 1 else 0 for v, i in values
240
+ s.writeData bytes
241
+
242
+ when LogicalTypeId.TINYINT, LogicalTypeId.UTINYINT
243
+ s.writeFieldId 102
244
+ bytes = new Uint8Array values.length
245
+ bytes[i] = (v ? 0) & 0xFF for v, i in values
246
+ s.writeData bytes
247
+
248
+ when LogicalTypeId.SMALLINT
249
+ s.writeFieldId 102
250
+ bytes = new Uint8Array values.length * 2
251
+ dv = new DataView bytes.buffer
252
+ dv.setInt16 i * 2, v ? 0, true for v, i in values
253
+ s.writeData bytes
254
+
255
+ when LogicalTypeId.USMALLINT
256
+ s.writeFieldId 102
257
+ bytes = new Uint8Array values.length * 2
258
+ dv = new DataView bytes.buffer
259
+ dv.setUint16 i * 2, v ? 0, true for v, i in values
260
+ s.writeData bytes
261
+
262
+ when LogicalTypeId.INTEGER
263
+ s.writeFieldId 102
264
+ bytes = new Uint8Array values.length * 4
265
+ dv = new DataView bytes.buffer
266
+ dv.setInt32 i * 4, v ? 0, true for v, i in values
267
+ s.writeData bytes
268
+
269
+ when LogicalTypeId.UINTEGER
270
+ s.writeFieldId 102
271
+ bytes = new Uint8Array values.length * 4
272
+ dv = new DataView bytes.buffer
273
+ dv.setUint32 i * 4, v ? 0, true for v, i in values
274
+ s.writeData bytes
275
+
276
+ when LogicalTypeId.BIGINT
277
+ s.writeFieldId 102
278
+ bytes = new Uint8Array values.length * 8
279
+ dv = new DataView bytes.buffer
280
+ dv.setBigInt64 i * 8, BigInt(v ? 0), true for v, i in values
281
+ s.writeData bytes
282
+
283
+ when LogicalTypeId.UBIGINT
284
+ s.writeFieldId 102
285
+ bytes = new Uint8Array values.length * 8
286
+ dv = new DataView bytes.buffer
287
+ dv.setBigUint64 i * 8, BigInt(v ? 0), true for v, i in values
288
+ s.writeData bytes
289
+
290
+ when LogicalTypeId.FLOAT
291
+ s.writeFieldId 102
292
+ bytes = new Uint8Array values.length * 4
293
+ dv = new DataView bytes.buffer
294
+ dv.setFloat32 i * 4, v ? 0, true for v, i in values
295
+ s.writeData bytes
296
+
297
+ when LogicalTypeId.DOUBLE
298
+ s.writeFieldId 102
299
+ bytes = new Uint8Array values.length * 8
300
+ dv = new DataView bytes.buffer
301
+ dv.setFloat64 i * 8, v ? 0, true for v, i in values
302
+ s.writeData bytes
303
+
304
+ when LogicalTypeId.DATE
305
+ s.writeFieldId 102
306
+ bytes = new Uint8Array values.length * 4
307
+ dv = new DataView bytes.buffer
308
+ for v, i in values
309
+ days = if v? then dateToDays v else 0
310
+ dv.setInt32 i * 4, days, true
311
+ s.writeData bytes
312
+
313
+ when LogicalTypeId.TIMESTAMP, LogicalTypeId.TIMESTAMP_TZ
314
+ s.writeFieldId 102
315
+ bytes = new Uint8Array values.length * 8
316
+ dv = new DataView bytes.buffer
317
+ for v, i in values
318
+ micros = if v? then timestampToMicros v else 0n
319
+ dv.setBigInt64 i * 8, micros, true
320
+ s.writeData bytes
321
+
322
+ else
323
+ s.writeList 102, values, (s, v) -> s.writeString String(v ? '')
324
+
325
+ s.writeEndMarker()
326
+
327
+ # ==============================================================================
328
+ # Helper functions
329
+ # ==============================================================================
330
+
331
+ createValidityBitmap = (values) ->
332
+ byteCount = Math.ceil values.length / 8
333
+ bitmap = new Uint8Array byteCount
334
+ for v, i in values
335
+ if v? and v isnt null
336
+ byteIdx = Math.floor i / 8
337
+ bitIdx = i % 8
338
+ bitmap[byteIdx] |= 1 << bitIdx
339
+ bitmap
340
+
341
+ dateToDays = (value) ->
342
+ if value instanceof Date
343
+ Math.floor value.getTime() / (24 * 60 * 60 * 1000)
344
+ else if typeof value is 'string'
345
+ Math.floor new Date(value).getTime() / (24 * 60 * 60 * 1000)
346
+ else if typeof value is 'number'
347
+ value
348
+ else
349
+ 0
350
+
351
+ timestampToMicros = (value) ->
352
+ if value instanceof Date
353
+ BigInt(value.getTime()) * 1000n
354
+ else if typeof value is 'string'
355
+ BigInt(new Date(value).getTime()) * 1000n
356
+ else if typeof value is 'number'
357
+ BigInt(value) * 1000n
358
+ else if typeof value is 'bigint'
359
+ value
360
+ else
361
+ 0n
362
+
363
+ mapDuckDBType = (typeName) ->
364
+ return LogicalTypeId.VARCHAR unless typeName
365
+ upper = String(typeName).toUpperCase()
366
+
367
+ switch upper
368
+ when 'BOOLEAN', 'BOOL' then LogicalTypeId.BOOLEAN
369
+ when 'TINYINT', 'INT1' then LogicalTypeId.TINYINT
370
+ when 'SMALLINT', 'INT2' then LogicalTypeId.SMALLINT
371
+ when 'INTEGER', 'INT4', 'INT', 'SIGNED' then LogicalTypeId.INTEGER
372
+ when 'BIGINT', 'INT8', 'LONG' then LogicalTypeId.BIGINT
373
+ when 'UTINYINT' then LogicalTypeId.UTINYINT
374
+ when 'USMALLINT' then LogicalTypeId.USMALLINT
375
+ when 'UINTEGER', 'UINT' then LogicalTypeId.UINTEGER
376
+ when 'UBIGINT' then LogicalTypeId.UBIGINT
377
+ when 'HUGEINT' then LogicalTypeId.HUGEINT
378
+ when 'UHUGEINT' then LogicalTypeId.UHUGEINT
379
+ when 'FLOAT', 'FLOAT4', 'REAL' then LogicalTypeId.FLOAT
380
+ when 'DOUBLE', 'FLOAT8', 'NUMERIC' then LogicalTypeId.DOUBLE
381
+ when 'DATE' then LogicalTypeId.DATE
382
+ when 'TIME' then LogicalTypeId.TIME
383
+ when 'TIMESTAMP', 'DATETIME' then LogicalTypeId.TIMESTAMP
384
+ when 'TIMESTAMP WITH TIME ZONE', 'TIMESTAMPTZ' then LogicalTypeId.TIMESTAMP_TZ
385
+ when 'VARCHAR', 'TEXT', 'STRING', 'CHAR', 'BPCHAR' then LogicalTypeId.VARCHAR
386
+ when 'BLOB', 'BYTEA', 'BINARY', 'VARBINARY' then LogicalTypeId.BLOB
387
+ when 'UUID' then LogicalTypeId.UUID
388
+ when 'INTERVAL' then LogicalTypeId.INTERVAL
389
+ when 'JSON' then LogicalTypeId.VARCHAR
390
+ else
391
+ if upper.startsWith 'DECIMAL' then LogicalTypeId.DOUBLE
392
+ else if upper.startsWith 'VARCHAR' then LogicalTypeId.VARCHAR
393
+ else if upper.startsWith 'CHAR' then LogicalTypeId.CHAR
394
+ else LogicalTypeId.VARCHAR
395
+
396
+ # ==============================================================================
397
+ # SQL Tokenizer (for syntax highlighting)
398
+ # ==============================================================================
399
+
400
+ TokenType =
401
+ IDENTIFIER: 0
402
+ NUMERIC_CONSTANT: 1
403
+ STRING_CONSTANT: 2
404
+ OPERATOR: 3
405
+ KEYWORD: 4
406
+ COMMENT: 5
407
+
408
+ SQL_KEYWORDS = new Set [
409
+ 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'IS', 'NULL',
410
+ 'AS', 'ON', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'FULL', 'CROSS',
411
+ 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'OFFSET', 'GROUP', 'HAVING',
412
+ 'UNION', 'ALL', 'DISTINCT', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET',
413
+ 'DELETE', 'CREATE', 'TABLE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD',
414
+ 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT',
415
+ 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'CAST', 'TRUE', 'FALSE',
416
+ 'WITH', 'RECURSIVE', 'OVER', 'PARTITION', 'WINDOW', 'ROWS',
417
+ 'RANGE', 'BETWEEN', 'UNBOUNDED', 'PRECEDING', 'FOLLOWING', 'CURRENT',
418
+ 'ROW', 'EXISTS', 'ANY', 'SOME', 'LIKE', 'ILIKE', 'SIMILAR', 'ESCAPE'
419
+ ]
420
+
421
+ export tokenizeSQL = (sql) ->
422
+ tokens = []
423
+ i = 0
424
+
425
+ while i < sql.length
426
+ start = i
427
+ char = sql[i]
428
+
429
+ if /\s/.test char
430
+ i++
431
+ continue
432
+
433
+ if char is '-' and sql[i + 1] is '-'
434
+ i++ while i < sql.length and sql[i] isnt '\n'
435
+ tokens.push { offset: start, type: TokenType.COMMENT }
436
+ continue
437
+
438
+ if char is '/' and sql[i + 1] is '*'
439
+ i += 2
440
+ i++ while i < sql.length - 1 and not (sql[i] is '*' and sql[i + 1] is '/')
441
+ i += 2
442
+ tokens.push { offset: start, type: TokenType.COMMENT }
443
+ continue
444
+
445
+ if char is "'"
446
+ i++
447
+ while i < sql.length
448
+ if sql[i] is "'"
449
+ if sql[i + 1] is "'" then i += 2 else (i++; break)
450
+ else i++
451
+ tokens.push { offset: start, type: TokenType.STRING_CONSTANT }
452
+ continue
453
+
454
+ if char is '"'
455
+ i++
456
+ i++ while i < sql.length and sql[i] isnt '"'
457
+ i++ if i < sql.length
458
+ tokens.push { offset: start, type: TokenType.IDENTIFIER }
459
+ continue
460
+
461
+ if /[0-9]/.test(char) or (char is '.' and /[0-9]/.test(sql[i + 1] or ''))
462
+ i++ while i < sql.length and /[0-9.eE+-]/.test sql[i]
463
+ tokens.push { offset: start, type: TokenType.NUMERIC_CONSTANT }
464
+ continue
465
+
466
+ if /[a-zA-Z_]/.test char
467
+ i++ while i < sql.length and /[a-zA-Z0-9_]/.test sql[i]
468
+ word = sql.slice(start, i).toUpperCase()
469
+ type = if SQL_KEYWORDS.has word then TokenType.KEYWORD else TokenType.IDENTIFIER
470
+ tokens.push { offset: start, type }
471
+ continue
472
+
473
+ if /[+\-*/%=<>!&|^~]/.test char
474
+ if sql.slice(i, i + 2) in ['<=', '>=', '<>', '!=', '||', '&&', '::', '->']
475
+ i += 2
476
+ else i++
477
+ tokens.push { offset: start, type: TokenType.OPERATOR }
478
+ continue
479
+
480
+ if /[(),;.\[\]{}]/.test char
481
+ i++
482
+ tokens.push { offset: start, type: TokenType.OPERATOR }
483
+ continue
484
+
485
+ i++
486
+
487
+ tokens
488
+
489
+ # ==============================================================================
490
+ # Type inference from JavaScript values
491
+ # ==============================================================================
492
+
493
+ export inferType = (value) ->
494
+ return 'VARCHAR' if value is null or value is undefined
495
+
496
+ switch typeof value
497
+ when 'boolean' then 'BOOLEAN'
498
+ when 'number'
499
+ if Number.isInteger value
500
+ if value >= -2147483648 and value <= 2147483647 then 'INTEGER' else 'BIGINT'
501
+ else 'DOUBLE'
502
+ when 'bigint' then 'BIGINT'
503
+ when 'string'
504
+ if /^\d{4}-\d{2}-\d{2}$/.test value then 'DATE'
505
+ else if /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test value then 'TIMESTAMP'
506
+ else 'VARCHAR'
507
+ when 'object'
508
+ if value instanceof Date then 'TIMESTAMP'
509
+ else 'VARCHAR'
510
+ else 'VARCHAR'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/db",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "DuckDB Server — Simple HTTP API for DuckDB queries",
5
5
  "type": "module",
6
6
  "main": "db.rip",
@@ -12,6 +12,9 @@
12
12
  "dev": "rip db.rip :memory:",
13
13
  "test": "bun test"
14
14
  },
15
+ "dependencies": {
16
+ "@rip-lang/api": "^0.5.0"
17
+ },
15
18
  "keywords": [
16
19
  "db",
17
20
  "database",
@@ -37,6 +40,7 @@
37
40
  "db.html",
38
41
  "lib/",
39
42
  "bin/",
40
- "README.md"
43
+ "README.md",
44
+ "PROTOCOL.md"
41
45
  ]
42
46
  }