mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 10:28:44 +00:00
@@ -43,6 +43,51 @@ describe("extractSqlParameters", () => {
|
||||
expect(extractSqlParameters(sql)).toEqual(["?1", "named", "shell_name", "mybatis_name", "sql_server_name"]);
|
||||
});
|
||||
|
||||
it("preserves PostgreSQL JSON question-mark operators", () => {
|
||||
const sql = `
|
||||
select
|
||||
c."CallResult",
|
||||
c."CallResult" -> 'data' ? 'callingResults' as "HasResult",
|
||||
c."CallResult" ?| array['data', 'error'] as "HasAnyResult",
|
||||
c."CallResult" ?& array['data', 'callingResults'] as "HasAllResults",
|
||||
c."CallResult" @? '$.data.callingResults' as "MatchesPath"
|
||||
from "FaultResultCallInfo" as c
|
||||
`;
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual([]);
|
||||
expect(substituteSqlParameters(sql, {}, { databaseType: "postgres" })).toBe(sql);
|
||||
});
|
||||
|
||||
it("keeps PostgreSQL positional placeholders around JSON operators", () => {
|
||||
const sql = "select ? as input_value from events where id = ? and payload ? ? and payload ?| ? and payload ?& ? limit ?";
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["?1", "?2", "?3", "?4", "?5", "?6"]);
|
||||
expect(
|
||||
substituteSqlParameters(
|
||||
sql,
|
||||
{
|
||||
"?1": { kind: "number", value: "1" },
|
||||
"?2": { kind: "number", value: "2" },
|
||||
"?3": { kind: "string", value: "callingResults" },
|
||||
"?4": { kind: "raw", value: "ARRAY['data', 'error']" },
|
||||
"?5": { kind: "raw", value: "ARRAY['data', 'callingResults']" },
|
||||
"?6": { kind: "number", value: "100" },
|
||||
},
|
||||
{ databaseType: "postgres" },
|
||||
),
|
||||
).toBe("select 1 as input_value from events where id = 2 and payload ? 'callingResults' and payload ?| ARRAY['data', 'error'] and payload ?& ARRAY['data', 'callingResults'] limit 100");
|
||||
});
|
||||
|
||||
it("keeps ordinary PostgreSQL positional placeholder contexts", () => {
|
||||
const sql = "select ?::jsonb, coalesce(?, '{}'::jsonb) from events where ? = id order by ? limit ? offset ?";
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["?1", "?2", "?3", "?4", "?5", "?6"]);
|
||||
});
|
||||
|
||||
it("keeps question marks as positional placeholders for other databases", () => {
|
||||
expect(extractSqlParameters("select payload ? 'callingResults' from events", { databaseType: "mysql" })).toEqual(["?1"]);
|
||||
});
|
||||
|
||||
it("ignores npm scoped packages in JDBCX MCP command arguments", () => {
|
||||
const sql = '{{ mcp(cmd=npx, args=-y @modelcontextprotocol/server-everything, tool=echo): {"message":"hello"} }}';
|
||||
expect(extractSqlParameters(sql)).toEqual([]);
|
||||
|
||||
@@ -47,6 +47,88 @@ 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"]);
|
||||
const POSTGRES_QUESTION_PARAMETER_PREFIX_KEYWORDS = new Set([
|
||||
"all",
|
||||
"and",
|
||||
"any",
|
||||
"as",
|
||||
"between",
|
||||
"by",
|
||||
"case",
|
||||
"collate",
|
||||
"distinct",
|
||||
"else",
|
||||
"fetch",
|
||||
"filter",
|
||||
"first",
|
||||
"for",
|
||||
"from",
|
||||
"group",
|
||||
"groups",
|
||||
"having",
|
||||
"ilike",
|
||||
"in",
|
||||
"interval",
|
||||
"into",
|
||||
"is",
|
||||
"like",
|
||||
"limit",
|
||||
"next",
|
||||
"not",
|
||||
"offset",
|
||||
"on",
|
||||
"or",
|
||||
"order",
|
||||
"over",
|
||||
"partition",
|
||||
"placing",
|
||||
"range",
|
||||
"returning",
|
||||
"rows",
|
||||
"select",
|
||||
"set",
|
||||
"similar",
|
||||
"some",
|
||||
"then",
|
||||
"to",
|
||||
"using",
|
||||
"values",
|
||||
"when",
|
||||
"where",
|
||||
"window",
|
||||
"zone",
|
||||
]);
|
||||
const POSTGRES_QUESTION_OPERATOR_TRAILING_KEYWORDS = new Set([
|
||||
"and",
|
||||
"as",
|
||||
"between",
|
||||
"else",
|
||||
"end",
|
||||
"except",
|
||||
"fetch",
|
||||
"filter",
|
||||
"from",
|
||||
"group",
|
||||
"having",
|
||||
"ilike",
|
||||
"in",
|
||||
"intersect",
|
||||
"is",
|
||||
"join",
|
||||
"like",
|
||||
"limit",
|
||||
"offset",
|
||||
"on",
|
||||
"or",
|
||||
"order",
|
||||
"over",
|
||||
"returning",
|
||||
"then",
|
||||
"union",
|
||||
"when",
|
||||
"where",
|
||||
"window",
|
||||
]);
|
||||
|
||||
export function readSqlBracedParameterAt(sql: string, start: number, options?: SqlParameterOptions): SqlBracedParameter | null {
|
||||
const open = sql.slice(start, start + 2);
|
||||
@@ -168,6 +250,10 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
|
||||
continue;
|
||||
}
|
||||
if (ch === "?" && isSyntaxEnabled("positional")) {
|
||||
if (isPostgresQuestionMarkOperator(sql, i, options?.databaseType)) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
positionalIndex += 1;
|
||||
const key = `?${positionalIndex}`;
|
||||
occurrences.push({ key, name: key, syntax: "positional", token: "?", start: i, end: i + 1 });
|
||||
@@ -242,6 +328,50 @@ function isOracleDatabaseLinkMarker(sql: string, index: number, databaseType: Da
|
||||
return PARAMETER_NAME_CHAR_RE.test(previous) || previous === "$" || previous === "#" || previous === '"';
|
||||
}
|
||||
|
||||
function isPostgresQuestionMarkOperator(sql: string, index: number, databaseType: DatabaseType | undefined): boolean {
|
||||
if (databaseType !== "postgres") return false;
|
||||
if (sql[index - 1] === "@" || sql[index + 1] === "|" || sql[index + 1] === "&") return true;
|
||||
|
||||
const previousIndex = previousSqlSignificantIndex(sql, index);
|
||||
if (previousIndex < 0 || !canEndPostgresExpression(sql, previousIndex)) return false;
|
||||
|
||||
const nextIndex = skipSqlWhitespaceAndComments(sql, index + 1);
|
||||
if (nextIndex >= sql.length) return false;
|
||||
const next = sql[nextIndex];
|
||||
if (next === "'" || next === '"' || next === "`" || next === "[" || next === "(" || next === "$" || next === ":" || next === "#" || next === "@" || next === "?" || next === "+" || next === "-") return true;
|
||||
if (!PARAMETER_NAME_CHAR_RE.test(next)) return false;
|
||||
|
||||
let end = nextIndex + 1;
|
||||
while (end < sql.length && PARAMETER_NAME_CHAR_RE.test(sql[end])) end += 1;
|
||||
return !POSTGRES_QUESTION_OPERATOR_TRAILING_KEYWORDS.has(sql.slice(nextIndex, end).toLowerCase());
|
||||
}
|
||||
|
||||
function previousSqlSignificantIndex(sql: string, start: number): number {
|
||||
let index = start - 1;
|
||||
while (index >= 0) {
|
||||
while (index >= 0 && /\s/.test(sql[index])) index -= 1;
|
||||
if (index >= 1 && sql[index - 1] === "*" && sql[index] === "/") {
|
||||
const commentStart = sql.lastIndexOf("/*", index - 1);
|
||||
if (commentStart >= 0) {
|
||||
index = commentStart - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function canEndPostgresExpression(sql: string, end: number): boolean {
|
||||
const previous = sql[end];
|
||||
if (previous === ")" || previous === "]" || previous === "}" || previous === "'" || previous === '"' || previous === "`" || previous === "$") return true;
|
||||
if (!PARAMETER_NAME_CHAR_RE.test(previous)) return false;
|
||||
|
||||
let start = end;
|
||||
while (start > 0 && PARAMETER_NAME_CHAR_RE.test(sql[start - 1])) start -= 1;
|
||||
return !POSTGRES_QUESTION_PARAMETER_PREFIX_KEYWORDS.has(sql.slice(start, end + 1).toLowerCase());
|
||||
}
|
||||
|
||||
function collectDuckDbStructFieldSeparators(sql: string): Set<number> {
|
||||
const separators = new Set<number>();
|
||||
const contexts: DuckDbStructLiteralContext[] = [];
|
||||
|
||||
Reference in New Issue
Block a user