@batadata/cli 0.2.9 → 0.2.11
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 +41 -8
- package/dist/commands/powdb.d.ts +15 -1
- package/dist/commands/powdb.js +115 -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,8 +848,17 @@ 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
|
-
|
|
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 ` +
|
|
838
862
|
'platform scaffolding on the target. The restore will likely fail with "relation already ' +
|
|
839
863
|
'exists" — rename or exclude it on the source to migrate cleanly.');
|
|
840
864
|
}
|
|
@@ -1083,7 +1107,11 @@ export async function importDb(args) {
|
|
|
1083
1107
|
"FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')");
|
|
1084
1108
|
if (tableListRes.code !== 0)
|
|
1085
1109
|
verifyFailed = "could not list source tables";
|
|
1086
|
-
|
|
1110
|
+
// Scaffolding-SCHEMA tables were deliberately excluded from the dump (the
|
|
1111
|
+
// target engine maintains its own copies), so they must not be counted on the
|
|
1112
|
+
// source side either — they'd read as "missing on target" mismatches. Named
|
|
1113
|
+
// scaffolding objects (public.health_check) stay in: they ARE dumped.
|
|
1114
|
+
const sourceTables = (tableListRes.code === 0 ? safeJsonArray(tableListRes.stdout) : []).filter((t) => !PLATFORM_SCAFFOLDING_SCHEMAS.includes(t.schema));
|
|
1087
1115
|
const srcCounts = new Map();
|
|
1088
1116
|
const tgtCounts = new Map();
|
|
1089
1117
|
if (!verifyFailed && sourceTables.length > 0) {
|
|
@@ -1125,8 +1153,13 @@ export async function importDb(args) {
|
|
|
1125
1153
|
verifyFailed = "sequence query failed";
|
|
1126
1154
|
}
|
|
1127
1155
|
else {
|
|
1128
|
-
|
|
1129
|
-
|
|
1156
|
+
// Mirror the table filter: source sequences inside scaffolding schemas
|
|
1157
|
+
// were never dumped, so they can't be expected on the target.
|
|
1158
|
+
for (const [k, v] of parseKvRows(sSeqRes.stdout)) {
|
|
1159
|
+
const schema = k.slice(0, k.indexOf("."));
|
|
1160
|
+
if (!PLATFORM_SCAFFOLDING_SCHEMAS.includes(schema))
|
|
1161
|
+
srcSeq.set(k, v);
|
|
1162
|
+
}
|
|
1130
1163
|
for (const [k, v] of parseKvRows(tSeqRes.stdout))
|
|
1131
1164
|
tgtSeq.set(k, v);
|
|
1132
1165
|
}
|
package/dist/commands/powdb.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface TableSchema {
|
|
|
45
45
|
constraints: ConstraintInfo[];
|
|
46
46
|
}
|
|
47
47
|
/** The Stage-A PowDB types a static PowQL artifact can carry. */
|
|
48
|
-
export type PowType = "int" | "float" | "str" | "bool" | "datetime";
|
|
48
|
+
export type PowType = "int" | "float" | "str" | "bool" | "datetime" | "json";
|
|
49
49
|
export interface TypeMapping {
|
|
50
50
|
/** null → PowDB has no Stage-A representation; the column is DROPPED. */
|
|
51
51
|
powType: PowType | null;
|
|
@@ -133,8 +133,16 @@ export interface TablePlan {
|
|
|
133
133
|
pgName: string;
|
|
134
134
|
powName: string;
|
|
135
135
|
columns: ColumnPlan[];
|
|
136
|
+
/** powNames of carried single-column secondary (non-unique) indexes. */
|
|
137
|
+
secondaryIndexes: string[];
|
|
136
138
|
warnings: string[];
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract the single plain column of a non-unique btree index definition, or
|
|
142
|
+
* null when the index can't be carried (multi-column, expression, partial,
|
|
143
|
+
* non-btree). Exported for unit testing.
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseSingleColumnIndex(definition: string): string | null;
|
|
138
146
|
/**
|
|
139
147
|
* Build the per-table PowQL `type` plan from a Postgres table's schema,
|
|
140
148
|
* accumulating an honesty note for every column/constraint that can't be
|
|
@@ -143,6 +151,12 @@ export interface TablePlan {
|
|
|
143
151
|
export declare function planTable(t: TableSchema): TablePlan;
|
|
144
152
|
/** Render a table plan as a PowQL `type` DDL block. */
|
|
145
153
|
export declare function renderTypeDdl(plan: TablePlan): string;
|
|
154
|
+
/**
|
|
155
|
+
* Render a plan's carried secondary indexes as `alter T add index .col;`
|
|
156
|
+
* statements (PowQL DDL, valid since 0.1.1; keyword columns backtick-quote
|
|
157
|
+
* inside the path, e.g. `alter Post add index .\`order\``).
|
|
158
|
+
*/
|
|
159
|
+
export declare function renderIndexDdl(plan: TablePlan): string[];
|
|
146
160
|
export interface InsertResult {
|
|
147
161
|
statements: string[];
|
|
148
162
|
warnings: string[];
|
package/dist/commands/powdb.js
CHANGED
|
@@ -8,7 +8,7 @@ import { emitError, isRetryable } from "../utils/errors.js";
|
|
|
8
8
|
import { resolveProjectId, resolveBranchId } from "../link.js";
|
|
9
9
|
// PowQL reserved words a column/table name must not collide with. Mirrors the
|
|
10
10
|
// FULL engine lexer keyword set (crates/query/src/lexer.rs POWQL_KEYWORDS as
|
|
11
|
-
// of PowDB 0.
|
|
11
|
+
// of PowDB 0.13.0 — which added `raw`, `json_type`, `json_text`). A colliding name is
|
|
12
12
|
// emitted backtick-quoted (0.10+ syntax) rather than renamed, so the exported
|
|
13
13
|
// column keeps its source name. Keep this list in sync on PowDB upgrades: a
|
|
14
14
|
// missing word here means a bare identifier that fails to parse on load.
|
|
@@ -21,7 +21,8 @@ const POWQL_KEYWORDS = new Set([
|
|
|
21
21
|
"having", "in", "index", "inner", "insert", "is", "join", "left", "length",
|
|
22
22
|
"let", "like", "limit", "link", "lower", "match", "materialize",
|
|
23
23
|
"materialized", "max", "min", "multi", "not", "now", "null", "offset", "on",
|
|
24
|
-
"
|
|
24
|
+
"json_text", "json_type", "or", "order", "outer", "over", "partition",
|
|
25
|
+
"pow", "rank", "raw", "refresh",
|
|
25
26
|
"required", "returning", "right", "rollback", "round", "row_number",
|
|
26
27
|
"schema", "select", "sqrt", "substring", "sum", "then", "transaction",
|
|
27
28
|
"trim", "true", "type", "union", "unique", "update", "upper", "upsert",
|
|
@@ -75,9 +76,14 @@ export function mapPgType(dataType) {
|
|
|
75
76
|
if (t === "bytea") {
|
|
76
77
|
return { powType: "str", note: "bytea carried as hex text (str) — PowDB has no static bytes literal for a script load" };
|
|
77
78
|
}
|
|
78
|
-
// json/jsonb
|
|
79
|
+
// json/jsonb → native PowDB `json` (0.12+): canonical binary (PJ1), `->`
|
|
80
|
+
// path querying. Canonicalization is honest-but-lossy for exact text: object
|
|
81
|
+
// keys re-sort bytewise and duplicate keys collapse last-wins (same as pg jsonb).
|
|
79
82
|
if (t === "json" || t === "jsonb") {
|
|
80
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
powType: "json",
|
|
85
|
+
note: `${t} stored as native PowDB json (requires PowDB 0.12+; canonical form — key order normalized${t === "json" ? ", duplicate keys last-wins" : ""})`,
|
|
86
|
+
};
|
|
81
87
|
}
|
|
82
88
|
// Everything else (arrays, enums, ranges, geometry, network, tsvector,
|
|
83
89
|
// composite/user-defined) has no honest Stage-A representation.
|
|
@@ -183,6 +189,16 @@ export function powqlLiteral(value, powType) {
|
|
|
183
189
|
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
184
190
|
return { literal: `"${escapePowqlString(raw)}"`, risky: isLoadRiskyString(raw) };
|
|
185
191
|
}
|
|
192
|
+
case "json": {
|
|
193
|
+
// PowQL inserts JSON as a string literal (validated + canonicalized by
|
|
194
|
+
// the engine). Rows arrive as parsed JSON over SQL-over-HTTP, so
|
|
195
|
+
// re-stringify the VALUE — a JS string here is a JSON string scalar
|
|
196
|
+
// document and must be emitted quoted-inside-the-document.
|
|
197
|
+
const doc = JSON.stringify(value);
|
|
198
|
+
if (doc === undefined)
|
|
199
|
+
return { skip: true, reason: `value has no JSON representation` };
|
|
200
|
+
return { literal: `"${escapePowqlString(doc)}"`, risky: isLoadRiskyString(doc) };
|
|
201
|
+
}
|
|
186
202
|
}
|
|
187
203
|
}
|
|
188
204
|
/**
|
|
@@ -229,9 +245,49 @@ export function parsePgDefault(defaultValue, powType) {
|
|
|
229
245
|
return { kind: "literal", text: `"${escapePowqlString(m[1].replace(/''/g, "'"))}"` };
|
|
230
246
|
return { kind: "drop", note: `expression default dropped (${raw})` };
|
|
231
247
|
}
|
|
248
|
+
// json: a quoted literal default ('{}'::jsonb) carries as a string-literal
|
|
249
|
+
// default only if its content is valid JSON — the engine would reject the
|
|
250
|
+
// whole type DDL otherwise.
|
|
251
|
+
if (powType === "json") {
|
|
252
|
+
const m = /^'([\s\S]*)'$/.exec(noCast);
|
|
253
|
+
if (m) {
|
|
254
|
+
const text = m[1].replace(/''/g, "'");
|
|
255
|
+
try {
|
|
256
|
+
JSON.parse(text);
|
|
257
|
+
return { kind: "literal", text: `"${escapePowqlString(text)}"` };
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return { kind: "drop", note: `non-JSON default on json column dropped (${raw})` };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { kind: "drop", note: `expression default dropped (${raw})` };
|
|
264
|
+
}
|
|
232
265
|
// datetime defaults are expressions (now(), CURRENT_TIMESTAMP) — never scalar.
|
|
233
266
|
return { kind: "drop", note: `expression default dropped (${raw})` };
|
|
234
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Extract the single plain column of a non-unique btree index definition, or
|
|
270
|
+
* null when the index can't be carried (multi-column, expression, partial,
|
|
271
|
+
* non-btree). Exported for unit testing.
|
|
272
|
+
*/
|
|
273
|
+
export function parseSingleColumnIndex(definition) {
|
|
274
|
+
if (/\bWHERE\b/i.test(definition))
|
|
275
|
+
return null; // partial
|
|
276
|
+
if (/\bINCLUDE\b/i.test(definition))
|
|
277
|
+
return null; // covering — the trailing parens are the INCLUDE list, not the key
|
|
278
|
+
if (/\bUSING\s+(?!btree\b)/i.test(definition))
|
|
279
|
+
return null; // gin/gist/hash/...
|
|
280
|
+
const m = /\(([^()]*)\)\s*$/.exec(definition);
|
|
281
|
+
if (!m)
|
|
282
|
+
return null; // expression index
|
|
283
|
+
const cols = m[1].split(",");
|
|
284
|
+
if (cols.length !== 1)
|
|
285
|
+
return null; // multi-column
|
|
286
|
+
const col = /^\s*(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_$]*))\s*(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST))?\s*$/i.exec(cols[0]);
|
|
287
|
+
if (!col)
|
|
288
|
+
return null;
|
|
289
|
+
return col[1] ?? col[2];
|
|
290
|
+
}
|
|
235
291
|
/**
|
|
236
292
|
* Build the per-table PowQL `type` plan from a Postgres table's schema,
|
|
237
293
|
* accumulating an honesty note for every column/constraint that can't be
|
|
@@ -264,14 +320,30 @@ export function planTable(t) {
|
|
|
264
320
|
warnings.push(`foreign key ${c.name} → ${c.foreignTableName ?? "?"} dropped — PowDB Stage A has no foreign keys`);
|
|
265
321
|
}
|
|
266
322
|
else if (type.includes("CHECK")) {
|
|
323
|
+
// PG17+ surfaces NOT NULL as synthetic check constraints named
|
|
324
|
+
// <reloid>_<attnum>_..._not_null (all-numeric prefix). Those ARE carried
|
|
325
|
+
// (as `required`), so warning about them would be noise — but a
|
|
326
|
+
// USER-named check that merely ends in _not_null must still warn.
|
|
327
|
+
if (/^\d+(_\d+)*_not_null$/.test(c.name))
|
|
328
|
+
continue;
|
|
267
329
|
warnings.push(`check constraint ${c.name} dropped — PowDB Stage A has no check constraints`);
|
|
268
330
|
}
|
|
269
331
|
}
|
|
270
|
-
//
|
|
271
|
-
//
|
|
332
|
+
// Single-column plain btree indexes carry as `alter T add index .col`
|
|
333
|
+
// statements (resolved against carried columns after the column loop below);
|
|
334
|
+
// anything else (multi-column, expression, partial, non-btree) is warned.
|
|
335
|
+
const pendingIndexCols = [];
|
|
272
336
|
for (const idx of t.indexes ?? []) {
|
|
273
|
-
|
|
274
|
-
|
|
337
|
+
// Only a leading CREATE UNIQUE INDEX is a unique index — a bare substring
|
|
338
|
+
// match would silently skip an index whose NAME merely contains "unique".
|
|
339
|
+
if (/^\s*CREATE\s+UNIQUE\s+INDEX\b/i.test(idx.definition))
|
|
340
|
+
continue;
|
|
341
|
+
const pgCol = parseSingleColumnIndex(idx.definition);
|
|
342
|
+
if (pgCol) {
|
|
343
|
+
pendingIndexCols.push({ idxName: idx.name, pgCol });
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
warnings.push(`index ${idx.name} not carried — only single-column plain btree indexes map to PowDB \`add index\``);
|
|
275
347
|
}
|
|
276
348
|
}
|
|
277
349
|
const columns = [];
|
|
@@ -312,12 +384,27 @@ export function planTable(t) {
|
|
|
312
384
|
defaultLiteral: auto ? null : defaultLiteral,
|
|
313
385
|
});
|
|
314
386
|
}
|
|
387
|
+
// Resolve carried secondary indexes against the columns that survived the
|
|
388
|
+
// mapping; an index on a dropped column is warned, not silently lost.
|
|
389
|
+
const byPgName = new Map(columns.map((c) => [c.pgName, c]));
|
|
390
|
+
const secondaryIndexes = [];
|
|
391
|
+
for (const { idxName, pgCol } of pendingIndexCols) {
|
|
392
|
+
const col = byPgName.get(pgCol);
|
|
393
|
+
if (!col) {
|
|
394
|
+
warnings.push(`index ${idxName} not carried — its column ${pgCol} was not representable`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (col.unique)
|
|
398
|
+
continue; // already an index via the unique modifier
|
|
399
|
+
secondaryIndexes.push(col.powName);
|
|
400
|
+
}
|
|
315
401
|
return {
|
|
316
402
|
pgQualified: `"${t.schema}"."${t.name}"`,
|
|
317
403
|
pgSchema: t.schema,
|
|
318
404
|
pgName: t.name,
|
|
319
405
|
powName: tPowName,
|
|
320
406
|
columns,
|
|
407
|
+
secondaryIndexes,
|
|
321
408
|
warnings,
|
|
322
409
|
};
|
|
323
410
|
}
|
|
@@ -340,6 +427,14 @@ export function renderTypeDdl(plan) {
|
|
|
340
427
|
lines.push("}");
|
|
341
428
|
return lines.join("\n");
|
|
342
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Render a plan's carried secondary indexes as `alter T add index .col;`
|
|
432
|
+
* statements (PowQL DDL, valid since 0.1.1; keyword columns backtick-quote
|
|
433
|
+
* inside the path, e.g. `alter Post add index .\`order\``).
|
|
434
|
+
*/
|
|
435
|
+
export function renderIndexDdl(plan) {
|
|
436
|
+
return plan.secondaryIndexes.map((powName) => `alter ${renderPowqlIdent(plan.powName)} add index .${renderPowqlIdent(powName)};`);
|
|
437
|
+
}
|
|
343
438
|
/**
|
|
344
439
|
* Render rows for one table as PowQL `insert` statements against its plan.
|
|
345
440
|
* Values that can't be represented are omitted (the field falls back to
|
|
@@ -499,6 +594,7 @@ async function powdbPull(args) {
|
|
|
499
594
|
const reports = [];
|
|
500
595
|
let totalRows = 0;
|
|
501
596
|
let anyLoadRisk = false;
|
|
597
|
+
let anyJson = false;
|
|
502
598
|
for (const t of tables) {
|
|
503
599
|
s?.update(`Exporting ${t.schema}.${t.name}`);
|
|
504
600
|
const plan = planTable(t);
|
|
@@ -522,6 +618,8 @@ async function powdbPull(args) {
|
|
|
522
618
|
}
|
|
523
619
|
if (inserts.loadRisk)
|
|
524
620
|
anyLoadRisk = true;
|
|
621
|
+
if (plan.columns.some((c) => c.powType === "json"))
|
|
622
|
+
anyJson = true;
|
|
525
623
|
// Emit the type DDL, its honesty notes, and the inserts for this table.
|
|
526
624
|
// Comments are `;`/newline-sanitized so they survive the `--exec` split.
|
|
527
625
|
parts.push(commentSafe(`# ── ${t.schema}.${t.name} → ${plan.powName} (${inserts.statements.length} row(s)) ──`));
|
|
@@ -533,6 +631,9 @@ async function powdbPull(args) {
|
|
|
533
631
|
}
|
|
534
632
|
else {
|
|
535
633
|
parts.push(ddl + ";");
|
|
634
|
+
const indexDdl = renderIndexDdl(plan);
|
|
635
|
+
if (indexDdl.length)
|
|
636
|
+
parts.push(indexDdl.join("\n"));
|
|
536
637
|
parts.push("");
|
|
537
638
|
if (inserts.statements.length) {
|
|
538
639
|
parts.push(inserts.statements.join("\n"));
|
|
@@ -557,6 +658,7 @@ async function powdbPull(args) {
|
|
|
557
658
|
tableCount: tables.length,
|
|
558
659
|
rowCount: totalRows,
|
|
559
660
|
loadRisk: anyLoadRisk,
|
|
661
|
+
usesJson: anyJson,
|
|
560
662
|
limit,
|
|
561
663
|
});
|
|
562
664
|
const artifact = header + "\n" + parts.join("\n") + "\n";
|
|
@@ -629,7 +731,9 @@ function buildArtifactHeader(o) {
|
|
|
629
731
|
// which lexes as two minus signs and fails to parse).
|
|
630
732
|
const lines = [
|
|
631
733
|
"# PowDB Stage A export — generated by `bata powdb pull`",
|
|
632
|
-
|
|
734
|
+
o.usesJson
|
|
735
|
+
? "# Target: PowDB 0.12+ (native json columns; reserved identifiers are backtick-quoted)"
|
|
736
|
+
: "# Target: PowDB 0.10+ (reserved identifiers are backtick-quoted)",
|
|
633
737
|
"#",
|
|
634
738
|
`# Source project : ${o.projectName} (${o.projectId})`,
|
|
635
739
|
`# Source branch : ${o.branchName} (${o.branchId})`,
|
|
@@ -642,7 +746,8 @@ function buildArtifactHeader(o) {
|
|
|
642
746
|
"#",
|
|
643
747
|
"# COMPAT-HONEST: `# WARN:` lines below name every Postgres feature that",
|
|
644
748
|
"# PowDB Stage A cannot carry faithfully (foreign keys, check constraints,",
|
|
645
|
-
"#
|
|
749
|
+
"# multi-column/expression/partial indexes, unmapped types, expression",
|
|
750
|
+
"# defaults, lossy numerics).",
|
|
646
751
|
"# PowDB is NOT Postgres-compatible beyond this mapping; nothing here claims",
|
|
647
752
|
"# otherwise, and no engine-speed comparison is implied.",
|
|
648
753
|
];
|
|
@@ -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();
|