All files / src/lib/formatters sql.ts

82.75% Statements 24/29
68.18% Branches 15/22
100% Functions 4/4
82.75% Lines 24/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                                108x                           171x 6x   165x                               3968x         3968x       3968x   3968x   468x   103x           3397x                               422x 67x     355x 355x 355x                                                           4x 2x     2x 2x   2x   2x 2x 2x            
/**
 * SQL syntax highlighting for DB span descriptions.
 *
 * Uses `@sentry/sqlish` to parse SQL-ish strings (including parameterized
 * queries with `%s`, `$1`, `?`) and applies ANSI colorization via the
 * project's chalk-based color palette.
 *
 * Respects {@link isPlainOutput} — all functions return plain uncolored
 * text when output is piped, `NO_COLOR` is set, or `SENTRY_PLAIN_OUTPUT=1`.
 */
 
import type { Token } from "@sentry/sqlish";
import { SQLishParser, string as sqlishFormat } from "@sentry/sqlish";
import { cyan, magenta, muted } from "./colors.js";
import { isPlainOutput } from "./plain-detect.js";
 
const parser = new SQLishParser();
 
/**
 * Check whether a span operation is a database span.
 *
 * Matches `"db"`, `"db.query"`, `"db.sql.query"`, `"db.redis"`, etc.
 * The `@sentry/sqlish` parser is tolerant of non-SQL content, so it's
 * safe to colorize any `db.*` description — non-SQL tokens simply render
 * as generic (uncolored) text.
 *
 * @param op - Span operation string (e.g. from `span.op`)
 * @returns `true` if the operation starts with `"db"`
 */
export function isDbSpanOp(op?: string | null): boolean {
  if (!op) {
    return false;
  }
  return op === "db" || op.startsWith("db.");
}
 
/**
 * Colorize a single token based on its type.
 *
 * Token → color mapping:
 * - `Keyword` → cyan (matches `codeFg` feel; stands out without being loud)
 * - `Parameter` → magenta (visually distinct for `%s`, `$1`, `?`)
 * - `CollapsedColumns` → muted (de-emphasized, not actual SQL)
 * - `LeftParenthesis` / `RightParenthesis` → muted (structural)
 * - `GenericToken` → default terminal foreground
 * - `Whitespace` → as-is
 */
function colorizeToken(token: Token): string {
  // Nested content: recurse
  Iif (Array.isArray(token.content)) {
    return token.content.map(colorizeToken).join("");
  }
 
  // Token content is another token (shouldn't happen per spec but handle gracefully)
  Iif (typeof token.content === "object" && token.content !== null) {
    return colorizeToken(token.content);
  }
 
  const text = typeof token.content === "string" ? token.content : "";
 
  switch (token.type) {
    case "Keyword":
      return cyan(text);
    case "Parameter":
      return magenta(text);
    case "CollapsedColumns":
    case "LeftParenthesis":
    case "RightParenthesis":
      return muted(text);
    default:
      return text;
  }
}
 
/**
 * Inline SQL syntax highlighting for a single line.
 *
 * Parses the SQL string with `@sentry/sqlish` and applies ANSI colors
 * to each token. Whitespace is normalized to single spaces.
 *
 * Returns the original string unchanged when {@link isPlainOutput} is true.
 *
 * @param sql - SQL-ish description from a span (e.g. `"SELECT * FROM users WHERE id = %s"`)
 * @returns ANSI-colored string, or plain text in non-TTY mode
 */
export function colorizeSql(sql: string): string {
  if (isPlainOutput()) {
    return sql;
  }
 
  try {
    const tokens = parser.parse(sql);
    return tokens.map(colorizeToken).join("");
  } catch {
    // If parsing fails, return the original string uncolored
    return sql;
  }
}
 
/**
 * Pretty-printed, colorized SQL block for detail views.
 *
 * In TTY mode: reformats the SQL with newlines at major keywords
 * (`SELECT`, `FROM`, `WHERE`, `ORDER`, etc.) and applies syntax
 * highlighting. Returns a section like:
 *
 * ```
 * ─── Query ───
 *
 * SELECT id, name
 * FROM users
 * WHERE id = %s
 * ```
 *
 * In non-TTY / plain mode: returns the original SQL as a compact
 * single line with no ANSI codes and no reformatting, safe for piping
 * and machine consumption.
 *
 * @param sql - SQL-ish description from a DB span
 * @returns Formatted section string
 */
export function formatSqlBlock(sql: string): string {
  if (isPlainOutput()) {
    return `\n─── Query ───\n\n${sql}\n`;
  }
 
  try {
    const tokens = parser.parse(sql);
    // Use sqlish's string formatter for structural formatting (newlines at keywords)
    const structured = sqlishFormat(tokens);
    // Re-parse the structured output to colorize it
    const coloredTokens = parser.parse(structured);
    const colored = coloredTokens.map(colorizeToken).join("");
    return `\n${muted("─── Query ───")}\n\n${colored}\n`;
  } catch {
    // Fallback: show unformatted but with header
    return `\n${muted("─── Query ───")}\n\n${sql}\n`;
  }
}