From 79412ea2cc86831e820918395fa60e6e7b44ef81 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 07:48:15 +0300 Subject: [PATCH 01/14] bindings/javascript: Improve error when prepare() called after close() --- bindings/javascript/index.d.ts | 2 ++ bindings/javascript/promise.js | 4 ++++ bindings/javascript/src/lib.rs | 9 +++++++++ bindings/javascript/sync.js | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/bindings/javascript/index.d.ts b/bindings/javascript/index.d.ts index f9447e696..097640feb 100644 --- a/bindings/javascript/index.d.ts +++ b/bindings/javascript/index.d.ts @@ -11,6 +11,8 @@ export declare class Database { constructor(path: string) /** Returns whether the database is in memory-only mode. */ get memory(): boolean + /** Returns whether the database connection is open. */ + get open(): boolean /** * Executes a batch of SQL statements. * diff --git a/bindings/javascript/promise.js b/bindings/javascript/promise.js index 7a54871c3..7ff274743 100644 --- a/bindings/javascript/promise.js +++ b/bindings/javascript/promise.js @@ -87,6 +87,10 @@ class Database { * @param {string} sql - The SQL statement string to prepare. */ prepare(sql) { + if (!this.open) { + throw new TypeError("The database connection is not open"); + } + if (!sql) { throw new RangeError("The supplied SQL string contains no statements"); } diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 7ce6eb521..df17170e1 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -35,6 +35,7 @@ pub struct Database { io: Arc, conn: Arc, is_memory: bool, + is_open: RefCell, } #[napi] @@ -76,6 +77,7 @@ impl Database { io, conn, is_memory, + is_open: RefCell::new(true), }) } @@ -85,6 +87,12 @@ impl Database { self.is_memory } + /// Returns whether the database connection is open. + #[napi(getter)] + pub fn open(&self) -> bool { + *self.is_open.borrow() + } + /// Executes a batch of SQL statements. /// /// # Arguments @@ -167,6 +175,7 @@ impl Database { /// `Ok(())` if the database is closed successfully. #[napi] pub fn close(&self) -> Result<()> { + *self.is_open.borrow_mut() = false; // Database close is handled automatically when dropped Ok(()) } diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js index bca456232..555d2c245 100644 --- a/bindings/javascript/sync.js +++ b/bindings/javascript/sync.js @@ -87,6 +87,10 @@ class Database { * @param {string} sql - The SQL statement string to prepare. */ prepare(sql) { + if (!this.open) { + throw new TypeError("The database connection is not open"); + } + if (!sql) { throw new RangeError("The supplied SQL string contains no statements"); } From f53adab4a8546016be01cf8c4e0dbcdd4695bd6a Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 07:50:12 +0300 Subject: [PATCH 02/14] serverless: Improve error when prepare() called after close() --- packages/turso-serverless/src/connection.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/turso-serverless/src/connection.ts b/packages/turso-serverless/src/connection.ts index 3f73a08bc..2a0f9a1e5 100644 --- a/packages/turso-serverless/src/connection.ts +++ b/packages/turso-serverless/src/connection.ts @@ -15,6 +15,7 @@ export interface Config extends SessionConfig {} export class Connection { private config: Config; private session: Session; + private isOpen: boolean = true; constructor(config: Config) { if (!config.url) { @@ -40,6 +41,9 @@ export class Connection { * ``` */ prepare(sql: string): Statement { + if (!this.isOpen) { + throw new TypeError("The database connection is not open"); + } return new Statement(this.config, sql); } @@ -97,6 +101,7 @@ export class Connection { * This sends a close request to the server to properly clean up the stream. */ async close(): Promise { + this.isOpen = false; await this.session.close(); } } From cda3375061a0eb21027e6fb263fef46b28b573da Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 07:50:23 +0300 Subject: [PATCH 03/14] testing/javascript: Enable prepare() after close() test case --- testing/javascript/__test__/async.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index a27797fff..448c5abe1 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -330,7 +330,7 @@ test.skip("errors", async (t) => { t.is(noTableError.rawCode, 1) }); -test.skip("Database.prepare() after close()", async (t) => { +test.serial("Database.prepare() after close()", async (t) => { const db = t.context.db; await db.close(); await t.throwsAsync(async () => { From 9ae96838ab2c0394cb259c0f92a1c6a7c596d3b8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 08:07:15 +0300 Subject: [PATCH 04/14] testing/javascript: Improve exec() after close() error --- bindings/javascript/promise.js | 4 ++++ bindings/javascript/sync.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/bindings/javascript/promise.js b/bindings/javascript/promise.js index 7ff274743..cd8d4d702 100644 --- a/bindings/javascript/promise.js +++ b/bindings/javascript/promise.js @@ -190,6 +190,10 @@ class Database { * @param {string} sql - The SQL statement string to execute. */ exec(sql) { + if (!this.open) { + throw new TypeError("The database connection is not open"); + } + try { this.db.batch(sql); } catch (err) { diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js index 555d2c245..a3bd934fc 100644 --- a/bindings/javascript/sync.js +++ b/bindings/javascript/sync.js @@ -190,6 +190,10 @@ class Database { * @param {string} sql - The SQL statement string to execute. */ exec(sql) { + if (!this.open) { + throw new TypeError("The database connection is not open"); + } + try { this.db.batch(sql); } catch (err) { From c9cec67d948d6cdab3712196410ae9d06e58c93a Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 08:08:19 +0300 Subject: [PATCH 05/14] serverless: Improve exec() after close() error --- packages/turso-serverless/src/connection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/turso-serverless/src/connection.ts b/packages/turso-serverless/src/connection.ts index 2a0f9a1e5..418822136 100644 --- a/packages/turso-serverless/src/connection.ts +++ b/packages/turso-serverless/src/connection.ts @@ -60,6 +60,9 @@ export class Connection { * ``` */ async exec(sql: string): Promise { + if (!this.isOpen) { + throw new TypeError("The database connection is not open"); + } return this.session.sequence(sql); } From 66b4de1ad91b52fa305a59dfffc34f46152f1209 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 08:08:35 +0300 Subject: [PATCH 06/14] testing/javascript: Enable exec() after close() test case --- testing/javascript/__test__/async.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index 448c5abe1..5fd3dd519 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -341,7 +341,7 @@ test.serial("Database.prepare() after close()", async (t) => { }); }); -test.skip("Database.exec() after close()", async (t) => { +test.serial("Database.exec() after close()", async (t) => { const db = t.context.db; await db.close(); await t.throwsAsync(async () => { From 2a36d133e39cb28fe6b9f23aedc4b6495d09b659 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:09:47 +0300 Subject: [PATCH 07/14] serverless: Improve pragma() after close() error --- packages/turso-serverless/src/connection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/turso-serverless/src/connection.ts b/packages/turso-serverless/src/connection.ts index 418822136..facc21562 100644 --- a/packages/turso-serverless/src/connection.ts +++ b/packages/turso-serverless/src/connection.ts @@ -94,6 +94,9 @@ export class Connection { * @returns Promise resolving to the result of the pragma */ async pragma(pragma: string): Promise { + if (!this.isOpen) { + throw new TypeError("The database connection is not open"); + } const sql = `PRAGMA ${pragma}`; return this.session.execute(sql); } From 53ac67a2be1c6a583eb92a0da4f94b9fa74e19ad Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:10:03 +0300 Subject: [PATCH 08/14] testing/javascript: Add pragma() after close() test case --- testing/javascript/__test__/async.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index 5fd3dd519..9c29b4860 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -341,6 +341,17 @@ test.serial("Database.prepare() after close()", async (t) => { }); }); +test.serial("Database.pragma() after close()", async (t) => { + const db = t.context.db; + await db.close(); + await t.throwsAsync(async () => { + await db.pragma("cache_size = 2000"); + }, { + instanceOf: TypeError, + message: "The database connection is not open" + }); +}); + test.serial("Database.exec() after close()", async (t) => { const db = t.context.db; await db.close(); From 6ad50f45816044618a625977b2a4c57da8a4653f Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:13:22 +0300 Subject: [PATCH 09/14] bindings/javascript: Fix prepare() error message format --- bindings/javascript/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index df17170e1..d1b77e9d7 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -122,12 +122,10 @@ impl Database { /// A `Statement` instance. #[napi] pub fn prepare(&self, sql: String) -> Result { - let stmt = self.conn.prepare(&sql).map_err(|e| { - Error::new( - Status::GenericFailure, - format!("Failed to prepare statement: {e}"), - ) - })?; + let stmt = self + .conn + .prepare(&sql) + .map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))?; let column_names: Vec = (0..stmt.num_columns()) .map(|i| std::ffi::CString::new(stmt.get_column_name(i).to_string()).unwrap()) .collect(); From 53acd22950f271a345c5138c323492ac4614088e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:22:35 +0300 Subject: [PATCH 10/14] serverless: Fix Statement.all() in expanded mode --- packages/turso-serverless/src/statement.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/turso-serverless/src/statement.ts b/packages/turso-serverless/src/statement.ts index c5dbbca99..3c5b3d27e 100644 --- a/packages/turso-serverless/src/statement.ts +++ b/packages/turso-serverless/src/statement.ts @@ -112,12 +112,15 @@ export class Statement { const result = await this.session.execute(this.sql, normalizedArgs); if (this.presentationMode === 'raw') { - // In raw mode, return arrays of values - // Each row is already an array with column properties added return result.rows.map((row: any) => [...row]); } - - return result.rows; + return result.rows.map((row: any) => { + const obj: any = {}; + result.columns.forEach((col: string, i: number) => { + obj[col] = row[i]; + }); + return obj; + }); } /** From 3a5e7f8fb6a4f6efffe8b886006829840219a180 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:22:48 +0300 Subject: [PATCH 11/14] testing/javascript: Enable Statement.all() test case --- testing/javascript/__test__/async.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index 9c29b4860..cc5991a45 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -147,7 +147,7 @@ test.skip("Statement.iterate()", async (t) => { } }); -test.skip("Statement.all()", async (t) => { +test.serial("Statement.all()", async (t) => { const db = t.context.db; const stmt = await db.prepare("SELECT * FROM users"); From 185b7016ddd6ceec34dbd820e438b9c5101c25d8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:23:16 +0300 Subject: [PATCH 12/14] testing/javascript: Enable Statement.all() raw mode test case --- testing/javascript/__test__/async.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index cc5991a45..943609ea9 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -158,7 +158,7 @@ test.serial("Statement.all()", async (t) => { t.deepEqual(await stmt.all(), expected); }); -test.skip("Statement.all() [raw]", async (t) => { +test.serial("Statement.all() [raw]", async (t) => { const db = t.context.db; const stmt = await db.prepare("SELECT * FROM users"); From ba37e1dc9ad4bc6a1520c18fdbf037a9df5dab2d Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 6 Aug 2025 09:25:22 +0300 Subject: [PATCH 13/14] testing/javascript: Enable some passing sync test cases --- testing/javascript/__test__/sync.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/javascript/__test__/sync.test.js b/testing/javascript/__test__/sync.test.js index d71e37ba4..7ef8cf001 100644 --- a/testing/javascript/__test__/sync.test.js +++ b/testing/javascript/__test__/sync.test.js @@ -415,7 +415,7 @@ test.skip("errors", async (t) => { } }); -test.skip("Database.prepare() after close()", async (t) => { +test.serial("Database.prepare() after close()", async (t) => { const db = t.context.db; db.close(); t.throws(() => { @@ -426,7 +426,7 @@ test.skip("Database.prepare() after close()", async (t) => { }); }); -test.skip("Database.exec() after close()", async (t) => { +test.serial("Database.exec() after close()", async (t) => { const db = t.context.db; db.close(); t.throws(() => { From fa6c92575157d2bae5e23c613acd85b0c8fe841e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Thu, 7 Aug 2025 07:47:10 +0300 Subject: [PATCH 14/14] bindings/javascript: Switch from RefCell to Cell --- bindings/javascript/src/lib.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index d1b77e9d7..01a5b3d5e 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -13,7 +13,11 @@ use napi::bindgen_prelude::*; use napi::{Env, Task}; use napi_derive::napi; -use std::{cell::RefCell, num::NonZeroUsize, sync::Arc}; +use std::{ + cell::{Cell, RefCell}, + num::NonZeroUsize, + sync::Arc, +}; /// Step result constants const STEP_ROW: u32 = 1; @@ -35,7 +39,7 @@ pub struct Database { io: Arc, conn: Arc, is_memory: bool, - is_open: RefCell, + is_open: Cell, } #[napi] @@ -77,7 +81,7 @@ impl Database { io, conn, is_memory, - is_open: RefCell::new(true), + is_open: Cell::new(true), }) } @@ -90,7 +94,7 @@ impl Database { /// Returns whether the database connection is open. #[napi(getter)] pub fn open(&self) -> bool { - *self.is_open.borrow() + self.is_open.get() } /// Executes a batch of SQL statements. @@ -173,7 +177,7 @@ impl Database { /// `Ok(())` if the database is closed successfully. #[napi] pub fn close(&self) -> Result<()> { - *self.is_open.borrow_mut() = false; + self.is_open.set(false); // Database close is handled automatically when dropped Ok(()) }