@rip-lang/db 1.0.1 → 1.0.2

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.
@@ -0,0 +1,546 @@
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
+ # ==============================================================================
7
+ # LogicalTypeId - matches DuckDB's internal type IDs
8
+ # ==============================================================================
9
+
10
+ export LogicalTypeId =
11
+ BOOLEAN: 10
12
+ TINYINT: 11
13
+ SMALLINT: 12
14
+ INTEGER: 13
15
+ BIGINT: 14
16
+ DATE: 15
17
+ TIME: 16
18
+ TIMESTAMP_SEC: 17
19
+ TIMESTAMP_MS: 18
20
+ TIMESTAMP: 19
21
+ TIMESTAMP_NS: 20
22
+ DECIMAL: 21
23
+ FLOAT: 22
24
+ DOUBLE: 23
25
+ CHAR: 24
26
+ VARCHAR: 25
27
+ BLOB: 26
28
+ INTERVAL: 27
29
+ UTINYINT: 28
30
+ USMALLINT: 29
31
+ UINTEGER: 30
32
+ UBIGINT: 31
33
+ TIMESTAMP_TZ: 32
34
+ TIME_TZ: 34
35
+ BIT: 36
36
+ BIGNUM: 39
37
+ UHUGEINT: 49
38
+ HUGEINT: 50
39
+ UUID: 54
40
+ STRUCT: 100
41
+ LIST: 101
42
+ MAP: 102
43
+ ENUM: 104
44
+ UNION: 107
45
+ ARRAY: 108
46
+
47
+ # Shared TextEncoder instance (avoid allocating per call)
48
+ textEncoder = new TextEncoder()
49
+
50
+ # ==============================================================================
51
+ # BinarySerializer - writes the DuckDB binary format
52
+ # ==============================================================================
53
+
54
+ export class BinarySerializer
55
+ constructor: ->
56
+ @buffer = []
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Primitive writers
60
+ # ---------------------------------------------------------------------------
61
+
62
+ writeUint8: (value) ->
63
+ @buffer.push value & 0xFF
64
+
65
+ writeUint16LE: (value) ->
66
+ @buffer.push value & 0xFF
67
+ @buffer.push (value >> 8) & 0xFF
68
+
69
+ writeUint32LE: (value) ->
70
+ @buffer.push value & 0xFF
71
+ @buffer.push (value >> 8) & 0xFF
72
+ @buffer.push (value >> 16) & 0xFF
73
+ @buffer.push (value >> 24) & 0xFF
74
+
75
+ writeInt32LE: (value) ->
76
+ @writeUint32LE value >>> 0
77
+
78
+ writeUint64LE: (value) ->
79
+ big = BigInt value
80
+ for i in [0...8]
81
+ @buffer.push Number (big >> BigInt(i * 8)) & 0xFFn
82
+
83
+ writeInt64LE: (value) ->
84
+ @writeUint64LE value
85
+
86
+ writeFloat32: (value) ->
87
+ buf = new ArrayBuffer 4
88
+ new DataView(buf).setFloat32 0, value, true
89
+ bytes = new Uint8Array buf
90
+ @buffer.push ...bytes
91
+
92
+ writeFloat64: (value) ->
93
+ buf = new ArrayBuffer 8
94
+ new DataView(buf).setFloat64 0, value, true
95
+ bytes = new Uint8Array buf
96
+ @buffer.push ...bytes
97
+
98
+ writeVarInt: (value) ->
99
+ v = value >>> 0
100
+ loop
101
+ byte = v & 0x7F
102
+ v >>>= 7
103
+ if v isnt 0
104
+ @buffer.push byte | 0x80
105
+ else
106
+ @buffer.push byte
107
+ break
108
+
109
+ writeString: (str) ->
110
+ bytes = textEncoder.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]
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
+
224
+ # allValid flag: 0 = all valid (no bitmap), non-zero = has validity bitmap
225
+ # The deserializer reads field 101 ONLY when allValid is truthy
226
+ s.writeFieldId 100
227
+ if hasNulls
228
+ s.writeUint8 1 # Has validity bitmap
229
+ s.writeFieldId 101
230
+ s.writeData createValidityBitmap values
231
+ else
232
+ s.writeUint8 0 # All valid, no bitmap needed
233
+
234
+ switch typeId
235
+ when LogicalTypeId.VARCHAR, LogicalTypeId.CHAR
236
+ s.writeList 102, values, (s, v) -> s.writeString String(v ? '')
237
+
238
+ when LogicalTypeId.BOOLEAN
239
+ s.writeFieldId 102
240
+ bytes = new Uint8Array values.length
241
+ for v, i in values
242
+ bytes[i] = if v then 1 else 0
243
+ s.writeData bytes
244
+
245
+ when LogicalTypeId.TINYINT, LogicalTypeId.UTINYINT
246
+ s.writeFieldId 102
247
+ bytes = new Uint8Array values.length
248
+ for v, i in values
249
+ bytes[i] = (v ? 0) & 0xFF
250
+ s.writeData bytes
251
+
252
+ when LogicalTypeId.SMALLINT
253
+ s.writeFieldId 102
254
+ bytes = new Uint8Array values.length * 2
255
+ dv = new DataView bytes.buffer
256
+ for v, i in values
257
+ dv.setInt16 i * 2, v ? 0, true
258
+ s.writeData bytes
259
+
260
+ when LogicalTypeId.USMALLINT
261
+ s.writeFieldId 102
262
+ bytes = new Uint8Array values.length * 2
263
+ dv = new DataView bytes.buffer
264
+ for v, i in values
265
+ dv.setUint16 i * 2, v ? 0, true
266
+ s.writeData bytes
267
+
268
+ when LogicalTypeId.INTEGER
269
+ s.writeFieldId 102
270
+ bytes = new Uint8Array values.length * 4
271
+ dv = new DataView bytes.buffer
272
+ for v, i in values
273
+ dv.setInt32 i * 4, v ? 0, true
274
+ s.writeData bytes
275
+
276
+ when LogicalTypeId.UINTEGER
277
+ s.writeFieldId 102
278
+ bytes = new Uint8Array values.length * 4
279
+ dv = new DataView bytes.buffer
280
+ for v, i in values
281
+ dv.setUint32 i * 4, v ? 0, true
282
+ s.writeData bytes
283
+
284
+ when LogicalTypeId.BIGINT
285
+ s.writeFieldId 102
286
+ bytes = new Uint8Array values.length * 8
287
+ dv = new DataView bytes.buffer
288
+ for v, i in values
289
+ dv.setBigInt64 i * 8, BigInt(v ? 0), true
290
+ s.writeData bytes
291
+
292
+ when LogicalTypeId.UBIGINT
293
+ s.writeFieldId 102
294
+ bytes = new Uint8Array values.length * 8
295
+ dv = new DataView bytes.buffer
296
+ for v, i in values
297
+ dv.setBigUint64 i * 8, BigInt(v ? 0), true
298
+ s.writeData bytes
299
+
300
+ when LogicalTypeId.FLOAT
301
+ s.writeFieldId 102
302
+ bytes = new Uint8Array values.length * 4
303
+ dv = new DataView bytes.buffer
304
+ for v, i in values
305
+ dv.setFloat32 i * 4, v ? 0, true
306
+ s.writeData bytes
307
+
308
+ when LogicalTypeId.DOUBLE
309
+ s.writeFieldId 102
310
+ bytes = new Uint8Array values.length * 8
311
+ dv = new DataView bytes.buffer
312
+ for v, i in values
313
+ dv.setFloat64 i * 8, v ? 0, true
314
+ s.writeData bytes
315
+
316
+ when LogicalTypeId.DATE
317
+ s.writeFieldId 102
318
+ bytes = new Uint8Array values.length * 4
319
+ dv = new DataView bytes.buffer
320
+ for v, i in values
321
+ days = if v? then dateToDays v else 0
322
+ dv.setInt32 i * 4, days, true
323
+ s.writeData bytes
324
+
325
+ when LogicalTypeId.TIMESTAMP, LogicalTypeId.TIMESTAMP_TZ
326
+ s.writeFieldId 102
327
+ bytes = new Uint8Array values.length * 8
328
+ dv = new DataView bytes.buffer
329
+ for v, i in values
330
+ micros = if v? then timestampToMicros v else 0n
331
+ dv.setBigInt64 i * 8, micros, true
332
+ s.writeData bytes
333
+
334
+ when LogicalTypeId.UUID
335
+ s.writeFieldId 102
336
+ bytes = new Uint8Array values.length * 16
337
+ dv = new DataView bytes.buffer
338
+ for v, i in values
339
+ { lo, hi } = uuidToHugeint(v)
340
+ dv.setBigUint64 i * 16, lo, true
341
+ dv.setBigInt64 i * 16 + 8, hi, true
342
+ s.writeData bytes
343
+
344
+ else
345
+ s.writeList 102, values, (s, v) -> s.writeString String(v ? '')
346
+
347
+ s.writeEndMarker()
348
+
349
+ # ==============================================================================
350
+ # Helper functions
351
+ # ==============================================================================
352
+
353
+ createValidityBitmap = (values) ->
354
+ # Must be uint64-aligned (8-byte chunks) — the UI reads validity with getBigUint64
355
+ byteCount = Math.ceil(values.length / 64) * 8
356
+ bitmap = new Uint8Array byteCount
357
+ for v, i in values
358
+ if v?
359
+ byteIdx = Math.floor i / 8
360
+ bitIdx = i % 8
361
+ bitmap[byteIdx] |= 1 << bitIdx
362
+ bitmap
363
+
364
+ dateToDays = (value) ->
365
+ if value instanceof Date
366
+ Math.floor value.getTime() / (24 * 60 * 60 * 1000)
367
+ else if typeof value is 'string'
368
+ Math.floor new Date(value).getTime() / (24 * 60 * 60 * 1000)
369
+ else if typeof value is 'number'
370
+ value
371
+ else
372
+ 0
373
+
374
+ timestampToMicros = (value) ->
375
+ if value instanceof Date
376
+ BigInt(value.getTime()) * 1000n
377
+ else if typeof value is 'string'
378
+ BigInt(new Date(value).getTime()) * 1000n
379
+ else if typeof value is 'number'
380
+ BigInt(value) * 1000n
381
+ else if typeof value is 'bigint'
382
+ value
383
+ else
384
+ 0n
385
+
386
+ uuidToHugeint = (uuid) ->
387
+ return { lo: 0n, hi: 0n } unless uuid
388
+ hex = String(uuid).replace(/-/g, '')
389
+ full = BigInt "0x#{hex}"
390
+ hi = (full >> 64n) ^ (1n << 63n) # XOR sign bit for DuckDB sorting
391
+ lo = full & ((1n << 64n) - 1n)
392
+ { lo, hi }
393
+
394
+ mapDuckDBType = (typeName) ->
395
+ return LogicalTypeId.VARCHAR unless typeName
396
+ upper = String(typeName).toUpperCase()
397
+
398
+ switch upper
399
+ when 'BOOLEAN', 'BOOL' then LogicalTypeId.BOOLEAN
400
+ when 'TINYINT', 'INT1' then LogicalTypeId.TINYINT
401
+ when 'SMALLINT', 'INT2' then LogicalTypeId.SMALLINT
402
+ when 'INTEGER', 'INT4', 'INT', 'SIGNED' then LogicalTypeId.INTEGER
403
+ when 'BIGINT', 'INT8', 'LONG' then LogicalTypeId.BIGINT
404
+ when 'UTINYINT' then LogicalTypeId.UTINYINT
405
+ when 'USMALLINT' then LogicalTypeId.USMALLINT
406
+ when 'UINTEGER', 'UINT' then LogicalTypeId.UINTEGER
407
+ when 'UBIGINT' then LogicalTypeId.UBIGINT
408
+ when 'HUGEINT' then LogicalTypeId.HUGEINT
409
+ when 'UHUGEINT' then LogicalTypeId.UHUGEINT
410
+ when 'FLOAT', 'FLOAT4', 'REAL' then LogicalTypeId.FLOAT
411
+ when 'DOUBLE', 'FLOAT8', 'NUMERIC' then LogicalTypeId.DOUBLE
412
+ when 'DATE' then LogicalTypeId.DATE
413
+ when 'TIME' then LogicalTypeId.TIME
414
+ when 'TIMESTAMP', 'DATETIME' then LogicalTypeId.TIMESTAMP
415
+ when 'TIMESTAMP WITH TIME ZONE', 'TIMESTAMPTZ' then LogicalTypeId.TIMESTAMP_TZ
416
+ when 'VARCHAR', 'TEXT', 'STRING', 'CHAR', 'BPCHAR' then LogicalTypeId.VARCHAR
417
+ when 'BLOB', 'BYTEA', 'BINARY', 'VARBINARY' then LogicalTypeId.BLOB
418
+ when 'UUID' then LogicalTypeId.UUID
419
+ when 'INTERVAL' then LogicalTypeId.INTERVAL
420
+ when 'JSON' then LogicalTypeId.VARCHAR
421
+ else
422
+ if upper.startsWith 'DECIMAL' then LogicalTypeId.VARCHAR # Serialize as string to preserve exact precision
423
+ else if upper.startsWith 'VARCHAR' then LogicalTypeId.VARCHAR
424
+ else if upper.startsWith 'CHAR' then LogicalTypeId.CHAR
425
+ else LogicalTypeId.VARCHAR
426
+
427
+ # ==============================================================================
428
+ # SQL Tokenizer (for syntax highlighting)
429
+ # ==============================================================================
430
+
431
+ TokenType =
432
+ IDENTIFIER: 0
433
+ NUMERIC_CONSTANT: 1
434
+ STRING_CONSTANT: 2
435
+ OPERATOR: 3
436
+ KEYWORD: 4
437
+ COMMENT: 5
438
+
439
+ SQL_KEYWORDS = new Set [
440
+ 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'IS', 'NULL',
441
+ 'AS', 'ON', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'FULL', 'CROSS',
442
+ 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'OFFSET', 'GROUP', 'HAVING',
443
+ 'UNION', 'ALL', 'DISTINCT', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET',
444
+ 'DELETE', 'CREATE', 'TABLE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD',
445
+ 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT',
446
+ 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'CAST', 'TRUE', 'FALSE',
447
+ 'WITH', 'RECURSIVE', 'OVER', 'PARTITION', 'WINDOW', 'ROWS',
448
+ 'RANGE', 'BETWEEN', 'UNBOUNDED', 'PRECEDING', 'FOLLOWING', 'CURRENT',
449
+ 'ROW', 'EXISTS', 'ANY', 'SOME', 'LIKE', 'ILIKE', 'SIMILAR', 'ESCAPE'
450
+ ]
451
+
452
+ export tokenizeSQL = (sql) ->
453
+ tokens = []
454
+ i = 0
455
+
456
+ while i < sql.length
457
+ start = i
458
+ char = sql[i]
459
+
460
+ if /\s/.test char
461
+ i++
462
+ continue
463
+
464
+ if char is '-' and sql[i + 1] is '-'
465
+ i++ while i < sql.length and sql[i] isnt '\n'
466
+ tokens.push { offset: start, type: TokenType.COMMENT }
467
+ continue
468
+
469
+ if char is '/' and sql[i + 1] is '*'
470
+ i += 2
471
+ i++ while i < sql.length - 1 and not (sql[i] is '*' and sql[i + 1] is '/')
472
+ i += 2
473
+ tokens.push { offset: start, type: TokenType.COMMENT }
474
+ continue
475
+
476
+ if char is "'"
477
+ i++
478
+ while i < sql.length
479
+ if sql[i] is "'"
480
+ if sql[i + 1] is "'"
481
+ i += 2
482
+ else
483
+ i++
484
+ break
485
+ else
486
+ i++
487
+ tokens.push { offset: start, type: TokenType.STRING_CONSTANT }
488
+ continue
489
+
490
+ if char is '"'
491
+ i++
492
+ i++ while i < sql.length and sql[i] isnt '"'
493
+ i++ if i < sql.length
494
+ tokens.push { offset: start, type: TokenType.IDENTIFIER }
495
+ continue
496
+
497
+ if /[0-9]/.test(char) or (char is '.' and /[0-9]/.test(sql[i + 1] or ''))
498
+ i++ while i < sql.length and /[0-9.eE+-]/.test sql[i]
499
+ tokens.push { offset: start, type: TokenType.NUMERIC_CONSTANT }
500
+ continue
501
+
502
+ if /[a-zA-Z_]/.test char
503
+ i++ while i < sql.length and /[a-zA-Z0-9_]/.test sql[i]
504
+ word = sql.slice(start, i).toUpperCase()
505
+ type = if SQL_KEYWORDS.has word then TokenType.KEYWORD else TokenType.IDENTIFIER
506
+ tokens.push { offset: start, type }
507
+ continue
508
+
509
+ if /[+\-*/%=<>!&|^~]/.test char
510
+ if sql.slice(i, i + 2) in ['<=', '>=', '<>', '!=', '||', '&&', '::', '->']
511
+ i += 2
512
+ else i++
513
+ tokens.push { offset: start, type: TokenType.OPERATOR }
514
+ continue
515
+
516
+ if /[(),;.\[\]{}]/.test char
517
+ i++
518
+ tokens.push { offset: start, type: TokenType.OPERATOR }
519
+ continue
520
+
521
+ i++
522
+
523
+ tokens
524
+
525
+ # ==============================================================================
526
+ # Type inference from JavaScript values
527
+ # ==============================================================================
528
+
529
+ export inferType = (value) ->
530
+ return 'VARCHAR' if value is null or value is undefined
531
+
532
+ switch typeof value
533
+ when 'boolean' then 'BOOLEAN'
534
+ when 'number'
535
+ if Number.isInteger value
536
+ if value >= -2147483648 and value <= 2147483647 then 'INTEGER' else 'BIGINT'
537
+ else 'DOUBLE'
538
+ when 'bigint' then 'BIGINT'
539
+ when 'string'
540
+ if /^\d{4}-\d{2}-\d{2}$/.test value then 'DATE'
541
+ else if /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test value then 'TIMESTAMP'
542
+ else 'VARCHAR'
543
+ when 'object'
544
+ if value instanceof Date then 'TIMESTAMPTZ'
545
+ else 'VARCHAR'
546
+ else 'VARCHAR'