b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
import { CompiledQuery } from "kysely";
|
|
import { describe, it, expect, vi } from "vitest";
|
|
|
|
import { CoalescingDOSqlDialect } from "../../src/db/coalescing-do-sql.js";
|
|
import type { BookmarkSink } from "../../src/db/do-sql-dialect.js";
|
|
import type { DOQueryResult, EmDashDBStub } from "../../src/db/do-sql-types.js";
|
|
|
|
function setup(
|
|
opts: {
|
|
query?: ReturnType<typeof vi.fn>;
|
|
batchQuery?: ReturnType<typeof vi.fn>;
|
|
readBookmark?: string;
|
|
bookmarkSink?: BookmarkSink;
|
|
onRpc?: () => void;
|
|
} = {},
|
|
) {
|
|
const query = opts.query ?? vi.fn().mockResolvedValue({ rows: [] });
|
|
const batchQuery = opts.batchQuery ?? vi.fn().mockResolvedValue([]);
|
|
const stub = { query, batchQuery } as unknown as EmDashDBStub;
|
|
const dialect = new CoalescingDOSqlDialect({
|
|
resolveStub: () => stub,
|
|
readBookmark: opts.readBookmark,
|
|
bookmarkSink: opts.bookmarkSink,
|
|
onRpc: opts.onRpc,
|
|
});
|
|
return { query, batchQuery, dialect };
|
|
}
|
|
|
|
describe("CoalescingDOSqlDialect", () => {
|
|
it("reports supportsMultipleConnections so Kysely won't serialize behind a mutex", () => {
|
|
const { dialect } = setup();
|
|
expect(dialect.createAdapter().supportsMultipleConnections).toBe(true);
|
|
});
|
|
|
|
it("rejects transactions (matches the non-coalescing driver)", async () => {
|
|
const { dialect } = setup();
|
|
const driver = dialect.createDriver();
|
|
const conn = await driver.acquireConnection();
|
|
await expect(driver.beginTransaction(conn, {})).rejects.toThrow(
|
|
/transactions are not supported/i,
|
|
);
|
|
});
|
|
|
|
it("batches same-turn SELECTs into one batchQuery RPC", async () => {
|
|
const batchQuery = vi
|
|
.fn()
|
|
.mockResolvedValue([{ rows: [{ id: "a" }] }, { rows: [{ id: "b" }] }] as DOQueryResult[]);
|
|
const { query, dialect } = setup({ batchQuery });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
const [r1, r2] = await Promise.all([
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM a")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM b")),
|
|
]);
|
|
|
|
expect(batchQuery).toHaveBeenCalledTimes(1);
|
|
expect(batchQuery.mock.calls[0]![0]).toEqual([
|
|
{ sql: "SELECT * FROM a", params: [] },
|
|
{ sql: "SELECT * FROM b", params: [] },
|
|
]);
|
|
expect(r1.rows).toEqual([{ id: "a" }]);
|
|
expect(r2.rows).toEqual([{ id: "b" }]);
|
|
expect(query).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("counts one RPC for a coalesced batch (N queries -> 1 round trip)", async () => {
|
|
const batchQuery = vi
|
|
.fn()
|
|
.mockResolvedValue([{ rows: [] }, { rows: [] }, { rows: [] }] as DOQueryResult[]);
|
|
const onRpc = vi.fn();
|
|
const { dialect } = setup({ batchQuery, onRpc });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
await Promise.all([
|
|
conn.executeQuery(CompiledQuery.raw("SELECT 1")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT 2")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT 3")),
|
|
]);
|
|
|
|
expect(onRpc).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("runs a lone SELECT via query(), not batchQuery", async () => {
|
|
const query = vi.fn().mockResolvedValue({ rows: [{ id: "x" }] });
|
|
const { batchQuery, dialect } = setup({ query });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
const result = await conn.executeQuery(CompiledQuery.raw("SELECT * FROM solo"));
|
|
|
|
expect(query).toHaveBeenCalledTimes(1);
|
|
expect(batchQuery).not.toHaveBeenCalled();
|
|
expect(result.rows).toEqual([{ id: "x" }]);
|
|
});
|
|
|
|
it("sends writes down the direct query() path, never the batch", async () => {
|
|
const query = vi.fn().mockResolvedValue({ rows: [], changes: 1, bookmark: "bm" });
|
|
const { batchQuery, dialect } = setup({ query });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("INSERT INTO a (id) VALUES (?)", ["1"]));
|
|
|
|
expect(query).toHaveBeenCalledWith("INSERT INTO a (id) VALUES (?)", ["1"], undefined);
|
|
expect(batchQuery).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("forwards the effective bookmark with the batch", async () => {
|
|
const batchQuery = vi.fn().mockResolvedValue([{ rows: [] }, { rows: [] }] as DOQueryResult[]);
|
|
const sink: BookmarkSink = { latest: "bm-fresh" };
|
|
const { dialect } = setup({ batchQuery, readBookmark: "bm-cookie", bookmarkSink: sink });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
await Promise.all([
|
|
conn.executeQuery(CompiledQuery.raw("SELECT 1")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT 2")),
|
|
]);
|
|
|
|
// Sink (freshest write bookmark) wins over the initial cookie bookmark.
|
|
expect(batchQuery).toHaveBeenCalledWith(expect.any(Array), { bookmark: "bm-fresh" });
|
|
});
|
|
|
|
it("falls back to individual query() calls when the batch RPC fails", async () => {
|
|
const batchQuery = vi.fn().mockRejectedValue(new Error("batch boom"));
|
|
const query = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ rows: [{ id: "a" }] })
|
|
.mockResolvedValueOnce({ rows: [{ id: "b" }] });
|
|
const { dialect } = setup({ query, batchQuery });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
const [r1, r2] = await Promise.all([
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM a")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM b")),
|
|
]);
|
|
|
|
expect(batchQuery).toHaveBeenCalledTimes(1);
|
|
expect(query).toHaveBeenCalledTimes(2);
|
|
expect(r1.rows).toEqual([{ id: "a" }]);
|
|
expect(r2.rows).toEqual([{ id: "b" }]);
|
|
});
|
|
|
|
it("rejects only the missing statement if the batch returns too few results", async () => {
|
|
const batchQuery = vi.fn().mockResolvedValue([{ rows: [{ id: "a" }] }] as DOQueryResult[]);
|
|
const { dialect } = setup({ batchQuery });
|
|
const conn = await dialect.createDriver().acquireConnection();
|
|
|
|
const results = await Promise.allSettled([
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM a")),
|
|
conn.executeQuery(CompiledQuery.raw("SELECT * FROM b")),
|
|
]);
|
|
|
|
expect(results[0]).toMatchObject({ status: "fulfilled" });
|
|
expect(results[1]).toMatchObject({ status: "rejected" });
|
|
});
|
|
});
|