Skip to content

Commit

Permalink
feat(ext/node): implement node:sqlite (#27308)
Browse files Browse the repository at this point in the history
  • Loading branch information
littledivy authored Jan 28, 2025
1 parent 5c64146 commit aeac5a6
Show file tree
Hide file tree
Showing 14 changed files with 689 additions and 6 deletions.
12 changes: 7 additions & 5 deletions Cargo.lock

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

36 changes: 36 additions & 0 deletions cli/bench/sqlite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// deno-lint-ignore-file no-console

import { DatabaseSync } from "node:sqlite";
import fs from "node:fs";

function bench(name, fun, count = 10000) {
const start = Date.now();
for (let i = 0; i < count; i++) fun();
const elapsed = Date.now() - start;
const rate = Math.floor(count / (elapsed / 1000));
console.log(` ${name}: time ${elapsed} ms rate ${rate}`);
}

for (const name of [":memory:", "test.db"]) {
console.log(`Benchmarking ${name}`);
try {
fs.unlinkSync(name);
} catch {
// Ignore
}

const db = new DatabaseSync(name);
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");

bench("prepare", () => db.prepare("SELECT * FROM test"));
bench("exec", () => db.exec("INSERT INTO test (name) VALUES ('foo')"));

const stmt = db.prepare("SELECT * FROM test");
bench("get", () => stmt.get());

const stmt2 = db.prepare("SELECT * FROM test WHERE id = ?");
bench("get (integer bind)", () => stmt2.get(1));

bench("all", () => stmt.all(), 1000);
}
2 changes: 2 additions & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ipnetwork = "0.20.0"
k256 = "0.13.1"
lazy-regex.workspace = true
libc.workspace = true
libsqlite3-sys = "0.30.1"
libz-sys.workspace = true
md-5 = { version = "0.10.5", features = ["oid"] }
md4 = "0.10.2"
Expand All @@ -81,6 +82,7 @@ regex.workspace = true
ring.workspace = true
ripemd = { version = "0.1.3", features = ["oid"] }
rsa.workspace = true
rusqlite.workspace = true
scrypt = "0.11.0"
sec1.workspace = true
serde = "1.0.149"
Expand Down
5 changes: 4 additions & 1 deletion ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,9 @@ deno_core::extension!(deno_node,
ops::inspector::op_inspector_enabled,
],
objects = [
ops::perf_hooks::EldHistogram
ops::perf_hooks::EldHistogram,
ops::sqlite::DatabaseSync,
ops::sqlite::StatementSync
],
esm_entry_point = "ext:deno_node/02_init.js",
esm = [
Expand Down Expand Up @@ -663,6 +665,7 @@ deno_core::extension!(deno_node,
"node:readline" = "readline.ts",
"node:readline/promises" = "readline/promises.ts",
"node:repl" = "repl.ts",
"node:sqlite" = "sqlite.ts",
"node:stream" = "stream.ts",
"node:stream/consumers" = "stream/consumers.mjs",
"node:stream/promises" = "stream/promises.mjs",
Expand Down
1 change: 1 addition & 0 deletions ext/node/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod os;
pub mod perf_hooks;
pub mod process;
pub mod require;
pub mod sqlite;
pub mod tls;
pub mod util;
pub mod v8;
Expand Down
162 changes: 162 additions & 0 deletions ext/node/ops/sqlite/database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2018-2025 the Deno authors. MIT license.

use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;

use deno_core::op2;
use deno_core::GarbageCollected;
use serde::Deserialize;

use super::SqliteError;
use super::StatementSync;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DatabaseSyncOptions {
#[serde(default = "true_fn")]
open: bool,
#[serde(default = "true_fn")]
enable_foreign_key_constraints: bool,
}

fn true_fn() -> bool {
true
}

impl Default for DatabaseSyncOptions {
fn default() -> Self {
DatabaseSyncOptions {
open: true,
enable_foreign_key_constraints: true,
}
}
}

pub struct DatabaseSync {
conn: Rc<RefCell<Option<rusqlite::Connection>>>,
options: DatabaseSyncOptions,
location: String,
}

impl GarbageCollected for DatabaseSync {}

// Represents a single connection to a SQLite database.
#[op2]
impl DatabaseSync {
// Constructs a new `DatabaseSync` instance.
//
// A SQLite database can be stored in a file or in memory. To
// use a file-backed database, the `location` should be a path.
// To use an in-memory database, the `location` should be special
// name ":memory:".
#[constructor]
#[cppgc]
fn new(
#[string] location: String,
#[serde] options: Option<DatabaseSyncOptions>,
) -> Result<DatabaseSync, SqliteError> {
let options = options.unwrap_or_default();

let db = if options.open {
let db = rusqlite::Connection::open(&location)?;
if options.enable_foreign_key_constraints {
db.execute("PRAGMA foreign_keys = ON", [])?;
}
Some(db)
} else {
None
};

Ok(DatabaseSync {
conn: Rc::new(RefCell::new(db)),
location,
options,
})
}

// Opens the database specified by `location` of this instance.
//
// This method should only be used when the database is not opened
// via the constructor. An exception is thrown if the database is
// already opened.
#[fast]
fn open(&self) -> Result<(), SqliteError> {
if self.conn.borrow().is_some() {
return Err(SqliteError::AlreadyOpen);
}

let db = rusqlite::Connection::open(&self.location)?;
if self.options.enable_foreign_key_constraints {
db.execute("PRAGMA foreign_keys = ON", [])?;
}

*self.conn.borrow_mut() = Some(db);

Ok(())
}

// Closes the database connection. An exception is thrown if the
// database is not open.
#[fast]
fn close(&self) -> Result<(), SqliteError> {
if self.conn.borrow().is_none() {
return Err(SqliteError::AlreadyClosed);
}

*self.conn.borrow_mut() = None;
Ok(())
}

// This method allows one or more SQL statements to be executed
// without returning any results.
//
// This method is a wrapper around sqlite3_exec().
#[fast]
fn exec(&self, #[string] sql: &str) -> Result<(), SqliteError> {
let db = self.conn.borrow();
let db = db.as_ref().ok_or(SqliteError::InUse)?;

let mut stmt = db.prepare_cached(sql)?;
stmt.raw_execute()?;

Ok(())
}

// Compiles an SQL statement into a prepared statement.
//
// This method is a wrapper around `sqlite3_prepare_v2()`.
#[cppgc]
fn prepare(&self, #[string] sql: &str) -> Result<StatementSync, SqliteError> {
let db = self.conn.borrow();
let db = db.as_ref().ok_or(SqliteError::InUse)?;

// SAFETY: lifetime of the connection is guaranteed by reference
// counting.
let raw_handle = unsafe { db.handle() };

let mut raw_stmt = std::ptr::null_mut();

// SAFETY: `sql` points to a valid memory location and its length
// is correct.
let r = unsafe {
libsqlite3_sys::sqlite3_prepare_v2(
raw_handle,
sql.as_ptr() as *const _,
sql.len() as i32,
&mut raw_stmt,
std::ptr::null_mut(),
)
};

if r != libsqlite3_sys::SQLITE_OK {
return Err(SqliteError::PrepareFailed);
}

Ok(StatementSync {
inner: raw_stmt,
db: self.conn.clone(),
use_big_ints: Cell::new(false),
})
}
}
41 changes: 41 additions & 0 deletions ext/node/ops/sqlite/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2018-2025 the Deno authors. MIT license.

mod database;
mod statement;

pub use database::DatabaseSync;
pub use statement::StatementSync;

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum SqliteError {
#[class(generic)]
#[error(transparent)]
SqliteError(#[from] rusqlite::Error),
#[class(generic)]
#[error("Database is already in use")]
InUse,
#[class(generic)]
#[error("Failed to step statement")]
FailedStep,
#[class(generic)]
#[error("Failed to bind parameter. {0}")]
FailedBind(&'static str),
#[class(generic)]
#[error("Unknown column type")]
UnknownColumnType,
#[class(generic)]
#[error("Failed to get SQL")]
GetSqlFailed,
#[class(generic)]
#[error("Database is already closed")]
AlreadyClosed,
#[class(generic)]
#[error("Database is already open")]
AlreadyOpen,
#[class(generic)]
#[error("Failed to prepare statement")]
PrepareFailed,
#[class(generic)]
#[error("Invalid constructor")]
InvalidConstructor,
}
Loading

0 comments on commit aeac5a6

Please sign in to comment.