fix(sql): ignore native SQL variables

This commit is contained in:
miracle authored and GitHub committed 2026-07-07 22:24:48 +08:00
1 parent 6cba08fccb
commit 9288717ac4
3 files changed
+440 -9

No files matched your search

+252 -7
View File
@@ -22,6 +22,7 @@ interface ParameterOccurrence extends SqlParameterDescriptor {
const PARAMETER_NAME_RE = /^[\p{L}_][\p{L}\p{N}_]*$/u;
const PARAMETER_NAME_START_RE = /[\p{L}_]/u;
const PARAMETER_NAME_CHAR_RE = /[\p{L}\p{N}_]/u;
const SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS = new Set(["table", "from", "join", "into", "update", "truncate"]);
export function extractSqlParameters(sql: string): string[] {
return extractSqlParameterDescriptors(sql).map((descriptor) => descriptor.key);
@@ -69,7 +70,7 @@ export function sqlParameterLiteral(input: SqlParameterInput): string {
function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
const occurrences: ParameterOccurrence[] = [];
const declaredSqlServerVariables = collectDeclaredSqlServerVariables(sql);
const nativeSqlServerParameters = collectNativeSqlServerParameters(sql);
let i = 0;
let dollarQuoteEnd = "";
let positionalIndex = 0;
@@ -146,13 +147,13 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
}
}
}
if (ch === "#") {
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@" && !declaredSqlServerVariables.has(name.toLowerCase())) {
if (name && next !== "@" && sql[i - 1] !== "@" && !nativeSqlServerParameters.declared.has(name.toLowerCase()) && !nativeSqlServerParameters.ignoredStarts.has(i)) {
occurrences.push({
key: name,
name,
@@ -179,8 +180,9 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
return occurrences;
}
function collectDeclaredSqlServerVariables(sql: string): Set<string> {
function collectNativeSqlServerParameters(sql: string): { declared: Set<string>; ignoredStarts: Set<number> } {
const declared = new Set<string>();
const ignoredStarts = new Set<number>();
let i = 0;
let dollarQuoteEnd = "";
@@ -219,7 +221,7 @@ function collectDeclaredSqlServerVariables(sql: string): Set<string> {
continue;
}
}
if (ch === "#") {
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
@@ -227,10 +229,26 @@ function collectDeclaredSqlServerVariables(sql: string): Set<string> {
i = collectDeclareStatementVariables(sql, i + "declare".length, declared);
continue;
}
if (matchesWord(sql, i, "set")) {
i = collectSetStatementVariables(sql, i + "set".length, declared);
continue;
}
if (matchesWord(sql, i, "select")) {
i = collectSelectAssignmentVariables(sql, i + "select".length, declared);
continue;
}
if ((matchesWord(sql, i, "create") || matchesWord(sql, i, "alter")) && isRoutineDefinitionStart(sql, i)) {
i = collectRoutineDefinitionVariables(sql, i, declared);
continue;
}
if (matchesWord(sql, i, "exec") || matchesWord(sql, i, "execute")) {
i = collectExecNamedArgumentStarts(sql, i + (matchesWord(sql, i, "exec") ? "exec".length : "execute".length), ignoredStarts);
continue;
}
i += 1;
}
return declared;
return { declared, ignoredStarts };
}
function collectDeclareStatementVariables(sql: string, start: number, declared: Set<string>): number {
@@ -256,7 +274,7 @@ function collectDeclareStatementVariables(sql: string, start: number, declared:
i = skipBlockComment(sql, i + 2);
continue;
}
if (ch === "#") {
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
@@ -273,6 +291,208 @@ function collectDeclareStatementVariables(sql: string, start: number, declared:
return i;
}
function collectSetStatementVariables(sql: string, start: number, declared: Set<string>): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
if (ch === ";") return i + 1;
if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i;
if (ch === "'" || ch === '"' || ch === "`") {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
continue;
}
if (ch === "-" && next === "-") {
i = skipLine(sql, i + 2);
continue;
}
if (ch === "/" && next === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@" && isSetAssignmentTarget(sql, i + 1 + name.length)) {
declared.add(name.toLowerCase());
i += 1 + name.length;
continue;
}
}
i += 1;
}
return i;
}
function collectSelectAssignmentVariables(sql: string, start: number, declared: Set<string>): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
if (ch === ";") return i + 1;
if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i;
if (matchesWord(sql, i, "from")) return i;
if (ch === "'" || ch === '"' || ch === "`") {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
continue;
}
if (ch === "-" && next === "-") {
i = skipLine(sql, i + 2);
continue;
}
if (ch === "/" && next === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@" && isSetAssignmentTarget(sql, i + 1 + name.length)) {
declared.add(name.toLowerCase());
i += 1 + name.length;
continue;
}
}
i += 1;
}
return i;
}
function collectRoutineDefinitionVariables(sql: string, start: number, declared: Set<string>): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
if (ch === ";") return i + 1;
if (matchesWord(sql, i, "as") || matchesWord(sql, i, "returns")) return i;
if (ch === "'" || ch === '"' || ch === "`") {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
continue;
}
if (ch === "-" && next === "-") {
i = skipLine(sql, i + 2);
continue;
}
if (ch === "/" && next === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@") {
declared.add(name.toLowerCase());
i += 1 + name.length;
continue;
}
}
i += 1;
}
return i;
}
function collectExecNamedArgumentStarts(sql: string, start: number, ignoredStarts: Set<number>): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
if (ch === ";") return i + 1;
if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i;
if (ch === "'" || ch === '"' || ch === "`") {
i = skipQuoted(sql, i, ch);
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
continue;
}
if (ch === "-" && next === "-") {
i = skipLine(sql, i + 2);
continue;
}
if (ch === "/" && next === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
if (isHashLineComment(sql, i)) {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@" && isSetAssignmentTarget(sql, i + 1 + name.length)) {
ignoredStarts.add(i);
i += 1 + name.length;
continue;
}
}
i += 1;
}
return i;
}
function isRoutineDefinitionStart(sql: string, start: number): boolean {
const keyword = matchesWord(sql, start, "create") ? "create" : matchesWord(sql, start, "alter") ? "alter" : "";
if (!keyword) return false;
let next = readNextKeyword(sql, start + keyword.length);
if (!next) return false;
if (keyword === "create" && next.word === "or") {
const afterOr = readNextKeyword(sql, next.end);
if (!afterOr || (afterOr.word !== "alter" && afterOr.word !== "replace")) return false;
next = readNextKeyword(sql, afterOr.end);
if (!next) return false;
}
return next.word === "procedure" || next.word === "proc" || next.word === "function";
}
function readNextKeyword(sql: string, start: number): { word: string; end: number } | null {
let i = start;
while (i < sql.length) {
while (i < sql.length && /\s/.test(sql[i])) i += 1;
if (sql[i] === "-" && sql[i + 1] === "-") {
i = skipLine(sql, i + 2);
continue;
}
if (sql[i] === "/" && sql[i + 1] === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
break;
}
if (!PARAMETER_NAME_START_RE.test(sql[i] ?? "")) return null;
let end = i + 1;
while (end < sql.length && PARAMETER_NAME_CHAR_RE.test(sql[end])) end += 1;
return { word: sql.slice(i, end).toLowerCase(), end };
}
function isSetAssignmentTarget(sql: string, start: number): boolean {
let i = start;
while (i < sql.length && /\s/.test(sql[i])) i += 1;
return sql[i] === "=" || (sql[i] === ":" && sql[i + 1] === "=");
}
function isLineStatementStart(sql: string, start: number): boolean {
let i = start - 1;
while (i >= 0 && (sql[i] === " " || sql[i] === "\t" || sql[i] === "\r")) i -= 1;
@@ -340,6 +560,31 @@ function skipBlockComment(sql: string, start: number): number {
return end === -1 ? sql.length : end + 2;
}
function isHashLineComment(sql: string, start: number): boolean {
if (sql[start] !== "#" || sql[start + 1] === "{") return false;
// Keep SQL Server #temp table names parseable while treating other # tokens as MySQL-style comments.
return !isSqlServerTempTableReference(sql, start);
}
function isSqlServerTempTableReference(sql: string, start: number): boolean {
let nameStart = start + 1;
if (sql[nameStart] === "#") nameStart += 1;
if (!PARAMETER_NAME_START_RE.test(sql[nameStart] ?? "")) return false;
const previous = previousKeyword(sql, start);
return !!previous && SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS.has(previous);
}
function previousKeyword(sql: string, start: number): string {
let end = start - 1;
while (end >= 0 && /\s/.test(sql[end])) end -= 1;
let begin = end;
while (begin >= 0 && PARAMETER_NAME_CHAR_RE.test(sql[begin])) begin -= 1;
begin += 1;
if (begin > end || !PARAMETER_NAME_START_RE.test(sql[begin] ?? "")) return "";
return sql.slice(begin, end + 1).toLowerCase();
}
function readDollarQuoteMarker(sql: string, start: number): string {
const match = sql.slice(start).match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/);
return match?.[0] ?? "";