[Feat]:实现SQL变量替换 (#2597)

This commit is contained in:
miracle authored and GitHub committed 2026-07-06 13:21:45 +08:00
1 parent 987b0fd263
commit b118ca31ed
12 files changed
+306 -33

No files matched your search

+198 -9
View File
@@ -5,20 +5,42 @@ export interface SqlParameterInput {
value: string;
}
interface ParameterOccurrence {
export type SqlParameterSyntax = "positional" | "named" | "shell" | "mybatis" | "sqlserver";
export interface SqlParameterDescriptor {
key: string;
name: string;
syntax: SqlParameterSyntax;
token: string;
}
interface ParameterOccurrence extends SqlParameterDescriptor {
start: number;
end: number;
}
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;
export function extractSqlParameters(sql: string): string[] {
return extractSqlParameterDescriptors(sql).map((descriptor) => descriptor.key);
}
export function extractSqlParameterDescriptors(sql: string): SqlParameterDescriptor[] {
const names = new Set<string>();
const descriptors: SqlParameterDescriptor[] = [];
for (const occurrence of findSqlParameterOccurrences(sql)) {
names.add(occurrence.name);
if (names.has(occurrence.key)) continue;
names.add(occurrence.key);
descriptors.push({
key: occurrence.key,
name: occurrence.name,
syntax: occurrence.syntax,
token: occurrence.token,
});
}
return [...names];
return descriptors;
}
export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>): string {
@@ -29,7 +51,7 @@ export function substituteSqlParameters(sql: string, values: Record<string, SqlP
let cursor = 0;
for (const occurrence of occurrences) {
result += sql.slice(cursor, occurrence.start);
result += sqlParameterLiteral(values[occurrence.name] ?? { kind: "string", value: "" });
result += sqlParameterLiteral(values[occurrence.key] ?? { kind: "string", value: "" });
cursor = occurrence.end;
}
result += sql.slice(cursor);
@@ -47,8 +69,10 @@ export function sqlParameterLiteral(input: SqlParameterInput): string {
function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
const occurrences: ParameterOccurrence[] = [];
const declaredSqlServerVariables = collectDeclaredSqlServerVariables(sql);
let i = 0;
let dollarQuoteEnd = "";
let positionalIndex = 0;
while (i < sql.length) {
if (dollarQuoteEnd) {
@@ -74,25 +98,73 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
i = skipLine(sql, i + 2);
continue;
}
if (ch === "#") {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "/" && next === "*") {
i = skipBlockComment(sql, i + 2);
continue;
}
if (ch === "?") {
positionalIndex += 1;
const key = `?${positionalIndex}`;
occurrences.push({ key, name: key, syntax: "positional", token: "?", start: i, end: i + 1 });
i += 1;
continue;
}
if (ch === ":") {
const name = readParameterName(sql, i + 1);
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") {
occurrences.push({
key: name,
name,
syntax: "named",
token: sql.slice(i, i + 1 + name.length),
start: i,
end: i + 1 + name.length,
});
i += 1 + name.length;
continue;
}
}
if (ch === "$" && next === "{") {
const end = sql.indexOf("}", i + 2);
if (end !== -1) {
const name = sql.slice(i + 2, end).trim();
if (PARAMETER_NAME_RE.test(name)) {
occurrences.push({ name, start: i, end: end + 1 });
occurrences.push({ key: name, name, syntax: "shell", token: sql.slice(i, end + 1), start: i, end: end + 1 });
i = end + 1;
continue;
}
}
}
if (ch === "#" && next === "{") {
const end = sql.indexOf("}", i + 2);
if (end !== -1) {
const name = sql.slice(i + 2, end).trim();
if (PARAMETER_NAME_RE.test(name)) {
occurrences.push({ key: name, name, syntax: "mybatis", token: sql.slice(i, end + 1), start: i, end: end + 1 });
i = end + 1;
continue;
}
}
}
if (ch === "#") {
i = skipLine(sql, i + 1);
continue;
}
if (ch === "@") {
const name = readParameterName(sql, i + 1);
if (name && next !== "@" && sql[i - 1] !== "@" && !declaredSqlServerVariables.has(name.toLowerCase())) {
occurrences.push({
key: name,
name,
syntax: "sqlserver",
token: sql.slice(i, i + 1 + name.length),
start: i,
end: i + 1 + name.length,
});
i += 1 + name.length;
continue;
}
}
if (ch === "$") {
const marker = readDollarQuoteMarker(sql, i);
if (marker) {
@@ -107,6 +179,123 @@ function findSqlParameterOccurrences(sql: string): ParameterOccurrence[] {
return occurrences;
}
function collectDeclaredSqlServerVariables(sql: string): Set<string> {
const declared = new Set<string>();
let i = 0;
let dollarQuoteEnd = "";
while (i < sql.length) {
if (dollarQuoteEnd) {
const end = sql.indexOf(dollarQuoteEnd, i);
if (end === -1) break;
i = end + dollarQuoteEnd.length;
dollarQuoteEnd = "";
continue;
}
const ch = sql[i];
const next = sql[i + 1];
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 (ch === "$") {
const marker = readDollarQuoteMarker(sql, i);
if (marker) {
dollarQuoteEnd = marker;
i += marker.length;
continue;
}
}
if (ch === "#") {
i = skipLine(sql, i + 1);
continue;
}
if (matchesWord(sql, i, "declare")) {
i = collectDeclareStatementVariables(sql, i + "declare".length, declared);
continue;
}
i += 1;
}
return declared;
}
function collectDeclareStatementVariables(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 (ch === "#") {
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 isLineStatementStart(sql: string, start: number): boolean {
let i = start - 1;
while (i >= 0 && (sql[i] === " " || sql[i] === "\t" || sql[i] === "\r")) i -= 1;
return i >= 0 && sql[i] === "\n";
}
function isSqlStatementKeyword(sql: string, start: number): boolean {
return ["select", "with", "insert", "update", "delete", "merge", "exec", "execute", "set", "if", "while", "begin", "create", "alter", "drop", "truncate"].some((keyword) => matchesWord(sql, start, keyword));
}
function matchesWord(sql: string, start: number, word: string): boolean {
const value = sql.slice(start, start + word.length);
if (value.toLowerCase() !== word) return false;
return !PARAMETER_NAME_CHAR_RE.test(sql[start - 1] ?? "") && !PARAMETER_NAME_CHAR_RE.test(sql[start + word.length] ?? "");
}
function readParameterName(sql: string, start: number): string {
if (!PARAMETER_NAME_START_RE.test(sql[start] ?? "")) return "";
let i = start + 1;
while (i < sql.length && PARAMETER_NAME_CHAR_RE.test(sql[i])) i += 1;
return sql.slice(start, i);
}
function skipQuoted(sql: string, start: number, quote: string): number {
let i = start + 1;
while (i < sql.length) {