From 1367b453e9892bffe6100b01e4da51f66bc63ee6 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Thu, 29 May 2025 15:59:07 -0300 Subject: [PATCH] bind/js: Add proper exec() method --- .../__test__/artifacts/basic-test.sql | 3 ++ .../__test__/better-sqlite3.spec.mjs | 18 ++++++++++ bindings/javascript/__test__/limbo.spec.mjs | 17 +++++++++ bindings/javascript/src/lib.rs | 36 +++++++++++++++++-- 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 bindings/javascript/__test__/artifacts/basic-test.sql diff --git a/bindings/javascript/__test__/artifacts/basic-test.sql b/bindings/javascript/__test__/artifacts/basic-test.sql new file mode 100644 index 000000000..e75ef4432 --- /dev/null +++ b/bindings/javascript/__test__/artifacts/basic-test.sql @@ -0,0 +1,3 @@ +CREATE TABLE users (name TEXT, age INTEGER); +INSERT INTO users (name, age) VALUES ('Bob', 24); +INSERT INTO users (name, age) VALUES ('Alice', 42); \ No newline at end of file diff --git a/bindings/javascript/__test__/better-sqlite3.spec.mjs b/bindings/javascript/__test__/better-sqlite3.spec.mjs index 998155562..38b5e7b00 100644 --- a/bindings/javascript/__test__/better-sqlite3.spec.mjs +++ b/bindings/javascript/__test__/better-sqlite3.spec.mjs @@ -1,4 +1,7 @@ import test from "ava"; +import fs from "node:fs"; +import { fileURLToPath } from "url"; +import path from "node:path" import Database from "better-sqlite3"; @@ -77,6 +80,21 @@ test("Test bind()", async (t) => { ); }); +test("Test exec()", async (t) => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const [db] = await connect(":memory:"); + const file = fs.readFileSync(path.resolve(__dirname, "./artifacts/basic-test.sql"), "utf8"); + db.exec(file); + let rows = db.prepare("SELECT * FROM users").iterate(); + for (const row of rows) { + t.truthy(row.name); + t.true(typeof row.age === "number"); + } +}); + + const connect = async (path) => { const db = new Database(path); return [db]; diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index b7ba2e6fc..be3326137 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -1,4 +1,7 @@ import test from "ava"; +import fs from "node:fs"; +import { fileURLToPath } from "url"; +import path from "node:path"; import { Database } from "../wrapper.js"; @@ -103,6 +106,20 @@ test("Test pluck(): Rows should only have the values of the first column", async } }); +test("Test exec()", async (t) => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const [db] = await connect(":memory:"); + const file = fs.readFileSync(path.resolve(__dirname, "./artifacts/basic-test.sql"), "utf8"); + db.exec(file); + let rows = db.prepare("SELECT * FROM users").iterate(); + for (const row of rows) { + t.truthy(row.name); + t.true(typeof row.age === "number"); + } +}); + const connect = async (path) => { const db = new Database(path); return [db]; diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index de112f8c3..e14c7ea6a 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -7,7 +7,7 @@ use std::rc::Rc; use std::sync::Arc; use limbo_core::types::Text; -use limbo_core::{maybe_init_database_file, LimboError}; +use limbo_core::{maybe_init_database_file, LimboError, StepResult}; use napi::iterator::Generator; use napi::JsObject; use napi::{bindgen_prelude::ObjectFinalize, Env, JsUnknown}; @@ -119,7 +119,39 @@ impl Database { #[napi] pub fn exec(&self, sql: String) -> napi::Result<()> { - self.conn.execute(sql).map_err(into_napi_error)?; + let query_runner = self.conn.query_runner(sql.as_bytes()); + + // Since exec doesn't return any values, we can just iterate over the results + for output in query_runner { + match output { + Ok(Some(mut stmt)) => loop { + match stmt.step() { + Ok(StepResult::Row) => continue, + Ok(StepResult::IO) => self.io.run_once().map_err(into_napi_error)?, + Ok(StepResult::Done) => break, + Ok(StepResult::Interrupt | StepResult::Busy) => { + return Err(napi::Error::new( + napi::Status::GenericFailure, + "Statement execution interrupted or busy".to_string(), + )); + } + Err(err) => { + return Err(napi::Error::new( + napi::Status::GenericFailure, + format!("Error executing SQL: {}", err), + )); + } + } + }, + Ok(None) => continue, + Err(err) => { + return Err(napi::Error::new( + napi::Status::GenericFailure, + format!("Error executing SQL: {}", err), + )); + } + } + } Ok(()) }