diff --git a/Cargo.lock b/Cargo.lock index 7a6108b2e7..7ffb99d2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,13 +720,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.106" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -2065,6 +2065,7 @@ dependencies = [ "k256", "lazy-regex", "libc", + "libsqlite3-sys", "libz-sys", "md-5", "md4", @@ -2086,6 +2087,7 @@ dependencies = [ "ring", "ripemd", "rsa", + "rusqlite", "scrypt", "sec1", "serde", @@ -4841,9 +4843,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b694a822684ddb75df4d657029161431bcb4a85c1856952f845b76912bc6fec" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", diff --git a/cli/bench/sqlite.js b/cli/bench/sqlite.js new file mode 100644 index 0000000000..c87dfe47e8 --- /dev/null +++ b/cli/bench/sqlite.js @@ -0,0 +1,36 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. 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); +} diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index d37bbb620e..a16de91ede 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -59,6 +59,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" @@ -80,6 +81,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" diff --git a/ext/node/lib.rs b/ext/node/lib.rs index a0efc12657..b7898039c3 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -437,7 +437,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 = [ @@ -661,6 +663,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", diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index 0b7be91860..8dae6aeff6 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -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; diff --git a/ext/node/ops/sqlite/database.rs b/ext/node/ops/sqlite/database.rs new file mode 100644 index 0000000000..c39aebcff6 --- /dev/null +++ b/ext/node/ops/sqlite/database.rs @@ -0,0 +1,162 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. 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>>, + 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, + ) -> Result { + 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 { + 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), + }) + } +} diff --git a/ext/node/ops/sqlite/mod.rs b/ext/node/ops/sqlite/mod.rs new file mode 100644 index 0000000000..fd786010ca --- /dev/null +++ b/ext/node/ops/sqlite/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +mod database; +mod statement; + +pub use database::DatabaseSync; +pub use statement::StatementSync; + +#[derive(Debug, thiserror::Error)] +pub enum SqliteError { + #[error(transparent)] + SqliteError(#[from] rusqlite::Error), + #[error("Database is already in use")] + InUse, + #[error("Failed to step statement")] + FailedStep, + #[error("Failed to bind parameter. {0}")] + FailedBind(&'static str), + #[error("Unknown column type")] + UnknownColumnType, + #[error("Failed to get SQL")] + GetSqlFailed, + #[error("Database is already closed")] + AlreadyClosed, + #[error("Database is already open")] + AlreadyOpen, + #[error("Failed to prepare statement")] + PrepareFailed, + #[error("Invalid constructor")] + InvalidConstructor, + #[error(transparent)] + Other(deno_core::error::AnyError), +} diff --git a/ext/node/ops/sqlite/statement.rs b/ext/node/ops/sqlite/statement.rs new file mode 100644 index 0000000000..4f93114e4e --- /dev/null +++ b/ext/node/ops/sqlite/statement.rs @@ -0,0 +1,357 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::cell::Cell; +use std::cell::RefCell; +use std::rc::Rc; + +use deno_core::op2; +use deno_core::v8; +use deno_core::GarbageCollected; +use libsqlite3_sys as ffi; +use serde::Serialize; + +use super::SqliteError; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RunStatementResult { + last_insert_rowid: i64, + changes: u64, +} + +pub struct StatementSync { + pub inner: *mut ffi::sqlite3_stmt, + pub db: Rc>>, + + pub use_big_ints: Cell, +} + +impl Drop for StatementSync { + fn drop(&mut self) { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // no other references to this pointer exist. + unsafe { + ffi::sqlite3_finalize(self.inner); + } + } +} + +struct ColumnIterator<'a> { + stmt: &'a StatementSync, + index: i32, + count: i32, +} + +impl<'a> ColumnIterator<'a> { + fn new(stmt: &'a StatementSync) -> Self { + let count = stmt.column_count(); + ColumnIterator { + stmt, + index: 0, + count, + } + } + + fn column_count(&self) -> usize { + self.count as usize + } +} + +impl<'a> Iterator for ColumnIterator<'a> { + type Item = (i32, &'a [u8]); + + fn next(&mut self) -> Option { + if self.index >= self.count { + return None; + } + + let index = self.index; + let name = self.stmt.column_name(self.index); + + self.index += 1; + Some((index, name)) + } +} + +impl GarbageCollected for StatementSync {} + +impl StatementSync { + // Clear the prepared statement back to its initial state. + fn reset(&self) { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + ffi::sqlite3_reset(self.inner); + } + } + + // Evaluate the prepared statement. + fn step(&self) -> Result { + let raw = self.inner; + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + let r = ffi::sqlite3_step(raw); + if r == ffi::SQLITE_DONE { + return Ok(true); + } + if r != ffi::SQLITE_ROW { + return Err(SqliteError::FailedStep); + } + } + + Ok(false) + } + + fn column_count(&self) -> i32 { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { ffi::sqlite3_column_count(self.inner) } + } + + fn column_name(&self, index: i32) -> &[u8] { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + let name = ffi::sqlite3_column_name(self.inner, index); + std::ffi::CStr::from_ptr(name as _).to_bytes() + } + } + + fn column_value<'a>( + &self, + index: i32, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Value> { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + match ffi::sqlite3_column_type(self.inner, index) { + ffi::SQLITE_INTEGER => { + let value = ffi::sqlite3_column_int64(self.inner, index); + if self.use_big_ints.get() { + v8::BigInt::new_from_i64(scope, value).into() + } else { + v8::Integer::new(scope, value as _).into() + } + } + ffi::SQLITE_FLOAT => { + let value = ffi::sqlite3_column_double(self.inner, index); + v8::Number::new(scope, value).into() + } + ffi::SQLITE_TEXT => { + let value = ffi::sqlite3_column_text(self.inner, index); + let value = std::ffi::CStr::from_ptr(value as _); + v8::String::new_from_utf8( + scope, + value.to_bytes(), + v8::NewStringType::Normal, + ) + .unwrap() + .into() + } + ffi::SQLITE_BLOB => { + let value = ffi::sqlite3_column_blob(self.inner, index); + let size = ffi::sqlite3_column_bytes(self.inner, index); + let value = + std::slice::from_raw_parts(value as *const u8, size as usize); + let value = + v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec()) + .make_shared(); + v8::ArrayBuffer::with_backing_store(scope, &value).into() + } + ffi::SQLITE_NULL => v8::null(scope).into(), + _ => v8::undefined(scope).into(), + } + } + } + + // Read the current row of the prepared statement. + fn read_row<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> Result>, SqliteError> { + if self.step()? { + return Ok(None); + } + + let iter = ColumnIterator::new(self); + + let num_cols = iter.column_count(); + + let mut names = Vec::with_capacity(num_cols); + let mut values = Vec::with_capacity(num_cols); + + for (index, name) in iter { + let value = self.column_value(index, scope); + let name = + v8::String::new_from_utf8(scope, name, v8::NewStringType::Normal) + .unwrap() + .into(); + + names.push(name); + values.push(value); + } + + let null = v8::null(scope).into(); + let result = + v8::Object::with_prototype_and_properties(scope, null, &names, &values); + + Ok(Some(result)) + } + + // Bind the parameters to the prepared statement. + fn bind_params( + &self, + scope: &mut v8::HandleScope, + params: Option<&v8::FunctionCallbackArguments>, + ) -> Result<(), SqliteError> { + let raw = self.inner; + + if let Some(params) = params { + let len = params.length(); + for i in 0..len { + let value = params.get(i); + + if value.is_number() { + let value = value.number_value(scope).unwrap(); + + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + ffi::sqlite3_bind_double(raw, i + 1, value); + } + } else if value.is_string() { + let value = value.to_rust_string_lossy(scope); + + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + // + // SQLITE_TRANSIENT is used to indicate that SQLite should make a copy of the data. + unsafe { + ffi::sqlite3_bind_text( + raw, + i + 1, + value.as_ptr() as *const _, + value.len() as i32, + ffi::SQLITE_TRANSIENT(), + ); + } + } else if value.is_null() { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + unsafe { + ffi::sqlite3_bind_null(raw, i + 1); + } + } else if value.is_array_buffer_view() { + let value: v8::Local = value.try_into().unwrap(); + let data = value.data(); + let size = value.byte_length(); + + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // as it lives as long as the StatementSync instance. + // + // SQLITE_TRANSIENT is used to indicate that SQLite should make a copy of the data. + unsafe { + ffi::sqlite3_bind_blob( + raw, + i + 1, + data, + size as i32, + ffi::SQLITE_TRANSIENT(), + ); + } + } else { + return Err(SqliteError::FailedBind("Unsupported type")); + } + } + } + + Ok(()) + } +} + +// Represents a single prepared statement. Cannot be initialized directly via constructor. +// Instances are created using `DatabaseSync#prepare`. +// +// A prepared statement is an efficient binary representation of the SQL used to create it. +#[op2] +impl StatementSync { + #[constructor] + #[cppgc] + fn new(_: bool) -> Result { + Err(SqliteError::InvalidConstructor) + } + + // Executes a prepared statement and returns the first result as an object. + // + // The prepared statement does not return any results, this method returns undefined. + // Optionally, parameters can be bound to the prepared statement. + fn get<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[varargs] params: Option<&v8::FunctionCallbackArguments>, + ) -> Result, SqliteError> { + self.reset(); + + self.bind_params(scope, params)?; + + let entry = self.read_row(scope)?; + let result = entry + .map(|r| r.into()) + .unwrap_or_else(|| v8::undefined(scope).into()); + + Ok(result) + } + + // Executes a prepared statement and returns an object summarizing the resulting + // changes. + // + // Optionally, parameters can be bound to the prepared statement. + #[serde] + fn run( + &self, + scope: &mut v8::HandleScope, + #[varargs] params: Option<&v8::FunctionCallbackArguments>, + ) -> Result { + let db = self.db.borrow(); + let db = db.as_ref().ok_or(SqliteError::InUse)?; + + self.bind_params(scope, params)?; + self.step()?; + + self.reset(); + + Ok(RunStatementResult { + last_insert_rowid: db.last_insert_rowid(), + changes: db.changes(), + }) + } + + // Executes a prepared statement and returns all results as an array of objects. + // + // If the prepared statement does not return any results, this method returns an empty array. + // Optionally, parameters can be bound to the prepared statement. + fn all<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[varargs] params: Option<&v8::FunctionCallbackArguments>, + ) -> Result, SqliteError> { + let mut arr = vec![]; + + self.bind_params(scope, params)?; + while let Some(result) = self.read_row(scope)? { + arr.push(result.into()); + } + + self.reset(); + + let arr = v8::Array::new_with_elements(scope, &arr); + Ok(arr) + } + + #[fast] + fn set_read_big_ints(&self, enabled: bool) { + self.use_big_ints.set(enabled); + } +} diff --git a/ext/node/polyfill.rs b/ext/node/polyfill.rs index 6e6e9d09c2..295053b434 100644 --- a/ext/node/polyfill.rs +++ b/ext/node/polyfill.rs @@ -71,6 +71,7 @@ generate_builtin_node_module_lists! { "readline", "readline/promises", "repl", + "sqlite", "stream", "stream/consumers", "stream/promises", diff --git a/ext/node/polyfills/01_require.js b/ext/node/polyfills/01_require.js index 3e90750cd3..3353da7ef7 100644 --- a/ext/node/polyfills/01_require.js +++ b/ext/node/polyfills/01_require.js @@ -142,6 +142,7 @@ import querystring from "node:querystring"; import readline from "node:readline"; import readlinePromises from "node:readline/promises"; import repl from "node:repl"; +import sqlite from "node:sqlite"; import stream from "node:stream"; import streamConsumers from "node:stream/consumers"; import streamPromises from "node:stream/promises"; @@ -253,6 +254,7 @@ function setupBuiltinModules() { readline, "readline/promises": readlinePromises, repl, + sqlite, stream, "stream/consumers": streamConsumers, "stream/promises": streamPromises, diff --git a/ext/node/polyfills/sqlite.ts b/ext/node/polyfills/sqlite.ts new file mode 100644 index 0000000000..229c463a22 --- /dev/null +++ b/ext/node/polyfills/sqlite.ts @@ -0,0 +1,9 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { DatabaseSync } from "ext:core/ops"; + +export { DatabaseSync }; + +export default { + DatabaseSync, +}; diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 196184f3e0..0e43c7524d 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -8725,6 +8725,7 @@ fn lsp_completions_node_specifier() { "node:readline", "node:readline/promises", "node:repl", + "node:sqlite", "node:stream", "node:stream/consumers", "node:stream/promises", diff --git a/tests/integration/node_unit_tests.rs b/tests/integration/node_unit_tests.rs index ef76e365a4..ed314e886e 100644 --- a/tests/integration/node_unit_tests.rs +++ b/tests/integration/node_unit_tests.rs @@ -90,6 +90,7 @@ util::unit_test_factory!( querystring_test, readline_test, repl_test, + sqlite_test, stream_test, string_decoder_test, timers_test, diff --git a/tests/unit_node/sqlite_test.ts b/tests/unit_node/sqlite_test.ts new file mode 100644 index 0000000000..a3b93e66ed --- /dev/null +++ b/tests/unit_node/sqlite_test.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { DatabaseSync } from "node:sqlite"; +import { assertEquals, assertThrows } from "@std/assert"; + +Deno.test("[node/sqlite] in-memory databases", () => { + const db1 = new DatabaseSync(":memory:"); + const db2 = new DatabaseSync(":memory:"); + db1.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);"); + db1.exec("INSERT INTO data (key) VALUES (1);"); + + db2.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);"); + db2.exec("INSERT INTO data (key) VALUES (1);"); + + assertEquals(db1.prepare("SELECT * FROM data").all(), [{ + key: 1, + __proto__: null, + }]); + assertEquals(db2.prepare("SELECT * FROM data").all(), [{ + key: 1, + __proto__: null, + }]); +}); + +Deno.test("[node/sqlite] Errors originating from SQLite should be thrown", () => { + const db = new DatabaseSync(":memory:"); + db.exec(` + CREATE TABLE test( + key INTEGER PRIMARY KEY + ) STRICT; + `); + const stmt = db.prepare("INSERT INTO test(key) VALUES(?)"); + assertEquals(stmt.run(1), { lastInsertRowid: 1, changes: 1 }); + + assertThrows(() => stmt.run(1), Error); +}); + +Deno.test( + { + permissions: { read: true, write: true }, + name: "[node/sqlite] PRAGMAs are supported", + }, + () => { + const tempDir = Deno.makeTempDirSync(); + const db = new DatabaseSync(`${tempDir}/test.db`); + + assertEquals(db.prepare("PRAGMA journal_mode = WAL").get(), { + journal_mode: "wal", + __proto__: null, + }); + + db.close(); + }, +); + +Deno.test("[node/sqlite] StatementSync read bigints are supported", () => { + const db = new DatabaseSync(":memory:"); + db.exec("CREATE TABLE data(key INTEGER PRIMARY KEY);"); + db.exec("INSERT INTO data (key) VALUES (1);"); + + const stmt = db.prepare("SELECT * FROM data"); + assertEquals(stmt.get(), { key: 1, __proto__: null }); + + stmt.setReadBigInts(true); + assertEquals(stmt.get(), { key: 1n, __proto__: null }); +});