diff --git a/crates/bindings/js/README.md b/crates/bindings/js/README.md index 86b645ab8a..d39983d158 100644 --- a/crates/bindings/js/README.md +++ b/crates/bindings/js/README.md @@ -3,6 +3,7 @@ ## Developing ```sh -$ npm install +$ cargo build +$ npm link $ node example.js ``` diff --git a/crates/bindings/js/example.js b/crates/bindings/js/examples/example.js similarity index 81% rename from crates/bindings/js/example.js rename to crates/bindings/js/examples/example.js index 9c6015ce2a..993dd28604 100644 --- a/crates/bindings/js/example.js +++ b/crates/bindings/js/examples/example.js @@ -1,4 +1,4 @@ -var libsql = require('.'); +import libsql from 'libsql-js'; var db = new libsql.Database(':memory:'); diff --git a/crates/bindings/js/examples/package.json b/crates/bindings/js/examples/package.json new file mode 100644 index 0000000000..0e2ecc6f03 --- /dev/null +++ b/crates/bindings/js/examples/package.json @@ -0,0 +1,8 @@ +{ + "name": "libsql-examples", + "type": "module", + "private": true, + "dependencies": { + "libsql-js": "^0.0.1" + } +} diff --git a/crates/bindings/js/index.js b/crates/bindings/js/index.js index 86c688efe0..055fa1a5b9 100644 --- a/crates/bindings/js/index.js +++ b/crates/bindings/js/index.js @@ -1,13 +1,29 @@ "use strict"; -const { databaseNew } = require("./index.node"); +const { databaseNew, databaseExec, databasePrepare, statementGet } = require("./index.node"); class Database { constructor(url) { this.db = databaseNew(url); } - all(sql, f) { + exec(sql) { + databaseExec.call(this.db, sql); + } + + prepare(sql) { + const stmt = databasePrepare.call(this.db, sql); + return new Statement(stmt); + } +} + +class Statement { + constructor(stmt) { + this.stmt = stmt; + } + + get(...bindParameters) { + return statementGet.call(this.stmt, ...bindParameters); } } diff --git a/crates/bindings/js/integration-tests/package.json b/crates/bindings/js/integration-tests/package.json new file mode 100644 index 0000000000..3cd31d5c73 --- /dev/null +++ b/crates/bindings/js/integration-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "libsql-js-integration-tests", + "type": "module", + "private": true, + "scripts": { + "test": "ava" + }, + "devDependencies": { + "ava": "^5.3.0" + }, + "dependencies": { + "better-sqlite3": "^8.4.0", + "libsql-js": "^0" + } +} diff --git a/crates/bindings/js/integration-tests/tests/better-sqlite3.test.js b/crates/bindings/js/integration-tests/tests/better-sqlite3.test.js new file mode 100644 index 0000000000..3392fc4390 --- /dev/null +++ b/crates/bindings/js/integration-tests/tests/better-sqlite3.test.js @@ -0,0 +1,17 @@ +import Database from "better-sqlite3"; +import test from "ava"; + +test("basic usage", (t) => { + const options = {}; + const db = new Database(":memory:", options); + + db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); + + db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); + + const userId = 1; + + const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userId); + + t.is(row.name, "Alice"); +}); diff --git a/crates/bindings/js/integration-tests/tests/libsql.js b/crates/bindings/js/integration-tests/tests/libsql.js new file mode 100644 index 0000000000..51df615ca8 --- /dev/null +++ b/crates/bindings/js/integration-tests/tests/libsql.js @@ -0,0 +1,17 @@ +import libsql from "libsql-js"; +import test from "ava"; + +test("basic usage", (t) => { + const options = {}; + const db = new libsql.Database(":memory:", options); + + db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); + + db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); + + const userId = 1; + + const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userId, "foo"); + + t.is(row.name, "Alice"); +}); diff --git a/crates/bindings/js/package-lock.json b/crates/bindings/js/package-lock.json index 1b9c86644b..51e0bb4b31 100644 --- a/crates/bindings/js/package-lock.json +++ b/crates/bindings/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "libsql-js", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "libsql-js", - "version": "0.1.0", + "version": "0.0.1", "hasInstallScript": true, "license": "ISC", "devDependencies": { diff --git a/crates/bindings/js/package.json b/crates/bindings/js/package.json index ef5b59a0e3..f5ac79ab17 100644 --- a/crates/bindings/js/package.json +++ b/crates/bindings/js/package.json @@ -1,6 +1,6 @@ { "name": "libsql-js", - "version": "0.1.0", + "version": "0.0.1", "description": "", "main": "index.js", "scripts": { diff --git a/crates/bindings/js/src/lib.rs b/crates/bindings/js/src/lib.rs index a4b6864594..88d46f1f9c 100644 --- a/crates/bindings/js/src/lib.rs +++ b/crates/bindings/js/src/lib.rs @@ -1,27 +1,101 @@ +use std::any::Any; + use libsql; use neon::prelude::*; struct Database { - _db: libsql::Database, + db: libsql::Database, + conn: libsql::Connection, } impl Finalize for Database {} impl Database { - fn new(db: libsql::Database) -> Self { - Database { _db: db } + fn new(db: libsql::Database, conn: libsql::Connection) -> Self { + Database { db, conn } } fn js_new(mut cx: FunctionContext) -> JsResult> { let url = cx.argument::(0)?.value(&mut cx); - let db = libsql::Database::open(url); - let db = Database::new(db); + let db = libsql::Database::open(url.clone()); + let conn = db.connect().unwrap(); + let db = Database::new(db, conn); Ok(cx.boxed(db)) } + + fn js_exec(mut cx: FunctionContext) -> JsResult { + let db = cx.this().downcast_or_throw::, _>(&mut cx)?; + let sql = cx.argument::(0)?.value(&mut cx); + db.conn.execute(sql, ()).unwrap(); + Ok(cx.undefined()) + } + + fn js_prepare(mut cx: FunctionContext) -> JsResult> { + let db = cx.this().downcast_or_throw::, _>(&mut cx)?; + let sql = cx.argument::(0)?.value(&mut cx); + let stmt = db.conn.prepare(sql).unwrap(); + let stmt = Statement { stmt }; + Ok(cx.boxed(stmt)) + } +} + +struct Statement { + stmt: libsql::Statement, +} + +impl Finalize for Statement {} + +fn js_value_to_value(cx: &mut FunctionContext, v: Handle<'_, JsValue>) -> libsql::Value { + if v.is_a::(cx) { + let v = v.downcast_or_throw::(cx).unwrap(); + let v = v.value(cx); + libsql::Value::Integer(v as i64) + } else if v.is_a::(cx) { + let v = v.downcast_or_throw::(cx).unwrap(); + let v = v.value(cx); + libsql::Value::Text(v) + } else { + todo!("unsupported type"); + } +} + +impl Statement { + fn js_get(mut cx: FunctionContext) -> JsResult { + let stmt = cx + .this() + .downcast_or_throw::, _>(&mut cx)?; + let mut params = vec![]; + for i in 0..cx.len() { + let v = cx.argument::(i)?; + let v = js_value_to_value(&mut cx, v); + params.push(v); + } + let params = libsql::Params::Positional(params); + let rows = stmt.stmt.execute(¶ms).unwrap(); + let row = rows.next().unwrap().unwrap(); + let result = cx.empty_object(); + for idx in 0..rows.column_count() { + let v = row.get_value(idx).unwrap(); + let column_name = rows.column_name(idx); + let key = cx.string(column_name); + let v: Handle<'_, JsValue> = match v { + libsql::Value::Null => cx.null().upcast(), + libsql::Value::Integer(v) => cx.number(v as f64).upcast(), + libsql::Value::Float(v) => cx.number(v).upcast(), + libsql::Value::Text(v) => cx.string(v).upcast(), + libsql::Value::Blob(v) => todo!("unsupported type"), + }; + result.set(&mut cx, key, v)?; + } + Ok(result) + } } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("databaseNew", Database::js_new)?; + cx.export_function("databaseExec", Database::js_exec)?; + cx.export_function("databasePrepare", Database::js_prepare)?; + cx.export_function("statementGet", Statement::js_get)?; Ok(()) } diff --git a/crates/core/src/params.rs b/crates/core/src/params.rs index b460614134..cbd707a200 100644 --- a/crates/core/src/params.rs +++ b/crates/core/src/params.rs @@ -1,3 +1,5 @@ +use crate::raw; + pub enum Params { None, Positional(Vec), @@ -44,3 +46,24 @@ impl From<&str> for Value { Value::Text(value.to_owned()) } } + +impl From for Value { + fn from(value: raw::Value) -> Value { + match value.value_type() { + crate::rows::ValueType::Null => Value::Null, + crate::rows::ValueType::Integer => Value::Integer(value.int().into()), + crate::rows::ValueType::Float => todo!(), + crate::rows::ValueType::Text => { + let v = value.text(); + if v.is_null() { + Value::Null + } else { + let v = unsafe { std::ffi::CStr::from_ptr(v as *const i8) }; + let v = v.to_str().unwrap(); + Value::Text(v.to_owned()) + } + } + crate::rows::ValueType::Blob => todo!(), + } + } +} diff --git a/crates/core/src/raw.rs b/crates/core/src/raw.rs index 363f1ac868..bcbabbfe4e 100644 --- a/crates/core/src/raw.rs +++ b/crates/core/src/raw.rs @@ -9,6 +9,12 @@ pub struct Statement { pub(crate) raw_stmt: *mut libsql_sys::ffi::sqlite3_stmt, } +// Safety: works as long as libSQL is compiled and set up with SERIALIZABLE threading model, which is the default. +unsafe impl Sync for Statement {} + +// Safety: works as long as libSQL is compiled and set up with SERIALIZABLE threading model, which is the default. +unsafe impl Send for Statement {} + impl Drop for Statement { fn drop(&mut self) { if !self.raw_stmt.is_null() { @@ -83,6 +89,13 @@ impl Statement { pub fn column_type(&self, idx: i32) -> i32 { unsafe { libsql_sys::ffi::sqlite3_column_type(self.raw_stmt, idx) } } + + pub fn column_name(&self, idx: i32) -> &str { + let raw_name = unsafe { libsql_sys::ffi::sqlite3_column_name(self.raw_stmt, idx) }; + let raw_name = unsafe { std::ffi::CStr::from_ptr(raw_name as *const i8) }; + let raw_name = raw_name.to_str().unwrap(); + raw_name + } } pub unsafe fn prepare_stmt(raw: *mut libsql_sys::ffi::sqlite3, sql: &str) -> Result { @@ -107,6 +120,11 @@ pub struct Value { } impl Value { + pub fn value_type(&self) -> crate::rows::ValueType { + let raw_type = unsafe { libsql_sys::ffi::sqlite3_value_type(self.raw_value) }; + crate::rows::ValueType::from(raw_type) + } + pub fn int(&self) -> i32 { unsafe { libsql_sys::ffi::sqlite3_value_int(self.raw_value) } } diff --git a/crates/core/src/rows.rs b/crates/core/src/rows.rs index 429f27e712..221764a32c 100644 --- a/crates/core/src/rows.rs +++ b/crates/core/src/rows.rs @@ -1,12 +1,12 @@ -use crate::{errors, raw, Error, Params, Result, Statement}; +use crate::{errors, raw, Error, Params, Result, Statement, Value}; use std::cell::RefCell; -use std::rc::Rc; +use std::sync::Arc; /// Query result rows. #[derive(Debug)] pub struct Rows { - pub(crate) stmt: Rc, + pub(crate) stmt: Arc, pub(crate) err: RefCell>, } @@ -31,6 +31,10 @@ impl Rows { pub fn column_count(&self) -> i32 { self.stmt.column_count() } + + pub fn column_name(&self, idx: i32) -> &str { + self.stmt.column_name(idx) + } } pub struct RowsFuture { @@ -59,7 +63,7 @@ impl futures::Future for RowsFuture { } pub struct Row { - pub(crate) stmt: Rc, + pub(crate) stmt: Arc, } impl Row { @@ -71,6 +75,11 @@ impl Row { T::from_sql(val) } + pub fn get_value(&self, idx: i32) -> Result { + let val = self.stmt.column_value(idx); + Ok(val.into()) + } + pub fn column_type(&self, idx: i32) -> Result { let val = self.stmt.column_type(idx); match val as u32 { @@ -82,6 +91,10 @@ impl Row { _ => Err(Error::UnknownColumnType(idx, val)), } } + + pub fn column_name(&self, idx: i32) -> &str { + self.stmt.column_name(idx) + } } pub enum ValueType { @@ -92,6 +105,19 @@ pub enum ValueType { Null, } +impl ValueType { + pub(crate) fn from(val_type: i32) -> ValueType { + match val_type as u32 { + libsql_sys::ffi::SQLITE_INTEGER => ValueType::Integer, + libsql_sys::ffi::SQLITE_FLOAT => ValueType::Float, + libsql_sys::ffi::SQLITE_BLOB => ValueType::Blob, + libsql_sys::ffi::SQLITE_TEXT => ValueType::Text, + libsql_sys::ffi::SQLITE_NULL => ValueType::Null, + _ => todo!(), + } + } +} + pub trait FromValue { fn from_sql(val: raw::Value) -> Result where diff --git a/crates/core/src/statement.rs b/crates/core/src/statement.rs index d62458b485..c143439dfc 100644 --- a/crates/core/src/statement.rs +++ b/crates/core/src/statement.rs @@ -1,18 +1,18 @@ use crate::{errors, raw, Error, Params, Result, Rows, Value}; use std::cell::RefCell; -use std::rc::Rc; +use std::sync::{Arc, Mutex}; /// A prepared statement. pub struct Statement { - inner: Rc, + inner: Arc, } impl Statement { pub(crate) fn prepare(raw: *mut libsql_sys::ffi::sqlite3, sql: &str) -> Result { match unsafe { raw::prepare_stmt(raw, sql) } { Ok(stmt) => Ok(Statement { - inner: Rc::new(stmt), + inner: Arc::new(stmt), }), Err(err) => Err(Error::PrepareFailed( sql.to_string(),