provider-endpoints-batch-integer-cast.test.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. /**
  3. * Regression test for: operator does not exist: integer = text
  4. *
  5. * The CTE `VALUES ($1), ($2)` generated by Drizzle infers columns as text.
  6. * The LATERAL join then compares integer (table column) to text (CTE column),
  7. * which PostgreSQL rejects. The fix adds an explicit `::integer` cast.
  8. */
  9. function sqlToString(sqlObj: unknown): string {
  10. const visited = new Set<unknown>();
  11. const walk = (node: unknown): string => {
  12. if (!node || visited.has(node)) return "";
  13. visited.add(node);
  14. if (typeof node === "string") return node;
  15. if (typeof node === "number") return String(node);
  16. if (typeof node === "object") {
  17. const anyNode = node as Record<string, unknown>;
  18. if (Array.isArray(anyNode)) {
  19. return anyNode.map(walk).join("");
  20. }
  21. if (anyNode.value !== undefined) {
  22. if (Array.isArray(anyNode.value)) {
  23. return (anyNode.value as unknown[]).map(walk).join("");
  24. }
  25. return walk(anyNode.value);
  26. }
  27. if (anyNode.queryChunks) {
  28. return walk(anyNode.queryChunks);
  29. }
  30. }
  31. return "";
  32. };
  33. return walk(sqlObj);
  34. }
  35. let capturedExecuteQuery: unknown = null;
  36. vi.mock("@/drizzle/db", () => ({
  37. db: {
  38. execute: vi.fn((query: unknown) => {
  39. capturedExecuteQuery = query;
  40. return Promise.resolve([]);
  41. }),
  42. },
  43. }));
  44. vi.mock("@/drizzle/schema", () => ({
  45. providerEndpointProbeLogs: {},
  46. providerEndpoints: {
  47. vendorId: "vendorId",
  48. providerType: "providerType",
  49. isEnabled: "isEnabled",
  50. lastProbeOk: "lastProbeOk",
  51. deletedAt: "deletedAt",
  52. },
  53. }));
  54. vi.mock("@/lib/logger", () => ({
  55. logger: {
  56. trace: vi.fn(),
  57. debug: vi.fn(),
  58. info: vi.fn(),
  59. warn: vi.fn(),
  60. error: vi.fn(),
  61. },
  62. }));
  63. describe("findProviderEndpointProbeLogsBatch - integer cast regression", () => {
  64. beforeEach(() => {
  65. capturedExecuteQuery = null;
  66. });
  67. it("CTE VALUES must cast endpoint IDs to ::integer to avoid 'integer = text' error", async () => {
  68. const { findProviderEndpointProbeLogsBatch } = await import(
  69. "@/repository/provider-endpoints-batch"
  70. );
  71. await findProviderEndpointProbeLogsBatch({
  72. endpointIds: [10, 20, 30],
  73. limitPerEndpoint: 5,
  74. });
  75. expect(capturedExecuteQuery).toBeTruthy();
  76. const sqlStr = sqlToString(capturedExecuteQuery);
  77. // The VALUES clause must contain ::integer casts to prevent PG type mismatch
  78. expect(sqlStr).toContain("::integer");
  79. });
  80. it("single endpoint ID also gets ::integer cast", async () => {
  81. const { findProviderEndpointProbeLogsBatch } = await import(
  82. "@/repository/provider-endpoints-batch"
  83. );
  84. await findProviderEndpointProbeLogsBatch({
  85. endpointIds: [42],
  86. limitPerEndpoint: 1,
  87. });
  88. expect(capturedExecuteQuery).toBeTruthy();
  89. const sqlStr = sqlToString(capturedExecuteQuery);
  90. expect(sqlStr).toContain("::integer");
  91. });
  92. it("empty endpointIds should not execute any query", async () => {
  93. const { findProviderEndpointProbeLogsBatch } = await import(
  94. "@/repository/provider-endpoints-batch"
  95. );
  96. const result = await findProviderEndpointProbeLogsBatch({
  97. endpointIds: [],
  98. limitPerEndpoint: 5,
  99. });
  100. expect(capturedExecuteQuery).toBeNull();
  101. expect(result.size).toBe(0);
  102. });
  103. });