@reddb-io/client-bun 1.0.8 → 1.1.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/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 for maximum performance.
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 >= 5 && this.pending.length > 0) {
81
+ while (this.buffer.length >= FRAME_HEADER_SIZE && this.pending.length > 0) {
41
82
  const totalLen = this.buffer.readUInt32LE(0)
42
- const frameSize = 4 + totalLen
43
- if (this.buffer.length < frameSize) break
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(5, frameSize)
47
- this.buffer = this.buffer.subarray(frameSize)
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
- if (msgType === MSG_ERROR) {
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 header = Buffer.alloc(5)
63
- header.writeUInt32LE(1 + payload.length, 0)
64
- header[4] = msgType
65
- this.socket.write(Buffer.concat([header, payload]))
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
- async query(sql: string): Promise<string> {
70
- const resp = await this._send(MSG_QUERY, Buffer.from(sql, 'utf8'))
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 queryParsed(sql: string): Promise<any> {
75
- return JSON.parse(await this.query(sql))
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 queryRaw(sql: string): Promise<number> {
79
- const resp = await this._send(MSG_QUERY, Buffer.from(sql, 'utf8'))
80
- return resp.payload.length
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 bulkInsert(collection: string, jsonPayloads: string[]): Promise<number> {
84
- const collBuf = Buffer.from(collection, 'utf8')
85
- const header = Buffer.alloc(2 + collBuf.length + 4)
86
- header.writeUInt16LE(collBuf.length, 0)
87
- collBuf.copy(header, 2)
88
- header.writeUInt32LE(jsonPayloads.length, 2 + collBuf.length)
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
- const resp = await this._send(MSG_BULK_INSERT, Buffer.concat(parts))
99
- if (resp.payload.length >= 8) {
100
- return Number(resp.payload.readBigUInt64LE(0))
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
- this.socket.end()
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
- const [host, portStr] = addr.split(':')
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(socket, data) {
238
+ data(_socket, data) {
158
239
  connRef?._onData(Buffer.from(data))
159
240
  },
160
- error(socket, error) {
161
- console.error('RedDB wire+tls error:', error)
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.8",
3
+ "version": "1.1.1",
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')