fix(kysely-adapter): report affected rows and insert id from bun/node sqlite dialects

This commit is contained in:
Taesu
2026-05-31 10:17:44 +09:00
parent 4c3bbc4e56
commit 00b404329c
5 changed files with 111 additions and 7 deletions
@@ -0,0 +1,5 @@
---
"@better-auth/kysely-adapter": patch
---
`deleteMany` and `updateMany` now return the correct number of affected rows on the `bun:sqlite` and `node:sqlite` dialects, which previously always returned `0`.
@@ -2,7 +2,7 @@
* @see {@link https://github.com/dylanblokhuis/kysely-bun-sqlite} - Fork of the original kysely-bun-sqlite package by @dylanblokhuis
*/
import type { Database } from "bun:sqlite";
import type { Database, SQLQueryBindings } from "bun:sqlite";
import type {
DatabaseConnection,
DatabaseIntrospector,
@@ -132,8 +132,21 @@ class BunSqliteConnection implements DatabaseConnection {
const { sql, parameters } = compiledQuery;
const stmt = this.#db.prepare(sql);
// SELECT and RETURNING statements expose columns and return rows.
// A plain mutation reports its row count and insert id only through run().
if (stmt.columnNames.length > 0) {
return Promise.resolve({
rows: stmt.all(...(parameters as SQLQueryBindings[])) as O[],
});
}
const { changes, lastInsertRowid } = stmt.run(
...(parameters as SQLQueryBindings[]),
);
return Promise.resolve({
rows: stmt.all(parameters as any) as O[],
rows: [],
numAffectedRows: changes != null ? BigInt(changes) : undefined,
insertId: lastInsertRowid != null ? BigInt(lastInsertRowid) : undefined,
});
}
@@ -0,0 +1,69 @@
import { DatabaseSync } from "node:sqlite";
import type { Generated } from "kysely";
import { Kysely } from "kysely";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { NodeSqliteDialect } from "./node-sqlite-dialect";
interface TestDatabase {
item: {
id: Generated<number>;
name: string;
};
}
describe("NodeSqliteDialect", () => {
let raw: DatabaseSync;
let db: Kysely<TestDatabase>;
beforeEach(async () => {
raw = new DatabaseSync(":memory:");
db = new Kysely<TestDatabase>({
dialect: new NodeSqliteDialect({ database: raw }),
});
await db.schema
.createTable("item")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
.addColumn("name", "text")
.execute();
await db
.insertInto("item")
.values([{ name: "a" }, { name: "b" }, { name: "c" }])
.execute();
});
afterEach(async () => {
await db.destroy();
});
// A mutation without a RETURNING clause must still report how many rows it
// touched. updateMany/deleteMany in the adapter read numUpdatedRows /
// numDeletedRows, which Kysely derives from the connection's numAffectedRows
// (falling back to 0 when absent). A dialect that drops it silently reports 0.
it("reports the number of rows affected by a delete", async () => {
const result = await db
.deleteFrom("item")
.where("name", "in", ["a", "b"])
.executeTakeFirst();
expect(Number(result.numDeletedRows)).toBe(2);
});
it("reports the number of rows affected by an update", async () => {
const result = await db
.updateTable("item")
.set({ name: "z" })
.executeTakeFirst();
expect(Number(result.numUpdatedRows)).toBe(3);
});
it("reports the inserted row id when no RETURNING clause is used", async () => {
const result = await db
.insertInto("item")
.values({ name: "d" })
.executeTakeFirst();
expect(result.insertId).toBeDefined();
expect(Number(result.insertId)).toBeGreaterThan(0);
});
});
@@ -2,7 +2,7 @@
* @see {@link https://nodejs.org/api/sqlite.html} - Node.js SQLite API documentation
*/
import type { DatabaseSync } from "node:sqlite";
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
import type {
DatabaseConnection,
DatabaseIntrospector,
@@ -132,10 +132,21 @@ class NodeSqliteConnection implements DatabaseConnection {
const { sql, parameters } = compiledQuery;
const stmt = this.#db.prepare(sql);
const rows = stmt.all(...(parameters as any[])) as O[];
// SELECT and RETURNING statements expose columns and return rows.
// A plain mutation reports its row count and insert id only through run().
if (stmt.columns().length > 0) {
return Promise.resolve({
rows: stmt.all(...(parameters as SQLInputValue[])) as O[],
});
}
const { changes, lastInsertRowid } = stmt.run(
...(parameters as SQLInputValue[]),
);
return Promise.resolve({
rows,
rows: [],
numAffectedRows: changes != null ? BigInt(changes) : undefined,
insertId: lastInsertRowid != null ? BigInt(lastInsertRowid) : undefined,
});
}
@@ -20,8 +20,14 @@ function asBunLikeDatabase(db: DatabaseSync) {
prepare(sql: string) {
const stmt = db.prepare(sql);
return {
all(params: SQLInputValue[]) {
return stmt.all(...(params ?? []));
get columnNames() {
return stmt.columns().map((column) => column.name);
},
all(...params: SQLInputValue[]) {
return stmt.all(...params);
},
run(...params: SQLInputValue[]) {
return stmt.run(...params);
},
};
},