@zvndev/powdb-client 0.4.0 → 0.6.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 +21 -0
- package/README.md +24 -1
- package/dist/index.d.ts +22 -4
- package/dist/index.js +87 -7
- package/dist/protocol.d.ts +41 -0
- package/dist/protocol.js +109 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 - 2026-06-17
|
|
4
|
+
|
|
5
|
+
- Kept the npm client release in lockstep with PowDB workspace v0.5.1.
|
|
6
|
+
- No TypeScript API changes; this is a dependency/version-alignment release.
|
|
7
|
+
|
|
8
|
+
|
|
3
9
|
All notable changes to `@zvndev/powdb-client`.
|
|
4
10
|
|
|
5
11
|
## Compatibility
|
|
6
12
|
|
|
7
13
|
| Client version | Compatible PowDB server | Notes |
|
|
8
14
|
|---|---|---|
|
|
15
|
+
| 0.5.x | 0.4.7+ | Adds `client.query(powql, params)` for `$N` parameter binding. The `QueryWithParams` (`0x04`) wire message is only understood by server ≥0.4.7; parameterized queries against an older server error out. Plain `query(powql)` calls remain compatible with 0.3.x–0.4.x. |
|
|
9
16
|
| 0.4.x | 0.3.x – 0.4.x | Wire protocol v1 plus the optional Connect `username` field. **Multi-user mode requires client ≥0.4.0 AND server ≥0.4.6** (the server enforces roles as of 0.4.6; 0.4.5 accepted the username but did not enforce `readonly`). When `user` is omitted the Connect frame is byte-identical to the 0.3.x shape, so legacy shared-password and no-auth servers work unchanged. |
|
|
10
17
|
| 0.3.x | 0.3.x – 0.4.x | Wire protocol v1. The client warns only on a major-version mismatch, so any `0.x` server connects. Minor server bumps may add new opcodes; the client tolerates unknown response codes by surfacing `PowDBError`. Pin both ends. **Caveat:** the 0.3.x client has no `user` option, so it cannot authenticate to a 0.4.5+ server running in **multi-user mode** (the server requires a username once any named user is defined). Shared-password mode works fine. |
|
|
11
18
|
|
|
@@ -13,6 +20,20 @@ The client warns on major-version mismatch with the server during the
|
|
|
13
20
|
handshake. Within `0.x`, treat any minor-version skew between client and
|
|
14
21
|
server as best-effort and pin both ends.
|
|
15
22
|
|
|
23
|
+
## [0.5.0] — 2026-06-10
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Parameter binding** — `client.query(powql, params)` accepts a second
|
|
27
|
+
argument of positional values bound to `$1`, `$2`, … placeholders in the
|
|
28
|
+
PowQL text. Values are sent as a typed parameter list in the new
|
|
29
|
+
`QueryWithParams` (`0x04`) wire message and bound at the token level on the
|
|
30
|
+
server, so they are injection-inert and byte-faithful (a value containing
|
|
31
|
+
PowQL syntax is stored verbatim, never re-parsed). Requires server ≥0.4.7.
|
|
32
|
+
|
|
33
|
+
### Compatibility
|
|
34
|
+
- Parameterized queries require **server ≥0.4.7**. Plain `query(powql)` (no
|
|
35
|
+
params) is unchanged and still works against 0.3.x–0.4.x servers.
|
|
36
|
+
|
|
16
37
|
## [0.4.0] — 2026-06-09
|
|
17
38
|
|
|
18
39
|
### Added
|
package/README.md
CHANGED
|
@@ -60,6 +60,27 @@ escapeIdent("User"); // → "User" (throws on invalid)
|
|
|
60
60
|
|
|
61
61
|
`escapeLiteral` accepts `string | number | bigint | boolean | null`. It rejects `NaN`/`Infinity`, `undefined`, objects, arrays, symbols, and `Date` — convert those yourself before passing them in.
|
|
62
62
|
|
|
63
|
+
### Parameter binding (`$N`)
|
|
64
|
+
|
|
65
|
+
For the strongest separation between code and data, pass values as positional `$N` parameters instead of interpolating them. The server binds each placeholder at the **token level** — a string becomes a literal token, never interpolated text — so an injection-shaped value is inert and can never change the query's shape. Placeholders are 1-based (`?` is not a placeholder; `??` is the COALESCE operator).
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Values are passed as the second argument, in $1, $2, … order.
|
|
69
|
+
await client.query("insert User { name := $1, email := $2, age := $3 }", [
|
|
70
|
+
name,
|
|
71
|
+
email,
|
|
72
|
+
age,
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const r = await client.query("User filter .email = $1 { .name }", [email]);
|
|
76
|
+
|
|
77
|
+
// null binds PowQL null; numbers bind as int when integral, float otherwise;
|
|
78
|
+
// bigint always binds as int.
|
|
79
|
+
await client.query("insert User { name := $1, age := $2 }", ["Dana", null]);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`QueryParam` is `string | number | bigint | boolean | null`. The params form sends the `QueryWithParams` (0x04) wire message and **requires powdb-server >= 0.4.7**. The plain `query(q)` and `query(q, { signal })` forms are unchanged.
|
|
83
|
+
|
|
63
84
|
## Authentication
|
|
64
85
|
|
|
65
86
|
For servers using the legacy shared password (`POWDB_PASSWORD`), pass
|
|
@@ -277,7 +298,7 @@ Returns a `Promise<Client>`. Options:
|
|
|
277
298
|
> **Multi-user servers:** requires client ≥0.4.0 (`user` option) and server
|
|
278
299
|
> ≥0.4.6 (enforced roles). See the version matrix under Authentication.
|
|
279
300
|
|
|
280
|
-
### `client.query(query, opts?)`
|
|
301
|
+
### `client.query(query, params?, opts?)`
|
|
281
302
|
|
|
282
303
|
Sends a PowQL query and returns a `Promise<QueryResult>`:
|
|
283
304
|
|
|
@@ -285,6 +306,8 @@ Sends a PowQL query and returns a `Promise<QueryResult>`:
|
|
|
285
306
|
- `{ kind: "scalar", value: string }` — for aggregates (`count`, `sum`, `avg`, etc.)
|
|
286
307
|
- `{ kind: "ok", affected: bigint }` — for mutations (`insert`, `update`, `delete`)
|
|
287
308
|
|
|
309
|
+
`params?: QueryParam[]` — positional values bound to `$1`, `$2`, … placeholders (see Parameter binding above; requires server ≥0.4.7). When omitted, the plain query path is used. The legacy two-argument `query(q, { signal })` form is still accepted — an array second argument is treated as params, an object as options.
|
|
310
|
+
|
|
288
311
|
`opts.signal?: AbortSignal` — aborts the returned promise (see Cancellation above).
|
|
289
312
|
|
|
290
313
|
Throws a `PowDBError` (see Structured errors above) on any failure.
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import * as tls from "node:tls";
|
|
|
18
18
|
import { EventEmitter } from "node:events";
|
|
19
19
|
import { type TypedRow, type TypedSchema } from "./typed.js";
|
|
20
20
|
/** Client library version. Compared to the server's reported version. */
|
|
21
|
-
export declare const CLIENT_VERSION = "0.
|
|
21
|
+
export declare const CLIENT_VERSION = "0.6.0";
|
|
22
22
|
export type QueryResult = {
|
|
23
23
|
kind: "rows";
|
|
24
24
|
columns: string[];
|
|
@@ -33,6 +33,15 @@ export type QueryResult = {
|
|
|
33
33
|
kind: "message";
|
|
34
34
|
message: string;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* A value bound to a positional `$N` placeholder in {@link Client.query}.
|
|
38
|
+
*
|
|
39
|
+
* The server binds these at the token level — a string is substituted as a
|
|
40
|
+
* literal token, never interpolated — so injection-shaped input is inert.
|
|
41
|
+
* Numbers bind as ints when integral and floats otherwise; `bigint` always
|
|
42
|
+
* binds as an int; `null` binds PowQL `null`.
|
|
43
|
+
*/
|
|
44
|
+
export type QueryParam = string | number | bigint | boolean | null;
|
|
36
45
|
export interface ClientOptions {
|
|
37
46
|
host: string;
|
|
38
47
|
port: number;
|
|
@@ -100,7 +109,16 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
100
109
|
* destroyed — the server will still eventually send its reply, which we
|
|
101
110
|
* silently discard so other in-flight queries keep working.
|
|
102
111
|
*/
|
|
103
|
-
query(query: string,
|
|
112
|
+
query(query: string, paramsOrOpts?: QueryParam[] | {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}, maybeOpts?: {
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}): Promise<QueryResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Run a SQL statement through the server-side SQL frontend. The plain
|
|
119
|
+
* {@link query} method remains PowQL for wire compatibility.
|
|
120
|
+
*/
|
|
121
|
+
querySql(query: string, opts?: {
|
|
104
122
|
signal?: AbortSignal;
|
|
105
123
|
}): Promise<QueryResult>;
|
|
106
124
|
/**
|
|
@@ -148,8 +166,8 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
148
166
|
private onClose;
|
|
149
167
|
}
|
|
150
168
|
export { encode, tryDecode } from "./protocol.js";
|
|
151
|
-
export type { Message } from "./protocol.js";
|
|
152
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, } from "./protocol.js";
|
|
169
|
+
export type { Message, WireParam } from "./protocol.js";
|
|
170
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, } from "./protocol.js";
|
|
153
171
|
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
154
172
|
export { Pool } from "./pool.js";
|
|
155
173
|
export type { PoolOptions } from "./pool.js";
|
package/dist/index.js
CHANGED
|
@@ -17,11 +17,33 @@
|
|
|
17
17
|
import * as net from "node:net";
|
|
18
18
|
import * as tls from "node:tls";
|
|
19
19
|
import { EventEmitter } from "node:events";
|
|
20
|
-
import { encode, tryDecode } from "./protocol.js";
|
|
20
|
+
import { encode, tryDecode, } from "./protocol.js";
|
|
21
21
|
import { PowDBError } from "./errors.js";
|
|
22
22
|
import { coerceRows, } from "./typed.js";
|
|
23
23
|
/** Client library version. Compared to the server's reported version. */
|
|
24
|
-
export const CLIENT_VERSION = "0.
|
|
24
|
+
export const CLIENT_VERSION = "0.6.0";
|
|
25
|
+
function socketChunkToBuffer(chunk) {
|
|
26
|
+
return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
27
|
+
}
|
|
28
|
+
/** Map a JS {@link QueryParam} to its wire encoding. */
|
|
29
|
+
function toWireParam(p) {
|
|
30
|
+
if (p === null)
|
|
31
|
+
return { tag: "null" };
|
|
32
|
+
switch (typeof p) {
|
|
33
|
+
case "string":
|
|
34
|
+
return { tag: "str", value: p };
|
|
35
|
+
case "boolean":
|
|
36
|
+
return { tag: "bool", value: p };
|
|
37
|
+
case "bigint":
|
|
38
|
+
return { tag: "int", value: p };
|
|
39
|
+
case "number":
|
|
40
|
+
return Number.isInteger(p)
|
|
41
|
+
? { tag: "int", value: BigInt(p) }
|
|
42
|
+
: { tag: "float", value: p };
|
|
43
|
+
default:
|
|
44
|
+
throw new PowDBError(`unsupported query parameter type: ${typeof p}`, "protocol_error");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
25
47
|
/** Module-level set of host:port pairs we've already warned about. */
|
|
26
48
|
const versionWarnings = new Set();
|
|
27
49
|
/** Extract the major component of a dotted version string, e.g. "0.2.0" → "0". */
|
|
@@ -57,7 +79,7 @@ export class Client extends EventEmitter {
|
|
|
57
79
|
super();
|
|
58
80
|
this.socket = socket;
|
|
59
81
|
this.serverVersion = serverVersion;
|
|
60
|
-
this.socket.on("data", (chunk) => this.onData(chunk));
|
|
82
|
+
this.socket.on("data", (chunk) => this.onData(socketChunkToBuffer(chunk)));
|
|
61
83
|
this.socket.on("error", (err) => this.onClose(err));
|
|
62
84
|
this.socket.on("close", () => this.onClose(null));
|
|
63
85
|
}
|
|
@@ -75,7 +97,7 @@ export class Client extends EventEmitter {
|
|
|
75
97
|
socket.removeListener("close", onClose);
|
|
76
98
|
};
|
|
77
99
|
const onData = (chunk) => {
|
|
78
|
-
scratch = Buffer.concat([scratch, chunk]);
|
|
100
|
+
scratch = Buffer.concat([scratch, socketChunkToBuffer(chunk)]);
|
|
79
101
|
let decoded;
|
|
80
102
|
try {
|
|
81
103
|
decoded = tryDecode(scratch);
|
|
@@ -143,10 +165,68 @@ export class Client extends EventEmitter {
|
|
|
143
165
|
* destroyed — the server will still eventually send its reply, which we
|
|
144
166
|
* silently discard so other in-flight queries keep working.
|
|
145
167
|
*/
|
|
146
|
-
async query(query,
|
|
168
|
+
async query(query, paramsOrOpts, maybeOpts) {
|
|
169
|
+
// Disambiguate the two overloads:
|
|
170
|
+
// query(q) — no params, no opts
|
|
171
|
+
// query(q, opts) — legacy 2-arg opts form (back-compat)
|
|
172
|
+
// query(q, params) — positional $N parameters
|
|
173
|
+
// query(q, params, opts) — params + opts
|
|
174
|
+
const hasParams = Array.isArray(paramsOrOpts);
|
|
175
|
+
const params = hasParams ? paramsOrOpts : undefined;
|
|
176
|
+
const opts = hasParams
|
|
177
|
+
? maybeOpts
|
|
178
|
+
: paramsOrOpts;
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
try {
|
|
181
|
+
const request = params === undefined
|
|
182
|
+
? { type: "Query", query }
|
|
183
|
+
: { type: "QueryWithParams", query, params: params.map(toWireParam) };
|
|
184
|
+
const reply = await this.send(request, opts);
|
|
185
|
+
let result;
|
|
186
|
+
switch (reply.type) {
|
|
187
|
+
case "ResultRows":
|
|
188
|
+
result = { kind: "rows", columns: reply.columns, rows: reply.rows };
|
|
189
|
+
break;
|
|
190
|
+
case "ResultScalar":
|
|
191
|
+
result = { kind: "scalar", value: reply.value };
|
|
192
|
+
break;
|
|
193
|
+
case "ResultOk":
|
|
194
|
+
result = { kind: "ok", affected: reply.affected };
|
|
195
|
+
break;
|
|
196
|
+
case "ResultMessage":
|
|
197
|
+
result = { kind: "message", message: reply.message };
|
|
198
|
+
break;
|
|
199
|
+
case "Error":
|
|
200
|
+
throw new PowDBError(`query failed: ${reply.message}`, "query_failed");
|
|
201
|
+
default:
|
|
202
|
+
throw new PowDBError(`unexpected reply: ${reply.type}`, "protocol_error");
|
|
203
|
+
}
|
|
204
|
+
this.emit("query", {
|
|
205
|
+
query,
|
|
206
|
+
durationMs: Date.now() - start,
|
|
207
|
+
ok: true,
|
|
208
|
+
kind: result.kind,
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
this.emit("query", {
|
|
214
|
+
query,
|
|
215
|
+
durationMs: Date.now() - start,
|
|
216
|
+
ok: false,
|
|
217
|
+
error: err,
|
|
218
|
+
});
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Run a SQL statement through the server-side SQL frontend. The plain
|
|
224
|
+
* {@link query} method remains PowQL for wire compatibility.
|
|
225
|
+
*/
|
|
226
|
+
async querySql(query, opts) {
|
|
147
227
|
const start = Date.now();
|
|
148
228
|
try {
|
|
149
|
-
const reply = await this.send({ type: "
|
|
229
|
+
const reply = await this.send({ type: "QuerySql", query }, opts);
|
|
150
230
|
let result;
|
|
151
231
|
switch (reply.type) {
|
|
152
232
|
case "ResultRows":
|
|
@@ -515,7 +595,7 @@ function openSocket(host, port, timeoutMs, tlsOpt) {
|
|
|
515
595
|
});
|
|
516
596
|
}
|
|
517
597
|
export { encode, tryDecode } from "./protocol.js";
|
|
518
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, } from "./protocol.js";
|
|
598
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, } from "./protocol.js";
|
|
519
599
|
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
520
600
|
export { Pool } from "./pool.js";
|
|
521
601
|
export { PowDBError, isPowDBError } from "./errors.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
export declare const MSG_CONNECT = 1;
|
|
10
10
|
export declare const MSG_CONNECT_OK = 2;
|
|
11
11
|
export declare const MSG_QUERY = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Query carrying positional `$N` parameters. Pure protocol addition: old
|
|
14
|
+
* servers reject it with the existing "unknown message type" error, and the
|
|
15
|
+
* plain `MSG_QUERY` frame is unchanged. Requires powdb-server ≥ 0.4.7.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MSG_QUERY_PARAMS = 4;
|
|
18
|
+
/** SQL query frame. Plain Query remains PowQL for backward compatibility. */
|
|
19
|
+
export declare const MSG_QUERY_SQL = 5;
|
|
12
20
|
export declare const MSG_RESULT_ROWS = 7;
|
|
13
21
|
export declare const MSG_RESULT_SCALAR = 8;
|
|
14
22
|
export declare const MSG_RESULT_OK = 9;
|
|
@@ -23,6 +31,32 @@ export declare const MAX_PAYLOAD_SIZE: number;
|
|
|
23
31
|
export declare const MAX_ROWS = 10000000;
|
|
24
32
|
/** Maximum number of columns allowed in a result set. */
|
|
25
33
|
export declare const MAX_COLUMNS = 4096;
|
|
34
|
+
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
35
|
+
export declare const MAX_PARAMS = 4096;
|
|
36
|
+
/**
|
|
37
|
+
* A positional parameter value for {@link MSG_QUERY_PARAMS}. Wire encoding
|
|
38
|
+
* per param: a 1-byte tag followed by the body —
|
|
39
|
+
* `0` null (no body), `1` int (8B LE i64), `2` float (8B LE f64),
|
|
40
|
+
* `3` bool (1B), `4` str (length-prefixed UTF-8).
|
|
41
|
+
*
|
|
42
|
+
* Integral numbers are sent as ints, non-integral numbers as floats; `null`
|
|
43
|
+
* binds PowQL `null`. `bigint` is always an int.
|
|
44
|
+
*/
|
|
45
|
+
export type WireParam = {
|
|
46
|
+
tag: "null";
|
|
47
|
+
} | {
|
|
48
|
+
tag: "int";
|
|
49
|
+
value: bigint;
|
|
50
|
+
} | {
|
|
51
|
+
tag: "float";
|
|
52
|
+
value: number;
|
|
53
|
+
} | {
|
|
54
|
+
tag: "bool";
|
|
55
|
+
value: boolean;
|
|
56
|
+
} | {
|
|
57
|
+
tag: "str";
|
|
58
|
+
value: string;
|
|
59
|
+
};
|
|
26
60
|
export type Message = {
|
|
27
61
|
type: "Connect";
|
|
28
62
|
dbName: string;
|
|
@@ -41,6 +75,13 @@ export type Message = {
|
|
|
41
75
|
} | {
|
|
42
76
|
type: "Query";
|
|
43
77
|
query: string;
|
|
78
|
+
} | {
|
|
79
|
+
type: "QuerySql";
|
|
80
|
+
query: string;
|
|
81
|
+
} | {
|
|
82
|
+
type: "QueryWithParams";
|
|
83
|
+
query: string;
|
|
84
|
+
params: WireParam[];
|
|
44
85
|
} | {
|
|
45
86
|
type: "ResultRows";
|
|
46
87
|
columns: string[];
|
package/dist/protocol.js
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
export const MSG_CONNECT = 0x01;
|
|
10
10
|
export const MSG_CONNECT_OK = 0x02;
|
|
11
11
|
export const MSG_QUERY = 0x03;
|
|
12
|
+
/**
|
|
13
|
+
* Query carrying positional `$N` parameters. Pure protocol addition: old
|
|
14
|
+
* servers reject it with the existing "unknown message type" error, and the
|
|
15
|
+
* plain `MSG_QUERY` frame is unchanged. Requires powdb-server ≥ 0.4.7.
|
|
16
|
+
*/
|
|
17
|
+
export const MSG_QUERY_PARAMS = 0x04;
|
|
18
|
+
/** SQL query frame. Plain Query remains PowQL for backward compatibility. */
|
|
19
|
+
export const MSG_QUERY_SQL = 0x05;
|
|
12
20
|
export const MSG_RESULT_ROWS = 0x07;
|
|
13
21
|
export const MSG_RESULT_SCALAR = 0x08;
|
|
14
22
|
export const MSG_RESULT_OK = 0x09;
|
|
@@ -24,6 +32,8 @@ export const MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
|
|
|
24
32
|
export const MAX_ROWS = 10_000_000;
|
|
25
33
|
/** Maximum number of columns allowed in a result set. */
|
|
26
34
|
export const MAX_COLUMNS = 4096;
|
|
35
|
+
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
36
|
+
export const MAX_PARAMS = 4096;
|
|
27
37
|
// ───── Encoding ────────────────────────────────────────────────────────────
|
|
28
38
|
export function encode(msg) {
|
|
29
39
|
let msgType;
|
|
@@ -52,6 +62,46 @@ export function encode(msg) {
|
|
|
52
62
|
payload = encodeString(msg.query);
|
|
53
63
|
msgType = MSG_QUERY;
|
|
54
64
|
break;
|
|
65
|
+
case "QuerySql":
|
|
66
|
+
payload = encodeString(msg.query);
|
|
67
|
+
msgType = MSG_QUERY_SQL;
|
|
68
|
+
break;
|
|
69
|
+
case "QueryWithParams": {
|
|
70
|
+
const parts = [encodeString(msg.query)];
|
|
71
|
+
const count = Buffer.alloc(2);
|
|
72
|
+
count.writeUInt16LE(msg.params.length, 0);
|
|
73
|
+
parts.push(count);
|
|
74
|
+
for (const p of msg.params) {
|
|
75
|
+
switch (p.tag) {
|
|
76
|
+
case "null":
|
|
77
|
+
parts.push(Buffer.from([0]));
|
|
78
|
+
break;
|
|
79
|
+
case "int": {
|
|
80
|
+
const b = Buffer.alloc(9);
|
|
81
|
+
b.writeUInt8(1, 0);
|
|
82
|
+
b.writeBigInt64LE(p.value, 1);
|
|
83
|
+
parts.push(b);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "float": {
|
|
87
|
+
const b = Buffer.alloc(9);
|
|
88
|
+
b.writeUInt8(2, 0);
|
|
89
|
+
b.writeDoubleLE(p.value, 1);
|
|
90
|
+
parts.push(b);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "bool":
|
|
94
|
+
parts.push(Buffer.from([3, p.value ? 1 : 0]));
|
|
95
|
+
break;
|
|
96
|
+
case "str":
|
|
97
|
+
parts.push(Buffer.from([4]), encodeString(p.value));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
payload = Buffer.concat(parts);
|
|
102
|
+
msgType = MSG_QUERY_PARAMS;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
55
105
|
case "ResultRows": {
|
|
56
106
|
const parts = [];
|
|
57
107
|
const colCount = Buffer.alloc(2);
|
|
@@ -150,6 +200,41 @@ function decodePayload(msgType, payload) {
|
|
|
150
200
|
return { type: "ConnectOk", version: decodeString(payload, cursor) };
|
|
151
201
|
case MSG_QUERY:
|
|
152
202
|
return { type: "Query", query: decodeString(payload, cursor) };
|
|
203
|
+
case MSG_QUERY_SQL: {
|
|
204
|
+
const query = decodeString(payload, cursor);
|
|
205
|
+
return { type: "QuerySql", query };
|
|
206
|
+
}
|
|
207
|
+
case MSG_QUERY_PARAMS: {
|
|
208
|
+
const query = decodeString(payload, cursor);
|
|
209
|
+
const count = readU16(payload, cursor, "param count");
|
|
210
|
+
if (count > MAX_PARAMS) {
|
|
211
|
+
throw new Error(`too many parameters: ${count} (max ${MAX_PARAMS})`);
|
|
212
|
+
}
|
|
213
|
+
const params = [];
|
|
214
|
+
for (let i = 0; i < count; i++) {
|
|
215
|
+
const tag = readU8(payload, cursor, "param tag");
|
|
216
|
+
switch (tag) {
|
|
217
|
+
case 0:
|
|
218
|
+
params.push({ tag: "null" });
|
|
219
|
+
break;
|
|
220
|
+
case 1:
|
|
221
|
+
params.push({ tag: "int", value: readI64(payload, cursor, "int param") });
|
|
222
|
+
break;
|
|
223
|
+
case 2:
|
|
224
|
+
params.push({ tag: "float", value: readF64(payload, cursor, "float param") });
|
|
225
|
+
break;
|
|
226
|
+
case 3:
|
|
227
|
+
params.push({ tag: "bool", value: readU8(payload, cursor, "bool param") !== 0 });
|
|
228
|
+
break;
|
|
229
|
+
case 4:
|
|
230
|
+
params.push({ tag: "str", value: decodeString(payload, cursor) });
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
throw new Error(`unknown param tag: ${tag}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { type: "QueryWithParams", query, params };
|
|
237
|
+
}
|
|
153
238
|
case MSG_RESULT_ROWS: {
|
|
154
239
|
const colCount = readU16(payload, cursor, "column count");
|
|
155
240
|
if (colCount > MAX_COLUMNS) {
|
|
@@ -213,6 +298,30 @@ function decodeString(buf, cursor) {
|
|
|
213
298
|
cursor.pos += len;
|
|
214
299
|
return s;
|
|
215
300
|
}
|
|
301
|
+
function readU8(buf, cursor, label) {
|
|
302
|
+
if (cursor.pos + 1 > buf.length) {
|
|
303
|
+
throw new Error(`truncated ${label}`);
|
|
304
|
+
}
|
|
305
|
+
const value = buf.readUInt8(cursor.pos);
|
|
306
|
+
cursor.pos += 1;
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
function readI64(buf, cursor, label) {
|
|
310
|
+
if (cursor.pos + 8 > buf.length) {
|
|
311
|
+
throw new Error(`truncated ${label}`);
|
|
312
|
+
}
|
|
313
|
+
const value = buf.readBigInt64LE(cursor.pos);
|
|
314
|
+
cursor.pos += 8;
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
function readF64(buf, cursor, label) {
|
|
318
|
+
if (cursor.pos + 8 > buf.length) {
|
|
319
|
+
throw new Error(`truncated ${label}`);
|
|
320
|
+
}
|
|
321
|
+
const value = buf.readDoubleLE(cursor.pos);
|
|
322
|
+
cursor.pos += 8;
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
216
325
|
function readU16(buf, cursor, label) {
|
|
217
326
|
if (cursor.pos + 2 > buf.length) {
|
|
218
327
|
throw new Error(`truncated ${label}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/powdb-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "TypeScript client for PowDB (PowQL wire protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"prepublishOnly": "npm run build"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@types/node": "^
|
|
63
|
+
"@types/node": "^25",
|
|
64
64
|
"tsx": "^4",
|
|
65
|
-
"typescript": "^
|
|
65
|
+
"typescript": "^6.0"
|
|
66
66
|
}
|
|
67
67
|
}
|