From 475c616176945d72f4330c92801f0c5e6398dc0f Mon Sep 17 00:00:00 2001 From: Serhii Tatarintsev Date: Mon, 23 Oct 2023 17:40:38 +0200 Subject: [PATCH] driver-adapters: Map libsql errors to Prisma errors (#4362) Similar approach to what we did with Neon: raw error data is returned from driver adapter in case of DB error, which then reuses Quaint's error handling code for adapter too. Close prisma/team-orm#393 --- quaint/src/connector/sqlite.rs | 2 + quaint/src/connector/sqlite/error.rs | 211 ++++++++---------- quaint/src/error.rs | 1 + .../js/adapter-libsql/src/libsql.ts | 48 ++-- .../js/driver-adapter-utils/src/types.ts | 8 + .../driver-adapters/js/pnpm-lock.yaml | 44 ++-- .../js/smoke-test-js/src/libquery/libquery.ts | 18 +- query-engine/driver-adapters/src/result.rs | 12 +- 8 files changed, 170 insertions(+), 174 deletions(-) diff --git a/quaint/src/connector/sqlite.rs b/quaint/src/connector/sqlite.rs index 6db49523c80a..3a1ef72b4883 100644 --- a/quaint/src/connector/sqlite.rs +++ b/quaint/src/connector/sqlite.rs @@ -1,6 +1,8 @@ mod conversion; mod error; +pub use error::SqliteError; + pub use rusqlite::{params_from_iter, version as sqlite_version}; use super::IsolationLevel; diff --git a/quaint/src/connector/sqlite/error.rs b/quaint/src/connector/sqlite/error.rs index fa8b83f3f28a..c10b335cb3c0 100644 --- a/quaint/src/connector/sqlite/error.rs +++ b/quaint/src/connector/sqlite/error.rs @@ -1,69 +1,45 @@ +use std::fmt; + use crate::error::*; use rusqlite::ffi; use rusqlite::types::FromSqlError; -impl From for Error { - fn from(e: rusqlite::Error) -> Error { - match e { - rusqlite::Error::ToSqlConversionFailure(error) => match error.downcast::() { - Ok(error) => *error, - Err(error) => { - let mut builder = Error::builder(ErrorKind::QueryError(error)); - - builder.set_original_message("Could not interpret parameters in an SQLite query."); - - builder.build() - } - }, - rusqlite::Error::InvalidQuery => { - let mut builder = Error::builder(ErrorKind::QueryError(e.into())); - - builder.set_original_message( - "Could not interpret the query or its parameters. Check the syntax and parameter types.", - ); - - builder.build() - } - rusqlite::Error::ExecuteReturnedResults => { - let mut builder = Error::builder(ErrorKind::QueryError(e.into())); - builder.set_original_message("Execute returned results, which is not allowed in SQLite."); - - builder.build() - } - - rusqlite::Error::QueryReturnedNoRows => Error::builder(ErrorKind::NotFound).build(), +#[derive(Debug)] +pub struct SqliteError { + pub extended_code: i32, + pub message: Option, +} - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::ConstraintViolation, - extended_code: 2067, - }, - Some(description), - ) => { - let constraint = description - .split(": ") - .nth(1) - .map(|s| s.split(", ")) - .map(|i| i.flat_map(|s| s.split('.').last())) - .map(DatabaseConstraint::fields) - .unwrap_or(DatabaseConstraint::CannotParse); +impl fmt::Display for SqliteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Error code {}: {}", + self.extended_code, + ffi::code_to_str(self.extended_code) + ) + } +} - let kind = ErrorKind::UniqueConstraintViolation { constraint }; - let mut builder = Error::builder(kind); +impl std::error::Error for SqliteError {} - builder.set_original_code("2067"); - builder.set_original_message(description); +impl SqliteError { + pub fn new(extended_code: i32, message: Option) -> Self { + Self { extended_code, message } + } - builder.build() - } + pub fn primary_code(&self) -> i32 { + self.extended_code & 0xFF + } +} - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::ConstraintViolation, - extended_code: 1555, - }, - Some(description), - ) => { +impl From for Error { + fn from(error: SqliteError) -> Self { + match error { + SqliteError { + extended_code: ffi::SQLITE_CONSTRAINT_UNIQUE | ffi::SQLITE_CONSTRAINT_PRIMARYKEY, + message: Some(description), + } => { let constraint = description .split(": ") .nth(1) @@ -75,19 +51,16 @@ impl From for Error { let kind = ErrorKind::UniqueConstraintViolation { constraint }; let mut builder = Error::builder(kind); - builder.set_original_code("1555"); + builder.set_original_code(error.extended_code.to_string()); builder.set_original_message(description); builder.build() } - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::ConstraintViolation, - extended_code: 1299, - }, - Some(description), - ) => { + SqliteError { + extended_code: ffi::SQLITE_CONSTRAINT_NOTNULL, + message: Some(description), + } => { let constraint = description .split(": ") .nth(1) @@ -99,64 +72,41 @@ impl From for Error { let kind = ErrorKind::NullConstraintViolation { constraint }; let mut builder = Error::builder(kind); - builder.set_original_code("1299"); - builder.set_original_message(description); - - builder.build() - } - - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::ConstraintViolation, - extended_code: 787, - }, - Some(description), - ) => { - let mut builder = Error::builder(ErrorKind::ForeignKeyConstraintViolation { - constraint: DatabaseConstraint::ForeignKey, - }); - - builder.set_original_code("787"); + builder.set_original_code(error.extended_code.to_string()); builder.set_original_message(description); builder.build() } - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::ConstraintViolation, - extended_code: 1811, - }, - Some(description), - ) => { + SqliteError { + extended_code: ffi::SQLITE_CONSTRAINT_FOREIGNKEY | ffi::SQLITE_CONSTRAINT_TRIGGER, + message: Some(description), + } => { let mut builder = Error::builder(ErrorKind::ForeignKeyConstraintViolation { constraint: DatabaseConstraint::ForeignKey, }); - builder.set_original_code("1811"); + builder.set_original_code(error.extended_code.to_string()); builder.set_original_message(description); builder.build() } - rusqlite::Error::SqliteFailure( - ffi::Error { - code: ffi::ErrorCode::DatabaseBusy, - extended_code, - }, - description, - ) => { + SqliteError { extended_code, message } if error.primary_code() == ffi::SQLITE_BUSY => { let mut builder = Error::builder(ErrorKind::SocketTimeout); builder.set_original_code(format!("{extended_code}")); - if let Some(description) = description { + if let Some(description) = message { builder.set_original_message(description); } builder.build() } - rusqlite::Error::SqliteFailure(ffi::Error { extended_code, .. }, ref description) => match description { + SqliteError { + extended_code, + ref message, + } => match message { Some(d) if d.starts_with("no such table") => { let table = d.split(": ").last().into(); let kind = ErrorKind::TableDoesNotExist { table }; @@ -188,8 +138,8 @@ impl From for Error { builder.build() } _ => { - let description = description.as_ref().map(|d| d.to_string()); - let mut builder = Error::builder(ErrorKind::QueryError(e.into())); + let description = message.as_ref().map(|d| d.to_string()); + let mut builder = Error::builder(ErrorKind::QueryError(error.into())); builder.set_original_code(format!("{extended_code}")); if let Some(description) = description { @@ -199,31 +149,50 @@ impl From for Error { builder.build() } }, + } + } +} - rusqlite::Error::SqlInputError { - error: ffi::Error { extended_code, .. }, - ref msg, - .. - } => match msg { - d if d.starts_with("no such column: ") => { - let column = d.split("no such column: ").last().into(); - let kind = ErrorKind::ColumnNotFound { column }; - - let mut builder = Error::builder(kind); - builder.set_original_code(extended_code.to_string()); - builder.set_original_message(d); +impl From for Error { + fn from(e: rusqlite::Error) -> Error { + match e { + rusqlite::Error::ToSqlConversionFailure(error) => match error.downcast::() { + Ok(error) => *error, + Err(error) => { + let mut builder = Error::builder(ErrorKind::QueryError(error)); - builder.build() - } - _ => { - let description = msg.clone(); - let mut builder = Error::builder(ErrorKind::QueryError(e.into())); - builder.set_original_code(extended_code.to_string()); - builder.set_original_message(description); + builder.set_original_message("Could not interpret parameters in an SQLite query."); builder.build() } }, + rusqlite::Error::InvalidQuery => { + let mut builder = Error::builder(ErrorKind::QueryError(e.into())); + + builder.set_original_message( + "Could not interpret the query or its parameters. Check the syntax and parameter types.", + ); + + builder.build() + } + rusqlite::Error::ExecuteReturnedResults => { + let mut builder = Error::builder(ErrorKind::QueryError(e.into())); + builder.set_original_message("Execute returned results, which is not allowed in SQLite."); + + builder.build() + } + + rusqlite::Error::QueryReturnedNoRows => Error::builder(ErrorKind::NotFound).build(), + + rusqlite::Error::SqliteFailure(ffi::Error { code: _, extended_code }, message) => { + SqliteError::new(extended_code, message).into() + } + + rusqlite::Error::SqlInputError { + error: ffi::Error { extended_code, .. }, + msg, + .. + } => SqliteError::new(extended_code, Some(msg)).into(), e => Error::builder(ErrorKind::QueryError(e.into())).build(), } diff --git a/quaint/src/error.rs b/quaint/src/error.rs index 0460b77100fb..705bb6b37ee0 100644 --- a/quaint/src/error.rs +++ b/quaint/src/error.rs @@ -8,6 +8,7 @@ use std::time::Duration; pub use crate::connector::mysql::MysqlError; pub use crate::connector::postgres::PostgresError; +pub use crate::connector::sqlite::SqliteError; #[derive(Debug, PartialEq, Eq)] pub enum DatabaseConstraint { diff --git a/query-engine/driver-adapters/js/adapter-libsql/src/libsql.ts b/query-engine/driver-adapters/js/adapter-libsql/src/libsql.ts index 5d104e8e2949..6528c8f44a8a 100644 --- a/query-engine/driver-adapters/js/adapter-libsql/src/libsql.ts +++ b/query-engine/driver-adapters/js/adapter-libsql/src/libsql.ts @@ -1,4 +1,4 @@ -import { Debug, ok } from '@prisma/driver-adapter-utils' +import { Debug, ok, err } from '@prisma/driver-adapter-utils' import type { DriverAdapter, Query, @@ -8,7 +8,12 @@ import type { Transaction, TransactionOptions, } from '@prisma/driver-adapter-utils' -import type { InStatement, Client as LibSqlClientRaw, Transaction as LibSqlTransactionRaw } from '@libsql/client' +import type { + InStatement, + Client as LibSqlClientRaw, + Transaction as LibSqlTransactionRaw, + ResultSet as LibSqlResultSet, +} from '@libsql/client' import { Mutex } from 'async-mutex' import { getColumnTypes, mapRow } from './conversion' @@ -33,17 +38,17 @@ class LibSqlQueryable implements const tag = '[js::query_raw]' debug(`${tag} %O`, query) - const { columns, rows, columnTypes: declaredColumnTypes } = await this.performIO(query) - - const columnTypes = getColumnTypes(declaredColumnTypes, rows) + const ioResult = await this.performIO(query) - const resultSet: ResultSet = { - columnNames: columns, - columnTypes, - rows: rows.map((row) => mapRow(row, columnTypes)), - } + return ioResult.map(({ columns, rows, columnTypes: declaredColumnTypes }) => { + const columnTypes = getColumnTypes(declaredColumnTypes, rows) - return ok(resultSet) + return { + columnNames: columns, + columnTypes, + rows: rows.map((row) => mapRow(row, columnTypes)), + } + }) } /** @@ -55,8 +60,7 @@ class LibSqlQueryable implements const tag = '[js::execute_raw]' debug(`${tag} %O`, query) - const { rowsAffected } = await this.performIO(query) - return ok(rowsAffected ?? 0) + return (await this.performIO(query)).map(({ rowsAffected }) => rowsAffected ?? 0) } /** @@ -64,14 +68,22 @@ class LibSqlQueryable implements * Should the query fail due to a connection error, the connection is * marked as unhealthy. */ - private async performIO(query: Query) { + private async performIO(query: Query): Promise> { const release = await this[LOCK_TAG].acquire() try { const result = await this.client.execute(query as InStatement) - return result + return ok(result) } catch (e) { const error = e as Error debug('Error in performIO: %O', error) + const rawCode = error['rawCode'] ?? e.cause?.['rawCode'] + if (typeof rawCode === 'number') { + return err({ + kind: 'Sqlite', + extendedCode: rawCode, + message: error.message, + }) + } throw error } finally { release() @@ -82,11 +94,7 @@ class LibSqlQueryable implements class LibSqlTransaction extends LibSqlQueryable implements Transaction { finished = false - constructor( - client: TransactionClient, - readonly options: TransactionOptions, - readonly unlockParent: () => void, - ) { + constructor(client: TransactionClient, readonly options: TransactionOptions, readonly unlockParent: () => void) { super(client) } diff --git a/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts b/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts index 104b23d233c5..92019f81824b 100644 --- a/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts +++ b/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts @@ -53,6 +53,14 @@ export type Error = message: string state: string } + | { + kind: 'Sqlite' + /** + * Sqlite extended error code: https://www.sqlite.org/rescode.html + */ + extendedCode: number + message: string + } export interface Queryable { readonly flavour: 'mysql' | 'postgres' | 'sqlite' diff --git a/query-engine/driver-adapters/js/pnpm-lock.yaml b/query-engine/driver-adapters/js/pnpm-lock.yaml index 3f7f13d3ff6a..9a82ffdbac63 100644 --- a/query-engine/driver-adapters/js/pnpm-lock.yaml +++ b/query-engine/driver-adapters/js/pnpm-lock.yaml @@ -435,21 +435,21 @@ packages: dependencies: '@libsql/hrana-client': 0.5.5 js-base64: 3.7.5 - libsql: 0.1.23 + libsql: 0.1.28 transitivePeerDependencies: - bufferutil - encoding - utf-8-validate - /@libsql/darwin-arm64@0.1.23: - resolution: {integrity: sha512-+V9aoOrZ47iYbY5NrcS0F2bDOCH407QI0wxAtss0CLOcFxlz/T6Nw0ryLK31GabklJQAmOXIyqkumLfz5HT64w==} + /@libsql/darwin-arm64@0.1.28: + resolution: {integrity: sha512-p4nldHUOhcl9ibnH1F6oiXV5Dl3PAcPB9VIjdjVvO3/URo5J7mhqRMuwJMKO5DZJJGtkKJ5IO0gu0hc90rnKIg==} cpu: [arm64] os: [darwin] requiresBuild: true optional: true - /@libsql/darwin-x64@0.1.23: - resolution: {integrity: sha512-toHo7s0HiMl4VCIfjhGXDe9bGWWo78eP8fxIbwU6RlaLO6MNV9fjHY/GjTWccWOwyxcT+q6X/kUc957HnoW3bg==} + /@libsql/darwin-x64@0.1.28: + resolution: {integrity: sha512-WaEK+Z+wP5sr0h8EcusSGHv4Mqc3smYICeG4P/wsbRDKQ2WUMWqZrpgqaBsm+WPbXogU2vpf+qGc8BnpFZ0ggw==} cpu: [x64] os: [darwin] requiresBuild: true @@ -484,22 +484,29 @@ packages: - bufferutil - utf-8-validate - /@libsql/linux-x64-gnu@0.1.23: - resolution: {integrity: sha512-U11LdjayakOj0lQCHDYkTgUfe4Q+7AjZZh8MzgEDF/9l0bmKNI3eFLWA3JD2Xm98yz65lUx95om0WKOKu5VW/w==} + /@libsql/linux-arm64-gnu@0.1.28: + resolution: {integrity: sha512-a17ANBuOqH2L8gdyET4Kg3XggQvxWnoA+7x7sDEX5NyWNyvr7P04WzNPAT0xAOWLclC1fDD6jM5sh/fbJk/7NA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@libsql/linux-x64-gnu@0.1.28: + resolution: {integrity: sha512-dkg+Ou7ApV0PHpZWd9c6NrYyc/WSNn5h/ScKotaMTLWlLL96XAMNwrYLpZpUj61I2y7QzU98XtMfiSD1Ux+VaA==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@libsql/linux-x64-musl@0.1.23: - resolution: {integrity: sha512-8UcCK2sPVzcafHsEmcU5IDp/NxjD6F6JFS5giijsMX5iGgxYQiiwTUMOmSxW0AWBeT4VY5U7G6rG5PC8JSFtfg==} + /@libsql/linux-x64-musl@0.1.28: + resolution: {integrity: sha512-ZuOxCDYlG+f1IDsxstmaxLtgG9HvlLuUKs0X3um4f5F5V+P+PF8qr08gSdD1IP2pj+JBOiwhQffaEpR1wupxhQ==} cpu: [x64] os: [linux] requiresBuild: true optional: true - /@libsql/win32-x64-msvc@0.1.23: - resolution: {integrity: sha512-HAugD66jTmRRRGNMLKRiaFeMOC3mgUsAiuO6NRdRz3nM6saf9e5QqN/Ppuu9yqHHcZfv7VhQ9UGlAvzVK64Itg==} + /@libsql/win32-x64-msvc@0.1.28: + resolution: {integrity: sha512-2cmUiMIsJLHpetebGeeYqUYaCPWEnwMjqxwu1ZEEbA5x8r+DNmIhLrc0QSQ29p7a5u14vbZnShNOtT/XG7vKew==} cpu: [x64] os: [win32] requiresBuild: true @@ -971,19 +978,20 @@ packages: /js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} - /libsql@0.1.23: - resolution: {integrity: sha512-Nf/1B2Glxvcnba4jYFhXcaYmicyBA3RRm0LVwBkTl8UWCIDbX+Ad7c1ecrQwixPLPffWOVxKIqyCNTuUHUkVgA==} + /libsql@0.1.28: + resolution: {integrity: sha512-yCKlT0ntV8ZIWTPGNClhQQeH/LNAzLjbbEgBvgLb+jfQwAuTbyvPpVVLwkZzesqja1nbkWApztW0pX81Jp0pkw==} cpu: [x64, arm64] os: [darwin, linux, win32] dependencies: '@neon-rs/load': 0.0.4 detect-libc: 2.0.2 optionalDependencies: - '@libsql/darwin-arm64': 0.1.23 - '@libsql/darwin-x64': 0.1.23 - '@libsql/linux-x64-gnu': 0.1.23 - '@libsql/linux-x64-musl': 0.1.23 - '@libsql/win32-x64-msvc': 0.1.23 + '@libsql/darwin-arm64': 0.1.28 + '@libsql/darwin-x64': 0.1.28 + '@libsql/linux-arm64-gnu': 0.1.28 + '@libsql/linux-x64-gnu': 0.1.28 + '@libsql/linux-x64-musl': 0.1.28 + '@libsql/win32-x64-msvc': 0.1.28 /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} diff --git a/query-engine/driver-adapters/js/smoke-test-js/src/libquery/libquery.ts b/query-engine/driver-adapters/js/smoke-test-js/src/libquery/libquery.ts index e94eacbae328..c50ad3e257ab 100644 --- a/query-engine/driver-adapters/js/smoke-test-js/src/libquery/libquery.ts +++ b/query-engine/driver-adapters/js/smoke-test-js/src/libquery/libquery.ts @@ -290,13 +290,13 @@ export function smokeTestLibquery( }) it('expected error (on duplicate insert) as json result (not throwing error)', async () => { - // clean up first await doQuery({ modelName: 'Unique', action: 'deleteMany', query: { + arguments: {}, selection: { - count: true, + $scalars: true, }, }, }) @@ -327,17 +327,9 @@ export function smokeTestLibquery( }, }) - if (flavour === 'postgres' || flavour === 'mysql') { - const result = await promise - console.log('[nodejs] error result', JSON.stringify(result, null, 2)) - assert.equal(result?.errors?.[0]?.['user_facing_error']?.['error_code'], 'P2002') - } else { - await assert.rejects(promise, (err) => { - assert(typeof err === 'object' && err !== null) - assert.match(err['message'], /unique/i) - return true - }) - } + const result = await promise + console.log('[nodejs] error result', JSON.stringify(result, null, 2)) + assert.equal(result?.errors?.[0]?.['user_facing_error']?.['error_code'], 'P2002') }) describe('read scalar and non scalar types', () => { diff --git a/query-engine/driver-adapters/src/result.rs b/query-engine/driver-adapters/src/result.rs index 08397d834ed0..c43f66a81e72 100644 --- a/query-engine/driver-adapters/src/result.rs +++ b/query-engine/driver-adapters/src/result.rs @@ -1,5 +1,5 @@ use napi::{bindgen_prelude::FromNapiValue, Env, JsUnknown, NapiValue}; -use quaint::error::{Error as QuaintError, MysqlError, PostgresError}; +use quaint::error::{Error as QuaintError, MysqlError, PostgresError, SqliteError}; use serde::Deserialize; #[derive(Deserialize)] @@ -21,6 +21,13 @@ pub struct MysqlErrorDef { pub state: String, } +#[derive(Deserialize)] +#[serde(remote = "SqliteError", rename_all = "camelCase")] +pub struct SqliteErrorDef { + pub extended_code: i32, + pub message: Option, +} + #[derive(Deserialize)] #[serde(tag = "kind")] /// Wrapper for JS-side errors @@ -33,7 +40,7 @@ pub(crate) enum DriverAdapterError { Postgres(#[serde(with = "PostgresErrorDef")] PostgresError), Mysql(#[serde(with = "MysqlErrorDef")] MysqlError), - // in the future, expected errors that map to known user errors with PXXX codes will also go here + Sqlite(#[serde(with = "SqliteErrorDef")] SqliteError), } impl FromNapiValue for DriverAdapterError { @@ -50,6 +57,7 @@ impl From for QuaintError { DriverAdapterError::GenericJs { id } => QuaintError::external_error(id), DriverAdapterError::Postgres(e) => e.into(), DriverAdapterError::Mysql(e) => e.into(), + DriverAdapterError::Sqlite(e) => e.into(), // in future, more error types would be added and we'll need to convert them to proper QuaintErrors here } }