diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 2a1ecb9928..d5322df2e0 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -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) { diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts index ea5cb2a18e..f75e9e297b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts @@ -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); diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts index c2d1c304c0..0657cf34d2 100644 --- a/apps/desktop/src/lib/sql/sqlFormatter.ts +++ b/apps/desktop/src/lib/sql/sqlFormatter.ts @@ -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 = DEFAULT_SQL_FORMATTER_SETTINGS): Promise { + try { + return await formatSqlText(sql, dialect, settings); + } catch (error) { + if (isSqlFormatterParseError(error)) return sql; + throw error; + } +} + /** * 压缩 SQL 时使用的方言。不同方言对引号、注释、转义的处理不同: * - `mysql`:保留 MySQL 可执行注释与 optimizer hint;单引号字符串支持反斜杠转义