@batadata/cli 0.1.16 → 0.2.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 CHANGED
@@ -87,6 +87,7 @@ flag always wins**, so linking never silently overrides an intentional request:
87
87
  | Command | Description |
88
88
  |---------|-------------|
89
89
  | `create <name>` | Create a project and wait for it to be ready |
90
+ | `import --source <uri>` | Migrate a Postgres/Neon database into BataDB (see [Migrate from Neon](#migrate-from-neon)) |
90
91
  | `connect <name>` | Open `psql` to a project (auto-wakes if suspended) |
91
92
  | `status` | Show all projects and their status |
92
93
  | `usage` | Show per-dimension cost (compute, storage, transfer) for the current period |
@@ -115,6 +116,47 @@ safety.
115
116
 
116
117
  Run `bata --help` for the full, authoritative command list, or `bata --version`.
117
118
 
119
+ ## Migrate from Neon
120
+
121
+ `bata import` moves an existing Postgres database — Neon or anything else — into a
122
+ fresh BataDB project in one command. It shells out to the standard `pg_dump` /
123
+ `psql` client tools (nothing proprietary), streams the dump straight through, and
124
+ verifies the result.
125
+
126
+ ```bash
127
+ bata import --source "postgresql://…@ep-x.neon.tech/neondb" # → a new project named `neondb`
128
+ bata import --source "$NEON_URL" --name my-app # name the new project
129
+ bata import --source "$NEON_URL" --project proj_abc123 --yes # into an existing project
130
+ ```
131
+
132
+ **Prerequisite:** local `pg_dump` **and** `psql` whose major version is **≥ the
133
+ source server's** (`pg_dump 16` cannot dump a PostgreSQL 17 server). Neon defaults
134
+ to PostgreSQL 17, so install matching client tools:
135
+
136
+ ```bash
137
+ brew install libpq # macOS (or: brew install postgresql@17)
138
+ sudo apt install postgresql-client-17 # Ubuntu
139
+ ```
140
+
141
+ What it does, in order: inspects the source, checks your client tools, creates (or
142
+ resolves) the target project, waits for its compute, **preflights extensions** (any
143
+ source extension the target can't provide hard-fails *before* the restore starts,
144
+ rather than dying mid-restore at `CREATE EXTENSION` and half-importing), streams
145
+ `pg_dump … --encoding=UTF8 | psql` (the `--encoding=UTF8` guarantees a valid-UTF-8
146
+ stream so non-UTF-8 source databases aren't corrupted in transit), then verifies
147
+ every table's row count and every sequence's value on both sides. It exits non-zero
148
+ on any mismatch. Importing into an existing `--project` that already holds user
149
+ objects requires `--yes`. `--json` emits a machine-readable result for agents.
150
+
151
+ **PostgreSQL 16 vs 17 (honest caveat).** BataDB runs PostgreSQL **16** today; Neon
152
+ defaults to **17**. Standard schemas migrate cleanly — `bata import` transparently
153
+ strips the one PG17-only line `pg_dump` emits (`SET transaction_timeout = 0;`) that
154
+ PG16 would reject. But genuinely **PG17-only** schema features (a data type, syntax,
155
+ or option that only exists in 17) **will fail loudly during restore** — the command
156
+ surfaces the exact `psql` errors and exits non-zero rather than pretending the
157
+ migration succeeded. See [docs/migrate-from-neon.md](../../docs/migrate-from-neon.md)
158
+ for the full guide and the manual `pg_dump | psql` fallback.
159
+
118
160
  ## Point-in-time restore (PITR)
119
161
 
120
162
  Recover a branch to an earlier point in time. Restore is **non-destructive**: it
@@ -0,0 +1,37 @@
1
+ /**
2
+ * bata compute — inspect and configure a branch's compute.
3
+ *
4
+ * `bata compute set` toggles the dedicated always-on tier and/or picks a fixed
5
+ * size for a branch's compute; `bata compute status` surfaces each branch's
6
+ * size + always-on state. Both resolve the compute via the branch (a branch has
7
+ * one compute today) so agents never handle raw compute IDs.
8
+ */
9
+ interface ComputeSetFlags {
10
+ projectId?: string;
11
+ branch?: string;
12
+ alwaysOn?: boolean;
13
+ size?: number;
14
+ }
15
+ /**
16
+ * Parse `compute set` flags. `--always-on`/`--no-always-on` are valueless
17
+ * booleans; `--size <cu>` (and `--size=<cu>`) takes a number. `--project` /
18
+ * `--branch` accept both spaced and `=` forms, matching the other commands.
19
+ */
20
+ export declare function parseComputeSetFlags(args: string[]): ComputeSetFlags;
21
+ export declare function computeSet(args: string[]): Promise<void>;
22
+ interface ComputeRefFlags {
23
+ projectId?: string;
24
+ branchRef?: string;
25
+ limit?: number;
26
+ }
27
+ /**
28
+ * Parse args for `compute restart` / `compute logs`. The branch is a positional
29
+ * `<id|name>` (or `--branch`, matching `compute set`); `--project` overrides the
30
+ * linked default; `--limit <n>` (logs only) bounds the line count.
31
+ */
32
+ export declare function parseComputeRefArgs(args: string[]): ComputeRefFlags;
33
+ export declare function computeRestart(args: string[]): Promise<void>;
34
+ export declare function computeLogs(args: string[]): Promise<void>;
35
+ export declare function computeStatus(args: string[]): Promise<void>;
36
+ export declare function handleCompute(args: string[]): Promise<void>;
37
+ export {};
@@ -0,0 +1,347 @@
1
+ /**
2
+ * bata compute — inspect and configure a branch's compute.
3
+ *
4
+ * `bata compute set` toggles the dedicated always-on tier and/or picks a fixed
5
+ * size for a branch's compute; `bata compute status` surfaces each branch's
6
+ * size + always-on state. Both resolve the compute via the branch (a branch has
7
+ * one compute today) so agents never handle raw compute IDs.
8
+ */
9
+ import { api, apiError, resolveTeamId } from "../api.js";
10
+ import { requireToken, isJsonMode } from "../config.js";
11
+ import { colors, log, json, spinner, table, heading } from "../utils/logger.js";
12
+ import { emitError } from "../utils/errors.js";
13
+ import { resolveProjectId } from "../link.js";
14
+ // Fixed dedicated sizes: 1 CU = 512MB / 0.25 vCPU, so {1,2,4,8,16} = 512MB→8GB.
15
+ // Mirrors DEDICATED_SIZE_CU in control-plane/src/routes/computes.ts.
16
+ const DEDICATED_SIZE_CU = [1, 2, 4, 8, 16];
17
+ /**
18
+ * Parse `compute set` flags. `--always-on`/`--no-always-on` are valueless
19
+ * booleans; `--size <cu>` (and `--size=<cu>`) takes a number. `--project` /
20
+ * `--branch` accept both spaced and `=` forms, matching the other commands.
21
+ */
22
+ export function parseComputeSetFlags(args) {
23
+ const flags = {};
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg === "--project")
27
+ flags.projectId = args[++i];
28
+ else if (arg.startsWith("--project="))
29
+ flags.projectId = arg.slice("--project=".length);
30
+ else if (arg === "--branch")
31
+ flags.branch = args[++i];
32
+ else if (arg.startsWith("--branch="))
33
+ flags.branch = arg.slice("--branch=".length);
34
+ else if (arg === "--always-on")
35
+ flags.alwaysOn = true;
36
+ else if (arg === "--no-always-on")
37
+ flags.alwaysOn = false;
38
+ else if (arg === "--size")
39
+ flags.size = Number(args[++i]);
40
+ else if (arg.startsWith("--size="))
41
+ flags.size = Number(arg.slice("--size=".length));
42
+ }
43
+ return flags;
44
+ }
45
+ /** Fetch the project (branches enriched with their computes). Null on failure. */
46
+ async function fetchProject(projectId, token, teamId) {
47
+ const query = {};
48
+ if (teamId)
49
+ query.team_id = teamId;
50
+ const res = await api.get(`/v1/projects/${projectId}`, token, query);
51
+ return res.ok ? res.data : null;
52
+ }
53
+ /** Resolve a branch by id OR name, else null. */
54
+ function findBranch(project, ref) {
55
+ return project.branches?.find((b) => b.id === ref || b.name === ref) ?? null;
56
+ }
57
+ /** The compute serving a branch (a branch has one today); prefer an active one. */
58
+ function branchCompute(branch) {
59
+ const computes = branch.computes ?? [];
60
+ return computes.find((comp) => comp.status === "active") ?? computes[0] ?? null;
61
+ }
62
+ export async function computeSet(args) {
63
+ const jsonMode = isJsonMode();
64
+ const flags = parseComputeSetFlags(args);
65
+ if (flags.alwaysOn === undefined && flags.size === undefined) {
66
+ emitError("MISSING_ARG", "Nothing to change.", "Pass --always-on/--no-always-on and/or --size <cu>.");
67
+ }
68
+ if (flags.size !== undefined && !DEDICATED_SIZE_CU.includes(flags.size)) {
69
+ emitError("INVALID_FLAG", `Invalid --size ${flags.size}. Allowed: ${DEDICATED_SIZE_CU.join(", ")} (512MB, 1GB, 2GB, 4GB, 8GB).`, "Example: bata compute set --branch main --size 8");
70
+ }
71
+ if (!flags.branch) {
72
+ emitError("MISSING_ARG", "A branch is required.", "Pass --branch <name-or-id>.");
73
+ }
74
+ const token = requireToken();
75
+ const projectId = resolveProjectId(flags.projectId).projectId;
76
+ if (!projectId) {
77
+ emitError("NO_PROJECT", "No project.", "Pass --project <id>, or run `bata link <project>` to set a default.");
78
+ }
79
+ const teamId = await resolveTeamId(token);
80
+ const s = jsonMode ? null : spinner(`Resolving branch ${colors.cyan(flags.branch)}`);
81
+ const project = await fetchProject(projectId, token, teamId);
82
+ if (!project) {
83
+ s?.stop();
84
+ emitError("API_UNAVAILABLE", "Failed to fetch project.", "");
85
+ }
86
+ const branch = findBranch(project, flags.branch);
87
+ if (!branch) {
88
+ s?.stop();
89
+ emitError("BRANCH_NOT_FOUND", `Branch "${flags.branch}" not found in this project.`, "List branches with: bata db branches --json");
90
+ }
91
+ const compute = branchCompute(branch);
92
+ if (!compute) {
93
+ s?.stop();
94
+ emitError("NOT_FOUND", `Branch "${branch.name}" has no compute yet.`, "Wait for the branch to finish provisioning, then retry.");
95
+ }
96
+ const body = {};
97
+ if (flags.alwaysOn !== undefined)
98
+ body.always_on = flags.alwaysOn;
99
+ if (flags.size !== undefined)
100
+ body.size_cu = flags.size;
101
+ const res = await api.patch(`/v1/computes/${compute.id}`, body, token);
102
+ s?.stop();
103
+ if (!res.ok) {
104
+ emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
105
+ : res.status === 404 ? "NOT_FOUND"
106
+ : res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
107
+ : "CLI_ERROR", apiError(res, "Failed to update compute"), "");
108
+ }
109
+ if (jsonMode) {
110
+ json({
111
+ compute: {
112
+ id: res.data.id,
113
+ branch: branch.name,
114
+ project_id: projectId,
115
+ always_on: res.data.alwaysOn ?? null,
116
+ size_cu: res.data.sizeCu ?? null,
117
+ status: res.data.status ?? null,
118
+ },
119
+ notes: res.data.notes ?? [],
120
+ });
121
+ return;
122
+ }
123
+ log();
124
+ log(` ${colors.green(">")} Compute for branch ${colors.cyan(branch.name)} updated`);
125
+ if (flags.alwaysOn !== undefined) {
126
+ log(` ${colors.dim("Always-on:")} ${res.data.alwaysOn ? colors.green("on") : "off"}`);
127
+ }
128
+ if (flags.size !== undefined) {
129
+ log(` ${colors.dim("Size:")} ${res.data.sizeCu ?? flags.size} CU`);
130
+ }
131
+ for (const note of res.data.notes ?? []) {
132
+ log(` ${colors.yellow("note:")} ${colors.dim(note)}`);
133
+ }
134
+ log();
135
+ }
136
+ /**
137
+ * Parse args for `compute restart` / `compute logs`. The branch is a positional
138
+ * `<id|name>` (or `--branch`, matching `compute set`); `--project` overrides the
139
+ * linked default; `--limit <n>` (logs only) bounds the line count.
140
+ */
141
+ export function parseComputeRefArgs(args) {
142
+ const flags = {};
143
+ for (let i = 0; i < args.length; i++) {
144
+ const arg = args[i];
145
+ if (arg === "--project")
146
+ flags.projectId = args[++i];
147
+ else if (arg.startsWith("--project="))
148
+ flags.projectId = arg.slice("--project=".length);
149
+ else if (arg === "--branch")
150
+ flags.branchRef = args[++i];
151
+ else if (arg.startsWith("--branch="))
152
+ flags.branchRef = arg.slice("--branch=".length);
153
+ else if (arg === "--limit")
154
+ flags.limit = Number(args[++i]);
155
+ else if (arg.startsWith("--limit="))
156
+ flags.limit = Number(arg.slice("--limit=".length));
157
+ else if (!arg.startsWith("-") && flags.branchRef === undefined)
158
+ flags.branchRef = arg;
159
+ }
160
+ return flags;
161
+ }
162
+ /** Map an API status to the CLI error-code contract (see utils/errors.ts). */
163
+ function computeErrorCode(status) {
164
+ if (status === 401 || status === 403)
165
+ return "INVALID_KEY";
166
+ if (status === 404)
167
+ return "NOT_FOUND";
168
+ if (status === 501)
169
+ return "NOT_IMPLEMENTED";
170
+ if (status >= 500 || status === 0)
171
+ return "API_UNAVAILABLE";
172
+ return "CLI_ERROR";
173
+ }
174
+ /**
175
+ * Resolve a branch's compute from a `<id|name>` ref, shared by restart/logs.
176
+ * Emits (and exits) on any failure; returns the resolved handles on success.
177
+ */
178
+ async function resolveBranchCompute(flags, jsonMode) {
179
+ if (!flags.branchRef) {
180
+ emitError("MISSING_ARG", "A branch is required.", "Pass a branch name or id, e.g. bata compute restart main.");
181
+ }
182
+ const token = requireToken();
183
+ const projectId = resolveProjectId(flags.projectId).projectId;
184
+ if (!projectId) {
185
+ emitError("NO_PROJECT", "No project.", "Pass --project <id>, or run `bata link <project>` to set a default.");
186
+ }
187
+ const teamId = await resolveTeamId(token);
188
+ const s = jsonMode ? null : spinner(`Resolving branch ${colors.cyan(flags.branchRef)}`);
189
+ const project = await fetchProject(projectId, token, teamId);
190
+ if (!project) {
191
+ s?.stop();
192
+ emitError("API_UNAVAILABLE", "Failed to fetch project.", "");
193
+ }
194
+ const branch = findBranch(project, flags.branchRef);
195
+ if (!branch) {
196
+ s?.stop();
197
+ emitError("BRANCH_NOT_FOUND", `Branch "${flags.branchRef}" not found in this project.`, "List branches with: bata db branches --json");
198
+ }
199
+ const compute = branchCompute(branch);
200
+ if (!compute) {
201
+ s?.stop();
202
+ emitError("NOT_FOUND", `Branch "${branch.name}" has no compute yet.`, "Wait for the branch to finish provisioning, then retry.");
203
+ }
204
+ s?.stop();
205
+ return { token, teamId, projectId: projectId, branch: branch, compute: compute };
206
+ }
207
+ export async function computeRestart(args) {
208
+ const jsonMode = isJsonMode();
209
+ const flags = parseComputeRefArgs(args);
210
+ const { token, branch, compute } = await resolveBranchCompute(flags, jsonMode);
211
+ const res = await api.post(`/v1/computes/${compute.id}/restart`, {}, token);
212
+ if (!res.ok) {
213
+ emitError(computeErrorCode(res.status), apiError(res, "Failed to restart compute"), "");
214
+ }
215
+ if (jsonMode) {
216
+ json({
217
+ compute: {
218
+ id: compute.id,
219
+ branch: branch.name,
220
+ status: res.data.status ?? null,
221
+ restarted: res.data.restarted ?? true,
222
+ },
223
+ });
224
+ return;
225
+ }
226
+ log();
227
+ log(` ${colors.green(">")} Compute for branch ${colors.cyan(branch.name)} restarted`);
228
+ log();
229
+ }
230
+ export async function computeLogs(args) {
231
+ const jsonMode = isJsonMode();
232
+ const flags = parseComputeRefArgs(args);
233
+ if (flags.limit !== undefined && (!Number.isInteger(flags.limit) || flags.limit < 1)) {
234
+ emitError("INVALID_FLAG", `Invalid --limit ${flags.limit}.`, "Pass a positive integer (max 500).");
235
+ }
236
+ const { token, branch, compute } = await resolveBranchCompute(flags, jsonMode);
237
+ const query = flags.limit !== undefined ? { limit: String(Math.min(flags.limit, 500)) } : undefined;
238
+ const res = await api.get(`/v1/computes/${compute.id}/logs`, token, query);
239
+ if (!res.ok) {
240
+ emitError(computeErrorCode(res.status), apiError(res, "Failed to fetch compute logs"), "");
241
+ }
242
+ const lines = res.data.lines ?? [];
243
+ if (jsonMode) {
244
+ json({ compute_id: compute.id, branch: branch.name, source: res.data.source, lines });
245
+ return;
246
+ }
247
+ heading(`Logs — ${branch.name} ${colors.dim(`(${res.data.source})`)}`);
248
+ if (lines.length === 0) {
249
+ log(` ${colors.dim("No recent log lines.")}`);
250
+ }
251
+ else {
252
+ for (const line of lines) {
253
+ log(` ${colors.dim(line.ts)} ${line.message}`);
254
+ }
255
+ }
256
+ log();
257
+ }
258
+ export async function computeStatus(args) {
259
+ const jsonMode = isJsonMode();
260
+ const flags = parseComputeSetFlags(args); // reuse --project/--branch parsing
261
+ const token = requireToken();
262
+ const projectId = resolveProjectId(flags.projectId).projectId;
263
+ if (!projectId) {
264
+ emitError("NO_PROJECT", "No project.", "Pass --project <id>, or run `bata link <project>` to set a default.");
265
+ }
266
+ const teamId = await resolveTeamId(token);
267
+ const s = jsonMode ? null : spinner("Fetching computes");
268
+ const project = await fetchProject(projectId, token, teamId);
269
+ s?.stop();
270
+ if (!project) {
271
+ emitError("API_UNAVAILABLE", "Failed to fetch project.", "");
272
+ }
273
+ let branchList = project.branches ?? [];
274
+ if (flags.branch) {
275
+ const b = findBranch(project, flags.branch);
276
+ if (!b) {
277
+ emitError("BRANCH_NOT_FOUND", `Branch "${flags.branch}" not found in this project.`, "List branches with: bata db branches --json");
278
+ }
279
+ branchList = [b];
280
+ }
281
+ const rows = branchList.map((b) => {
282
+ const compute = branchCompute(b);
283
+ return {
284
+ branch: b.name,
285
+ compute_id: compute?.id ?? null,
286
+ status: compute?.status ?? null,
287
+ size_cu: compute?.sizeCu ?? null,
288
+ always_on: compute?.alwaysOn ?? false,
289
+ };
290
+ });
291
+ if (jsonMode) {
292
+ json({ project_id: projectId, computes: rows });
293
+ return;
294
+ }
295
+ heading(`Computes — ${project.name}`);
296
+ if (rows.length === 0) {
297
+ log(` ${colors.dim("No branches found.")}`);
298
+ log();
299
+ return;
300
+ }
301
+ table(["BRANCH", "STATUS", "SIZE (CU)", "ALWAYS-ON"], rows.map((r) => [
302
+ r.branch,
303
+ r.status ?? "-",
304
+ r.size_cu != null ? String(r.size_cu) : "-",
305
+ r.always_on ? colors.green("on") : "-",
306
+ ]));
307
+ log();
308
+ }
309
+ function computeHelp() {
310
+ log();
311
+ log(` ${colors.bold("bata compute")} — inspect and configure branch compute`);
312
+ log();
313
+ log(` ${colors.cyan("bata compute status [--project <id>] [--branch <name-or-id>]")}`);
314
+ log(` ${colors.cyan("bata compute set --branch <name-or-id> [--always-on|--no-always-on] [--size <cu>]")}`);
315
+ log(` ${colors.cyan("bata compute restart <name-or-id> [--project <id>]")}`);
316
+ log(` ${colors.cyan("bata compute logs <name-or-id> [--limit <n>] [--project <id>]")}`);
317
+ log();
318
+ log(` ${colors.dim("--always-on / --no-always-on")} Dedicated always-on primary (no cold starts; flat bill)`);
319
+ log(` ${colors.dim("--size <cu>")} Fixed size: ${DEDICATED_SIZE_CU.join(", ")} CU (512MB, 1GB, 2GB, 4GB, 8GB)`);
320
+ log(` ${colors.dim("--limit <n>")} Log lines to fetch (default 100, max 500)`);
321
+ log(` ${colors.dim("--project <id>")} Target project (else the linked/default project)`);
322
+ log(` ${colors.dim("--branch <name-or-id>")} Target branch by name OR id`);
323
+ log();
324
+ log(` ${colors.dim("Changing --size restarts the compute briefly to apply it.")}`);
325
+ log(` ${colors.dim("restart gracefully bounces an active compute in place (stop → start).")}`);
326
+ log();
327
+ }
328
+ export async function handleCompute(args) {
329
+ const sub = args[0];
330
+ if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
331
+ computeHelp();
332
+ return;
333
+ }
334
+ switch (sub) {
335
+ case "set":
336
+ return computeSet(args.slice(1));
337
+ case "status":
338
+ case "info":
339
+ return computeStatus(args.slice(1));
340
+ case "restart":
341
+ return computeRestart(args.slice(1));
342
+ case "logs":
343
+ return computeLogs(args.slice(1));
344
+ default:
345
+ emitError("INVALID_FLAG", `Unknown subcommand: compute ${sub}`, "Available: set, status, restart, logs");
346
+ }
347
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * bata import — Neon (or any Postgres) → BataDB migration.
3
+ *
4
+ * bata import --source <postgres-uri> [--project <id> | --name <name>] [--yes] [--json]
5
+ *
6
+ * Agent-native: shells out to the standard `pg_dump` / `psql` client tools
7
+ * (zero runtime deps) and streams a dump straight into a fresh BataDB project.
8
+ *
9
+ * BataDB runs PostgreSQL 16 today; Neon defaults to 17. A PG17 → PG16
10
+ * dump/restore is clean for standard schemas EXCEPT that pg_dump 17 emits
11
+ * `SET transaction_timeout = 0;` — a GUC PG16 doesn't know — into the dump
12
+ * preamble. We strip that one line (and any future target-unknown GUC SETs)
13
+ * out of the stream. Genuinely PG17-only schema features still fail loudly at
14
+ * restore time, and we surface those errors honestly rather than pretending
15
+ * the migration succeeded.
16
+ */
17
+ /**
18
+ * GUC SET statements pg_dump emits that a BataDB (PG16) target rejects. Kept as
19
+ * an extensible list so a future target-unknown GUC is a one-line addition.
20
+ * `transaction_timeout` is new in PG17 and is the only one seen in practice.
21
+ */
22
+ export declare const TARGET_UNKNOWN_GUCS: string[];
23
+ /**
24
+ * A streaming line filter that drops target-unknown GUC `SET` statements from a
25
+ * pg_dump stream. It buffers a partial trailing line across chunk boundaries so
26
+ * a `SET transaction_timeout = 0;` split across two reads is still recognised
27
+ * and removed. `push()` returns the text to forward to psql for that chunk;
28
+ * `flush()` returns any buffered final line (a dump without a trailing newline).
29
+ *
30
+ * PREAMBLE-ONLY: pg_dump emits the target-unknown GUCs ONLY in the initial
31
+ * connection preamble (the block of `SET …;` / `SELECT pg_catalog.set_config(…)`
32
+ * statements at the very top). Filtering the whole stream is unsafe: a dumped
33
+ * function/procedure body can contain a standalone line `SET transaction_timeout
34
+ * = 0;` inside its dollar-quoted text, and a COPY data row can be literally
35
+ * anything — mutating those would silently corrupt the restore. So we only strip
36
+ * inside the preamble: the first line that is NOT one of {empty, a `--` comment,
37
+ * `SET …;`, `SELECT pg_catalog.set_config(…);`} permanently ends the preamble,
38
+ * after which every line passes through verbatim. A GUC-looking SET after the
39
+ * preamble is therefore NOT stripped — which is correct: transaction_timeout only
40
+ * ever appears in the preamble; were a future pg_dump to emit it mid-dump, the
41
+ * restore would fail loudly rather than be silently altered.
42
+ */
43
+ export declare function makeGucLineFilter(gucs?: string[]): {
44
+ push: (chunk: string) => string;
45
+ flush: () => string;
46
+ };
47
+ /**
48
+ * Extract a PostgreSQL major version from any of the shapes we encounter:
49
+ * - `server_version_num` (e.g. "170002", "160003", "90603")
50
+ * - dotted `server_version` (e.g. "17.2", "16.3 (Ubuntu 16.3-1.pgdg…)")
51
+ * - `pg_dump --version` output ("pg_dump (PostgreSQL) 16.3")
52
+ * Returns the integer major, or null if none can be found.
53
+ */
54
+ export declare function parsePgMajor(v: string): number | null;
55
+ /** A client tool must be at least the source major (pg_dump can't dump a newer
56
+ * server than itself; psql can't parse a newer pg_dump's meta-commands). */
57
+ export declare function toolMajorSatisfies(toolMajor: number, sourceMajor: number): boolean;
58
+ /** Clear, actionable message when a local client tool is too old for the source. */
59
+ export declare function toolVersionErrorMessage(tool: string, toolMajor: number, sourceMajor: number): string;
60
+ /** Message when psql can't restore the stream THIS pg_dump produces. */
61
+ export declare function psqlVsDumpErrorMessage(psqlMajor: number, dumpMajor: number): string;
62
+ /**
63
+ * Verdict on whether the local client tools can migrate a source of `sourceMajor`
64
+ * (null when the source version couldn't be parsed). Rules:
65
+ * - pg_dump must be ≥ source (can't dump a newer server than itself)
66
+ * - psql must be ≥ source AND ≥ pg_dump — the restore stream is produced by the
67
+ * LOCAL pg_dump, and pg_dump 17 emits \restrict/\unrestrict regardless of the
68
+ * source version, so psql 16 + pg_dump 17 fails even against a PG16 source.
69
+ * Effectively psql must satisfy max(sourceMajor, dumpMajor). Returns the install
70
+ * target so the caller can point the user at the right client package.
71
+ */
72
+ export declare function checkClientVersions(dumpMajor: number, psqlMajor: number, sourceMajor: number | null): {
73
+ ok: true;
74
+ } | {
75
+ ok: false;
76
+ message: string;
77
+ needMajor: number;
78
+ };
79
+ /** Pull the first meaningful line out of a psql connection error for the user. */
80
+ export declare function sourcePreflightErrorMessage(stderr: string): string;
81
+ /** Default target project name = the source database name, else the URI path. */
82
+ export declare function defaultProjectName(preflightDb: string | undefined, sourceUri: string): string;
83
+ export interface CountRow {
84
+ name: string;
85
+ source: string;
86
+ target: string;
87
+ }
88
+ /**
89
+ * Compare two name→value maps (table row counts, or sequence last_values) and
90
+ * return only the mismatches. A name present on one side but not the other is a
91
+ * mismatch too (rendered as ∅). Deterministically sorted by name.
92
+ */
93
+ export declare function diffCounts(source: Map<string, string>, target: Map<string, string>): CountRow[];
94
+ /** Build the `table()` rows for a verify mismatch report (tables + sequences). */
95
+ export declare function mismatchRows(tableRows: CountRow[], seqRows: CountRow[]): string[][];
96
+ /** Double-quote a SQL identifier (escaping embedded quotes). */
97
+ export declare function quoteIdent(id: string): string;
98
+ /**
99
+ * Map a child process 'close' `(code, signal)` to the numeric exit used for the
100
+ * migration verdict. A signal death (`code === null`, e.g. OOM-killer or an
101
+ * operator `kill`) is a FAILURE — it must never read as success just because its
102
+ * peer exited cleanly. The one exception: when WE deliberately SIGTERM'd the dump
103
+ * after a restore failure (`intentionalKill`), that path already has a nonzero
104
+ * restoreCode so the overall verdict is false regardless; we normalize to 0 to
105
+ * avoid a redundant, confusing error line.
106
+ */
107
+ export declare function resolveChildExit(code: number | null, signal: string | null, opts?: {
108
+ intentionalKill?: boolean;
109
+ }): {
110
+ code: number;
111
+ error?: string;
112
+ };
113
+ /**
114
+ * Source extensions the target can't satisfy = source extensions not in the
115
+ * target's available set. `plpgsql` is built into every PostgreSQL and always
116
+ * present, so it's excluded — it can never be a real gap even though it shows up
117
+ * in pg_extension. Used both for the pre-restore availability gate (against
118
+ * `pg_available_extensions`) and the residual post-restore warning (against the
119
+ * target's installed `pg_extension`).
120
+ */
121
+ export declare function missingExtensions(sourceExts: string[], targetHasExts: string[]): string[];
122
+ /**
123
+ * Objects BataDB's provisioning seeds into EVERY new project — platform furniture,
124
+ * not user data. Because a freshly created target ALWAYS contains these, they must
125
+ * be excluded from all TARGET-side "unexpected object" accounting: the
126
+ * created-project hard-fail, the existing-target leftover warnings/JSON, and the
127
+ * `--yes` non-empty guard (a target holding ONLY these is effectively empty). They
128
+ * are NEVER excluded on the SOURCE side — a source `public.health_check` (or a
129
+ * source `neon_migration` schema, e.g. a migration FROM another BataDB) is treated
130
+ * as real data and must verify normally.
131
+ *
132
+ * Two flavours:
133
+ * - Named objects: `public.health_check` (+ its sequence) — the data-path
134
+ * health-check row/table seeded on provision.
135
+ * - Whole schemas: `neon_migration` — our Neon-derived compute engine's
136
+ * compute_ctl records applied internal SQL migrations here (table
137
+ * `neon_migration.migration_id`) on EVERY compute startup. Real Neon hides this
138
+ * schema from customer `pg_tables`; our compute exposes it, so ANY object under
139
+ * `neon_migration.*` is platform furniture, never user data.
140
+ */
141
+ export declare const PLATFORM_SCAFFOLDING_TABLES: string[];
142
+ export declare const PLATFORM_SCAFFOLDING_SEQUENCES: string[];
143
+ /** Schemas whose ENTIRE contents are platform furniture (any `<schema>.*` object). */
144
+ export declare const PLATFORM_SCAFFOLDING_SCHEMAS: string[];
145
+ /** Is this qualified name one of BataDB's seeded platform-scaffolding objects —
146
+ * either an exact named object, or anything inside a scaffolding schema? */
147
+ export declare function isPlatformScaffolding(qualifiedName: string): boolean;
148
+ /** Drop platform-scaffolding names — applied ONLY to TARGET-side object lists,
149
+ * never to source accounting. */
150
+ export declare function excludeScaffolding(names: string[]): string[];
151
+ /**
152
+ * Scaffolding names that appear in a SOURCE object list — i.e. a user table that
153
+ * shares a name with BataDB's seeded platform table. On restore that `CREATE
154
+ * TABLE` clashes with the scaffolding already present on the target: with
155
+ * ON_ERROR_STOP the restore fails ("relation already exists" → RESTORE_FAILED),
156
+ * or, if the schemas happen to be compatible, rows append and the row-count verify
157
+ * catches the drift. Either way verify stays honest — the source name is NOT
158
+ * excluded from source accounting; we only warn the user up front.
159
+ */
160
+ export declare function scaffoldingCollisions(sourceQualifiedNames: string[]): string[];
161
+ /**
162
+ * Qualified names present on the TARGET but absent from the source — used for both
163
+ * tables and sequences. Verification only enumerates SOURCE objects, so on a
164
+ * `--project --yes` import into a non-empty target these are invisible to the
165
+ * count/value checks: they were left untouched and aren't covered by verify.
166
+ * Surfaced honestly (a warning for an existing target; unexpected for a freshly
167
+ * created one). Sorted, deduped by set.
168
+ */
169
+ export declare function targetOnlyNames(sourceQualified: string[], targetQualified: string[]): string[];
170
+ export declare function parseImportArgs(args: string[]): {
171
+ source?: string;
172
+ projectId?: string;
173
+ name?: string;
174
+ help: boolean;
175
+ } | {
176
+ error: string;
177
+ };
178
+ export declare function importDb(args: string[]): Promise<void>;