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

@@ -116,6 +116,94 @@ describe("extractSqlParameters", () => {
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["?1", "?2", "?3", "?4", "?5", "?6"]);
});
it("does not expose date format tokens after PostgreSQL ARRAY expressions", () => {
const sql = `
WITH rec_flow AS (
SELECT
order_no,
ARRAY[
concat(
operator_name, '(', COALESCE(remark, ''), ')[',
CASE operate_action
WHEN 'CREATE' THEN '创建工单'
WHEN 'SUBMIT' THEN '提交至下一处理人'
WHEN 'BACK' THEN '退回上一环节'
WHEN 'FINISH' THEN '已完成'
WHEN 'SUBMIT-CONFIRM' THEN '提交给创建人确认'
WHEN 'COMPLETE' THEN '确认工单'
ELSE operate_action
END, ']'
)
]::varchar[]
|| CASE
WHEN operate_action NOT IN ('FINISH','COMPLETE')
THEN ARRAY[target_handler_name]::varchar[]
ELSE ARRAY[]::varchar[]
END AS name_arr,
rn
FROM (
SELECT
'ORD20260821001' AS order_no,
'张三' AS operator_name,
'发起流程' AS remark,
'SUBMIT' AS operate_action,
'李四' AS target_handler_name,
1 AS rn
) AS mock_t3_flow
)
SELECT to_char(current_timestamp, 'yyyy-MM-dd HH24:mi:ss');
`;
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual([]);
});
it("keeps PostgreSQL ARRAY literals and subscripts in the lexical stream", () => {
const dateSql = "to_char(current_timestamp, 'yyyy-MM-dd HH24:mi:ss')";
for (const arrayExpression of ["ARRAY['x']", "ARRAY[']']", "ARRAY['[']", "ARRAY['a]b']", "ARRAY[]::varchar[]"]) {
expect(extractSqlParameters(`SELECT ${arrayExpression}, ${dateSql};`, { databaseType: "postgres" })).toEqual([]);
}
expect(extractSqlParameters("SELECT ARRAY['x'][:array_index], values[:subscript_index];", { databaseType: "postgres" })).toEqual([]);
});
it("does not treat a standalone PostgreSQL date format as a parameter", () => {
expect(extractSqlParameters("SELECT to_char(current_timestamp, 'HH24:MI:SS');", { databaseType: "postgres" })).toEqual([]);
});
it("keeps PostgreSQL named parameters inside ARRAY constructors", () => {
const sql = "SELECT ARRAY[:first_value, :second_value];";
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["first_value", "second_value"]);
expect(substituteSqlParameters(sql, { first_value: { kind: "number", value: "1" }, second_value: { kind: "number", value: "2" } }, { databaseType: "postgres" })).toBe("SELECT ARRAY[1, 2];");
expect(extractSqlParameters("SELECT :id;", { databaseType: "postgres" })).toEqual(["id"]);
});
it("does not treat PostgreSQL slice bounds as named parameters", () => {
const sql = "SELECT arr[lower:upper], arr[:upper], ARRAY[arr[:nested_upper], :constructor_value];";
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["constructor_value"]);
expect(substituteSqlParameters(sql, { constructor_value: { kind: "number", value: "7" } }, { databaseType: "postgres" })).toBe("SELECT arr[lower:upper], arr[:upper], ARRAY[arr[:nested_upper], 7];");
});
it("keeps named parameters inside nested PostgreSQL ARRAY constructors and parenthesized subscripts", () => {
const sql = "SELECT ARRAY[[:first_value, :second_value], ARRAY[:third_value]], values[(:subscript_index)];";
expect(extractSqlParameters(sql, { databaseType: "postgres" })).toEqual(["first_value", "second_value", "third_value", "subscript_index"]);
});
it("preserves SQL Server bracketed identifiers while scanning parameters", () => {
const sql = "SELECT [column:inside], :actual";
expect(extractSqlParameters(sql, { databaseType: "sqlserver" })).toEqual(["actual"]);
expect(substituteSqlParameters(sql, { actual: { kind: "number", value: "7" } }, { databaseType: "sqlserver" })).toBe("SELECT [column:inside], 7");
});
it.each(["sqlite", "jdbc", "access"] as const)("preserves bracketed identifiers for %s", (databaseType) => {
expect(extractSqlParameters("SELECT [column:inside], :actual", { databaseType })).toEqual(["actual"]);
});
it("keeps historical bracket scanning when no database dialect is supplied", () => {
expect(extractSqlParameters("SELECT [column:inside], :actual")).toEqual(["actual"]);
});
it("keeps question marks as positional placeholders for other databases", () => {
expect(extractSqlParameters("select payload ? 'callingResults' from events", { databaseType: "mysql" })).toEqual(["?1"]);
});
+16
View File
@@ -60,6 +60,22 @@ describe("expandSqlVariables", () => {
expect(expandSqlVariables(sql)).toEqual({ sql, expanded: false });
});
it("keeps PostgreSQL ARRAY literals from corrupting later variable references", () => {
const sql = ["@set selected = 7;", "select ARRAY[']']::varchar[], @selected, ${missing}"].join("\n");
expect(expandSqlVariables(sql, { databaseType: "postgres" }).sql).toBe("select ARRAY[']']::varchar[], 7, ${missing}");
});
it("keeps multiline PostgreSQL ARRAY values in @set declarations", () => {
const sql = ["@set values = ARRAY[", " 'first',", " 'second'", "];", "select @values;"].join("\n");
expect(expandSqlVariables(sql, { databaseType: "postgres" }).sql).toBe("select ARRAY[\n 'first',\n 'second'\n];");
});
it.each(["sqlserver", "sqlite", "jdbc"] as const)("preserves %s bracket identifiers during variable expansion", (databaseType) => {
const sql = ["@set selected = 7;", "select [column:@ignored], @selected"].join("\n");
expect(expandSqlVariables(sql, { databaseType }).sql).toBe("select [column:@ignored], 7");
});
it("keeps a value's own quotes and parentheses intact", () => {
const sql = ["@set filter = (status = 'drafted' and deleted_at is null);", "select * from t where @filter"].join("\n");
expect(expandSqlVariables(sql).sql).toBe("select * from t where (status = 'drafted' and deleted_at is null)");