@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 +258 -0
- package/README.md +28 -0
- package/db.rip +92 -1
- package/lib/duckdb-binary.rip +510 -0
- package/package.json +6 -2
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.
|
|
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
|
}
|