fix(sql): only treat backslash as string escape for MySQL-family dialects

Closes #8189
This commit is contained in:
t8y2 committed 2026-09-04 18:55:27 +08:00
1 parent 4047686f93
commit e56823334f
3 files changed
+39 -6

No files matched your search

+3 -1
View File
@@ -12,6 +12,7 @@ import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints";
import type { SqlSemanticBuildOptions, SqlSemanticSpan } from "@/lib/sql/semantic/types";
import { isEditorStatePlausibleFor, resolveLexicalLeafFromSyntaxTree, resolveSqlStatementWindow } from "@/lib/sql/sqlSyntaxTreeWindow";
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
import { BACKSLASH_ESCAPE_STRING_DIALECTS } from "@/lib/sql/sqlStatementRanges";
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identifierSearch";
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
@@ -2887,7 +2888,8 @@ const MYSQL_DASH_COMMENT_DIALECTS = new Set<DatabaseType>(["mysql", "doris", "st
// why that's unsafe (a trailing backslash before a closing quote in a dialect that doesn't escape
// it, e.g. a Postgres Windows-path string literal, would misread the real closing quote as
// escaped and swallow the rest of the query).
const BACKSLASH_ESCAPE_STRING_DIALECTS = new Set<DatabaseType>(["mysql", "doris", "starrocks", "hive", "argo", "impala", "spark"]);
// The authoritative set lives in sqlStatementRanges.ts (the statement splitter depends on it too);
// import it here to keep a single source of truth.
// Table/schema/db unquoted-identifier continue class needs @ and # in addition to what
// SQL_IDENTIFIER_CONTINUE_CHAR covers, so splice its inner class body into a locally-built class
+22 -5
View File
@@ -273,6 +273,15 @@ const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
// ranges must stay whole instead of splitting at every body semicolon.
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle", "xugu", "argo"]);
const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]);
// Backslash escaping inside '...'/"..." strings is a MySQL-family extension; in standard SQL '\'
// is a complete one-char string and quotes are escaped by doubling (''). Treating backslash as an
// escape unconditionally makes ESCAPE '\' swallow its closing quote and the following statement
// boundary, so the next statement loses its run button (#8189). Gate it by dialect, matching the
// tokenizer/completion side.
export const BACKSLASH_ESCAPE_STRING_DIALECTS: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "hive", "argo", "impala", "spark", "databend"]);
function allowsBackslashStringEscape(databaseType?: DatabaseType): boolean {
return !!databaseType && BACKSLASH_ESCAPE_STRING_DIALECTS.has(databaseType);
}
const MYSQL_CREATE_TABLE_OPTION_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]);
const MYSQL_ROUTINE_OBJECT_TYPES = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]);
const MYSQL_NON_ROUTINE_CREATE_TYPES = new Set(["DATABASE", "INDEX", "LOGFILE", "ROLE", "SCHEMA", "SERVER", "SPATIAL", "TABLE", "TEMPORARY", "UNIQUE", "USER", "VIEW"]);
@@ -303,6 +312,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
const statements: RawStatement[] = [];
const len = sql.length;
const supportsDelimiterCommands = databaseType === "mysql";
const backslashEscapes = allowsBackslashStringEscape(databaseType);
let statementStart = -1;
let statementEnd = -1;
@@ -419,8 +429,10 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
if (state === "single") {
markContent(i);
// Backslash escapes the next char (e.g. PostgreSQL standard_conforming_strings=off style).
if (ch === "\\" && next) {
// Only MySQL-family dialects treat backslash as an escape inside '...' (see
// BACKSLASH_ESCAPE_STRING_DIALECTS); in standard SQL '\' is a literal char and must not
// consume the next char, otherwise the closing quote is swallowed (#8189).
if (ch === "\\" && next && backslashEscapes) {
i += 2;
continue;
}
@@ -836,6 +848,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d
// Recover soft statement boundaries while the user is still typing an
// EXPLAIN option list; otherwise its unmatched opener hides every later line.
const unclosedExplainOptionsStart = explainOptionsStart !== null && skipBalancedParens(sql, explainOptionsStart, databaseType, parameterOptions) === null ? explainOptionsStart : null;
const backslashEscapes = allowsBackslashStringEscape(databaseType);
let state: QuoteState | "lineComment" | "blockComment" = "none";
let dollarTag = "";
let parenDepth = 0;
@@ -895,7 +908,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d
}
if (state === "single") {
if (ch === "\\" && next) {
if (ch === "\\" && next && backslashEscapes) {
i += 2;
continue;
}
@@ -1058,6 +1071,7 @@ function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean
function topLevelWordsBefore(sql: string, from: number, to: number, limit: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): string[] {
const words: string[] = [];
const backslashEscapes = allowsBackslashStringEscape(databaseType);
let state: QuoteState | "lineComment" | "blockComment" = "none";
let dollarTag = "";
let parenDepth = 0;
@@ -1098,7 +1112,7 @@ function topLevelWordsBefore(sql: string, from: number, to: number, limit: numbe
}
if (state === "single") {
if (ch === "\\" && next) {
if (ch === "\\" && next && backslashEscapes) {
i += 2;
continue;
}
@@ -1398,6 +1412,7 @@ function trimRangeEnd(sql: string, from: number, to: number): number {
}
function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryFrom: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): number {
const backslashEscapes = allowsBackslashStringEscape(databaseType);
let state: QuoteState | "lineComment" | "blockComment" = "none";
let dollarTag = "";
let lastContentEnd = from;
@@ -1441,7 +1456,7 @@ function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryF
if (state === "single") {
lastContentEnd = i + 1;
if (ch === "\\" && next) {
if (ch === "\\" && next && backslashEscapes) {
i += 2;
lastContentEnd = i;
continue;
@@ -1724,6 +1739,8 @@ function mysqlRoutineTokens(sql: string, parameterOptions?: SqlParameterOptions,
continue;
}
if (state === "single") {
// mysqlRoutineTokens runs only for MYSQL_ROUTINE_BLOCK_DATABASES (all MySQL-family), so
// backslash escaping here (and in the double branch below) is unconditionally correct.
if (ch === "\\" && next) {
i += 2;
continue;