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
161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
import { CompiledQuery } from "kysely";
|
|
import { describe, it, expect, vi } from "vitest";
|
|
|
|
import { DOSqlDialect } from "../../src/db/do-sql-dialect.js";
|
|
import type { BookmarkSink, DOSqlDialectConfig } from "../../src/db/do-sql-dialect.js";
|
|
import type { EmDashDBStub } from "../../src/db/do-sql-types.js";
|
|
|
|
function createMockStub(queryFn = vi.fn()): EmDashDBStub {
|
|
return { query: queryFn, batchQuery: vi.fn() } as unknown as EmDashDBStub;
|
|
}
|
|
|
|
function createConfig(
|
|
queryFn = vi.fn(),
|
|
extra: Partial<DOSqlDialectConfig> = {},
|
|
): { config: DOSqlDialectConfig; resolveStub: ReturnType<typeof vi.fn> } {
|
|
const stub = createMockStub(queryFn);
|
|
const resolveStub = vi.fn(() => stub);
|
|
return { config: { resolveStub, ...extra }, resolveStub };
|
|
}
|
|
|
|
describe("DOSqlDialect", () => {
|
|
it("creates a SqliteAdapter and SqliteQueryCompiler", () => {
|
|
const { config } = createConfig();
|
|
const dialect = new DOSqlDialect(config);
|
|
expect(dialect.createAdapter().constructor.name).toBe("SqliteAdapter");
|
|
expect(dialect.createQueryCompiler().constructor.name).toBe("SqliteQueryCompiler");
|
|
});
|
|
});
|
|
|
|
describe("DOSqlDriver", () => {
|
|
it("rejects transactions (matches D1, so withTransaction degrades)", async () => {
|
|
const { config } = createConfig();
|
|
const driver = new DOSqlDialect(config).createDriver();
|
|
const conn = await driver.acquireConnection();
|
|
await expect(driver.beginTransaction(conn, {})).rejects.toThrow(
|
|
/transactions are not supported/i,
|
|
);
|
|
});
|
|
|
|
it("resolves a stub per acquire (driver must not cache a per-request stub)", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [] });
|
|
const { config, resolveStub } = createConfig(queryFn);
|
|
const driver = new DOSqlDialect(config).createDriver();
|
|
|
|
await driver.acquireConnection();
|
|
await driver.acquireConnection();
|
|
|
|
// The driver never caches: a DO stub is a per-request I/O object, so the
|
|
// cross-request-cached singleton dialect would otherwise reuse a stale stub.
|
|
expect(resolveStub).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("a memoizing resolveStub (the request-scoped pattern) yields one stub per request", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [] });
|
|
const stub = { query: queryFn, batchQuery: vi.fn() } as unknown as EmDashDBStub;
|
|
const make = vi.fn(() => stub);
|
|
// Mirrors createRequestScopedDb: a per-request closure memoizes the stub.
|
|
let cached: EmDashDBStub | undefined;
|
|
const resolveStub = () => (cached ??= make());
|
|
const driver = new DOSqlDialect({ resolveStub }).createDriver();
|
|
|
|
const c1 = await driver.acquireConnection();
|
|
await c1.executeQuery(CompiledQuery.raw("SELECT 1"));
|
|
const c2 = await driver.acquireConnection();
|
|
await c2.executeQuery(CompiledQuery.raw("SELECT 2"));
|
|
|
|
expect(make).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("DOSqlConnection", () => {
|
|
it("passes sql and parameters to the stub", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [], changes: 0 });
|
|
const { config } = createConfig(queryFn);
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("SELECT * FROM users WHERE id = ?", ["abc"]));
|
|
|
|
expect(queryFn).toHaveBeenCalledWith("SELECT * FROM users WHERE id = ?", ["abc"], undefined);
|
|
});
|
|
|
|
it("converts changes to bigint numAffectedRows", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [], changes: 3 });
|
|
const { config } = createConfig(queryFn);
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
const result = await conn.executeQuery(CompiledQuery.raw("UPDATE users SET name = ?", ["x"]));
|
|
|
|
expect(result.numAffectedRows).toBe(3n);
|
|
});
|
|
|
|
it("leaves numAffectedRows undefined for reads", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [{ count: 5 }] });
|
|
const { config } = createConfig(queryFn);
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
const result = await conn.executeQuery(CompiledQuery.raw("SELECT count(*) as count FROM x"));
|
|
|
|
expect(result.numAffectedRows).toBeUndefined();
|
|
expect(result.rows).toEqual([{ count: 5 }]);
|
|
});
|
|
|
|
it("forwards the read-your-writes bookmark only on reads", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [] });
|
|
const { config } = createConfig(queryFn, { readBookmark: "bm-123" });
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("SELECT * FROM posts"));
|
|
expect(queryFn).toHaveBeenLastCalledWith("SELECT * FROM posts", [], { bookmark: "bm-123" });
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("INSERT INTO posts (id) VALUES (?)", ["1"]));
|
|
// Writes never carry the read bookmark.
|
|
expect(queryFn).toHaveBeenLastCalledWith("INSERT INTO posts (id) VALUES (?)", ["1"], undefined);
|
|
});
|
|
|
|
it("records the latest write bookmark into the sink", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [], changes: 1, bookmark: "bm-new" });
|
|
const sink: BookmarkSink = {};
|
|
const { config } = createConfig(queryFn, { bookmarkSink: sink });
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("INSERT INTO posts (id) VALUES (?)", ["1"]));
|
|
|
|
expect(sink.latest).toBe("bm-new");
|
|
});
|
|
|
|
it("read-after-write: a read uses the fresh write bookmark, not the stale cookie one", async () => {
|
|
// Simulates create() then findById() in one request: the write mints a
|
|
// new bookmark; the follow-up read must wait for THAT, not the request's
|
|
// initial cookie bookmark, or it would miss the row on a lagging replica.
|
|
const queryFn = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ rows: [], changes: 1, bookmark: "bm-after-write" })
|
|
.mockResolvedValueOnce({ rows: [{ id: "1" }] });
|
|
const sink: BookmarkSink = {};
|
|
const { config } = createConfig(queryFn, {
|
|
readBookmark: "bm-stale-cookie",
|
|
bookmarkSink: sink,
|
|
});
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("INSERT INTO posts (id) VALUES (?)", ["1"]));
|
|
await conn.executeQuery(CompiledQuery.raw("SELECT * FROM posts WHERE id = ?", ["1"]));
|
|
|
|
expect(queryFn).toHaveBeenLastCalledWith("SELECT * FROM posts WHERE id = ?", ["1"], {
|
|
bookmark: "bm-after-write",
|
|
});
|
|
});
|
|
|
|
it("falls back to the initial cookie bookmark for reads before any write", async () => {
|
|
const queryFn = vi.fn().mockResolvedValue({ rows: [] });
|
|
const sink: BookmarkSink = {};
|
|
const { config } = createConfig(queryFn, { readBookmark: "bm-cookie", bookmarkSink: sink });
|
|
const conn = await new DOSqlDialect(config).createDriver().acquireConnection();
|
|
|
|
await conn.executeQuery(CompiledQuery.raw("SELECT * FROM posts"));
|
|
|
|
expect(queryFn).toHaveBeenLastCalledWith("SELECT * FROM posts", [], { bookmark: "bm-cookie" });
|
|
});
|
|
});
|