@batadata/cli 0.2.6 → 0.2.8

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.
@@ -18,18 +18,18 @@ export declare function parseBranchCreateArgs(args: string[]): {
18
18
  expiresIn?: string;
19
19
  purpose?: string;
20
20
  };
21
- export declare function connect(): Promise<void>;
21
+ export declare function connect(args?: string[]): Promise<void>;
22
22
  export declare function url(args?: string[]): Promise<void>;
23
- export declare function branches(): Promise<void>;
23
+ export declare function branches(args?: string[]): Promise<void>;
24
24
  export declare function branchCreate(args?: string[]): Promise<void>;
25
- export declare function branchDelete(name?: string): Promise<void>;
25
+ export declare function branchDelete(args?: string[]): Promise<void>;
26
26
  /**
27
27
  * `bata db branch protect <name-or-id>` / `unprotect <name-or-id>` — flip a
28
28
  * branch's protection flag (PATCH /v1/branches/:id). A protected branch refuses
29
29
  * delete/reset/rollback/reap — the safety rail for headless/agent ops. The ref
30
30
  * is resolved by name OR id, like the other branch commands.
31
31
  */
32
- export declare function branchSetProtected(ref: string | undefined, wantProtected: boolean): Promise<void>;
32
+ export declare function branchSetProtected(args: string[], wantProtected: boolean): Promise<void>;
33
33
  /**
34
34
  * `bata db branch checkout <name-or-id>` — pin a branch into the directory's
35
35
  * `.batadata/project.json` so later `db query` / `db url` target it with no
@@ -37,7 +37,7 @@ export declare function branchSetProtected(ref: string | undefined, wantProtecte
37
37
  * branch ref is resolved by name OR id, just like `db query --branch`.
38
38
  */
39
39
  export declare function branchCheckout(args: string[]): Promise<void>;
40
- export declare function studio(): Promise<void>;
40
+ export declare function studio(args?: string[]): Promise<void>;
41
41
  /**
42
42
  * Pull `--branch <ref>` / `--at <ISO-timestamp|LSN>` flags (and their `=` forms)
43
43
  * out of the query args and return them plus the remaining args, which join into
@@ -6,7 +6,7 @@ import { colors, log, json, error, success, spinner, table, heading, info as log
6
6
  import { prompt, confirmDestructive } from "../utils/prompts.js";
7
7
  import { openBrowser } from "../utils/open.js";
8
8
  import { emitError, isRetryable } from "../utils/errors.js";
9
- import { resolveProjectId, resolveBranchId, findLinkFile, writeLinkFile } from "../link.js";
9
+ import { resolveProjectId, readLinkFile, findLinkFile, writeLinkFile } from "../link.js";
10
10
  // Share the exact LSN/timestamp discriminator `bata restore` uses so `db query
11
11
  // --at` classifies a restore point identically.
12
12
  import { classifyRestorePoint } from "./restore.js";
@@ -140,7 +140,7 @@ async function resolveBranchRef(projectId, token, teamId, ref) {
140
140
  return null;
141
141
  return result.branches.find((b) => b.id === ref || b.name === ref) ?? null;
142
142
  }
143
- export async function connect() {
143
+ export async function connect(args = []) {
144
144
  // psql is an interactive session — there's no headless equivalent. Don't spawn
145
145
  // it in --json or non-TTY contexts; point agents at the headless surfaces.
146
146
  if (isJsonMode() || !process.stdin.isTTY) {
@@ -148,7 +148,8 @@ export async function connect() {
148
148
  }
149
149
  const token = requireToken();
150
150
  const config = loadConfig();
151
- const projectId = resolveProjectId().projectId;
151
+ const { projectId: projectFlag } = parseProjectFlag(args);
152
+ const projectId = resolveProjectId(projectFlag).projectId;
152
153
  if (!projectId) {
153
154
  emitError("NO_PROJECT", "No default project.", "Run bata projects create or set one with bata projects info <id>.");
154
155
  }
@@ -215,11 +216,15 @@ export async function url(args = []) {
215
216
  "\n");
216
217
  process.stdout.write(conn.connection_uri + "\n");
217
218
  }
218
- export async function branches() {
219
+ export async function branches(args = []) {
219
220
  const token = requireToken();
220
221
  const config = loadConfig();
221
222
  const jsonMode = isJsonMode();
222
- const projectId = resolveProjectId().projectId;
223
+ // `db branches` used to take NO args, so an explicit `--project <id>` was
224
+ // SILENTLY IGNORED and the default/linked project answered — the wrong-target
225
+ // class the 0.1.6 projects fix closed for projects info/delete.
226
+ const { projectId: projectFlag } = parseProjectFlag(args);
227
+ const projectId = resolveProjectId(projectFlag).projectId;
223
228
  if (!projectId) {
224
229
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
225
230
  }
@@ -278,11 +283,16 @@ export async function branchCreate(args = []) {
278
283
  const jsonMode = isJsonMode();
279
284
  const token = requireToken();
280
285
  const config = loadConfig();
281
- const projectId = resolveProjectId().projectId;
286
+ // Strip --project BEFORE the positional parse. Pre-fix, `--project <id>` was
287
+ // both ignored (branch landed on the default project) AND its VALUE bled into
288
+ // the positional name when it led the args (`db branch create --project X dev`
289
+ // created a branch literally named "X" — on the wrong project).
290
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
291
+ const projectId = resolveProjectId(projectFlag).projectId;
282
292
  if (!projectId) {
283
293
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
284
294
  }
285
- const { name, expiresIn, purpose } = parseBranchCreateArgs(args);
295
+ const { name, expiresIn, purpose } = parseBranchCreateArgs(rest);
286
296
  // A relative TTL (--expires-in 2h) becomes an absolute expires_at the server
287
297
  // enforces. Parse it up front so a bad duration fails fast, before any call.
288
298
  let expiresAt;
@@ -330,9 +340,11 @@ export async function branchCreate(args = []) {
330
340
  purpose: res.data.purpose ?? purpose ?? null,
331
341
  },
332
342
  // The branch row exists immediately, but its compute may still be
333
- // provisioning — poll `db branches` for status before connecting.
343
+ // provisioning — poll `db branches` for status before connecting. The
344
+ // poll command carries the resolved project explicitly so following it
345
+ // verbatim can never land on a different linked/default project.
334
346
  ready: false,
335
- poll: "bata db branches --json",
347
+ poll: `bata db branches --project ${projectId} --json`,
336
348
  });
337
349
  return;
338
350
  }
@@ -341,29 +353,35 @@ export async function branchCreate(args = []) {
341
353
  if (expiresAt) {
342
354
  log(` ${colors.dim("Expires")} ${formatDate(expiresAt)} ${colors.dim("(auto-deleted)")}`);
343
355
  }
344
- log(` ${colors.dim("Poll readiness with")} ${colors.cyan("bata db branches")}`);
356
+ log(` ${colors.dim("Poll readiness with")} ${colors.cyan(`bata db branches --project ${projectId}`)}`);
345
357
  log();
346
358
  }
347
- export async function branchDelete(name) {
359
+ export async function branchDelete(args = []) {
348
360
  const jsonMode = isJsonMode();
349
361
  const token = requireToken();
350
362
  const config = loadConfig();
351
- const projectId = resolveProjectId().projectId;
363
+ // --project used to be silently ignored here (the delete hit the default/
364
+ // linked project's branch list) — same wrong-target class as `db branches`.
365
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
366
+ const projectId = resolveProjectId(projectFlag).projectId;
367
+ const ref = rest[0];
352
368
  if (!projectId) {
353
369
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
354
370
  }
355
- if (!name) {
356
- emitError("MISSING_ARG", "Branch name is required.", "Usage: bata db branch delete <name>");
371
+ if (!ref) {
372
+ emitError("MISSING_ARG", "Branch name or id is required.", "Usage: bata db branch delete <name-or-id>");
357
373
  }
358
374
  // Skip the prompt headlessly (--yes / --json / no TTY) so agents and CI can
359
375
  // actually delete — and never report success without deleting.
360
- const ok = await confirmDestructive(`Delete branch ${colors.cyan(name)}?`);
376
+ const ok = await confirmDestructive(`Delete branch ${colors.cyan(ref)}?`);
361
377
  if (!ok) {
362
378
  log(" Aborted.");
363
379
  return;
364
380
  }
365
- const s = jsonMode ? null : spinner(`Deleting branch ${name}`);
366
- // We need to find the branch ID first
381
+ const s = jsonMode ? null : spinner(`Deleting branch ${ref}`);
382
+ // Fetch the branch list explicitly (not via resolveBranchRef, which folds an
383
+ // API outage into null) so a 5xx stays API_UNAVAILABLE/exit 6 — an agent must
384
+ // never read a retryable outage as "branch doesn't exist".
367
385
  const query = {};
368
386
  if (config.defaultTeam)
369
387
  query.team_id = config.defaultTeam;
@@ -372,10 +390,12 @@ export async function branchDelete(name) {
372
390
  s?.stop();
373
391
  emitError(projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(projRes, "Failed to fetch branches."), "");
374
392
  }
375
- const branch = projRes.data.branches.find((b) => b.name === name);
393
+ // Match by name OR id, like checkout/protect/query — delete used to match
394
+ // by name only, so a real branch id got "not found".
395
+ const branch = projRes.data.branches.find((b) => b.id === ref || b.name === ref);
376
396
  if (!branch) {
377
397
  s?.stop();
378
- emitError("BRANCH_NOT_FOUND", `Branch "${name}" not found.`, "List branches with: bata db branches --json");
398
+ emitError("BRANCH_NOT_FOUND", `Branch "${ref}" not found in this project.`, "List branches with: bata db branches --json");
379
399
  }
380
400
  const res = await api.del(`/v1/branches/${branch.id}`, token, query);
381
401
  s?.stop();
@@ -383,11 +403,11 @@ export async function branchDelete(name) {
383
403
  emitError(res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE" : "CLI_ERROR", apiError(res, "Failed to delete branch."), "");
384
404
  }
385
405
  if (jsonMode) {
386
- json({ branch: { id: branch.id, name, project_id: projectId }, deleted: true });
406
+ json({ branch: { id: branch.id, name: branch.name, project_id: projectId }, deleted: true });
387
407
  return;
388
408
  }
389
409
  log();
390
- log(` ${colors.green(">")} Branch ${colors.cyan(name)} deleted`);
410
+ log(` ${colors.green(">")} Branch ${colors.cyan(branch.name)} deleted`);
391
411
  log();
392
412
  }
393
413
  /**
@@ -396,11 +416,13 @@ export async function branchDelete(name) {
396
416
  * delete/reset/rollback/reap — the safety rail for headless/agent ops. The ref
397
417
  * is resolved by name OR id, like the other branch commands.
398
418
  */
399
- export async function branchSetProtected(ref, wantProtected) {
419
+ export async function branchSetProtected(args, wantProtected) {
400
420
  const jsonMode = isJsonMode();
401
421
  const token = requireToken();
402
422
  const config = loadConfig();
403
- const projectId = resolveProjectId().projectId;
423
+ const { projectId: projectFlag, rest } = parseProjectFlag(args);
424
+ const projectId = resolveProjectId(projectFlag).projectId;
425
+ const ref = rest[0];
404
426
  const verb = wantProtected ? "protect" : "unprotect";
405
427
  if (!projectId) {
406
428
  emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
@@ -505,8 +527,9 @@ export async function branchCheckout(args) {
505
527
  log(` ${colors.dim("Link file:")} ${colors.dim(linkFile)}`);
506
528
  log();
507
529
  }
508
- export async function studio() {
509
- const projectId = resolveProjectId().projectId;
530
+ export async function studio(args = []) {
531
+ const { projectId: projectFlag } = parseProjectFlag(args);
532
+ const projectId = resolveProjectId(projectFlag).projectId;
510
533
  const studioUrl = projectId
511
534
  ? `https://bench-app-one.vercel.app/studio?project=${projectId}`
512
535
  : "https://bench-app-one.vercel.app/studio";
@@ -652,9 +675,6 @@ async function runTimeTravelQuery(opts) {
652
675
  export async function query(args = []) {
653
676
  const jsonMode = isJsonMode();
654
677
  const { branchId: branchFlag, at, projectId: projectFlag, rest } = parseQueryFlags(args);
655
- // Branch precedence: an explicit --branch wins, else the branch pinned by
656
- // `bata db branch checkout` in .batadata/project.json, else the primary.
657
- const branchId = resolveBranchId(branchFlag).branchId;
658
678
  const sql = rest.join(" ").trim();
659
679
  if (!sql) {
660
680
  emitError("MISSING_ARG", "SQL query is required.", 'Usage: bata db query "SELECT 1"');
@@ -663,6 +683,14 @@ export async function query(args = []) {
663
683
  const config = loadConfig();
664
684
  // Project precedence: --project flag > .batadata link > config default.
665
685
  const projectId = resolveProjectId(projectFlag).projectId;
686
+ // Branch precedence: an explicit --branch wins, else the branch pinned by
687
+ // `bata db branch checkout` in .batadata/project.json — but ONLY when the
688
+ // query targets the linked project. A pinned branch from project A must never
689
+ // resolve against an explicit `--project B` (it misses → BRANCH_NOT_FOUND, or
690
+ // worse, a name collision silently targets the wrong branch).
691
+ const link = readLinkFile()?.link;
692
+ const pinnedBranch = link && link.projectId === projectId ? link.branchId ?? undefined : undefined;
693
+ const branchId = branchFlag ?? pinnedBranch;
666
694
  if (!projectId) {
667
695
  emitError("NO_PROJECT", "No project for db query.", "Pass --project <id>, or run `bata link <project>` to set a default.");
668
696
  }
@@ -786,8 +814,9 @@ function dbHelp(sub) {
786
814
  log(` ${colors.bold("bata db query")} — run a SQL query against a branch`);
787
815
  log();
788
816
  usage('bata db query "SELECT 1"');
789
- usage("bata db query <sql> [--branch <id-or-name>] [--at <timestamp|LSN>] [--json]");
817
+ usage("bata db query <sql> [--project <id>] [--branch <id-or-name>] [--at <timestamp|LSN>] [--json]");
790
818
  log();
819
+ note("--project <id> Target a specific project (default: linked/default project)");
791
820
  note("--branch <id-or-name> Target a specific branch by id OR name (default: the project's primary)");
792
821
  note("--at <timestamp|LSN> Time-travel: run the query AS OF a past point (ISO-8601");
793
822
  note(" timestamp e.g. 2026-07-04T12:00:00Z, or an LSN e.g. 0/15994B0).");
@@ -799,7 +828,7 @@ function dbHelp(sub) {
799
828
  case "branches":
800
829
  log(` ${colors.bold("bata db branches")} — list branches and their compute status`);
801
830
  log();
802
- usage("bata db branches [--json]");
831
+ usage("bata db branches [--project <id>] [--json]");
803
832
  log();
804
833
  note("STATUS shows the live compute state; a ✓ means ready to query.");
805
834
  note("--json includes computeStatus + ready — poll these after create/cold start.");
@@ -807,12 +836,13 @@ function dbHelp(sub) {
807
836
  case "branch":
808
837
  log(` ${colors.bold("bata db branch")} — create, delete, or check out a branch`);
809
838
  log();
810
- usage("bata db branch create <name> [--expires-in <2h|30m|7d>] [--purpose <text>]");
811
- usage("bata db branch delete <name> [--yes]");
839
+ usage("bata db branch create <name> [--project <id>] [--expires-in <2h|30m|7d>] [--purpose <text>]");
840
+ usage("bata db branch delete <name-or-id> [--project <id>] [--yes]");
812
841
  usage("bata db branch checkout <name-or-id> [--project <id>]");
813
- usage("bata db branch protect <name-or-id>");
814
- usage("bata db branch unprotect <name-or-id>");
842
+ usage("bata db branch protect <name-or-id> [--project <id>]");
843
+ usage("bata db branch unprotect <name-or-id> [--project <id>]");
815
844
  log();
845
+ note("--project Target a specific project (default: linked/default project).");
816
846
  note("--expires-in Auto-delete the branch after this long (units: s/m/h/d/w).");
817
847
  note("--purpose Free-text note describing why the branch exists.");
818
848
  note("checkout pins the branch into .batadata/project.json so db query/url");
@@ -823,12 +853,12 @@ function dbHelp(sub) {
823
853
  case "url":
824
854
  log(` ${colors.bold("bata db url")} — print the connection string`);
825
855
  log();
826
- usage("bata db url [--json]");
856
+ usage("bata db url [--project <id>] [--json]");
827
857
  break;
828
858
  case "connect":
829
859
  log(` ${colors.bold("bata db connect")} — open an interactive psql session`);
830
860
  log();
831
- usage("bata db connect");
861
+ usage("bata db connect [--project <id>]");
832
862
  note("Interactive only — use `bata db query` / `bata db url` headlessly.");
833
863
  break;
834
864
  case "studio":
@@ -865,27 +895,27 @@ export async function handleDb(args) {
865
895
  }
866
896
  switch (sub) {
867
897
  case "connect":
868
- return connect();
898
+ return connect(args.slice(1));
869
899
  case "url":
870
900
  return url(args.slice(1));
871
901
  case "branches":
872
- return branches();
902
+ return branches(args.slice(1));
873
903
  case "branch": {
874
904
  const action = args[1];
875
905
  if (action === "create")
876
906
  return branchCreate(args.slice(2));
877
907
  if (action === "delete")
878
- return branchDelete(args[2]);
908
+ return branchDelete(args.slice(2));
879
909
  if (action === "checkout")
880
910
  return branchCheckout(args.slice(2));
881
911
  if (action === "protect")
882
- return branchSetProtected(args[2], true);
912
+ return branchSetProtected(args.slice(2), true);
883
913
  if (action === "unprotect")
884
- return branchSetProtected(args[2], false);
914
+ return branchSetProtected(args.slice(2), false);
885
915
  emitError("INVALID_FLAG", `Unknown: db branch ${action || ""}`, "Available: create, delete, checkout, protect, unprotect");
886
916
  }
887
917
  case "studio":
888
- return studio();
918
+ return studio(args.slice(1));
889
919
  case "query":
890
920
  return query(args.slice(1));
891
921
  default:
@@ -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
@@ -5,14 +5,26 @@ import { requireToken, isJsonMode, loadConfig } from "../config.js";
5
5
  import { colors, log, json, spinner, heading, table } from "../utils/logger.js";
6
6
  import { emitError, isRetryable } from "../utils/errors.js";
7
7
  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`.
8
+ // PowQL reserved words a column/table name must not collide with. Mirrors the
9
+ // FULL engine lexer keyword set (crates/query/src/lexer.rs POWQL_KEYWORDS as
10
+ // of PowDB 0.10.0 which added `schema`/`describe`). A colliding name is
11
+ // emitted backtick-quoted (0.10+ syntax) rather than renamed, so the exported
12
+ // column keeps its source name. Keep this list in sync on PowDB upgrades: a
13
+ // missing word here means a bare identifier that fails to parse on load.
11
14
  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",
15
+ "abs", "add", "alter", "and", "as", "asc", "auto", "avg", "begin",
16
+ "between", "case", "cast", "ceil", "column", "commit", "concat", "conflict",
17
+ "count", "cross", "date_add", "date_diff", "default", "delete",
18
+ "dense_rank", "desc", "describe", "distinct", "drop", "else", "end",
19
+ "exists", "explain", "extract", "false", "filter", "floor", "group",
20
+ "having", "in", "index", "inner", "insert", "is", "join", "left", "length",
21
+ "let", "like", "limit", "link", "lower", "match", "materialize",
22
+ "materialized", "max", "min", "multi", "not", "now", "null", "offset", "on",
23
+ "or", "order", "outer", "over", "partition", "pow", "rank", "refresh",
24
+ "required", "returning", "right", "rollback", "round", "row_number",
25
+ "schema", "select", "sqrt", "substring", "sum", "then", "transaction",
26
+ "trim", "true", "type", "union", "unique", "update", "upper", "upsert",
27
+ "view", "when",
16
28
  ]);
17
29
  /**
18
30
  * Map a Postgres data type (as reported by schema introspection) to the PowDB
@@ -73,8 +85,9 @@ export function mapPgType(dataType) {
73
85
  /**
74
86
  * Sanitize a Postgres identifier into a valid PowQL identifier (leading
75
87
  * 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.
88
+ * changed and whether it collides with a PowQL keyword. A keyword collision is
89
+ * NOT renamed the name is kept and `renderPowqlIdent` backtick-quotes it at
90
+ * emit time (PowDB 0.10+ syntax), so exported columns keep their source names.
78
91
  */
79
92
  export function powqlIdentifier(name) {
80
93
  let ident = name.replace(/[^A-Za-z0-9_]/g, "_");
@@ -82,11 +95,17 @@ export function powqlIdentifier(name) {
82
95
  ident = "_" + ident;
83
96
  }
84
97
  const keyword = POWQL_KEYWORDS.has(ident.toLowerCase());
85
- if (keyword) {
86
- ident = ident + "_";
87
- }
88
98
  return { ident, changed: ident !== name, keyword };
89
99
  }
100
+ /**
101
+ * Render an identifier for emitted PowQL: backtick-quoted when it collides
102
+ * with a reserved word (the 0.10 lexer accepts `` `name` `` in every
103
+ * identifier position), bare otherwise. Identifiers are pre-sanitized by
104
+ * `powqlIdentifier`, so no other escaping is needed inside the backticks.
105
+ */
106
+ export function renderPowqlIdent(ident) {
107
+ return POWQL_KEYWORDS.has(ident.toLowerCase()) ? `\`${ident}\`` : ident;
108
+ }
90
109
  /**
91
110
  * Escape a string for a PowQL double-quoted literal. The engine lexer honors
92
111
  * `\" \\ \n \t`; every other character passes through verbatim. Returns the
@@ -221,7 +240,10 @@ export function planTable(t) {
221
240
  const warnings = [];
222
241
  const { ident: tPowName, changed: nameChanged, keyword } = powqlIdentifier(t.name);
223
242
  if (nameChanged) {
224
- warnings.push(`table renamed to \`${tPowName}\`${keyword ? " (collides with a PowQL keyword)" : " (name not a valid PowQL identifier)"}`);
243
+ warnings.push(`table renamed to \`${tPowName}\` (name not a valid PowQL identifier)`);
244
+ }
245
+ else if (keyword) {
246
+ warnings.push(`table name "${t.name}" is a PowQL reserved word — emitted backtick-quoted (requires PowDB 0.10+)`);
225
247
  }
226
248
  // Single-column UNIQUE constraints → the PowQL `unique` modifier.
227
249
  const singleUniqueCols = new Set();
@@ -263,7 +285,10 @@ export function planTable(t) {
263
285
  }
264
286
  const { ident: colPowName, changed: colChanged, keyword: colKeyword } = powqlIdentifier(col.name);
265
287
  if (colChanged) {
266
- warnings.push(`column ${col.name} renamed to \`${colPowName}\`${colKeyword ? " (collides with a PowQL keyword)" : ""}`);
288
+ warnings.push(`column ${col.name} renamed to \`${colPowName}\` (name not a valid PowQL identifier)`);
289
+ }
290
+ else if (colKeyword) {
291
+ warnings.push(`column "${col.name}" is a PowQL reserved word — emitted backtick-quoted (requires PowDB 0.10+)`);
267
292
  }
268
293
  const def = parsePgDefault(col.defaultValue, mapping.powType);
269
294
  let auto = false;
@@ -297,7 +322,7 @@ export function planTable(t) {
297
322
  }
298
323
  /** Render a table plan as a PowQL `type` DDL block. */
299
324
  export function renderTypeDdl(plan) {
300
- const lines = [`type ${plan.powName} {`];
325
+ const lines = [`type ${renderPowqlIdent(plan.powName)} {`];
301
326
  const fieldLines = plan.columns.map((c) => {
302
327
  const mods = [];
303
328
  if (c.required)
@@ -308,7 +333,7 @@ export function renderTypeDdl(plan) {
308
333
  mods.push("auto");
309
334
  const prefix = mods.length ? mods.join(" ") + " " : "";
310
335
  const def = c.defaultLiteral ? ` default ${c.defaultLiteral}` : "";
311
- return ` ${prefix}${c.powName}: ${c.powType}${def}`;
336
+ return ` ${prefix}${renderPowqlIdent(c.powName)}: ${c.powType}${def}`;
312
337
  });
313
338
  lines.push(fieldLines.join(",\n"));
314
339
  lines.push("}");
@@ -337,14 +362,14 @@ export function buildInserts(plan, rows) {
337
362
  }
338
363
  if (res.risky)
339
364
  loadRisk = true;
340
- assigns.push(`${col.powName} := ${res.literal}`);
365
+ assigns.push(`${renderPowqlIdent(col.powName)} := ${res.literal}`);
341
366
  anyField = true;
342
367
  }
343
368
  if (!anyField) {
344
369
  skippedRows++;
345
370
  continue;
346
371
  }
347
- statements.push(`insert ${plan.powName} { ${assigns.join(", ")} };`);
372
+ statements.push(`insert ${renderPowqlIdent(plan.powName)} { ${assigns.join(", ")} };`);
348
373
  }
349
374
  const warnings = [];
350
375
  for (const [col, reason] of skipNotes) {
@@ -603,6 +628,7 @@ function buildArtifactHeader(o) {
603
628
  // which lexes as two minus signs and fails to parse).
604
629
  const lines = [
605
630
  "# PowDB Stage A export — generated by `bata powdb pull`",
631
+ "# Target: PowDB 0.10+ (reserved identifiers are backtick-quoted)",
606
632
  "#",
607
633
  `# Source project : ${o.projectName} (${o.projectId})`,
608
634
  `# Source branch : ${o.branchName} (${o.branchId})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batadata/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "CLI for BataDB — serverless Postgres platform",
5
5
  "bin": {
6
6
  "bata": "./dist/index.js"