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..cd8d4d702 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"); } @@ -186,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/src/lib.rs b/bindings/javascript/src/lib.rs index 7ce6eb521..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,6 +39,7 @@ pub struct Database { io: Arc, conn: Arc, is_memory: bool, + is_open: Cell, } #[napi] @@ -76,6 +81,7 @@ impl Database { io, conn, is_memory, + is_open: Cell::new(true), }) } @@ -85,6 +91,12 @@ impl Database { self.is_memory } + /// Returns whether the database connection is open. + #[napi(getter)] + pub fn open(&self) -> bool { + self.is_open.get() + } + /// Executes a batch of SQL statements. /// /// # Arguments @@ -114,12 +126,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(); @@ -167,6 +177,7 @@ impl Database { /// `Ok(())` if the database is closed successfully. #[napi] pub fn close(&self) -> Result<()> { + self.is_open.set(false); // Database close is handled automatically when dropped Ok(()) } diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js index bca456232..a3bd934fc 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"); } @@ -186,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/packages/turso-serverless/src/connection.ts b/packages/turso-serverless/src/connection.ts index 3f73a08bc..facc21562 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); } @@ -56,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); } @@ -87,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); } @@ -97,6 +107,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(); } } 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; + }); } /** diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index a27797fff..943609ea9 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"); @@ -158,7 +158,7 @@ test.skip("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"); @@ -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 () => { @@ -341,7 +341,18 @@ test.skip("Database.prepare() after close()", async (t) => { }); }); -test.skip("Database.exec() 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(); await t.throwsAsync(async () => { 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(() => {