fix(duckdb): ignore struct field separators as parameters

This commit is contained in:
t8y2 committed 2026-07-31 23:15:11 +08:00
1 parent b3c4c9a912
commit 07d02daded
2 files changed
+167 -1

No files matched your search

+107 -1
View File
@@ -27,6 +27,13 @@ interface ParameterOccurrence extends SqlParameterDescriptor {
replacement?: "string-fragment";
}
interface DuckDbStructLiteralContext {
bracketDepth: number;
parenthesisDepth: number;
separators: Set<number>;
valid: boolean;
}
type ComplexTypeDeclarationKind = "struct" | "variant";
type TriggerPseudoRecordName = "new" | "old" | "parent" | "eventinfo";
@@ -108,6 +115,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
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>();
let i = 0;
let dollarQuoteEnd = "";
@@ -168,7 +176,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
}
if (ch === ":" && supportsNamedParameters && isSyntaxEnabled("named")) {
const name = readParameterName(sql, i + 1);
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=" && !complexTypeFieldSeparators.has(i) && !triggerPseudoRecordFieldStarts.has(i)) {
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=" && !complexTypeFieldSeparators.has(i) && !duckDbStructFieldSeparators.has(i) && !triggerPseudoRecordFieldStarts.has(i)) {
occurrences.push({
key: name,
name,
@@ -222,6 +230,104 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
return occurrences;
}
function collectDuckDbStructFieldSeparators(sql: string): Set<number> {
const separators = new Set<number>();
const contexts: DuckDbStructLiteralContext[] = [];
let cursor = 0;
let dollarQuoteEnd = "";
while (cursor < sql.length) {
if (dollarQuoteEnd) {
const end = sql.indexOf(dollarQuoteEnd, cursor);
if (end === -1) break;
cursor = end + dollarQuoteEnd.length;
dollarQuoteEnd = "";
continue;
}
const ch = sql[cursor];
const next = sql[cursor + 1];
if (ch === "'" || ch === '"' || ch === "`") {
cursor = skipQuoted(sql, cursor, ch);
continue;
}
if (ch === "-" && next === "-") {
cursor = skipLine(sql, cursor + 2);
continue;
}
if (ch === "/" && next === "*") {
cursor = skipBlockComment(sql, cursor + 2);
continue;
}
if (isHashLineComment(sql, cursor)) {
cursor = skipLine(sql, cursor + 1);
continue;
}
if (ch === "$") {
const marker = readDollarQuoteMarker(sql, cursor);
if (marker) {
dollarQuoteEnd = marker;
cursor += marker.length;
continue;
}
}
if (ch === "{") {
const separator = readDuckDbStructFieldSeparator(sql, cursor + 1);
contexts.push({
bracketDepth: 0,
parenthesisDepth: 0,
separators: new Set(separator === null ? [] : [separator]),
valid: separator !== null,
});
cursor += 1;
continue;
}
if (ch === "}") {
const context = contexts.pop();
if (context?.valid) {
const parent = contexts[contexts.length - 1];
const destination = parent ? parent.separators : separators;
for (const separator of context.separators) destination.add(separator);
}
cursor += 1;
continue;
}
const context = contexts[contexts.length - 1];
if (!context) {
cursor += 1;
continue;
}
if (ch === "(") context.parenthesisDepth += 1;
else if (ch === ")" && context.parenthesisDepth > 0) context.parenthesisDepth -= 1;
else if (ch === "[") context.bracketDepth += 1;
else if (ch === "]" && context.bracketDepth > 0) context.bracketDepth -= 1;
else if (ch === "," && context.valid && context.parenthesisDepth === 0 && context.bracketDepth === 0) {
const separator = readDuckDbStructFieldSeparator(sql, cursor + 1);
if (separator !== null) context.separators.add(separator);
}
cursor += 1;
}
return separators;
}
function readDuckDbStructFieldSeparator(sql: string, start: number): number | null {
const fieldStart = skipSqlWhitespaceAndComments(sql, start);
const ch = sql[fieldStart];
let fieldNameEnd = fieldStart;
if (ch === "'" || ch === '"' || ch === "`") fieldNameEnd = skipQuoted(sql, fieldStart, ch);
else {
const fieldName = readParameterName(sql, fieldStart);
if (!fieldName) return null;
fieldNameEnd += fieldName.length;
}
const separator = skipSqlWhitespaceAndComments(sql, fieldNameEnd);
return sql[separator] === ":" ? separator : null;
}
// Oracle and Dameng expose trigger rows through colon-prefixed pseudo-records,
// unlike PostgreSQL's unprefixed NEW/OLD records. Keep ordinary :name binds enabled.
function collectTriggerPseudoRecordFieldStarts(sql: string, databaseType?: DatabaseType): Set<number> {