diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index 302cced929..db67954da1 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -281,6 +281,11 @@ describe("splitSqlStatementRanges", () => { expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2"]); }); + it("keeps SQL Server temporary table names instead of treating them as hash comments", () => { + const sql = "DROP TABLE IF EXISTS #Temp;\nSELECT * FROM ##GlobalTemp;"; + expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["DROP TABLE IF EXISTS #Temp", "SELECT * FROM ##GlobalTemp"]); + }); + it("keeps MyBatis placeholders instead of treating them as hash comments", () => { const sql = "SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym};\nSELECT 2"; expect(rangeSqlTexts(splitSqlStatementRanges(sql, "kingbase"))).toEqual(["SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym}", "SELECT 2"]); diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index 8462c07453..385fc5727b 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -433,7 +433,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType i = newline === -1 ? len : newline + 1; continue; } - if (startsHashLineComment(sql, i, parameterOptions)) { + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { const newline = sql.indexOf("\n", i); i = newline === -1 ? len : newline + 1; continue; @@ -651,8 +651,8 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d if (lineStarts.length <= 1) return [statement]; const boundaries: Array<{ hitFrom: number; from: number; keyword: string }> = []; - let currentKeyword = softStatementKeywordAt(sql, statement.from, databaseType); - let currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, statement.from); + let currentKeyword = softStatementKeywordAt(sql, statement.from, databaseType, parameterOptions); + let currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, statement.from, databaseType, parameterOptions); let currentBodyKeyword = currentExplainTargetKeyword ?? currentKeyword; let consumedWithMainStatement = false; let consumedExplainStatement = false; @@ -671,7 +671,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d continue; } - if (isSetOperationQueryContinuation(sql, statement.from, lineStart.from, lineStart.keyword, parameterOptions)) { + if (isSetOperationQueryContinuation(sql, statement.from, lineStart.from, lineStart.keyword, databaseType, parameterOptions)) { continue; } @@ -717,7 +717,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d boundaries.push(lineStart); currentKeyword = lineStart.keyword; - currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, lineStart.from); + currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, lineStart.from, databaseType, parameterOptions); currentBodyKeyword = currentExplainTargetKeyword ?? currentKeyword; consumedWithMainStatement = false; consumedExplainStatement = false; @@ -729,7 +729,7 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d for (let index = 0; index < boundaries.length; index += 1) { const boundary = boundaries[index]; const next = boundaries[index + 1]; - const to = next ? trimRangeEndBeforeNextBoundary(sql, boundary.from, next.from, parameterOptions) : trimRangeEnd(sql, boundary.from, statement.to); + const to = next ? trimRangeEndBeforeNextBoundary(sql, boundary.from, next.from, databaseType, parameterOptions) : trimRangeEnd(sql, boundary.from, statement.to); if (to > boundary.from) { ranges.push({ hitFrom: boundary.hitFrom, @@ -749,7 +749,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d const explainOptionsStart = explainOptionsParenAt(sql, statement.from); // 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) === null ? explainOptionsStart : null; + const unclosedExplainOptionsStart = explainOptionsStart !== null && skipBalancedParens(sql, explainOptionsStart, databaseType, parameterOptions) === null ? explainOptionsStart : null; let state: QuoteState | "lineComment" | "blockComment" = "none"; let dollarTag = ""; let parenDepth = 0; @@ -761,10 +761,10 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d const ch = sql[i]; const next = sql[i + 1] ?? ""; - if (state === "none" && firstNonWhitespaceOnLine === -1 && ch !== "\n" && ch !== "\r" && !isSqlWhitespace(ch) && !startsLineComment(sql, i, parameterOptions) && !startsBlockComment(sql, i)) { + if (state === "none" && firstNonWhitespaceOnLine === -1 && ch !== "\n" && ch !== "\r" && !isSqlWhitespace(ch) && !startsLineComment(sql, i, databaseType, parameterOptions) && !startsBlockComment(sql, i)) { firstNonWhitespaceOnLine = i; if (parenDepth === 0) { - const keyword = softStatementKeywordAt(sql, i, databaseType); + const keyword = softStatementKeywordAt(sql, i, databaseType, parameterOptions); if (keyword) { starts.push({ hitFrom: lineStart, from: i, keyword }); } @@ -860,7 +860,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d i += 2; continue; } - if (startsHashLineComment(sql, i, parameterOptions)) { + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { state = "lineComment"; i += 1; continue; @@ -910,14 +910,14 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d return starts; } -function softStatementKeywordAt(sql: string, pos: number, databaseType?: DatabaseType): string | null { +function softStatementKeywordAt(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): string | null { const match = /^[A-Za-z_][\w$]*/.exec(sql.slice(pos)); if (!match) return null; const keyword = match[0].toUpperCase(); if (keyword === "REPLACE" && nextNonWhitespaceChar(sql, pos + match[0].length) === "(") return null; // COMMENT is also a common column name. Only COMMENT ON starts a standalone // SQL command; otherwise a line-start projection column must stay in SELECT. - if (keyword === "COMMENT" && nextSqlWord(sql, pos + match[0].length) !== "ON") return null; + if (keyword === "COMMENT" && nextSqlWord(sql, pos + match[0].length, databaseType, parameterOptions) !== "ON") return null; return softStatementStartKeywords(databaseType).has(keyword) ? keyword : null; } @@ -925,9 +925,9 @@ function softStatementStartKeywords(databaseType?: DatabaseType): Set { return new Set([...COMMON_SOFT_STATEMENT_START_KEYWORDS, ...(databaseType ? (DATABASE_SOFT_STATEMENT_KEYWORDS[databaseType] ?? []) : [])]); } -function isSetOperationQueryContinuation(sql: string, from: number, to: number, keyword: string, parameterOptions?: SqlParameterOptions): boolean { +function isSetOperationQueryContinuation(sql: string, from: number, to: number, keyword: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean { if (keyword !== "SELECT" && keyword !== "WITH") return false; - const words = topLevelWordsBefore(sql, from, to, 3, parameterOptions); + const words = topLevelWordsBefore(sql, from, to, 3, databaseType, parameterOptions); const last = words[words.length - 1]; if (last && SET_OPERATION_KEYWORDS.has(last)) return true; if (last && SET_OPERATION_MODIFIER_KEYWORDS.has(last)) { @@ -953,8 +953,8 @@ function isClickHouseAlterTableUpdateContinuation(sql: string, statementFrom: nu function isMysqlAlterTableTruncatePartitionContinuation(sql: string, statementFrom: number, lineStartFrom: number, keyword: string, databaseType?: DatabaseType): boolean { if (databaseType !== "mysql" || keyword !== "TRUNCATE") return false; - if (!startsWithSqlWords(sql, statementFrom, ["ALTER", "TABLE"])) return false; - return nextSqlWord(sql, lineStartFrom + keyword.length) === "PARTITION"; + if (!startsWithSqlWords(sql, statementFrom, ["ALTER", "TABLE"], databaseType)) return false; + return nextSqlWord(sql, lineStartFrom + keyword.length, databaseType) === "PARTITION"; } function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean { @@ -962,7 +962,7 @@ function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text); } -function topLevelWordsBefore(sql: string, from: number, to: number, limit: number, parameterOptions?: SqlParameterOptions): string[] { +function topLevelWordsBefore(sql: string, from: number, to: number, limit: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): string[] { const words: string[] = []; let state: QuoteState | "lineComment" | "blockComment" = "none"; let dollarTag = ""; @@ -1054,7 +1054,7 @@ function topLevelWordsBefore(sql: string, from: number, to: number, limit: numbe i += 2; continue; } - if (startsHashLineComment(sql, i, parameterOptions)) { + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { state = "lineComment"; i += 1; continue; @@ -1124,21 +1124,21 @@ function nextNonWhitespaceChar(sql: string, pos: number): string | null { return i < sql.length ? sql[i] : null; } -function nextSqlWord(sql: string, pos: number): string | null { - return nextSqlWordToken(sql, pos)?.word ?? null; +function nextSqlWord(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): string | null { + return nextSqlWordToken(sql, pos, databaseType, parameterOptions)?.word ?? null; } -function startsWithSqlWords(sql: string, pos: number, expectedWords: readonly string[]): boolean { +function startsWithSqlWords(sql: string, pos: number, expectedWords: readonly string[], databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean { let i = pos; for (const expectedWord of expectedWords) { - const token = nextSqlWordToken(sql, i); + const token = nextSqlWordToken(sql, i, databaseType, parameterOptions); if (token?.word !== expectedWord) return false; i = token.end; } return true; } -function nextSqlWordToken(sql: string, pos: number): { word: string; end: number } | null { +function nextSqlWordToken(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): { word: string; end: number } | null { let i = pos; // SQL comments may legally appear anywhere whitespace can separate keywords. while (i < sql.length) { @@ -1151,7 +1151,7 @@ function nextSqlWordToken(sql: string, pos: number): { word: string; end: number while (i < sql.length && sql[i] !== "\n" && sql[i] !== "\r") i += 1; continue; } - if (sql[i] === "#") { + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { i += 1; while (i < sql.length && sql[i] !== "\n" && sql[i] !== "\r") i += 1; continue; @@ -1183,7 +1183,7 @@ function explainOptionsParenAt(sql: string, pos: number): number | null { return sql[i] === "(" ? i : null; } -function explainLikeTargetKeywordAt(sql: string, pos: number): string | null { +function explainLikeTargetKeywordAt(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): string | null { const prefixMatch = /^[A-Za-z_][\w$]*/.exec(sql.slice(pos)); const prefix = prefixMatch?.[0]?.toUpperCase(); if (!isExplainLikeKeyword(prefix ?? null)) return null; @@ -1194,7 +1194,7 @@ function explainLikeTargetKeywordAt(sql: string, pos: number): string | null { // sit between the keyword and its target statement. DESC/DESCRIBE take no // options — a paren there is a subquery (ClickHouse `DESCRIBE (SELECT ...)`). if (prefix === "EXPLAIN" && sql[i] === "(") { - const optionsEnd = skipBalancedParens(sql, i); + const optionsEnd = skipBalancedParens(sql, i, databaseType, parameterOptions); if (optionsEnd === null) return null; i = optionsEnd; while (i < sql.length && isSqlWhitespace(sql[i])) i += 1; @@ -1204,7 +1204,7 @@ function explainLikeTargetKeywordAt(sql: string, pos: number): string | null { return targetKeyword && EXPLAIN_STATEMENT_KEYWORDS.has(targetKeyword) ? targetKeyword : null; } -function skipBalancedParens(sql: string, pos: number): number | null { +function skipBalancedParens(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): number | null { let state: "none" | "single" | "double" | "lineComment" | "blockComment" = "none"; let depth = 0; let i = pos; @@ -1251,6 +1251,11 @@ function skipBalancedParens(sql: string, pos: number): number | null { i += 2; continue; } + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { + state = "lineComment"; + i += 1; + continue; + } if (ch === "/" && next === "*") { state = "blockComment"; i += 2; @@ -1277,12 +1282,12 @@ function skipBalancedParens(sql: string, pos: number): number | null { return null; } -function startsLineComment(sql: string, pos: number, parameterOptions?: SqlParameterOptions): boolean { - return (sql[pos] === "-" && sql[pos + 1] === "-") || startsHashLineComment(sql, pos, parameterOptions); +function startsLineComment(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean { + return (sql[pos] === "-" && sql[pos + 1] === "-") || startsHashLineComment(sql, pos, databaseType, parameterOptions); } -function startsHashLineComment(sql: string, pos: number, parameterOptions?: SqlParameterOptions): boolean { - if (sql[pos] !== "#") return false; +function startsHashLineComment(sql: string, pos: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean { + if (databaseType === "sqlserver" || sql[pos] !== "#") return false; return readSqlBracedParameterAt(sql, pos, parameterOptions)?.syntax !== "mybatis"; } @@ -1298,7 +1303,7 @@ function trimRangeEnd(sql: string, from: number, to: number): number { return end; } -function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryFrom: number, parameterOptions?: SqlParameterOptions): number { +function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryFrom: number, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): number { let state: QuoteState | "lineComment" | "blockComment" = "none"; let dollarTag = ""; let lastContentEnd = from; @@ -1399,7 +1404,7 @@ function trimRangeEndBeforeNextBoundary(sql: string, from: number, nextBoundaryF i += 2; continue; } - if (startsHashLineComment(sql, i, parameterOptions)) { + if (startsHashLineComment(sql, i, databaseType, parameterOptions)) { state = "lineComment"; i += 1; continue; @@ -1587,7 +1592,7 @@ function mysqlRoutineTokens(sql: string, parameterOptions?: SqlParameterOptions) i += 2; continue; } - if (startsHashLineComment(sql, i, parameterOptions)) { + if (startsHashLineComment(sql, i, undefined, parameterOptions)) { state = "lineComment"; i += 1; continue;