Skip to content

Commit

Permalink
driver-adapters: Map libsql errors to Prisma errors (#4362)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Serhii Tatarintsev authored Oct 23, 2023
1 parent 28291c7 commit 475c616
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 174 deletions.
2 changes: 2 additions & 0 deletions quaint/src/connector/sqlite.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
211 changes: 90 additions & 121 deletions quaint/src/connector/sqlite/error.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,45 @@
use std::fmt;

use crate::error::*;
use rusqlite::ffi;
use rusqlite::types::FromSqlError;

impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Error {
match e {
rusqlite::Error::ToSqlConversionFailure(error) => match error.downcast::<Error>() {
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<String>,
}

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<String>) -> 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<SqliteError> 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)
Expand All @@ -75,19 +51,16 @@ impl From<rusqlite::Error> 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)
Expand All @@ -99,64 +72,41 @@ impl From<rusqlite::Error> 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 };
Expand Down Expand Up @@ -188,8 +138,8 @@ impl From<rusqlite::Error> 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 {
Expand All @@ -199,31 +149,50 @@ impl From<rusqlite::Error> 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<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Error {
match e {
rusqlite::Error::ToSqlConversionFailure(error) => match error.downcast::<Error>() {
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(),
}
Expand Down
1 change: 1 addition & 0 deletions quaint/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
48 changes: 28 additions & 20 deletions query-engine/driver-adapters/js/adapter-libsql/src/libsql.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'

Expand All @@ -33,17 +38,17 @@ class LibSqlQueryable<ClientT extends StdClient | TransactionClient> 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)),
}
})
}

/**
Expand All @@ -55,23 +60,30 @@ class LibSqlQueryable<ClientT extends StdClient | TransactionClient> 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)
}

/**
* Run a query against the database, returning the result set.
* 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<Result<LibSqlResultSet>> {
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()
Expand All @@ -82,11 +94,7 @@ class LibSqlQueryable<ClientT extends StdClient | TransactionClient> implements
class LibSqlTransaction extends LibSqlQueryable<TransactionClient> implements Transaction {
finished = false

constructor(
client: TransactionClient,
readonly options: TransactionOptions,
readonly unlockParent: () => void,
) {
constructor(client: TransactionClient, readonly options: TransactionOptions, readonly unlockParent: () => void) {
super(client)
}

Expand Down
Loading

0 comments on commit 475c616

Please sign in to comment.