javascript: Implement transactions API

This commit is contained in:
Pekka Enberg
2025-08-19 16:29:02 +03:00
parent 387d384394
commit 54b4fdaa7d
5 changed files with 90 additions and 15 deletions

View File

@@ -38,6 +38,7 @@ class Database {
db: NativeDB; db: NativeDB;
memory: boolean; memory: boolean;
open: boolean; open: boolean;
private _inTransaction: boolean = false;
/** /**
* Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
@@ -61,9 +62,7 @@ class Database {
Object.defineProperties(this, { Object.defineProperties(this, {
inTransaction: { inTransaction: {
get() { get: () => this._inTransaction,
throw new Error("not implemented");
},
}, },
name: { name: {
get() { get() {
@@ -117,12 +116,15 @@ class Database {
const wrapTxn = (mode) => { const wrapTxn = (mode) => {
return (...bindParameters) => { return (...bindParameters) => {
db.exec("BEGIN " + mode); db.exec("BEGIN " + mode);
db._inTransaction = true;
try { try {
const result = fn(...bindParameters); const result = fn(...bindParameters);
db.exec("COMMIT"); db.exec("COMMIT");
db._inTransaction = false;
return result; return result;
} catch (err) { } catch (err) {
db.exec("ROLLBACK"); db.exec("ROLLBACK");
db._inTransaction = false;
throw err; throw err;
} }
}; };

View File

@@ -38,6 +38,7 @@ class Database {
db: NativeDB; db: NativeDB;
memory: boolean; memory: boolean;
open: boolean; open: boolean;
private _inTransaction: boolean = false;
/** /**
* Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created.
* *
@@ -65,9 +66,7 @@ class Database {
this.memory = db.memory; this.memory = db.memory;
Object.defineProperties(this, { Object.defineProperties(this, {
inTransaction: { inTransaction: {
get() { get: () => this._inTransaction,
throw new Error("not implemented");
},
}, },
name: { name: {
get() { get() {
@@ -121,12 +120,15 @@ class Database {
const wrapTxn = (mode) => { const wrapTxn = (mode) => {
return (...bindParameters) => { return (...bindParameters) => {
db.exec("BEGIN " + mode); db.exec("BEGIN " + mode);
db._inTransaction = true;
try { try {
const result = fn(...bindParameters); const result = fn(...bindParameters);
db.exec("COMMIT"); db.exec("COMMIT");
db._inTransaction = false;
return result; return result;
} catch (err) { } catch (err) {
db.exec("ROLLBACK"); db.exec("ROLLBACK");
db._inTransaction = false;
throw err; throw err;
} }
}; };

View File

@@ -17,6 +17,7 @@ export class Connection {
private session: Session; private session: Session;
private isOpen: boolean = true; private isOpen: boolean = true;
private defaultSafeIntegerMode: boolean = false; private defaultSafeIntegerMode: boolean = false;
private _inTransaction: boolean = false;
constructor(config: Config) { constructor(config: Config) {
if (!config.url) { if (!config.url) {
@@ -24,6 +25,19 @@ export class Connection {
} }
this.config = config; this.config = config;
this.session = new Session(config); this.session = new Session(config);
// Define inTransaction property
Object.defineProperty(this, 'inTransaction', {
get: () => this._inTransaction,
enumerable: true
});
}
/**
* Whether the database is currently in a transaction.
*/
get inTransaction(): boolean {
return this._inTransaction;
} }
/** /**
@@ -143,6 +157,63 @@ export class Connection {
this.defaultSafeIntegerMode = toggle === false ? false : true; this.defaultSafeIntegerMode = toggle === false ? false : true;
} }
/**
* Returns a function that executes the given function in a transaction.
*
* @param fn - The function to wrap in a transaction
* @returns A function that will execute fn within a transaction
*
* @example
* ```typescript
* const insert = await client.prepare("INSERT INTO users (name) VALUES (?)");
* const insertMany = client.transaction((users) => {
* for (const user of users) {
* insert.run([user]);
* }
* });
*
* await insertMany(['Alice', 'Bob', 'Charlie']);
* ```
*/
transaction(fn: (...args: any[]) => any): any {
if (typeof fn !== "function") {
throw new TypeError("Expected first argument to be a function");
}
const db = this;
const wrapTxn = (mode: string) => {
return async (...bindParameters: any[]) => {
await db.exec("BEGIN " + mode);
db._inTransaction = true;
try {
const result = await fn(...bindParameters);
await db.exec("COMMIT");
db._inTransaction = false;
return result;
} catch (err) {
await db.exec("ROLLBACK");
db._inTransaction = false;
throw err;
}
};
};
const properties = {
default: { value: wrapTxn("") },
deferred: { value: wrapTxn("DEFERRED") },
immediate: { value: wrapTxn("IMMEDIATE") },
exclusive: { value: wrapTxn("EXCLUSIVE") },
database: { value: this, enumerable: true },
};
Object.defineProperties(properties.default.value, properties);
Object.defineProperties(properties.deferred.value, properties);
Object.defineProperties(properties.immediate.value, properties);
Object.defineProperties(properties.exclusive.value, properties);
return properties.default.value;
}
/** /**
* Close the connection. * Close the connection.
* *

View File

@@ -145,16 +145,16 @@ test.serial("Database.pragma() after close()", async (t) => {
// Database.transaction() // Database.transaction()
// ========================================================================== // ==========================================================================
test.skip("Database.transaction()", async (t) => { test.serial("Database.transaction()", async (t) => {
const db = t.context.db; const db = t.context.db;
const insert = await db.prepare( const insert = await db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)" "INSERT INTO users(name, email) VALUES (:name, :email)"
); );
const insertMany = db.transaction((users) => { const insertMany = db.transaction(async (users) => {
t.is(db.inTransaction, true); t.is(db.inTransaction, true);
for (const user of users) insert.run(user); for (const user of users) await insert.run(user);
}); });
t.is(db.inTransaction, false); t.is(db.inTransaction, false);
@@ -166,12 +166,12 @@ test.skip("Database.transaction()", async (t) => {
t.is(db.inTransaction, false); t.is(db.inTransaction, false);
const stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); const stmt = await db.prepare("SELECT * FROM users WHERE id = ?");
t.is(stmt.get(3).name, "Joey"); t.is((await stmt.get(3)).name, "Joey");
t.is(stmt.get(4).name, "Sally"); t.is((await stmt.get(4)).name, "Sally");
t.is(stmt.get(5).name, "Junior"); t.is((await stmt.get(5)).name, "Junior");
}); });
test.skip("Database.transaction().immediate()", async (t) => { test.serial("Database.transaction().immediate()", async (t) => {
const db = t.context.db; const db = t.context.db;
const insert = await db.prepare( const insert = await db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)" "INSERT INTO users(name, email) VALUES (:name, :email)"

View File

@@ -138,7 +138,7 @@ test.serial("Database.pragma() after close()", async (t) => {
// Database.transaction() // Database.transaction()
// ========================================================================== // ==========================================================================
test.skip("Database.transaction()", async (t) => { test.serial("Database.transaction()", async (t) => {
const db = t.context.db; const db = t.context.db;
const insert = db.prepare( const insert = db.prepare(
@@ -164,7 +164,7 @@ test.skip("Database.transaction()", async (t) => {
t.is(stmt.get(5).name, "Junior"); t.is(stmt.get(5).name, "Junior");
}); });
test.skip("Database.transaction().immediate()", async (t) => { test.serial("Database.transaction().immediate()", async (t) => {
const db = t.context.db; const db = t.context.db;
const insert = db.prepare( const insert = db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)" "INSERT INTO users(name, email) VALUES (:name, :email)"