fix(sql): only treat backslash as string escape for MySQL-family dialects
Closes #8189
This commit is contained in:
3 files changed
+39
-6
No files matched your search
@@ -369,6 +369,20 @@ describe("splitSqlStatementRanges", () => {
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 'it''s; ok'", "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("treats backslash as a literal inside single quotes for standard SQL (issue #8189)", () => {
|
||||
// ESCAPE '\' is a complete one-char string in standard SQL; the closing quote must not be
|
||||
// swallowed, so the following statement keeps its own range (and thus its run button).
|
||||
const sql = "SELECT * FROM MAXRELATIONSHIP m WHERE m.PARENT LIKE 'A\\_%' ESCAPE '\\';\nSELECT * FROM USER;";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "postgres"))).toEqual(["SELECT * FROM MAXRELATIONSHIP m WHERE m.PARENT LIKE 'A\\_%' ESCAPE '\\'", "SELECT * FROM USER"]);
|
||||
// No databaseType (generic) must behave the same as a standard-SQL dialect.
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql)).length).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps backslash-escaped quotes as one string for MySQL (no regression)", () => {
|
||||
const sql = "SELECT 'a\\'b; still string';\nSELECT 2";
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "mysql"))).toEqual(["SELECT 'a\\'b; still string'", "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("ignores semicolons inside double-quoted identifiers", () => {
|
||||
const sql = 'SELECT "a;b";\nSELECT 2';
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(['SELECT "a;b"', "SELECT 2"]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user