Skip to content

Commit a055d51

Browse files
committed
Use structured errors
1 parent 30c0d06 commit a055d51

27 files changed

+366
-327
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv
2222
const_format = "0.2.34"
2323
futures-lite = { version = "2.6.0", default-features = false, features = ["alloc"] }
2424
rustc-hash = { version = "2.1", default-features = false }
25+
thiserror = { version = "2", default-features = false }
2526

2627
[dependencies.uuid]
2728
version = "1.4.1"

crates/core/src/bson/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ impl BsonError {
4747
}
4848
}
4949

50+
impl core::error::Error for BsonError {}
51+
5052
impl Display for BsonError {
5153
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
5254
self.err.fmt(f)

crates/core/src/checkpoint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use sqlite_nostd as sqlite;
1111
use sqlite_nostd::{Connection, Context, Value};
1212

1313
use crate::create_sqlite_text_fn;
14-
use crate::error::SQLiteError;
14+
use crate::error::PowerSyncError;
1515
use crate::sync::checkpoint::{validate_checkpoint, OwnedBucketChecksum};
1616
use crate::sync::line::Checkpoint;
1717

@@ -24,7 +24,7 @@ struct CheckpointResult {
2424
fn powersync_validate_checkpoint_impl(
2525
ctx: *mut sqlite::context,
2626
args: &[*mut sqlite::value],
27-
) -> Result<String, SQLiteError> {
27+
) -> Result<String, PowerSyncError> {
2828
let data = args[0].text();
2929
let checkpoint: Checkpoint = serde_json::from_str(data)?;
3030
let db = ctx.db_handle();

crates/core/src/crud_vtab.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use sqlite::{Connection, ResultCode, Value};
1212
use sqlite_nostd::ManagedStmt;
1313
use sqlite_nostd::{self as sqlite, ColumnType};
1414

15-
use crate::error::SQLiteError;
15+
use crate::error::{PowerSyncError, RawPowerSyncError};
1616
use crate::ext::SafeManagedStmt;
1717
use crate::schema::TableInfoFlags;
1818
use crate::state::DatabaseState;
@@ -81,11 +81,11 @@ impl VirtualTable {
8181
}
8282
}
8383

84-
fn handle_insert(&mut self, args: &[*mut sqlite::value]) -> Result<(), SQLiteError> {
84+
fn handle_insert(&mut self, args: &[*mut sqlite::value]) -> Result<(), PowerSyncError> {
8585
let current_tx = self
8686
.current_tx
8787
.as_mut()
88-
.ok_or_else(|| SQLiteError::misuse("No tx_id"))?;
88+
.ok_or_else(|| PowerSyncError::from(RawPowerSyncError::CrudVtabOutsideOfTransaction))?;
8989
let db = self.db;
9090

9191
if self.state.is_in_sync_local.load(Ordering::Relaxed) {
@@ -178,7 +178,7 @@ impl VirtualTable {
178178
Ok(())
179179
}
180180

181-
fn begin(&mut self) -> Result<(), SQLiteError> {
181+
fn begin(&mut self) -> Result<(), PowerSyncError> {
182182
let db = self.db;
183183

184184
// language=SQLite
@@ -187,7 +187,7 @@ impl VirtualTable {
187187
let tx_id = if statement.step()? == ResultCode::ROW {
188188
statement.column_int64(0) - 1
189189
} else {
190-
return Err(SQLiteError::from(ResultCode::ABORT));
190+
return Err(PowerSyncError::from(RawPowerSyncError::Internal));
191191
};
192192

193193
self.current_tx = Some(ActiveCrudTransaction {
@@ -328,7 +328,7 @@ extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int {
328328
extern "C" fn begin(vtab: *mut sqlite::vtab) -> c_int {
329329
let tab = unsafe { &mut *(vtab.cast::<VirtualTable>()) };
330330
let result = tab.begin();
331-
vtab_result(vtab, result)
331+
vtab_result(vtab, tab.db, result)
332332
}
333333

334334
extern "C" fn commit(vtab: *mut sqlite::vtab) -> c_int {
@@ -361,7 +361,7 @@ extern "C" fn update(
361361
// INSERT
362362
let tab = unsafe { &mut *(vtab.cast::<VirtualTable>()) };
363363
let result = tab.handle_insert(&args[2..]);
364-
vtab_result(vtab, result)
364+
vtab_result(vtab, tab.db, result)
365365
} else {
366366
// UPDATE - not supported
367367
ResultCode::MISUSE as c_int

crates/core/src/diff.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ use sqlite_nostd::{Connection, Context, Value};
1010
use serde_json as json;
1111

1212
use crate::create_sqlite_text_fn;
13-
use crate::error::SQLiteError;
13+
use crate::error::{PowerSyncError, RawPowerSyncError};
1414

1515
fn powersync_diff_impl(
1616
_ctx: *mut sqlite::context,
1717
args: &[*mut sqlite::value],
18-
) -> Result<String, SQLiteError> {
18+
) -> Result<String, PowerSyncError> {
1919
let data_old = args[0].text();
2020
let data_new = args[1].text();
2121

2222
diff_objects(data_old, data_new)
2323
}
2424

25-
pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, SQLiteError> {
25+
pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, PowerSyncError> {
2626
let v_new: json::Value = json::from_str(data_new)?;
2727
let v_old: json::Value = json::from_str(data_old)?;
2828

@@ -56,7 +56,7 @@ pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, SQLiteErro
5656

5757
Ok(json::Value::Object(left).to_string())
5858
} else {
59-
Err(SQLiteError::from(ResultCode::MISMATCH))
59+
Err(RawPowerSyncError::ExpectedJsonObject.into())
6060
}
6161
}
6262

crates/core/src/error.rs

Lines changed: 106 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,145 @@
1+
use core::fmt::Display;
2+
13
use alloc::{
24
borrow::Cow,
5+
boxed::Box,
36
format,
47
string::{String, ToString},
58
};
6-
use core::error::Error;
79
use sqlite_nostd::{context, sqlite3, Connection, Context, ResultCode};
10+
use thiserror::Error;
811

912
use crate::bson::BsonError;
1013

11-
#[derive(Debug)]
12-
pub struct SQLiteError(pub ResultCode, pub Option<Cow<'static, str>>);
14+
/// A [RawPowerSyncError], but boxed.
15+
///
16+
/// We allocate errors in boxes to avoid large [Result] types returning these.
17+
pub struct PowerSyncError {
18+
inner: Box<RawPowerSyncError>,
19+
}
1320

14-
impl SQLiteError {
15-
pub fn with_description(code: ResultCode, message: impl Into<Cow<'static, str>>) -> Self {
16-
Self(code, Some(message.into()))
21+
impl PowerSyncError {
22+
pub fn from_sqlite(code: ResultCode, context: impl Into<Cow<'static, str>>) -> Self {
23+
RawPowerSyncError::Sqlite {
24+
code,
25+
context: Some(context.into()),
26+
}
27+
.into()
1728
}
1829

19-
pub fn misuse(message: impl Into<Cow<'static, str>>) -> Self {
20-
Self::with_description(ResultCode::MISUSE, message)
30+
pub fn argument_error(desc: &'static str) -> Self {
31+
RawPowerSyncError::ArgumentError { desc }.into()
2132
}
22-
}
2333

24-
impl core::fmt::Display for SQLiteError {
25-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26-
write!(f, "SQLiteError: {:?}", self.0)?;
27-
if let Some(desc) = &self.1 {
28-
write!(f, ", desc: {}", desc)?;
29-
}
30-
Ok(())
34+
pub fn state_error(desc: &'static str) -> Self {
35+
RawPowerSyncError::StateError { desc }.into()
3136
}
32-
}
3337

34-
impl SQLiteError {
3538
pub fn apply_to_ctx(self, description: &str, ctx: *mut context) {
36-
let SQLiteError(code, message) = self;
37-
38-
if let Some(msg) = message {
39-
ctx.result_error(&format!("{:} {:}", description, msg));
40-
} else {
41-
let error = ctx.db_handle().errmsg().unwrap();
42-
if error == "not an error" {
43-
ctx.result_error(&format!("{:}", description));
44-
} else {
45-
ctx.result_error(&format!("{:} {:}", description, error));
39+
let mut desc = self.description(ctx.db_handle());
40+
desc.insert_str(0, description);
41+
desc.insert_str(description.len(), ": ");
42+
43+
ctx.result_error(&desc);
44+
ctx.result_error_code(self.sqlite_error_code());
45+
}
46+
47+
/// Obtains a description of this error, fetching it from SQLite if necessary.
48+
pub fn description(&self, db: *mut sqlite3) -> String {
49+
if let RawPowerSyncError::Sqlite { .. } = &*self.inner {
50+
let message = db.errmsg().unwrap_or(String::from("Conversion error"));
51+
if message != "not an error" {
52+
return format!("{}, caused by: {message}", self.inner);
4653
}
4754
}
48-
ctx.result_error_code(code);
55+
56+
self.inner.to_string()
4957
}
50-
}
5158

52-
impl Error for SQLiteError {}
59+
pub fn sqlite_error_code(&self) -> ResultCode {
60+
use RawPowerSyncError::*;
5361

54-
pub trait PSResult<T> {
55-
fn into_db_result(self, db: *mut sqlite3) -> Result<T, SQLiteError>;
62+
match self.inner.as_ref() {
63+
Sqlite { code, .. } => *code,
64+
InvalidPendingStatement { .. }
65+
| InvalidBucketPriority
66+
| ExpectedJsonObject
67+
| ArgumentError { .. }
68+
| StateError { .. }
69+
| JsonObjectTooBig
70+
| CrudVtabOutsideOfTransaction => ResultCode::MISUSE,
71+
MissingClientId | Internal => ResultCode::ABORT,
72+
JsonError { .. } | BsonError { .. } => ResultCode::CONSTRAINT_DATATYPE,
73+
}
74+
}
5675
}
5776

58-
impl<T> PSResult<T> for Result<T, ResultCode> {
59-
fn into_db_result(self, db: *mut sqlite3) -> Result<T, SQLiteError> {
60-
if let Err(code) = self {
61-
let message = db.errmsg().unwrap_or(String::from("Conversion error"));
62-
if message == "not an error" {
63-
Err(SQLiteError(code, None))
64-
} else {
65-
Err(SQLiteError(code, Some(message.into())))
66-
}
67-
} else if let Ok(r) = self {
68-
Ok(r)
69-
} else {
70-
Err(SQLiteError(ResultCode::ABORT, None))
71-
}
77+
impl Display for PowerSyncError {
78+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79+
self.inner.fmt(f)
7280
}
7381
}
7482

75-
impl From<ResultCode> for SQLiteError {
76-
fn from(value: ResultCode) -> Self {
77-
SQLiteError(value, None)
83+
impl From<RawPowerSyncError> for PowerSyncError {
84+
fn from(value: RawPowerSyncError) -> Self {
85+
return PowerSyncError {
86+
inner: Box::new(value),
87+
};
7888
}
7989
}
8090

81-
impl From<serde_json::Error> for SQLiteError {
82-
fn from(value: serde_json::Error) -> Self {
83-
SQLiteError::with_description(ResultCode::ABORT, value.to_string())
91+
impl From<ResultCode> for PowerSyncError {
92+
fn from(value: ResultCode) -> Self {
93+
return RawPowerSyncError::Sqlite {
94+
code: value,
95+
context: None,
96+
}
97+
.into();
8498
}
8599
}
86100

87-
impl From<core::fmt::Error> for SQLiteError {
88-
fn from(value: core::fmt::Error) -> Self {
89-
SQLiteError::with_description(ResultCode::INTERNAL, format!("{}", value))
101+
impl From<serde_json::Error> for PowerSyncError {
102+
fn from(value: serde_json::Error) -> Self {
103+
RawPowerSyncError::JsonError(value).into()
90104
}
91105
}
92106

93-
impl From<BsonError> for SQLiteError {
107+
impl From<BsonError> for PowerSyncError {
94108
fn from(value: BsonError) -> Self {
95-
SQLiteError::with_description(ResultCode::ERROR, value.to_string())
109+
RawPowerSyncError::from(value).into()
96110
}
97111
}
112+
113+
#[derive(Error, Debug)]
114+
pub enum RawPowerSyncError {
115+
#[error("internal SQLite call returned {code}")]
116+
Sqlite {
117+
code: ResultCode,
118+
context: Option<Cow<'static, str>>,
119+
},
120+
#[error("invalid argument: {desc}")]
121+
ArgumentError { desc: &'static str },
122+
#[error("invalid state: {desc}")]
123+
StateError { desc: &'static str },
124+
#[error("Function required a JSON object, but got another type of JSON value")]
125+
ExpectedJsonObject,
126+
#[error("No client_id found in ps_kv")]
127+
MissingClientId,
128+
#[error("Invalid pending statement for raw table: {description}")]
129+
InvalidPendingStatement { description: Cow<'static, str> },
130+
#[error("Invalid bucket priority value")]
131+
InvalidBucketPriority,
132+
#[error("Internal PowerSync error")]
133+
Internal,
134+
#[error("Error decoding JSON: {0}")]
135+
JsonError(serde_json::Error),
136+
#[error("Error decoding BSON")]
137+
BsonError {
138+
#[from]
139+
source: BsonError,
140+
},
141+
#[error("Too many arguments passed to json_object_fragment")]
142+
JsonObjectTooBig,
143+
#[error("No tx_id")]
144+
CrudVtabOutsideOfTransaction,
145+
}

0 commit comments

Comments
 (0)