Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

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" });
});
});