@batadata/cli 0.2.7 → 0.2.9

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.
@@ -37,7 +37,7 @@ interface IndexInfo {
37
37
  name: string;
38
38
  definition: string;
39
39
  }
40
- interface TableSchema {
40
+ export interface TableSchema {
41
41
  schema: string;
42
42
  name: string;
43
43
  columns: ColumnInfo[];
@@ -62,14 +62,22 @@ export declare function mapPgType(dataType: string): TypeMapping;
62
62
  /**
63
63
  * Sanitize a Postgres identifier into a valid PowQL identifier (leading
64
64
  * alpha/underscore, then alphanumeric/underscore). Reports whether it had to be
65
- * changed and whether it collides with a PowQL keyword, so the caller can flag
66
- * the rename honestly rather than emit a name that means something else.
65
+ * changed and whether it collides with a PowQL keyword. A keyword collision is
66
+ * NOT renamed the name is kept and `renderPowqlIdent` backtick-quotes it at
67
+ * emit time (PowDB 0.10+ syntax), so exported columns keep their source names.
67
68
  */
68
69
  export declare function powqlIdentifier(name: string): {
69
70
  ident: string;
70
71
  changed: boolean;
71
72
  keyword: boolean;
72
73
  };
74
+ /**
75
+ * Render an identifier for emitted PowQL: backtick-quoted when it collides
76
+ * with a reserved word (the 0.10 lexer accepts `` `name` `` in every
77
+ * identifier position), bare otherwise. Identifiers are pre-sanitized by
78
+ * `powqlIdentifier`, so no other escaping is needed inside the backticks.
79
+ */
80
+ export declare function renderPowqlIdent(ident: string): string;
73
81
  /**
74
82
  * Escape a string for a PowQL double-quoted literal. The engine lexer honors
75
83
  * `\" \\ \n \t`; every other character passes through verbatim. Returns the
@@ -3,16 +3,29 @@ import * as path from "node:path";
3
3
  import { api, apiError } from "../api.js";
4
4
  import { requireToken, isJsonMode, loadConfig } from "../config.js";
5
5
  import { colors, log, json, spinner, heading, table } from "../utils/logger.js";
6
+ import { confirmDestructive } from "../utils/prompts.js";
6
7
  import { emitError, isRetryable } from "../utils/errors.js";
7
8
  import { resolveProjectId, resolveBranchId } from "../link.js";
8
- // PowQL reserved words a column/table name must not collide with (from the
9
- // engine lexer keyword set). A colliding name is renamed and flagged rather
10
- // than silently producing a broken `type`/`insert`.
9
+ // PowQL reserved words a column/table name must not collide with. Mirrors the
10
+ // FULL engine lexer keyword set (crates/query/src/lexer.rs POWQL_KEYWORDS as
11
+ // of PowDB 0.10.0 which added `schema`/`describe`). A colliding name is
12
+ // emitted backtick-quoted (0.10+ syntax) rather than renamed, so the exported
13
+ // column keeps its source name. Keep this list in sync on PowDB upgrades: a
14
+ // missing word here means a bare identifier that fails to parse on load.
11
15
  const POWQL_KEYWORDS = new Set([
12
- "type", "filter", "order", "limit", "offset", "insert", "update", "delete",
13
- "default", "upsert", "returning", "group", "having", "distinct", "and", "or",
14
- "not", "is", "null", "true", "false", "asc", "desc", "like", "in", "between",
15
- "required", "unique", "auto", "count", "sum", "avg", "min", "max",
16
+ "abs", "add", "alter", "and", "as", "asc", "auto", "avg", "begin",
17
+ "between", "case", "cast", "ceil", "column", "commit", "concat", "conflict",
18
+ "count", "cross", "date_add", "date_diff", "default", "delete",
19
+ "dense_rank", "desc", "describe", "distinct", "drop", "else", "end",
20
+ "exists", "explain", "extract", "false", "filter", "floor", "group",
21
+ "having", "in", "index", "inner", "insert", "is", "join", "left", "length",
22
+ "let", "like", "limit", "link", "lower", "match", "materialize",
23
+ "materialized", "max", "min", "multi", "not", "now", "null", "offset", "on",
24
+ "or", "order", "outer", "over", "partition", "pow", "rank", "refresh",
25
+ "required", "returning", "right", "rollback", "round", "row_number",
26
+ "schema", "select", "sqrt", "substring", "sum", "then", "transaction",
27
+ "trim", "true", "type", "union", "unique", "update", "upper", "upsert",
28
+ "view", "when",
16
29
  ]);
17
30
  /**
18
31
  * Map a Postgres data type (as reported by schema introspection) to the PowDB
@@ -73,8 +86,9 @@ export function mapPgType(dataType) {
73
86
  /**
74
87
  * Sanitize a Postgres identifier into a valid PowQL identifier (leading
75
88
  * alpha/underscore, then alphanumeric/underscore). Reports whether it had to be
76
- * changed and whether it collides with a PowQL keyword, so the caller can flag
77
- * the rename honestly rather than emit a name that means something else.
89
+ * changed and whether it collides with a PowQL keyword. A keyword collision is
90
+ * NOT renamed the name is kept and `renderPowqlIdent` backtick-quotes it at
91
+ * emit time (PowDB 0.10+ syntax), so exported columns keep their source names.
78
92
  */
79
93
  export function powqlIdentifier(name) {
80
94
  let ident = name.replace(/[^A-Za-z0-9_]/g, "_");
@@ -82,11 +96,17 @@ export function powqlIdentifier(name) {
82
96
  ident = "_" + ident;
83
97
  }
84
98
  const keyword = POWQL_KEYWORDS.has(ident.toLowerCase());
85
- if (keyword) {
86
- ident = ident + "_";
87
- }
88
99
  return { ident, changed: ident !== name, keyword };
89
100
  }
101
+ /**
102
+ * Render an identifier for emitted PowQL: backtick-quoted when it collides
103
+ * with a reserved word (the 0.10 lexer accepts `` `name` `` in every
104
+ * identifier position), bare otherwise. Identifiers are pre-sanitized by
105
+ * `powqlIdentifier`, so no other escaping is needed inside the backticks.
106
+ */
107
+ export function renderPowqlIdent(ident) {
108
+ return POWQL_KEYWORDS.has(ident.toLowerCase()) ? `\`${ident}\`` : ident;
109
+ }
90
110
  /**
91
111
  * Escape a string for a PowQL double-quoted literal. The engine lexer honors
92
112
  * `\" \\ \n \t`; every other character passes through verbatim. Returns the
@@ -221,7 +241,10 @@ export function planTable(t) {
221
241
  const warnings = [];
222
242
  const { ident: tPowName, changed: nameChanged, keyword } = powqlIdentifier(t.name);
223
243
  if (nameChanged) {
224
- warnings.push(`table renamed to \`${tPowName}\`${keyword ? " (collides with a PowQL keyword)" : " (name not a valid PowQL identifier)"}`);
244
+ warnings.push(`table renamed to \`${tPowName}\` (name not a valid PowQL identifier)`);
245
+ }
246
+ else if (keyword) {
247
+ warnings.push(`table name "${t.name}" is a PowQL reserved word — emitted backtick-quoted (requires PowDB 0.10+)`);
225
248
  }
226
249
  // Single-column UNIQUE constraints → the PowQL `unique` modifier.
227
250
  const singleUniqueCols = new Set();
@@ -263,7 +286,10 @@ export function planTable(t) {
263
286
  }
264
287
  const { ident: colPowName, changed: colChanged, keyword: colKeyword } = powqlIdentifier(col.name);
265
288
  if (colChanged) {
266
- warnings.push(`column ${col.name} renamed to \`${colPowName}\`${colKeyword ? " (collides with a PowQL keyword)" : ""}`);
289
+ warnings.push(`column ${col.name} renamed to \`${colPowName}\` (name not a valid PowQL identifier)`);
290
+ }
291
+ else if (colKeyword) {
292
+ warnings.push(`column "${col.name}" is a PowQL reserved word — emitted backtick-quoted (requires PowDB 0.10+)`);
267
293
  }
268
294
  const def = parsePgDefault(col.defaultValue, mapping.powType);
269
295
  let auto = false;
@@ -297,7 +323,7 @@ export function planTable(t) {
297
323
  }
298
324
  /** Render a table plan as a PowQL `type` DDL block. */
299
325
  export function renderTypeDdl(plan) {
300
- const lines = [`type ${plan.powName} {`];
326
+ const lines = [`type ${renderPowqlIdent(plan.powName)} {`];
301
327
  const fieldLines = plan.columns.map((c) => {
302
328
  const mods = [];
303
329
  if (c.required)
@@ -308,7 +334,7 @@ export function renderTypeDdl(plan) {
308
334
  mods.push("auto");
309
335
  const prefix = mods.length ? mods.join(" ") + " " : "";
310
336
  const def = c.defaultLiteral ? ` default ${c.defaultLiteral}` : "";
311
- return ` ${prefix}${c.powName}: ${c.powType}${def}`;
337
+ return ` ${prefix}${renderPowqlIdent(c.powName)}: ${c.powType}${def}`;
312
338
  });
313
339
  lines.push(fieldLines.join(",\n"));
314
340
  lines.push("}");
@@ -337,14 +363,14 @@ export function buildInserts(plan, rows) {
337
363
  }
338
364
  if (res.risky)
339
365
  loadRisk = true;
340
- assigns.push(`${col.powName} := ${res.literal}`);
366
+ assigns.push(`${renderPowqlIdent(col.powName)} := ${res.literal}`);
341
367
  anyField = true;
342
368
  }
343
369
  if (!anyField) {
344
370
  skippedRows++;
345
371
  continue;
346
372
  }
347
- statements.push(`insert ${plan.powName} { ${assigns.join(", ")} };`);
373
+ statements.push(`insert ${renderPowqlIdent(plan.powName)} { ${assigns.join(", ")} };`);
348
374
  }
349
375
  const warnings = [];
350
376
  for (const [col, reason] of skipNotes) {
@@ -603,6 +629,7 @@ function buildArtifactHeader(o) {
603
629
  // which lexes as two minus signs and fails to parse).
604
630
  const lines = [
605
631
  "# PowDB Stage A export — generated by `bata powdb pull`",
632
+ "# Target: PowDB 0.10+ (reserved identifiers are backtick-quoted)",
606
633
  "#",
607
634
  `# Source project : ${o.projectName} (${o.projectId})`,
608
635
  `# Source branch : ${o.branchName} (${o.branchId})`,
@@ -834,7 +861,57 @@ async function powdbLifecycle(action, args) {
834
861
  return;
835
862
  }
836
863
  log();
837
- log(` ${colors.green(">")} ${colors.cyan(projectId)} ${colors.dim("engine=powdb state=")}${res.data.state}`);
864
+ const ver = res.data.server_version;
865
+ log(` ${colors.green(">")} ${colors.cyan(projectId)} ${colors.dim("engine=powdb state=")}${res.data.state}${ver ? colors.dim(" server=") + ver : ""}`);
866
+ log();
867
+ }
868
+ /**
869
+ * `bata powdb upgrade --to <version>` — pin the project's server to a new
870
+ * engine version. Server-side this is park→backup→flip→wake→verify with
871
+ * automatic rollback to the pre-upgrade backup on failure. Storage-format
872
+ * releases (0.11+) make a verified upgrade a ONE-WAY door once new-format
873
+ * data is written, so this always confirms unless --yes/--json.
874
+ */
875
+ async function powdbUpgrade(args) {
876
+ const token = requireToken();
877
+ const jsonMode = isJsonMode();
878
+ let toVersion;
879
+ const rest = [];
880
+ for (let i = 0; i < args.length; i++) {
881
+ const a = args[i];
882
+ if (a === "--to")
883
+ toVersion = args[++i];
884
+ else if (a.startsWith("--to="))
885
+ toVersion = a.slice("--to=".length);
886
+ else
887
+ rest.push(a);
888
+ }
889
+ const positional = rest.find((a) => !a.startsWith("-"));
890
+ const projectId = powdbProjectFlag(rest) ?? positional ?? resolvePowdbProject(rest);
891
+ if (!toVersion || !/^\d+\.\d+\.\d+$/.test(toVersion)) {
892
+ emitError("MISSING_ARG", "A target version is required.", "Usage: bata powdb upgrade --to <x.y.z> [--project <id>]");
893
+ }
894
+ const ok = await confirmDestructive(`Upgrade ${colors.cyan(projectId)} to powdb-server ${colors.cyan(toVersion)}? A format-changing release cannot be downgraded once new data is written.`);
895
+ if (!ok) {
896
+ log(" Aborted.");
897
+ return;
898
+ }
899
+ const s = jsonMode ? null : spinner(`Upgrading ${projectId} to ${toVersion} (park -> backup -> flip -> wake -> verify)`);
900
+ const res = await api.post(`/v1/powdb/${encodeURIComponent(projectId)}/upgrade`, { to_version: toVersion }, token);
901
+ s?.stop();
902
+ if (!res.ok) {
903
+ emitError(res.status === 404 ? "NOT_FOUND"
904
+ : res.status === 401 || res.status === 403 ? "INVALID_KEY"
905
+ : res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
906
+ : "CLI_ERROR", apiError(res, "PowDB upgrade failed (instance rolled back to its pre-upgrade state)"), "");
907
+ return;
908
+ }
909
+ if (jsonMode) {
910
+ json({ project_id: projectId, ...res.data });
911
+ return;
912
+ }
913
+ log();
914
+ log(` ${colors.green(">")} ${colors.cyan(projectId)} upgraded to powdb-server ${colors.cyan(res.data.server_version)} ${colors.dim("(verified)")}`);
838
915
  log();
839
916
  }
840
917
  // ─── Help + dispatch ──────────────────────────────────────────────────────────
@@ -857,6 +934,8 @@ function powdbHelp() {
857
934
  log(` ${colors.cyan("status")} Show the server state (running | parked) [--project <id>]`);
858
935
  log(` ${colors.cyan("park")} Scale the server to zero (WAL is durable) [--project <id>]`);
859
936
  log(` ${colors.cyan("wake")} Start a parked server (~tens of ms) [--project <id>]`);
937
+ log(` ${colors.cyan("upgrade")} Pin the server to a new engine version --to <x.y.z> [--project <id>]`);
938
+ log(` ${colors.dim(" park -> backup -> flip -> wake -> verify, auto-rollback on failure")}`);
860
939
  log();
861
940
  log(` ${colors.dim("Options (pull):")}`);
862
941
  log(` ${colors.dim("--project <id> override the linked/default project")}`);
@@ -892,10 +971,12 @@ export async function handlePowdb(args) {
892
971
  return powdbLifecycle("park", args.slice(1));
893
972
  case "wake":
894
973
  return powdbLifecycle("wake", args.slice(1));
974
+ case "upgrade":
975
+ return powdbUpgrade(args.slice(1));
895
976
  case undefined:
896
977
  powdbHelp();
897
978
  return;
898
979
  default:
899
- emitError("INVALID_FLAG", `Unknown subcommand: powdb ${sub}`, "Available: pull, query, exec, status, park, wake. Run `bata powdb --help`.");
980
+ emitError("INVALID_FLAG", `Unknown subcommand: powdb ${sub}`, "Available: pull, query, exec, status, park, wake, upgrade. Run `bata powdb --help`.");
900
981
  }
901
982
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batadata/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "CLI for BataDB — serverless Postgres platform",
5
5
  "bin": {
6
6
  "bata": "./dist/index.js"