diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts index ab728b7ed7..308bb82f2c 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts @@ -57,6 +57,13 @@ describe("sqlFormatter", () => { await expect(formatSqlForEditing(sql, "mysql")).resolves.toBe(sql); }); + it("keeps editor SQL unchanged when it contains full-width characters the tokenizer can't parse", async () => { + const sql = "update t set a=concat(t.入池时间(审核通过时间),' 00:00:00') where t.入池时间(审核通过时间) ≠ '';"; + + await expect(formatSqlText(sql, "mysql")).rejects.toThrow("Parse error: Unexpected"); + 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); diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts index 0b44f031da..6c53e934dc 100644 --- a/apps/desktop/src/lib/sql/sqlFormatter.ts +++ b/apps/desktop/src/lib/sql/sqlFormatter.ts @@ -339,8 +339,21 @@ function applySqlFormatterLayout(sql: string, settings: SqlFormatterSettings, di return formatted; } +/** + * sql-formatter throws two distinct shapes when it can't parse the input: + * - Grammar-level (nearley parser): `Parse error at token: ... ` with `.offset`/`.token` + * set on the error, e.g. valid tokens in an unexpected position (mid-edit SQL). + * - Lexer-level (TokenizerEngine): a plain `Parse error: Unexpected "..." at line ...` + * when a character sequence doesn't match any token rule at all — e.g. full-width + * punctuation (`≠`, `(`, `)`) that MySQL accepts in identifiers/expressions but + * sql-formatter's tokenizer doesn't recognize. + * Both mean "sql-formatter can't handle this text", so both should fall back to the + * original SQL instead of surfacing a failure. + */ function isSqlFormatterParseError(error: unknown): boolean { - if (!(error instanceof Error) || !error.message.startsWith("Parse error at token:")) return false; + if (!(error instanceof Error)) return false; + if (error.message.startsWith("Parse error: Unexpected ")) return true; + if (!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; }