@reddb-io/client-bun 1.0.8 → 1.1.0
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/CHANGELOG.md +6 -0
- package/index.ts +298 -76
- package/package.json +5 -2
- package/params.test.ts +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @reddb-io/client-bun
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`9bef862`](https://github.com/reddb-io/reddb/commit/9bef862babe56cfbc75850c94c2b8f863a6fd8de) Thanks [@filipeforattini](https://github.com/filipeforattini)! - Prepare the 1.1 line with parameterized query coverage across engine, transports, and drivers; ASK citation, streaming, cache, failover, audit, and gRPC/MCP surfaces; MVCC transaction recovery improvements; graph/vector/probabilistic query fixes; SDK helper APIs; and release asset hardening.
|
|
8
|
+
|
|
3
9
|
## 1.0.8
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/index.ts
CHANGED
|
@@ -1,31 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RedDB Wire Protocol Client for Bun
|
|
2
|
+
* RedDB Wire Protocol Client for Bun.
|
|
3
3
|
*
|
|
4
|
-
* Uses Bun's native TCP socket
|
|
5
|
-
* Speaks the RedDB binary TCP wire protocol directly.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* import { connect } from '@reddb-io/client-bun'
|
|
9
|
-
* const conn = await connect('127.0.0.1:5050')
|
|
10
|
-
* const result = await conn.query('SELECT * FROM users WHERE _entity_id = 1')
|
|
11
|
-
* conn.close()
|
|
4
|
+
* Uses Bun's native TCP socket and speaks the RedWire v1 frame protocol.
|
|
12
5
|
*/
|
|
13
6
|
|
|
7
|
+
const MAGIC = 0xfe
|
|
8
|
+
const SUPPORTED_VERSION = 0x01
|
|
9
|
+
const FRAME_HEADER_SIZE = 16
|
|
10
|
+
const MAX_FRAME_SIZE = 16 * 1024 * 1024
|
|
11
|
+
const FEATURE_PARAMS = 0x0000_0001
|
|
12
|
+
|
|
14
13
|
const MSG_QUERY = 0x01
|
|
15
14
|
const MSG_RESULT = 0x02
|
|
16
15
|
const MSG_ERROR = 0x03
|
|
17
16
|
const MSG_BULK_INSERT = 0x04
|
|
18
17
|
const MSG_BULK_OK = 0x05
|
|
18
|
+
const MSG_HELLO = 0x10
|
|
19
|
+
const MSG_HELLO_ACK = 0x11
|
|
20
|
+
const MSG_AUTH_RESPONSE = 0x13
|
|
21
|
+
const MSG_AUTH_OK = 0x14
|
|
22
|
+
const MSG_AUTH_FAIL = 0x15
|
|
23
|
+
const MSG_BYE = 0x16
|
|
24
|
+
const MSG_QUERY_WITH_PARAMS = 0x28
|
|
25
|
+
|
|
26
|
+
const VALUE_NULL = 0x00
|
|
27
|
+
const VALUE_BOOL = 0x01
|
|
28
|
+
const VALUE_INT = 0x02
|
|
29
|
+
const VALUE_FLOAT = 0x03
|
|
30
|
+
const VALUE_TEXT = 0x04
|
|
31
|
+
const VALUE_BYTES = 0x05
|
|
32
|
+
const VALUE_VECTOR = 0x06
|
|
33
|
+
const VALUE_JSON = 0x07
|
|
34
|
+
const VALUE_TIMESTAMP = 0x08
|
|
35
|
+
const VALUE_UUID = 0x09
|
|
36
|
+
|
|
37
|
+
type QueryParam =
|
|
38
|
+
| null
|
|
39
|
+
| boolean
|
|
40
|
+
| number
|
|
41
|
+
| string
|
|
42
|
+
| Uint8Array
|
|
43
|
+
| Date
|
|
44
|
+
| Float32Array
|
|
45
|
+
| Float64Array
|
|
46
|
+
| number[]
|
|
47
|
+
| Record<string, unknown>
|
|
19
48
|
|
|
20
49
|
interface PendingRequest {
|
|
21
50
|
resolve: (value: { type: number; payload: Buffer }) => void
|
|
22
51
|
reject: (error: Error) => void
|
|
23
52
|
}
|
|
24
53
|
|
|
54
|
+
export class RedDBError extends Error {
|
|
55
|
+
code: string
|
|
56
|
+
|
|
57
|
+
constructor(code: string, message: string) {
|
|
58
|
+
super(message)
|
|
59
|
+
this.name = 'RedDBError'
|
|
60
|
+
this.code = code
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
export class RedDBConnection {
|
|
26
65
|
private socket: ReturnType<typeof Bun.connect> extends Promise<infer T> ? T : never
|
|
27
66
|
private pending: PendingRequest[] = []
|
|
28
67
|
private buffer = Buffer.alloc(0)
|
|
68
|
+
private nextCorr = 1n
|
|
69
|
+
private serverFeatures = 0
|
|
29
70
|
|
|
30
71
|
constructor(socket: any) {
|
|
31
72
|
this.socket = socket
|
|
@@ -37,19 +78,22 @@ export class RedDBConnection {
|
|
|
37
78
|
}
|
|
38
79
|
|
|
39
80
|
private _tryResolve() {
|
|
40
|
-
while (this.buffer.length >=
|
|
81
|
+
while (this.buffer.length >= FRAME_HEADER_SIZE && this.pending.length > 0) {
|
|
41
82
|
const totalLen = this.buffer.readUInt32LE(0)
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
if (totalLen < FRAME_HEADER_SIZE || totalLen > MAX_FRAME_SIZE) {
|
|
84
|
+
const { reject } = this.pending.shift()!
|
|
85
|
+
reject(new RedDBError('FRAME_INVALID_LENGTH', `length=${totalLen}`))
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (this.buffer.length < totalLen) break
|
|
44
89
|
|
|
45
90
|
const msgType = this.buffer[4]
|
|
46
|
-
const payload = this.buffer.subarray(
|
|
47
|
-
this.buffer = this.buffer.subarray(
|
|
91
|
+
const payload = this.buffer.subarray(FRAME_HEADER_SIZE, totalLen)
|
|
92
|
+
this.buffer = this.buffer.subarray(totalLen)
|
|
48
93
|
|
|
49
94
|
const { resolve, reject } = this.pending.shift()!
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
reject(new Error(payload.toString('utf8')))
|
|
95
|
+
if (msgType === MSG_ERROR || msgType === MSG_AUTH_FAIL) {
|
|
96
|
+
reject(new RedDBError('ENGINE', payload.toString('utf8')))
|
|
53
97
|
} else {
|
|
54
98
|
resolve({ type: msgType, payload: Buffer.from(payload) })
|
|
55
99
|
}
|
|
@@ -59,77 +103,111 @@ export class RedDBConnection {
|
|
|
59
103
|
private _send(msgType: number, payload: Buffer): Promise<{ type: number; payload: Buffer }> {
|
|
60
104
|
return new Promise((resolve, reject) => {
|
|
61
105
|
this.pending.push({ resolve, reject })
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
const corr = this.nextCorr
|
|
107
|
+
this.nextCorr += 1n
|
|
108
|
+
this.socket.write(encodeFrame(msgType, corr, payload))
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async _handshake() {
|
|
113
|
+
this.socket.write(Buffer.from([MAGIC, SUPPORTED_VERSION]))
|
|
114
|
+
const hello = jsonBytes({
|
|
115
|
+
versions: [SUPPORTED_VERSION],
|
|
116
|
+
auth_methods: ['anonymous', 'bearer'],
|
|
117
|
+
features: 0,
|
|
118
|
+
client_name: 'reddb-bun/1.0',
|
|
66
119
|
})
|
|
120
|
+
const ack = await this._send(MSG_HELLO, hello)
|
|
121
|
+
if (ack.type !== MSG_HELLO_ACK) {
|
|
122
|
+
throw new RedDBError('PROTOCOL', `expected HelloAck, got ${ack.type}`)
|
|
123
|
+
}
|
|
124
|
+
const parsedAck = jsonOf(ack.payload) ?? {}
|
|
125
|
+
const chosenAuth = parsedAck.auth
|
|
126
|
+
if (chosenAuth !== 'anonymous') {
|
|
127
|
+
throw new RedDBError('AUTH_REFUSED', `unsupported auth method: ${chosenAuth}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const authOk = await this._send(MSG_AUTH_RESPONSE, Buffer.alloc(0))
|
|
131
|
+
if (authOk.type !== MSG_AUTH_OK) {
|
|
132
|
+
throw new RedDBError('PROTOCOL', `expected AuthOk, got ${authOk.type}`)
|
|
133
|
+
}
|
|
134
|
+
const session = jsonOf(authOk.payload) ?? {}
|
|
135
|
+
this.serverFeatures = numberOr(session.features, numberOr(parsedAck.features, 0))
|
|
67
136
|
}
|
|
68
137
|
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
supportsParams(): boolean {
|
|
139
|
+
return (this.serverFeatures & FEATURE_PARAMS) === FEATURE_PARAMS
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async query(sql: string, params: QueryParam[]): Promise<string>
|
|
143
|
+
async query(sql: string, ...params: QueryParam[]): Promise<string>
|
|
144
|
+
async query(sql: string, ...params: unknown[]): Promise<string> {
|
|
145
|
+
const wireParams = normalizeQueryParams(params)
|
|
146
|
+
const hasParams = wireParams.length > 0
|
|
147
|
+
if (hasParams && !this.supportsParams()) {
|
|
148
|
+
throw new RedDBError(
|
|
149
|
+
'PARAMS_UNSUPPORTED',
|
|
150
|
+
'server did not advertise FEATURE_PARAMS; upgrade the server',
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
const resp = await this._send(
|
|
154
|
+
hasParams ? MSG_QUERY_WITH_PARAMS : MSG_QUERY,
|
|
155
|
+
hasParams
|
|
156
|
+
? encodeQueryWithParams(sql, wireParams)
|
|
157
|
+
: Buffer.from(sql, 'utf8'),
|
|
158
|
+
)
|
|
159
|
+
if (resp.type !== MSG_RESULT) {
|
|
160
|
+
throw new RedDBError('PROTOCOL', `expected Result, got ${resp.type}`)
|
|
161
|
+
}
|
|
71
162
|
return resp.payload.toString('utf8')
|
|
72
163
|
}
|
|
73
164
|
|
|
74
|
-
async
|
|
75
|
-
|
|
165
|
+
async execute(sql: string, params: QueryParam[]): Promise<string>
|
|
166
|
+
async execute(sql: string, ...params: QueryParam[]): Promise<string>
|
|
167
|
+
async execute(sql: string, ...params: unknown[]): Promise<string> {
|
|
168
|
+
return this.query(sql, ...(params as QueryParam[]))
|
|
76
169
|
}
|
|
77
170
|
|
|
78
|
-
async
|
|
79
|
-
|
|
80
|
-
|
|
171
|
+
async queryParsed(sql: string, params: QueryParam[]): Promise<any>
|
|
172
|
+
async queryParsed(sql: string, ...params: QueryParam[]): Promise<any>
|
|
173
|
+
async queryParsed(sql: string, ...params: unknown[]): Promise<any> {
|
|
174
|
+
return JSON.parse(await this.query(sql, ...(params as QueryParam[])))
|
|
81
175
|
}
|
|
82
176
|
|
|
83
|
-
async
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const parts: Buffer[] = [header]
|
|
91
|
-
for (const p of jsonPayloads) {
|
|
92
|
-
const jsonBuf = Buffer.from(p, 'utf8')
|
|
93
|
-
const lenBuf = Buffer.alloc(4)
|
|
94
|
-
lenBuf.writeUInt32LE(jsonBuf.length, 0)
|
|
95
|
-
parts.push(lenBuf, jsonBuf)
|
|
96
|
-
}
|
|
177
|
+
async queryRaw(sql: string, params: QueryParam[]): Promise<number>
|
|
178
|
+
async queryRaw(sql: string, ...params: QueryParam[]): Promise<number>
|
|
179
|
+
async queryRaw(sql: string, ...params: unknown[]): Promise<number> {
|
|
180
|
+
const resp = await this.query(sql, ...(params as QueryParam[]))
|
|
181
|
+
return Buffer.byteLength(resp)
|
|
182
|
+
}
|
|
97
183
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
184
|
+
async bulkInsert(collection: string, jsonPayloads: string[]): Promise<number> {
|
|
185
|
+
const payloads = jsonPayloads.map((p) => JSON.parse(p))
|
|
186
|
+
const resp = await this._send(
|
|
187
|
+
MSG_BULK_INSERT,
|
|
188
|
+
jsonBytes({ collection, payloads }),
|
|
189
|
+
)
|
|
190
|
+
if (resp.type !== MSG_BULK_OK) {
|
|
191
|
+
throw new RedDBError('PROTOCOL', `expected BulkOk, got ${resp.type}`)
|
|
101
192
|
}
|
|
193
|
+
const json = jsonOf(resp.payload)
|
|
194
|
+
if (json && typeof json.affected === 'number') return json.affected
|
|
195
|
+
if (resp.payload.length >= 8) return Number(resp.payload.readBigUInt64LE(0))
|
|
102
196
|
return 0
|
|
103
197
|
}
|
|
104
198
|
|
|
105
199
|
close() {
|
|
106
|
-
|
|
200
|
+
try {
|
|
201
|
+
this.socket.write(encodeFrame(MSG_BYE, this.nextCorr, Buffer.alloc(0)))
|
|
202
|
+
this.nextCorr += 1n
|
|
203
|
+
} finally {
|
|
204
|
+
this.socket.end()
|
|
205
|
+
}
|
|
107
206
|
}
|
|
108
207
|
}
|
|
109
208
|
|
|
110
209
|
export async function connect(addr: string): Promise<RedDBConnection> {
|
|
111
|
-
|
|
112
|
-
const port = parseInt(portStr, 10)
|
|
113
|
-
|
|
114
|
-
let connRef: RedDBConnection | null = null
|
|
115
|
-
|
|
116
|
-
const socket = await Bun.connect({
|
|
117
|
-
hostname: host,
|
|
118
|
-
port,
|
|
119
|
-
socket: {
|
|
120
|
-
data(socket, data) {
|
|
121
|
-
connRef?._onData(Buffer.from(data))
|
|
122
|
-
},
|
|
123
|
-
error(socket, error) {
|
|
124
|
-
console.error('RedDB wire error:', error)
|
|
125
|
-
},
|
|
126
|
-
close() {},
|
|
127
|
-
open() {},
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
connRef = new RedDBConnection(socket)
|
|
132
|
-
return connRef
|
|
210
|
+
return connectWithOptions(addr)
|
|
133
211
|
}
|
|
134
212
|
|
|
135
213
|
/**
|
|
@@ -141,24 +219,27 @@ export async function connectTls(
|
|
|
141
219
|
addr: string,
|
|
142
220
|
opts: { ca?: string; rejectUnauthorized?: boolean } = {},
|
|
143
221
|
): Promise<RedDBConnection> {
|
|
222
|
+
return connectWithOptions(addr, {
|
|
223
|
+
ca: opts.ca,
|
|
224
|
+
rejectUnauthorized: opts.rejectUnauthorized ?? true,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function connectWithOptions(addr: string, tls?: { ca?: string; rejectUnauthorized: boolean }) {
|
|
144
229
|
const [host, portStr] = addr.split(':')
|
|
145
230
|
const port = parseInt(portStr, 10)
|
|
146
|
-
|
|
147
231
|
let connRef: RedDBConnection | null = null
|
|
148
232
|
|
|
149
233
|
const socket = await Bun.connect({
|
|
150
234
|
hostname: host,
|
|
151
235
|
port,
|
|
152
|
-
tls: {
|
|
153
|
-
ca: opts.ca,
|
|
154
|
-
rejectUnauthorized: opts.rejectUnauthorized ?? true,
|
|
155
|
-
},
|
|
236
|
+
...(tls ? { tls } : {}),
|
|
156
237
|
socket: {
|
|
157
|
-
data(
|
|
238
|
+
data(_socket, data) {
|
|
158
239
|
connRef?._onData(Buffer.from(data))
|
|
159
240
|
},
|
|
160
|
-
error(
|
|
161
|
-
console.error('RedDB wire
|
|
241
|
+
error(_socket, error) {
|
|
242
|
+
console.error('RedDB wire error:', error)
|
|
162
243
|
},
|
|
163
244
|
close() {},
|
|
164
245
|
open() {},
|
|
@@ -166,5 +247,146 @@ export async function connectTls(
|
|
|
166
247
|
})
|
|
167
248
|
|
|
168
249
|
connRef = new RedDBConnection(socket)
|
|
250
|
+
await connRef._handshake()
|
|
169
251
|
return connRef
|
|
170
252
|
}
|
|
253
|
+
|
|
254
|
+
function encodeFrame(kind: number, correlationId: bigint, payload: Buffer): Buffer {
|
|
255
|
+
const totalLen = FRAME_HEADER_SIZE + payload.length
|
|
256
|
+
if (totalLen > MAX_FRAME_SIZE) {
|
|
257
|
+
throw new RedDBError('FRAME_TOO_LARGE', `frame ${totalLen} > ${MAX_FRAME_SIZE}`)
|
|
258
|
+
}
|
|
259
|
+
const out = Buffer.alloc(totalLen)
|
|
260
|
+
out.writeUInt32LE(totalLen, 0)
|
|
261
|
+
out[4] = kind
|
|
262
|
+
out[5] = 0
|
|
263
|
+
out.writeUInt16LE(0, 6)
|
|
264
|
+
out.writeBigUInt64LE(correlationId, 8)
|
|
265
|
+
payload.copy(out, FRAME_HEADER_SIZE)
|
|
266
|
+
return out
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeQueryParams(args: unknown[]): QueryParam[] {
|
|
270
|
+
if (args.length === 0) return []
|
|
271
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0] as QueryParam[]
|
|
272
|
+
return args as QueryParam[]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function encodeQueryWithParams(sql: string, params: QueryParam[]): Buffer {
|
|
276
|
+
if (params.length > 65_536) {
|
|
277
|
+
throw new RedDBError('PARAM_COUNT_OVER_LIMIT', `param_count ${params.length} > 65536`)
|
|
278
|
+
}
|
|
279
|
+
const sqlBytes = Buffer.from(sql, 'utf8')
|
|
280
|
+
const values = params.map(encodeValue)
|
|
281
|
+
let total = 4 + sqlBytes.length + 4
|
|
282
|
+
for (const value of values) total += value.length
|
|
283
|
+
const out = Buffer.alloc(total)
|
|
284
|
+
let pos = 0
|
|
285
|
+
out.writeUInt32LE(sqlBytes.length, pos); pos += 4
|
|
286
|
+
sqlBytes.copy(out, pos); pos += sqlBytes.length
|
|
287
|
+
out.writeUInt32LE(values.length, pos); pos += 4
|
|
288
|
+
for (const value of values) {
|
|
289
|
+
value.copy(out, pos)
|
|
290
|
+
pos += value.length
|
|
291
|
+
}
|
|
292
|
+
return out
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function encodeValue(value: QueryParam): Buffer {
|
|
296
|
+
if (value === null) return Buffer.from([VALUE_NULL])
|
|
297
|
+
if (typeof value === 'boolean') return Buffer.from([VALUE_BOOL, value ? 1 : 0])
|
|
298
|
+
if (typeof value === 'number') {
|
|
299
|
+
const out = Buffer.alloc(9)
|
|
300
|
+
if (Number.isInteger(value) && value >= -(2 ** 53) && value <= 2 ** 53) {
|
|
301
|
+
out[0] = VALUE_INT
|
|
302
|
+
out.writeBigInt64LE(BigInt(value), 1)
|
|
303
|
+
} else {
|
|
304
|
+
out[0] = VALUE_FLOAT
|
|
305
|
+
out.writeDoubleLE(value, 1)
|
|
306
|
+
}
|
|
307
|
+
return out
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === 'string') return encodeBytes(VALUE_TEXT, Buffer.from(value, 'utf8'))
|
|
310
|
+
if (value instanceof Uint8Array) {
|
|
311
|
+
return encodeBytes(VALUE_BYTES, Buffer.from(value.buffer, value.byteOffset, value.byteLength))
|
|
312
|
+
}
|
|
313
|
+
if (value instanceof Date) {
|
|
314
|
+
if (Number.isNaN(value.getTime())) {
|
|
315
|
+
throw new RedDBError('UNSUPPORTED_PARAM', 'cannot encode invalid Date query parameter')
|
|
316
|
+
}
|
|
317
|
+
const out = Buffer.alloc(9)
|
|
318
|
+
out[0] = VALUE_TIMESTAMP
|
|
319
|
+
out.writeBigInt64LE(BigInt(value.getTime()) * 1_000_000n, 1)
|
|
320
|
+
return out
|
|
321
|
+
}
|
|
322
|
+
if (value instanceof Float32Array) return encodeVector(value)
|
|
323
|
+
if (value instanceof Float64Array) return encodeVector(Float32Array.from(value))
|
|
324
|
+
if (Array.isArray(value)) {
|
|
325
|
+
if (value.every((item) => typeof item === 'number')) {
|
|
326
|
+
return encodeVector(Float32Array.from(value))
|
|
327
|
+
}
|
|
328
|
+
throw new RedDBError('UNSUPPORTED_PARAM', 'array query parameters must contain only numbers')
|
|
329
|
+
}
|
|
330
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
331
|
+
const record = value as Record<string, unknown>
|
|
332
|
+
const keys = Object.keys(record)
|
|
333
|
+
if (keys.length === 1 && typeof record.$uuid === 'string') {
|
|
334
|
+
const out = Buffer.alloc(17)
|
|
335
|
+
out[0] = VALUE_UUID
|
|
336
|
+
Buffer.from(record.$uuid.replace(/-/g, ''), 'hex').copy(out, 1)
|
|
337
|
+
return out
|
|
338
|
+
}
|
|
339
|
+
return encodeBytes(VALUE_JSON, Buffer.from(canonicalJson(value), 'utf8'))
|
|
340
|
+
}
|
|
341
|
+
throw new RedDBError('UNSUPPORTED_PARAM', `cannot encode query parameter of type ${typeof value}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function encodeBytes(tag: number, bytes: Buffer): Buffer {
|
|
345
|
+
const out = Buffer.alloc(1 + 4 + bytes.length)
|
|
346
|
+
out[0] = tag
|
|
347
|
+
out.writeUInt32LE(bytes.length, 1)
|
|
348
|
+
bytes.copy(out, 5)
|
|
349
|
+
return out
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function encodeVector(values: Float32Array): Buffer {
|
|
353
|
+
const out = Buffer.alloc(1 + 4 + values.length * 4)
|
|
354
|
+
out[0] = VALUE_VECTOR
|
|
355
|
+
out.writeUInt32LE(values.length, 1)
|
|
356
|
+
for (let i = 0; i < values.length; i++) {
|
|
357
|
+
out.writeFloatLE(values[i], 5 + i * 4)
|
|
358
|
+
}
|
|
359
|
+
return out
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function canonicalJson(value: unknown): string {
|
|
363
|
+
if (value === null) return 'null'
|
|
364
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'null'
|
|
365
|
+
if (typeof value === 'string') return JSON.stringify(value)
|
|
366
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
367
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
368
|
+
if (typeof value === 'object') {
|
|
369
|
+
const record = value as Record<string, unknown>
|
|
370
|
+
return `{${Object.keys(record).sort()
|
|
371
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
|
372
|
+
.join(',')}}`
|
|
373
|
+
}
|
|
374
|
+
return 'null'
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function jsonBytes(value: unknown): Buffer {
|
|
378
|
+
return Buffer.from(JSON.stringify(value), 'utf8')
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function jsonOf(bytes: Buffer): any {
|
|
382
|
+
if (bytes.length === 0) return null
|
|
383
|
+
try {
|
|
384
|
+
return JSON.parse(bytes.toString('utf8'))
|
|
385
|
+
} catch {
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function numberOr(value: unknown, fallback: number): number {
|
|
391
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
392
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/client-bun",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "RedDB wire protocol client for Bun — ultra-fast native TCP",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,5 +10,8 @@
|
|
|
10
10
|
"client",
|
|
11
11
|
"bun",
|
|
12
12
|
"wire-protocol"
|
|
13
|
-
]
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun run params.test.ts"
|
|
16
|
+
}
|
|
14
17
|
}
|
package/params.test.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { connect } from '../js/src/index.js'
|
|
6
|
+
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const DEFAULT_BINARY = resolve(HERE, '..', '..', 'target', 'debug', 'red')
|
|
9
|
+
const BINARY = process.env.REDDB_BINARY_PATH || DEFAULT_BINARY
|
|
10
|
+
|
|
11
|
+
function assert(condition: unknown, message: string): asserts condition {
|
|
12
|
+
if (!condition) throw new Error(message)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertEqual<T>(actual: T, expected: T, message: string) {
|
|
16
|
+
if (actual !== expected) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!existsSync(BINARY)) {
|
|
24
|
+
console.log(`SKIP: binary not found at ${BINARY}`)
|
|
25
|
+
process.exit(0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const db = await connect('memory://', { binary: BINARY })
|
|
29
|
+
try {
|
|
30
|
+
await db.query('CREATE TABLE bun_params (id INTEGER, name TEXT)')
|
|
31
|
+
await db.query('INSERT INTO bun_params (id, name) VALUES ($1, $2)', [1, 'Bun'])
|
|
32
|
+
await db.query('INSERT INTO bun_params (id, name) VALUES ($1, $2)', [2, 'Node'])
|
|
33
|
+
|
|
34
|
+
const selected = await db.query(
|
|
35
|
+
'SELECT * FROM bun_params WHERE id = $1 AND name = $2',
|
|
36
|
+
[1, 'Bun'],
|
|
37
|
+
)
|
|
38
|
+
assert(Array.isArray(selected.rows), 'SELECT rows should be an array')
|
|
39
|
+
assertEqual(selected.rows.length, 1, 'SELECT should match one row')
|
|
40
|
+
assertEqual(selected.rows[0].name, 'Bun', 'SELECT should bind text and int params')
|
|
41
|
+
|
|
42
|
+
await db.query(
|
|
43
|
+
'INSERT INTO bun_embeddings VECTOR (dense, content) VALUES ($1, $2)',
|
|
44
|
+
[new Float32Array([1.0, 0.0]), 'bun vector'],
|
|
45
|
+
)
|
|
46
|
+
await db.query(
|
|
47
|
+
'INSERT INTO bun_embeddings VECTOR (dense, content) VALUES ($1, $2)',
|
|
48
|
+
[new Float32Array([0.0, 1.0]), 'other vector'],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const similar = await db.query(
|
|
52
|
+
'SEARCH SIMILAR $1 COLLECTION bun_embeddings LIMIT 1',
|
|
53
|
+
[new Float32Array([1.0, 0.0])],
|
|
54
|
+
)
|
|
55
|
+
assert(Array.isArray(similar.rows), 'SEARCH rows should be an array')
|
|
56
|
+
assertEqual(similar.rows.length, 1, 'SEARCH should match one vector row')
|
|
57
|
+
assertEqual(similar.rows[0].score, 1, 'SEARCH should bind Float32Array vector')
|
|
58
|
+
} finally {
|
|
59
|
+
await db.close()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('ok shared SDK parameterized queries run under Bun')
|