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

283 lines
9.7 KiB
TypeScript

import { CompiledQuery, Kysely } from "kysely";
import { describe, expect, it } from "vitest";
import { CoalescingD1Dialect } from "../../src/db/coalescing-d1.js";
interface MockResultConfig {
rows?: Record<string, unknown>[];
changes?: number;
lastRowId?: number;
error?: Error;
}
interface MockStatement {
sql: string;
params: unknown[];
bind: (...params: unknown[]) => MockStatement;
all: () => Promise<unknown>;
}
/**
* Hand-rolled mock of the D1Database subset the dialect uses (prepare/batch).
* Records every `all()` and `batch()` call (and their order) so tests can
* assert exactly which statements were coalesced.
*/
function createMockD1(resultsBySql: Record<string, MockResultConfig> = {}) {
const operations: string[] = [];
const batchCalls: MockStatement[][] = [];
const allCalls: MockStatement[] = [];
function d1Result(stmt: MockStatement) {
const config = resultsBySql[stmt.sql] ?? {};
if (config.error) throw config.error;
return {
success: true,
results: config.rows ?? [],
meta: { changes: config.changes ?? 0, last_row_id: config.lastRowId ?? 0 },
};
}
const database = {
prepare(sql: string): MockStatement {
const stmt: MockStatement = {
sql,
params: [],
bind(...params: unknown[]) {
stmt.params = params;
return stmt;
},
async all() {
operations.push(`all:${stmt.sql}`);
allCalls.push(stmt);
return d1Result(stmt);
},
};
return stmt;
},
// D1 batches are atomic: a single bad statement rejects the whole call,
// which the throw inside d1Result reproduces.
async batch(statements: MockStatement[]) {
operations.push(`batch:${statements.map((s) => s.sql).join("|")}`);
batchCalls.push(statements);
return statements.map((s) => d1Result(s));
},
};
return { database, operations, batchCalls, allCalls };
}
function createDb(mock: ReturnType<typeof createMockD1>) {
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- mock implements the prepare/batch subset the dialect uses
const database = mock.database as unknown as D1Database;
return new Kysely<any>({ dialect: new CoalescingD1Dialect({ database }) });
}
describe("CoalescingD1Dialect", () => {
it("coalesces SELECTs issued in the same turn into one batch in issue order", async () => {
const rowsA = [{ id: 1 }];
const rowsB = [{ id: 2 }, { id: 3 }];
const mock = createMockD1({
"SELECT * FROM a": { rows: rowsA },
"select * from b where id = ?": { rows: rowsB },
});
const db = createDb(mock);
const [r1, r2] = await Promise.all([
db.executeQuery(CompiledQuery.raw("SELECT * FROM a")),
db.executeQuery(CompiledQuery.raw("select * from b where id = ?", ["abc"])),
]);
expect(mock.batchCalls).toHaveLength(1);
expect(mock.batchCalls[0]?.map((s) => s.sql)).toEqual([
"SELECT * FROM a",
"select * from b where id = ?",
]);
// Each caller gets its own rows, and bind params are preserved.
expect(r1.rows).toEqual(rowsA);
expect(r2.rows).toEqual(rowsB);
expect(mock.batchCalls[0]?.[1]?.params).toEqual(["abc"]);
// Nothing went through the single-statement path.
expect(mock.allCalls).toHaveLength(0);
});
it("does not batch sequentially awaited SELECTs", async () => {
const mock = createMockD1({
"select 1": { rows: [{ one: 1 }] },
"select 2": { rows: [{ two: 2 }] },
});
const db = createDb(mock);
const r1 = await db.executeQuery(CompiledQuery.raw("select 1"));
const r2 = await db.executeQuery(CompiledQuery.raw("select 2"));
expect(mock.batchCalls).toHaveLength(0);
expect(mock.allCalls.map((s) => s.sql)).toEqual(["select 1", "select 2"]);
expect(r1.rows).toEqual([{ one: 1 }]);
expect(r2.rows).toEqual([{ two: 2 }]);
// Lone SELECTs flow through the buffer but report no affected rows.
expect(r1.numAffectedRows).toBeUndefined();
});
it("executes writes and CTEs immediately while same-turn SELECTs still coalesce", async () => {
const mock = createMockD1({ "insert into a (name) values (?)": { changes: 1, lastRowId: 7 } });
const db = createDb(mock);
await Promise.all([
db.executeQuery(CompiledQuery.raw("select * from a")),
db.executeQuery(CompiledQuery.raw("insert into a (name) values (?)", ["x"])),
db.executeQuery(CompiledQuery.raw("update a set name = ?", ["y"])),
db.executeQuery(CompiledQuery.raw("delete from a where id = ?", [1])),
db.executeQuery(CompiledQuery.raw("select * from b")),
// WITH is never coalesced: SQLite allows CTEs on writes.
db.executeQuery(CompiledQuery.raw("with x as (select 1) delete from a")),
]);
// Non-SELECTs hit the direct path before the macrotask flush fires.
expect(mock.operations).toEqual([
"all:insert into a (name) values (?)",
"all:update a set name = ?",
"all:delete from a where id = ?",
"all:with x as (select 1) delete from a",
"batch:select * from a|select * from b",
]);
});
it("falls back to individual execution when the batch rejects", async () => {
const failure = new Error("no such column: nope");
const mock = createMockD1({
"select good1": { rows: [{ ok: 1 }] },
"select nope": { error: failure },
"select good2": { rows: [{ ok: 2 }] },
});
const db = createDb(mock);
const [r1, r2, r3] = await Promise.allSettled([
db.executeQuery(CompiledQuery.raw("select good1")),
db.executeQuery(CompiledQuery.raw("select nope")),
db.executeQuery(CompiledQuery.raw("select good2")),
]);
// One atomic batch was attempted, then every statement re-ran solo.
expect(mock.batchCalls).toHaveLength(1);
expect(mock.allCalls.map((s) => s.sql)).toEqual([
"select good1",
"select nope",
"select good2",
]);
expect(r1.status).toBe("fulfilled");
if (r1.status === "fulfilled") expect(r1.value.rows).toEqual([{ ok: 1 }]);
expect(r2.status).toBe("rejected");
if (r2.status === "rejected") expect(r2.reason).toBe(failure);
expect(r3.status).toBe("fulfilled");
if (r3.status === "fulfilled") expect(r3.value.rows).toEqual([{ ok: 2 }]);
});
it("maps rows, numAffectedRows and insertId on the direct path", async () => {
const mock = createMockD1({
"insert into a (name) values (?)": { changes: 2, lastRowId: 7 },
"delete from a where 1 = 0": { changes: 0 },
});
const db = createDb(mock);
const inserted = await db.executeQuery(
CompiledQuery.raw("insert into a (name) values (?)", ["x"]),
);
expect(inserted.rows).toEqual([]);
expect(inserted.numAffectedRows).toBe(2n);
expect(inserted.insertId).toBe(7n);
expect(mock.allCalls[0]?.params).toEqual(["x"]);
// Zero changes maps to undefined, matching kysely-d1.
const deleted = await db.executeQuery(CompiledQuery.raw("delete from a where 1 = 0"));
expect(deleted.numAffectedRows).toBeUndefined();
expect(mock.batchCalls).toHaveLength(0);
});
it("starts a new batch window after a flush", async () => {
const mock = createMockD1();
const db = createDb(mock);
await Promise.all([
db.executeQuery(CompiledQuery.raw("select 1")),
db.executeQuery(CompiledQuery.raw("select 2")),
]);
await Promise.all([
db.executeQuery(CompiledQuery.raw("select 3")),
db.executeQuery(CompiledQuery.raw("select 4")),
]);
expect(mock.batchCalls).toHaveLength(2);
expect(mock.batchCalls[0]?.map((s) => s.sql)).toEqual(["select 1", "select 2"]);
expect(mock.batchCalls[1]?.map((s) => s.sql)).toEqual(["select 3", "select 4"]);
});
it("never overlaps physical session calls: a write and a SELECT batch run serially", async () => {
// The driver flips supportsMultipleConnections to true, which removes
// Kysely's connection mutex. A same-turn write (direct path) must still
// not run concurrently with the buffered-SELECT batch on the shared D1
// session — overlapping calls could interleave the session bookmark.
// This mock counts how many physical calls are in flight at once; the
// pre-serialization code reaches 2 here.
let inFlight = 0;
let maxInFlight = 0;
const ops: string[] = [];
const okResult = () => ({ success: true, results: [], meta: { changes: 0, last_row_id: 0 } });
async function hold<T>(label: string, value: T): Promise<T> {
inFlight++;
maxInFlight = Math.max(maxInFlight, inFlight);
ops.push(label);
// Hold the "connection" across a macrotask so a concurrent call,
// if one were issued, would be observed in flight simultaneously.
await new Promise((resolve) => setTimeout(resolve, 0));
inFlight--;
return value;
}
function makeStatement(sql: string): MockStatement {
const stmt: MockStatement = {
sql,
params: [],
bind(...params: unknown[]) {
stmt.params = params;
return stmt;
},
all: () => hold(`all:${stmt.sql}`, okResult()),
};
return stmt;
}
const database = {
prepare: (sql: string) => makeStatement(sql),
batch: (statements: MockStatement[]) =>
hold(
`batch:${statements.map((s) => s.sql).join("|")}`,
statements.map(() => okResult()),
),
};
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- mock implements the prepare/batch subset the dialect uses
const db = new Kysely<any>({
dialect: new CoalescingD1Dialect({ database: database as unknown as D1Database }),
});
await Promise.all([
db.executeQuery(CompiledQuery.raw("select * from a")),
db.executeQuery(CompiledQuery.raw("select * from b")),
db.executeQuery(CompiledQuery.raw("update a set n = ?", ["x"])),
]);
expect(maxInFlight).toBe(1);
// The write executes first (direct path, enqueued immediately), then the
// coalesced SELECT batch — never overlapping.
expect(ops).toEqual(["all:update a set n = ?", "batch:select * from a|select * from b"]);
});
it("rejects transactions", async () => {
const db = createDb(createMockD1());
await expect(
db.transaction().execute(async () => {
// never reached
}),
).rejects.toThrow("Transactions are not supported");
});
});