@zvndev/powdb-client 0.8.1 → 0.10.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/README.md +124 -2
- package/dist/cjs/errors.d.ts +70 -0
- package/dist/cjs/errors.js +68 -0
- package/dist/cjs/escape.d.ts +54 -0
- package/dist/cjs/escape.js +131 -0
- package/dist/cjs/index.d.ts +393 -0
- package/dist/cjs/index.js +1046 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pool.d.ts +140 -0
- package/dist/cjs/pool.js +335 -0
- package/dist/cjs/protocol.d.ts +185 -0
- package/dist/cjs/protocol.js +645 -0
- package/dist/cjs/script.d.ts +25 -0
- package/dist/cjs/script.js +69 -0
- package/dist/cjs/typed.d.ts +49 -0
- package/dist/cjs/typed.js +105 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +30 -0
- package/dist/index.d.ts +137 -4
- package/dist/index.js +255 -77
- package/dist/pool.d.ts +18 -1
- package/dist/pool.js +18 -1
- package/dist/script.d.ts +25 -0
- package/dist/script.js +66 -0
- package/package.json +18 -6
package/dist/index.js
CHANGED
|
@@ -18,10 +18,20 @@ import * as net from "node:net";
|
|
|
18
18
|
import * as tls from "node:tls";
|
|
19
19
|
import { EventEmitter } from "node:events";
|
|
20
20
|
import { encode, tryDecode, MAX_SYNC_PULL_BYTES, MAX_SYNC_PULL_UNITS, } from "./protocol.js";
|
|
21
|
-
import { PowDBError } from "./errors.js";
|
|
21
|
+
import { PowDBError, PowDBScriptError, isPowDBError } from "./errors.js";
|
|
22
|
+
import { splitStatements } from "./script.js";
|
|
22
23
|
import { coerceRows, } from "./typed.js";
|
|
23
24
|
/** Client library version. Compared to the server's reported version. */
|
|
24
|
-
export const CLIENT_VERSION = "0.
|
|
25
|
+
export const CLIENT_VERSION = "0.10.0";
|
|
26
|
+
/** Transaction-control statements a `transactional` script may not contain. */
|
|
27
|
+
const TX_CONTROL_RE = /^(begin|commit|rollback)\b/i;
|
|
28
|
+
/**
|
|
29
|
+
* Leading trivia (whitespace and `#` line comments) that may precede a
|
|
30
|
+
* statement's first real token. Must be stripped before matching
|
|
31
|
+
* {@link TX_CONTROL_RE}: the server's lexer skips comments, so
|
|
32
|
+
* `"# note\ncommit"` executes a commit — the guard has to see it too.
|
|
33
|
+
*/
|
|
34
|
+
const LEADING_TRIVIA_RE = /^(?:\s+|#[^\n]*)+/;
|
|
25
35
|
function socketChunkToBuffer(chunk) {
|
|
26
36
|
return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
27
37
|
}
|
|
@@ -123,91 +133,99 @@ export class Client extends EventEmitter {
|
|
|
123
133
|
pending = [];
|
|
124
134
|
closed = false;
|
|
125
135
|
closeError = null;
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
/** Settled once the Connect→ConnectOk handshake completes (or fails). */
|
|
137
|
+
handshake = Promise.resolve();
|
|
138
|
+
/** True once ConnectOk has been received. */
|
|
139
|
+
handshakeComplete = false;
|
|
140
|
+
_serverVersion = "";
|
|
141
|
+
/**
|
|
142
|
+
* Server version from the ConnectOk frame. For a client opened with
|
|
143
|
+
* `eager: true` this is `""` until the handshake reply arrives (await
|
|
144
|
+
* {@link ready} to guarantee it is populated).
|
|
145
|
+
*/
|
|
146
|
+
get serverVersion() {
|
|
147
|
+
return this._serverVersion;
|
|
148
|
+
}
|
|
149
|
+
constructor(socket) {
|
|
128
150
|
super();
|
|
129
151
|
this.socket = socket;
|
|
130
|
-
this.serverVersion = serverVersion;
|
|
131
152
|
this.socket.on("data", (chunk) => this.onData(socketChunkToBuffer(chunk)));
|
|
132
153
|
this.socket.on("error", (err) => this.onClose(err));
|
|
133
154
|
this.socket.on("close", () => this.onClose(null));
|
|
134
155
|
}
|
|
135
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Open a connection, send Connect, and wait for ConnectOk.
|
|
158
|
+
*
|
|
159
|
+
* With `eager: true`, resolve as soon as the Connect frame is written
|
|
160
|
+
* instead — queries may be issued immediately and are pipelined behind
|
|
161
|
+
* the handshake (see {@link ClientOptions.eager} and {@link ready}).
|
|
162
|
+
*/
|
|
136
163
|
static async connect(opts) {
|
|
137
|
-
const { host, port, path, dbName = "default", password = null, user, connectTimeoutMs = 5000, tls: tlsOpt = false, } = opts;
|
|
164
|
+
const { host, port, path, dbName = "default", password = null, user, connectTimeoutMs = 5000, tls: tlsOpt = false, eager = false, } = opts;
|
|
138
165
|
if (path === undefined && (host === undefined || port === undefined)) {
|
|
139
166
|
throw new PowDBError("connect requires either { path } (Unix socket) or { host, port } (TCP)", "connect_failed");
|
|
140
167
|
}
|
|
141
168
|
const socket = await openSocket({ host, port, path }, connectTimeoutMs, tlsOpt);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const client = new Client(socket);
|
|
170
|
+
client.startHandshake({ type: "Connect", dbName, password, username: user ?? null }, path ?? `${host}:${port}`);
|
|
171
|
+
if (!eager) {
|
|
172
|
+
await client.ready();
|
|
173
|
+
}
|
|
174
|
+
return client;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolves once the Connect→ConnectOk handshake has completed; rejects
|
|
178
|
+
* with the handshake error if it failed. For non-eager clients this has
|
|
179
|
+
* already settled by the time {@link connect} returns. Eager callers can
|
|
180
|
+
* await it to learn the handshake outcome without issuing a query.
|
|
181
|
+
*/
|
|
182
|
+
ready() {
|
|
183
|
+
return this.handshake;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Write the Connect frame and register the handshake as the first entry
|
|
187
|
+
* in the pending queue. The reply-matching machinery is strictly FIFO, so
|
|
188
|
+
* the ConnectOk (or Error) frame is matched to the handshake before any
|
|
189
|
+
* pipelined query sees a reply. On failure, every queued query is
|
|
190
|
+
* rejected with the handshake error and the socket is torn down.
|
|
191
|
+
*/
|
|
192
|
+
startHandshake(connect, versionWarnKey) {
|
|
193
|
+
this.handshake = this.send(connect).then((reply) => {
|
|
194
|
+
if (reply.type === "Error") {
|
|
195
|
+
throw new PowDBError(`connect failed: ${reply.message}`, "auth_failed");
|
|
196
|
+
}
|
|
197
|
+
if (reply.type !== "ConnectOk") {
|
|
198
|
+
throw new PowDBError(`expected ConnectOk, got ${reply.type}`, "protocol_error");
|
|
199
|
+
}
|
|
200
|
+
this.handshakeComplete = true;
|
|
201
|
+
this._serverVersion = reply.version;
|
|
202
|
+
// Advisory: warn once per host:port if the server's major differs
|
|
203
|
+
// from the client's. Do not throw or close — this is best-effort.
|
|
204
|
+
const serverMajor = majorOf(reply.version);
|
|
205
|
+
const clientMajor = majorOf(CLIENT_VERSION);
|
|
206
|
+
if (serverMajor !== clientMajor) {
|
|
207
|
+
if (!versionWarnings.has(versionWarnKey)) {
|
|
208
|
+
versionWarnings.add(versionWarnKey);
|
|
209
|
+
console.warn(`[powdb] server version ${reply.version} major (${serverMajor}) ` +
|
|
210
|
+
`differs from client ${CLIENT_VERSION} major (${clientMajor}); ` +
|
|
211
|
+
`behaviour may be inconsistent.`);
|
|
171
212
|
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
213
|
+
}
|
|
214
|
+
}, (err) => {
|
|
215
|
+
// The connection dropped before a handshake reply — surface it as a
|
|
216
|
+
// connect failure (transient, retryable) rather than a bare close.
|
|
217
|
+
if (isPowDBError(err) && err.code === "closed") {
|
|
218
|
+
throw new PowDBError("connection closed during handshake", "connect_failed", { cause: err });
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
});
|
|
222
|
+
// On handshake failure, reject everything queued behind it and tear the
|
|
223
|
+
// socket down. The extra no-op catch keeps an eager caller that never
|
|
224
|
+
// awaits ready() from tripping an unhandled-rejection crash.
|
|
225
|
+
this.handshake.catch((err) => {
|
|
226
|
+
this.onClose(err);
|
|
227
|
+
this.socket.destroy();
|
|
186
228
|
});
|
|
187
|
-
socket.write(encode({ type: "Connect", dbName, password, username: user ?? null }));
|
|
188
|
-
const reply = await handshake;
|
|
189
|
-
if (reply.type === "Error") {
|
|
190
|
-
socket.destroy();
|
|
191
|
-
throw new PowDBError(`connect failed: ${reply.message}`, "auth_failed");
|
|
192
|
-
}
|
|
193
|
-
if (reply.type !== "ConnectOk") {
|
|
194
|
-
socket.destroy();
|
|
195
|
-
throw new PowDBError(`expected ConnectOk, got ${reply.type}`, "protocol_error");
|
|
196
|
-
}
|
|
197
|
-
// Advisory: warn once per host:port if the server's major differs
|
|
198
|
-
// from the client's. Do not throw or close — this is best-effort.
|
|
199
|
-
const serverMajor = majorOf(reply.version);
|
|
200
|
-
const clientMajor = majorOf(CLIENT_VERSION);
|
|
201
|
-
if (serverMajor !== clientMajor) {
|
|
202
|
-
const key = path ?? `${host}:${port}`;
|
|
203
|
-
if (!versionWarnings.has(key)) {
|
|
204
|
-
versionWarnings.add(key);
|
|
205
|
-
console.warn(`[powdb] server version ${reply.version} major (${serverMajor}) ` +
|
|
206
|
-
`differs from client ${CLIENT_VERSION} major (${clientMajor}); ` +
|
|
207
|
-
`behaviour may be inconsistent.`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return new Client(socket, reply.version);
|
|
211
229
|
}
|
|
212
230
|
/**
|
|
213
231
|
* Run a PowQL statement and return the typed result.
|
|
@@ -471,6 +489,136 @@ export class Client extends EventEmitter {
|
|
|
471
489
|
}
|
|
472
490
|
return coerceRows(result.columns, result.rows, schema);
|
|
473
491
|
}
|
|
492
|
+
async execScript(script, opts) {
|
|
493
|
+
const statements = splitStatements(script);
|
|
494
|
+
const continueOnError = opts?.continueOnError === true;
|
|
495
|
+
const transactional = opts?.transactional === true;
|
|
496
|
+
const signal = opts?.signal;
|
|
497
|
+
if (transactional && continueOnError) {
|
|
498
|
+
throw new PowDBError("execScript: `transactional` and `continueOnError` are mutually exclusive", "protocol_error");
|
|
499
|
+
}
|
|
500
|
+
if (transactional) {
|
|
501
|
+
for (let i = 0; i < statements.length; i++) {
|
|
502
|
+
if (TX_CONTROL_RE.test(statements[i].replace(LEADING_TRIVIA_RE, ""))) {
|
|
503
|
+
throw new PowDBError(`execScript: a transactional script may not contain its own transaction control (statement ${i + 1}: ${statements[i]})`, "protocol_error");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Open the transaction and WAIT for its reply before dispatching
|
|
507
|
+
// anything: if `begin` fails there must be nothing else on the wire.
|
|
508
|
+
// Deliberately no signal — an abort after `begin` is on the wire
|
|
509
|
+
// would leave the transaction open with no rollback; an
|
|
510
|
+
// already-aborted signal stops the loop below at statement 0 and
|
|
511
|
+
// takes the rollback path.
|
|
512
|
+
await this.query("begin");
|
|
513
|
+
}
|
|
514
|
+
const outcomes = new Array(statements.length);
|
|
515
|
+
const inFlight = [];
|
|
516
|
+
let firstFailureIndex = -1;
|
|
517
|
+
let firstFailureError = null;
|
|
518
|
+
let dispatched = 0;
|
|
519
|
+
for (let i = 0; i < statements.length; i++) {
|
|
520
|
+
// Stop dispatching when the script is already doomed: fail-fast saw
|
|
521
|
+
// an error, the caller aborted, or the connection is gone.
|
|
522
|
+
if (this.closed || signal?.aborted)
|
|
523
|
+
break;
|
|
524
|
+
if (!continueOnError && firstFailureIndex !== -1)
|
|
525
|
+
break;
|
|
526
|
+
const statement = statements[i];
|
|
527
|
+
const idx = i;
|
|
528
|
+
dispatched++;
|
|
529
|
+
// query() writes the frame synchronously before its first await, so
|
|
530
|
+
// this loop puts every statement on the wire without waiting for any
|
|
531
|
+
// reply; the FIFO pending queue matches replies back in order.
|
|
532
|
+
inFlight.push(this.query(statement, signal ? { signal } : undefined).then((result) => {
|
|
533
|
+
outcomes[idx] = { statement, ok: true, result };
|
|
534
|
+
}, (error) => {
|
|
535
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
536
|
+
outcomes[idx] = { statement, ok: false, error: err };
|
|
537
|
+
// Replies are FIFO so failures normally arrive in statement
|
|
538
|
+
// order; keep the earliest defensively regardless.
|
|
539
|
+
if (firstFailureIndex === -1 || idx < firstFailureIndex) {
|
|
540
|
+
firstFailureIndex = idx;
|
|
541
|
+
firstFailureError = err;
|
|
542
|
+
}
|
|
543
|
+
}));
|
|
544
|
+
// Backpressure: only yield when the kernel buffer is full. These
|
|
545
|
+
// yields are also the windows in which an already-arrived error reply
|
|
546
|
+
// or abort can stop further dispatch (the checks at the loop head).
|
|
547
|
+
if (this.socket.writableNeedDrain)
|
|
548
|
+
await this.drained();
|
|
549
|
+
}
|
|
550
|
+
// Every dispatched statement has a handler attached, so this never
|
|
551
|
+
// rejects; it resolves once all in-flight replies have settled.
|
|
552
|
+
await Promise.all(inFlight);
|
|
553
|
+
if (!continueOnError) {
|
|
554
|
+
if (firstFailureIndex === -1 && dispatched < statements.length) {
|
|
555
|
+
// Dispatch stopped without any statement failing — e.g. a signal
|
|
556
|
+
// that was already aborted before the first write. Treat the first
|
|
557
|
+
// undispatched statement as the failure point.
|
|
558
|
+
firstFailureIndex = dispatched;
|
|
559
|
+
firstFailureError = signal?.aborted
|
|
560
|
+
? abortError(signal)
|
|
561
|
+
: (this.closeError ?? new PowDBError("client is closed", "closed"));
|
|
562
|
+
}
|
|
563
|
+
if (firstFailureIndex !== -1) {
|
|
564
|
+
if (transactional) {
|
|
565
|
+
// Every dispatched reply has settled, so the connection is quiet
|
|
566
|
+
// and the transaction is still open. Best-effort rollback — the
|
|
567
|
+
// throw below is what callers act on either way, and if the
|
|
568
|
+
// connection died the server aborts the transaction itself.
|
|
569
|
+
try {
|
|
570
|
+
await this.query("rollback");
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
/* connection may already be gone */
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const cause = firstFailureError;
|
|
577
|
+
const results = [];
|
|
578
|
+
for (let i = 0; i < firstFailureIndex; i++) {
|
|
579
|
+
const o = outcomes[i];
|
|
580
|
+
if (o !== undefined && o.ok)
|
|
581
|
+
results.push(o.result);
|
|
582
|
+
}
|
|
583
|
+
throw new PowDBScriptError(`script failed at statement ${firstFailureIndex + 1}/${statements.length}: ${cause.message}`, isPowDBError(cause) ? cause.code : "query_failed", {
|
|
584
|
+
statementIndex: firstFailureIndex,
|
|
585
|
+
statement: statements[firstFailureIndex],
|
|
586
|
+
results,
|
|
587
|
+
cause,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (transactional) {
|
|
591
|
+
// Commit only after every statement's reply settled successfully —
|
|
592
|
+
// this is the all-or-nothing guarantee. The commit reply is not
|
|
593
|
+
// part of the returned results. No signal here either: aborting a
|
|
594
|
+
// commit already on the wire buys nothing but ambiguity.
|
|
595
|
+
try {
|
|
596
|
+
await this.query("commit");
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
try {
|
|
600
|
+
await this.query("rollback");
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
/* connection may already be gone */
|
|
604
|
+
}
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return outcomes.map((o) => o.result);
|
|
609
|
+
}
|
|
610
|
+
// continueOnError: synthesize outcomes for statements that were never
|
|
611
|
+
// dispatched (abort or connection loss) so the array stays dense.
|
|
612
|
+
if (dispatched < statements.length) {
|
|
613
|
+
const reason = signal?.aborted
|
|
614
|
+
? abortError(signal)
|
|
615
|
+
: (this.closeError ?? new PowDBError("client is closed", "closed"));
|
|
616
|
+
for (let i = dispatched; i < statements.length; i++) {
|
|
617
|
+
outcomes[i] = { statement: statements[i], ok: false, error: reason };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return outcomes;
|
|
621
|
+
}
|
|
474
622
|
/**
|
|
475
623
|
* Poll a query on an interval and invoke `onRows` for every successful
|
|
476
624
|
* run. Returns an unsubscribe function. Does NOT deduplicate results —
|
|
@@ -636,6 +784,26 @@ export class Client extends EventEmitter {
|
|
|
636
784
|
});
|
|
637
785
|
});
|
|
638
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Resolve once the socket's write buffer has drained. Also resolves if
|
|
789
|
+
* the client closes while waiting, so a dead peer cannot hang a
|
|
790
|
+
* backpressured `execScript` dispatch loop forever.
|
|
791
|
+
*/
|
|
792
|
+
drained() {
|
|
793
|
+
return new Promise((resolve) => {
|
|
794
|
+
if (!this.socket.writableNeedDrain || this.closed) {
|
|
795
|
+
resolve();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const done = () => {
|
|
799
|
+
this.socket.removeListener("drain", done);
|
|
800
|
+
this.removeListener("close", done);
|
|
801
|
+
resolve();
|
|
802
|
+
};
|
|
803
|
+
this.socket.on("drain", done);
|
|
804
|
+
this.on("close", done);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
639
807
|
onData(chunk) {
|
|
640
808
|
// Append to the chunk queue — O(1) — and lazily concat only when
|
|
641
809
|
// we actually need contiguous bytes to decode.
|
|
@@ -739,8 +907,17 @@ export class Client extends EventEmitter {
|
|
|
739
907
|
if (this.closed && err === null)
|
|
740
908
|
return;
|
|
741
909
|
this.closed = true;
|
|
742
|
-
|
|
743
|
-
|
|
910
|
+
let error = err ?? new PowDBError("connection closed", "closed");
|
|
911
|
+
// A teardown before ConnectOk is a handshake failure: everything queued
|
|
912
|
+
// (the handshake entry and any eagerly pipelined queries) rejects with
|
|
913
|
+
// the handshake error. Auth/protocol errors already carry the precise
|
|
914
|
+
// cause; anything else becomes a retryable connect failure.
|
|
915
|
+
if (!this.handshakeComplete &&
|
|
916
|
+
!(isPowDBError(error) &&
|
|
917
|
+
(error.code === "auth_failed" || error.code === "protocol_error"))) {
|
|
918
|
+
error = new PowDBError("connection closed during handshake", "connect_failed", { cause: err ?? undefined });
|
|
919
|
+
}
|
|
920
|
+
this.closeError = error;
|
|
744
921
|
while (this.pending.length > 0) {
|
|
745
922
|
const entry = this.pending.shift();
|
|
746
923
|
if (!entry.settled) {
|
|
@@ -804,5 +981,6 @@ export { encode, tryDecode } from "./protocol.js";
|
|
|
804
981
|
export { MAX_PAYLOAD_SIZE, MAX_ROWS, MAX_COLUMNS, MAX_PARAMS, MAX_SYNC_UNITS, MAX_SYNC_PULL_UNITS, MAX_SYNC_PULL_BYTES, } from "./protocol.js";
|
|
805
982
|
export { escapeLiteral, escapeIdent, ident, powql, PowqlIdent, } from "./escape.js";
|
|
806
983
|
export { Pool } from "./pool.js";
|
|
807
|
-
export {
|
|
984
|
+
export { splitStatements } from "./script.js";
|
|
985
|
+
export { PowDBError, isPowDBError, PowDBScriptError, isPowDBScriptError, } from "./errors.js";
|
|
808
986
|
export { coerceValue, coerceRow, coerceRows, } from "./typed.js";
|
package/dist/pool.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* await pool.close();
|
|
21
21
|
*/
|
|
22
|
-
import { Client, type ClientOptions } from "./index.js";
|
|
22
|
+
import { Client, type ClientOptions, type QueryResult, type ScriptStatementOutcome } from "./index.js";
|
|
23
23
|
export interface PoolOptions extends ClientOptions {
|
|
24
24
|
/** Maximum concurrent clients. Default 10. */
|
|
25
25
|
max?: number;
|
|
@@ -110,6 +110,23 @@ export declare class Pool {
|
|
|
110
110
|
* to leak a client on an unhandled exception.
|
|
111
111
|
*/
|
|
112
112
|
withClient<T>(fn: (c: Client) => Promise<T>): Promise<T>;
|
|
113
|
+
/**
|
|
114
|
+
* Run a multi-statement PowQL script on ONE pooled connection, pipelined.
|
|
115
|
+
*
|
|
116
|
+
* Checks out a single client for the whole script (statement order —
|
|
117
|
+
* and `transactional: true` — therefore holds), delegates to
|
|
118
|
+
* {@link Client.execScript}, and releases the client on success or
|
|
119
|
+
* destroys it on error — same lifecycle as {@link withClient}.
|
|
120
|
+
*/
|
|
121
|
+
execScript(script: string, opts?: {
|
|
122
|
+
continueOnError?: false;
|
|
123
|
+
transactional?: boolean;
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
}): Promise<QueryResult[]>;
|
|
126
|
+
execScript(script: string, opts: {
|
|
127
|
+
continueOnError: true;
|
|
128
|
+
signal?: AbortSignal;
|
|
129
|
+
}): Promise<ScriptStatementOutcome[]>;
|
|
113
130
|
/**
|
|
114
131
|
* Close the pool. Rejects all pending waiters, awaits `close()` on every
|
|
115
132
|
* idle client, and marks the pool closed. Subsequent `acquire` calls
|
package/dist/pool.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* await pool.close();
|
|
21
21
|
*/
|
|
22
|
-
import { Client } from "./index.js";
|
|
22
|
+
import { Client, } from "./index.js";
|
|
23
23
|
import { isPowDBError } from "./errors.js";
|
|
24
24
|
/**
|
|
25
25
|
* Is this error worth retrying? Only the transient connect-phase errors
|
|
@@ -251,6 +251,23 @@ export class Pool {
|
|
|
251
251
|
throw err;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
+
async execScript(script, opts) {
|
|
255
|
+
if (opts?.continueOnError === true) {
|
|
256
|
+
return this.withClient((c) => c.execScript(script, {
|
|
257
|
+
continueOnError: true,
|
|
258
|
+
...(opts.transactional !== undefined
|
|
259
|
+
? { transactional: opts.transactional }
|
|
260
|
+
: {}),
|
|
261
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
return this.withClient((c) => c.execScript(script, {
|
|
265
|
+
...(opts?.transactional !== undefined
|
|
266
|
+
? { transactional: opts.transactional }
|
|
267
|
+
: {}),
|
|
268
|
+
...(opts?.signal ? { signal: opts.signal } : {}),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
254
271
|
/**
|
|
255
272
|
* Close the pool. Rejects all pending waiters, awaits `close()` on every
|
|
256
273
|
* idle client, and marks the pool closed. Subsequent `acquire` calls
|
package/dist/script.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statement-aware PowQL script splitting.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the server-side splitter (`powdb_query::lexer::split_statements`)
|
|
5
|
+
* exactly, so a script file behaves the same whether it is fed to the CLI's
|
|
6
|
+
* `--exec` / `.powql` path or to {@link Client.execScript} over the wire:
|
|
7
|
+
*
|
|
8
|
+
* - `;` splits statements only at the top level.
|
|
9
|
+
* - `;` inside a `"..."` string literal never splits. A backslash inside a
|
|
10
|
+
* string consumes the next character unconditionally (mirroring the
|
|
11
|
+
* PowQL lexer), so `\"` and `\;` stay inside the string.
|
|
12
|
+
* - `#` starts a comment that runs to end-of-line; a `;` inside a comment
|
|
13
|
+
* never splits.
|
|
14
|
+
* - Segments are trimmed; empty segments (leading/doubled/trailing `;`,
|
|
15
|
+
* blank lines) are dropped.
|
|
16
|
+
* - Never errors: an unterminated string simply makes the rest of the
|
|
17
|
+
* input the final segment.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Split a PowQL script into individual statements.
|
|
21
|
+
*
|
|
22
|
+
* String-literal- and `#`-comment-aware `;` splitting with the exact
|
|
23
|
+
* semantics of the CLI/server splitter (see module docs above).
|
|
24
|
+
*/
|
|
25
|
+
export declare function splitStatements(input: string): string[];
|
package/dist/script.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statement-aware PowQL script splitting.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the server-side splitter (`powdb_query::lexer::split_statements`)
|
|
5
|
+
* exactly, so a script file behaves the same whether it is fed to the CLI's
|
|
6
|
+
* `--exec` / `.powql` path or to {@link Client.execScript} over the wire:
|
|
7
|
+
*
|
|
8
|
+
* - `;` splits statements only at the top level.
|
|
9
|
+
* - `;` inside a `"..."` string literal never splits. A backslash inside a
|
|
10
|
+
* string consumes the next character unconditionally (mirroring the
|
|
11
|
+
* PowQL lexer), so `\"` and `\;` stay inside the string.
|
|
12
|
+
* - `#` starts a comment that runs to end-of-line; a `;` inside a comment
|
|
13
|
+
* never splits.
|
|
14
|
+
* - Segments are trimmed; empty segments (leading/doubled/trailing `;`,
|
|
15
|
+
* blank lines) are dropped.
|
|
16
|
+
* - Never errors: an unterminated string simply makes the rest of the
|
|
17
|
+
* input the final segment.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Split a PowQL script into individual statements.
|
|
21
|
+
*
|
|
22
|
+
* String-literal- and `#`-comment-aware `;` splitting with the exact
|
|
23
|
+
* semantics of the CLI/server splitter (see module docs above).
|
|
24
|
+
*/
|
|
25
|
+
export function splitStatements(input) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let start = 0;
|
|
28
|
+
let state = "normal";
|
|
29
|
+
for (let i = 0; i < input.length; i++) {
|
|
30
|
+
const c = input[i];
|
|
31
|
+
switch (state) {
|
|
32
|
+
case "normal":
|
|
33
|
+
if (c === ";") {
|
|
34
|
+
const seg = input.slice(start, i).trim();
|
|
35
|
+
if (seg.length > 0)
|
|
36
|
+
out.push(seg);
|
|
37
|
+
start = i + 1;
|
|
38
|
+
}
|
|
39
|
+
else if (c === '"') {
|
|
40
|
+
state = "in-string";
|
|
41
|
+
}
|
|
42
|
+
else if (c === "#") {
|
|
43
|
+
state = "in-comment";
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "in-string":
|
|
47
|
+
// Mirror the lexer: a backslash consumes the next char
|
|
48
|
+
// unconditionally, so `\"` and `\;` stay inside the string.
|
|
49
|
+
if (c === "\\") {
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
else if (c === '"') {
|
|
53
|
+
state = "normal";
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case "in-comment":
|
|
57
|
+
if (c === "\n")
|
|
58
|
+
state = "normal";
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const seg = input.slice(start).trim();
|
|
63
|
+
if (seg.length > 0)
|
|
64
|
+
out.push(seg);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/powdb-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "TypeScript client for PowDB (PowQL wire protocol)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
|
-
"
|
|
12
|
-
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/cjs/index.d.ts",
|
|
18
|
+
"default": "./dist/cjs/index.js"
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
},
|
|
15
22
|
"files": ["dist", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
@@ -47,7 +54,9 @@
|
|
|
47
54
|
"@types/node": { "optional": true }
|
|
48
55
|
},
|
|
49
56
|
"scripts": {
|
|
50
|
-
"build": "
|
|
57
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
58
|
+
"build:esm": "tsc -p tsconfig.json",
|
|
59
|
+
"build:cjs": "tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }) + '\\n')\"",
|
|
51
60
|
"test": "tsx test/run-with-server.ts test/client.test.ts",
|
|
52
61
|
"test:escape": "tsx test/escape.test.ts",
|
|
53
62
|
"test:protocol": "tsx test/protocol.test.ts",
|
|
@@ -56,6 +65,9 @@
|
|
|
56
65
|
"test:pool": "tsx test/run-with-server.ts test/pool.test.ts",
|
|
57
66
|
"test:typed": "tsx test/typed.test.ts",
|
|
58
67
|
"test:errors": "tsx test/errors.test.ts",
|
|
68
|
+
"test:script": "tsx test/script.test.ts",
|
|
69
|
+
"test:eager": "tsx test/run-with-server.ts test/eager.test.ts",
|
|
70
|
+
"test:exec-script": "tsx test/run-with-server.ts test/exec-script.test.ts",
|
|
59
71
|
"demo": "tsx demo/demo.ts",
|
|
60
72
|
"clean": "rm -rf dist",
|
|
61
73
|
"prepublishOnly": "npm run build"
|