@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,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DEMO DATABASE (`mason init demo`): a Postgres Mason hosts itself, so a first run needs no Docker and no Postgres
|
|
3
|
+
* install. PGlite (Postgres 18 compiled to WebAssembly, the catalog's own engine, src/api/postgres/catalog.ts) persisted
|
|
4
|
+
* in a folder, seeded at the folder's first open with a small synthetic web shop (src/demo/seed.ts), and served over the
|
|
5
|
+
* Postgres wire protocol on a loopback port, so Mason reads it exactly as it reads an application's database: its own
|
|
6
|
+
* client (src/cdc/pg-wire.ts: the checks, the catalog reads) and DuckDB's `postgres` extension through libpq (the copy's
|
|
7
|
+
* chunks, the poll). `change()` writes a round of changes through PGlite's own API, so the copy visibly keeps current.
|
|
8
|
+
*
|
|
9
|
+
* WHAT IT IS NOT: a server for anything but a demo. It listens on 127.0.0.1 ONLY and accepts any user and any password
|
|
10
|
+
* (no authentication round trip: AuthenticationOk at once), because nothing but this machine can reach it and it holds
|
|
11
|
+
* nothing but the seed. It has no walsender (PGlite has none: `pg_replication_slots` is empty), so it is polled, never
|
|
12
|
+
* streamed; a StartupMessage asking for replication is refused (0A000). COPY FROM STDIN is not supported: a batch ends
|
|
13
|
+
* at a Query, a Sync or a Flush, never at a CopyDone (nothing Mason sends writes through the wire).
|
|
14
|
+
*
|
|
15
|
+
* THE STARTUP PHASE IS OURS, THE QUERY PHASE IS POSTGRES'S. PGlite answers protocol bytes (`execProtocolRaw`) but not
|
|
16
|
+
* the startup phase (SSLRequest, StartupMessage, authentication), so this module does it: an SSLRequest or a
|
|
17
|
+
* GSSENCRequest is told `N` (no TLS on loopback), a CancelRequest closes (nothing to cancel), the StartupMessage is
|
|
18
|
+
* checked (the database must be `demo`, else 3D000; protocol 3.x, a newer minor negotiated down to 3.0) and answered
|
|
19
|
+
* AuthenticationOk, ParameterStatus, BackendKeyData (a pid of this module's numbering) and ReadyForQuery `I`. After it,
|
|
20
|
+
* every frame a client sends is relayed to PGlite AS ITS OWN BYTES, by a framer of this module's (a type byte and a
|
|
21
|
+
* length): the query phase is never decoded and re-encoded, so nothing a client sends (a COPY's binary, a Bind's
|
|
22
|
+
* parameters) can be changed on the way; `FrontendDecoder` (src/api/postgres/protocol.ts) decodes only the startup
|
|
23
|
+
* frames, handed to it one whole frame at a time.
|
|
24
|
+
*
|
|
25
|
+
* ONE SESSION FOR EVERY CLIENT, SO ONE LOCK. PGlite is a single backend: two connections' messages interleaved inside
|
|
26
|
+
* a transaction would run one client's statement in the other's transaction. So a connection takes THE lock when it has
|
|
27
|
+
* a batch to send — the frames up to and including a Query `Q`, a Sync `S` or a Flush `H` — and gives it back only once
|
|
28
|
+
* the reply's last ReadyForQuery says `I` (idle, no transaction open); while it says `T` or `E` (or a Flush left the
|
|
29
|
+
* extended protocol mid-cycle, no ReadyForQuery at all) the connection keeps it, and its next batch runs under it.
|
|
30
|
+
* Terminate `X` ends the connection; one that ends holding the lock (mid-transaction, or gone) is cleaned up through
|
|
31
|
+
* PGlite first — a Sync when it was mid-cycle, a ROLLBACK when a transaction is open — so the next holder starts idle.
|
|
32
|
+
* `change()` and the startup's settings take the same lock. Waiters are served first come, first served.
|
|
33
|
+
*
|
|
34
|
+
* WHAT THE CLIENTS SEND (traced: `trace`): Mason's own client, simple Queries only. libpq under the extension: SSLRequest
|
|
35
|
+
* (sslmode prefer) then a StartupMessage of user, database and application_name alone; then per use of a pooled
|
|
36
|
+
* connection `SELECT 1`, the statements, `DISCARD ALL`; every read inside `BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE
|
|
37
|
+
* READ READ ONLY` … `COMMIT`, each statement an unnamed Parse + Sync, a Describe + Sync, then the statement again as a
|
|
38
|
+
* simple Query `COPY (…) TO STDOUT (FORMAT "binary")`, which PGlite answers (CopyOutResponse, CopyData, CopyDone). An
|
|
39
|
+
* unnamed statement is shared by every client of the one session, so a Parse and its Describe in two batches are safe
|
|
40
|
+
* only inside a transaction, which holds the lock between them — where every one traced was.
|
|
41
|
+
*
|
|
42
|
+
* THE DEADLOCK THIS ALLOWS, reasoned about: a client holding the lock inside a transaction whose own progress waits for
|
|
43
|
+
* ANOTHER client of this database that is queued behind the lock — two DuckDB connections where the first's COMMIT
|
|
44
|
+
* waited on the second's read, or a process whose thread pool is all blocked in libpq reads queued here while the
|
|
45
|
+
* holder's next statement needs one of those threads. Mason's own readers do not do that (each copy chunk and each poll
|
|
46
|
+
* is one DuckDB transaction on one connection, its source transaction ended at its commit, which waits on no other
|
|
47
|
+
* reader), and the test of three parallel readers holds it; but the serialisation makes Mason's parallel readers take
|
|
48
|
+
* turns, so a copy here reads one chunk at a time whatever `MASON_COPY_PARALLEL` says. A wait past `LONG_WAIT_MS` is
|
|
49
|
+
* said in the log, naming who holds the lock and in what state, so a hang is never silent.
|
|
50
|
+
*
|
|
51
|
+
* THE SETTINGS A CLIENT ASKS FOR ARE THE ONE SESSION'S. The StartupMessage's parameters other than user / database are
|
|
52
|
+
* session settings (DateStyle, TimeZone, extra_float_digits, IntervalStyle, client_encoding, application_name,
|
|
53
|
+
* search_path, and every `-c name=value` of libpq's `options`), applied with `set_config` under the lock before the
|
|
54
|
+
* ReadyForQuery, and applied again (one statement) whenever the lock passes to a client from another, so each client
|
|
55
|
+
* meets the settings it asked for at every turn of its own. A DISCARD ALL or a RESET (DuckDB's `postgres` extension sends
|
|
56
|
+
* DISCARD ALL each time its pool takes a connection back) returns the session to PGlite's defaults where Postgres would
|
|
57
|
+
* return it to the startup's, so after one (Postgres's CommandComplete tag says it ran) the client's settings are
|
|
58
|
+
* applied again at its next turn. A client's own SET lasts until another client's turn. Every Mason client asks for
|
|
59
|
+
* nothing that differs between them where it matters: pg-wire.ts asks for ISO dates, UTC and extra_float_digits 3 (and
|
|
60
|
+
* reads JSON text), the extension asks for nothing but its application_name and reads every value in binary.
|
|
61
|
+
*
|
|
62
|
+
* PGLITE'S EXTENDED PROTOCOL DIFFERS FROM POSTGRES'S IN ONE WAY, which the relay corrects: after an error it sends a
|
|
63
|
+
* ReadyForQuery AT ONCE, and another at the Sync (measured: a failed Parse/Bind/Execute/Sync answers `E Z Z`; a failed
|
|
64
|
+
* Parse/Flush `E Z`). Postgres sends one, at the Sync. So a batch that ends in a Sync relays only its last
|
|
65
|
+
* ReadyForQuery, and one that ends in a Flush relays none. What comes after the error up to the Sync PGlite ignores
|
|
66
|
+
* itself, as Postgres's `ignore_till_sync` does (measured: a Bind/Execute/Sync after a failed Parse/Flush answers `Z`
|
|
67
|
+
* alone) — while the client that erred holds the lock, a Flush leaving it mid-cycle, so no other client's message is
|
|
68
|
+
* among the ignored.
|
|
69
|
+
*
|
|
70
|
+
* PGLITE RUNS ON THIS THREAD, not a worker as the catalog's does: the demo's statements are small (its largest is a copy
|
|
71
|
+
* chunk of the 15,000-row table, tens of milliseconds), and `change()` writes through the same instance; a statement
|
|
72
|
+
* here holds the thread for that long, which a demo can afford and a real source never asks of Mason.
|
|
73
|
+
*/
|
|
74
|
+
import * as fs from "node:fs";
|
|
75
|
+
import * as net from "node:net";
|
|
76
|
+
import * as path from "node:path";
|
|
77
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
78
|
+
import { FrontendDecoder, MAX_MESSAGE_LENGTH, MAX_STARTUP_LENGTH, authenticationOk, backendKeyData, cstr, errorResponse, frame, int32, parameterStatus, readyForQuery, splitFrames, sslResponse, } from "../api/postgres/protocol.js";
|
|
79
|
+
import { CHANGE_STATE_SQL, DEMO_TABLES, changeSql, seedStatements } from "./seed.js";
|
|
80
|
+
import { DEMO_DATABASE, DEMO_USER, demoUrl } from "./seed.js";
|
|
81
|
+
/** the demo's URL and names are the seed's module's (no PGlite there: `mason init demo` and the run's port check read
|
|
82
|
+
* them without loading this one) */
|
|
83
|
+
export { DEMO_DATABASE, DEMO_USER, demoUrl };
|
|
84
|
+
/** a wait for the lock longer than this is said in the log, once per wait */
|
|
85
|
+
const LONG_WAIT_MS = 10_000;
|
|
86
|
+
/** the parameters a ParameterStatus reports at startup that do not change per client: read from PGlite once, at open */
|
|
87
|
+
const SERVER_PARAMETERS = ["server_version", "server_encoding", "integer_datetimes", "standard_conforming_strings",
|
|
88
|
+
"is_superuser", "session_authorization", "in_hot_standby"];
|
|
89
|
+
/** the ones a client's settings move: read again after each client's startup */
|
|
90
|
+
const SESSION_PARAMETERS = ["client_encoding", "DateStyle", "TimeZone", "IntervalStyle", "application_name", "search_path"];
|
|
91
|
+
/** startup parameters that are not settings */
|
|
92
|
+
const NOT_SETTINGS = new Set(["user", "database", "options", "replication"]);
|
|
93
|
+
/** THE ONE PGLITE SESSION'S LOCK: first come, first served; a holder keeps it across batches until it releases. */
|
|
94
|
+
class SessionLock {
|
|
95
|
+
#holder = null;
|
|
96
|
+
#waiting = [];
|
|
97
|
+
#say;
|
|
98
|
+
constructor(say) { this.#say = say; }
|
|
99
|
+
get holder() { return this.#holder; }
|
|
100
|
+
async acquire(owner) {
|
|
101
|
+
if (this.#holder === owner)
|
|
102
|
+
return;
|
|
103
|
+
if (this.#holder === null) {
|
|
104
|
+
this.#holder = owner;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const since = Date.now();
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
const h = this.#holder;
|
|
110
|
+
this.#say(`demo database: ${nameOf(owner)} has waited ${Math.round((Date.now() - since) / 1000)} s for ${h === null ? "nobody" : nameOf(h)}` +
|
|
111
|
+
`${h instanceof Connection ? `, which holds a transaction open (${h.stateText()})` : ""}: the demo database runs one statement at a time`);
|
|
112
|
+
}, LONG_WAIT_MS);
|
|
113
|
+
timer.unref();
|
|
114
|
+
await new Promise((grant) => this.#waiting.push({ owner, grant }));
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
}
|
|
117
|
+
release(owner) {
|
|
118
|
+
if (this.#holder !== owner)
|
|
119
|
+
return;
|
|
120
|
+
const next = this.#waiting.shift();
|
|
121
|
+
this.#holder = next?.owner ?? null;
|
|
122
|
+
next?.grant();
|
|
123
|
+
}
|
|
124
|
+
/** runs `fn` holding the lock, and releases it whatever `fn` does */
|
|
125
|
+
async exclusive(owner, fn) {
|
|
126
|
+
await this.acquire(owner);
|
|
127
|
+
try {
|
|
128
|
+
return await fn();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
this.release(owner);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const nameOf = (o) => (typeof o === "string" ? o : `connection ${o.pid}${o.application ? ` (${o.application})` : ""}`);
|
|
136
|
+
const errorText = (e) => (e instanceof Error ? e.message : String(e));
|
|
137
|
+
/** a PGlite error's SQLSTATE, when it carries one */
|
|
138
|
+
const sqlstateOf = (e) => {
|
|
139
|
+
const code = e?.code;
|
|
140
|
+
return typeof code === "string" && /^[0-9A-Z]{5}$/.test(code) ? code : "XX000";
|
|
141
|
+
};
|
|
142
|
+
/** libpq's `options` string: its `-c name=value` / `--name=value` settings (a backslash escapes a space, as libpq's) */
|
|
143
|
+
export function optionSettings(options) {
|
|
144
|
+
const words = [];
|
|
145
|
+
let word = "";
|
|
146
|
+
let inWord = false;
|
|
147
|
+
for (let i = 0; i < options.length; i++) {
|
|
148
|
+
const ch = options[i];
|
|
149
|
+
if (ch === "\\" && i + 1 < options.length) {
|
|
150
|
+
word += options[++i];
|
|
151
|
+
inWord = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (/\s/.test(ch)) {
|
|
155
|
+
if (inWord)
|
|
156
|
+
words.push(word);
|
|
157
|
+
word = "";
|
|
158
|
+
inWord = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
word += ch;
|
|
162
|
+
inWord = true;
|
|
163
|
+
}
|
|
164
|
+
if (inWord)
|
|
165
|
+
words.push(word);
|
|
166
|
+
const out = [];
|
|
167
|
+
for (let i = 0; i < words.length; i++) {
|
|
168
|
+
const w = words[i];
|
|
169
|
+
let setting;
|
|
170
|
+
if (w === "-c")
|
|
171
|
+
setting = words[++i];
|
|
172
|
+
else if (w.startsWith("-c"))
|
|
173
|
+
setting = w.slice(2);
|
|
174
|
+
else if (w.startsWith("--"))
|
|
175
|
+
setting = w.slice(2);
|
|
176
|
+
const eq = setting?.indexOf("=") ?? -1;
|
|
177
|
+
if (setting !== undefined && eq > 0)
|
|
178
|
+
out.push([setting.slice(0, eq).replace(/-/g, "_"), setting.slice(eq + 1)]);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
/** One client's connection: the startup phase, then its frames relayed in batches under the lock. */
|
|
183
|
+
class Connection {
|
|
184
|
+
pid;
|
|
185
|
+
application = "";
|
|
186
|
+
/** the settings its StartupMessage asked for: the one session's whenever it is this connection's turn */
|
|
187
|
+
settings = [];
|
|
188
|
+
#socket;
|
|
189
|
+
#source;
|
|
190
|
+
#pending = [];
|
|
191
|
+
#pendingLength = 0;
|
|
192
|
+
#startup = true;
|
|
193
|
+
/** the frames of the batch being gathered */
|
|
194
|
+
#batch = [];
|
|
195
|
+
/** every batch, the startup and the end run one after another */
|
|
196
|
+
#chain = Promise.resolve();
|
|
197
|
+
/** the last ReadyForQuery relayed: null while the extended protocol is mid-cycle (a Flush) */
|
|
198
|
+
#status = "I";
|
|
199
|
+
#closed = false;
|
|
200
|
+
#ended = null;
|
|
201
|
+
constructor(socket, source, pid) {
|
|
202
|
+
this.#socket = socket;
|
|
203
|
+
this.#source = source;
|
|
204
|
+
this.pid = pid;
|
|
205
|
+
socket.setNoDelay(true);
|
|
206
|
+
socket.on("data", (d) => this.#onData(d));
|
|
207
|
+
socket.on("error", (e) => { this.#source.note(`demo database: connection ${this.pid}: ${errorText(e)}`); this.#socket.destroy(); });
|
|
208
|
+
socket.on("close", () => { this.#closed = true; void this.end(); });
|
|
209
|
+
}
|
|
210
|
+
stateText() {
|
|
211
|
+
return this.#status === null ? "mid extended-protocol cycle" : this.#status === "T" ? "in a transaction" : this.#status === "E" ? "in a failed transaction" : "idle";
|
|
212
|
+
}
|
|
213
|
+
#then(step) {
|
|
214
|
+
this.#chain = this.#chain.then(step).catch((e) => {
|
|
215
|
+
// a step's own failure is said and ends the connection; the end's cleanup leaves the session idle
|
|
216
|
+
this.#source.note(`demo database: connection ${this.pid}: ${errorText(e)}`);
|
|
217
|
+
this.#send(errorResponse({ severity: "FATAL", code: sqlstateOf(e), message: errorText(e) }));
|
|
218
|
+
this.#socket.destroy();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
#send(...frames) {
|
|
222
|
+
if (!this.#closed && this.#socket.writable)
|
|
223
|
+
this.#socket.write(frames.length === 1 ? frames[0] : Buffer.concat(frames));
|
|
224
|
+
}
|
|
225
|
+
#fatal(code, message) {
|
|
226
|
+
this.#send(errorResponse({ severity: "FATAL", code, message }));
|
|
227
|
+
this.#socket.end();
|
|
228
|
+
}
|
|
229
|
+
#onData(d) {
|
|
230
|
+
this.#pending.push(d);
|
|
231
|
+
this.#pendingLength += d.length;
|
|
232
|
+
for (;;) {
|
|
233
|
+
const head = this.#startup ? 4 : 5;
|
|
234
|
+
if (this.#pendingLength < head)
|
|
235
|
+
return;
|
|
236
|
+
let buf = this.#pending.length === 1 ? this.#pending[0] : Buffer.concat(this.#pending, this.#pendingLength);
|
|
237
|
+
const len = buf.readInt32BE(head - 4);
|
|
238
|
+
const limit = this.#startup ? MAX_STARTUP_LENGTH : MAX_MESSAGE_LENGTH;
|
|
239
|
+
if (len < 4 || len > limit) {
|
|
240
|
+
this.#source.note(`demo database: connection ${this.pid} sent a frame of length ${len}; closed`);
|
|
241
|
+
this.#pending = [];
|
|
242
|
+
this.#pendingLength = 0;
|
|
243
|
+
this.#socket.destroy();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const whole = head - 4 + len;
|
|
247
|
+
if (buf.length < whole) {
|
|
248
|
+
this.#pending = [buf];
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const f = Buffer.from(buf.subarray(0, whole));
|
|
252
|
+
buf = buf.subarray(whole);
|
|
253
|
+
this.#pending = buf.length > 0 ? [buf] : [];
|
|
254
|
+
this.#pendingLength = buf.length;
|
|
255
|
+
if (this.#startup)
|
|
256
|
+
this.#startupFrame(f);
|
|
257
|
+
else
|
|
258
|
+
this.#queryFrame(f);
|
|
259
|
+
if (this.#closed)
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
#startupFrame(f) {
|
|
264
|
+
const decoder = new FrontendDecoder();
|
|
265
|
+
decoder.push(f);
|
|
266
|
+
let m;
|
|
267
|
+
try {
|
|
268
|
+
m = decoder.next();
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
this.#source.note(`demo database: connection ${this.pid}: ${errorText(e)}; closed`);
|
|
272
|
+
this.#socket.destroy();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (m === null) {
|
|
276
|
+
this.#socket.destroy();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
switch (m.type) {
|
|
280
|
+
case "SSLRequest":
|
|
281
|
+
case "GSSENCRequest":
|
|
282
|
+
// no TLS and no GSSAPI on loopback: the client continues in the clear (sslmode prefer, disable, allow)
|
|
283
|
+
this.#send(sslResponse(false));
|
|
284
|
+
return;
|
|
285
|
+
case "CancelRequest":
|
|
286
|
+
// the one session runs one statement at a time, each short: there is nothing to cancel
|
|
287
|
+
this.#socket.end();
|
|
288
|
+
return;
|
|
289
|
+
case "Startup":
|
|
290
|
+
this.#startup = false;
|
|
291
|
+
this.#then(() => this.#open(m.major, m.minor, m.params));
|
|
292
|
+
return;
|
|
293
|
+
default:
|
|
294
|
+
this.#socket.destroy();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async #open(major, minor, params) {
|
|
298
|
+
if (this.#closed)
|
|
299
|
+
return;
|
|
300
|
+
if (major !== 3) {
|
|
301
|
+
this.#fatal("0A000", `unsupported frontend protocol ${major}.${minor}: the demo database speaks 3.0`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (params.replication !== undefined && params.replication !== "false" && params.replication !== "0") {
|
|
305
|
+
// nothing a user reads names the engine (CLAUDE.md): the demo has no walsender, said as what it lacks
|
|
306
|
+
this.#fatal("0A000", "the demo database has no change log to stream: Mason polls it");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const database = params.database ?? params.user ?? "";
|
|
310
|
+
if (database !== DEMO_DATABASE) {
|
|
311
|
+
this.#source.note(`demo database: connection ${this.pid} asked for database ${JSON.stringify(database)}; refused (only ${DEMO_DATABASE} is served)`);
|
|
312
|
+
this.#fatal("3D000", `database "${database}" does not exist`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// protocol options (_pq_.*) and a newer minor are negotiated away, as Postgres does: this server speaks 3.0
|
|
316
|
+
const options = Object.keys(params).filter((k) => k.startsWith("_pq_."));
|
|
317
|
+
const negotiate = minor > 0 || options.length > 0 ? [frame("v", int32(0), int32(options.length), ...options.map(cstr))] : [];
|
|
318
|
+
const settings = [
|
|
319
|
+
...Object.entries(params).filter(([k]) => !NOT_SETTINGS.has(k) && !k.startsWith("_pq_.")),
|
|
320
|
+
...optionSettings(params.options ?? ""),
|
|
321
|
+
];
|
|
322
|
+
this.application = params.application_name ?? "";
|
|
323
|
+
this.settings = settings;
|
|
324
|
+
this.#source.trace?.(`demo database: connection ${this.pid} starts with ${JSON.stringify(params)}`);
|
|
325
|
+
const session = await this.#source.applySettings(this);
|
|
326
|
+
if (this.#closed)
|
|
327
|
+
return;
|
|
328
|
+
this.#send(...negotiate, authenticationOk(), ...Object.entries({ ...this.#source.serverParameters, ...session }).map(([k, v]) => parameterStatus(k, v)), backendKeyData(this.pid, (Math.random() * 0x7fffffff) | 0), readyForQuery("I"));
|
|
329
|
+
}
|
|
330
|
+
#queryFrame(f) {
|
|
331
|
+
const type = String.fromCharCode(f[0]);
|
|
332
|
+
if (type === "X") {
|
|
333
|
+
this.#socket.end();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.#batch.push(f);
|
|
337
|
+
if (type === "Q" || type === "S" || type === "H") {
|
|
338
|
+
const batch = this.#batch;
|
|
339
|
+
this.#batch = [];
|
|
340
|
+
this.#then(() => this.#run(batch, type));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async #run(frames, end) {
|
|
344
|
+
if (this.#closed)
|
|
345
|
+
return;
|
|
346
|
+
this.#source.trace?.(`demo database: connection ${this.pid}: ${frames.map(describeFrame).join(" ")}`);
|
|
347
|
+
const held = this.#source.lock.holder === this;
|
|
348
|
+
await this.#source.lock.acquire(this);
|
|
349
|
+
if (this.#closed) {
|
|
350
|
+
if (this.#status === "I")
|
|
351
|
+
this.#source.lock.release(this);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// the session's settings are this client's again when another client's turn (or a DISCARD / RESET) moved them
|
|
355
|
+
if (!held)
|
|
356
|
+
await this.#source.reapply(this);
|
|
357
|
+
const reply = splitFrames(await this.#source.exec(Buffer.concat(frames)));
|
|
358
|
+
// a DISCARD ALL or a RESET (the extension's pool sends DISCARD ALL as it puts a connection back) returned the
|
|
359
|
+
// session to PGlite's defaults, where Postgres returns a session to its startup's: this client's are applied again
|
|
360
|
+
// at its next turn (Postgres's own reply says what ran: its CommandComplete tag)
|
|
361
|
+
if (reply.some((r) => r.type === "C" && /^(DISCARD|RESET)\b/.test(r.frame.toString("latin1", 5, r.frame.length - 1))))
|
|
362
|
+
this.#source.settingsOf = null;
|
|
363
|
+
let out = reply;
|
|
364
|
+
if (end === "H") {
|
|
365
|
+
out = reply.filter((r) => r.type !== "Z");
|
|
366
|
+
}
|
|
367
|
+
else if (end === "S") {
|
|
368
|
+
const last = reply.findLastIndex((r) => r.type === "Z");
|
|
369
|
+
out = reply.filter((r, i) => r.type !== "Z" || i === last);
|
|
370
|
+
}
|
|
371
|
+
const z = out.findLast((r) => r.type === "Z");
|
|
372
|
+
this.#status = z === undefined ? (end === "H" ? null : this.#status) : String.fromCharCode(z.frame[5]);
|
|
373
|
+
if (out.length > 0)
|
|
374
|
+
this.#send(...out.map((r) => r.frame));
|
|
375
|
+
if (this.#status === "I")
|
|
376
|
+
this.#source.lock.release(this);
|
|
377
|
+
}
|
|
378
|
+
/** ends the connection (idempotent): once its batches are done, the session it held is left idle and the lock freed */
|
|
379
|
+
end() {
|
|
380
|
+
this.#closed = true;
|
|
381
|
+
this.#ended ??= (async () => {
|
|
382
|
+
// batches already queued run to their end (or see the connection closed), then the cleanup
|
|
383
|
+
await this.#chain;
|
|
384
|
+
const lock = this.#source.lock;
|
|
385
|
+
if (lock.holder === this) {
|
|
386
|
+
try {
|
|
387
|
+
if (this.#status === null)
|
|
388
|
+
this.#status = statusOf(await this.#source.exec(frame("S")));
|
|
389
|
+
if (this.#status === "T" || this.#status === "E") {
|
|
390
|
+
this.#source.note(`demo database: connection ${this.pid} ended inside a transaction; rolled back`);
|
|
391
|
+
this.#status = statusOf(await this.#source.exec(frame("Q", cstr("ROLLBACK"))));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
this.#source.note(`demo database: connection ${this.pid}: the rollback after it ended failed: ${errorText(e)}`);
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
lock.release(this);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.#source.forget(this);
|
|
402
|
+
this.#socket.destroy();
|
|
403
|
+
})();
|
|
404
|
+
return this.#ended;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** the status byte of a reply's last ReadyForQuery (I when it has none: a cleanup's reply always ends in one) */
|
|
408
|
+
function statusOf(reply) {
|
|
409
|
+
const z = splitFrames(reply).findLast((r) => r.type === "Z");
|
|
410
|
+
return z === undefined ? "I" : String.fromCharCode(z.frame[5]);
|
|
411
|
+
}
|
|
412
|
+
/** a frame as the trace says it: its type, and a Query's or a Parse's statement */
|
|
413
|
+
function describeFrame(f) {
|
|
414
|
+
const type = String.fromCharCode(f[0]);
|
|
415
|
+
if (type === "Q")
|
|
416
|
+
return `Q[${f.toString("utf8", 5, f.length - 1)}]`;
|
|
417
|
+
if (type === "P") {
|
|
418
|
+
const nameEnd = f.indexOf(0, 5);
|
|
419
|
+
const sqlEnd = f.indexOf(0, nameEnd + 1);
|
|
420
|
+
return `P${nameEnd > 5 ? `(${f.toString("utf8", 5, nameEnd)})` : ""}[${f.toString("utf8", nameEnd + 1, sqlEnd)}]`;
|
|
421
|
+
}
|
|
422
|
+
return type;
|
|
423
|
+
}
|
|
424
|
+
export class DemoSource {
|
|
425
|
+
port;
|
|
426
|
+
url;
|
|
427
|
+
/** the schema was seeded at THIS open (the folder was new) */
|
|
428
|
+
seeded;
|
|
429
|
+
/** @internal the lock every use of the one PGlite session takes */
|
|
430
|
+
lock;
|
|
431
|
+
/** @internal the ParameterStatus values that are PGlite's own, read at open */
|
|
432
|
+
serverParameters;
|
|
433
|
+
/** @internal one line per frame batch relayed, when asked for (development: what a client sends) */
|
|
434
|
+
trace;
|
|
435
|
+
#db;
|
|
436
|
+
#server;
|
|
437
|
+
#log;
|
|
438
|
+
#connections = new Set();
|
|
439
|
+
#closing = null;
|
|
440
|
+
constructor(o) {
|
|
441
|
+
this.#db = o.db;
|
|
442
|
+
this.#server = o.server;
|
|
443
|
+
this.port = o.port;
|
|
444
|
+
this.url = demoUrl(o.port);
|
|
445
|
+
this.seeded = o.seeded;
|
|
446
|
+
this.#log = o.log;
|
|
447
|
+
this.lock = o.lock;
|
|
448
|
+
this.serverParameters = o.serverParameters;
|
|
449
|
+
this.trace = o.trace;
|
|
450
|
+
}
|
|
451
|
+
/** boots PGlite at `dir` (made if absent), seeds the schema at the folder's first open, listens on 127.0.0.1:`port`
|
|
452
|
+
* (throws an Error naming the port when it is held); `log` gets one line per notable event. Port 0 takes a free one,
|
|
453
|
+
* which `port` then says. `trace` (development) gets every batch a client sends. */
|
|
454
|
+
static async open(o) {
|
|
455
|
+
// the port first: a port that is held refuses at once, before a new folder is made and seeded for nothing. A
|
|
456
|
+
// connection that arrives before PGlite is up is held until it is (accepted below), or closed when the open fails
|
|
457
|
+
let source = null;
|
|
458
|
+
const early = [];
|
|
459
|
+
let nextPid = 1000;
|
|
460
|
+
const server = net.createServer((socket) => {
|
|
461
|
+
const pid = nextPid++;
|
|
462
|
+
if (source === null) {
|
|
463
|
+
early.push({ socket, pid });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
source.#accept(socket, pid);
|
|
467
|
+
});
|
|
468
|
+
const port = await new Promise((resolve, reject) => {
|
|
469
|
+
server.once("error", (e) => {
|
|
470
|
+
reject(new Error(e.code === "EADDRINUSE"
|
|
471
|
+
// what failed alone: the way out is the caller's to say, once (src/cli/demo.ts openDemo: the project's own words)
|
|
472
|
+
? `the demo database cannot listen on 127.0.0.1:${o.port}: port ${o.port} is in use`
|
|
473
|
+
: `the demo database cannot listen on 127.0.0.1:${o.port}: ${e.message}`, { cause: e }));
|
|
474
|
+
});
|
|
475
|
+
server.listen({ host: "127.0.0.1", port: o.port }, () => {
|
|
476
|
+
const a = server.address();
|
|
477
|
+
resolve(typeof a === "object" && a !== null ? a.port : o.port);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
server.on("error", (e) => o.log(`demo database: the listener on 127.0.0.1:${port} failed: ${e.message}`));
|
|
481
|
+
let db = null;
|
|
482
|
+
try {
|
|
483
|
+
fs.mkdirSync(o.dir, { recursive: true });
|
|
484
|
+
const t0 = Date.now();
|
|
485
|
+
db = await boot(path.resolve(o.dir), o.log);
|
|
486
|
+
const lock = new SessionLock(o.log);
|
|
487
|
+
const exists = (await db.query("SELECT to_regclass('public.orders')::text AS t")).rows[0]?.t ?? null;
|
|
488
|
+
let seeded = false;
|
|
489
|
+
if (exists === null) {
|
|
490
|
+
await seed(db);
|
|
491
|
+
seeded = true;
|
|
492
|
+
const counts = await countRows(db);
|
|
493
|
+
o.log(`demo database: seeded ${DEMO_TABLES.map((t) => `${t} ${counts[t].toLocaleString("en-US")}`).join(", ")} rows in ${Date.now() - t0} ms, at ${o.dir}`);
|
|
494
|
+
}
|
|
495
|
+
// ANALYZE AT EVERY OPEN, so the catalog's row estimates are the counts (`mason tables`' rows, the copy's
|
|
496
|
+
// smallest-first order): the seed's rows commit before an ANALYZE can run, and "seeded" is told by the tables
|
|
497
|
+
// alone, so a process ended between the two would leave a folder never analysed; ~20k rows, milliseconds
|
|
498
|
+
await db.exec("ANALYZE");
|
|
499
|
+
const row = (await db.query(`SELECT ${SERVER_PARAMETERS.map((p) => `current_setting('${p}') AS "${p}"`).join(", ")}`)).rows[0] ?? {};
|
|
500
|
+
const serverParameters = {};
|
|
501
|
+
for (const p of SERVER_PARAMETERS)
|
|
502
|
+
serverParameters[p] = String(row[p] ?? "");
|
|
503
|
+
// PGlite's single-user backend leaves session_authorization empty; Postgres reports the session's user
|
|
504
|
+
if (serverParameters.session_authorization === "")
|
|
505
|
+
serverParameters.session_authorization = DEMO_USER;
|
|
506
|
+
source = new DemoSource({ db, server, port, seeded, log: o.log, lock, serverParameters, trace: o.trace });
|
|
507
|
+
for (const e of early.splice(0))
|
|
508
|
+
source.#accept(e.socket, e.pid);
|
|
509
|
+
o.log(`demo database: listening on 127.0.0.1:${port} (${source.url}), data in ${o.dir}`);
|
|
510
|
+
return source;
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
514
|
+
for (const c of early.splice(0))
|
|
515
|
+
c.socket.destroy();
|
|
516
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
517
|
+
if (db !== null) {
|
|
518
|
+
await db.close().catch((ce) => o.log(`demo database: PGlite did not close cleanly after a failed open: ${errorText(ce)}`));
|
|
519
|
+
}
|
|
520
|
+
throw new Error(`the demo database at ${o.dir} did not open: ${err.message}`, { cause: err });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
#accept(socket, pid) {
|
|
524
|
+
if (this.#closing !== null) {
|
|
525
|
+
socket.destroy();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.#connections.add(new Connection(socket, this, pid));
|
|
529
|
+
}
|
|
530
|
+
/** @internal */
|
|
531
|
+
forget(c) {
|
|
532
|
+
this.#connections.delete(c);
|
|
533
|
+
if (this.settingsOf === c)
|
|
534
|
+
this.settingsOf = null;
|
|
535
|
+
}
|
|
536
|
+
/** @internal a notable event, for the log */
|
|
537
|
+
note(line) { this.#log(line); }
|
|
538
|
+
/** @internal protocol bytes through PGlite: only under the lock */
|
|
539
|
+
exec(bytes) {
|
|
540
|
+
if (this.#closing !== null && this.#db.closed)
|
|
541
|
+
return Promise.reject(new Error("the demo database is closed"));
|
|
542
|
+
return this.#db.execProtocolRaw(bytes);
|
|
543
|
+
}
|
|
544
|
+
/** @internal the connection whose settings the one session holds now (null: PGlite's defaults, or unknown) */
|
|
545
|
+
settingsOf = null;
|
|
546
|
+
/** @internal a connection's startup settings, as one statement, under the lock it holds */
|
|
547
|
+
async reapply(c) {
|
|
548
|
+
if (this.settingsOf === c)
|
|
549
|
+
return;
|
|
550
|
+
if (c.settings.length > 0) {
|
|
551
|
+
await this.#db.query(`SELECT ${c.settings.map((_, i) => `set_config($${2 * i + 1}, $${2 * i + 2}, false)`).join(", ")}`, c.settings.flat());
|
|
552
|
+
}
|
|
553
|
+
this.settingsOf = c;
|
|
554
|
+
}
|
|
555
|
+
/** @internal a client's startup settings applied to the one session, and the values it then reports */
|
|
556
|
+
applySettings(c) {
|
|
557
|
+
return this.lock.exclusive("a client's startup", async () => {
|
|
558
|
+
this.settingsOf = null;
|
|
559
|
+
await this.reapply(c);
|
|
560
|
+
const row = (await this.#db.query(`SELECT ${SESSION_PARAMETERS.map((p) => `current_setting('${p}') AS "${p}"`).join(", ")}`)).rows[0] ?? {};
|
|
561
|
+
const out = {};
|
|
562
|
+
for (const p of SESSION_PARAMETERS)
|
|
563
|
+
out[p] = String(row[p] ?? "");
|
|
564
|
+
return out;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/** one round of changes (a new order with its items, an older order's status moved on): what it did, one line */
|
|
568
|
+
change() {
|
|
569
|
+
if (this.#closing !== null)
|
|
570
|
+
return Promise.reject(new Error("the demo database is closed"));
|
|
571
|
+
return this.lock.exclusive("change()", async () => {
|
|
572
|
+
const r = (await this.#db.query(CHANGE_STATE_SQL)).rows[0];
|
|
573
|
+
if (r === undefined)
|
|
574
|
+
throw new Error("the demo database's change could not read its state (no row)");
|
|
575
|
+
const state = {
|
|
576
|
+
newest: Number(r.newest), customers: Number(r.customers),
|
|
577
|
+
prices: JSON.parse(r.prices),
|
|
578
|
+
moving: r.moving === null ? null : JSON.parse(r.moving),
|
|
579
|
+
};
|
|
580
|
+
const c = changeSql(state);
|
|
581
|
+
const id = await this.#db.transaction(async (tx) => {
|
|
582
|
+
let last;
|
|
583
|
+
for (const s of c.statements)
|
|
584
|
+
last = (await tx.query(s)).rows[0]?.id ?? last;
|
|
585
|
+
return last ?? "?";
|
|
586
|
+
});
|
|
587
|
+
return c.said(id);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** ends every connection, the listener and PGlite, its data flushed to `dir`; idempotent */
|
|
591
|
+
close() {
|
|
592
|
+
this.#closing ??= (async () => {
|
|
593
|
+
const listening = new Promise((resolve) => this.#server.close(() => resolve()));
|
|
594
|
+
// every connection's socket goes; each one's end runs its cleanup (a rollback under the lock) before it settles
|
|
595
|
+
await Promise.all([...this.#connections].map((c) => c.end()));
|
|
596
|
+
await listening;
|
|
597
|
+
// the lock's last holder (a change() in flight) finishes before PGlite closes
|
|
598
|
+
await this.lock.exclusive("close()", async () => { await this.#db.close(); });
|
|
599
|
+
})();
|
|
600
|
+
return this.#closing;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* PGLITE AS DATABASE `demo` AND USER `demo`, so `current_database()` and `current_user` are what the URL says (PGlite
|
|
605
|
+
* boots into `postgres` as `postgres`, and cannot boot into a database that does not exist yet). A folder without them
|
|
606
|
+
* — a new one, or one whose first open stopped before they were made — is booted once as `postgres` to make them (the
|
|
607
|
+
* role a superuser, as an app's owner often is: every catalog read and `pg_control_system()` are its), then booted
|
|
608
|
+
* again as `demo`.
|
|
609
|
+
*/
|
|
610
|
+
async function boot(dir, log) {
|
|
611
|
+
const asDemo = async () => {
|
|
612
|
+
const db = new PGlite({ dataDir: dir, database: DEMO_DATABASE, username: DEMO_USER });
|
|
613
|
+
await db.waitReady;
|
|
614
|
+
return db;
|
|
615
|
+
};
|
|
616
|
+
if (fs.existsSync(path.join(dir, "PG_VERSION"))) {
|
|
617
|
+
try {
|
|
618
|
+
return await asDemo();
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
// the folder is a Postgres without the demo database or role yet: made below, then booted again; any other
|
|
622
|
+
// failure fails that boot too, and is thrown from it
|
|
623
|
+
log(`demo database: ${dir} did not boot as database ${DEMO_DATABASE} (${errorText(e)}); making it`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const admin = new PGlite({ dataDir: dir });
|
|
627
|
+
try {
|
|
628
|
+
await admin.waitReady;
|
|
629
|
+
const has = (await admin.query(`SELECT EXISTS (SELECT FROM pg_database WHERE datname = '${DEMO_DATABASE}') AS db, EXISTS (SELECT FROM pg_roles WHERE rolname = '${DEMO_USER}') AS role`)).rows[0];
|
|
630
|
+
if (has?.role !== true)
|
|
631
|
+
await admin.exec(`CREATE ROLE ${DEMO_USER} SUPERUSER LOGIN`);
|
|
632
|
+
// CREATE DATABASE refuses a transaction block: a statement of its own
|
|
633
|
+
if (has?.db !== true)
|
|
634
|
+
await admin.exec(`CREATE DATABASE ${DEMO_DATABASE} OWNER ${DEMO_USER}`);
|
|
635
|
+
}
|
|
636
|
+
finally {
|
|
637
|
+
await admin.close();
|
|
638
|
+
}
|
|
639
|
+
return asDemo();
|
|
640
|
+
}
|
|
641
|
+
/** the seed, in one transaction (the ANALYZE that makes the row estimates the counts is every open's, after it) */
|
|
642
|
+
async function seed(db) {
|
|
643
|
+
const s = seedStatements(new Date());
|
|
644
|
+
await db.transaction(async (tx) => {
|
|
645
|
+
await tx.exec(s.ddl);
|
|
646
|
+
for (const statement of s.rows)
|
|
647
|
+
await tx.exec(statement);
|
|
648
|
+
for (const statement of s.after)
|
|
649
|
+
await tx.exec(statement);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
async function countRows(db) {
|
|
653
|
+
const r = (await db.query(`SELECT ${DEMO_TABLES.map((t) => `(SELECT count(*) FROM ${t})::text AS ${t}`).join(", ")}`)).rows[0] ?? {};
|
|
654
|
+
return Object.fromEntries(DEMO_TABLES.map((t) => [t, Number(r[t] ?? 0)]));
|
|
655
|
+
}
|