fix(editor): ignore formatter parse errors for incomplete SQL

Closes #5462
This commit is contained in:
t8y2
2026-08-06 22:41:42 +08:00
parent 48e5ef331a
commit 670ddcfcb3
3 changed files with 31 additions and 3 deletions
@@ -18,7 +18,7 @@ import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc,
import { currentStatementFrameRangeTo, shouldRebuildCurrentStatementFrame, visualSqlColumnsWithInlineHints } from "@/lib/sql/currentStatementFrame";
import { expandToSqlStatementWindow, parseInsertValueHints } from "@/lib/sql/insertValueHints";
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
import { formatSqlText, compressSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { formatSqlForEditing, compressSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { detectAndFormatStructured } from "@/lib/sql/autoFormat";
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits";
@@ -2563,7 +2563,7 @@ async function formatCurrentSql() {
toast(t("toolbar.formatAutoDetectFailed"), 3000);
return;
} else {
formatted = await formatSqlText(source, props.formatDialect ?? props.dialect ?? "generic", settingsStore.editorSettings.sqlFormatter);
formatted = await formatSqlForEditing(source, props.formatDialect ?? props.dialect ?? "generic", settingsStore.editorSettings.sqlFormatter);
}
}
if (view.value !== currentView || currentView.state !== originalState || currentView.state.sliceDoc(from, to) !== source) {
+14 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { formatSqlForDisplay, formatSqlText, MAX_SQL_FORMAT_CHARS, sqlFormatDialectForDbType, UnsupportedStructuredInputError } from "@/lib/sql/sqlFormatter";
import { formatSqlForDisplay, formatSqlForEditing, formatSqlText, MAX_SQL_FORMAT_CHARS, sqlFormatDialectForDbType, UnsupportedStructuredInputError } from "@/lib/sql/sqlFormatter";
describe("sqlFormatter", () => {
it("maps PostgreSQL-compatible database types to the postgres formatter dialect", () => {
@@ -50,6 +50,19 @@ describe("sqlFormatter", () => {
expect(formatted).toContain("AS id");
});
it("keeps incomplete editor SQL unchanged when the formatter cannot parse it", async () => {
const sql = "select *\nfrom dbname.\n;";
await expect(formatSqlText(sql, "mysql")).rejects.toThrow("Parse error at token:");
await expect(formatSqlForEditing(sql, "mysql")).resolves.toBe(sql);
});
it("keeps non-parse editor formatting failures visible", async () => {
const oversizedSql = "x".repeat(MAX_SQL_FORMAT_CHARS + 1);
await expect(formatSqlForEditing(oversizedSql, "mysql")).rejects.toThrow("SQL is too large to format safely.");
});
it("returns the original SQL for display when formatting fails", async () => {
const oversizedSql = "x".repeat(MAX_SQL_FORMAT_CHARS + 1);
+15
View File
@@ -106,6 +106,21 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge
}
}
function isSqlFormatterParseError(error: unknown): boolean {
if (!(error instanceof Error) || !error.message.startsWith("Parse error at token:")) return false;
const candidate = error as Error & { offset?: unknown; token?: unknown };
return typeof candidate.offset === "number" && candidate.token !== undefined;
}
export async function formatSqlForEditing(sql: string, dialect: SqlFormatDialect = "generic", settings: Partial<SqlFormatterSettings> = DEFAULT_SQL_FORMATTER_SETTINGS): Promise<string> {
try {
return await formatSqlText(sql, dialect, settings);
} catch (error) {
if (isSqlFormatterParseError(error)) return sql;
throw error;
}
}
/**
* 压缩 SQL 时使用的方言。不同方言对引号、注释、转义的处理不同:
* - `mysql`:保留 MySQL 可执行注释与 optimizer hint;单引号字符串支持反斜杠转义