@zvndev/powdb-client 0.19.0 → 0.20.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 +36 -0
- package/README.md +85 -3
- package/dist/cjs/escape.d.ts +62 -0
- package/dist/cjs/escape.js +128 -1
- package/dist/cjs/index.d.ts +67 -7
- package/dist/cjs/index.js +93 -6
- package/dist/cjs/protocol.d.ts +17 -0
- package/dist/cjs/protocol.js +26 -1
- package/dist/escape.d.ts +62 -0
- package/dist/escape.js +122 -0
- package/dist/index.d.ts +67 -7
- package/dist/index.js +88 -7
- package/dist/protocol.d.ts +17 -0
- package/dist/protocol.js +25 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.20.0 - 2026-07-25
|
|
6
|
+
|
|
7
|
+
- **Security (medium): bounded result allocation.** A hostile or MITM'd server
|
|
8
|
+
could declare 10,000,000 single-column rows backed by a ~40 MB frame of empty
|
|
9
|
+
cells and make the client allocate roughly 1.9 GB (measured), since a cell
|
|
10
|
+
costs 4 bytes on the wire but far more as a JS value. Result decoding now
|
|
11
|
+
enforces `MAX_RESULT_CELLS` (2,000,000 cells per frame) on both the legacy and
|
|
12
|
+
native paths. Results larger than that must be paged with `limit`/`offset`.
|
|
13
|
+
- `queryTyped` is now generic and takes positional `$N` parameters:
|
|
14
|
+
`queryTyped<User>(q, schema, params?, opts?)`. Typed rows and injection-safe
|
|
15
|
+
binding were previously mutually exclusive. The old
|
|
16
|
+
`queryTyped(q, schema, opts?)` form is unchanged.
|
|
17
|
+
- New `queryObjects<Row>(query, params?, opts?)`: object rows keyed by column
|
|
18
|
+
name on the LOSSLESS native path, so no schema is needed (bytes stay bytes,
|
|
19
|
+
JSON stays recursive data, out-of-range integers stay `bigint`).
|
|
20
|
+
`querySqlObjects<Row>(query, opts?)` is the SQL counterpart. New `NativeRow`
|
|
21
|
+
type.
|
|
22
|
+
- New SQL composition helpers in `escape.ts`: `sql` tagged template,
|
|
23
|
+
`sqlIdent`, `escapeSqlLiteral`, `escapeSqlIdent`. PowDB's SQL frontend has NO
|
|
24
|
+
parameter binding on any wire frame, so string concatenation was previously
|
|
25
|
+
the only way to build SQL with user input. These escape for PowDB's own SQL
|
|
26
|
+
lexer (`''` doubling plus backslash escaping, identifiers validated rather
|
|
27
|
+
than quoted). Escaping is weaker than binding: prefer PowQL `$N` parameters
|
|
28
|
+
for untrusted input.
|
|
29
|
+
|
|
30
|
+
## 0.19.1 - 2026-07-24
|
|
31
|
+
|
|
32
|
+
- `SUPPORTED_CATALOG_VERSION` raised from 6 to 7: sync/replica clients are now
|
|
33
|
+
accepted by servers whose catalog activated v7 (entity links, appended links
|
|
34
|
+
section before the CRC). The client treats catalog payloads as opaque bytes,
|
|
35
|
+
so no decoding change; `assertServerCatalogVersionSupported` simply accepts
|
|
36
|
+
v7. Engine-side 0.19.1 also ships link introspection (`schema links`,
|
|
37
|
+
extended `describe`) whose results flow through the existing `query` path.
|
|
38
|
+
|
|
3
39
|
## 0.19.0 - 2026-07-22
|
|
4
40
|
|
|
5
41
|
- Version-alignment release in lockstep with workspace v0.19.0 (entity links:
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zvndev/powdb-client
|
|
2
2
|
|
|
3
|
-
TypeScript client for [PowDB](https://github.com/
|
|
3
|
+
TypeScript client for [PowDB](https://github.com/ZVN-DEV/powdb). Speaks the native binary wire protocol over TCP (or TLS).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -60,6 +60,24 @@ 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
|
+
### SQL composition (`sql`, `sqlIdent`, `escapeSqlLiteral`)
|
|
64
|
+
|
|
65
|
+
**The SQL frontend has no parameter binding.** `querySql`, `querySqlNative`, and `querySqlObjects` take a statement and nothing else: there is no `QuerySqlParams` wire frame, so `$N` placeholders cannot be bound on the SQL path. Prefer PowQL with `$N` parameters for anything built from untrusted input.
|
|
66
|
+
|
|
67
|
+
When you must build SQL, use the `sql` tagged template rather than concatenation. It escapes for PowDB's own SQL lexer: `'` is doubled and a backslash is escaped (PowDB's SQL lexer honours backslash escapes, unlike standard SQL). Identifiers are validated, not quoted, because the lexer reads a double-quoted run as a string literal.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { sql, sqlIdent, escapeSqlLiteral, escapeSqlIdent } from "@zvndev/powdb-client";
|
|
71
|
+
|
|
72
|
+
const q = sql`SELECT name FROM ${sqlIdent("User")} WHERE name = ${userName}`;
|
|
73
|
+
await client.querySql(q);
|
|
74
|
+
|
|
75
|
+
escapeSqlLiteral("o'neil"); // → "'o''neil'"
|
|
76
|
+
escapeSqlIdent("User"); // → "User" (throws on anything else)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Escaping is a weaker guarantee than binding: it depends on the value landing in a string or number position, and it cannot make an identifier or keyword safe. Track the missing `QuerySqlParams` frame if you need real binding on the SQL path.
|
|
80
|
+
|
|
63
81
|
### Parameter binding (`$N`)
|
|
64
82
|
|
|
65
83
|
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).
|
|
@@ -412,6 +430,45 @@ output always parses, so this fallback is inert in normal use).
|
|
|
412
430
|
Bytes columns are intentionally unsupported by `queryTyped` because the
|
|
413
431
|
legacy wire format renders `<N bytes>`. Use `queryNative` for exact bytes.
|
|
414
432
|
|
|
433
|
+
## Object rows on the lossless path (`queryObjects`)
|
|
434
|
+
|
|
435
|
+
`queryTyped` coerces the legacy stringly-typed path with a caller-supplied
|
|
436
|
+
schema. `queryObjects` needs no schema: it runs on the **native** wire surface,
|
|
437
|
+
where the server states each cell's type, and returns objects keyed by column
|
|
438
|
+
name. It accepts `$N` parameters and an abort signal exactly like `query`.
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
interface User {
|
|
442
|
+
name: string;
|
|
443
|
+
age: number;
|
|
444
|
+
prefs: unknown;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const users = await client.queryObjects<User>(
|
|
448
|
+
"User filter .age > $1 { .name, .age, .prefs }",
|
|
449
|
+
[25],
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
users[0].age; // number (bigint when outside the safe integer range)
|
|
453
|
+
users[0].prefs; // recursively decoded JSON, not a string
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`querySqlObjects<Row>(sql, opts?)` is the SQL counterpart. `queryTyped` is also
|
|
457
|
+
generic and takes parameters, so typed rows and injection-safe binding compose:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const rows = await client.queryTyped<User>(
|
|
461
|
+
"User filter .age > $1 { .name, .age }",
|
|
462
|
+
{ name: "str", age: "int" },
|
|
463
|
+
[25],
|
|
464
|
+
);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The generic is an unchecked assertion about the query's shape, like a SQL
|
|
468
|
+
driver's row type: nothing validates it at runtime. Duplicate column names
|
|
469
|
+
collapse (last one wins), so alias them in the projection. A non-rows result
|
|
470
|
+
(a count, a write) throws `PowDBError(code="query_failed")`.
|
|
471
|
+
|
|
415
472
|
## Structured errors
|
|
416
473
|
|
|
417
474
|
Every error thrown by the client is a `PowDBError` with a stable `.code`:
|
|
@@ -592,10 +649,26 @@ Runs SQL through the native typed wire surface and returns a
|
|
|
592
649
|
`Promise<NativeQueryResult>`. It has the same no-replay guarantee as
|
|
593
650
|
`queryNative()`.
|
|
594
651
|
|
|
595
|
-
### `client.queryTyped(query, schema, opts?)`
|
|
652
|
+
### `client.queryTyped<Row>(query, schema, params?, opts?)`
|
|
596
653
|
|
|
597
654
|
Like `query()`, but coerces each row's string values to JS types using the
|
|
598
|
-
supplied schema and returns `Promise<TypedRow[]
|
|
655
|
+
supplied schema and returns `Promise<Row[]>` (`TypedRow[]` when no generic is
|
|
656
|
+
given). Accepts positional `$N` parameters in the same third-argument position
|
|
657
|
+
as `query()`, so typed rows and parameter binding compose. The legacy
|
|
658
|
+
`queryTyped(q, schema, { signal })` form still works. See Schema-coerced rows
|
|
659
|
+
above.
|
|
660
|
+
|
|
661
|
+
### `client.queryObjects<Row>(query, params?, opts?)`
|
|
662
|
+
|
|
663
|
+
Runs PowQL on the lossless native surface and returns object rows keyed by
|
|
664
|
+
column name (`Promise<Row[]>`, default `NativeRow[]`). No schema needed.
|
|
665
|
+
Parameters and abort options match `query()`. Throws
|
|
666
|
+
`PowDBError(code="query_failed")` if the result is not rows-shaped.
|
|
667
|
+
|
|
668
|
+
### `client.querySqlObjects<Row>(query, opts?)`
|
|
669
|
+
|
|
670
|
+
SQL counterpart of `queryObjects`. The SQL path cannot bind parameters: build
|
|
671
|
+
statements with the `sql` tagged template (see SQL composition above).
|
|
599
672
|
|
|
600
673
|
### `client.execScript(script, opts?)`
|
|
601
674
|
|
|
@@ -655,6 +728,10 @@ Getters: `size`, `idle`, `closed`.
|
|
|
655
728
|
- `ident(name)` — wrap a string so `powql` treats it as an identifier
|
|
656
729
|
- `escapeLiteral(value)` — render a JS value as a PowQL literal
|
|
657
730
|
- `escapeIdent(name)` — validate an identifier (throws `TypeError` on invalid)
|
|
731
|
+
- `sql`: tagged template for the SQL frontend; escapes literals, validates identifiers
|
|
732
|
+
- `sqlIdent(name)`: wrap a string so `sql` treats it as a SQL identifier
|
|
733
|
+
- `escapeSqlLiteral(value)`: render a JS value as a PowDB SQL literal
|
|
734
|
+
- `escapeSqlIdent(name)`: validate a SQL identifier (throws `TypeError` on invalid)
|
|
658
735
|
- `splitStatements(script)` — the statement-aware script splitter used by `execScript`
|
|
659
736
|
|
|
660
737
|
## Limits
|
|
@@ -664,6 +741,11 @@ The client enforces the same frame limits as the server and throws on violation:
|
|
|
664
741
|
- `MAX_PAYLOAD_SIZE` — 64 MiB per frame
|
|
665
742
|
- `MAX_ROWS` — 10,000,000 rows per result
|
|
666
743
|
- `MAX_COLUMNS` — 4,096 columns per result
|
|
744
|
+
- `MAX_RESULT_CELLS`: 2,000,000 cells (rows x columns) per result frame. This
|
|
745
|
+
caps how much heap a server-declared result shape can force the client to
|
|
746
|
+
allocate: a cell is only 4 bytes on the wire but costs far more as a JS
|
|
747
|
+
value, so without the cap a ~40 MB frame could expand to roughly 1.9 GB. Page
|
|
748
|
+
larger results with `limit`/`offset`.
|
|
667
749
|
|
|
668
750
|
## Requirements
|
|
669
751
|
|
package/dist/cjs/escape.d.ts
CHANGED
|
@@ -52,3 +52,65 @@ export declare function escapeLiteral(value: string | number | bigint | boolean
|
|
|
52
52
|
* const q = powql`insert ${ident(table)} { name := ${userName} }`;
|
|
53
53
|
*/
|
|
54
54
|
export declare function powql(strings: TemplateStringsArray, ...values: unknown[]): string;
|
|
55
|
+
/**
|
|
56
|
+
* SQL composition helpers for PowDB's SQL frontend.
|
|
57
|
+
*
|
|
58
|
+
* READ THIS FIRST. PowDB's SQL surface has NO parameter binding on any wire
|
|
59
|
+
* frame: `querySql`/`querySqlNative` take a statement and nothing else, and
|
|
60
|
+
* there is no `QuerySqlParams` message. The PowQL surface does have real `$N`
|
|
61
|
+
* binding (`client.query(q, [values])`), where the server substitutes a literal
|
|
62
|
+
* TOKEN and injection-shaped input is inert.
|
|
63
|
+
*
|
|
64
|
+
* So: **prefer PowQL with `$N` parameters for anything built from untrusted
|
|
65
|
+
* input.** These helpers exist because the SQL path would otherwise leave
|
|
66
|
+
* string concatenation as the only option, which is strictly worse. They escape
|
|
67
|
+
* correctly for PowDB's own SQL lexer (`crates/query/src/sql.rs`), but escaping
|
|
68
|
+
* is a weaker guarantee than binding: it depends on the value landing in a
|
|
69
|
+
* string/number position, and it cannot make an identifier or a keyword safe.
|
|
70
|
+
*
|
|
71
|
+
* PowDB SQL string rules (verified against `lex_sql` in `crates/query/src/sql.rs`):
|
|
72
|
+
* - Strings are delimited by `'`.
|
|
73
|
+
* - `''` inside a single-quoted string is a literal `'`.
|
|
74
|
+
* - A backslash also escapes the next character (`\n`, `\t`, and `\X` → `X`),
|
|
75
|
+
* which standard SQL does NOT do, so a literal backslash must be doubled.
|
|
76
|
+
* - `"` also opens a string in this lexer, so double quotes cannot be used to
|
|
77
|
+
* quote an identifier. Identifiers are therefore validated, never quoted.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* Render a JS value as a PowDB SQL literal. Same accepted types as
|
|
81
|
+
* {@link escapeLiteral}: `string`, `number`, `bigint`, `boolean`, `null`.
|
|
82
|
+
*
|
|
83
|
+
* - string → `'...'` with `\` doubled and `'` doubled
|
|
84
|
+
* - number → decimal; rejects `NaN`/`±Infinity`
|
|
85
|
+
* - bigint → decimal digits
|
|
86
|
+
* - boolean → `true` / `false`
|
|
87
|
+
* - null → `null`
|
|
88
|
+
*/
|
|
89
|
+
export declare function escapeSqlLiteral(value: string | number | bigint | boolean | null): string;
|
|
90
|
+
/**
|
|
91
|
+
* Validate a SQL identifier (table, column, alias). Returns it unchanged on
|
|
92
|
+
* success and throws `TypeError` otherwise.
|
|
93
|
+
*
|
|
94
|
+
* Identifiers are validated rather than quoted because PowDB's SQL lexer reads
|
|
95
|
+
* `"..."` as a string literal, so there is no identifier-quoting syntax to fall
|
|
96
|
+
* back on. Only `^[A-Za-z_][A-Za-z0-9_]*$` is accepted.
|
|
97
|
+
*/
|
|
98
|
+
export declare function escapeSqlIdent(name: string): string;
|
|
99
|
+
/** Wrapper marking a string as a SQL identifier for the `sql` tagged template. */
|
|
100
|
+
export declare class SqlIdent {
|
|
101
|
+
readonly name: string;
|
|
102
|
+
constructor(name: string);
|
|
103
|
+
}
|
|
104
|
+
/** Factory for {@link SqlIdent}. */
|
|
105
|
+
export declare function sqlIdent(name: string): SqlIdent;
|
|
106
|
+
/**
|
|
107
|
+
* Tagged template for SQL composition. Interpolated values are escaped as SQL
|
|
108
|
+
* literals; wrap one in `sqlIdent(...)` to interpolate an identifier.
|
|
109
|
+
*
|
|
110
|
+
* const q = sql`SELECT name FROM ${sqlIdent(table)} WHERE name = ${userName}`;
|
|
111
|
+
* await client.querySql(q);
|
|
112
|
+
*
|
|
113
|
+
* Escaping, not binding: see the section note above, and prefer PowQL's `$N`
|
|
114
|
+
* parameters when the input is untrusted.
|
|
115
|
+
*/
|
|
116
|
+
export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): string;
|
package/dist/cjs/escape.js
CHANGED
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
* literal backslash must be `\\` (otherwise it would swallow the next char).
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.PowqlIdent = void 0;
|
|
23
|
+
exports.SqlIdent = exports.PowqlIdent = void 0;
|
|
24
24
|
exports.ident = ident;
|
|
25
25
|
exports.escapeIdent = escapeIdent;
|
|
26
26
|
exports.escapeLiteral = escapeLiteral;
|
|
27
27
|
exports.powql = powql;
|
|
28
|
+
exports.escapeSqlLiteral = escapeSqlLiteral;
|
|
29
|
+
exports.escapeSqlIdent = escapeSqlIdent;
|
|
30
|
+
exports.sqlIdent = sqlIdent;
|
|
31
|
+
exports.sql = sql;
|
|
28
32
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29
33
|
/** Wrapper marking a string as an identifier (vs a literal) for `powql` tagged templates. */
|
|
30
34
|
class PowqlIdent {
|
|
@@ -108,8 +112,131 @@ function powql(strings, ...values) {
|
|
|
108
112
|
return out;
|
|
109
113
|
}
|
|
110
114
|
// ──────────────────────────────────────────────────────────
|
|
115
|
+
// SQL frontend
|
|
116
|
+
// ──────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* SQL composition helpers for PowDB's SQL frontend.
|
|
119
|
+
*
|
|
120
|
+
* READ THIS FIRST. PowDB's SQL surface has NO parameter binding on any wire
|
|
121
|
+
* frame: `querySql`/`querySqlNative` take a statement and nothing else, and
|
|
122
|
+
* there is no `QuerySqlParams` message. The PowQL surface does have real `$N`
|
|
123
|
+
* binding (`client.query(q, [values])`), where the server substitutes a literal
|
|
124
|
+
* TOKEN and injection-shaped input is inert.
|
|
125
|
+
*
|
|
126
|
+
* So: **prefer PowQL with `$N` parameters for anything built from untrusted
|
|
127
|
+
* input.** These helpers exist because the SQL path would otherwise leave
|
|
128
|
+
* string concatenation as the only option, which is strictly worse. They escape
|
|
129
|
+
* correctly for PowDB's own SQL lexer (`crates/query/src/sql.rs`), but escaping
|
|
130
|
+
* is a weaker guarantee than binding: it depends on the value landing in a
|
|
131
|
+
* string/number position, and it cannot make an identifier or a keyword safe.
|
|
132
|
+
*
|
|
133
|
+
* PowDB SQL string rules (verified against `lex_sql` in `crates/query/src/sql.rs`):
|
|
134
|
+
* - Strings are delimited by `'`.
|
|
135
|
+
* - `''` inside a single-quoted string is a literal `'`.
|
|
136
|
+
* - A backslash also escapes the next character (`\n`, `\t`, and `\X` → `X`),
|
|
137
|
+
* which standard SQL does NOT do, so a literal backslash must be doubled.
|
|
138
|
+
* - `"` also opens a string in this lexer, so double quotes cannot be used to
|
|
139
|
+
* quote an identifier. Identifiers are therefore validated, never quoted.
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* Render a JS value as a PowDB SQL literal. Same accepted types as
|
|
143
|
+
* {@link escapeLiteral}: `string`, `number`, `bigint`, `boolean`, `null`.
|
|
144
|
+
*
|
|
145
|
+
* - string → `'...'` with `\` doubled and `'` doubled
|
|
146
|
+
* - number → decimal; rejects `NaN`/`±Infinity`
|
|
147
|
+
* - bigint → decimal digits
|
|
148
|
+
* - boolean → `true` / `false`
|
|
149
|
+
* - null → `null`
|
|
150
|
+
*/
|
|
151
|
+
function escapeSqlLiteral(value) {
|
|
152
|
+
if (value === null)
|
|
153
|
+
return "null";
|
|
154
|
+
const t = typeof value;
|
|
155
|
+
if (t === "string") {
|
|
156
|
+
// Backslash first: this lexer treats `\` as an escape inside strings, so a
|
|
157
|
+
// lone backslash would otherwise swallow the quote doubling that follows.
|
|
158
|
+
const escaped = value
|
|
159
|
+
.replace(/\\/g, "\\\\")
|
|
160
|
+
.replace(/'/g, "''");
|
|
161
|
+
return `'${escaped}'`;
|
|
162
|
+
}
|
|
163
|
+
if (t === "number") {
|
|
164
|
+
const n = value;
|
|
165
|
+
if (!Number.isFinite(n)) {
|
|
166
|
+
throw new TypeError(`escapeSqlLiteral: non-finite number ${String(n)} cannot be represented as a SQL literal`);
|
|
167
|
+
}
|
|
168
|
+
return String(n);
|
|
169
|
+
}
|
|
170
|
+
if (t === "bigint") {
|
|
171
|
+
return value.toString(10);
|
|
172
|
+
}
|
|
173
|
+
if (t === "boolean") {
|
|
174
|
+
return value ? "true" : "false";
|
|
175
|
+
}
|
|
176
|
+
throw new TypeError(`escapeSqlLiteral: unsupported type ${describe(value)}`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate a SQL identifier (table, column, alias). Returns it unchanged on
|
|
180
|
+
* success and throws `TypeError` otherwise.
|
|
181
|
+
*
|
|
182
|
+
* Identifiers are validated rather than quoted because PowDB's SQL lexer reads
|
|
183
|
+
* `"..."` as a string literal, so there is no identifier-quoting syntax to fall
|
|
184
|
+
* back on. Only `^[A-Za-z_][A-Za-z0-9_]*$` is accepted.
|
|
185
|
+
*/
|
|
186
|
+
function escapeSqlIdent(name) {
|
|
187
|
+
if (typeof name !== "string") {
|
|
188
|
+
throw new TypeError(`escapeSqlIdent: expected string, got ${typeof name}`);
|
|
189
|
+
}
|
|
190
|
+
if (name.length === 0) {
|
|
191
|
+
throw new TypeError("escapeSqlIdent: identifier must not be empty");
|
|
192
|
+
}
|
|
193
|
+
if (!IDENT_RE.test(name)) {
|
|
194
|
+
throw new TypeError(`escapeSqlIdent: invalid identifier ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`);
|
|
195
|
+
}
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
/** Wrapper marking a string as a SQL identifier for the `sql` tagged template. */
|
|
199
|
+
class SqlIdent {
|
|
200
|
+
name;
|
|
201
|
+
constructor(name) {
|
|
202
|
+
this.name = name;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.SqlIdent = SqlIdent;
|
|
206
|
+
/** Factory for {@link SqlIdent}. */
|
|
207
|
+
function sqlIdent(name) {
|
|
208
|
+
return new SqlIdent(name);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Tagged template for SQL composition. Interpolated values are escaped as SQL
|
|
212
|
+
* literals; wrap one in `sqlIdent(...)` to interpolate an identifier.
|
|
213
|
+
*
|
|
214
|
+
* const q = sql`SELECT name FROM ${sqlIdent(table)} WHERE name = ${userName}`;
|
|
215
|
+
* await client.querySql(q);
|
|
216
|
+
*
|
|
217
|
+
* Escaping, not binding: see the section note above, and prefer PowQL's `$N`
|
|
218
|
+
* parameters when the input is untrusted.
|
|
219
|
+
*/
|
|
220
|
+
function sql(strings, ...values) {
|
|
221
|
+
let out = strings[0] ?? "";
|
|
222
|
+
for (let i = 0; i < values.length; i++) {
|
|
223
|
+
out += renderSqlInterpolation(values[i]);
|
|
224
|
+
out += strings[i + 1] ?? "";
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
// ──────────────────────────────────────────────────────────
|
|
111
229
|
// internals
|
|
112
230
|
// ──────────────────────────────────────────────────────────
|
|
231
|
+
function renderSqlInterpolation(value) {
|
|
232
|
+
if (value instanceof SqlIdent) {
|
|
233
|
+
return escapeSqlIdent(value.name);
|
|
234
|
+
}
|
|
235
|
+
if (value instanceof PowqlIdent) {
|
|
236
|
+
throw new TypeError("sql: use sqlIdent(...) for SQL identifiers, not ident(...)");
|
|
237
|
+
}
|
|
238
|
+
return escapeSqlLiteral(value);
|
|
239
|
+
}
|
|
113
240
|
function renderInterpolation(value) {
|
|
114
241
|
if (value instanceof PowqlIdent) {
|
|
115
242
|
return escapeIdent(value.name);
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -19,15 +19,21 @@ import { EventEmitter } from "node:events";
|
|
|
19
19
|
import { type NativeJson, type SyncRepairAction, type WireRetainedUnit, type WireSyncStatus, type WireValue } from "./protocol.js";
|
|
20
20
|
import { type TypedRow, type TypedSchema } from "./typed.js";
|
|
21
21
|
/** Client library version. Compared to the server's reported version. */
|
|
22
|
-
export declare const CLIENT_VERSION = "0.
|
|
22
|
+
export declare const CLIENT_VERSION = "0.20.0";
|
|
23
23
|
/**
|
|
24
24
|
* The maximum catalog format version this client can read. State this as the
|
|
25
25
|
* `catalogVersion` in sync pull requests: the server accepts any replica whose
|
|
26
26
|
* maximum is at least its active catalog format and rejects an older replica.
|
|
27
27
|
* When validating a server-reported catalog version, accept anything at or
|
|
28
28
|
* below this and reject anything newer (the client cannot read it).
|
|
29
|
+
*
|
|
30
|
+
* v7 is the entity-links format: it appends a relationship-link section
|
|
31
|
+
* before the trailing CRC, staircase-defaulted so every older file still
|
|
32
|
+
* loads (a pre-v7 file simply has zero links). It activates lazily on the
|
|
33
|
+
* first `link` declaration. The client treats catalog payloads as opaque
|
|
34
|
+
* bytes and only states this ceiling in the sync handshake.
|
|
29
35
|
*/
|
|
30
|
-
export declare const SUPPORTED_CATALOG_VERSION =
|
|
36
|
+
export declare const SUPPORTED_CATALOG_VERSION = 7;
|
|
31
37
|
/**
|
|
32
38
|
* Throw when a server-reported catalog format is newer than this client can
|
|
33
39
|
* read. Accepts `serverCatalogVersion <= SUPPORTED_CATALOG_VERSION`; rejects a
|
|
@@ -51,6 +57,11 @@ export type QueryResult = {
|
|
|
51
57
|
export type { NativeJson } from "./protocol.js";
|
|
52
58
|
/** A value returned by the lossless native wire surface. */
|
|
53
59
|
export type NativeValue = null | number | bigint | boolean | string | Uint8Array | NativeJson;
|
|
60
|
+
/**
|
|
61
|
+
* One row of a native (lossless) result, keyed by column name. The default row
|
|
62
|
+
* type of {@link Client.queryObjects} when no generic is supplied.
|
|
63
|
+
*/
|
|
64
|
+
export type NativeRow = Record<string, NativeValue>;
|
|
54
65
|
export type NativeQueryResult = {
|
|
55
66
|
kind: "rows";
|
|
56
67
|
columns: string[];
|
|
@@ -376,13 +387,62 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
376
387
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
377
388
|
* rules and supported column types.
|
|
378
389
|
*
|
|
379
|
-
* Returns
|
|
390
|
+
* Returns `Row[]`, an array of objects keyed by column name (`TypedRow[]`
|
|
391
|
+
* when no generic is supplied). The generic is an unchecked assertion about
|
|
392
|
+
* the query's shape: the schema drives coercion, nothing validates the type.
|
|
393
|
+
* Positional `$N` parameters are accepted in the same position as
|
|
394
|
+
* {@link query}, so typed rows and injection-safe binding compose.
|
|
395
|
+
*
|
|
396
|
+
* This is the LEGACY stringly-typed path plus schema coercion. For lossless
|
|
397
|
+
* object rows with no schema at all, use {@link queryObjects}.
|
|
398
|
+
*
|
|
380
399
|
* Throws `PowDBError(code="query_failed")` if the query is not a
|
|
381
400
|
* rows-returning query.
|
|
382
401
|
*/
|
|
383
|
-
queryTyped(query: string, schema: TypedSchema,
|
|
402
|
+
queryTyped<Row extends TypedRow = TypedRow>(query: string, schema: TypedSchema, paramsOrOpts?: QueryParam[] | {
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
}, maybeOpts?: {
|
|
405
|
+
signal?: AbortSignal;
|
|
406
|
+
}): Promise<Row[]>;
|
|
407
|
+
/**
|
|
408
|
+
* Run PowQL on the LOSSLESS native wire surface and return object rows keyed
|
|
409
|
+
* by column name.
|
|
410
|
+
*
|
|
411
|
+
* This is {@link queryNative} plus the row-to-object step, so no schema is
|
|
412
|
+
* needed: the server states each cell's type, bytes stay bytes, JSON stays
|
|
413
|
+
* recursive data, and out-of-range integers stay `bigint`. Contrast with
|
|
414
|
+
* {@link queryTyped}, which coerces the legacy stringly-typed path using a
|
|
415
|
+
* caller-supplied schema.
|
|
416
|
+
*
|
|
417
|
+
* interface User { name: string; age: number }
|
|
418
|
+
* const users = await client.queryObjects<User>(
|
|
419
|
+
* "User filter .age > $1 { .name, .age }",
|
|
420
|
+
* [25],
|
|
421
|
+
* );
|
|
422
|
+
*
|
|
423
|
+
* The generic is an unchecked assertion about the query's shape, exactly like
|
|
424
|
+
* a SQL driver's row type: nothing validates it at runtime. Duplicate column
|
|
425
|
+
* names collapse (last one wins), so alias them in the projection.
|
|
426
|
+
*
|
|
427
|
+
* Throws `PowDBError(code="query_failed")` if the query is not rows-returning.
|
|
428
|
+
*/
|
|
429
|
+
queryObjects<Row = NativeRow>(query: string, paramsOrOpts?: QueryParam[] | {
|
|
430
|
+
signal?: AbortSignal;
|
|
431
|
+
}, maybeOpts?: {
|
|
432
|
+
signal?: AbortSignal;
|
|
433
|
+
}): Promise<Row[]>;
|
|
434
|
+
/**
|
|
435
|
+
* SQL counterpart of {@link queryObjects}: runs SQL on the lossless native
|
|
436
|
+
* surface and returns object rows keyed by column name.
|
|
437
|
+
*
|
|
438
|
+
* The SQL frontend cannot bind parameters on any wire frame yet, so build SQL
|
|
439
|
+
* with {@link escapeSqlLiteral} / {@link sqlIdent} (or the `sql` tagged
|
|
440
|
+
* template) rather than string concatenation. For parameter binding, use the
|
|
441
|
+
* PowQL surface, which has real `$N` placeholders.
|
|
442
|
+
*/
|
|
443
|
+
querySqlObjects<Row = NativeRow>(query: string, opts?: {
|
|
384
444
|
signal?: AbortSignal;
|
|
385
|
-
}): Promise<
|
|
445
|
+
}): Promise<Row[]>;
|
|
386
446
|
/**
|
|
387
447
|
* Execute a multi-statement PowQL script on this connection, pipelined.
|
|
388
448
|
*
|
|
@@ -466,8 +526,8 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
466
526
|
}
|
|
467
527
|
export { encode, tryDecode } from "./protocol.js";
|
|
468
528
|
export type { Message, SyncRepairAction, WireValue, WireParam, WireRetainedUnit, WireSyncStatus, } from "./protocol.js";
|
|
469
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
470
|
-
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
529
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_RESULT_CELLS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
530
|
+
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, escapeSqlLiteral, escapeSqlIdent, sqlIdent, sql, SqlIdent, } from "./escape.js";
|
|
471
531
|
export { Pool } from "./pool.js";
|
|
472
532
|
export type { PoolOptions } from "./pool.js";
|
|
473
533
|
export { splitStatements } from "./script.js";
|
package/dist/cjs/index.js
CHANGED
|
@@ -49,7 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
49
|
};
|
|
50
50
|
})();
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
-
exports.coerceRows = exports.coerceRow = exports.coerceValue = exports.WIRE_ERROR_CLASS = exports.isPowDBScriptError = exports.PowDBScriptError = exports.isPowDBError = exports.PowDBError = exports.errorCodeForWireClass = exports.splitStatements = exports.Pool = exports.PowqlIdent = exports.powql = exports.ident = exports.escapeIdent = exports.escapeLiteral = exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_COLUMNS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.tryDecode = exports.encode = exports.Client = exports.SUPPORTED_CATALOG_VERSION = exports.CLIENT_VERSION = void 0;
|
|
52
|
+
exports.coerceRows = exports.coerceRow = exports.coerceValue = exports.WIRE_ERROR_CLASS = exports.isPowDBScriptError = exports.PowDBScriptError = exports.isPowDBError = exports.PowDBError = exports.errorCodeForWireClass = exports.splitStatements = exports.Pool = exports.SqlIdent = exports.sql = exports.sqlIdent = exports.escapeSqlIdent = exports.escapeSqlLiteral = exports.PowqlIdent = exports.powql = exports.ident = exports.escapeIdent = exports.escapeLiteral = exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_COLUMNS = exports.MAX_RESULT_CELLS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.tryDecode = exports.encode = exports.Client = exports.SUPPORTED_CATALOG_VERSION = exports.CLIENT_VERSION = void 0;
|
|
53
53
|
exports.assertServerCatalogVersionSupported = assertServerCatalogVersionSupported;
|
|
54
54
|
const net = __importStar(require("node:net"));
|
|
55
55
|
const tls = __importStar(require("node:tls"));
|
|
@@ -59,15 +59,21 @@ const errors_js_1 = require("./errors.js");
|
|
|
59
59
|
const script_js_1 = require("./script.js");
|
|
60
60
|
const typed_js_1 = require("./typed.js");
|
|
61
61
|
/** Client library version. Compared to the server's reported version. */
|
|
62
|
-
exports.CLIENT_VERSION = "0.
|
|
62
|
+
exports.CLIENT_VERSION = "0.20.0";
|
|
63
63
|
/**
|
|
64
64
|
* The maximum catalog format version this client can read. State this as the
|
|
65
65
|
* `catalogVersion` in sync pull requests: the server accepts any replica whose
|
|
66
66
|
* maximum is at least its active catalog format and rejects an older replica.
|
|
67
67
|
* When validating a server-reported catalog version, accept anything at or
|
|
68
68
|
* below this and reject anything newer (the client cannot read it).
|
|
69
|
+
*
|
|
70
|
+
* v7 is the entity-links format: it appends a relationship-link section
|
|
71
|
+
* before the trailing CRC, staircase-defaulted so every older file still
|
|
72
|
+
* loads (a pre-v7 file simply has zero links). It activates lazily on the
|
|
73
|
+
* first `link` declaration. The client treats catalog payloads as opaque
|
|
74
|
+
* bytes and only states this ceiling in the sync handshake.
|
|
69
75
|
*/
|
|
70
|
-
exports.SUPPORTED_CATALOG_VERSION =
|
|
76
|
+
exports.SUPPORTED_CATALOG_VERSION = 7;
|
|
71
77
|
/**
|
|
72
78
|
* Throw when a server-reported catalog format is newer than this client can
|
|
73
79
|
* read. Accepts `serverCatalogVersion <= SUPPORTED_CATALOG_VERSION`; rejects a
|
|
@@ -157,6 +163,24 @@ function nativeQueryResult(reply) {
|
|
|
157
163
|
throw new errors_js_1.PowDBError(`unexpected reply to native query: ${reply.type}`, "protocol_error");
|
|
158
164
|
}
|
|
159
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Turn a rows-shaped native result into object rows keyed by column name.
|
|
168
|
+
* Non-rows results are a caller mistake, not a server error, so they raise the
|
|
169
|
+
* same `query_failed` code `queryTyped` uses.
|
|
170
|
+
*/
|
|
171
|
+
function nativeRowObjects(result, method) {
|
|
172
|
+
if (result.kind !== "rows") {
|
|
173
|
+
throw new errors_js_1.PowDBError(`${method}: expected rows result, got ${result.kind}`, "query_failed");
|
|
174
|
+
}
|
|
175
|
+
const { columns, rows } = result;
|
|
176
|
+
return rows.map((values) => {
|
|
177
|
+
const row = {};
|
|
178
|
+
for (let i = 0; i < columns.length; i++) {
|
|
179
|
+
row[columns[i]] = values[i] ?? null;
|
|
180
|
+
}
|
|
181
|
+
return row;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
160
184
|
function rawNativeQueryResult(reply) {
|
|
161
185
|
switch (reply.type) {
|
|
162
186
|
case "ResultRowsNative":
|
|
@@ -704,17 +728,74 @@ class Client extends node_events_1.EventEmitter {
|
|
|
704
728
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
705
729
|
* rules and supported column types.
|
|
706
730
|
*
|
|
707
|
-
* Returns
|
|
731
|
+
* Returns `Row[]`, an array of objects keyed by column name (`TypedRow[]`
|
|
732
|
+
* when no generic is supplied). The generic is an unchecked assertion about
|
|
733
|
+
* the query's shape: the schema drives coercion, nothing validates the type.
|
|
734
|
+
* Positional `$N` parameters are accepted in the same position as
|
|
735
|
+
* {@link query}, so typed rows and injection-safe binding compose.
|
|
736
|
+
*
|
|
737
|
+
* This is the LEGACY stringly-typed path plus schema coercion. For lossless
|
|
738
|
+
* object rows with no schema at all, use {@link queryObjects}.
|
|
739
|
+
*
|
|
708
740
|
* Throws `PowDBError(code="query_failed")` if the query is not a
|
|
709
741
|
* rows-returning query.
|
|
710
742
|
*/
|
|
711
|
-
async queryTyped(query, schema,
|
|
712
|
-
|
|
743
|
+
async queryTyped(query, schema, paramsOrOpts, maybeOpts) {
|
|
744
|
+
// Same overload disambiguation as `query`, so typed rows and `$N`
|
|
745
|
+
// parameter binding are usable together:
|
|
746
|
+
// queryTyped(q, schema) no params, no opts
|
|
747
|
+
// queryTyped(q, schema, opts) legacy 3-arg opts form
|
|
748
|
+
// queryTyped(q, schema, params) positional $N parameters
|
|
749
|
+
// queryTyped(q, schema, params, opts) params plus opts
|
|
750
|
+
const result = Array.isArray(paramsOrOpts)
|
|
751
|
+
? await this.query(query, paramsOrOpts, maybeOpts)
|
|
752
|
+
: await this.query(query, paramsOrOpts);
|
|
713
753
|
if (result.kind !== "rows") {
|
|
714
754
|
throw new errors_js_1.PowDBError(`queryTyped: expected rows result, got ${result.kind}`, "query_failed");
|
|
715
755
|
}
|
|
716
756
|
return (0, typed_js_1.coerceRows)(result.columns, result.rows, schema);
|
|
717
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Run PowQL on the LOSSLESS native wire surface and return object rows keyed
|
|
760
|
+
* by column name.
|
|
761
|
+
*
|
|
762
|
+
* This is {@link queryNative} plus the row-to-object step, so no schema is
|
|
763
|
+
* needed: the server states each cell's type, bytes stay bytes, JSON stays
|
|
764
|
+
* recursive data, and out-of-range integers stay `bigint`. Contrast with
|
|
765
|
+
* {@link queryTyped}, which coerces the legacy stringly-typed path using a
|
|
766
|
+
* caller-supplied schema.
|
|
767
|
+
*
|
|
768
|
+
* interface User { name: string; age: number }
|
|
769
|
+
* const users = await client.queryObjects<User>(
|
|
770
|
+
* "User filter .age > $1 { .name, .age }",
|
|
771
|
+
* [25],
|
|
772
|
+
* );
|
|
773
|
+
*
|
|
774
|
+
* The generic is an unchecked assertion about the query's shape, exactly like
|
|
775
|
+
* a SQL driver's row type: nothing validates it at runtime. Duplicate column
|
|
776
|
+
* names collapse (last one wins), so alias them in the projection.
|
|
777
|
+
*
|
|
778
|
+
* Throws `PowDBError(code="query_failed")` if the query is not rows-returning.
|
|
779
|
+
*/
|
|
780
|
+
async queryObjects(query, paramsOrOpts, maybeOpts) {
|
|
781
|
+
const result = Array.isArray(paramsOrOpts)
|
|
782
|
+
? await this.queryNative(query, paramsOrOpts, maybeOpts)
|
|
783
|
+
: await this.queryNative(query, paramsOrOpts);
|
|
784
|
+
return nativeRowObjects(result, "queryObjects");
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* SQL counterpart of {@link queryObjects}: runs SQL on the lossless native
|
|
788
|
+
* surface and returns object rows keyed by column name.
|
|
789
|
+
*
|
|
790
|
+
* The SQL frontend cannot bind parameters on any wire frame yet, so build SQL
|
|
791
|
+
* with {@link escapeSqlLiteral} / {@link sqlIdent} (or the `sql` tagged
|
|
792
|
+
* template) rather than string concatenation. For parameter binding, use the
|
|
793
|
+
* PowQL surface, which has real `$N` placeholders.
|
|
794
|
+
*/
|
|
795
|
+
async querySqlObjects(query, opts) {
|
|
796
|
+
const result = await this.querySqlNative(query, opts);
|
|
797
|
+
return nativeRowObjects(result, "querySqlObjects");
|
|
798
|
+
}
|
|
718
799
|
async execScript(script, opts) {
|
|
719
800
|
const statements = (0, script_js_1.splitStatements)(script);
|
|
720
801
|
const continueOnError = opts?.continueOnError === true;
|
|
@@ -1210,6 +1291,7 @@ Object.defineProperty(exports, "tryDecode", { enumerable: true, get: function ()
|
|
|
1210
1291
|
var protocol_js_3 = require("./protocol.js");
|
|
1211
1292
|
Object.defineProperty(exports, "MAX_PAYLOAD_SIZE", { enumerable: true, get: function () { return protocol_js_3.MAX_PAYLOAD_SIZE; } });
|
|
1212
1293
|
Object.defineProperty(exports, "MAX_ROWS", { enumerable: true, get: function () { return protocol_js_3.MAX_ROWS; } });
|
|
1294
|
+
Object.defineProperty(exports, "MAX_RESULT_CELLS", { enumerable: true, get: function () { return protocol_js_3.MAX_RESULT_CELLS; } });
|
|
1213
1295
|
Object.defineProperty(exports, "MAX_COLUMNS", { enumerable: true, get: function () { return protocol_js_3.MAX_COLUMNS; } });
|
|
1214
1296
|
Object.defineProperty(exports, "MAX_PARAMS", { enumerable: true, get: function () { return protocol_js_3.MAX_PARAMS; } });
|
|
1215
1297
|
Object.defineProperty(exports, "MAX_SYNC_UNITS", { enumerable: true, get: function () { return protocol_js_3.MAX_SYNC_UNITS; } });
|
|
@@ -1221,6 +1303,11 @@ Object.defineProperty(exports, "escapeIdent", { enumerable: true, get: function
|
|
|
1221
1303
|
Object.defineProperty(exports, "ident", { enumerable: true, get: function () { return escape_js_1.ident; } });
|
|
1222
1304
|
Object.defineProperty(exports, "powql", { enumerable: true, get: function () { return escape_js_1.powql; } });
|
|
1223
1305
|
Object.defineProperty(exports, "PowqlIdent", { enumerable: true, get: function () { return escape_js_1.PowqlIdent; } });
|
|
1306
|
+
Object.defineProperty(exports, "escapeSqlLiteral", { enumerable: true, get: function () { return escape_js_1.escapeSqlLiteral; } });
|
|
1307
|
+
Object.defineProperty(exports, "escapeSqlIdent", { enumerable: true, get: function () { return escape_js_1.escapeSqlIdent; } });
|
|
1308
|
+
Object.defineProperty(exports, "sqlIdent", { enumerable: true, get: function () { return escape_js_1.sqlIdent; } });
|
|
1309
|
+
Object.defineProperty(exports, "sql", { enumerable: true, get: function () { return escape_js_1.sql; } });
|
|
1310
|
+
Object.defineProperty(exports, "SqlIdent", { enumerable: true, get: function () { return escape_js_1.SqlIdent; } });
|
|
1224
1311
|
var pool_js_1 = require("./pool.js");
|
|
1225
1312
|
Object.defineProperty(exports, "Pool", { enumerable: true, get: function () { return pool_js_1.Pool; } });
|
|
1226
1313
|
var script_js_2 = require("./script.js");
|
package/dist/cjs/protocol.d.ts
CHANGED
|
@@ -43,6 +43,23 @@ export declare const MAX_PAYLOAD_SIZE: number;
|
|
|
43
43
|
export declare const MAX_ROWS = 10000000;
|
|
44
44
|
/** Maximum number of columns allowed in a result set. */
|
|
45
45
|
export declare const MAX_COLUMNS = 4096;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum number of decoded cells (rows x columns) in one result frame.
|
|
48
|
+
*
|
|
49
|
+
* MAX_ROWS and the per-frame byte-shape check are not enough on their own: a
|
|
50
|
+
* cell costs 4 bytes on the wire at minimum (an empty string's length prefix)
|
|
51
|
+
* but far more than that in JS heap, since every row is its own array. A
|
|
52
|
+
* hostile or MITM'd server could therefore declare 10M single-column rows,
|
|
53
|
+
* back them with a ~40 MB frame of empty cells, and make the client allocate
|
|
54
|
+
* roughly 1.9 GB (measured) before anything downstream could react. The
|
|
55
|
+
* default transport is plaintext, so this is reachable by anyone on the path.
|
|
56
|
+
*
|
|
57
|
+
* This cap bounds that allocation absolutely instead of trusting the declared
|
|
58
|
+
* count: a result frame can never expand past a few hundred MB whatever the
|
|
59
|
+
* server claims. Results genuinely larger than this must be paged with
|
|
60
|
+
* `limit`/`offset`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MAX_RESULT_CELLS = 2000000;
|
|
46
63
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
47
64
|
export declare const MAX_PARAMS = 4096;
|
|
48
65
|
/** Maximum retained units accepted in one sync pull result. */
|
package/dist/cjs/protocol.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This mirrors crates/server/src/protocol.rs — keep them in sync.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_COLUMNS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.MSG_RESULT_SCALAR_NATIVE = exports.MSG_RESULT_ROWS_NATIVE = exports.MSG_QUERY_SQL_NATIVE = exports.MSG_QUERY_PARAMS_NATIVE = exports.MSG_QUERY_NATIVE = exports.MSG_PONG = exports.MSG_PING = exports.MSG_DISCONNECT = exports.MSG_RESULT_MSG = exports.MSG_ERROR = exports.MSG_RESULT_OK = exports.MSG_RESULT_SCALAR = exports.MSG_RESULT_ROWS = exports.MSG_SYNC_ACK_RESULT = exports.MSG_SYNC_PULL_RESULT = exports.MSG_SYNC_STATUS_RESULT = exports.MSG_SYNC_ACK = exports.MSG_SYNC_PULL = exports.MSG_SYNC_STATUS = exports.MSG_QUERY_SQL = exports.MSG_QUERY_PARAMS = exports.MSG_QUERY = exports.MSG_CONNECT_OK = exports.MSG_CONNECT = void 0;
|
|
11
|
+
exports.MAX_SYNC_PULL_BYTES = exports.MAX_SYNC_PULL_UNITS = exports.MAX_SYNC_UNITS = exports.MAX_PARAMS = exports.MAX_RESULT_CELLS = exports.MAX_COLUMNS = exports.MAX_ROWS = exports.MAX_PAYLOAD_SIZE = exports.MSG_RESULT_SCALAR_NATIVE = exports.MSG_RESULT_ROWS_NATIVE = exports.MSG_QUERY_SQL_NATIVE = exports.MSG_QUERY_PARAMS_NATIVE = exports.MSG_QUERY_NATIVE = exports.MSG_PONG = exports.MSG_PING = exports.MSG_DISCONNECT = exports.MSG_RESULT_MSG = exports.MSG_ERROR = exports.MSG_RESULT_OK = exports.MSG_RESULT_SCALAR = exports.MSG_RESULT_ROWS = exports.MSG_SYNC_ACK_RESULT = exports.MSG_SYNC_PULL_RESULT = exports.MSG_SYNC_STATUS_RESULT = exports.MSG_SYNC_ACK = exports.MSG_SYNC_PULL = exports.MSG_SYNC_STATUS = exports.MSG_QUERY_SQL = exports.MSG_QUERY_PARAMS = exports.MSG_QUERY = exports.MSG_CONNECT_OK = exports.MSG_CONNECT = void 0;
|
|
12
12
|
exports.encode = encode;
|
|
13
13
|
exports.tryDecode = tryDecode;
|
|
14
14
|
exports.MSG_CONNECT = 0x01;
|
|
@@ -49,6 +49,29 @@ exports.MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
|
|
|
49
49
|
exports.MAX_ROWS = 10_000_000;
|
|
50
50
|
/** Maximum number of columns allowed in a result set. */
|
|
51
51
|
exports.MAX_COLUMNS = 4096;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum number of decoded cells (rows x columns) in one result frame.
|
|
54
|
+
*
|
|
55
|
+
* MAX_ROWS and the per-frame byte-shape check are not enough on their own: a
|
|
56
|
+
* cell costs 4 bytes on the wire at minimum (an empty string's length prefix)
|
|
57
|
+
* but far more than that in JS heap, since every row is its own array. A
|
|
58
|
+
* hostile or MITM'd server could therefore declare 10M single-column rows,
|
|
59
|
+
* back them with a ~40 MB frame of empty cells, and make the client allocate
|
|
60
|
+
* roughly 1.9 GB (measured) before anything downstream could react. The
|
|
61
|
+
* default transport is plaintext, so this is reachable by anyone on the path.
|
|
62
|
+
*
|
|
63
|
+
* This cap bounds that allocation absolutely instead of trusting the declared
|
|
64
|
+
* count: a result frame can never expand past a few hundred MB whatever the
|
|
65
|
+
* server claims. Results genuinely larger than this must be paged with
|
|
66
|
+
* `limit`/`offset`.
|
|
67
|
+
*/
|
|
68
|
+
exports.MAX_RESULT_CELLS = 2_000_000;
|
|
69
|
+
/** Reject a declared result shape whose cell count exceeds MAX_RESULT_CELLS. */
|
|
70
|
+
function checkResultCells(rowCount, colCount) {
|
|
71
|
+
if (rowCount * colCount > exports.MAX_RESULT_CELLS) {
|
|
72
|
+
throw new Error(`result too large: ${rowCount * colCount} cells (${rowCount} rows x ${colCount} columns, max ${exports.MAX_RESULT_CELLS})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
52
75
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
53
76
|
exports.MAX_PARAMS = 4096;
|
|
54
77
|
/** Maximum retained units accepted in one sync pull result. */
|
|
@@ -490,6 +513,7 @@ function decodePayload(msgType, payload) {
|
|
|
490
513
|
if (remaining < minRowBytes) {
|
|
491
514
|
throw new Error(`row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
492
515
|
}
|
|
516
|
+
checkResultCells(rowCount, colCount);
|
|
493
517
|
const rows = [];
|
|
494
518
|
for (let r = 0; r < rowCount; r++) {
|
|
495
519
|
const row = [];
|
|
@@ -522,6 +546,7 @@ function decodePayload(msgType, payload) {
|
|
|
522
546
|
if (BigInt(payload.length - cursor.pos) < minimumBytes) {
|
|
523
547
|
throw new Error(`native row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
524
548
|
}
|
|
549
|
+
checkResultCells(rowCount, colCount);
|
|
525
550
|
const rows = [];
|
|
526
551
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
527
552
|
const row = [];
|
package/dist/escape.d.ts
CHANGED
|
@@ -52,3 +52,65 @@ export declare function escapeLiteral(value: string | number | bigint | boolean
|
|
|
52
52
|
* const q = powql`insert ${ident(table)} { name := ${userName} }`;
|
|
53
53
|
*/
|
|
54
54
|
export declare function powql(strings: TemplateStringsArray, ...values: unknown[]): string;
|
|
55
|
+
/**
|
|
56
|
+
* SQL composition helpers for PowDB's SQL frontend.
|
|
57
|
+
*
|
|
58
|
+
* READ THIS FIRST. PowDB's SQL surface has NO parameter binding on any wire
|
|
59
|
+
* frame: `querySql`/`querySqlNative` take a statement and nothing else, and
|
|
60
|
+
* there is no `QuerySqlParams` message. The PowQL surface does have real `$N`
|
|
61
|
+
* binding (`client.query(q, [values])`), where the server substitutes a literal
|
|
62
|
+
* TOKEN and injection-shaped input is inert.
|
|
63
|
+
*
|
|
64
|
+
* So: **prefer PowQL with `$N` parameters for anything built from untrusted
|
|
65
|
+
* input.** These helpers exist because the SQL path would otherwise leave
|
|
66
|
+
* string concatenation as the only option, which is strictly worse. They escape
|
|
67
|
+
* correctly for PowDB's own SQL lexer (`crates/query/src/sql.rs`), but escaping
|
|
68
|
+
* is a weaker guarantee than binding: it depends on the value landing in a
|
|
69
|
+
* string/number position, and it cannot make an identifier or a keyword safe.
|
|
70
|
+
*
|
|
71
|
+
* PowDB SQL string rules (verified against `lex_sql` in `crates/query/src/sql.rs`):
|
|
72
|
+
* - Strings are delimited by `'`.
|
|
73
|
+
* - `''` inside a single-quoted string is a literal `'`.
|
|
74
|
+
* - A backslash also escapes the next character (`\n`, `\t`, and `\X` → `X`),
|
|
75
|
+
* which standard SQL does NOT do, so a literal backslash must be doubled.
|
|
76
|
+
* - `"` also opens a string in this lexer, so double quotes cannot be used to
|
|
77
|
+
* quote an identifier. Identifiers are therefore validated, never quoted.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* Render a JS value as a PowDB SQL literal. Same accepted types as
|
|
81
|
+
* {@link escapeLiteral}: `string`, `number`, `bigint`, `boolean`, `null`.
|
|
82
|
+
*
|
|
83
|
+
* - string → `'...'` with `\` doubled and `'` doubled
|
|
84
|
+
* - number → decimal; rejects `NaN`/`±Infinity`
|
|
85
|
+
* - bigint → decimal digits
|
|
86
|
+
* - boolean → `true` / `false`
|
|
87
|
+
* - null → `null`
|
|
88
|
+
*/
|
|
89
|
+
export declare function escapeSqlLiteral(value: string | number | bigint | boolean | null): string;
|
|
90
|
+
/**
|
|
91
|
+
* Validate a SQL identifier (table, column, alias). Returns it unchanged on
|
|
92
|
+
* success and throws `TypeError` otherwise.
|
|
93
|
+
*
|
|
94
|
+
* Identifiers are validated rather than quoted because PowDB's SQL lexer reads
|
|
95
|
+
* `"..."` as a string literal, so there is no identifier-quoting syntax to fall
|
|
96
|
+
* back on. Only `^[A-Za-z_][A-Za-z0-9_]*$` is accepted.
|
|
97
|
+
*/
|
|
98
|
+
export declare function escapeSqlIdent(name: string): string;
|
|
99
|
+
/** Wrapper marking a string as a SQL identifier for the `sql` tagged template. */
|
|
100
|
+
export declare class SqlIdent {
|
|
101
|
+
readonly name: string;
|
|
102
|
+
constructor(name: string);
|
|
103
|
+
}
|
|
104
|
+
/** Factory for {@link SqlIdent}. */
|
|
105
|
+
export declare function sqlIdent(name: string): SqlIdent;
|
|
106
|
+
/**
|
|
107
|
+
* Tagged template for SQL composition. Interpolated values are escaped as SQL
|
|
108
|
+
* literals; wrap one in `sqlIdent(...)` to interpolate an identifier.
|
|
109
|
+
*
|
|
110
|
+
* const q = sql`SELECT name FROM ${sqlIdent(table)} WHERE name = ${userName}`;
|
|
111
|
+
* await client.querySql(q);
|
|
112
|
+
*
|
|
113
|
+
* Escaping, not binding: see the section note above, and prefer PowQL's `$N`
|
|
114
|
+
* parameters when the input is untrusted.
|
|
115
|
+
*/
|
|
116
|
+
export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): string;
|
package/dist/escape.js
CHANGED
|
@@ -100,8 +100,130 @@ export function powql(strings, ...values) {
|
|
|
100
100
|
return out;
|
|
101
101
|
}
|
|
102
102
|
// ──────────────────────────────────────────────────────────
|
|
103
|
+
// SQL frontend
|
|
104
|
+
// ──────────────────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* SQL composition helpers for PowDB's SQL frontend.
|
|
107
|
+
*
|
|
108
|
+
* READ THIS FIRST. PowDB's SQL surface has NO parameter binding on any wire
|
|
109
|
+
* frame: `querySql`/`querySqlNative` take a statement and nothing else, and
|
|
110
|
+
* there is no `QuerySqlParams` message. The PowQL surface does have real `$N`
|
|
111
|
+
* binding (`client.query(q, [values])`), where the server substitutes a literal
|
|
112
|
+
* TOKEN and injection-shaped input is inert.
|
|
113
|
+
*
|
|
114
|
+
* So: **prefer PowQL with `$N` parameters for anything built from untrusted
|
|
115
|
+
* input.** These helpers exist because the SQL path would otherwise leave
|
|
116
|
+
* string concatenation as the only option, which is strictly worse. They escape
|
|
117
|
+
* correctly for PowDB's own SQL lexer (`crates/query/src/sql.rs`), but escaping
|
|
118
|
+
* is a weaker guarantee than binding: it depends on the value landing in a
|
|
119
|
+
* string/number position, and it cannot make an identifier or a keyword safe.
|
|
120
|
+
*
|
|
121
|
+
* PowDB SQL string rules (verified against `lex_sql` in `crates/query/src/sql.rs`):
|
|
122
|
+
* - Strings are delimited by `'`.
|
|
123
|
+
* - `''` inside a single-quoted string is a literal `'`.
|
|
124
|
+
* - A backslash also escapes the next character (`\n`, `\t`, and `\X` → `X`),
|
|
125
|
+
* which standard SQL does NOT do, so a literal backslash must be doubled.
|
|
126
|
+
* - `"` also opens a string in this lexer, so double quotes cannot be used to
|
|
127
|
+
* quote an identifier. Identifiers are therefore validated, never quoted.
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Render a JS value as a PowDB SQL literal. Same accepted types as
|
|
131
|
+
* {@link escapeLiteral}: `string`, `number`, `bigint`, `boolean`, `null`.
|
|
132
|
+
*
|
|
133
|
+
* - string → `'...'` with `\` doubled and `'` doubled
|
|
134
|
+
* - number → decimal; rejects `NaN`/`±Infinity`
|
|
135
|
+
* - bigint → decimal digits
|
|
136
|
+
* - boolean → `true` / `false`
|
|
137
|
+
* - null → `null`
|
|
138
|
+
*/
|
|
139
|
+
export function escapeSqlLiteral(value) {
|
|
140
|
+
if (value === null)
|
|
141
|
+
return "null";
|
|
142
|
+
const t = typeof value;
|
|
143
|
+
if (t === "string") {
|
|
144
|
+
// Backslash first: this lexer treats `\` as an escape inside strings, so a
|
|
145
|
+
// lone backslash would otherwise swallow the quote doubling that follows.
|
|
146
|
+
const escaped = value
|
|
147
|
+
.replace(/\\/g, "\\\\")
|
|
148
|
+
.replace(/'/g, "''");
|
|
149
|
+
return `'${escaped}'`;
|
|
150
|
+
}
|
|
151
|
+
if (t === "number") {
|
|
152
|
+
const n = value;
|
|
153
|
+
if (!Number.isFinite(n)) {
|
|
154
|
+
throw new TypeError(`escapeSqlLiteral: non-finite number ${String(n)} cannot be represented as a SQL literal`);
|
|
155
|
+
}
|
|
156
|
+
return String(n);
|
|
157
|
+
}
|
|
158
|
+
if (t === "bigint") {
|
|
159
|
+
return value.toString(10);
|
|
160
|
+
}
|
|
161
|
+
if (t === "boolean") {
|
|
162
|
+
return value ? "true" : "false";
|
|
163
|
+
}
|
|
164
|
+
throw new TypeError(`escapeSqlLiteral: unsupported type ${describe(value)}`);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Validate a SQL identifier (table, column, alias). Returns it unchanged on
|
|
168
|
+
* success and throws `TypeError` otherwise.
|
|
169
|
+
*
|
|
170
|
+
* Identifiers are validated rather than quoted because PowDB's SQL lexer reads
|
|
171
|
+
* `"..."` as a string literal, so there is no identifier-quoting syntax to fall
|
|
172
|
+
* back on. Only `^[A-Za-z_][A-Za-z0-9_]*$` is accepted.
|
|
173
|
+
*/
|
|
174
|
+
export function escapeSqlIdent(name) {
|
|
175
|
+
if (typeof name !== "string") {
|
|
176
|
+
throw new TypeError(`escapeSqlIdent: expected string, got ${typeof name}`);
|
|
177
|
+
}
|
|
178
|
+
if (name.length === 0) {
|
|
179
|
+
throw new TypeError("escapeSqlIdent: identifier must not be empty");
|
|
180
|
+
}
|
|
181
|
+
if (!IDENT_RE.test(name)) {
|
|
182
|
+
throw new TypeError(`escapeSqlIdent: invalid identifier ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`);
|
|
183
|
+
}
|
|
184
|
+
return name;
|
|
185
|
+
}
|
|
186
|
+
/** Wrapper marking a string as a SQL identifier for the `sql` tagged template. */
|
|
187
|
+
export class SqlIdent {
|
|
188
|
+
name;
|
|
189
|
+
constructor(name) {
|
|
190
|
+
this.name = name;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Factory for {@link SqlIdent}. */
|
|
194
|
+
export function sqlIdent(name) {
|
|
195
|
+
return new SqlIdent(name);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Tagged template for SQL composition. Interpolated values are escaped as SQL
|
|
199
|
+
* literals; wrap one in `sqlIdent(...)` to interpolate an identifier.
|
|
200
|
+
*
|
|
201
|
+
* const q = sql`SELECT name FROM ${sqlIdent(table)} WHERE name = ${userName}`;
|
|
202
|
+
* await client.querySql(q);
|
|
203
|
+
*
|
|
204
|
+
* Escaping, not binding: see the section note above, and prefer PowQL's `$N`
|
|
205
|
+
* parameters when the input is untrusted.
|
|
206
|
+
*/
|
|
207
|
+
export function sql(strings, ...values) {
|
|
208
|
+
let out = strings[0] ?? "";
|
|
209
|
+
for (let i = 0; i < values.length; i++) {
|
|
210
|
+
out += renderSqlInterpolation(values[i]);
|
|
211
|
+
out += strings[i + 1] ?? "";
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
// ──────────────────────────────────────────────────────────
|
|
103
216
|
// internals
|
|
104
217
|
// ──────────────────────────────────────────────────────────
|
|
218
|
+
function renderSqlInterpolation(value) {
|
|
219
|
+
if (value instanceof SqlIdent) {
|
|
220
|
+
return escapeSqlIdent(value.name);
|
|
221
|
+
}
|
|
222
|
+
if (value instanceof PowqlIdent) {
|
|
223
|
+
throw new TypeError("sql: use sqlIdent(...) for SQL identifiers, not ident(...)");
|
|
224
|
+
}
|
|
225
|
+
return escapeSqlLiteral(value);
|
|
226
|
+
}
|
|
105
227
|
function renderInterpolation(value) {
|
|
106
228
|
if (value instanceof PowqlIdent) {
|
|
107
229
|
return escapeIdent(value.name);
|
package/dist/index.d.ts
CHANGED
|
@@ -19,15 +19,21 @@ import { EventEmitter } from "node:events";
|
|
|
19
19
|
import { type NativeJson, type SyncRepairAction, type WireRetainedUnit, type WireSyncStatus, type WireValue } from "./protocol.js";
|
|
20
20
|
import { type TypedRow, type TypedSchema } from "./typed.js";
|
|
21
21
|
/** Client library version. Compared to the server's reported version. */
|
|
22
|
-
export declare const CLIENT_VERSION = "0.
|
|
22
|
+
export declare const CLIENT_VERSION = "0.20.0";
|
|
23
23
|
/**
|
|
24
24
|
* The maximum catalog format version this client can read. State this as the
|
|
25
25
|
* `catalogVersion` in sync pull requests: the server accepts any replica whose
|
|
26
26
|
* maximum is at least its active catalog format and rejects an older replica.
|
|
27
27
|
* When validating a server-reported catalog version, accept anything at or
|
|
28
28
|
* below this and reject anything newer (the client cannot read it).
|
|
29
|
+
*
|
|
30
|
+
* v7 is the entity-links format: it appends a relationship-link section
|
|
31
|
+
* before the trailing CRC, staircase-defaulted so every older file still
|
|
32
|
+
* loads (a pre-v7 file simply has zero links). It activates lazily on the
|
|
33
|
+
* first `link` declaration. The client treats catalog payloads as opaque
|
|
34
|
+
* bytes and only states this ceiling in the sync handshake.
|
|
29
35
|
*/
|
|
30
|
-
export declare const SUPPORTED_CATALOG_VERSION =
|
|
36
|
+
export declare const SUPPORTED_CATALOG_VERSION = 7;
|
|
31
37
|
/**
|
|
32
38
|
* Throw when a server-reported catalog format is newer than this client can
|
|
33
39
|
* read. Accepts `serverCatalogVersion <= SUPPORTED_CATALOG_VERSION`; rejects a
|
|
@@ -51,6 +57,11 @@ export type QueryResult = {
|
|
|
51
57
|
export type { NativeJson } from "./protocol.js";
|
|
52
58
|
/** A value returned by the lossless native wire surface. */
|
|
53
59
|
export type NativeValue = null | number | bigint | boolean | string | Uint8Array | NativeJson;
|
|
60
|
+
/**
|
|
61
|
+
* One row of a native (lossless) result, keyed by column name. The default row
|
|
62
|
+
* type of {@link Client.queryObjects} when no generic is supplied.
|
|
63
|
+
*/
|
|
64
|
+
export type NativeRow = Record<string, NativeValue>;
|
|
54
65
|
export type NativeQueryResult = {
|
|
55
66
|
kind: "rows";
|
|
56
67
|
columns: string[];
|
|
@@ -376,13 +387,62 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
376
387
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
377
388
|
* rules and supported column types.
|
|
378
389
|
*
|
|
379
|
-
* Returns
|
|
390
|
+
* Returns `Row[]`, an array of objects keyed by column name (`TypedRow[]`
|
|
391
|
+
* when no generic is supplied). The generic is an unchecked assertion about
|
|
392
|
+
* the query's shape: the schema drives coercion, nothing validates the type.
|
|
393
|
+
* Positional `$N` parameters are accepted in the same position as
|
|
394
|
+
* {@link query}, so typed rows and injection-safe binding compose.
|
|
395
|
+
*
|
|
396
|
+
* This is the LEGACY stringly-typed path plus schema coercion. For lossless
|
|
397
|
+
* object rows with no schema at all, use {@link queryObjects}.
|
|
398
|
+
*
|
|
380
399
|
* Throws `PowDBError(code="query_failed")` if the query is not a
|
|
381
400
|
* rows-returning query.
|
|
382
401
|
*/
|
|
383
|
-
queryTyped(query: string, schema: TypedSchema,
|
|
402
|
+
queryTyped<Row extends TypedRow = TypedRow>(query: string, schema: TypedSchema, paramsOrOpts?: QueryParam[] | {
|
|
403
|
+
signal?: AbortSignal;
|
|
404
|
+
}, maybeOpts?: {
|
|
405
|
+
signal?: AbortSignal;
|
|
406
|
+
}): Promise<Row[]>;
|
|
407
|
+
/**
|
|
408
|
+
* Run PowQL on the LOSSLESS native wire surface and return object rows keyed
|
|
409
|
+
* by column name.
|
|
410
|
+
*
|
|
411
|
+
* This is {@link queryNative} plus the row-to-object step, so no schema is
|
|
412
|
+
* needed: the server states each cell's type, bytes stay bytes, JSON stays
|
|
413
|
+
* recursive data, and out-of-range integers stay `bigint`. Contrast with
|
|
414
|
+
* {@link queryTyped}, which coerces the legacy stringly-typed path using a
|
|
415
|
+
* caller-supplied schema.
|
|
416
|
+
*
|
|
417
|
+
* interface User { name: string; age: number }
|
|
418
|
+
* const users = await client.queryObjects<User>(
|
|
419
|
+
* "User filter .age > $1 { .name, .age }",
|
|
420
|
+
* [25],
|
|
421
|
+
* );
|
|
422
|
+
*
|
|
423
|
+
* The generic is an unchecked assertion about the query's shape, exactly like
|
|
424
|
+
* a SQL driver's row type: nothing validates it at runtime. Duplicate column
|
|
425
|
+
* names collapse (last one wins), so alias them in the projection.
|
|
426
|
+
*
|
|
427
|
+
* Throws `PowDBError(code="query_failed")` if the query is not rows-returning.
|
|
428
|
+
*/
|
|
429
|
+
queryObjects<Row = NativeRow>(query: string, paramsOrOpts?: QueryParam[] | {
|
|
430
|
+
signal?: AbortSignal;
|
|
431
|
+
}, maybeOpts?: {
|
|
432
|
+
signal?: AbortSignal;
|
|
433
|
+
}): Promise<Row[]>;
|
|
434
|
+
/**
|
|
435
|
+
* SQL counterpart of {@link queryObjects}: runs SQL on the lossless native
|
|
436
|
+
* surface and returns object rows keyed by column name.
|
|
437
|
+
*
|
|
438
|
+
* The SQL frontend cannot bind parameters on any wire frame yet, so build SQL
|
|
439
|
+
* with {@link escapeSqlLiteral} / {@link sqlIdent} (or the `sql` tagged
|
|
440
|
+
* template) rather than string concatenation. For parameter binding, use the
|
|
441
|
+
* PowQL surface, which has real `$N` placeholders.
|
|
442
|
+
*/
|
|
443
|
+
querySqlObjects<Row = NativeRow>(query: string, opts?: {
|
|
384
444
|
signal?: AbortSignal;
|
|
385
|
-
}): Promise<
|
|
445
|
+
}): Promise<Row[]>;
|
|
386
446
|
/**
|
|
387
447
|
* Execute a multi-statement PowQL script on this connection, pipelined.
|
|
388
448
|
*
|
|
@@ -466,8 +526,8 @@ export declare class Client extends EventEmitter<ClientEvents> {
|
|
|
466
526
|
}
|
|
467
527
|
export { encode, tryDecode } from "./protocol.js";
|
|
468
528
|
export type { Message, SyncRepairAction, WireValue, WireParam, WireRetainedUnit, WireSyncStatus, } from "./protocol.js";
|
|
469
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
470
|
-
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
529
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_RESULT_CELLS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
530
|
+
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, escapeSqlLiteral, escapeSqlIdent, sqlIdent, sql, SqlIdent, } from "./escape.js";
|
|
471
531
|
export { Pool } from "./pool.js";
|
|
472
532
|
export type { PoolOptions } from "./pool.js";
|
|
473
533
|
export { splitStatements } from "./script.js";
|
package/dist/index.js
CHANGED
|
@@ -22,15 +22,21 @@ import { errorCodeForWireClass, isPowDBError, PowDBError, PowDBScriptError, } fr
|
|
|
22
22
|
import { splitStatements } from "./script.js";
|
|
23
23
|
import { coerceRows, } from "./typed.js";
|
|
24
24
|
/** Client library version. Compared to the server's reported version. */
|
|
25
|
-
export const CLIENT_VERSION = "0.
|
|
25
|
+
export const CLIENT_VERSION = "0.20.0";
|
|
26
26
|
/**
|
|
27
27
|
* The maximum catalog format version this client can read. State this as the
|
|
28
28
|
* `catalogVersion` in sync pull requests: the server accepts any replica whose
|
|
29
29
|
* maximum is at least its active catalog format and rejects an older replica.
|
|
30
30
|
* When validating a server-reported catalog version, accept anything at or
|
|
31
31
|
* below this and reject anything newer (the client cannot read it).
|
|
32
|
+
*
|
|
33
|
+
* v7 is the entity-links format: it appends a relationship-link section
|
|
34
|
+
* before the trailing CRC, staircase-defaulted so every older file still
|
|
35
|
+
* loads (a pre-v7 file simply has zero links). It activates lazily on the
|
|
36
|
+
* first `link` declaration. The client treats catalog payloads as opaque
|
|
37
|
+
* bytes and only states this ceiling in the sync handshake.
|
|
32
38
|
*/
|
|
33
|
-
export const SUPPORTED_CATALOG_VERSION =
|
|
39
|
+
export const SUPPORTED_CATALOG_VERSION = 7;
|
|
34
40
|
/**
|
|
35
41
|
* Throw when a server-reported catalog format is newer than this client can
|
|
36
42
|
* read. Accepts `serverCatalogVersion <= SUPPORTED_CATALOG_VERSION`; rejects a
|
|
@@ -120,6 +126,24 @@ function nativeQueryResult(reply) {
|
|
|
120
126
|
throw new PowDBError(`unexpected reply to native query: ${reply.type}`, "protocol_error");
|
|
121
127
|
}
|
|
122
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Turn a rows-shaped native result into object rows keyed by column name.
|
|
131
|
+
* Non-rows results are a caller mistake, not a server error, so they raise the
|
|
132
|
+
* same `query_failed` code `queryTyped` uses.
|
|
133
|
+
*/
|
|
134
|
+
function nativeRowObjects(result, method) {
|
|
135
|
+
if (result.kind !== "rows") {
|
|
136
|
+
throw new PowDBError(`${method}: expected rows result, got ${result.kind}`, "query_failed");
|
|
137
|
+
}
|
|
138
|
+
const { columns, rows } = result;
|
|
139
|
+
return rows.map((values) => {
|
|
140
|
+
const row = {};
|
|
141
|
+
for (let i = 0; i < columns.length; i++) {
|
|
142
|
+
row[columns[i]] = values[i] ?? null;
|
|
143
|
+
}
|
|
144
|
+
return row;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
123
147
|
function rawNativeQueryResult(reply) {
|
|
124
148
|
switch (reply.type) {
|
|
125
149
|
case "ResultRowsNative":
|
|
@@ -667,17 +691,74 @@ export class Client extends EventEmitter {
|
|
|
667
691
|
* using the caller-supplied schema. See `./typed.ts` for the coercion
|
|
668
692
|
* rules and supported column types.
|
|
669
693
|
*
|
|
670
|
-
* Returns
|
|
694
|
+
* Returns `Row[]`, an array of objects keyed by column name (`TypedRow[]`
|
|
695
|
+
* when no generic is supplied). The generic is an unchecked assertion about
|
|
696
|
+
* the query's shape: the schema drives coercion, nothing validates the type.
|
|
697
|
+
* Positional `$N` parameters are accepted in the same position as
|
|
698
|
+
* {@link query}, so typed rows and injection-safe binding compose.
|
|
699
|
+
*
|
|
700
|
+
* This is the LEGACY stringly-typed path plus schema coercion. For lossless
|
|
701
|
+
* object rows with no schema at all, use {@link queryObjects}.
|
|
702
|
+
*
|
|
671
703
|
* Throws `PowDBError(code="query_failed")` if the query is not a
|
|
672
704
|
* rows-returning query.
|
|
673
705
|
*/
|
|
674
|
-
async queryTyped(query, schema,
|
|
675
|
-
|
|
706
|
+
async queryTyped(query, schema, paramsOrOpts, maybeOpts) {
|
|
707
|
+
// Same overload disambiguation as `query`, so typed rows and `$N`
|
|
708
|
+
// parameter binding are usable together:
|
|
709
|
+
// queryTyped(q, schema) no params, no opts
|
|
710
|
+
// queryTyped(q, schema, opts) legacy 3-arg opts form
|
|
711
|
+
// queryTyped(q, schema, params) positional $N parameters
|
|
712
|
+
// queryTyped(q, schema, params, opts) params plus opts
|
|
713
|
+
const result = Array.isArray(paramsOrOpts)
|
|
714
|
+
? await this.query(query, paramsOrOpts, maybeOpts)
|
|
715
|
+
: await this.query(query, paramsOrOpts);
|
|
676
716
|
if (result.kind !== "rows") {
|
|
677
717
|
throw new PowDBError(`queryTyped: expected rows result, got ${result.kind}`, "query_failed");
|
|
678
718
|
}
|
|
679
719
|
return coerceRows(result.columns, result.rows, schema);
|
|
680
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Run PowQL on the LOSSLESS native wire surface and return object rows keyed
|
|
723
|
+
* by column name.
|
|
724
|
+
*
|
|
725
|
+
* This is {@link queryNative} plus the row-to-object step, so no schema is
|
|
726
|
+
* needed: the server states each cell's type, bytes stay bytes, JSON stays
|
|
727
|
+
* recursive data, and out-of-range integers stay `bigint`. Contrast with
|
|
728
|
+
* {@link queryTyped}, which coerces the legacy stringly-typed path using a
|
|
729
|
+
* caller-supplied schema.
|
|
730
|
+
*
|
|
731
|
+
* interface User { name: string; age: number }
|
|
732
|
+
* const users = await client.queryObjects<User>(
|
|
733
|
+
* "User filter .age > $1 { .name, .age }",
|
|
734
|
+
* [25],
|
|
735
|
+
* );
|
|
736
|
+
*
|
|
737
|
+
* The generic is an unchecked assertion about the query's shape, exactly like
|
|
738
|
+
* a SQL driver's row type: nothing validates it at runtime. Duplicate column
|
|
739
|
+
* names collapse (last one wins), so alias them in the projection.
|
|
740
|
+
*
|
|
741
|
+
* Throws `PowDBError(code="query_failed")` if the query is not rows-returning.
|
|
742
|
+
*/
|
|
743
|
+
async queryObjects(query, paramsOrOpts, maybeOpts) {
|
|
744
|
+
const result = Array.isArray(paramsOrOpts)
|
|
745
|
+
? await this.queryNative(query, paramsOrOpts, maybeOpts)
|
|
746
|
+
: await this.queryNative(query, paramsOrOpts);
|
|
747
|
+
return nativeRowObjects(result, "queryObjects");
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* SQL counterpart of {@link queryObjects}: runs SQL on the lossless native
|
|
751
|
+
* surface and returns object rows keyed by column name.
|
|
752
|
+
*
|
|
753
|
+
* The SQL frontend cannot bind parameters on any wire frame yet, so build SQL
|
|
754
|
+
* with {@link escapeSqlLiteral} / {@link sqlIdent} (or the `sql` tagged
|
|
755
|
+
* template) rather than string concatenation. For parameter binding, use the
|
|
756
|
+
* PowQL surface, which has real `$N` placeholders.
|
|
757
|
+
*/
|
|
758
|
+
async querySqlObjects(query, opts) {
|
|
759
|
+
const result = await this.querySqlNative(query, opts);
|
|
760
|
+
return nativeRowObjects(result, "querySqlObjects");
|
|
761
|
+
}
|
|
681
762
|
async execScript(script, opts) {
|
|
682
763
|
const statements = splitStatements(script);
|
|
683
764
|
const continueOnError = opts?.continueOnError === true;
|
|
@@ -1167,8 +1248,8 @@ function openSocket(target, timeoutMs, tlsOpt) {
|
|
|
1167
1248
|
});
|
|
1168
1249
|
}
|
|
1169
1250
|
export { encode, tryDecode } from "./protocol.js";
|
|
1170
|
-
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
1171
|
-
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
1251
|
+
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_RESULT_CELLS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
1252
|
+
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, escapeSqlLiteral, escapeSqlIdent, sqlIdent, sql, SqlIdent, } from "./escape.js";
|
|
1172
1253
|
export { Pool } from "./pool.js";
|
|
1173
1254
|
export { splitStatements } from "./script.js";
|
|
1174
1255
|
export { errorCodeForWireClass, PowDBError, isPowDBError, PowDBScriptError, isPowDBScriptError, WIRE_ERROR_CLASS, } from "./errors.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -43,6 +43,23 @@ export declare const MAX_PAYLOAD_SIZE: number;
|
|
|
43
43
|
export declare const MAX_ROWS = 10000000;
|
|
44
44
|
/** Maximum number of columns allowed in a result set. */
|
|
45
45
|
export declare const MAX_COLUMNS = 4096;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum number of decoded cells (rows x columns) in one result frame.
|
|
48
|
+
*
|
|
49
|
+
* MAX_ROWS and the per-frame byte-shape check are not enough on their own: a
|
|
50
|
+
* cell costs 4 bytes on the wire at minimum (an empty string's length prefix)
|
|
51
|
+
* but far more than that in JS heap, since every row is its own array. A
|
|
52
|
+
* hostile or MITM'd server could therefore declare 10M single-column rows,
|
|
53
|
+
* back them with a ~40 MB frame of empty cells, and make the client allocate
|
|
54
|
+
* roughly 1.9 GB (measured) before anything downstream could react. The
|
|
55
|
+
* default transport is plaintext, so this is reachable by anyone on the path.
|
|
56
|
+
*
|
|
57
|
+
* This cap bounds that allocation absolutely instead of trusting the declared
|
|
58
|
+
* count: a result frame can never expand past a few hundred MB whatever the
|
|
59
|
+
* server claims. Results genuinely larger than this must be paged with
|
|
60
|
+
* `limit`/`offset`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MAX_RESULT_CELLS = 2000000;
|
|
46
63
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
47
64
|
export declare const MAX_PARAMS = 4096;
|
|
48
65
|
/** Maximum retained units accepted in one sync pull result. */
|
package/dist/protocol.js
CHANGED
|
@@ -44,6 +44,29 @@ export const MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
|
|
|
44
44
|
export const MAX_ROWS = 10_000_000;
|
|
45
45
|
/** Maximum number of columns allowed in a result set. */
|
|
46
46
|
export const MAX_COLUMNS = 4096;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum number of decoded cells (rows x columns) in one result frame.
|
|
49
|
+
*
|
|
50
|
+
* MAX_ROWS and the per-frame byte-shape check are not enough on their own: a
|
|
51
|
+
* cell costs 4 bytes on the wire at minimum (an empty string's length prefix)
|
|
52
|
+
* but far more than that in JS heap, since every row is its own array. A
|
|
53
|
+
* hostile or MITM'd server could therefore declare 10M single-column rows,
|
|
54
|
+
* back them with a ~40 MB frame of empty cells, and make the client allocate
|
|
55
|
+
* roughly 1.9 GB (measured) before anything downstream could react. The
|
|
56
|
+
* default transport is plaintext, so this is reachable by anyone on the path.
|
|
57
|
+
*
|
|
58
|
+
* This cap bounds that allocation absolutely instead of trusting the declared
|
|
59
|
+
* count: a result frame can never expand past a few hundred MB whatever the
|
|
60
|
+
* server claims. Results genuinely larger than this must be paged with
|
|
61
|
+
* `limit`/`offset`.
|
|
62
|
+
*/
|
|
63
|
+
export const MAX_RESULT_CELLS = 2_000_000;
|
|
64
|
+
/** Reject a declared result shape whose cell count exceeds MAX_RESULT_CELLS. */
|
|
65
|
+
function checkResultCells(rowCount, colCount) {
|
|
66
|
+
if (rowCount * colCount > MAX_RESULT_CELLS) {
|
|
67
|
+
throw new Error(`result too large: ${rowCount * colCount} cells (${rowCount} rows x ${colCount} columns, max ${MAX_RESULT_CELLS})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
47
70
|
/** Maximum number of bound parameters in a QueryWithParams message. */
|
|
48
71
|
export const MAX_PARAMS = 4096;
|
|
49
72
|
/** Maximum retained units accepted in one sync pull result. */
|
|
@@ -485,6 +508,7 @@ function decodePayload(msgType, payload) {
|
|
|
485
508
|
if (remaining < minRowBytes) {
|
|
486
509
|
throw new Error(`row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
487
510
|
}
|
|
511
|
+
checkResultCells(rowCount, colCount);
|
|
488
512
|
const rows = [];
|
|
489
513
|
for (let r = 0; r < rowCount; r++) {
|
|
490
514
|
const row = [];
|
|
@@ -517,6 +541,7 @@ function decodePayload(msgType, payload) {
|
|
|
517
541
|
if (BigInt(payload.length - cursor.pos) < minimumBytes) {
|
|
518
542
|
throw new Error(`native row data too short for declared shape: ${rowCount} rows x ${colCount} columns`);
|
|
519
543
|
}
|
|
544
|
+
checkResultCells(rowCount, colCount);
|
|
520
545
|
const rows = [];
|
|
521
546
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
522
547
|
const row = [];
|