fix(pgvector): boolean filter casing, LIKE escaping, and TS scalar coercion (#5264)
This commit is contained in:
@@ -47,8 +47,14 @@ const OPERATOR_SQL_MAP: Record<string, { template: string; numeric: boolean }> =
|
||||
template: "NOT (payload->>'%KEY%' = ANY($%IDX%::text[]))",
|
||||
numeric: false,
|
||||
},
|
||||
contains: { template: "payload->>'%KEY%' LIKE $%IDX%", numeric: false },
|
||||
icontains: { template: "payload->>'%KEY%' ILIKE $%IDX%", numeric: false },
|
||||
contains: {
|
||||
template: "payload->>'%KEY%' LIKE $%IDX% ESCAPE '\\'",
|
||||
numeric: false,
|
||||
},
|
||||
icontains: {
|
||||
template: "payload->>'%KEY%' ILIKE $%IDX% ESCAPE '\\'",
|
||||
numeric: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function buildFilterConditions(
|
||||
@@ -119,7 +125,11 @@ export function buildFilterConditions(
|
||||
if (op === "in" || op === "nin") {
|
||||
values.push((opValue as any[]).map(String));
|
||||
} else if (op === "contains" || op === "icontains") {
|
||||
values.push(`%${opValue}%`);
|
||||
const escaped = String(opValue)
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/%/g, "\\%")
|
||||
.replace(/_/g, "\\_");
|
||||
values.push(`%${escaped}%`);
|
||||
} else if (mapping.numeric) {
|
||||
values.push(Number(opValue));
|
||||
} else {
|
||||
@@ -133,7 +143,11 @@ export function buildFilterConditions(
|
||||
paramIndex++;
|
||||
} else {
|
||||
conditions.push(`payload->>'${safeKey}' = $${paramIndex}`);
|
||||
values.push(value);
|
||||
if (typeof value === "boolean") {
|
||||
values.push(JSON.stringify(value));
|
||||
} else {
|
||||
values.push(String(value));
|
||||
}
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,17 +120,27 @@ describe("buildFilterConditions", () => {
|
||||
test("contains operator", () => {
|
||||
const result = buildFilterConditions({ name: { contains: "alice" } }, 1);
|
||||
expect(result.conditions).toHaveLength(1);
|
||||
expect(result.conditions[0]).toContain("LIKE $1");
|
||||
expect(result.conditions[0]).toContain("LIKE $1 ESCAPE");
|
||||
expect(result.values).toEqual(["%alice%"]);
|
||||
});
|
||||
|
||||
test("icontains operator", () => {
|
||||
const result = buildFilterConditions({ name: { icontains: "Alice" } }, 1);
|
||||
expect(result.conditions).toHaveLength(1);
|
||||
expect(result.conditions[0]).toContain("ILIKE $1");
|
||||
expect(result.conditions[0]).toContain("ILIKE $1 ESCAPE");
|
||||
expect(result.values).toEqual(["%Alice%"]);
|
||||
});
|
||||
|
||||
test("contains escapes LIKE wildcards", () => {
|
||||
const result = buildFilterConditions({ name: { contains: "50%_off" } }, 1);
|
||||
expect(result.values).toEqual(["%50\\%\\_off%"]);
|
||||
});
|
||||
|
||||
test("icontains escapes LIKE wildcards", () => {
|
||||
const result = buildFilterConditions({ promo: { icontains: "a%b_c" } }, 1);
|
||||
expect(result.values).toEqual(["%a\\%b\\_c%"]);
|
||||
});
|
||||
|
||||
test("wildcard value", () => {
|
||||
const result = buildFilterConditions({ metadata_key: "*" }, 1);
|
||||
expect(result.conditions).toHaveLength(1);
|
||||
@@ -216,4 +226,19 @@ describe("buildFilterConditions", () => {
|
||||
expect(result.conditions[2]).toContain("$5");
|
||||
expect(result.paramIndex).toBe(6);
|
||||
});
|
||||
|
||||
test("boolean true uses JSON casing", () => {
|
||||
const result = buildFilterConditions({ is_active: true }, 1);
|
||||
expect(result.values).toEqual(["true"]);
|
||||
});
|
||||
|
||||
test("boolean false uses JSON casing", () => {
|
||||
const result = buildFilterConditions({ is_active: false }, 1);
|
||||
expect(result.values).toEqual(["false"]);
|
||||
});
|
||||
|
||||
test("numeric scalar becomes string", () => {
|
||||
const result = buildFilterConditions({ priority: 42 }, 1);
|
||||
expect(result.values).toEqual(["42"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user