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;
memory: 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.
@@ -61,9 +62,7 @@ class Database {
Object.defineProperties(this, {
inTransaction: {
get() {
throw new Error("not implemented");
},
get: () => this._inTransaction,
},
name: {
get() {
@@ -117,12 +116,15 @@ class Database {
const wrapTxn = (mode) => {
return (...bindParameters) => {
db.exec("BEGIN " + mode);
db._inTransaction = true;
try {
const result = fn(...bindParameters);
db.exec("COMMIT");
db._inTransaction = false;
return result;
} catch (err) {
db.exec("ROLLBACK");
db._inTransaction = false;
throw err;
}
};

View File

@@ -38,6 +38,7 @@ class Database {
db: NativeDB;
memory: 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.
*
@@ -65,9 +66,7 @@ class Database {
this.memory = db.memory;
Object.defineProperties(this, {
inTransaction: {
get() {
throw new Error("not implemented");
},
get: () => this._inTransaction,
},
name: {
get() {
@@ -121,12 +120,15 @@ class Database {
const wrapTxn = (mode) => {
return (...bindParameters) => {
db.exec("BEGIN " + mode);
db._inTransaction = true;
try {
const result = fn(...bindParameters);
db.exec("COMMIT");
db._inTransaction = false;
return result;
} catch (err) {
db.exec("ROLLBACK");
db._inTransaction = false;
throw err;
}
};

View File

@@ -17,6 +17,7 @@ export class Connection {
private session: Session;
private isOpen: boolean = true;
private defaultSafeIntegerMode: boolean = false;
private _inTransaction: boolean = false;
constructor(config: Config) {
if (!config.url) {
@@ -24,6 +25,19 @@ export class Connection {
}
this.config = 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;
}
/**
* 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.
*

View File

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

View File

@@ -138,7 +138,7 @@ test.serial("Database.pragma() after close()", async (t) => {
// Database.transaction()
// ==========================================================================
test.skip("Database.transaction()", async (t) => {
test.serial("Database.transaction()", async (t) => {
const db = t.context.db;
const insert = db.prepare(
@@ -164,7 +164,7 @@ test.skip("Database.transaction()", async (t) => {
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 insert = db.prepare(
"INSERT INTO users(name, email) VALUES (:name, :email)"