diff --git a/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts b/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts index 1907c383b0..2592095712 100644 --- a/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts @@ -75,4 +75,37 @@ describe("codemirrorSqlDialect", () => { expect(createDbxCodeMirrorSqlDialect(langSql, "mysql", databaseType).spec.doubleQuotedStrings, databaseType).toBe(false); } }); + + it("enables backslashEscapes for MySQL-family dialects and ClickHouse while keeping it disabled for standard dialects", () => { + const backslashEscapesTypes: DatabaseType[] = ["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase", "clickhouse", "hive", "spark", "impala", "argo", "databend"]; + for (const databaseType of backslashEscapesTypes) { + expect(createDbxCodeMirrorSqlDialect(langSql, "mysql", databaseType).spec.backslashEscapes, databaseType).toBe(true); + } + + const standardTypes: DatabaseType[] = ["postgres", "sqlserver", "sqlite", "oracle", "dameng"]; + for (const databaseType of standardTypes) { + expect(createDbxCodeMirrorSqlDialect(langSql, "mysql", databaseType).spec.backslashEscapes, databaseType).toBeUndefined(); + } + }); + + it("correctly tokenizes string literals containing escaped quotes in MySQL statements without corrupting trailing code", () => { + const dialect = createDbxCodeMirrorSqlDialect(langSql, "mysql", "mysql"); + const statement = ["SELECT CONCAT('\\'', sl.id) id, sl.settlement_price_tax '本次结算金额含税',", "CASE sc.`type`", " WHEN 0 THEN '物资'", " WHEN 1 THEN '设备'", " ELSE ''", "END AS '合同类型'"].join("\n"); + + expect(nodeNameAt(dialect, statement, "'\\''")).toBe("String"); + expect(nodeNameAt(dialect, statement, "'本次结算金额含税'")).toBe("String"); + expect(nodeNameAt(dialect, statement, "CASE")).toBe("Keyword"); + expect(nodeNameAt(dialect, statement, "`type`")).toBe("QuotedIdentifier"); + expect(nodeNameAt(dialect, statement, "WHEN")).toBe("Keyword"); + expect(nodeNameAt(dialect, statement, "THEN")).toBe("Keyword"); + expect(nodeNameAt(dialect, statement, "'物资'")).toBe("String"); + expect(nodeNameAt(dialect, statement, "'设备'")).toBe("String"); + expect(nodeNameAt(dialect, statement, "''")).toBe("String"); + expect(nodeNameAt(dialect, statement, "END")).toBe("Keyword"); + expect(nodeNameAt(dialect, statement, "'合同类型'")).toBe("String"); + + const doubleQuoteStatement = 'SELECT "escaped\\"quote", col FROM tbl'; + expect(nodeNameAt(dialect, doubleQuoteStatement, '"escaped\\"quote"')).toBe("String"); + expect(nodeNameAt(dialect, doubleQuoteStatement, "col")).toBe("Identifier"); + }); }); diff --git a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts index b7de10824d..798e15f888 100644 --- a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts +++ b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts @@ -10,6 +10,11 @@ const MYSQL_CODEMIRROR_DATABASE_TYPES = new Set(["mysql", "doris", const POSTGRES_CODEMIRROR_DATABASE_TYPES = new Set(["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "uxdb", "vastbase", "opengauss", "questdb"]); const ORACLE_CODEMIRROR_DATABASE_TYPES = new Set(["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]); const SQLITE_CODEMIRROR_DATABASE_TYPES = new Set(["sqlite", "rqlite", "turso", "cloudflare-d1"]); +// Non-MySQL-wire dialects whose servers still interpret backslash escapes in string +// literals; the mysql-family types are covered by isMysql at the define() site. Mirrors +// BACKSLASH_ESCAPE_STRING_DIALECTS in lib/sql/sqlStatementRanges.ts so the editor +// tokenizer and the statement splitter agree on escape semantics per dialect. +const BACKSLASH_ESCAPE_CODEMIRROR_DATABASE_TYPES = new Set(["hive", "argo", "impala", "spark", "databend"]); const CODEMIRROR_SQLITE_EXTENSION_KEYWORDS = new Set("abort analyze attach autoincrement conflict database detach exclusive fail glob ignore index indexed instead isnull notnull offset plan pragma query raise regexp reindex rename replace temp vacuum virtual".split(" ")); const STANDARD_SQL_TYPES = "array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying"; @@ -245,6 +250,11 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu spaceAfterDashes: false, } : {}), + ...(isMysql || (databaseType && BACKSLASH_ESCAPE_CODEMIRROR_DATABASE_TYPES.has(databaseType)) + ? { + backslashEscapes: true, + } + : {}), ...(isPlsql ? { doubleQuotedStrings: false } : {}), doubleDollarQuotedStrings: false, });