@mason-data/cli 0.0.0-stage → 0.7.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/LICENSE +202 -0
- package/README.md +268 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +477 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1771 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +588 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +198 -0
- package/dist/src/cli/command.js +173 -0
- package/dist/src/cli/commands.js +1277 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +669 -0
- package/dist/src/cli/doctor.js +260 -0
- package/dist/src/cli/guides.js +158 -0
- package/dist/src/cli/init.js +366 -0
- package/dist/src/cli/machine.js +162 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +347 -0
- package/dist/src/cli/project.js +129 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +220 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +311 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +301 -0
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +564 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +268 -0
- package/dist/src/duck/executor.js +567 -0
- package/dist/src/duck/explain.js +238 -0
- package/dist/src/duck/functions.js +819 -0
- package/dist/src/duck/gaps.js +94 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +188 -0
- package/dist/src/duck/kept-route.js +299 -0
- package/dist/src/duck/kept.js +964 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +378 -0
- package/dist/src/duck/plan.js +259 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +5101 -0
- package/dist/src/duck/store.js +907 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2929 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +1001 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +237 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +76 -0
- package/dist/src/web/app.js +491 -0
- package/dist/src/web/index.html +44 -0
- package/package.json +22 -4
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE POSTGRES WIRE PROTOCOL, VERSION 3.0 — framing only. What a frontend (a BI tool, JDBC, psql) sends, decoded
|
|
3
|
+
* into plain objects; what a backend answers, built as bytes; and the backend side decoded too, for the tests and the
|
|
4
|
+
* catalog sidecar that read a real Postgres's replies. This module knows no
|
|
5
|
+
* SQL, no type and no value: a parameter is bytes, a column of a DataRow is bytes, and the encoding of a value is
|
|
6
|
+
* src/api/postgres/encode.ts's, held to Postgres byte for byte by its own harness.
|
|
7
|
+
*
|
|
8
|
+
* WHY HAND-WRITTEN, AND DEPENDENCY-FREE (node:buffer only), as the ClickHouse stack's token module was: the whole risk surface of
|
|
9
|
+
* a wire library is its decoder, and this one can be read in full. Every message is one type byte, an int32 length
|
|
10
|
+
* that COUNTS ITSELF but not the type byte, and a body; the first message of a connection has no type byte (length +
|
|
11
|
+
* a protocol code: an SSLRequest, a GSSENCRequest, a CancelRequest, or the version followed by the startup
|
|
12
|
+
* parameters), and after an SSLRequest or a GSSENCRequest the frontend sends another startup-phase message, so the
|
|
13
|
+
* decoder stays in that phase by itself (`expectStartup` puts it back there for a decoder reused over a TLS upgrade).
|
|
14
|
+
*
|
|
15
|
+
* THE RULES THIS EMBODIES:
|
|
16
|
+
* - A message type the server does not implement is DECODED, as `Unknown {code}`, never skipped and never thrown on:
|
|
17
|
+
* the server refuses it by name with an ErrorResponse, which is what a tool can act on (CLAUDE.md, "an error a
|
|
18
|
+
* developer meets says what to do about it"). Only a malformed frame — a length under 4, a length over
|
|
19
|
+
* MAX_MESSAGE_LENGTH, a string without its NUL, a count the body cannot hold — throws, naming what was wrong, and
|
|
20
|
+
* the server closes the connection: there is no way to resynchronise a corrupt stream.
|
|
21
|
+
* - `next()` returns null for "need more bytes" and never a partial message: a chunk may split a message anywhere,
|
|
22
|
+
* one byte at a time included (tests/postgres-api/protocol.test.ts pushes a byte at a time).
|
|
23
|
+
* - Strings are UTF-8 (`client_encoding` is UTF8 or refused, §4.6); a parameter's bytes are passed through untouched.
|
|
24
|
+
*/
|
|
25
|
+
/** Longer than this and a frame is refused: no query, parameter or row this API carries comes near it, and a
|
|
26
|
+
* length field read from a stray byte must not make the decoder wait for gigabytes. */
|
|
27
|
+
export const MAX_MESSAGE_LENGTH = 64 * 1024 * 1024;
|
|
28
|
+
/** Postgres's own cap on a startup packet (MAX_STARTUP_PACKET_LENGTH); a Password carries a token, far below it. Before
|
|
29
|
+
* authentication a client may send nothing larger (review of #480: a 64 MiB frame was accepted from anyone). */
|
|
30
|
+
export const MAX_STARTUP_LENGTH = 10_000;
|
|
31
|
+
/** The protocol codes a startup-phase message may carry in place of a version. */
|
|
32
|
+
export const SSL_REQUEST_CODE = 80877103;
|
|
33
|
+
export const GSSENC_REQUEST_CODE = 80877104;
|
|
34
|
+
export const CANCEL_REQUEST_CODE = 80877102;
|
|
35
|
+
/** A frame the decoder cannot make sense of; the server closes the connection on it. */
|
|
36
|
+
export class ProtocolError extends Error {
|
|
37
|
+
}
|
|
38
|
+
/** A cursor over one message body: every read checks its bounds, so a lying count throws instead of reading past the
|
|
39
|
+
* body into the next message (or into zeroes). */
|
|
40
|
+
class BodyReader {
|
|
41
|
+
#buf;
|
|
42
|
+
#off = 0;
|
|
43
|
+
#what;
|
|
44
|
+
constructor(buf, what) { this.#buf = buf; this.#what = what; }
|
|
45
|
+
#need(n) {
|
|
46
|
+
if (this.#off + n > this.#buf.length) {
|
|
47
|
+
throw new ProtocolError(`malformed ${this.#what} message: needs ${n} more byte(s) at offset ${this.#off} of a ${this.#buf.length}-byte body`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
byte() { this.#need(1); return this.#buf[this.#off++]; }
|
|
51
|
+
int16() { this.#need(2); const v = this.#buf.readInt16BE(this.#off); this.#off += 2; return v; }
|
|
52
|
+
int32() { this.#need(4); const v = this.#buf.readInt32BE(this.#off); this.#off += 4; return v; }
|
|
53
|
+
cstring() {
|
|
54
|
+
const end = this.#buf.indexOf(0, this.#off);
|
|
55
|
+
if (end === -1)
|
|
56
|
+
throw new ProtocolError(`malformed ${this.#what} message: a string at offset ${this.#off} has no terminating NUL`);
|
|
57
|
+
const s = this.#buf.toString("utf8", this.#off, end);
|
|
58
|
+
this.#off = end + 1;
|
|
59
|
+
return s;
|
|
60
|
+
}
|
|
61
|
+
bytes(n) {
|
|
62
|
+
if (n < 0)
|
|
63
|
+
throw new ProtocolError(`malformed ${this.#what} message: a negative length ${n} at offset ${this.#off}`);
|
|
64
|
+
this.#need(n);
|
|
65
|
+
const b = Buffer.from(this.#buf.subarray(this.#off, this.#off + n));
|
|
66
|
+
this.#off += n;
|
|
67
|
+
return b;
|
|
68
|
+
}
|
|
69
|
+
count(what) {
|
|
70
|
+
const n = this.int16();
|
|
71
|
+
if (n < 0)
|
|
72
|
+
throw new ProtocolError(`malformed ${this.#what} message: ${n} ${what}`);
|
|
73
|
+
return n;
|
|
74
|
+
}
|
|
75
|
+
rest() { return Buffer.from(this.#buf.subarray(this.#off)); }
|
|
76
|
+
get done() { return this.#off >= this.#buf.length; }
|
|
77
|
+
}
|
|
78
|
+
/** The next frame of `buf` at `off`: its length (type byte excluded, length field included) when the frame is whole,
|
|
79
|
+
* or null for "need more bytes". `headerLength` is 5 with a type byte, 4 in the startup phase. */
|
|
80
|
+
/** The length field of the frame at the start of `buf` (type byte excluded, the field itself included), checked
|
|
81
|
+
* against `limit`; `headerLength` is 5 with a type byte, 4 in the startup phase. */
|
|
82
|
+
function frameLength(buf, headerLength, limit) {
|
|
83
|
+
const len = buf.readInt32BE(headerLength - 4);
|
|
84
|
+
if (len < 4)
|
|
85
|
+
throw new ProtocolError(`malformed message: a length of ${len} (the length counts itself, so it is at least 4)`);
|
|
86
|
+
if (len > limit)
|
|
87
|
+
throw new ProtocolError(`message too long: ${len} bytes (the limit is ${limit})`);
|
|
88
|
+
return len;
|
|
89
|
+
}
|
|
90
|
+
/** Decodes what a frontend sends. Push every chunk the socket delivers; call `next()` until it returns null. */
|
|
91
|
+
export class FrontendDecoder {
|
|
92
|
+
/** the bytes of the frame being assembled, once its header has arrived */
|
|
93
|
+
#buf = Buffer.alloc(0);
|
|
94
|
+
/** chunks received since, concatenated ONCE, when the frame they complete is whole (never per chunk: a frame in
|
|
95
|
+
* 64 KiB pieces re-copied everything received so far on every piece — review of #480) */
|
|
96
|
+
#pending = [];
|
|
97
|
+
#pendingLength = 0;
|
|
98
|
+
#startup = true;
|
|
99
|
+
/** the largest frame accepted: MAX_STARTUP_LENGTH until the session says it is authenticated */
|
|
100
|
+
maxLength = MAX_STARTUP_LENGTH;
|
|
101
|
+
/** Back to the startup phase: the next message has no type byte. A fresh decoder starts there; after an SSLRequest
|
|
102
|
+
* or GSSENCRequest the decoder stays there by itself. */
|
|
103
|
+
expectStartup() { this.#startup = true; }
|
|
104
|
+
get inStartupPhase() { return this.#startup; }
|
|
105
|
+
push(chunk) {
|
|
106
|
+
this.#pending.push(Buffer.from(chunk));
|
|
107
|
+
this.#pendingLength += chunk.length;
|
|
108
|
+
}
|
|
109
|
+
/** Makes `#buf` hold at least `need` bytes when that many have arrived, moving the pending chunks in with one copy. */
|
|
110
|
+
#assemble(need) {
|
|
111
|
+
if (this.#buf.length >= need)
|
|
112
|
+
return true;
|
|
113
|
+
if (this.#buf.length + this.#pendingLength < need)
|
|
114
|
+
return false;
|
|
115
|
+
this.#buf = Buffer.concat([this.#buf, ...this.#pending]);
|
|
116
|
+
this.#pending = [];
|
|
117
|
+
this.#pendingLength = 0;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/** The next whole message, or null when more bytes are needed. Throws ProtocolError on a malformed frame. */
|
|
121
|
+
next() {
|
|
122
|
+
return this.#startup ? this.#nextStartup() : this.#nextTyped();
|
|
123
|
+
}
|
|
124
|
+
/** the next frame's length once its header is here (checked against the limit at once, before its body is held),
|
|
125
|
+
* or null while the header or the body is still arriving */
|
|
126
|
+
#whole(headerLength) {
|
|
127
|
+
if (!this.#assemble(headerLength))
|
|
128
|
+
return null;
|
|
129
|
+
const len = frameLength(this.#buf, headerLength, this.#startup ? Math.min(this.maxLength, MAX_STARTUP_LENGTH) : this.maxLength);
|
|
130
|
+
return this.#assemble(headerLength - 4 + len) ? len : null;
|
|
131
|
+
}
|
|
132
|
+
#take(headerLength, len) {
|
|
133
|
+
const body = this.#buf.subarray(headerLength, headerLength - 4 + len);
|
|
134
|
+
this.#buf = Buffer.from(this.#buf.subarray(headerLength - 4 + len));
|
|
135
|
+
return body;
|
|
136
|
+
}
|
|
137
|
+
#nextStartup() {
|
|
138
|
+
const len = this.#whole(4);
|
|
139
|
+
if (len === null)
|
|
140
|
+
return null;
|
|
141
|
+
const r = new BodyReader(this.#take(4, len), "startup");
|
|
142
|
+
const code = r.int32();
|
|
143
|
+
if (code === SSL_REQUEST_CODE)
|
|
144
|
+
return { type: "SSLRequest" };
|
|
145
|
+
if (code === GSSENC_REQUEST_CODE)
|
|
146
|
+
return { type: "GSSENCRequest" };
|
|
147
|
+
// Anything else ends the startup phase: a CancelRequest closes the connection, a Startup opens the session.
|
|
148
|
+
this.#startup = false;
|
|
149
|
+
if (code === CANCEL_REQUEST_CODE)
|
|
150
|
+
return { type: "CancelRequest", pid: r.int32(), secret: r.int32() };
|
|
151
|
+
const major = code >>> 16;
|
|
152
|
+
const minor = code & 0xffff;
|
|
153
|
+
const params = {};
|
|
154
|
+
// Key/value pairs, each a NUL-terminated string, closed by an empty key. A body that ends without the closing
|
|
155
|
+
// empty key (libpq always sends it) is tolerated as a complete list: nothing can be missing from it.
|
|
156
|
+
while (!r.done) {
|
|
157
|
+
const key = r.cstring();
|
|
158
|
+
if (key === "")
|
|
159
|
+
break;
|
|
160
|
+
params[key] = r.cstring();
|
|
161
|
+
}
|
|
162
|
+
return { type: "Startup", major, minor, params };
|
|
163
|
+
}
|
|
164
|
+
#nextTyped() {
|
|
165
|
+
const len = this.#whole(5);
|
|
166
|
+
if (len === null)
|
|
167
|
+
return null;
|
|
168
|
+
const code = String.fromCharCode(this.#buf[0]);
|
|
169
|
+
const r = new BodyReader(this.#take(5, len), `'${code}'`);
|
|
170
|
+
switch (code) {
|
|
171
|
+
case "p": return { type: "Password", password: r.cstring() };
|
|
172
|
+
case "Q": return { type: "Query", sql: r.cstring() };
|
|
173
|
+
case "P": {
|
|
174
|
+
const name = r.cstring();
|
|
175
|
+
const sql = r.cstring();
|
|
176
|
+
const n = r.count("parameter types");
|
|
177
|
+
const paramTypes = [];
|
|
178
|
+
for (let i = 0; i < n; i++)
|
|
179
|
+
paramTypes.push(r.int32());
|
|
180
|
+
return { type: "Parse", name, sql, paramTypes };
|
|
181
|
+
}
|
|
182
|
+
case "B": {
|
|
183
|
+
const portal = r.cstring();
|
|
184
|
+
const statement = r.cstring();
|
|
185
|
+
const nf = r.count("parameter format codes");
|
|
186
|
+
const paramFormats = [];
|
|
187
|
+
for (let i = 0; i < nf; i++)
|
|
188
|
+
paramFormats.push(r.int16());
|
|
189
|
+
const np = r.count("parameters");
|
|
190
|
+
const params = [];
|
|
191
|
+
for (let i = 0; i < np; i++) {
|
|
192
|
+
const size = r.int32();
|
|
193
|
+
params.push(size === -1 ? null : r.bytes(size));
|
|
194
|
+
}
|
|
195
|
+
const nr = r.count("result format codes");
|
|
196
|
+
const resultFormats = [];
|
|
197
|
+
for (let i = 0; i < nr; i++)
|
|
198
|
+
resultFormats.push(r.int16());
|
|
199
|
+
return { type: "Bind", portal, statement, paramFormats, params, resultFormats };
|
|
200
|
+
}
|
|
201
|
+
case "D":
|
|
202
|
+
case "C": {
|
|
203
|
+
const kind = String.fromCharCode(r.byte());
|
|
204
|
+
if (kind !== "S" && kind !== "P") {
|
|
205
|
+
throw new ProtocolError(`malformed '${code}' message: kind ${JSON.stringify(kind)} is neither 'S' (statement) nor 'P' (portal)`);
|
|
206
|
+
}
|
|
207
|
+
const name = r.cstring();
|
|
208
|
+
return code === "D" ? { type: "Describe", kind, name } : { type: "Close", kind, name };
|
|
209
|
+
}
|
|
210
|
+
case "E": return { type: "Execute", portal: r.cstring(), maxRows: r.int32() };
|
|
211
|
+
case "S": return { type: "Sync" };
|
|
212
|
+
case "H": return { type: "Flush" };
|
|
213
|
+
case "X": return { type: "Terminate" };
|
|
214
|
+
default: return { type: "Unknown", code };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
219
|
+
// Backend message builders. Each returns one whole frame.
|
|
220
|
+
export const cstr = (s) => Buffer.concat([Buffer.from(s, "utf8"), Buffer.from([0])]);
|
|
221
|
+
export const int16 = (n) => { const b = Buffer.alloc(2); b.writeInt16BE(n); return b; };
|
|
222
|
+
export const int32 = (n) => { const b = Buffer.alloc(4); b.writeInt32BE(n); return b; };
|
|
223
|
+
/** A typed frame: the type byte, the length (itself included), the body. */
|
|
224
|
+
export function frame(type, ...parts) {
|
|
225
|
+
const body = Buffer.concat(parts);
|
|
226
|
+
const head = Buffer.alloc(5);
|
|
227
|
+
head.write(type, 0, "latin1");
|
|
228
|
+
head.writeInt32BE(body.length + 4, 1);
|
|
229
|
+
return Buffer.concat([head, body]);
|
|
230
|
+
}
|
|
231
|
+
/** The one-byte answer to an SSLRequest: 'S' (upgrade) or 'N' (continue in the clear). Not a frame. */
|
|
232
|
+
export const sslResponse = (accept) => Buffer.from(accept ? "S" : "N", "latin1");
|
|
233
|
+
export const authenticationCleartextPassword = () => frame("R", int32(3));
|
|
234
|
+
export const authenticationOk = () => frame("R", int32(0));
|
|
235
|
+
export const parameterStatus = (name, value) => frame("S", cstr(name), cstr(value));
|
|
236
|
+
export const backendKeyData = (pid, secret) => frame("K", int32(pid), int32(secret));
|
|
237
|
+
export const readyForQuery = (status) => frame("Z", Buffer.from(status, "latin1"));
|
|
238
|
+
export function rowDescription(fields) {
|
|
239
|
+
const parts = [int16(fields.length)];
|
|
240
|
+
for (const f of fields) {
|
|
241
|
+
parts.push(cstr(f.name), int32(f.tableOid ?? 0), int16(f.columnAttr ?? 0), int32(f.typeOid), int16(f.typeSize), int32(f.typeModifier), int16(f.format));
|
|
242
|
+
}
|
|
243
|
+
return frame("T", ...parts);
|
|
244
|
+
}
|
|
245
|
+
/** One row: each column's encoded bytes, or null for SQL NULL (sent as a length of -1). */
|
|
246
|
+
export function dataRow(values) {
|
|
247
|
+
const parts = [int16(values.length)];
|
|
248
|
+
for (const v of values) {
|
|
249
|
+
if (v === null)
|
|
250
|
+
parts.push(int32(-1));
|
|
251
|
+
else
|
|
252
|
+
parts.push(int32(v.length), v);
|
|
253
|
+
}
|
|
254
|
+
return frame("D", ...parts);
|
|
255
|
+
}
|
|
256
|
+
export const commandComplete = (tag) => frame("C", cstr(tag));
|
|
257
|
+
export const emptyQueryResponse = () => frame("I");
|
|
258
|
+
export const parseComplete = () => frame("1");
|
|
259
|
+
export const bindComplete = () => frame("2");
|
|
260
|
+
export const closeComplete = () => frame("3");
|
|
261
|
+
export const noData = () => frame("n");
|
|
262
|
+
export const portalSuspended = () => frame("s");
|
|
263
|
+
export function parameterDescription(oids) {
|
|
264
|
+
return frame("t", int16(oids.length), ...oids.map(int32));
|
|
265
|
+
}
|
|
266
|
+
/** The standard fields, each a type byte and a NUL-terminated string, closed by a zero byte. `S` and `V` both carry the
|
|
267
|
+
* severity: `V` is the never-localised one, which clients since 9.6 read. */
|
|
268
|
+
function noticeFields(f) {
|
|
269
|
+
const parts = [
|
|
270
|
+
Buffer.from("S"), cstr(f.severity), Buffer.from("V"), cstr(f.severity), Buffer.from("C"), cstr(f.code),
|
|
271
|
+
Buffer.from("M"), cstr(f.message),
|
|
272
|
+
];
|
|
273
|
+
if (f.detail !== undefined)
|
|
274
|
+
parts.push(Buffer.from("D"), cstr(f.detail));
|
|
275
|
+
if (f.hint !== undefined)
|
|
276
|
+
parts.push(Buffer.from("H"), cstr(f.hint));
|
|
277
|
+
if (f.position !== undefined)
|
|
278
|
+
parts.push(Buffer.from("P"), cstr(String(f.position)));
|
|
279
|
+
parts.push(Buffer.from([0]));
|
|
280
|
+
return parts;
|
|
281
|
+
}
|
|
282
|
+
export const errorResponse = (fields) => frame("E", ...noticeFields(fields));
|
|
283
|
+
export const noticeResponse = (fields) => frame("N", ...noticeFields(fields));
|
|
284
|
+
function decodeNoticeFields(r) {
|
|
285
|
+
const fields = {};
|
|
286
|
+
for (;;) {
|
|
287
|
+
const code = r.byte();
|
|
288
|
+
if (code === 0)
|
|
289
|
+
break;
|
|
290
|
+
fields[String.fromCharCode(code)] = r.cstring();
|
|
291
|
+
}
|
|
292
|
+
return fields;
|
|
293
|
+
}
|
|
294
|
+
function decodeBackendBody(code, body) {
|
|
295
|
+
const r = new BodyReader(body, `backend '${code}'`);
|
|
296
|
+
switch (code) {
|
|
297
|
+
case "R": {
|
|
298
|
+
const auth = r.int32();
|
|
299
|
+
if (auth === 0)
|
|
300
|
+
return { type: "AuthenticationOk" };
|
|
301
|
+
if (auth === 3)
|
|
302
|
+
return { type: "AuthenticationCleartextPassword" };
|
|
303
|
+
return { type: "Authentication", code: auth, data: r.rest() };
|
|
304
|
+
}
|
|
305
|
+
case "S": return { type: "ParameterStatus", name: r.cstring(), value: r.cstring() };
|
|
306
|
+
case "K": return { type: "BackendKeyData", pid: r.int32(), secret: r.int32() };
|
|
307
|
+
case "Z": return { type: "ReadyForQuery", status: String.fromCharCode(r.byte()) };
|
|
308
|
+
case "T": {
|
|
309
|
+
const n = r.count("fields");
|
|
310
|
+
const fields = [];
|
|
311
|
+
for (let i = 0; i < n; i++) {
|
|
312
|
+
const name = r.cstring();
|
|
313
|
+
const tableOid = r.int32();
|
|
314
|
+
const columnAttr = r.int16();
|
|
315
|
+
const typeOid = r.int32();
|
|
316
|
+
const typeSize = r.int16();
|
|
317
|
+
const typeModifier = r.int32();
|
|
318
|
+
const format = r.int16();
|
|
319
|
+
if (format !== 0 && format !== 1)
|
|
320
|
+
throw new ProtocolError(`malformed RowDescription: field ${name} has format code ${format}`);
|
|
321
|
+
fields.push({ name, tableOid, columnAttr, typeOid, typeSize, typeModifier, format });
|
|
322
|
+
}
|
|
323
|
+
return { type: "RowDescription", fields };
|
|
324
|
+
}
|
|
325
|
+
case "D": {
|
|
326
|
+
const n = r.count("columns");
|
|
327
|
+
const values = [];
|
|
328
|
+
for (let i = 0; i < n; i++) {
|
|
329
|
+
const size = r.int32();
|
|
330
|
+
values.push(size === -1 ? null : r.bytes(size));
|
|
331
|
+
}
|
|
332
|
+
return { type: "DataRow", values };
|
|
333
|
+
}
|
|
334
|
+
case "C": return { type: "CommandComplete", tag: r.cstring() };
|
|
335
|
+
case "I": return { type: "EmptyQueryResponse" };
|
|
336
|
+
case "1": return { type: "ParseComplete" };
|
|
337
|
+
case "2": return { type: "BindComplete" };
|
|
338
|
+
case "3": return { type: "CloseComplete" };
|
|
339
|
+
case "n": return { type: "NoData" };
|
|
340
|
+
case "s": return { type: "PortalSuspended" };
|
|
341
|
+
case "t": {
|
|
342
|
+
const n = r.count("parameters");
|
|
343
|
+
const oids = [];
|
|
344
|
+
for (let i = 0; i < n; i++)
|
|
345
|
+
oids.push(r.int32());
|
|
346
|
+
return { type: "ParameterDescription", oids };
|
|
347
|
+
}
|
|
348
|
+
case "E": return { type: "ErrorResponse", fields: decodeNoticeFields(r) };
|
|
349
|
+
case "N": return { type: "NoticeResponse", fields: decodeNoticeFields(r) };
|
|
350
|
+
default: return { type: "Unknown", code, body: r.rest() };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Decodes what a backend sends, chunk by chunk, as FrontendDecoder does for the other direction. */
|
|
354
|
+
export class BackendDecoder {
|
|
355
|
+
#buf = Buffer.alloc(0);
|
|
356
|
+
push(chunk) {
|
|
357
|
+
this.#buf = this.#buf.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.#buf, chunk]);
|
|
358
|
+
}
|
|
359
|
+
next() {
|
|
360
|
+
if (this.#buf.length < 5)
|
|
361
|
+
return null;
|
|
362
|
+
const len = frameLength(this.#buf, 5, MAX_MESSAGE_LENGTH);
|
|
363
|
+
if (this.#buf.length < 1 + len)
|
|
364
|
+
return null;
|
|
365
|
+
const code = String.fromCharCode(this.#buf[0]);
|
|
366
|
+
const body = this.#buf.subarray(5, 1 + len);
|
|
367
|
+
this.#buf = Buffer.from(this.#buf.subarray(1 + len));
|
|
368
|
+
return decodeBackendBody(code, body);
|
|
369
|
+
}
|
|
370
|
+
/** bytes pushed and not yet consumed by a whole message */
|
|
371
|
+
get pending() { return this.#buf.length; }
|
|
372
|
+
}
|
|
373
|
+
/** Every message of a buffer that holds whole messages only (a `execProtocolRaw` reply, a builder's output). A
|
|
374
|
+
* trailing partial message throws: the caller promised a complete reply. */
|
|
375
|
+
/** The whole frames of a backend reply, each with its type byte, undecoded: what the server relays or inspects. */
|
|
376
|
+
export function splitFrames(reply) {
|
|
377
|
+
const buf = Buffer.from(reply.buffer, reply.byteOffset, reply.byteLength);
|
|
378
|
+
const out = [];
|
|
379
|
+
let off = 0;
|
|
380
|
+
while (off < buf.length) {
|
|
381
|
+
if (off + 5 > buf.length)
|
|
382
|
+
throw new ProtocolError(`the reply ends inside a frame header at byte ${off}`);
|
|
383
|
+
const len = buf.readInt32BE(off + 1);
|
|
384
|
+
if (len < 4 || off + 1 + len > buf.length)
|
|
385
|
+
throw new ProtocolError(`the reply has a frame of length ${len} at byte ${off}`);
|
|
386
|
+
out.push({ type: String.fromCharCode(buf[off]), frame: Buffer.from(buf.subarray(off, off + 1 + len)) });
|
|
387
|
+
off += 1 + len;
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
/** The fields of an ErrorResponse or NoticeResponse frame (type byte included), by their one-letter code. */
|
|
392
|
+
export function decodeNoticeFrame(frame) {
|
|
393
|
+
return decodeNoticeFields(new BodyReader(frame.subarray(5), `backend '${String.fromCharCode(frame[0])}'`));
|
|
394
|
+
}
|
|
395
|
+
export function decodeBackend(buffer) {
|
|
396
|
+
const d = new BackendDecoder();
|
|
397
|
+
d.push(Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength));
|
|
398
|
+
const out = [];
|
|
399
|
+
for (let m = d.next(); m !== null; m = d.next())
|
|
400
|
+
out.push(m);
|
|
401
|
+
if (d.pending !== 0)
|
|
402
|
+
throw new ProtocolError(`decodeBackend: ${d.pending} trailing byte(s) do not make a whole message`);
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE POSTGRES LISTENER SHOWS A TOOL: the copied tables, each column with the Postgres type the source spells it
|
|
3
|
+
* as, each foreign key between two copied tables as a foreign key, the source's views. One shape, built by the sync
|
|
4
|
+
* from the copy (src/duck/sync.ts `schema()`) and read by the catalog sidecar (src/api/postgres/catalog.ts creates
|
|
5
|
+
* these tables, empty, in a Postgres inside the process, so a tool's introspection is Postgres's own) and the wire
|
|
6
|
+
* encoders (src/api/postgres/encode.ts: the type decides the OID and the format).
|
|
7
|
+
*
|
|
8
|
+
* Pure and dependency-free: the tests build one by hand.
|
|
9
|
+
*/
|
|
10
|
+
/** The Postgres types the encoders know by name, by their `pg_type.typname` (encode.ts); a copied column of another
|
|
11
|
+
* type carries the source's own spelling and OID (`spelled`, `typeOid`). */
|
|
12
|
+
export const PG_API_TYPES = ["int2", "int4", "int8", "float4", "float8", "numeric", "text", "bool", "uuid", "date", "timestamptz", "jsonb"];
|
|
13
|
+
export const isPgApiType = (t) => typeof t === "string" && PG_API_TYPES.includes(t);
|
|
14
|
+
/** How each type is SPELLED in `format_type` / a CREATE TABLE, the catalog's own words. */
|
|
15
|
+
export const PG_API_TYPE_SPELLING = {
|
|
16
|
+
int2: "smallint", int4: "integer", int8: "bigint", float4: "real", float8: "double precision", numeric: "numeric", text: "text",
|
|
17
|
+
bool: "boolean", uuid: "uuid", date: "date", timestamptz: "timestamp with time zone", jsonb: "jsonb",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A type as format_type spells it (`mood`, `other.level`, `"My Mood"`, `"a.b"."c"`, an array's `[]` left off): its
|
|
21
|
+
* schema as written (null when it is not qualified) and its name as pg_type.typname holds it (unquoted).
|
|
22
|
+
*/
|
|
23
|
+
export function typeNameParts(spelled) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
let at = 0;
|
|
26
|
+
while (at < spelled.length) {
|
|
27
|
+
if (spelled[at] === '"') {
|
|
28
|
+
let end = at + 1, out = "";
|
|
29
|
+
for (; end < spelled.length; end++) {
|
|
30
|
+
if (spelled[end] === '"') {
|
|
31
|
+
if (spelled[end + 1] === '"') {
|
|
32
|
+
out += '"';
|
|
33
|
+
end++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
out += spelled[end];
|
|
39
|
+
}
|
|
40
|
+
parts.push(`"${out}`);
|
|
41
|
+
at = end + 1;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const dot = spelled.indexOf(".", at);
|
|
45
|
+
parts.push(spelled.slice(at, dot < 0 ? undefined : dot));
|
|
46
|
+
at = dot < 0 ? spelled.length : dot;
|
|
47
|
+
}
|
|
48
|
+
if (spelled[at] === ".")
|
|
49
|
+
at++;
|
|
50
|
+
}
|
|
51
|
+
const bare = (p) => p.startsWith('"') ? p.slice(1) : p;
|
|
52
|
+
const quoted = (p) => p.startsWith('"') ? `"${p.slice(1).replaceAll('"', '""')}"` : p;
|
|
53
|
+
return parts.length > 1 ? { schema: quoted(parts[parts.length - 2]), name: bare(parts[parts.length - 1]) } : { schema: null, name: bare(parts[0] ?? "") };
|
|
54
|
+
}
|