Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ clean-repos:
rm -rf repos/

# Run all tests in Docker
test: test-deno-2.7
test: test-node22 test-node24 test-deno-2.6 test-deno-2.7

# Run Node + Vitest tests (Drizzle shared suite)
test-node22:
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ const db = drizzle(client.run, client.batch, { schema: { users } });
client.db.exec("PRAGMA journal_mode = WAL");
```

### Drizzle Kit Driver (Experimental)

A drop-in replacement for better-sqlite3 in drizzle-kit's connections is available:

```ts
import { createDrizzleKitDriver } from "@hotsauce/drizzle-runtime-sqlite/kit";

const driver = await createDrizzleKitDriver({ url: "./database.db" });

// Returns drizzle-kit compatible interface:
// { query, run, proxy, transactionProxy, migrate, packageName: "node:sqlite" }
```

> **Note:** This driver is intended for integration into drizzle-kit itself and is not currently tested independently. Use at your own risk.

## Requirements

- Deno 2.6.0+ (first version with `stmt.columns()` support)
Expand Down
9 changes: 7 additions & 2 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
{
"name": "@hotsauce/drizzle-runtime-sqlite",
"version": "0.1.0",
"exports": "./mod.ts",
"version": "0.1.1",
"exports": {
".": "./mod.ts",
"./kit": "./src/kit.ts"
},
"imports": {
"drizzle-orm": "npm:drizzle-orm@0.45.1",
"drizzle-orm/sqlite-core": "npm:drizzle-orm@0.45.1/sqlite-core",
"drizzle-orm/sqlite-proxy": "npm:drizzle-orm@0.45.1/sqlite-proxy",
"drizzle-orm/migrator": "npm:drizzle-orm@0.45.1/migrator",
"drizzle-orm/sqlite-proxy/migrator": "npm:drizzle-orm@0.45.1/sqlite-proxy/migrator",
"vitest": "npm:vitest@^1.6.0",
"vitest/config": "npm:vitest@^1.6.0/config"
},
Expand Down
92 changes: 2 additions & 90 deletions src/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,7 @@ import type {
ProxyResult,
SqliteOptions,
} from "./types.ts";

/**
* Extended StatementSync interface for newer Node.js APIs not yet in Deno's types.
* These APIs exist in Node 22.16+/24+.
*/
interface ExtendedStatementMethods {
columns(): { name: string }[];
setReturnArrays(enabled: boolean): void;
setAllowUnknownNamedParameters(enabled: boolean): void;
}

/**
* Cast a StatementSync to include extended methods.
*/
function asExtended(stmt: StatementSync): ExtendedStatementMethods {
return stmt as unknown as ExtendedStatementMethods;
}
import { asExtended, executeWithArrayMode } from "./shared.ts";

/**
* Apply statement options from SqliteOptions to a prepared statement.
Expand All @@ -53,25 +37,6 @@ function applyStatementOptions(
}
}

/**
* Convert an object row to an array using column order from statement metadata.
* Note: This doesn't work correctly for joins with duplicate column names.
*/
function rowToArray(
row: Record<string, unknown>,
columns: { name: string }[],
): unknown[] {
return columns.map((col) => row[col.name]);
}

/**
* Check if the statement supports setReturnArrays (Node 22.16+/24+)
*/
function hasArrayMode(stmt: StatementSync): boolean {
return typeof (stmt as unknown as ExtendedStatementMethods)
.setReturnArrays === "function";
}

/**
* Execute a prepared statement based on the method type.
*
Expand All @@ -85,61 +50,8 @@ function executeStatement(
params: unknown[],
method: ProxyMethod,
): ProxyResult {
// Try to use array mode if available (Node 22.16+/24+)
const extStmt = asExtended(stmt);
const useArrayMode = hasArrayMode(stmt);
if (useArrayMode) {
extStmt.setReturnArrays(true);
}

const typedParams = params as SupportedValueType[];

switch (method) {
case "run": {
stmt.run(...typedParams);
return { rows: [] };
}
case "get": {
const row = stmt.get(...typedParams) as
| unknown[]
| Record<string, unknown>
| undefined;
if (row === undefined) {
return { rows: undefined };
}
if (useArrayMode) {
// Already an array
return { rows: row as unknown[] };
}
// Convert object to array using column metadata
const columns = extStmt.columns();
return { rows: rowToArray(row as Record<string, unknown>, columns) };
}
case "all":
case "values": {
const rows = stmt.all(...typedParams) as unknown[][] | Record<
string,
unknown
>[];
if (rows.length === 0) {
return { rows: [] };
}
if (useArrayMode) {
// Already arrays
return { rows: rows as unknown[][] };
}
// Convert objects to arrays using column metadata
const columns = extStmt.columns();
return {
rows: (rows as Record<string, unknown>[]).map((row) =>
rowToArray(row, columns)
),
};
}
default: {
throw new Error(`Unknown method: ${method}`);
}
}
return executeWithArrayMode(stmt, typedParams, method) as ProxyResult;
}

/**
Expand Down
214 changes: 214 additions & 0 deletions src/kit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* Drizzle Kit driver for node:sqlite
*
* Provides a drop-in replacement for better-sqlite3 in drizzle-kit's connections.
*/

import { DatabaseSync } from "node:sqlite";
import type { MigrationConfig } from "drizzle-orm/migrator";
import {
asExtended,
executeWithArrayMode,
hasArrayMode,
normaliseSQLiteUrl,
prepareSqliteParams,
rowToArray,
} from "./shared.ts";

/** SQLiteDB interface matching drizzle-kit's expectations */
export interface SQLiteDB {
query: <T>(sql: string, params?: unknown[]) => Promise<T[]>;
run: (query: string) => Promise<void>;
}

/**
* Proxy params matching drizzle-kit's ProxyParams.
* This is a practical subset - drizzle-kit's full type has mode/method required,
* but SQLite proxy paths don't always provide them.
*/
export interface ProxyParams {
sql: string;
params?: unknown[];
method?: "run" | "all" | "get" | "values" | "execute";
mode?: "array" | "object";
}

/** Proxy function type matching drizzle-kit */
export type Proxy = (params: ProxyParams) => Promise<unknown[]>;

/** Transaction proxy params */
export interface TransactionQuery {
sql: string;
method?: "run" | "all" | "get" | "values";
}

/** Transaction proxy function type matching drizzle-kit */
export type TransactionProxy = (
queries: TransactionQuery[],
) => Promise<(unknown[] | Error)[]>;

/** Credentials for SQLite connection */
export interface SqliteCredentials {
url: string;
}

/** Return type of createDrizzleKitDriver */
export interface DrizzleKitDriver extends SQLiteDB {
packageName: "node:sqlite";
proxy: Proxy;
transactionProxy: TransactionProxy;
migrate: (config: MigrationConfig) => Promise<void>;
}

/**
* Create a Drizzle Kit compatible driver using node:sqlite.
*
* This provides the same interface as drizzle-kit's better-sqlite3 driver,
* allowing it to be used as a drop-in replacement.
*
* @param credentials - SQLite credentials with url property
* @returns Driver object compatible with drizzle-kit
*
* @example
* ```ts
* import { createDrizzleKitDriver } from "@hotsauce/drizzle-runtime-sqlite/kit";
*
* const driver = await createDrizzleKitDriver({ url: "./database.db" });
* // Use with drizzle-kit internals
* ```
*/
export async function createDrizzleKitDriver(
credentials: SqliteCredentials,
): Promise<DrizzleKitDriver> {
const { drizzle } = await import("drizzle-orm/sqlite-proxy");
const { migrate } = await import("drizzle-orm/sqlite-proxy/migrator");

const sqlite = new DatabaseSync(normaliseSQLiteUrl(credentials.url));

/**
* Callback for sqlite-proxy following its expected semantics.
* Note: This is only used internally for the migration drizzle instance.
* Migrations use "run" method, so get/all semantics don't matter in practice,
* but we keep them correct for safety.
*/
const sqliteProxyCallback = (
sql: string,
params: unknown[],
method: "run" | "all" | "get" | "values",
): { rows: unknown[] | unknown[][] | undefined } => {
const stmt = sqlite.prepare(sql);
return executeWithArrayMode(stmt, params, method);
};

// Create drizzle instance for migrations (internal use only)
// Note: drizzle's AsyncRemoteCallback expects { rows: any[] }, but sqlite-proxy
// semantics return undefined for get() with no row. We coerce here since
// migrations only use "run" method which always returns { rows: [] }.
const drzl = drizzle((sql, params, method) => {
const result = sqliteProxyCallback(sql, params as unknown[], method);
return Promise.resolve({
rows: result.rows ?? [],
});
});

const migrateFn = (config: MigrationConfig): Promise<void> => {
return migrate(
drzl,
(queries) => {
for (const query of queries) {
sqliteProxyCallback(query, [], "run");
}
return Promise.resolve();
},
config,
);
};

const db: SQLiteDB = {
query: <T>(sql: string, params: unknown[] = []): Promise<T[]> => {
const stmt = sqlite.prepare(sql);
return Promise.resolve(
stmt.all(...(params as Parameters<typeof stmt.all>)) as T[],
);
},
run: (query: string): Promise<void> => {
sqlite.prepare(query).run();
return Promise.resolve();
},
};

const proxy: Proxy = (params: ProxyParams): Promise<unknown[]> => {
const preparedParams = prepareSqliteParams(params.params || []);
const stmt = sqlite.prepare(params.sql);

const method = params.method ?? "all";

if (method === "values" || method === "get" || method === "all") {
const useArrayMode = hasArrayMode(stmt);
if (useArrayMode && params.mode === "array") {
asExtended(stmt).setReturnArrays(true);
}

const rows = stmt.all(
...(preparedParams as Parameters<typeof stmt.all>),
) as unknown[][] | Record<string, unknown>[];

if (params.mode === "array" && !useArrayMode && rows.length > 0) {
// Fallback: convert objects to arrays using column metadata
const columns = asExtended(stmt).columns();
return Promise.resolve(
(rows as Record<string, unknown>[]).map((row) =>
rowToArray(row, columns)
),
);
}

return Promise.resolve(rows);
}

stmt.run(...(preparedParams as Parameters<typeof stmt.run>));
return Promise.resolve([]);
};

const transactionProxy: TransactionProxy = (
queries: TransactionQuery[],
): Promise<(unknown[] | Error)[]> => {
const results: (unknown[] | Error)[] = [];

try {
sqlite.exec("BEGIN");

for (const query of queries) {
const stmt = sqlite.prepare(query.sql);
const method = query.method ?? "all";

let result: unknown[] = [];
if (method === "values" || method === "get" || method === "all") {
result = stmt.all() as unknown[];
} else {
stmt.run();
}
results.push(result);
}

sqlite.exec("COMMIT");
} catch (error) {
try {
sqlite.exec("ROLLBACK");
} catch {
// Ignore rollback errors
}
results.push(error as Error);
}

return Promise.resolve(results);
};

return {
...db,
packageName: "node:sqlite",
proxy,
transactionProxy,
migrate: migrateFn,
};
}
Loading