@reddb-io/client-bun 1.23.2 → 1.23.4

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,9 @@
1
1
  # @reddb-io/client-bun
2
2
 
3
+ ## 1.23.4
4
+
5
+ ## 1.23.3
6
+
3
7
  ## 1.23.2
4
8
 
5
9
  ## 1.23.1
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # Bun RedWire client
2
+
3
+ ```ts
4
+ import { connect } from '@reddb-io/client-bun'
5
+
6
+ const db = await connect('127.0.0.1:6380')
7
+ try {
8
+ const result = await db.queryParsed('SELECT id,name FROM users WHERE id=$1', [1])
9
+ console.log(result.result.records.map(record => record.values))
10
+ } finally {
11
+ db.close()
12
+ }
13
+ ```
14
+
15
+ On servers advertising `FEATURE_PARAMS`, bound and unbound queries both return
16
+ the canonical query envelope: records are at `result.records`, write counts at
17
+ `affected_rows` (when nonzero), and the command at `statement_type` (for writes).
18
+ `query()` returns its JSON string; `queryParsed()` parses it. This also applies
19
+ to `query(sql, [])`. Previously unbound queries returned only a summary, which
20
+ omitted records; consumers of that summary must update their field paths.
21
+
22
+ Older servers without `FEATURE_PARAMS` retain the legacy summary response for
23
+ unbound queries. Binding values to such servers raises `PARAMS_UNSUPPORTED`.
24
+
25
+ Run the shared SDK smoke test and this package's actual TCP-client regression:
26
+
27
+ ```sh
28
+ REDDB_BINARY_PATH=/absolute/path/to/red bun run test
29
+ ```
30
+
31
+ <!-- contract-matrix:begin -->
32
+ ## Public-surface support
33
+
34
+ > Generated from [`docs/conformance/public-surface-contract-matrix.json`](/docs/conformance/public-surface-contract-matrix.json) by `scripts/gen-docs-from-matrix.mjs`. Do not edit between the markers by hand — run `node scripts/gen-docs-from-matrix.mjs --write`. The matrix is the source of truth; this block can never claim more than it, and CI (`docs-matrix`) fails on drift.
35
+ >
36
+ > Driver-helper (SDK Helper Spec v1.0) support for every public promise. A helper not marked supported here is not promised by this driver.
37
+
38
+ | Promise | driver_helpers |
39
+ | --- | --- |
40
+ | **PSC-001** — RedDB is one multi-model database (tables, graph, KV, timeseries, probabilistic, vector, queue, documents) backed by a single file. | ✅ supported |
41
+ | **PSC-002** — MATCH supports node, edge, label, property, and LIMIT projections. | ✅ supported |
42
+ | **PSC-003** — GRAPH algorithms accept semantic identifiers, limits, ordering, and return stable rich rows. | ❌ unsupported |
43
+ | **PSC-004** — INSERT creates rows, documents, and native timeseries points. | ✅ supported |
44
+ | **PSC-005** — HLL/SKETCH/FILTER expose write and read commands for cardinality, frequency, and membership. | ⚠️ partial |
45
+ | **PSC-006** — Timeseries stores timestamped metrics with tags and supports query/readback. | ⚠️ partial |
46
+ | **PSC-007** — Documents are first-class: create, read, update, delete, and SQL analytics over JSON. | ✅ supported |
47
+ | **PSC-008** — KV helpers expose get/put/delete; get of a missing key returns null, delete reports affected. | ✅ supported |
48
+ | **PSC-009** — Queue helpers expose create/push/peek/pop/len/purge with FIFO semantics; empty pop is not an error. | ✅ supported |
49
+ | **PSC-010** — Transactions are imperative (begin/commit/rollback) plus a run(callback) form; empty SQL rejects with INVALID_ARGUMENT. | ✅ supported |
50
+ | **PSC-011** — SQL aggregate, projection, expression, and mutation behaviour matches ordinary SQL expectations where advertised. | ✅ supported |
51
+ | **PSC-012** — Server transports expose the same query contract as embedded (HTTP, RedWire, gRPC parity). | ✅ supported |
52
+ | **PSC-013** — Official drivers implement the SDK Helper Spec v1.0 conformance suite (all 22 §12 case IDs). | ✅ supported |
53
+ | **PSC-014** — ASK / SEARCH semantic surfaces return ranked results with stable shape. | ⚠️ partial |
54
+
55
+ _Status legend: ✅ supported · ⚠️ partial (known gaps) · ❌ unsupported._
56
+ <!-- contract-matrix:end -->
57
+
58
+ ## SQL quotation compatibility
59
+
60
+ SQL double quotes delimit identifiers; single quotes delimit text. For example,
61
+ `SELECT "title" FROM articles WHERE author = 'alice'` reads the `title` column.
62
+ Bind application values as parameters. JSON object/array strings keep double
63
+ quotes. See the [SQL quotation migration guide](../../docs/query/sql-quoting.md)
64
+ before upgrading applications that used double-quoted SQL text literals.
package/index.ts CHANGED
@@ -144,15 +144,18 @@ export class RedDBConnection {
144
144
  async query(sql: string, ...params: unknown[]): Promise<string> {
145
145
  const wireParams = normalizeQueryParams(params)
146
146
  const hasParams = wireParams.length > 0
147
- if (hasParams && !this.supportsParams()) {
147
+ const fullResult = this.supportsParams()
148
+ if (hasParams && !fullResult) {
148
149
  throw new RedDBError(
149
150
  'PARAMS_UNSUPPORTED',
150
151
  'server did not advertise FEATURE_PARAMS; upgrade the server',
151
152
  )
152
153
  }
154
+ // Legacy Query returns a summary only. Capable servers return rows via
155
+ // QueryWithParams even when the parameter vector is empty.
153
156
  const resp = await this._send(
154
- hasParams ? MSG_QUERY_WITH_PARAMS : MSG_QUERY,
155
- hasParams
157
+ fullResult ? MSG_QUERY_WITH_PARAMS : MSG_QUERY,
158
+ fullResult
156
159
  ? encodeQueryWithParams(sql, wireParams)
157
160
  : Buffer.from(sql, 'utf8'),
158
161
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/client-bun",
3
- "version": "1.23.2",
3
+ "version": "1.23.4",
4
4
  "description": "RedDB wire protocol client for Bun — ultra-fast native TCP",
5
5
  "main": "index.ts",
6
6
  "license": "MIT",
@@ -12,6 +12,6 @@
12
12
  "wire-protocol"
13
13
  ],
14
14
  "scripts": {
15
- "test": "bun run params.test.ts"
15
+ "test": "bun run params.test.ts && bun run wire-legacy.test.ts && bun run wire.test.ts"
16
16
  }
17
17
  }
package/params.test.ts CHANGED
@@ -55,6 +55,17 @@ try {
55
55
  assert(Array.isArray(similar.rows), 'SEARCH rows should be an array')
56
56
  assertEqual(similar.rows.length, 1, 'SEARCH should match one vector row')
57
57
  assertEqual(similar.rows[0].score, 1, 'SEARCH should bind Float32Array vector')
58
+
59
+ const fractional = new Float32Array([0.8, 0.6])
60
+ await db.query('INSERT INTO fractional_vectors VECTOR (dense, content) VALUES ($1, $2)',
61
+ [fractional, 'fractional embedding'])
62
+ const fractionalMatch = await db.query('SEARCH SIMILAR $1 COLLECTION fractional_vectors LIMIT 1',
63
+ [fractional])
64
+ assertEqual(fractionalMatch.rows.length, 1, 'fractional vector should remain searchable')
65
+ assert(Math.abs(fractionalMatch.rows[0].score - 1) < 1e-6, 'identical fractional vectors should match')
66
+ const axisMatch = await db.query('SEARCH SIMILAR $1 COLLECTION fractional_vectors LIMIT 1',
67
+ [new Float32Array([1, 0])])
68
+ assert(Math.abs(axisMatch.rows[0].score - 0.8) < 1e-6, 'fractional coordinates survive binding')
58
69
  } finally {
59
70
  await db.close()
60
71
  }
@@ -0,0 +1,66 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { createServer } from 'node:net'
3
+ import { connect, RedDBError } from './index'
4
+
5
+ // The old protocol is intentionally summary-only. A small protocol peer lets
6
+ // us exercise capability negotiation without installing an obsolete engine.
7
+ const queries: number[] = []
8
+ const server = createServer((socket) => {
9
+ socket.setTimeout(5_000, () => socket.destroy())
10
+ let input = Buffer.alloc(0)
11
+ let magicReceived = false
12
+ socket.on('data', (chunk) => {
13
+ input = Buffer.concat([input, chunk])
14
+ if (!magicReceived) {
15
+ if (input.length < 2) return
16
+ assert.deepEqual([...input.subarray(0, 2)], [0xfe, 1])
17
+ input = input.subarray(2)
18
+ magicReceived = true
19
+ }
20
+ while (input.length >= 16) {
21
+ const length = input.readUInt32LE(0)
22
+ assert(length >= 16 && length <= 16 * 1024 * 1024)
23
+ if (input.length < length) return
24
+ const frame = input.subarray(0, length)
25
+ input = input.subarray(length)
26
+ const kind = frame[4]
27
+ if (kind === 0x16) { socket.end(); return }
28
+ let responseKind: number
29
+ let body: object
30
+ if (kind === 0x10) {
31
+ responseKind = 0x11
32
+ body = { auth: 'anonymous', features: 0 }
33
+ } else if (kind === 0x13) {
34
+ responseKind = 0x14
35
+ body = { features: 0 }
36
+ } else {
37
+ queries.push(kind)
38
+ responseKind = 0x02
39
+ body = { ok: true, affected: 0, statement: 'select' }
40
+ }
41
+ const payload = Buffer.from(JSON.stringify(body))
42
+ const response = Buffer.alloc(16 + payload.length)
43
+ response.writeUInt32LE(response.length, 0)
44
+ response[4] = responseKind
45
+ frame.copy(response, 8, 8, 16)
46
+ payload.copy(response, 16)
47
+ socket.write(response)
48
+ }
49
+ })
50
+ })
51
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
52
+ const address = server.address()
53
+ assert(address && typeof address === 'object')
54
+ let db: Awaited<ReturnType<typeof connect>> | undefined
55
+ try {
56
+ db = await connect(`127.0.0.1:${address.port}`)
57
+ assert.equal(db.supportsParams(), false)
58
+ assert.deepEqual(await db.queryParsed('SELECT 42'), { ok: true, affected: 0, statement: 'select' })
59
+ await assert.rejects(db.query('SELECT $1', [42]),
60
+ (error) => error instanceof RedDBError && error.code === 'PARAMS_UNSUPPORTED')
61
+ assert.deepEqual(queries, [0x01])
62
+ console.log('ok Bun legacy capability fallback')
63
+ } finally {
64
+ db?.close()
65
+ await new Promise<void>((resolve) => server.close(() => resolve()))
66
+ }
package/wire.test.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { strict as assert } from 'node:assert'
2
+ import { mkdtempSync, rmSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+ import { createServer } from 'node:net'
6
+ import { connect } from './index'
7
+
8
+ // Exercise this package's TCP client against a real server. An absent binary
9
+ // is a test failure: silently skipping hid the unbound-query regression.
10
+ const binary = process.env.REDDB_BINARY_PATH || resolve(import.meta.dir, '../../target/debug/red')
11
+ const reservation = createServer()
12
+ await new Promise<void>((resolve, reject) => {
13
+ reservation.once('error', reject)
14
+ reservation.listen(0, '127.0.0.1', resolve)
15
+ })
16
+ const address = reservation.address()
17
+ assert(address && typeof address === 'object')
18
+ const endpoint = `127.0.0.1:${address.port}`
19
+ await new Promise<void>((resolve) => reservation.close(() => resolve()))
20
+ const directory = mkdtempSync(join(tmpdir(), 'reddb-bun-wire-'))
21
+ const server = Bun.spawn([binary, 'server', '--path', join(directory, 'db.rdb'),
22
+ '--bind', endpoint, '--no-auth', '--no-log-file'], { stdout: 'ignore', stderr: 'inherit' })
23
+ let db: Awaited<ReturnType<typeof connect>> | undefined
24
+ try {
25
+ const deadline = Date.now() + 30_000
26
+ while (Date.now() < deadline) {
27
+ if (server.exitCode !== null) throw new Error(`server exited ${server.exitCode}`)
28
+ try {
29
+ const response = await fetch(`http://${endpoint}/health`, { signal: AbortSignal.timeout(500) })
30
+ if (response.ok) break
31
+ } catch {}
32
+ await Bun.sleep(100)
33
+ }
34
+ db = await connect(endpoint)
35
+ assert(db.supportsParams())
36
+ await db.query('CREATE TABLE wire_rows (id INTEGER, name TEXT)')
37
+ const inserted = await db.queryParsed("INSERT INTO wire_rows (id,name) VALUES (1,'Bun')")
38
+ assert.equal(inserted.affected_rows, 1)
39
+ const unbound = await db.queryParsed('SELECT id,name FROM wire_rows')
40
+ const bound = await db.queryParsed('SELECT id,name FROM wire_rows WHERE id=$1', [1])
41
+ assert.deepEqual(unbound.result.records.map((record: any) => record.values), [{ id: 1, name: 'Bun' }])
42
+ assert.deepEqual(unbound.result.records.map((record: any) => record.values), bound.result.records.map((record: any) => record.values))
43
+ assert.deepEqual((await db.queryParsed('SELECT id,name FROM wire_rows', [])).result.records.map((record: any) => record.values), bound.result.records.map((record: any) => record.values))
44
+ assert.deepEqual((await db.queryParsed('SELECT id FROM wire_rows WHERE id=2')).result.records, [])
45
+ await assert.rejects(db.query('SELECT * FROM absent_wire_table'))
46
+ const cli = Bun.spawnSync([binary, 'query', '--bind', endpoint, '--json',
47
+ "INSERT INTO wire_rows (id,name) VALUES (2,'CLI')"])
48
+ assert.equal(cli.exitCode, 0, cli.stderr.toString())
49
+ const insertedByCli = JSON.parse(cli.stdout.toString()).data
50
+ assert.equal(insertedByCli.affected, 1)
51
+ assert.equal(insertedByCli.statement, 'insert')
52
+ console.log('ok Bun wire bound/unbound result contract')
53
+ } finally {
54
+ db?.close()
55
+ server.kill()
56
+ await server.exited
57
+ rmSync(directory, { recursive: true, force: true })
58
+ }