branch: main
sql_iterator.spec.ts
9693 bytesRaw
import { describe, test, expect } from "vitest";
import { mf, mfUrl } from "./mf";

const productNames = ["Laptop", "Mouse", "Keyboard", "Monitor", "Headphones"];
const expectedTypedStrings = [
  "Product 1: Laptop - $999.99 (in stock: true)",
  "Product 2: Mouse - $29.99 (in stock: true)",
  "Product 3: Keyboard - $79.99 (in stock: false)",
  "Product 4: Monitor - $299.99 (in stock: true)",
  "Product 5: Headphones - $149.99 (in stock: false)",
];

describe("sql iterator durable object", () => {
  test("next() iterator returns typed results", async () => {
    const resp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/test/next`,
    );
    expect(resp.status).toBe(200);

    const text = await resp.text();
    expect(text).toContain("next() iterator results:");

    // Check that we get all 5 products
    for (const s of expectedTypedStrings) {
      expect(text).toContain(s);
    }
  });

  test("raw() iterator returns column names and raw values", async () => {
    const resp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/test/raw`,
    );
    expect(resp.status).toBe(200);

    const text = await resp.text();
    expect(text).toContain("raw() iterator results:");

    // Check column names are included
    expect(text).toContain("Columns: id, name, price, in_stock");

    // Check that we get raw data rows - should contain all products
    for (const name of productNames) {
      expect(text).toContain(`String("${name}")`);
    }

    // Check for different data types in raw format
    expect(text).toContain("Integer("); // For IDs
    expect(text).toContain("Float(999.99)");
    expect(text).toContain("Integer(1)"); // in_stock: true
    expect(text).toContain("Integer(0)"); // in_stock: false
  });

  test("different object instances have independent data", async () => {
    // Test first instance
    const resp1 = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/instance1/next`,
    );
    expect(resp1.status).toBe(200);
    const text1 = await resp1.text();
    expect(text1).toContain("Product 1: Laptop");

    // Test second instance - should have same seeded data
    const resp2 = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/instance2/next`,
    );
    expect(resp2.status).toBe(200);
    const text2 = await resp2.text();
    expect(text2).toContain("Product 1: Laptop");

    // Both should be identical since they use the same seed data
    expect(text1).toBe(text2);
  });

  test("iterator handles empty results gracefully", async () => {
    // Create a new instance to test empty query behavior
    const resp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/empty-test/raw`,
    );
    expect(resp.status).toBe(200);

    const text = await resp.text();
    // Should still show column names even with data
    expect(text).toContain("Columns: id, name, price, in_stock");
  });

  test("next() iterator handles deserialization errors", async () => {
    const resp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/test/next-invalid`,
    );
    expect(resp.status).toBe(200);

    const text = await resp.text();
    expect(text).toContain("next-invalid() iterator results:");
    
    // Should have 5 error messages (one for each row that failed deserialization)
    const deserializationErrors = text.match(/Error deserializing row:/g);
    expect(deserializationErrors).toBeTruthy();
    expect(deserializationErrors!.length).toBe(5);
    
    // Check that the error messages contain information about the type mismatch
    expect(text).toContain("invalid type");
  });

  test.each([
    ["root", ""],
    ["invalid", "/invalid"],
  ])("%s endpoint returns help message", async (_, path) => {
    const resp = await mf.dispatchFetch(
      `http://fake.host/sql-iterator/test${path}`,
    );
    expect(resp.status).toBe(200);

    const text = await resp.text();
    expect(text).toBe("SQL Iterator Test - try /next, /raw, /next-invalid, /blob-next, /blob-raw, or /blob-roundtrip endpoints");
  });

  test("data consistency between next() and raw() methods", async () => {
    // Get data from next() method
    const nextResp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/consistency-test/next`,
    );
    expect(nextResp.status).toBe(200);
    const nextText = await nextResp.text();

    // Get data from raw() method
    const rawResp = await mf.dispatchFetch(
      `${mfUrl}sql-iterator/consistency-test/raw`,
    );
    expect(rawResp.status).toBe(200);
    const rawText = await rawResp.text();

    // Both should contain the same 5 products
    for (const name of productNames) {
      expect(nextText).toContain(name);
      expect(rawText).toContain(name);
    }

    // Raw should show 5 data rows (plus column header)
    const rowMatches = rawText.match(/Row: \[/g);
    expect(rowMatches).toBeTruthy();
    expect(rowMatches!.length).toBe(5);
  });

  describe("BLOB handling", () => {
    test("blob-next() iterator returns BLOB data correctly", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/blob-test/blob-next`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("blob-next() iterator results:");

      // Check for various BLOB test cases
      expect(text).toContain("binary_data"); // Binary data test case
      expect(text).toContain("empty_blob"); // Empty blob test case
      expect(text).toContain("text_as_blob"); // Text converted to blob
      expect(text).toContain("large_blob"); // Large blob test case

      // Check that binary data is displayed correctly
      expect(text).toContain("[0, 1, 2, 3, 255, 254]");
      
      // Check empty blob handling
      expect(text).toContain("data: []");
      
      // Check large blob truncation
      expect(text).toContain("[1000 bytes total]");
    });

    test("blob-raw() iterator returns raw BLOB values", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/blob-test/blob-raw`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("blob-raw() iterator results:");

      // Check column names are included
      expect(text).toContain("Columns: id, name, data");

      // Check BLOB data in raw format
      expect(text).toContain("Blob([0, 1, 2, 3, 255, 254])");
      expect(text).toContain("Blob([])"); // Empty blob
      expect(text).toContain("Blob([72, 101, 108, 108, 111"); // "Hello" bytes
      
      // Check large blob truncation in raw format
      expect(text).toMatch(/Blob\(\[0, 1, 2, 3, 4, 5, 6, 7, 8, 9\]\.\.\..*1000 bytes/);
    });

    test("blob roundtrip test verifies data integrity", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/roundtrip-test/blob-roundtrip`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("blob-roundtrip test results:");

      // Check that original data is shown
      expect(text).toContain("Original data: [222, 173, 190, 239, 0, 255]");

      // Check that both next() and raw() methods return matching data
      expect(text).toContain("next() result: [222, 173, 190, 239, 0, 255], matches_original: true");
      expect(text).toContain("raw() result: [222, 173, 190, 239, 0, 255], matches_original: true");
    });

    test("BLOB data consistency between next() and raw() methods", async () => {
      // Get data from blob-next() method
      const nextResp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/consistency-blob-test/blob-next`,
      );
      expect(nextResp.status).toBe(200);
      const nextText = await nextResp.text();

      // Get data from blob-raw() method
      const rawResp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/consistency-blob-test/blob-raw`,
      );
      expect(rawResp.status).toBe(200);
      const rawText = await rawResp.text();

      // Both should contain the same BLOB test data
      const blobNames = ["binary_data", "empty_blob", "text_as_blob", "large_blob"];
      for (const name of blobNames) {
        expect(nextText).toContain(name);
        expect(rawText).toContain(name);
      }

      // Both should show 4 BLOB data rows
      const nextRows = nextText.match(/BlobData \d+:/g);
      const rawRows = rawText.match(/Row: \[/g);
      expect(nextRows).toBeTruthy();
      expect(rawRows).toBeTruthy();
      expect(nextRows!.length).toBe(4);
      expect(rawRows!.length).toBe(4);
    });

    test("empty BLOB handling", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/empty-blob-test/blob-next`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("empty_blob");
      expect(text).toContain("data: []");
    });

    test("large BLOB handling", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/large-blob-test/blob-raw`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("large_blob");
      // Large blob should be truncated for display
      expect(text).toMatch(/Blob\(\[.*\]\.\.\..*1000 bytes/);
    });

    test("binary data with null bytes handling", async () => {
      const resp = await mf.dispatchFetch(
        `${mfUrl}sql-iterator/binary-test/blob-next`,
      );
      expect(resp.status).toBe(200);

      const text = await resp.text();
      expect(text).toContain("binary_data");
      // Should handle null bytes (0x00) and high bytes (0xFF) correctly
      expect(text).toContain("[0, 1, 2, 3, 255, 254]");
    });
  });
});