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

+88 -42
View File
@@ -227,7 +227,7 @@ export function substituteSqlParameters(sql: string, values: Record<string, SqlP
cursor = occurrence.end;
}
result += sql.slice(cursor);
return decodeMyBatisEntities ? decodeMyBatisXmlComparisonEntities(result) : result;
return decodeMyBatisEntities ? decodeMyBatisXmlComparisonEntities(result, options?.databaseType) : result;
}
const MYBATIS_WHERE_LEADING_CONJUNCTION_RE = /^(?:AND|OR)\s+/i;
@@ -305,7 +305,7 @@ function unquoteListValue(value: string): string {
}
}
function decodeMyBatisXmlComparisonEntities(sql: string): string {
function decodeMyBatisXmlComparisonEntities(sql: string, databaseType?: DatabaseType): string {
let result = "";
let cursor = 0;
let i = 0;
@@ -319,7 +319,7 @@ function decodeMyBatisXmlComparisonEntities(sql: string): string {
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -367,16 +367,19 @@ export function sqlParameterLiteral(input: SqlParameterInput): string {
function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions): ParameterOccurrence[] {
const occurrences: ParameterOccurrence[] = [];
const nativeSqlServerParameters = collectNativeSqlServerParameters(sql);
const supportsNamedParameters = options?.databaseType !== "saphana";
const databaseType = options?.databaseType;
const nativeSqlServerParameters = collectNativeSqlServerParameters(sql, databaseType);
const supportsNamedParameters = databaseType !== "saphana";
const enabledSyntaxes = options?.enabledSyntaxes ? new Set(options.enabledSyntaxes) : null;
const isSyntaxEnabled = (syntax: SqlParameterSyntax) => !enabledSyntaxes || enabledSyntaxes.has(syntax);
const complexTypeFieldSeparators = supportsNamedParameters && isSyntaxEnabled("named") ? collectComplexTypeFieldSeparators(sql) : new Set<number>();
const duckDbStructFieldSeparators = supportsNamedParameters && isSyntaxEnabled("named") && options?.databaseType === "duckdb" ? collectDuckDbStructFieldSeparators(sql) : new Set<number>();
const triggerPseudoRecordFieldStarts = supportsNamedParameters && isSyntaxEnabled("named") ? collectTriggerPseudoRecordFieldStarts(sql, options?.databaseType) : new Set<number>();
const complexTypeFieldSeparators = supportsNamedParameters && isSyntaxEnabled("named") ? collectComplexTypeFieldSeparators(sql, databaseType) : new Set<number>();
const duckDbStructFieldSeparators = supportsNamedParameters && isSyntaxEnabled("named") && databaseType === "duckdb" ? collectDuckDbStructFieldSeparators(sql) : new Set<number>();
const triggerPseudoRecordFieldStarts = supportsNamedParameters && isSyntaxEnabled("named") ? collectTriggerPseudoRecordFieldStarts(sql, databaseType) : new Set<number>();
let i = 0;
let dollarQuoteEnd = "";
let positionalIndex = 0;
let parenthesisDepth = 0;
const postgresBracketStack: Array<{ constructor: boolean; parenthesisDepth: number }> = [];
while (i < sql.length) {
if (dollarQuoteEnd) {
@@ -391,7 +394,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
const next = sql[i + 1];
if (ch === "<" && isSyntaxEnabled("mybatis")) {
const foreach = readMyBatisForeachAt(sql, i);
const foreach = readMyBatisForeachAt(sql, i, databaseType);
if (foreach) {
occurrences.push({
key: foreach.collection,
@@ -446,8 +449,31 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
i = skipQuoted(sql, i, ch);
continue;
}
if (databaseType === "postgres" && ch === "(") {
parenthesisDepth += 1;
i += 1;
continue;
}
if (databaseType === "postgres" && ch === ")") {
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
i += 1;
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
if (databaseType === "postgres") {
postgresBracketStack.push({
constructor: isPostgresArrayConstructorBracket(sql, i, postgresBracketStack[postgresBracketStack.length - 1]?.constructor ?? false),
parenthesisDepth,
});
i += 1;
continue;
}
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (databaseType === "postgres" && ch === "]") {
postgresBracketStack.pop();
i += 1;
continue;
}
if (ch === "-" && next === "-") {
@@ -477,6 +503,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
sql[i + 1] !== "=" &&
!complexTypeFieldSeparators.has(i) &&
!duckDbStructFieldSeparators.has(i) &&
!isPostgresSliceSeparator(postgresBracketStack, parenthesisDepth) &&
!isDuckDbCompactPrefixAliasSeparator(sql, i, options?.databaseType) &&
!triggerPseudoRecordFieldStarts.has(i) &&
!isMysqlRoutineLabelSeparator(sql, i, name, options?.databaseType)
@@ -544,7 +571,7 @@ function isMysqlRoutineLabelSeparator(sql: string, colonIndex: number, following
return prefix.length === 0 || prefix.endsWith(";") || MYSQL_ROUTINE_LABEL_CONTEXTS.has(previousKeyword(sql, labelStart));
}
function readMyBatisForeachAt(sql: string, start: number): { collection: string; render: MyBatisForeach; end: number } | null {
function readMyBatisForeachAt(sql: string, start: number, databaseType?: DatabaseType): { collection: string; render: MyBatisForeach; end: number } | null {
if (!/^<foreach(?:\s|>)/i.test(sql.slice(start))) return null;
const openingEnd = findXmlTagEnd(sql, start);
if (openingEnd === -1) return null;
@@ -556,7 +583,7 @@ function readMyBatisForeachAt(sql: string, start: number): { collection: string;
const index = attributes.get("index")?.trim();
if (!PARAMETER_NAME_RE.test(collection) || !PARAMETER_NAME_RE.test(item) || (index !== undefined && !PARAMETER_NAME_RE.test(index))) return null;
const close = findMatchingXmlTagClose(sql, openingEnd + 1, "foreach");
const close = findMatchingXmlTagClose(sql, openingEnd + 1, "foreach", databaseType);
if (!close) return null;
return {
collection,
@@ -624,7 +651,7 @@ function decodeXmlAttribute(value: string): string {
return value.replace(/&(?:lt|gt|amp|quot|apos);/g, (entity) => ({ "&lt;": "<", "&gt;": ">", "&amp;": "&", "&quot;": '"', "&apos;": "'" })[entity] ?? entity);
}
function findMatchingXmlTagClose(sql: string, start: number, tagName: string): { start: number; end: number } | null {
function findMatchingXmlTagClose(sql: string, start: number, tagName: string, databaseType?: DatabaseType): { start: number; end: number } | null {
let depth = 1;
let i = start;
@@ -637,7 +664,7 @@ function findMatchingXmlTagClose(sql: string, start: number, tagName: string): {
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -865,7 +892,7 @@ function collectTriggerPseudoRecordFieldStarts(sql: string, databaseType?: Datab
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -998,7 +1025,7 @@ function isJdbcxMcpScopedPackage(sql: string, start: number, nameEnd: number): b
}
// Doris-style complex types use colons between field names and types; those are not bind parameters.
function collectComplexTypeFieldSeparators(sql: string): Set<number> {
function collectComplexTypeFieldSeparators(sql: string, databaseType?: DatabaseType): Set<number> {
const separators = new Set<number>();
let i = 0;
let dollarQuoteEnd = "";
@@ -1019,7 +1046,7 @@ function collectComplexTypeFieldSeparators(sql: string): Set<number> {
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1044,7 +1071,7 @@ function collectComplexTypeFieldSeparators(sql: string): Set<number> {
}
const declaration = readComplexTypeDeclaration(sql, i);
if (declaration) {
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators) + 1;
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators, databaseType) + 1;
continue;
}
i += 1;
@@ -1053,7 +1080,7 @@ function collectComplexTypeFieldSeparators(sql: string): Set<number> {
return separators;
}
function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: number, kind: ComplexTypeDeclarationKind, separators: Set<number>): number {
function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: number, kind: ComplexTypeDeclarationKind, separators: Set<number>, databaseType?: DatabaseType): number {
let i = start;
let genericDepth = 0;
let parenthesisDepth = 0;
@@ -1067,7 +1094,7 @@ function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: numb
continue;
}
if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i;
const fieldNameEnd = readComplexTypeFieldNameEnd(sql, i, kind);
const fieldNameEnd = readComplexTypeFieldNameEnd(sql, i, kind, databaseType);
if (fieldNameEnd > i) {
const separator = skipSqlWhitespaceAndComments(sql, fieldNameEnd);
if (sql[separator] === ":") {
@@ -1089,7 +1116,7 @@ function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: numb
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1106,7 +1133,7 @@ function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: numb
}
const declaration = readComplexTypeDeclaration(sql, i);
if (declaration) {
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators) + 1;
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators, databaseType) + 1;
continue;
}
if (ch === ";" && genericDepth === 0 && parenthesisDepth === 0) return i;
@@ -1148,12 +1175,12 @@ function readComplexTypeDeclaration(sql: string, start: number): { kind: Complex
return sql[openingBracket] === "<" ? { kind, openingBracket } : null;
}
function readComplexTypeFieldNameEnd(sql: string, start: number, kind: ComplexTypeDeclarationKind): number {
function readComplexTypeFieldNameEnd(sql: string, start: number, kind: ComplexTypeDeclarationKind, databaseType?: DatabaseType): number {
if (kind === "variant") return readVariantFieldNameEnd(sql, start);
const ch = sql[start];
if (ch === '"' || ch === "`") return skipQuoted(sql, start, ch);
if (ch === "[") return skipBracketIdentifier(sql, start);
if (ch === "[") return skipBracketIdentifier(sql, start, databaseType);
if (!PARAMETER_NAME_START_RE.test(ch ?? "")) return start;
let i = start + 1;
@@ -1189,7 +1216,7 @@ function skipSqlWhitespaceAndComments(sql: string, start: number): number {
return i;
}
function collectNativeSqlServerParameters(sql: string): { declared: Set<string>; ignoredStarts: Set<number> } {
function collectNativeSqlServerParameters(sql: string, databaseType?: DatabaseType): { declared: Set<string>; ignoredStarts: Set<number> } {
const declared = new Set<string>();
const ignoredStarts = new Set<number>();
let i = 0;
@@ -1211,7 +1238,7 @@ function collectNativeSqlServerParameters(sql: string): { declared: Set<string>;
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1235,23 +1262,23 @@ function collectNativeSqlServerParameters(sql: string): { declared: Set<string>;
continue;
}
if (matchesWord(sql, i, "declare")) {
i = collectDeclareStatementVariables(sql, i + "declare".length, declared);
i = collectDeclareStatementVariables(sql, i + "declare".length, declared, databaseType);
continue;
}
if (matchesWord(sql, i, "set")) {
i = collectSetStatementVariables(sql, i + "set".length, declared);
i = collectSetStatementVariables(sql, i + "set".length, declared, databaseType);
continue;
}
if (matchesWord(sql, i, "select")) {
i = collectSelectAssignmentVariables(sql, i + "select".length, declared);
i = collectSelectAssignmentVariables(sql, i + "select".length, declared, databaseType);
continue;
}
if ((matchesWord(sql, i, "create") || matchesWord(sql, i, "alter")) && isRoutineDefinitionStart(sql, i)) {
i = collectRoutineDefinitionVariables(sql, i, declared);
i = collectRoutineDefinitionVariables(sql, i, declared, databaseType);
continue;
}
if (matchesWord(sql, i, "exec") || matchesWord(sql, i, "execute")) {
i = collectExecNamedArgumentStarts(sql, i + (matchesWord(sql, i, "exec") ? "exec".length : "execute".length), ignoredStarts);
i = collectExecNamedArgumentStarts(sql, i + (matchesWord(sql, i, "exec") ? "exec".length : "execute".length), ignoredStarts, databaseType);
continue;
}
i += 1;
@@ -1260,7 +1287,7 @@ function collectNativeSqlServerParameters(sql: string): { declared: Set<string>;
return { declared, ignoredStarts };
}
function collectDeclareStatementVariables(sql: string, start: number, declared: Set<string>): number {
function collectDeclareStatementVariables(sql: string, start: number, declared: Set<string>, databaseType?: DatabaseType): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
@@ -1272,7 +1299,7 @@ function collectDeclareStatementVariables(sql: string, start: number, declared:
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1300,7 +1327,7 @@ function collectDeclareStatementVariables(sql: string, start: number, declared:
return i;
}
function collectSetStatementVariables(sql: string, start: number, declared: Set<string>): number {
function collectSetStatementVariables(sql: string, start: number, declared: Set<string>, databaseType?: DatabaseType): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
@@ -1312,7 +1339,7 @@ function collectSetStatementVariables(sql: string, start: number, declared: Set<
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1340,7 +1367,7 @@ function collectSetStatementVariables(sql: string, start: number, declared: Set<
return i;
}
function collectSelectAssignmentVariables(sql: string, start: number, declared: Set<string>): number {
function collectSelectAssignmentVariables(sql: string, start: number, declared: Set<string>, databaseType?: DatabaseType): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
@@ -1353,7 +1380,7 @@ function collectSelectAssignmentVariables(sql: string, start: number, declared:
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1381,7 +1408,7 @@ function collectSelectAssignmentVariables(sql: string, start: number, declared:
return i;
}
function collectRoutineDefinitionVariables(sql: string, start: number, declared: Set<string>): number {
function collectRoutineDefinitionVariables(sql: string, start: number, declared: Set<string>, databaseType?: DatabaseType): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
@@ -1393,7 +1420,7 @@ function collectRoutineDefinitionVariables(sql: string, start: number, declared:
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1421,7 +1448,7 @@ function collectRoutineDefinitionVariables(sql: string, start: number, declared:
return i;
}
function collectExecNamedArgumentStarts(sql: string, start: number, ignoredStarts: Set<number>): number {
function collectExecNamedArgumentStarts(sql: string, start: number, ignoredStarts: Set<number>, databaseType?: DatabaseType): number {
let i = start;
while (i < sql.length) {
const ch = sql[i];
@@ -1433,7 +1460,7 @@ function collectExecNamedArgumentStarts(sql: string, start: number, ignoredStart
continue;
}
if (ch === "[") {
i = skipBracketIdentifier(sql, i);
i = skipBracketIdentifier(sql, i, databaseType);
continue;
}
if (ch === "-" && next === "-") {
@@ -1636,7 +1663,12 @@ 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
// so quoted array elements cannot corrupt the following SQL state.
if (databaseType === "postgres") return start + 1;
let i = start + 1;
while (i < sql.length) {
if (sql[i] === "]") {
@@ -1676,6 +1708,20 @@ function isSqlServerTempTableReference(sql: string, start: number): boolean {
return !!previous && SQL_SERVER_TEMP_TABLE_CONTEXT_KEYWORDS.has(previous);
}
function isPostgresArrayConstructorBracket(sql: string, start: number, parentIsConstructor: boolean): boolean {
if (previousKeyword(sql, start) === "array") return true;
if (!parentIsConstructor) return false;
let previous = start - 1;
while (previous >= 0 && /\s/.test(sql[previous])) previous -= 1;
return sql[previous] === "[" || sql[previous] === ",";
}
function isPostgresSliceSeparator(stack: Array<{ constructor: boolean; parenthesisDepth: number }>, parenthesisDepth: number): boolean {
const context = stack[stack.length - 1];
return !!context && !context.constructor && context.parenthesisDepth === parenthesisDepth;
}
function previousKeyword(sql: string, start: number): string {
let end = start - 1;
while (end >= 0 && /\s/.test(sql[end])) end -= 1;