@batadata/cli 0.2.10 → 0.2.12
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/dist/commands/import.d.ts +12 -1
- package/dist/commands/import.js +65 -10
- package/dist/commands/projects.d.ts +1 -0
- package/dist/commands/projects.js +21 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -136,7 +136,18 @@ export declare function missingExtensions(sourceExts: string[], targetHasExts: s
|
|
|
136
136
|
* compute_ctl records applied internal SQL migrations here (table
|
|
137
137
|
* `neon_migration.migration_id`) on EVERY compute startup. Real Neon hides this
|
|
138
138
|
* schema from customer `pg_tables`; our compute exposes it, so ANY object under
|
|
139
|
-
* `neon_migration.*` is platform furniture, never user data.
|
|
139
|
+
* `neon_migration.*` is platform furniture, never user data. Same story for
|
|
140
|
+
* `neon` — compute_ctl seeds it with engine helper functions on every startup,
|
|
141
|
+
* so a BataDB source's dump would carry `CREATE SCHEMA neon` straight into the
|
|
142
|
+
* identical schema every target is provisioned with.
|
|
143
|
+
*
|
|
144
|
+
* Named objects are treated differently from whole schemas. A source
|
|
145
|
+
* `public.health_check` is still dumped and verified as real data (colliding
|
|
146
|
+
* loudly on the target rather than being silently dropped). Scaffolding SCHEMAS,
|
|
147
|
+
* by contrast, are engine furniture on the source too whenever they exist, so
|
|
148
|
+
* they are excluded from the dump itself (pg_dump --exclude-schema) and from
|
|
149
|
+
* source-side verify accounting — the target's own engine already maintains its
|
|
150
|
+
* copies, and restoring them is guaranteed to fail with "already exists".
|
|
140
151
|
*/
|
|
141
152
|
export declare const PLATFORM_SCAFFOLDING_TABLES: string[];
|
|
142
153
|
export declare const PLATFORM_SCAFFOLDING_SEQUENCES: string[];
|
package/dist/commands/import.js
CHANGED
|
@@ -273,12 +273,23 @@ export function missingExtensions(sourceExts, targetHasExts) {
|
|
|
273
273
|
* compute_ctl records applied internal SQL migrations here (table
|
|
274
274
|
* `neon_migration.migration_id`) on EVERY compute startup. Real Neon hides this
|
|
275
275
|
* schema from customer `pg_tables`; our compute exposes it, so ANY object under
|
|
276
|
-
* `neon_migration.*` is platform furniture, never user data.
|
|
276
|
+
* `neon_migration.*` is platform furniture, never user data. Same story for
|
|
277
|
+
* `neon` — compute_ctl seeds it with engine helper functions on every startup,
|
|
278
|
+
* so a BataDB source's dump would carry `CREATE SCHEMA neon` straight into the
|
|
279
|
+
* identical schema every target is provisioned with.
|
|
280
|
+
*
|
|
281
|
+
* Named objects are treated differently from whole schemas. A source
|
|
282
|
+
* `public.health_check` is still dumped and verified as real data (colliding
|
|
283
|
+
* loudly on the target rather than being silently dropped). Scaffolding SCHEMAS,
|
|
284
|
+
* by contrast, are engine furniture on the source too whenever they exist, so
|
|
285
|
+
* they are excluded from the dump itself (pg_dump --exclude-schema) and from
|
|
286
|
+
* source-side verify accounting — the target's own engine already maintains its
|
|
287
|
+
* copies, and restoring them is guaranteed to fail with "already exists".
|
|
277
288
|
*/
|
|
278
289
|
export const PLATFORM_SCAFFOLDING_TABLES = ["public.health_check"];
|
|
279
290
|
export const PLATFORM_SCAFFOLDING_SEQUENCES = ["public.health_check_id_seq"];
|
|
280
291
|
/** Schemas whose ENTIRE contents are platform furniture (any `<schema>.*` object). */
|
|
281
|
-
export const PLATFORM_SCAFFOLDING_SCHEMAS = ["neon_migration"];
|
|
292
|
+
export const PLATFORM_SCAFFOLDING_SCHEMAS = ["neon_migration", "neon"];
|
|
282
293
|
let PLATFORM_SCAFFOLDING_NAMES = new Set([
|
|
283
294
|
...PLATFORM_SCAFFOLDING_TABLES,
|
|
284
295
|
...PLATFORM_SCAFFOLDING_SEQUENCES,
|
|
@@ -432,7 +443,11 @@ function runMigration(sourceUri, targetUri) {
|
|
|
432
443
|
// data corruption that row-count verify wouldn't catch. Forcing UTF8 makes the
|
|
433
444
|
// server convert, guaranteeing a valid-UTF-8 stream; the dump carries
|
|
434
445
|
// `SET client_encoding = 'UTF8';`, which restores correctly into any target.
|
|
435
|
-
|
|
446
|
+
// Scaffolding SCHEMAS are excluded at the dump: every target is provisioned
|
|
447
|
+
// with its own copies, so restoring them can only fail ("already exists").
|
|
448
|
+
// Read at call time — applyServerScaffolding() may have extended the list.
|
|
449
|
+
const excludeArgs = PLATFORM_SCAFFOLDING_SCHEMAS.flatMap((s) => ["--exclude-schema", s]);
|
|
450
|
+
const dump = spawn("pg_dump", [sourceUri, "--no-owner", "--no-privileges", "--encoding=UTF8", ...excludeArgs], {
|
|
436
451
|
stdio: ["ignore", "pipe", "pipe"],
|
|
437
452
|
env,
|
|
438
453
|
});
|
|
@@ -833,10 +848,20 @@ export async function importDb(args) {
|
|
|
833
848
|
const sourceScaffoldingHits = srcCollisionRes.code === 0
|
|
834
849
|
? scaffoldingCollisions(srcCollisionRes.stdout.split("\n").map((s) => s.trim()).filter(Boolean))
|
|
835
850
|
: [];
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
851
|
+
// Schema-level hits are engine furniture and are EXCLUDED from the dump
|
|
852
|
+
// outright (see runMigration); only named-object collisions still restore
|
|
853
|
+
// into a clash on the target.
|
|
854
|
+
const schemaHits = sourceScaffoldingHits.filter((n) => PLATFORM_SCAFFOLDING_SCHEMAS.some((s) => n.startsWith(`${s}.`)));
|
|
855
|
+
const namedHits = sourceScaffoldingHits.filter((n) => !schemaHits.includes(n));
|
|
856
|
+
if (schemaHits.length > 0 && !jsonMode) {
|
|
857
|
+
warn(`Source contains engine-internal object(s) ${schemaHits.join(", ")} — platform furniture the ` +
|
|
858
|
+
"target maintains itself. They are skipped by the migration (not customer data).");
|
|
859
|
+
}
|
|
860
|
+
if (namedHits.length > 0 && !jsonMode) {
|
|
861
|
+
warn(`Source contains ${namedHits.join(", ")}, which shares a name with BataDB's seeded ` +
|
|
862
|
+
"platform scaffolding. On a freshly created target the seeded copy is dropped so the " +
|
|
863
|
+
"source's version restores as real data; importing into an existing --project target " +
|
|
864
|
+
'will likely fail with "relation already exists".');
|
|
840
865
|
}
|
|
841
866
|
// ── Phase 2: check pg_dump AND psql are new enough ──────────────────────────
|
|
842
867
|
// BOTH matter: pg_dump can't dump a newer server than itself, and psql must be
|
|
@@ -1048,6 +1073,27 @@ export async function importDb(args) {
|
|
|
1048
1073
|
const strandedHintJson = "The target project was left in place (NOT deleted) for diagnosis/retry — re-run with " +
|
|
1049
1074
|
`--project ${targetProjectId} --yes after fixing` +
|
|
1050
1075
|
(created ? `, or remove it with: bata projects delete ${targetProjectId} --yes.` : ".");
|
|
1076
|
+
// A freshly CREATED target's named scaffolding (public.health_check + its
|
|
1077
|
+
// sequence) is disposable furniture — but the SOURCE may carry same-named
|
|
1078
|
+
// tables (always true migrating FROM another BataDB, where provisioning
|
|
1079
|
+
// seeded them too). Doctrine says source objects are real data and must
|
|
1080
|
+
// restore + verify normally, so drop the target's seeded copies first;
|
|
1081
|
+
// otherwise the restore is GUARANTEED to die on "already exists". Never
|
|
1082
|
+
// touched on a --project target (created === false): its objects may be the
|
|
1083
|
+
// user's own data, and the old collide-loudly behavior stays.
|
|
1084
|
+
if (created && namedHits.length > 0) {
|
|
1085
|
+
phase("Dropping seeded scaffolding the source will re-create");
|
|
1086
|
+
for (const qn of namedHits) {
|
|
1087
|
+
const dot = qn.indexOf(".");
|
|
1088
|
+
const dropSql = `DROP TABLE IF EXISTS ${quoteIdent(qn.slice(0, dot))}.${quoteIdent(qn.slice(dot + 1))} CASCADE`;
|
|
1089
|
+
const dropRes = runPsql(targetUri, dropSql);
|
|
1090
|
+
if (dropRes.code !== 0) {
|
|
1091
|
+
emitError("RESTORE_FAILED", `Could not clear the target's seeded ${qn} before restore.`, `The source has its own ${qn}, which cannot restore over the target's seeded copy. ` +
|
|
1092
|
+
strandedHintJson, 1);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1051
1097
|
// ── Phase 4: migrate ───────────────────────────────────────────────────────
|
|
1052
1098
|
phase("Migrating schema + data (pg_dump | psql)");
|
|
1053
1099
|
const migration = await runMigration(source, targetUri);
|
|
@@ -1083,7 +1129,11 @@ export async function importDb(args) {
|
|
|
1083
1129
|
"FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')");
|
|
1084
1130
|
if (tableListRes.code !== 0)
|
|
1085
1131
|
verifyFailed = "could not list source tables";
|
|
1086
|
-
|
|
1132
|
+
// Scaffolding-SCHEMA tables were deliberately excluded from the dump (the
|
|
1133
|
+
// target engine maintains its own copies), so they must not be counted on the
|
|
1134
|
+
// source side either — they'd read as "missing on target" mismatches. Named
|
|
1135
|
+
// scaffolding objects (public.health_check) stay in: they ARE dumped.
|
|
1136
|
+
const sourceTables = (tableListRes.code === 0 ? safeJsonArray(tableListRes.stdout) : []).filter((t) => !PLATFORM_SCAFFOLDING_SCHEMAS.includes(t.schema));
|
|
1087
1137
|
const srcCounts = new Map();
|
|
1088
1138
|
const tgtCounts = new Map();
|
|
1089
1139
|
if (!verifyFailed && sourceTables.length > 0) {
|
|
@@ -1125,8 +1175,13 @@ export async function importDb(args) {
|
|
|
1125
1175
|
verifyFailed = "sequence query failed";
|
|
1126
1176
|
}
|
|
1127
1177
|
else {
|
|
1128
|
-
|
|
1129
|
-
|
|
1178
|
+
// Mirror the table filter: source sequences inside scaffolding schemas
|
|
1179
|
+
// were never dumped, so they can't be expected on the target.
|
|
1180
|
+
for (const [k, v] of parseKvRows(sSeqRes.stdout)) {
|
|
1181
|
+
const schema = k.slice(0, k.indexOf("."));
|
|
1182
|
+
if (!PLATFORM_SCAFFOLDING_SCHEMAS.includes(schema))
|
|
1183
|
+
srcSeq.set(k, v);
|
|
1184
|
+
}
|
|
1130
1185
|
for (const [k, v] of parseKvRows(tSeqRes.stdout))
|
|
1131
1186
|
tgtSeq.set(k, v);
|
|
1132
1187
|
}
|
|
@@ -108,18 +108,21 @@ export async function list() {
|
|
|
108
108
|
export function parseCreateComputeArgs(args) {
|
|
109
109
|
let tierRaw;
|
|
110
110
|
let sizeRaw;
|
|
111
|
+
let placementRaw;
|
|
111
112
|
for (let i = 0; i < args.length; i++) {
|
|
112
113
|
const arg = args[i];
|
|
113
|
-
if (arg === "--tier" || arg === "--size") {
|
|
114
|
+
if (arg === "--tier" || arg === "--size" || arg === "--placement") {
|
|
114
115
|
// The spaced form needs a value: reject a missing one (end of args, or
|
|
115
116
|
// the next token is another flag) instead of silently using the default.
|
|
116
117
|
const next = args[i + 1];
|
|
117
118
|
if (next === undefined || next.startsWith("-")) {
|
|
118
|
-
const usage = arg === "--tier" ? "serverless|always-on" : "1|2|4|8|16|32|64|128";
|
|
119
|
+
const usage = arg === "--tier" ? "serverless|always-on" : arg === "--placement" ? "fly|density" : "1|2|4|8|16|32|64|128";
|
|
119
120
|
return { error: `${arg} requires a value (${usage}).` };
|
|
120
121
|
}
|
|
121
122
|
if (arg === "--tier")
|
|
122
123
|
tierRaw = next;
|
|
124
|
+
else if (arg === "--placement")
|
|
125
|
+
placementRaw = next;
|
|
123
126
|
else
|
|
124
127
|
sizeRaw = next;
|
|
125
128
|
i++;
|
|
@@ -128,6 +131,8 @@ export function parseCreateComputeArgs(args) {
|
|
|
128
131
|
tierRaw = arg.slice("--tier=".length);
|
|
129
132
|
else if (arg.startsWith("--size="))
|
|
130
133
|
sizeRaw = arg.slice("--size=".length);
|
|
134
|
+
else if (arg.startsWith("--placement="))
|
|
135
|
+
placementRaw = arg.slice("--placement=".length);
|
|
131
136
|
}
|
|
132
137
|
let tier = "serverless";
|
|
133
138
|
if (tierRaw !== undefined) {
|
|
@@ -158,7 +163,19 @@ export function parseCreateComputeArgs(args) {
|
|
|
158
163
|
}
|
|
159
164
|
sizeCu = parsed;
|
|
160
165
|
}
|
|
161
|
-
|
|
166
|
+
let placement;
|
|
167
|
+
if (placementRaw !== undefined) {
|
|
168
|
+
const normalized = placementRaw.toLowerCase();
|
|
169
|
+
if (normalized === "fly" || normalized === "density")
|
|
170
|
+
placement = normalized;
|
|
171
|
+
else
|
|
172
|
+
return { error: `Invalid --placement "${placementRaw}". Use "fly" or "density".` };
|
|
173
|
+
// Placement pins a SERVERLESS compute's provider; always-on is Fly always.
|
|
174
|
+
if (tier === "always_on") {
|
|
175
|
+
return { error: `--placement applies to serverless projects only (always-on runs on Fly).` };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { tier, sizeCu, ...(placement ? { placement } : {}) };
|
|
162
179
|
}
|
|
163
180
|
/** `--flag <value>` / `--flag=<value>` reader. Distinguishes "absent"
|
|
164
181
|
* (undefined) from "present but missing its value" ({ error }) so a headless
|
|
@@ -275,6 +292,7 @@ export async function create(args = []) {
|
|
|
275
292
|
// One friendly step: the compute is born on the chosen tier + size (no
|
|
276
293
|
// follow-up `bata compute set` needed). Defaults to serverless / 1 CU.
|
|
277
294
|
compute: { tier: compute.tier, size_cu: compute.sizeCu },
|
|
295
|
+
...(compute.placement ? { placement: compute.placement } : {}),
|
|
278
296
|
};
|
|
279
297
|
if (teamId) {
|
|
280
298
|
body.team_id = teamId;
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ function help() {
|
|
|
51
51
|
log(` ${colors.bold("Projects")}`);
|
|
52
52
|
log(` ${colors.cyan("projects")} List all projects`);
|
|
53
53
|
log(` ${colors.cyan("projects list")} Alias for status`);
|
|
54
|
-
log(` ${colors.cyan("projects create")} Create a new project ${colors.dim("(--tier serverless|always-on, --size 1|2|4|8|16|32|64|128)")}`);
|
|
54
|
+
log(` ${colors.cyan("projects create")} Create a new project ${colors.dim("(--tier serverless|always-on, --size 1|2|4|8|16|32|64|128, --placement fly|density)")}`);
|
|
55
55
|
log(` ${colors.cyan("projects info")} Show project details`);
|
|
56
56
|
log(` ${colors.cyan("projects delete")} Delete a project`);
|
|
57
57
|
log();
|