Merge 'bindings/javascript: Improve error handling compatibility with better-sqlite3' from Mikaël Francoeur

This PR brings the error handling of the js bindings one step closer to
better-sqlite3. There is still some work left for the error handling to
be 100% compatible.
This is my first non-trivial Rust PR, so if you have any comments that
can help me improve, please leave them on the PR.
-----
as part of https://github.com/tursodatabase/turso/issues/1900

Reviewed-by: Diego Reis (@el-yawd)

Closes #2009
This commit is contained in:
Pekka Enberg
2025-07-09 12:16:28 +03:00
8 changed files with 313 additions and 221 deletions

View File

@@ -13,7 +13,7 @@ crate-type = ["cdylib"]
[dependencies]
turso_core = { workspace = true }
napi = { version = "2.16.17", default-features = false, features = ["napi4"] }
napi-derive = { version = "2.16.13", default-features = false }
napi-derive = { version = "2.16.13", default-features = true }
[build-dependencies]
napi-build = "2.2.0"

View File

@@ -32,7 +32,7 @@ const genDatabaseFilename = () => {
return `test-${crypto.randomBytes(8).toString('hex')}.db`;
};
new DualTest().onlySqlitePasses("opening a read-only database fails if the file doesn't exist", async (t) => {
new DualTest().both("opening a read-only database fails if the file doesn't exist", async (t) => {
t.throws(() => t.context.connect(genDatabaseFilename(), { readonly: true }),
{
any: true,
@@ -104,7 +104,21 @@ inMemoryTest.both("Empty prepared statement should throw", async (t) => {
() => {
db.prepare("");
},
{ instanceOf: Error },
{ any: true }
);
});
inMemoryTest.onlySqlitePasses("Empty prepared statement should throw the correct error", async (t) => {
// the previous test can be removed once this one passes in Turso
const db = t.context.db;
t.throws(
() => {
db.prepare("");
},
{
instanceOf: RangeError,
message: "The supplied SQL string contains no statements",
},
);
});
@@ -156,9 +170,12 @@ inMemoryTest.both("Statement shouldn't bind twice with bind()", async (t) => {
t.throws(
() => {
db.bind("Bob");
stmt.bind("Bob");
},
{
instanceOf: TypeError,
message: 'The bind() method can only be invoked once per statement object',
},
{ instanceOf: Error },
);
});
@@ -372,3 +389,4 @@ inMemoryTest.both("Test Statement.source", async t => {
t.is(stmt.source, sql);
});

View File

@@ -377,7 +377,7 @@ dualTest.both("Database.pragma()", async (t) => {
t.deepEqual(db.pragma("cache_size"), [{ "cache_size": 2000 }]);
});
dualTest.onlySqlitePasses("errors", async (t) => {
dualTest.both("errors", async (t) => {
const db = t.context.db;
const syntaxError = await t.throws(() => {
@@ -385,7 +385,7 @@ dualTest.onlySqlitePasses("errors", async (t) => {
}, {
any: true,
instanceOf: t.context.errorType,
message: 'near "SYNTAX": syntax error',
message: /near "SYNTAX": syntax error/,
code: 'SQLITE_ERROR'
});
const noTableError = await t.throws(() => {
@@ -393,7 +393,7 @@ dualTest.onlySqlitePasses("errors", async (t) => {
}, {
any: true,
instanceOf: t.context.errorType,
message: "no such table: missing_table",
message: /(Parse error: Table missing_table not found|no such table: missing_table)/,
code: 'SQLITE_ERROR'
});

View File

@@ -3,41 +3,41 @@
/* auto-generated by NAPI-RS */
export interface Options {
readonly: boolean
fileMustExist: boolean
timeout: number
export interface OpenDatabaseOptions {
readonly?: boolean
fileMustExist?: boolean
timeout?: number
}
export interface PragmaOptions {
simple: boolean
}
export declare class Database {
memory: boolean
readonly: boolean
inTransaction: boolean
open: boolean
name: string
constructor(path: string, options?: Options | undefined | null)
constructor(path: string, options?: OpenDatabaseOptions | undefined | null)
prepare(sql: string): Statement
transaction(): void
pragma(): void
pragma(pragmaName: string, options?: PragmaOptions | undefined | null): unknown
backup(): void
serialize(): void
function(): void
aggregate(): void
table(): void
loadExtension(): void
loadExtension(path: string): void
exec(sql: string): void
close(): void
}
export declare class Statement {
database: Database
source: string
reader: boolean
readonly: boolean
busy: boolean
get(): unknown
all(): NapiResult
run(args: Array<unknown>): void
static iterate(): void
static pluck(): void
get(args?: Array<unknown> | undefined | null): unknown
run(args?: Array<unknown> | undefined | null): unknown
iterate(args?: Array<unknown> | undefined | null): IteratorStatement
all(args?: Array<unknown> | undefined | null): unknown
pluck(pluck?: boolean | undefined | null): void
static expand(): void
static raw(): void
raw(raw?: boolean | undefined | null): void
static columns(): void
static bind(): void
bind(args?: Array<unknown> | undefined | null): Statement
}
export declare class IteratorStatement { }

View File

@@ -5,325 +5,313 @@
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require("path");
const { join } = require('path')
const { platform, arch } = process;
const { platform, arch } = process
let nativeBinding = null;
let localFileExisted = false;
let loadError = null;
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== "function") {
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require("child_process")
.execSync("which ldd")
.toString()
.trim();
return readFileSync(lddPath, "utf8").includes("musl");
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true;
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header;
return !glibcVersionRuntime;
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case "android":
case 'android':
switch (arch) {
case "arm64":
localFileExisted = existsSync(
join(__dirname, "turso.android-arm64.node"),
);
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'turso.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require("./turso.android-arm64.node");
nativeBinding = require('./turso.android-arm64.node')
} else {
nativeBinding = require("@tursodatabase/turso-android-arm64");
nativeBinding = require('@tursodatabase/turso-android-arm64')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
case "arm":
localFileExisted = existsSync(
join(__dirname, "turso.android-arm-eabi.node"),
);
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'turso.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require("./turso.android-arm-eabi.node");
nativeBinding = require('./turso.android-arm-eabi.node')
} else {
nativeBinding = require("@tursodatabase/turso-android-arm-eabi");
nativeBinding = require('@tursodatabase/turso-android-arm-eabi')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`);
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break;
case "win32":
break
case 'win32':
switch (arch) {
case "x64":
case 'x64':
localFileExisted = existsSync(
join(__dirname, "turso.win32-x64-msvc.node"),
);
join(__dirname, 'turso.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.win32-x64-msvc.node");
nativeBinding = require('./turso.win32-x64-msvc.node')
} else {
nativeBinding = require("@tursodatabase/turso-win32-x64-msvc");
nativeBinding = require('@tursodatabase/turso-win32-x64-msvc')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
case "ia32":
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, "turso.win32-ia32-msvc.node"),
);
join(__dirname, 'turso.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.win32-ia32-msvc.node");
nativeBinding = require('./turso.win32-ia32-msvc.node')
} else {
nativeBinding = require("@tursodatabase/turso-win32-ia32-msvc");
nativeBinding = require('@tursodatabase/turso-win32-ia32-msvc')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
case "arm64":
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, "turso.win32-arm64-msvc.node"),
);
join(__dirname, 'turso.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.win32-arm64-msvc.node");
nativeBinding = require('./turso.win32-arm64-msvc.node')
} else {
nativeBinding = require("@tursodatabase/turso-win32-arm64-msvc");
nativeBinding = require('@tursodatabase/turso-win32-arm64-msvc')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`);
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break;
case "darwin":
localFileExisted = existsSync(
join(__dirname, "turso.darwin-universal.node"),
);
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'turso.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require("./turso.darwin-universal.node");
nativeBinding = require('./turso.darwin-universal.node')
} else {
nativeBinding = require("@tursodatabase/turso-darwin-universal");
nativeBinding = require('@tursodatabase/turso-darwin-universal')
}
break;
break
} catch {}
switch (arch) {
case "x64":
localFileExisted = existsSync(
join(__dirname, "turso.darwin-x64.node"),
);
case 'x64':
localFileExisted = existsSync(join(__dirname, 'turso.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require("./turso.darwin-x64.node");
nativeBinding = require('./turso.darwin-x64.node')
} else {
nativeBinding = require("@tursodatabase/turso-darwin-x64");
nativeBinding = require('@tursodatabase/turso-darwin-x64')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
case "arm64":
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, "turso.darwin-arm64.node"),
);
join(__dirname, 'turso.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.darwin-arm64.node");
nativeBinding = require('./turso.darwin-arm64.node')
} else {
nativeBinding = require("@tursodatabase/turso-darwin-arm64");
nativeBinding = require('@tursodatabase/turso-darwin-arm64')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`);
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break;
case "freebsd":
if (arch !== "x64") {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`);
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(
join(__dirname, "turso.freebsd-x64.node"),
);
localFileExisted = existsSync(join(__dirname, 'turso.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require("./turso.freebsd-x64.node");
nativeBinding = require('./turso.freebsd-x64.node')
} else {
nativeBinding = require("@tursodatabase/turso-freebsd-x64");
nativeBinding = require('@tursodatabase/turso-freebsd-x64')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
case "linux":
break
case 'linux':
switch (arch) {
case "x64":
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, "turso.linux-x64-musl.node"),
);
join(__dirname, 'turso.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-x64-musl.node");
nativeBinding = require('./turso.linux-x64-musl.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-x64-musl");
nativeBinding = require('@tursodatabase/turso-linux-x64-musl')
}
} catch (e) {
loadError = e;
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, "turso.linux-x64-gnu.node"),
);
join(__dirname, 'turso.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-x64-gnu.node");
nativeBinding = require('./turso.linux-x64-gnu.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-x64-gnu");
nativeBinding = require('@tursodatabase/turso-linux-x64-gnu')
}
} catch (e) {
loadError = e;
loadError = e
}
}
break;
case "arm64":
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, "turso.linux-arm64-musl.node"),
);
join(__dirname, 'turso.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-arm64-musl.node");
nativeBinding = require('./turso.linux-arm64-musl.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-arm64-musl");
nativeBinding = require('@tursodatabase/turso-linux-arm64-musl')
}
} catch (e) {
loadError = e;
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, "turso.linux-arm64-gnu.node"),
);
join(__dirname, 'turso.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-arm64-gnu.node");
nativeBinding = require('./turso.linux-arm64-gnu.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-arm64-gnu");
nativeBinding = require('@tursodatabase/turso-linux-arm64-gnu')
}
} catch (e) {
loadError = e;
loadError = e
}
}
break;
case "arm":
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, "turso.linux-arm-musleabihf.node"),
);
join(__dirname, 'turso.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-arm-musleabihf.node");
nativeBinding = require('./turso.linux-arm-musleabihf.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-arm-musleabihf");
nativeBinding = require('@tursodatabase/turso-linux-arm-musleabihf')
}
} catch (e) {
loadError = e;
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, "turso.linux-arm-gnueabihf.node"),
);
join(__dirname, 'turso.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-arm-gnueabihf.node");
nativeBinding = require('./turso.linux-arm-gnueabihf.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-arm-gnueabihf");
nativeBinding = require('@tursodatabase/turso-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e;
loadError = e
}
}
break;
case "riscv64":
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, "turso.linux-riscv64-musl.node"),
);
join(__dirname, 'turso.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-riscv64-musl.node");
nativeBinding = require('./turso.linux-riscv64-musl.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-riscv64-musl");
nativeBinding = require('@tursodatabase/turso-linux-riscv64-musl')
}
} catch (e) {
loadError = e;
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, "turso.linux-riscv64-gnu.node"),
);
join(__dirname, 'turso.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-riscv64-gnu.node");
nativeBinding = require('./turso.linux-riscv64-gnu.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-riscv64-gnu");
nativeBinding = require('@tursodatabase/turso-linux-riscv64-gnu')
}
} catch (e) {
loadError = e;
loadError = e
}
}
break;
case "s390x":
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, "turso.linux-s390x-gnu.node"),
);
join(__dirname, 'turso.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require("./turso.linux-s390x-gnu.node");
nativeBinding = require('./turso.linux-s390x-gnu.node')
} else {
nativeBinding = require("@tursodatabase/turso-linux-s390x-gnu");
nativeBinding = require('@tursodatabase/turso-linux-s390x-gnu')
}
} catch (e) {
loadError = e;
loadError = e
}
break;
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`);
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break;
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError;
throw loadError
}
throw new Error(`Failed to load native binding`);
throw new Error(`Failed to load native binding`)
}
const { Database, Statement } = nativeBinding;
const { Database, Statement, IteratorStatement } = nativeBinding
module.exports.Database = Database;
module.exports.Statement = Statement;
module.exports.Database = Database
module.exports.Statement = Statement
module.exports.IteratorStatement = IteratorStatement

View File

@@ -0,0 +1,22 @@
'use strict';
const descriptor = { value: 'SqliteError', writable: true, enumerable: false, configurable: true };
function SqliteError(message, code, rawCode) {
if (new.target !== SqliteError) {
return new SqliteError(message, code);
}
if (typeof code !== 'string') {
throw new TypeError('Expected second argument to be a string');
}
Error.call(this, message);
descriptor.value = '' + message;
Object.defineProperty(this, 'message', descriptor);
Error.captureStackTrace(this, SqliteError);
this.code = code;
this.rawCode = rawCode
}
Object.setPrototypeOf(SqliteError, Error);
Object.setPrototypeOf(SqliteError.prototype, Error.prototype);
Object.defineProperty(SqliteError.prototype, 'name', descriptor);
module.exports = SqliteError;

View File

@@ -14,12 +14,18 @@ use turso_core::{LimboError, StepResult};
#[derive(Default)]
#[napi(object)]
pub struct OpenDatabaseOptions {
pub readonly: bool,
pub file_must_exist: bool,
pub timeout: u32,
pub readonly: Option<bool>,
pub file_must_exist: Option<bool>,
pub timeout: Option<u32>,
// verbose => Callback,
}
impl OpenDatabaseOptions {
fn readonly(&self) -> bool {
self.readonly.unwrap_or(false)
}
}
#[napi(object)]
pub struct PragmaOptions {
pub simple: bool,
@@ -55,28 +61,30 @@ impl ObjectFinalize for Database {
#[napi]
impl Database {
#[napi(constructor)]
pub fn new(path: String, options: Option<OpenDatabaseOptions>) -> napi::Result<Self> {
pub fn new(path: String, options: Option<OpenDatabaseOptions>) -> napi::Result<Self, String> {
let memory = path == ":memory:";
let io: Arc<dyn turso_core::IO> = if memory {
Arc::new(turso_core::MemoryIO::new())
} else {
Arc::new(turso_core::PlatformIO::new().map_err(into_napi_error)?)
Arc::new(turso_core::PlatformIO::new().map_err(into_napi_sqlite_error)?)
};
let opts = options.unwrap_or_default();
let flag = if opts.readonly {
let flag = if opts.readonly() {
turso_core::OpenFlags::ReadOnly
} else {
turso_core::OpenFlags::Create
};
let file = io.open_file(&path, flag, false).map_err(into_napi_error)?;
let file = io
.open_file(&path, flag, false)
.map_err(|err| into_napi_error_with_message("SQLITE_CANTOPEN".to_owned(), err))?;
let db_file = Arc::new(DatabaseFile::new(file));
let db = turso_core::Database::open(io.clone(), &path, db_file, false, false)
.map_err(into_napi_error)?;
let conn = db.connect().map_err(into_napi_error)?;
.map_err(into_napi_sqlite_error)?;
let conn = db.connect().map_err(into_napi_sqlite_error)?;
Ok(Self {
readonly: opts.readonly,
readonly: opts.readonly(),
memory,
_db: db,
conn,
@@ -131,16 +139,6 @@ impl Database {
}
}
#[napi]
pub fn readonly(&self) -> bool {
self.readonly
}
#[napi]
pub fn open(&self) -> bool {
self.open
}
#[napi]
pub fn backup(&self) {
todo!()
@@ -176,7 +174,7 @@ impl Database {
}
#[napi]
pub fn exec(&self, sql: String) -> napi::Result<()> {
pub fn exec(&self, sql: String) -> napi::Result<(), String> {
let query_runner = self.conn.query_runner(sql.as_bytes());
// Since exec doesn't return any values, we can just iterate over the results
@@ -185,17 +183,17 @@ impl Database {
Ok(Some(mut stmt)) => loop {
match stmt.step() {
Ok(StepResult::Row) => continue,
Ok(StepResult::IO) => stmt.run_once().map_err(into_napi_error)?,
Ok(StepResult::IO) => stmt.run_once().map_err(into_napi_sqlite_error)?,
Ok(StepResult::Done) => break,
Ok(StepResult::Interrupt | StepResult::Busy) => {
return Err(napi::Error::new(
napi::Status::GenericFailure,
"SQLITE_ERROR".to_owned(),
"Statement execution interrupted or busy".to_string(),
));
}
Err(err) => {
return Err(napi::Error::new(
napi::Status::GenericFailure,
"SQLITE_ERROR".to_owned(),
format!("Error executing SQL: {}", err),
));
}
@@ -204,7 +202,7 @@ impl Database {
Ok(None) => continue,
Err(err) => {
return Err(napi::Error::new(
napi::Status::GenericFailure,
"SQLITE_ERROR".to_owned(),
format!("Error executing SQL: {}", err),
));
}
@@ -263,7 +261,7 @@ impl Statement {
#[napi]
pub fn get(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let mut stmt = self.check_and_bind(args)?;
let mut stmt = self.check_and_bind(env, args)?;
loop {
let step = stmt.step().map_err(into_napi_error)?;
@@ -324,7 +322,7 @@ impl Statement {
// TODO: Return Info object (https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#runbindparameters---object)
#[napi]
pub fn run(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let stmt = self.check_and_bind(args)?;
let stmt = self.check_and_bind(env, args)?;
self.internal_all(env, stmt)
}
@@ -335,7 +333,12 @@ impl Statement {
env: Env,
args: Option<Vec<JsUnknown>>,
) -> napi::Result<IteratorStatement> {
self.check_and_bind(args)?;
if let Some(some_args) = args.as_ref() {
if some_args.iter().len() != 0 {
self.check_and_bind(env, args)?;
}
}
Ok(IteratorStatement {
stmt: Rc::clone(&self.inner),
_database: self.database.clone(),
@@ -346,7 +349,7 @@ impl Statement {
#[napi]
pub fn all(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let stmt = self.check_and_bind(args)?;
let stmt = self.check_and_bind(env, args)?;
self.internal_all(env, stmt)
}
@@ -444,8 +447,9 @@ impl Statement {
}
#[napi]
pub fn bind(&mut self, args: Option<Vec<JsUnknown>>) -> napi::Result<Self> {
self.check_and_bind(args)?;
pub fn bind(&mut self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<Self, String> {
self.check_and_bind(env, args)
.map_err(with_sqlite_error_message)?;
self.binded = true;
Ok(self.clone())
@@ -455,16 +459,22 @@ impl Statement {
/// and bind values do variables. The expected type for args is `Option<Vec<JsUnknown>>`
fn check_and_bind(
&self,
env: Env,
args: Option<Vec<JsUnknown>>,
) -> napi::Result<RefMut<'_, turso_core::Statement>> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
if self.binded {
return Err(napi::Error::new(
napi::Status::InvalidArg,
"This statement already has bound parameters",
));
let err = napi::Error::new(
into_convertible_type_error_message("TypeError"),
"The bind() method can only be invoked once per statement object",
);
unsafe {
napi::JsTypeError::from(err).throw_into(env.raw());
}
return Err(napi::Error::from_status(napi::Status::PendingException));
}
for (i, elem) in args.into_iter().enumerate() {
@@ -630,6 +640,29 @@ impl turso_core::DatabaseStorage for DatabaseFile {
}
#[inline]
pub fn into_napi_error(limbo_error: LimboError) -> napi::Error {
fn into_napi_error(limbo_error: LimboError) -> napi::Error {
napi::Error::new(napi::Status::GenericFailure, format!("{limbo_error}"))
}
#[inline]
fn into_napi_sqlite_error(limbo_error: LimboError) -> napi::Error<String> {
napi::Error::new(String::from("SQLITE_ERROR"), format!("{limbo_error}"))
}
#[inline]
fn into_napi_error_with_message(
error_code: String,
limbo_error: LimboError,
) -> napi::Error<String> {
napi::Error::new(error_code, format!("{limbo_error}"))
}
#[inline]
fn with_sqlite_error_message(err: napi::Error) -> napi::Error<String> {
napi::Error::new("SQLITE_ERROR".to_owned(), err.reason)
}
#[inline]
fn into_convertible_type_error_message(error_type: &str) -> String {
"[TURSO_CONVERT_TYPE]".to_owned() + error_type
}

View File

@@ -2,6 +2,28 @@
const { Database: NativeDB } = require("./index.js");
const SqliteError = require("./sqlite-error.js");
const convertibleErrorTypes = { TypeError };
const CONVERTIBLE_ERROR_PREFIX = '[TURSO_CONVERT_TYPE]';
function convertError(err) {
if ((err.code ?? '').startsWith(CONVERTIBLE_ERROR_PREFIX)) {
return createErrorByName(err.code.substring(CONVERTIBLE_ERROR_PREFIX.length), err.message);
}
return new SqliteError(err.message, err.code, err.rawCode);
}
function createErrorByName(name, message) {
const ErrorConstructor = convertibleErrorTypes[name];
if (!ErrorConstructor) {
throw new Error(`unknown error type ${name} from Turso`);
}
return new ErrorConstructor(message);
}
/**
* Database represents a connection that can prepare and execute SQL statements.
*/
@@ -145,7 +167,11 @@ class Database {
* @param {string} sql - The SQL statement string to execute.
*/
exec(sql) {
this.db.exec(sql);
try {
this.db.exec(sql);
} catch (err) {
throw convertError(err);
}
}
/**
@@ -264,8 +290,13 @@ class Statement {
* @returns this - Statement with binded parameters
*/
bind(...bindParameters) {
return this.stmt.bind(bindParameters.flat());
try {
return new Statement(this.stmt.bind(bindParameters.flat()), this.db);
} catch (err) {
throw convertError(err);
}
}
}
module.exports = Database;
module.exports.SqliteError = SqliteError;