diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml index f02a8abe9..7d62ede4c 100644 --- a/bindings/javascript/Cargo.toml +++ b/bindings/javascript/Cargo.toml @@ -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" diff --git a/bindings/javascript/__test__/better-sqlite3.spec.mjs b/bindings/javascript/__test__/better-sqlite3.spec.mjs index a5fdf93fe..992cda585 100644 --- a/bindings/javascript/__test__/better-sqlite3.spec.mjs +++ b/bindings/javascript/__test__/better-sqlite3.spec.mjs @@ -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); }); + diff --git a/bindings/javascript/__test__/sync.spec.mjs b/bindings/javascript/__test__/sync.spec.mjs index f8c016021..87fe1d7d4 100644 --- a/bindings/javascript/__test__/sync.spec.mjs +++ b/bindings/javascript/__test__/sync.spec.mjs @@ -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' }); diff --git a/bindings/javascript/index.d.ts b/bindings/javascript/index.d.ts index 99433b962..37041f67a 100644 --- a/bindings/javascript/index.d.ts +++ b/bindings/javascript/index.d.ts @@ -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): void - static iterate(): void - static pluck(): void + get(args?: Array | undefined | null): unknown + run(args?: Array | undefined | null): unknown + iterate(args?: Array | undefined | null): IteratorStatement + all(args?: Array | 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 | undefined | null): Statement } +export declare class IteratorStatement { } diff --git a/bindings/javascript/index.js b/bindings/javascript/index.js index 4e9bf54a7..c1f087ea5 100644 --- a/bindings/javascript/index.js +++ b/bindings/javascript/index.js @@ -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 diff --git a/bindings/javascript/sqlite-error.js b/bindings/javascript/sqlite-error.js new file mode 100644 index 000000000..82356bc36 --- /dev/null +++ b/bindings/javascript/sqlite-error.js @@ -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; + diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index de842bd85..15c32940f 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -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, + pub file_must_exist: Option, + pub timeout: Option, // 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) -> napi::Result { + pub fn new(path: String, options: Option) -> napi::Result { let memory = path == ":memory:"; let io: Arc = 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>) -> napi::Result { - 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>) -> napi::Result { - 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>, ) -> napi::Result { - 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>) -> napi::Result { - 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>) -> napi::Result { - self.check_and_bind(args)?; + pub fn bind(&mut self, env: Env, args: Option>) -> napi::Result { + 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>` fn check_and_bind( &self, + env: Env, args: Option>, ) -> napi::Result> { 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 { + 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 { + napi::Error::new(error_code, format!("{limbo_error}")) +} + +#[inline] +fn with_sqlite_error_message(err: napi::Error) -> napi::Error { + 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 +} diff --git a/bindings/javascript/wrapper.js b/bindings/javascript/wrapper.js index c42e1246d..0d4c53c96 100644 --- a/bindings/javascript/wrapper.js +++ b/bindings/javascript/wrapper.js @@ -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;