import { beforeEach, describe, expect, it, vi } from "vitest"; /** * Regression test for: operator does not exist: integer = text * * The CTE `VALUES ($1), ($2)` generated by Drizzle infers columns as text. * The LATERAL join then compares integer (table column) to text (CTE column), * which PostgreSQL rejects. The fix adds an explicit `::integer` cast. */ function sqlToString(sqlObj: unknown): string { const visited = new Set(); const walk = (node: unknown): string => { if (!node || visited.has(node)) return ""; visited.add(node); if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (typeof node === "object") { const anyNode = node as Record; if (Array.isArray(anyNode)) { return anyNode.map(walk).join(""); } if (anyNode.value !== undefined) { if (Array.isArray(anyNode.value)) { return (anyNode.value as unknown[]).map(walk).join(""); } return walk(anyNode.value); } if (anyNode.queryChunks) { return walk(anyNode.queryChunks); } } return ""; }; return walk(sqlObj); } let capturedExecuteQuery: unknown = null; vi.mock("@/drizzle/db", () => ({ db: { execute: vi.fn((query: unknown) => { capturedExecuteQuery = query; return Promise.resolve([]); }), }, })); vi.mock("@/drizzle/schema", () => ({ providerEndpointProbeLogs: {}, providerEndpoints: { vendorId: "vendorId", providerType: "providerType", isEnabled: "isEnabled", lastProbeOk: "lastProbeOk", deletedAt: "deletedAt", }, })); vi.mock("@/lib/logger", () => ({ logger: { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); describe("findProviderEndpointProbeLogsBatch - integer cast regression", () => { beforeEach(() => { capturedExecuteQuery = null; }); it("CTE VALUES must cast endpoint IDs to ::integer to avoid 'integer = text' error", async () => { const { findProviderEndpointProbeLogsBatch } = await import( "@/repository/provider-endpoints-batch" ); await findProviderEndpointProbeLogsBatch({ endpointIds: [10, 20, 30], limitPerEndpoint: 5, }); expect(capturedExecuteQuery).toBeTruthy(); const sqlStr = sqlToString(capturedExecuteQuery); // The VALUES clause must contain ::integer casts to prevent PG type mismatch expect(sqlStr).toContain("::integer"); }); it("single endpoint ID also gets ::integer cast", async () => { const { findProviderEndpointProbeLogsBatch } = await import( "@/repository/provider-endpoints-batch" ); await findProviderEndpointProbeLogsBatch({ endpointIds: [42], limitPerEndpoint: 1, }); expect(capturedExecuteQuery).toBeTruthy(); const sqlStr = sqlToString(capturedExecuteQuery); expect(sqlStr).toContain("::integer"); }); it("empty endpointIds should not execute any query", async () => { const { findProviderEndpointProbeLogsBatch } = await import( "@/repository/provider-endpoints-batch" ); const result = await findProviderEndpointProbeLogsBatch({ endpointIds: [], limitPerEndpoint: 5, }); expect(capturedExecuteQuery).toBeNull(); expect(result.size).toBe(0); }); });