Skip to content

Commit 05ba512

Browse files
committed
Better enums for errors
1 parent a055d51 commit 05ba512

File tree

14 files changed

+177
-92
lines changed

14 files changed

+177
-92
lines changed

crates/core/src/checkpoint.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ fn powersync_validate_checkpoint_impl(
2626
args: &[*mut sqlite::value],
2727
) -> Result<String, PowerSyncError> {
2828
let data = args[0].text();
29-
let checkpoint: Checkpoint = serde_json::from_str(data)?;
29+
let checkpoint: Checkpoint =
30+
serde_json::from_str(data).map_err(PowerSyncError::json_argument_error)?;
3031
let db = ctx.db_handle();
3132
let buckets: Vec<OwnedBucketChecksum> = checkpoint
3233
.buckets
@@ -45,7 +46,7 @@ fn powersync_validate_checkpoint_impl(
4546
failed_buckets: failed_buckets,
4647
};
4748

48-
Ok(json::to_string(&result)?)
49+
Ok(json::to_string(&result).map_err(PowerSyncError::internal)?)
4950
}
5051

5152
create_sqlite_text_fn!(

crates/core/src/crud_vtab.rs

Lines changed: 5 additions & 4 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::{PowerSyncError, RawPowerSyncError};
15+
use crate::error::PowerSyncError;
1616
use crate::ext::SafeManagedStmt;
1717
use crate::schema::TableInfoFlags;
1818
use crate::state::DatabaseState;
@@ -85,7 +85,7 @@ impl VirtualTable {
8585
let current_tx = self
8686
.current_tx
8787
.as_mut()
88-
.ok_or_else(|| PowerSyncError::from(RawPowerSyncError::CrudVtabOutsideOfTransaction))?;
88+
.ok_or_else(|| PowerSyncError::state_error("Not in tx"))?;
8989
let db = self.db;
9090

9191
if self.state.is_in_sync_local.load(Ordering::Relaxed) {
@@ -162,7 +162,8 @@ impl VirtualTable {
162162
} else {
163163
None
164164
},
165-
})?;
165+
})
166+
.map_err(PowerSyncError::internal)?;
166167
stmt.bind_text(2, &serialized, sqlite::Destructor::STATIC)?;
167168
stmt.exec()?;
168169

@@ -187,7 +188,7 @@ impl VirtualTable {
187188
let tx_id = if statement.step()? == ResultCode::ROW {
188189
statement.column_int64(0) - 1
189190
} else {
190-
return Err(PowerSyncError::from(RawPowerSyncError::Internal));
191+
return Err(PowerSyncError::unknown_internal());
191192
};
192193

193194
self.current_tx = Some(ActiveCrudTransaction {

crates/core/src/diff.rs

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

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

1515
fn powersync_diff_impl(
1616
_ctx: *mut sqlite::context,
@@ -23,8 +23,10 @@ fn powersync_diff_impl(
2323
}
2424

2525
pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, PowerSyncError> {
26-
let v_new: json::Value = json::from_str(data_new)?;
27-
let v_old: json::Value = json::from_str(data_old)?;
26+
let v_new: json::Value =
27+
json::from_str(data_new).map_err(PowerSyncError::json_argument_error)?;
28+
let v_old: json::Value =
29+
json::from_str(data_old).map_err(PowerSyncError::json_argument_error)?;
2830

2931
if let (json::Value::Object(mut left), json::Value::Object(mut right)) = (v_new, v_old) {
3032
// Remove all null values
@@ -56,7 +58,7 @@ pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, PowerSyncE
5658

5759
Ok(json::Value::Object(left).to_string())
5860
} else {
59-
Err(RawPowerSyncError::ExpectedJsonObject.into())
61+
return Err(PowerSyncError::argument_error("expected two JSON objects"));
6062
}
6163
}
6264

crates/core/src/error.rs

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,44 @@ impl PowerSyncError {
2727
.into()
2828
}
2929

30-
pub fn argument_error(desc: &'static str) -> Self {
31-
RawPowerSyncError::ArgumentError { desc }.into()
30+
pub fn argument_error(desc: impl Into<Cow<'static, str>>) -> Self {
31+
RawPowerSyncError::ArgumentError {
32+
desc: desc.into(),
33+
cause: PowerSyncErrorCause::Unknown,
34+
}
35+
.into()
36+
}
37+
38+
pub fn json_argument_error(cause: serde_json::Error) -> Self {
39+
RawPowerSyncError::ArgumentError {
40+
desc: "".into(),
41+
cause: PowerSyncErrorCause::Json(cause),
42+
}
43+
.into()
44+
}
45+
46+
pub fn json_local_error(cause: serde_json::Error) -> Self {
47+
RawPowerSyncError::LocalDataError {
48+
cause: PowerSyncErrorCause::Json(cause),
49+
}
50+
.into()
3251
}
3352

3453
pub fn state_error(desc: &'static str) -> Self {
3554
RawPowerSyncError::StateError { desc }.into()
3655
}
3756

57+
pub fn unknown_internal() -> Self {
58+
Self::internal(PowerSyncErrorCause::Unknown)
59+
}
60+
61+
pub fn internal(cause: impl Into<PowerSyncErrorCause>) -> Self {
62+
RawPowerSyncError::Internal {
63+
cause: cause.into(),
64+
}
65+
.into()
66+
}
67+
3868
pub fn apply_to_ctx(self, description: &str, ctx: *mut context) {
3969
let mut desc = self.description(ctx.db_handle());
4070
desc.insert_str(0, description);
@@ -61,15 +91,10 @@ impl PowerSyncError {
6191

6292
match self.inner.as_ref() {
6393
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,
94+
InvalidBucketPriority | ArgumentError { .. } | StateError { .. } => ResultCode::MISUSE,
95+
MissingClientId | SyncProtocolError { .. } => ResultCode::ABORT,
96+
LocalDataError { .. } => ResultCode::CORRUPT,
97+
Internal { .. } => ResultCode::INTERNAL,
7398
}
7499
}
75100
}
@@ -98,48 +123,85 @@ impl From<ResultCode> for PowerSyncError {
98123
}
99124
}
100125

101-
impl From<serde_json::Error> for PowerSyncError {
102-
fn from(value: serde_json::Error) -> Self {
103-
RawPowerSyncError::JsonError(value).into()
104-
}
105-
}
106-
107-
impl From<BsonError> for PowerSyncError {
108-
fn from(value: BsonError) -> Self {
109-
RawPowerSyncError::from(value).into()
110-
}
111-
}
112-
126+
/// A structured enumeration of possible errors that can occur in the core extension.
113127
#[derive(Error, Debug)]
114128
pub enum RawPowerSyncError {
129+
/// An internal call to SQLite made by the core extension has failed. We store the original
130+
/// result code and an optional context describing what the core extension was trying to do when
131+
/// the error occurred.
132+
///
133+
/// We don't call `sqlite3_errstr` at the time the error is created. Instead, we stop using the
134+
/// database, bubble the error up to the outermost function/vtab definition and then use
135+
/// [PowerSyncError::description] to create a detailed error message.
136+
///
137+
/// This error should _never_ be created for anything but rethrowing underlying SQLite errors.
115138
#[error("internal SQLite call returned {code}")]
116139
Sqlite {
117140
code: ResultCode,
118141
context: Option<Cow<'static, str>>,
119142
},
120-
#[error("invalid argument: {desc}")]
121-
ArgumentError { desc: &'static str },
143+
/// A user (e.g. the one calling a PowerSync function, likely an SDK) has provided invalid
144+
/// arguments.
145+
///
146+
/// This always indicates an error in how the core extension is used.
147+
#[error("invalid argument: {desc}. {cause}")]
148+
ArgumentError {
149+
desc: Cow<'static, str>,
150+
cause: PowerSyncErrorCause,
151+
},
152+
/// A PowerSync function or vtab was used in a state where it's unavailable.
153+
///
154+
/// This always indicates an error in how the core extension is used.
122155
#[error("invalid state: {desc}")]
123156
StateError { desc: &'static str },
124-
#[error("Function required a JSON object, but got another type of JSON value")]
125-
ExpectedJsonObject,
157+
/// We've received a sync line we couldn't parse, or in a state where it doesn't make sense
158+
/// (e.g. a checkpoint diff before we've ever received a checkpoint).
159+
///
160+
/// This interrupts a sync iteration as we cannot reasonably continue afterwards (the client and
161+
/// server are necessarily in different states).
162+
#[error("Sync protocol error: {desc}. {cause}")]
163+
SyncProtocolError {
164+
desc: &'static str,
165+
cause: PowerSyncErrorCause,
166+
},
167+
/// There's invalid local data in the database (like malformed JSON in the oplog table).
168+
#[error("invalid local data")]
169+
LocalDataError { cause: PowerSyncErrorCause },
126170
#[error("No client_id found in ps_kv")]
127171
MissingClientId,
128-
#[error("Invalid pending statement for raw table: {description}")]
129-
InvalidPendingStatement { description: Cow<'static, str> },
130172
#[error("Invalid bucket priority value")]
131173
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,
174+
#[error("Internal PowerSync error. {cause}")]
175+
Internal { cause: PowerSyncErrorCause },
176+
}
177+
178+
#[derive(Debug)]
179+
pub enum PowerSyncErrorCause {
180+
Json(serde_json::Error),
181+
Bson(BsonError),
182+
Unknown,
183+
}
184+
185+
impl From<serde_json::Error> for PowerSyncErrorCause {
186+
fn from(value: serde_json::Error) -> Self {
187+
return PowerSyncErrorCause::Json(value);
188+
}
189+
}
190+
191+
impl From<BsonError> for PowerSyncErrorCause {
192+
fn from(value: BsonError) -> Self {
193+
return PowerSyncErrorCause::Bson(value);
194+
}
195+
}
196+
197+
impl Display for PowerSyncErrorCause {
198+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199+
write!(f, "cause: ")?;
200+
201+
match self {
202+
PowerSyncErrorCause::Json(error) => error.fmt(f),
203+
PowerSyncErrorCause::Bson(error) => error.fmt(f),
204+
PowerSyncErrorCause::Unknown => write!(f, "unknown"),
205+
}
206+
}
145207
}

crates/core/src/json_merge.rs

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

1010
use crate::create_sqlite_text_fn;
11-
use crate::error::{PowerSyncError, RawPowerSyncError};
11+
use crate::error::PowerSyncError;
1212

1313
/// Given any number of JSON TEXT arguments, merge them into a single JSON object.
1414
///
@@ -25,7 +25,7 @@ fn powersync_json_merge_impl(
2525
for arg in args {
2626
let chunk = arg.text();
2727
if chunk.is_empty() || !chunk.starts_with('{') || !chunk.ends_with('}') {
28-
return Err(RawPowerSyncError::ExpectedJsonObject.into());
28+
return Err(PowerSyncError::argument_error("Expected json object"));
2929
}
3030

3131
// Strip outer braces

crates/core/src/migrations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use sqlite::ResultCode;
88
use sqlite_nostd as sqlite;
99
use sqlite_nostd::{Connection, Context};
1010

11-
use crate::error::{PowerSyncError, RawPowerSyncError};
11+
use crate::error::PowerSyncError;
1212
use crate::fix_data::apply_v035_fix;
1313
use crate::sync::BucketPriority;
1414

@@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS ps_migration(id INTEGER PRIMARY KEY, down_migrations
3131
local_db.prepare_v2("SELECT ifnull(max(id), 0) as version FROM ps_migration")?;
3232
let rc = current_version_stmt.step()?;
3333
if rc != ResultCode::ROW {
34-
return Err(PowerSyncError::from(RawPowerSyncError::Internal));
34+
return Err(PowerSyncError::unknown_internal());
3535
}
3636

3737
let mut current_version = current_version_stmt.column_int(0);

crates/core/src/operations.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ pub fn insert_operation(db: *mut sqlite::sqlite3, data: &str) -> Result<(), Powe
1717
buckets: Vec<DataLine<'a>>,
1818
}
1919

20-
let batch: BucketBatch = serde_json::from_str(data)?;
20+
let batch: BucketBatch =
21+
serde_json::from_str(data).map_err(PowerSyncError::json_argument_error)?;
2122
let adapter = StorageAdapter::new(db)?;
2223

2324
for line in &batch.buckets {

crates/core/src/schema/management.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ SELECT name, internal_name, local_only FROM powersync_tables WHERE name NOT IN (
132132

133133
fn update_indexes(db: *mut sqlite::sqlite3, schema: &str) -> Result<(), PowerSyncError> {
134134
let mut statements: Vec<String> = alloc::vec![];
135-
let schema = serde_json::from_str::<Schema>(schema)?;
135+
let schema =
136+
serde_json::from_str::<Schema>(schema).map_err(PowerSyncError::json_argument_error)?;
136137
let mut expected_index_names: Vec<String> = vec![];
137138

138139
{
@@ -205,7 +206,8 @@ SELECT
205206
AND sqlite_master.name NOT IN (SELECT value FROM json_each(?))
206207
",
207208
)?;
208-
let json_names = serde_json::to_string(&expected_index_names)?;
209+
let json_names =
210+
serde_json::to_string(&expected_index_names).map_err(PowerSyncError::internal)?;
209211
statement.bind_text(1, &json_names, sqlite::Destructor::STATIC)?;
210212

211213
while statement.step()? == ResultCode::ROW {

crates/core/src/sync/interface.rs

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

14-
use crate::error::{PowerSyncError, RawPowerSyncError};
14+
use crate::error::PowerSyncError;
1515
use crate::schema::Schema;
1616
use crate::state::DatabaseState;
1717

@@ -132,12 +132,12 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc<DatabaseState>) -> Result<(
132132
debug_assert!(!ctx.db_handle().get_autocommit());
133133

134134
let controller = unsafe { ctx.user_data().cast::<SqlController>().as_mut() }
135-
.ok_or_else(|| PowerSyncError::from(RawPowerSyncError::Internal))?;
135+
.ok_or_else(|| PowerSyncError::unknown_internal())?;
136136

137137
let args = sqlite::args!(argc, argv);
138138
let [op, payload] = args else {
139139
// This should be unreachable, we register the function with two arguments.
140-
return Err(PowerSyncError::from(RawPowerSyncError::Internal));
140+
return Err(PowerSyncError::unknown_internal());
141141
};
142142

143143
if op.value_type() != ColumnType::Text {
@@ -150,7 +150,8 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc<DatabaseState>) -> Result<(
150150
let event = match op {
151151
"start" => SyncControlRequest::StartSyncStream({
152152
if payload.value_type() == ColumnType::Text {
153-
serde_json::from_str(payload.text())?
153+
serde_json::from_str(payload.text())
154+
.map_err(PowerSyncError::json_argument_error)?
154155
} else {
155156
StartSyncStream::default()
156157
}
@@ -182,7 +183,8 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc<DatabaseState>) -> Result<(
182183
};
183184

184185
let instructions = controller.client.push_event(event)?;
185-
let formatted = serde_json::to_string(&instructions)?;
186+
let formatted =
187+
serde_json::to_string(&instructions).map_err(PowerSyncError::internal)?;
186188
ctx.result_text_transient(&formatted);
187189

188190
Ok(())

crates/core/src/sync/storage_adapter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@ impl StorageAdapter {
192192
};
193193

194194
// TODO: Avoid this serialization, it's currently used to bind JSON SQL parameters.
195-
let serialized_args = serde_json::to_string(&args)?;
195+
let serialized_args =
196+
serde_json::to_string(&args).map_err(PowerSyncError::internal)?;
196197
let mut sync = SyncOperation::new(
197198
state,
198199
self.db,

0 commit comments

Comments
 (0)