1 parent
fc2db2a17c
commit
55e3616938
2 files changed
+59
-2
No files matched your search
@@ -521,6 +521,58 @@ describe("Oracle and Dameng trigger pseudo-records", () => {
|
||||
});
|
||||
|
||||
describe("substituteSqlParameters", () => {
|
||||
it("decodes XML comparison entities when substituting MyBatis parameters", () => {
|
||||
const sql = "select * from orders where created_at >= #{start} and created_at < #{end} and owner_id = #{owner_id} or reviewer_id = #{owner_id}";
|
||||
|
||||
expect(
|
||||
substituteSqlParameters(
|
||||
sql,
|
||||
{
|
||||
start: { kind: "string", value: "2026-01-01" },
|
||||
end: { kind: "string", value: "2026-02-01" },
|
||||
owner_id: { kind: "number", value: "7" },
|
||||
},
|
||||
{ enabledSyntaxes: ["mybatis"] },
|
||||
),
|
||||
).toBe("select * from orders where created_at >= '2026-01-01' and created_at < '2026-02-01' and owner_id = 7 or reviewer_id = 7");
|
||||
});
|
||||
|
||||
it("only decodes exact comparison entities in MyBatis SQL source fragments", () => {
|
||||
const sql = "select '&', '"', ''', '&lt;', '<', '<', '<source>', #{raw_value}, #{string_value} where score > #{minimum} /* <source-comment> */";
|
||||
|
||||
expect(
|
||||
substituteSqlParameters(
|
||||
sql,
|
||||
{
|
||||
raw_value: { kind: "raw", value: "'<raw>'" },
|
||||
string_value: { kind: "string", value: "<string>" },
|
||||
minimum: { kind: "number", value: "10" },
|
||||
},
|
||||
{ enabledSyntaxes: ["mybatis"] },
|
||||
),
|
||||
).toBe("select '&', '"', ''', '&lt;', '<', '<', '<source>', '<raw>', '<string>' where score > 10 /* <source-comment> */");
|
||||
});
|
||||
|
||||
it("does not decode XML entities without an enabled valid MyBatis replacement", () => {
|
||||
const mybatisSql = "select * from orders where total > #{minimum} and owner = ${owner}";
|
||||
const shellSql = "select * from orders where total < ${maximum}";
|
||||
const invalidMybatisSql = "select * from orders where total > #{1minimum} and owner = ${owner}";
|
||||
const otherSyntaxSql = "select ?, :named, @sql_server_name where total > 10";
|
||||
const plainSql = "select '<plain>'";
|
||||
|
||||
expect(substituteSqlParameters(mybatisSql, { owner: { kind: "string", value: "alice" } }, { enabledSyntaxes: ["shell"] })).toBe("select * from orders where total > #{minimum} and owner = 'alice'");
|
||||
expect(substituteSqlParameters(shellSql, { maximum: { kind: "number", value: "10" } }, { enabledSyntaxes: ["shell"] })).toBe("select * from orders where total < 10");
|
||||
expect(substituteSqlParameters(invalidMybatisSql, { owner: { kind: "string", value: "alice" } })).toBe("select * from orders where total > #{1minimum} and owner = 'alice'");
|
||||
expect(
|
||||
substituteSqlParameters(otherSyntaxSql, {
|
||||
"?1": { kind: "number", value: "1" },
|
||||
named: { kind: "number", value: "2" },
|
||||
sql_server_name: { kind: "number", value: "3" },
|
||||
}),
|
||||
).toBe("select 1, 2, 3 where total > 10");
|
||||
expect(substituteSqlParameters(plainSql, {})).toBe(plainSql);
|
||||
});
|
||||
|
||||
it("replaces placeholders with SQL literals", () => {
|
||||
const sql = "select * from t where dt >= ${start_date} and amount > ${amount} and enabled = ${enabled}";
|
||||
expect(
|
||||
|
||||
@@ -166,21 +166,26 @@ export function extractSqlParameterDescriptors(sql: string, options?: SqlParamet
|
||||
export function substituteSqlParameters(sql: string, values: Record<string, SqlParameterInput>, options?: SqlParameterOptions): string {
|
||||
const occurrences = findSqlParameterOccurrences(sql, options);
|
||||
if (!occurrences.length) return sql;
|
||||
const decodeSourceFragment = occurrences.some((occurrence) => occurrence.syntax === "mybatis") ? decodeMyBatisXmlComparisonEntities : (fragment: string) => fragment;
|
||||
|
||||
let result = "";
|
||||
let cursor = 0;
|
||||
for (const occurrence of occurrences) {
|
||||
result += sql.slice(cursor, occurrence.start);
|
||||
result += decodeSourceFragment(sql.slice(cursor, occurrence.start));
|
||||
const input = values[occurrence.key] ?? { kind: "string", value: "" };
|
||||
// Embedded placeholders stay inside the surrounding SQL string, so their value
|
||||
// must be escaped as text instead of being wrapped in a second SQL literal.
|
||||
result += occurrence.replacement === "string-fragment" ? sqlParameterStringFragment(input) : sqlParameterLiteral(input);
|
||||
cursor = occurrence.end;
|
||||
}
|
||||
result += sql.slice(cursor);
|
||||
result += decodeSourceFragment(sql.slice(cursor));
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeMyBatisXmlComparisonEntities(fragment: string): string {
|
||||
return fragment.replace(/&(?:lt|gt);/g, (entity) => (entity === "<" ? "<" : ">"));
|
||||
}
|
||||
|
||||
export function sqlParameterLiteral(input: SqlParameterInput): string {
|
||||
if (input.kind === "null") return "NULL";
|
||||
const raw = input.value;
|
||||
|
||||
Reference in new issue
Block a user