@batadata/cli 0.1.11 → 0.1.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/schema.d.ts +20 -0
- package/dist/commands/schema.js +229 -14
- package/dist/commands/usage.d.ts +14 -0
- package/dist/commands/usage.js +32 -9
- package/package.json +1 -1
|
@@ -3,4 +3,24 @@ export declare function ormLabel(q: {
|
|
|
3
3
|
model?: string | null;
|
|
4
4
|
action?: string | null;
|
|
5
5
|
}): string;
|
|
6
|
+
/**
|
|
7
|
+
* Pull `--branch <ref>` / `--project <id>` (and their `=`-joined forms) out of
|
|
8
|
+
* `schema dump` args. Both flags are VALUE-consuming: a silently-ignored
|
|
9
|
+
* `--project` would target the wrong project (the exact db-query bug this
|
|
10
|
+
* command must not repeat). Remaining positionals are returned in `rest`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseSchemaDumpArgs(args: string[]): {
|
|
13
|
+
branch?: string;
|
|
14
|
+
projectId?: string;
|
|
15
|
+
rest: string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Pull `--project <id>` out of `schema diff` args, leaving the two positional
|
|
19
|
+
* branch refs (`<from> <to>`) in `rest`. Consuming the flag value keeps a
|
|
20
|
+
* `--project` from being mistaken for a branch ref.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseSchemaDiffArgs(args: string[]): {
|
|
23
|
+
projectId?: string;
|
|
24
|
+
rest: string[];
|
|
25
|
+
};
|
|
6
26
|
export declare function handleSchema(args: string[]): Promise<void>;
|
package/dist/commands/schema.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { api, apiError } from "../api.js";
|
|
3
|
-
import { requireToken, isJsonMode } from "../config.js";
|
|
3
|
+
import { requireToken, isJsonMode, loadConfig } from "../config.js";
|
|
4
4
|
import { colors, log, json, spinner, table, heading } from "../utils/logger.js";
|
|
5
|
-
import { emitError } from "../utils/errors.js";
|
|
6
|
-
import { resolveProjectId } from "../link.js";
|
|
5
|
+
import { emitError, isRetryable } from "../utils/errors.js";
|
|
6
|
+
import { resolveProjectId, resolveBranchId } from "../link.js";
|
|
7
7
|
const NOT_IMPLEMENTED_HINT = "Use `bata schema check <file.sql> --fail-on breaking` to gate migrations today.";
|
|
8
8
|
/**
|
|
9
9
|
* Unimplemented schema subcommand. Never exits 0 for a no-op: emits the
|
|
@@ -178,38 +178,253 @@ function renderReport(v) {
|
|
|
178
178
|
log(` ${colors.dim(`· ${n}`)}`);
|
|
179
179
|
log();
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Pull `--branch <ref>` / `--project <id>` (and their `=`-joined forms) out of
|
|
183
|
+
* `schema dump` args. Both flags are VALUE-consuming: a silently-ignored
|
|
184
|
+
* `--project` would target the wrong project (the exact db-query bug this
|
|
185
|
+
* command must not repeat). Remaining positionals are returned in `rest`.
|
|
186
|
+
*/
|
|
187
|
+
export function parseSchemaDumpArgs(args) {
|
|
188
|
+
let branch;
|
|
189
|
+
let projectId;
|
|
190
|
+
const rest = [];
|
|
191
|
+
for (let i = 0; i < args.length; i++) {
|
|
192
|
+
const a = args[i];
|
|
193
|
+
if (a === "--branch")
|
|
194
|
+
branch = args[++i];
|
|
195
|
+
else if (a.startsWith("--branch="))
|
|
196
|
+
branch = a.slice("--branch=".length);
|
|
197
|
+
else if (a === "--project")
|
|
198
|
+
projectId = args[++i];
|
|
199
|
+
else if (a.startsWith("--project="))
|
|
200
|
+
projectId = a.slice("--project=".length);
|
|
201
|
+
else
|
|
202
|
+
rest.push(a);
|
|
203
|
+
}
|
|
204
|
+
return { branch, projectId, rest };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Pull `--project <id>` out of `schema diff` args, leaving the two positional
|
|
208
|
+
* branch refs (`<from> <to>`) in `rest`. Consuming the flag value keeps a
|
|
209
|
+
* `--project` from being mistaken for a branch ref.
|
|
210
|
+
*/
|
|
211
|
+
export function parseSchemaDiffArgs(args) {
|
|
212
|
+
let projectId;
|
|
213
|
+
const rest = [];
|
|
214
|
+
for (let i = 0; i < args.length; i++) {
|
|
215
|
+
const a = args[i];
|
|
216
|
+
if (a === "--project")
|
|
217
|
+
projectId = args[++i];
|
|
218
|
+
else if (a.startsWith("--project="))
|
|
219
|
+
projectId = a.slice("--project=".length);
|
|
220
|
+
else
|
|
221
|
+
rest.push(a);
|
|
222
|
+
}
|
|
223
|
+
return { projectId, rest };
|
|
224
|
+
}
|
|
225
|
+
/** List a project's branches (GET /v1/projects/:id). Null on lookup failure. */
|
|
226
|
+
async function listProjectBranches(projectId, token, teamId) {
|
|
227
|
+
const query = {};
|
|
228
|
+
if (teamId)
|
|
229
|
+
query.team_id = teamId;
|
|
230
|
+
const res = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
231
|
+
if (!res.ok)
|
|
232
|
+
return null;
|
|
233
|
+
return res.data.branches ?? [];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Resolve a `--branch <ref>` (id OR name) to a concrete branch id, or the
|
|
237
|
+
* project's primary branch when no ref is given. Agents pass the name they used
|
|
238
|
+
* with `db branch create`, so name resolution is mandatory (id-only 404s).
|
|
239
|
+
* Exits via emitError with BRANCH_NOT_FOUND when nothing matches.
|
|
240
|
+
*/
|
|
241
|
+
async function resolveBranchIdForSchema(projectId, token, teamId, ref) {
|
|
242
|
+
const branches = await listProjectBranches(projectId, token, teamId);
|
|
243
|
+
if (!branches) {
|
|
244
|
+
emitError("API_UNAVAILABLE", "Failed to list branches for this project.", "Check the project id and try again.");
|
|
245
|
+
}
|
|
246
|
+
if (ref) {
|
|
247
|
+
const match = branches.find((b) => b.id === ref || b.name === ref);
|
|
248
|
+
if (!match) {
|
|
249
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${ref}" not found in this project.`, "List branches with: bata db branches --json");
|
|
250
|
+
}
|
|
251
|
+
return match.id;
|
|
252
|
+
}
|
|
253
|
+
const primary = branches.find((b) => b.isPrimary) ?? branches[0];
|
|
254
|
+
if (!primary) {
|
|
255
|
+
emitError("BRANCH_NOT_FOUND", "This project has no branches to introspect.", "Create one with: bata db branch create <name>");
|
|
256
|
+
}
|
|
257
|
+
return primary.id;
|
|
258
|
+
}
|
|
259
|
+
/** Shared handling for a failed schema/diff request: retryable → COMPUTE_STARTING (exit 6). */
|
|
260
|
+
function failSchemaRequest(res, fallback) {
|
|
261
|
+
const body = res.data;
|
|
262
|
+
if (isRetryable({ status: res.status, code: body?.code, message: body?.error })) {
|
|
263
|
+
emitError("COMPUTE_STARTING", apiError(res, fallback), "compute is starting; retry in a few seconds");
|
|
264
|
+
}
|
|
265
|
+
const code = res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
266
|
+
: res.status === 404 ? "NO_PROJECT"
|
|
267
|
+
: "CLI_ERROR";
|
|
268
|
+
emitError(code, apiError(res, fallback), "");
|
|
269
|
+
}
|
|
270
|
+
async function schemaDump(args) {
|
|
271
|
+
const jsonMode = isJsonMode();
|
|
272
|
+
const { branch: branchFlag, projectId: projectFlag } = parseSchemaDumpArgs(args);
|
|
273
|
+
const token = requireToken();
|
|
274
|
+
const config = loadConfig();
|
|
275
|
+
// Project precedence: --project flag > .batadata link > config default.
|
|
276
|
+
const projectId = resolveProjectId(projectFlag).projectId;
|
|
277
|
+
if (!projectId) {
|
|
278
|
+
emitError("NO_PROJECT", "No project for schema dump.", "Pass --project <id>, or run `bata link <project>` to set a default.");
|
|
279
|
+
}
|
|
280
|
+
// Branch precedence: explicit --branch wins, else the branch pinned by
|
|
281
|
+
// `bata db branch checkout`, else the project's primary branch.
|
|
282
|
+
const branchRef = resolveBranchId(branchFlag).branchId;
|
|
283
|
+
const s = jsonMode ? null : spinner("Introspecting schema");
|
|
284
|
+
const branchId = await resolveBranchIdForSchema(projectId, token, config.defaultTeam, branchRef);
|
|
285
|
+
const res = await api.get(`/v1/schema/${projectId}`, token, { branch_id: branchId });
|
|
286
|
+
s?.stop();
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
failSchemaRequest(res, "Schema introspection failed");
|
|
289
|
+
if (jsonMode) {
|
|
290
|
+
json(res.data); // server payload is the source of truth
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
renderSchema(res.data.branch_id, res.data.schema);
|
|
294
|
+
}
|
|
295
|
+
async function schemaDiff(args) {
|
|
296
|
+
const jsonMode = isJsonMode();
|
|
297
|
+
const { projectId: projectFlag, rest } = parseSchemaDiffArgs(args);
|
|
298
|
+
const [fromRef, toRef] = rest;
|
|
299
|
+
if (!fromRef || !toRef) {
|
|
300
|
+
emitError("MISSING_ARG", "Two branches are required.", "Usage: bata schema diff <from-branch> <to-branch> [--project <id>]");
|
|
301
|
+
}
|
|
302
|
+
const token = requireToken();
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
const projectId = resolveProjectId(projectFlag).projectId;
|
|
305
|
+
if (!projectId) {
|
|
306
|
+
emitError("NO_PROJECT", "No project for schema diff.", "Pass --project <id>, or run `bata link <project>` to set a default.");
|
|
307
|
+
}
|
|
308
|
+
const s = jsonMode ? null : spinner("Diffing branch schemas");
|
|
309
|
+
const fromId = await resolveBranchIdForSchema(projectId, token, config.defaultTeam, fromRef);
|
|
310
|
+
const toId = await resolveBranchIdForSchema(projectId, token, config.defaultTeam, toRef);
|
|
311
|
+
const res = await api.get(`/v1/schema/${projectId}/diff`, token, { from: fromId, to: toId });
|
|
312
|
+
s?.stop();
|
|
313
|
+
if (!res.ok)
|
|
314
|
+
failSchemaRequest(res, "Schema diff failed");
|
|
315
|
+
if (jsonMode) {
|
|
316
|
+
json(res.data);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
renderDiff(fromRef, toRef, res.data.diff);
|
|
320
|
+
}
|
|
321
|
+
function nullable(c) {
|
|
322
|
+
return c.isNullable ? "" : colors.dim(" NOT NULL");
|
|
323
|
+
}
|
|
324
|
+
function renderSchema(branchId, schema) {
|
|
325
|
+
heading("Schema");
|
|
326
|
+
log(` ${colors.dim(`branch ${colors.cyan(branchId)} · ${schema.tables.length} table${schema.tables.length === 1 ? "" : "s"}`)}`);
|
|
327
|
+
log();
|
|
328
|
+
if (schema.tables.length === 0) {
|
|
329
|
+
log(` ${colors.dim("No tables in this branch's schema.")}`);
|
|
330
|
+
log();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const t of schema.tables) {
|
|
334
|
+
log(` ${colors.bold(`${t.schema}.${t.name}`)}`);
|
|
335
|
+
for (const col of t.columns) {
|
|
336
|
+
const pk = col.isPrimaryKey ? colors.yellow(" PK") : "";
|
|
337
|
+
const def = col.defaultValue ? colors.dim(` = ${col.defaultValue}`) : "";
|
|
338
|
+
log(` ${col.name} ${colors.cyan(col.dataType)}${nullable(col)}${pk}${def}`);
|
|
339
|
+
}
|
|
340
|
+
for (const idx of t.indexes) {
|
|
341
|
+
log(` ${colors.dim(`index ${idx.name}`)}`);
|
|
342
|
+
}
|
|
343
|
+
for (const con of t.constraints) {
|
|
344
|
+
const fk = con.foreignTableName ? colors.dim(` -> ${con.foreignTableSchema}.${con.foreignTableName}.${con.foreignColumnName}`) : "";
|
|
345
|
+
log(` ${colors.dim(`constraint ${con.name} (${con.type})`)}${fk}`);
|
|
346
|
+
}
|
|
347
|
+
log();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function renderDiff(fromRef, toRef, diff) {
|
|
351
|
+
heading("Schema diff");
|
|
352
|
+
log(` ${colors.dim(`${colors.cyan(fromRef)} -> ${colors.cyan(toRef)}`)}`);
|
|
353
|
+
log();
|
|
354
|
+
if (diff.identical) {
|
|
355
|
+
log(` ${colors.green(">")} Schemas are identical.`);
|
|
356
|
+
log();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
for (const t of diff.addedTables) {
|
|
360
|
+
log(` ${colors.green("+ table")} ${colors.bold(`${t.schema}.${t.name}`)} ${colors.dim(`(${t.columns.length} columns)`)}`);
|
|
361
|
+
}
|
|
362
|
+
for (const t of diff.removedTables) {
|
|
363
|
+
log(` ${colors.red("- table")} ${colors.bold(`${t.schema}.${t.name}`)}`);
|
|
364
|
+
}
|
|
365
|
+
for (const t of diff.changedTables) {
|
|
366
|
+
log(` ${colors.yellow("~ table")} ${colors.bold(`${t.schema}.${t.name}`)}`);
|
|
367
|
+
for (const c of t.addedColumns)
|
|
368
|
+
log(` ${colors.green(`+ column ${c.name} ${c.dataType}`)}`);
|
|
369
|
+
for (const c of t.removedColumns)
|
|
370
|
+
log(` ${colors.red(`- column ${c.name}`)}`);
|
|
371
|
+
for (const c of t.changedColumns) {
|
|
372
|
+
const fields = c.changes.map((ch) => `${ch.field}: ${String(ch.from)} -> ${String(ch.to)}`).join(", ");
|
|
373
|
+
log(` ${colors.yellow(`~ column ${c.name}`)} ${colors.dim(fields)}`);
|
|
374
|
+
}
|
|
375
|
+
for (const i of t.addedIndexes)
|
|
376
|
+
log(` ${colors.green(`+ index ${i.name}`)}`);
|
|
377
|
+
for (const i of t.removedIndexes)
|
|
378
|
+
log(` ${colors.red(`- index ${i.name}`)}`);
|
|
379
|
+
for (const i of t.changedIndexes)
|
|
380
|
+
log(` ${colors.yellow(`~ index ${i.name}`)}`);
|
|
381
|
+
for (const con of t.addedConstraints)
|
|
382
|
+
log(` ${colors.green(`+ constraint ${con.name} (${con.type})`)}`);
|
|
383
|
+
for (const con of t.removedConstraints)
|
|
384
|
+
log(` ${colors.red(`- constraint ${con.name}`)}`);
|
|
385
|
+
for (const con of t.changedConstraints)
|
|
386
|
+
log(` ${colors.yellow(`~ constraint ${con.name}`)}`);
|
|
387
|
+
}
|
|
388
|
+
log();
|
|
389
|
+
}
|
|
181
390
|
export async function handleSchema(args) {
|
|
182
391
|
const sub = args[0];
|
|
183
392
|
switch (sub) {
|
|
184
393
|
case "check":
|
|
185
394
|
return schemaCheck(args.slice(1));
|
|
395
|
+
case "dump":
|
|
396
|
+
case "show": // alias — the plan calls it `schema show`; `dump` is the agent-friendly verb
|
|
397
|
+
return schemaDump(args.slice(1));
|
|
398
|
+
case "diff":
|
|
399
|
+
return schemaDiff(args.slice(1));
|
|
186
400
|
case "init":
|
|
187
401
|
notImplemented("init");
|
|
188
402
|
case "push":
|
|
189
403
|
notImplemented("push");
|
|
190
404
|
case "pull":
|
|
191
405
|
notImplemented("pull");
|
|
192
|
-
case "diff":
|
|
193
|
-
notImplemented("diff");
|
|
194
406
|
default:
|
|
195
407
|
log();
|
|
196
408
|
log(` ${colors.bold("bata schema")} ${colors.dim("— schema safety & management")}`);
|
|
197
409
|
log();
|
|
198
410
|
log(` ${colors.dim("Commands:")}`);
|
|
199
|
-
log(` ${colors.cyan("check <file|->")}
|
|
200
|
-
log(` ${colors.cyan("
|
|
201
|
-
log(` ${colors.cyan("
|
|
202
|
-
log(` ${colors.cyan("
|
|
203
|
-
log(` ${colors.cyan("
|
|
411
|
+
log(` ${colors.cyan("check <file|->")} Check a proposed DDL change against live query traffic`);
|
|
412
|
+
log(` ${colors.cyan("dump")} Dump a branch's live schema (tables, columns, indexes, constraints)`);
|
|
413
|
+
log(` ${colors.cyan("diff <from> <to>")} Diff two branches' schemas`);
|
|
414
|
+
log(` ${colors.cyan("init")} Initialize schema from existing database ${colors.dim("(coming soon)")}`);
|
|
415
|
+
log(` ${colors.cyan("push")} Push schema changes to database ${colors.dim("(coming soon)")}`);
|
|
416
|
+
log(` ${colors.cyan("pull")} Pull schema from database ${colors.dim("(coming soon)")}`);
|
|
204
417
|
log();
|
|
205
418
|
log(` ${colors.dim("Options:")}`);
|
|
206
|
-
log(` ${colors.dim("--branch <id>
|
|
207
|
-
log(` ${colors.dim("--
|
|
419
|
+
log(` ${colors.dim("--branch <name|id> target a branch (dump; default: primary)")}`);
|
|
420
|
+
log(` ${colors.dim("--project <id> override the linked/default project")}`);
|
|
421
|
+
log(` ${colors.dim("--json machine-readable output (dump/diff)")}`);
|
|
422
|
+
log(` ${colors.dim("--window <1h|24h|7d|30d> check corpus window (default 24h)")}`);
|
|
208
423
|
log(` ${colors.dim("--fail-on <breaking|risky> exit 2 to gate CI (fail-closed: 'unknown' also trips)")}`);
|
|
209
424
|
log();
|
|
210
425
|
log(` ${colors.dim("Examples:")}`);
|
|
211
|
-
log(` ${colors.dim("bata schema
|
|
212
|
-
log(` ${colors.dim(
|
|
426
|
+
log(` ${colors.dim("bata schema dump --branch main --json")}`);
|
|
427
|
+
log(` ${colors.dim("bata schema diff main feature-x --json")}`);
|
|
213
428
|
log(` ${colors.dim("bata schema check migration.sql --fail-on breaking # CI gate")}`);
|
|
214
429
|
log();
|
|
215
430
|
}
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -22,3 +22,17 @@ export declare function modelActionLabel(q: {
|
|
|
22
22
|
model: string | null;
|
|
23
23
|
action: string | null;
|
|
24
24
|
}): string;
|
|
25
|
+
/**
|
|
26
|
+
* Build the /turbine-queries request from `--by-query` flags. Exported + pure so
|
|
27
|
+
* the flag wiring is unit-testable — a flag that parses but is never forwarded
|
|
28
|
+
* (a silently-ignored flag) is an agent-usability bug, so we prove it here.
|
|
29
|
+
*
|
|
30
|
+
* Recognised: `--window <1h|24h|7d|30d>` (default 24h), `--branch <id>`,
|
|
31
|
+
* `--tag <label>` (Lever L2 — filter cost attribution to one query tag).
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildByQueryRequest(args: string[]): {
|
|
34
|
+
timeRange: string;
|
|
35
|
+
branch?: string;
|
|
36
|
+
tag?: string;
|
|
37
|
+
query: Record<string, string>;
|
|
38
|
+
};
|
package/dist/commands/usage.js
CHANGED
|
@@ -197,6 +197,25 @@ export function modelActionLabel(q) {
|
|
|
197
197
|
return q.action;
|
|
198
198
|
return colors.dim("raw");
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Build the /turbine-queries request from `--by-query` flags. Exported + pure so
|
|
202
|
+
* the flag wiring is unit-testable — a flag that parses but is never forwarded
|
|
203
|
+
* (a silently-ignored flag) is an agent-usability bug, so we prove it here.
|
|
204
|
+
*
|
|
205
|
+
* Recognised: `--window <1h|24h|7d|30d>` (default 24h), `--branch <id>`,
|
|
206
|
+
* `--tag <label>` (Lever L2 — filter cost attribution to one query tag).
|
|
207
|
+
*/
|
|
208
|
+
export function buildByQueryRequest(args) {
|
|
209
|
+
const timeRange = parseValueFlag(args, "--window") ?? "24h";
|
|
210
|
+
const branch = parseValueFlag(args, "--branch");
|
|
211
|
+
const tag = parseValueFlag(args, "--tag");
|
|
212
|
+
const query = { timeRange, orderBy: "totalTime", limit: "20" };
|
|
213
|
+
if (branch)
|
|
214
|
+
query.branch_id = branch;
|
|
215
|
+
if (tag)
|
|
216
|
+
query.tag = tag;
|
|
217
|
+
return { timeRange, branch, tag, query };
|
|
218
|
+
}
|
|
200
219
|
async function usageByQuery(args) {
|
|
201
220
|
const jsonMode = isJsonMode();
|
|
202
221
|
const token = requireToken();
|
|
@@ -205,11 +224,7 @@ async function usageByQuery(args) {
|
|
|
205
224
|
if (!projectId) {
|
|
206
225
|
emitError("NO_PROJECT", "No project for --by-query.", "Pass --project <id>, or run `bata link <project>` to set a default.");
|
|
207
226
|
}
|
|
208
|
-
const timeRange =
|
|
209
|
-
const branch = parseValueFlag(args, "--branch");
|
|
210
|
-
const query = { timeRange, orderBy: "totalTime", limit: "20" };
|
|
211
|
-
if (branch)
|
|
212
|
-
query.branch_id = branch;
|
|
227
|
+
const { timeRange, branch, tag, query } = buildByQueryRequest(args);
|
|
213
228
|
const s = jsonMode ? null : spinner("Attributing compute cost by query");
|
|
214
229
|
const res = await api.get(`/v1/insights/${projectId}/turbine-queries`, token, query);
|
|
215
230
|
s?.stop();
|
|
@@ -232,6 +247,7 @@ async function usageByQuery(args) {
|
|
|
232
247
|
project_id: projectId,
|
|
233
248
|
window: timeRange,
|
|
234
249
|
branch_id: branch ?? null,
|
|
250
|
+
tag: tag ?? null,
|
|
235
251
|
// Carry the basis so a consumer can always see HOW the estimate was made
|
|
236
252
|
// and that it is off the billing path.
|
|
237
253
|
cost_basis: {
|
|
@@ -245,6 +261,7 @@ async function usageByQuery(args) {
|
|
|
245
261
|
fingerprint: q.fingerprint,
|
|
246
262
|
model: q.model,
|
|
247
263
|
action: q.action,
|
|
264
|
+
tag: q.tag ?? null,
|
|
248
265
|
sql_template: q.sqlTemplate,
|
|
249
266
|
calls: q.calls,
|
|
250
267
|
mean_time_ms: q.meanTimeMs,
|
|
@@ -256,16 +273,22 @@ async function usageByQuery(args) {
|
|
|
256
273
|
return;
|
|
257
274
|
}
|
|
258
275
|
heading("Cost by query — estimated");
|
|
259
|
-
log(` ${colors.dim(`Project ${projectId} · window ${timeRange}${branch ? ` · branch ${branch}` : ""}`)}`);
|
|
276
|
+
log(` ${colors.dim(`Project ${projectId} · window ${timeRange}${branch ? ` · branch ${branch}` : ""}${tag ? ` · tag ${tag}` : ""}`)}`);
|
|
260
277
|
log();
|
|
261
278
|
if (rows.length === 0) {
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
if (tag) {
|
|
280
|
+
log(` ${colors.dim(`No ORM query traffic tagged "${tag}" in this window.`)}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
log(` ${colors.dim("No ORM query traffic reported in this window.")}`);
|
|
284
|
+
log(` ${colors.dim("Wire up createInsightsReporter() to start attributing cost.")}`);
|
|
285
|
+
}
|
|
264
286
|
log();
|
|
265
287
|
return;
|
|
266
288
|
}
|
|
267
|
-
table(["QUERY", "CALLS", "MEAN", "EST. COST"], rows.map((q) => [
|
|
289
|
+
table(["QUERY", "TAG", "CALLS", "MEAN", "EST. COST"], rows.map((q) => [
|
|
268
290
|
modelActionLabel(q),
|
|
291
|
+
q.tag ?? colors.dim("—"),
|
|
269
292
|
q.calls.toLocaleString(),
|
|
270
293
|
`${q.meanTimeMs}ms`,
|
|
271
294
|
estCost(q.estimatedCostCents),
|