mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 12:56:05 +00:00
inlined libsql dialect, rewrote d1 to use generic sqlite
This commit is contained in:
@@ -1,42 +1,76 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { SqliteConnection } from "bknd/data";
|
||||
import type { ConnQuery, ConnQueryResults } from "data/connection/Connection";
|
||||
import { D1Dialect } from "kysely-d1";
|
||||
import {
|
||||
genericSqlite,
|
||||
type GenericSqliteConnection,
|
||||
} from "data/connection/sqlite/GenericSqliteConnection";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
export type D1SqliteConnection = GenericSqliteConnection<D1Database>;
|
||||
|
||||
export type D1ConnectionConfig<DB extends D1Database | D1DatabaseSession = D1Database> = {
|
||||
binding: DB;
|
||||
};
|
||||
|
||||
export class D1Connection<
|
||||
DB extends D1Database | D1DatabaseSession = D1Database,
|
||||
> extends SqliteConnection<DB> {
|
||||
override name = "sqlite-d1";
|
||||
export function d1Sqlite(config: D1ConnectionConfig<D1Database>) {
|
||||
const db = config.binding;
|
||||
|
||||
protected override readonly supported = {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
};
|
||||
return genericSqlite(
|
||||
"d1-sqlite",
|
||||
db,
|
||||
(utils) => {
|
||||
const getStmt = (sql: string, parameters?: any[] | readonly any[]) =>
|
||||
db.prepare(sql).bind(...(parameters || []));
|
||||
|
||||
constructor(private config: D1ConnectionConfig<DB>) {
|
||||
super({
|
||||
const mapResult = (res: D1Result<any>): QueryResult<any> => {
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const numAffectedRows =
|
||||
res.meta.changes > 0 ? utils.parseBigInt(res.meta.changes) : undefined;
|
||||
const insertId = res.meta.last_row_id
|
||||
? utils.parseBigInt(res.meta.last_row_id)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
insertId,
|
||||
numAffectedRows,
|
||||
rows: res.results,
|
||||
// @ts-ignore
|
||||
meta: res.meta,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
batch: async (stmts) => {
|
||||
const res = await db.batch(
|
||||
stmts.map(({ sql, parameters }) => {
|
||||
return getStmt(sql, parameters);
|
||||
}),
|
||||
);
|
||||
return res.map(mapResult);
|
||||
},
|
||||
query: utils.buildQueryFn({
|
||||
all: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.all()).rows;
|
||||
},
|
||||
run: async (sql, parameters) => {
|
||||
const prep = getStmt(sql, parameters);
|
||||
return mapResult(await prep.run());
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
},
|
||||
{
|
||||
supports: {
|
||||
batching: true,
|
||||
softscans: false,
|
||||
},
|
||||
excludeTables: ["_cf_KV", "_cf_METADATA"],
|
||||
dialect: D1Dialect,
|
||||
dialectArgs: [{ database: config.binding as D1Database }],
|
||||
});
|
||||
}
|
||||
|
||||
override async executeQueries<O extends ConnQuery[]>(...qbs: O): Promise<ConnQueryResults<O>> {
|
||||
const compiled = this.getCompiled(...qbs);
|
||||
|
||||
const db = this.config.binding;
|
||||
|
||||
const res = await db.batch(
|
||||
compiled.map(({ sql, parameters }) => {
|
||||
return db.prepare(sql).bind(...parameters);
|
||||
}),
|
||||
);
|
||||
|
||||
return this.withTransformedRows(res, "results") as any;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
54
app/src/adapter/cloudflare/connection/D1Connection.vitest.ts
Normal file
54
app/src/adapter/cloudflare/connection/D1Connection.vitest.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
|
||||
import { viTestRunner } from "adapter/node/vitest";
|
||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||
import { Miniflare } from "miniflare";
|
||||
import { d1Sqlite } from "./D1Connection";
|
||||
import { sql } from "kysely";
|
||||
|
||||
describe("d1Sqlite", async () => {
|
||||
const mf = new Miniflare({
|
||||
modules: true,
|
||||
script: "export default { async fetch() { return new Response(null); } }",
|
||||
d1Databases: ["DB"],
|
||||
});
|
||||
const binding = (await mf.getD1Database("DB")) as D1Database;
|
||||
|
||||
test("connection", async () => {
|
||||
const conn = d1Sqlite({ binding });
|
||||
expect(conn.supports("batching")).toBe(true);
|
||||
expect(conn.supports("softscans")).toBe(false);
|
||||
});
|
||||
|
||||
test("query details", async () => {
|
||||
const conn = d1Sqlite({ binding });
|
||||
|
||||
const res = await conn.executeQuery(sql`select 1`.compile(conn.kysely));
|
||||
expect(res.rows).toEqual([{ "1": 1 }]);
|
||||
expect(res.numAffectedRows).toBe(undefined);
|
||||
expect(res.insertId).toBe(undefined);
|
||||
// @ts-expect-error
|
||||
expect(res.meta.changed_db).toBe(false);
|
||||
// @ts-expect-error
|
||||
expect(res.meta.rows_read).toBe(0);
|
||||
|
||||
const batchResult = await conn.executeQueries(
|
||||
sql`select 1`.compile(conn.kysely),
|
||||
sql`select 2`.compile(conn.kysely),
|
||||
);
|
||||
|
||||
// rewrite to get index
|
||||
for (const [index, result] of batchResult.entries()) {
|
||||
expect(result.rows).toEqual([{ [String(index + 1)]: index + 1 }]);
|
||||
expect(result.numAffectedRows).toBe(undefined);
|
||||
expect(result.insertId).toBe(undefined);
|
||||
// @ts-expect-error
|
||||
expect(result.meta.changed_db).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
connectionTestSuite(viTestRunner, {
|
||||
makeConnection: () => d1Sqlite({ binding }),
|
||||
rawDialectDetails: [],
|
||||
});
|
||||
});
|
||||
138
app/src/adapter/cloudflare/connection/D1Dialect.ts
Normal file
138
app/src/adapter/cloudflare/connection/D1Dialect.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler,
|
||||
type CompiledQuery,
|
||||
type DatabaseConnection,
|
||||
type DatabaseIntrospector,
|
||||
type Dialect,
|
||||
type Driver,
|
||||
type Kysely,
|
||||
type QueryCompiler,
|
||||
type QueryResult,
|
||||
} from "kysely";
|
||||
|
||||
/**
|
||||
* Config for the D1 dialect. Pass your D1 instance to this object that you bound in `wrangler.toml`.
|
||||
*/
|
||||
export interface D1DialectConfig {
|
||||
database: D1Database;
|
||||
}
|
||||
|
||||
/**
|
||||
* D1 dialect that adds support for [Cloudflare D1][0] in [Kysely][1].
|
||||
* The constructor takes the instance of your D1 database that you bound in `wrangler.toml`.
|
||||
*
|
||||
* ```typescript
|
||||
* new D1Dialect({
|
||||
* database: env.DB,
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* [0]: https://blog.cloudflare.com/introducing-d1/
|
||||
* [1]: https://github.com/koskimas/kysely
|
||||
*/
|
||||
export class D1Dialect implements Dialect {
|
||||
#config: D1DialectConfig;
|
||||
|
||||
constructor(config: D1DialectConfig) {
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
createAdapter() {
|
||||
return new SqliteAdapter();
|
||||
}
|
||||
|
||||
createDriver(): Driver {
|
||||
return new D1Driver(this.#config);
|
||||
}
|
||||
|
||||
createQueryCompiler(): QueryCompiler {
|
||||
return new SqliteQueryCompiler();
|
||||
}
|
||||
|
||||
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db);
|
||||
}
|
||||
}
|
||||
|
||||
class D1Driver implements Driver {
|
||||
#config: D1DialectConfig;
|
||||
|
||||
constructor(config: D1DialectConfig) {
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
return new D1Connection(this.#config);
|
||||
}
|
||||
|
||||
async beginTransaction(conn: D1Connection): Promise<void> {
|
||||
return await conn.beginTransaction();
|
||||
}
|
||||
|
||||
async commitTransaction(conn: D1Connection): Promise<void> {
|
||||
return await conn.commitTransaction();
|
||||
}
|
||||
|
||||
async rollbackTransaction(conn: D1Connection): Promise<void> {
|
||||
return await conn.rollbackTransaction();
|
||||
}
|
||||
|
||||
async releaseConnection(_conn: D1Connection): Promise<void> {}
|
||||
|
||||
async destroy(): Promise<void> {}
|
||||
}
|
||||
|
||||
class D1Connection implements DatabaseConnection {
|
||||
#config: D1DialectConfig;
|
||||
|
||||
constructor(config: D1DialectConfig) {
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
||||
const results = await this.#config.database
|
||||
.prepare(compiledQuery.sql)
|
||||
.bind(...compiledQuery.parameters)
|
||||
.all();
|
||||
if (results.error) {
|
||||
throw new Error(results.error);
|
||||
}
|
||||
|
||||
const numAffectedRows = results.meta.changes > 0 ? BigInt(results.meta.changes) : undefined;
|
||||
|
||||
return {
|
||||
insertId:
|
||||
results.meta.last_row_id === undefined || results.meta.last_row_id === null
|
||||
? undefined
|
||||
: BigInt(results.meta.last_row_id),
|
||||
rows: (results?.results as O[]) || [],
|
||||
numAffectedRows,
|
||||
// @ts-ignore deprecated in kysely >= 0.23, keep for backward compatibility.
|
||||
numUpdatedOrDeletedRows: numAffectedRows,
|
||||
};
|
||||
}
|
||||
|
||||
async beginTransaction() {
|
||||
throw new Error("Transactions are not supported yet.");
|
||||
}
|
||||
|
||||
async commitTransaction() {
|
||||
throw new Error("Transactions are not supported yet.");
|
||||
}
|
||||
|
||||
async rollbackTransaction() {
|
||||
throw new Error("Transactions are not supported yet.");
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useYield: <explanation>
|
||||
async *streamQuery<O>(
|
||||
_compiledQuery: CompiledQuery,
|
||||
_chunkSize: number,
|
||||
): AsyncIterableIterator<QueryResult<O>> {
|
||||
throw new Error("D1 Driver does not support streaming");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user