Skip to content

Commit

Permalink
Merge pull request #211 from penberg/javascript
Browse files Browse the repository at this point in the history
Improve JavaScript bindings
  • Loading branch information
penberg authored Jul 17, 2023
2 parents 334f663 + 8d35c44 commit f820c2c
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 19 deletions.
3 changes: 2 additions & 1 deletion crates/bindings/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Developing

```sh
$ npm install
$ cargo build
$ npm link
$ node example.js
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
var libsql = require('.');
import libsql from 'libsql-js';

var db = new libsql.Database(':memory:');

Expand Down
8 changes: 8 additions & 0 deletions crates/bindings/js/examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "libsql-examples",
"type": "module",
"private": true,
"dependencies": {
"libsql-js": "^0.0.1"
}
}
20 changes: 18 additions & 2 deletions crates/bindings/js/index.js
Original file line number Diff line number Diff line change
@@ -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);
}
}

Expand Down
15 changes: 15 additions & 0 deletions crates/bindings/js/integration-tests/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
17 changes: 17 additions & 0 deletions crates/bindings/js/integration-tests/tests/better-sqlite3.test.js
Original file line number Diff line number Diff line change
@@ -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', '[email protected]')");

const userId = 1;

const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userId);

t.is(row.name, "Alice");
});
17 changes: 17 additions & 0 deletions crates/bindings/js/integration-tests/tests/libsql.js
Original file line number Diff line number Diff line change
@@ -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', '[email protected]')");

const userId = 1;

const row = db.prepare("SELECT * FROM users WHERE id = ?").get(userId, "foo");

t.is(row.name, "Alice");
});
4 changes: 2 additions & 2 deletions crates/bindings/js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/bindings/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "libsql-js",
"version": "0.1.0",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
84 changes: 79 additions & 5 deletions crates/bindings/js/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<JsBox<Database>> {
let url = cx.argument::<JsString>(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<JsUndefined> {
let db = cx.this().downcast_or_throw::<JsBox<Database>, _>(&mut cx)?;
let sql = cx.argument::<JsString>(0)?.value(&mut cx);
db.conn.execute(sql, ()).unwrap();
Ok(cx.undefined())
}

fn js_prepare(mut cx: FunctionContext) -> JsResult<JsBox<Statement>> {
let db = cx.this().downcast_or_throw::<JsBox<Database>, _>(&mut cx)?;
let sql = cx.argument::<JsString>(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::<JsNumber, _>(cx) {
let v = v.downcast_or_throw::<JsNumber, _>(cx).unwrap();
let v = v.value(cx);
libsql::Value::Integer(v as i64)
} else if v.is_a::<JsString, _>(cx) {
let v = v.downcast_or_throw::<JsString, _>(cx).unwrap();
let v = v.value(cx);
libsql::Value::Text(v)
} else {
todo!("unsupported type");
}
}

impl Statement {
fn js_get(mut cx: FunctionContext) -> JsResult<JsObject> {
let stmt = cx
.this()
.downcast_or_throw::<JsBox<Statement>, _>(&mut cx)?;
let mut params = vec![];
for i in 0..cx.len() {
let v = cx.argument::<JsValue>(i)?;
let v = js_value_to_value(&mut cx, v);
params.push(v);
}
let params = libsql::Params::Positional(params);
let rows = stmt.stmt.execute(&params).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(())
}
23 changes: 23 additions & 0 deletions crates/core/src/params.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::raw;

pub enum Params {
None,
Positional(Vec<Value>),
Expand Down Expand Up @@ -44,3 +46,24 @@ impl From<&str> for Value {
Value::Text(value.to_owned())
}
}

impl From<raw::Value> 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!(),
}
}
}
18 changes: 18 additions & 0 deletions crates/core/src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Statement> {
Expand All @@ -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) }
}
Expand Down
34 changes: 30 additions & 4 deletions crates/core/src/rows.rs
Original file line number Diff line number Diff line change
@@ -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<raw::Statement>,
pub(crate) stmt: Arc<raw::Statement>,
pub(crate) err: RefCell<Option<i32>>,
}

Expand All @@ -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 {
Expand Down Expand Up @@ -59,7 +63,7 @@ impl futures::Future for RowsFuture {
}

pub struct Row {
pub(crate) stmt: Rc<raw::Statement>,
pub(crate) stmt: Arc<raw::Statement>,
}

impl Row {
Expand All @@ -71,6 +75,11 @@ impl Row {
T::from_sql(val)
}

pub fn get_value(&self, idx: i32) -> Result<Value> {
let val = self.stmt.column_value(idx);
Ok(val.into())
}

pub fn column_type(&self, idx: i32) -> Result<ValueType> {
let val = self.stmt.column_type(idx);
match val as u32 {
Expand All @@ -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 {
Expand All @@ -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<Self>
where
Expand Down
Loading

0 comments on commit f820c2c

Please sign in to comment.