dbx/apps/desktop/src/lib/sqlFormatter.ts

100 lines
3.3 KiB
TypeScript

import { DEFAULT_SQL_FORMATTER_SETTINGS, sqlFormatterOptions, type SqlFormatterSettings } from "@/lib/sqlFormatterConfig";
export type SqlFormatDialect = "mysql" | "postgres" | "sqlite" | "sqlserver" | "generic";
export const MAX_SQL_FORMAT_CHARS = 1_000_000;
/**
* Maps a connection's database type to the SQL-formatter dialect to use.
*
* Postgres-compatible engines (GaussDB/openGauss/Kingbase/...) reuse the
* "postgres" grammar, SQLite-compatible ones reuse "sqlite", and anything
* unrecognized falls back to the permissive "generic" dialect. Centralized
* here so every surface that formats SQL (editor, object source, DDL viewers)
* stays in sync.
*/
export function sqlFormatDialectForDbType(dbType: string | null | undefined): SqlFormatDialect {
switch (dbType) {
case "mysql":
return "mysql";
case "postgres":
case "kwdb":
case "gaussdb":
case "opengauss":
case "questdb":
case "kingbase":
case "highgo":
case "vastbase":
case "redshift":
return "postgres";
case "sqlite":
case "rqlite":
case "turso":
return "sqlite";
case "sqlserver":
return "sqlserver";
default:
return "generic";
}
}
function formatterLanguage(dialect: SqlFormatDialect) {
switch (dialect) {
case "mysql":
return "mysql";
case "postgres":
return "postgresql";
case "sqlite":
return "sqlite";
case "sqlserver":
return "transactsql";
default:
return "sql";
}
}
export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "generic", settings: Partial<SqlFormatterSettings> = DEFAULT_SQL_FORMATTER_SETTINGS): Promise<string> {
if (!sql.trim()) return sql;
if (sql.length > MAX_SQL_FORMAT_CHARS) {
throw new Error("SQL is too large to format safely.");
}
const { format } = await import("sql-formatter");
const options = sqlFormatterOptions(settings);
const language = formatterLanguage(dialect);
try {
return format(sql, { language, ...options });
} catch (err) {
// The generic "sql" dialect can't parse many real-world constructs (PostgreSQL
// `::` casts, GaussDB/openGauss materialized-view DDL, T-SQL specifics, ...).
// Retry once with the more permissive PostgreSQL grammar, which is a superset
// that tolerates most of these, before surfacing the failure.
if (language !== "postgresql") {
try {
return format(sql, { language: "postgresql", ...options });
} catch {
// fall through to the original error below
}
}
throw err;
}
}
/**
* Format SQL for *display* (object source, view/table DDL viewers).
*
* Unlike `formatSqlText`, this never throws: if the SQL can't be parsed by the
* formatter (vendor-specific DDL, oversized input, ...) the original text is
* returned unchanged so the viewer still shows the source. Use this for
* read-only/auto-format surfaces; use `formatSqlText` where a thrown error
* should surface to the user (e.g. the explicit "Format SQL" command).
*/
export async function formatSqlForDisplay(sql: string, dialect: SqlFormatDialect = "generic", settings: Partial<SqlFormatterSettings> = DEFAULT_SQL_FORMATTER_SETTINGS): Promise<string> {
if (!sql.trim()) return sql;
try {
return await formatSqlText(sql, dialect, settings);
} catch {
return sql;
}
}