@reddb-io/client-bun 1.23.2 → 1.24.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/README.md +64 -0
- package/index.ts +6 -3
- package/package.json +2 -2
- package/params.test.ts +11 -0
- package/wire-legacy.test.ts +66 -0
- package/wire.test.ts +58 -0
package/CHANGELOG.md
CHANGED
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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.
|
|
3
|
+
"version": "1.24.0",
|
|
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
|
+
}
|