fix(sql): respect PostgreSQL ARRAY brackets

This commit is contained in:
jearhe authored and GitHub committed 2026-08-22 14:01:46 +08:00
1 parent 11179676ed
commit f274a599ed
6 files changed
+282 -57

No files matched your search

+28 -14
View File
@@ -11,6 +11,8 @@
// reference is only expanded when a matching `@set` declaration exists; any
// unresolved `${name}` or `@name` remains available to the parameter dialog.
import type { DatabaseType } from "@/types/database";
const VARIABLE_NAME_START_RE = /[\p{L}_]/u;
const VARIABLE_NAME_CHAR_RE = /[\p{L}\p{N}_]/u;
@@ -21,6 +23,7 @@ export interface SqlVariableExpansion {
export interface SqlVariableExpansionOptions {
declarationSql?: string;
databaseType?: DatabaseType;
}
interface DeclarationSpan {
@@ -39,7 +42,7 @@ interface DeclarationSpan {
*/
export function expandSqlVariables(sql: string, options: SqlVariableExpansionOptions = {}): SqlVariableExpansion {
const declarationSql = options.declarationSql ?? sql;
const declarations = collectDeclarations(declarationSql);
const declarations = collectDeclarations(declarationSql, options.databaseType);
if (!declarations.length) return { sql, expanded: false };
const values = new Map<string, string>();
@@ -49,18 +52,18 @@ export function expandSqlVariables(sql: string, options: SqlVariableExpansionOpt
// Only spans inside the execution target are removed. Declarations supplied
// by the surrounding document are context and must not affect target offsets.
const targetDeclarations = declarationSql === sql ? declarations : collectDeclarations(sql);
const targetDeclarations = declarationSql === sql ? declarations : collectDeclarations(sql, options.databaseType);
let result = sql;
for (let i = targetDeclarations.length - 1; i >= 0; i -= 1) {
const declaration = targetDeclarations[i];
result = stripDeclaration(result, declaration.start, declaration.end);
}
result = replaceReferences(result, values);
result = replaceReferences(result, values, options.databaseType);
return { sql: result, expanded: result !== sql };
}
function collectDeclarations(sql: string): DeclarationSpan[] {
function collectDeclarations(sql: string, databaseType?: DatabaseType): DeclarationSpan[] {
const declarations: DeclarationSpan[] = [];
let i = 0;
let dollarQuoteEnd = "";
@@ -82,7 +85,7 @@ function collectDeclarations(sql: string): DeclarationSpan[] {
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -102,7 +105,7 @@ function collectDeclarations(sql: string): DeclarationSpan[] {
}
}
if (ch === "@" && matchesWord(sql, i + 1, "set") && isStatementStart(sql, i)) {
const declaration = readDeclaration(sql, i);
const declaration = readDeclaration(sql, i, databaseType);
if (declaration) {
declarations.push(declaration);
i = declaration.end;
@@ -118,7 +121,7 @@ function collectDeclarations(sql: string): DeclarationSpan[] {
// Parse `@set name = value` starting at `start` (the `@`). The value runs until
// the terminating `;` or end of input, honouring nested quotes, comments and
// parentheses so that `IN (...)` lists and quoted strings survive intact.
function readDeclaration(sql: string, start: number): DeclarationSpan | null {
function readDeclaration(sql: string, start: number, databaseType?: DatabaseType): DeclarationSpan | null {
let i = start + 1 + "set".length;
i = skipInlineWhitespace(sql, i);
@@ -132,7 +135,7 @@ function readDeclaration(sql: string, start: number): DeclarationSpan | null {
i = skipInlineWhitespace(sql, i);
const valueStart = i;
const valueEnd = readValueEnd(sql, i);
const valueEnd = readValueEnd(sql, i, databaseType);
const value = sql.slice(valueStart, valueEnd).trim();
if (!value) return null;
@@ -143,9 +146,10 @@ function readDeclaration(sql: string, start: number): DeclarationSpan | null {
return { name, value, start, end };
}
function readValueEnd(sql: string, start: number): number {
function readValueEnd(sql: string, start: number, databaseType?: DatabaseType): number {
let i = start;
let depth = 0;
let bracketDepth = 0;
while (i < sql.length) {
const ch = sql[i];
const next = sql[i + 1];
@@ -154,9 +158,15 @@ function readValueEnd(sql: string, start: number): number {
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
if (databaseType === "postgres") {
bracketDepth += 1;
i += 1;
continue;
}
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (databaseType === "postgres" && ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
if (ch === "-" && next === "-") return i;
if (ch === "/" && next === "*") return i;
if (ch === "$") {
@@ -169,13 +179,13 @@ function readValueEnd(sql: string, start: number): number {
}
if (ch === "(") depth += 1;
else if (ch === ")") depth = Math.max(0, depth - 1);
else if ((ch === ";" || ch === "\n") && depth === 0) return i;
else if ((ch === ";" || ch === "\n") && depth === 0 && bracketDepth === 0) return i;
i += 1;
}
return sql.length;
}
function replaceReferences(sql: string, values: Map<string, string>): string {
function replaceReferences(sql: string, values: Map<string, string>, databaseType?: DatabaseType): string {
let result = "";
let i = 0;
let dollarQuoteEnd = "";
@@ -200,7 +210,7 @@ function replaceReferences(sql: string, values: Map<string, string>): string {
continue;
}
if (ch === "[") {
const end = skipBracketIdentifier(sql, i);
const end = skipBracketIdentifier(sql, i, databaseType);
result += sql.slice(i, end);
i = end;
continue;
@@ -317,7 +327,11 @@ function skipQuoted(sql: string, start: number, quote: string): number {
return sql.length;
}
function skipBracketIdentifier(sql: string, start: number): number {
function skipBracketIdentifier(sql: string, start: number, databaseType?: DatabaseType): number {
// PostgreSQL uses square brackets for ARRAY constructors and subscripts, not
// for delimited identifiers. Leave the opening bracket in the lexical stream.
if (databaseType === "postgres") return start + 1;
let i = start + 1;
while (i < sql.length) {
if (sql[i] === "]") {