Skip to content

Commit 9bf55e8

Browse files
committed
Bring back db result utility
1 parent 7d326e4 commit 9bf55e8

File tree

6 files changed

+122
-36
lines changed

6 files changed

+122
-36
lines changed

crates/core/src/crud_vtab.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int {
329329
extern "C" fn begin(vtab: *mut sqlite::vtab) -> c_int {
330330
let tab = unsafe { &mut *(vtab.cast::<VirtualTable>()) };
331331
let result = tab.begin();
332-
vtab_result(vtab, tab.db, result)
332+
vtab_result(vtab, result)
333333
}
334334

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

crates/core/src/error.rs

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
use core::fmt::Display;
1+
use core::{error::Error, fmt::Display};
22

33
use alloc::{
44
borrow::Cow,
55
boxed::Box,
6-
format,
76
string::{String, ToString},
87
};
98
use sqlite_nostd::{context, sqlite3, Connection, Context, ResultCode};
@@ -15,16 +14,31 @@ use crate::bson::BsonError;
1514
///
1615
/// We allocate errors in boxes to avoid large [Result] types (given the large size of the
1716
/// [RawPowerSyncError] enum type).
17+
#[derive(Debug)]
1818
pub struct PowerSyncError {
1919
inner: Box<RawPowerSyncError>,
2020
}
2121

2222
impl PowerSyncError {
23-
pub fn from_sqlite(code: ResultCode, context: impl Into<Cow<'static, str>>) -> Self {
24-
RawPowerSyncError::Sqlite {
23+
fn errstr(db: *mut sqlite3) -> Option<String> {
24+
let message = db.errmsg().unwrap_or(String::from("Conversion error"));
25+
if message != "not an error" {
26+
Some(message)
27+
} else {
28+
None
29+
}
30+
}
31+
32+
pub fn from_sqlite(
33+
db: *mut sqlite3,
34+
code: ResultCode,
35+
context: impl Into<Cow<'static, str>>,
36+
) -> Self {
37+
RawPowerSyncError::Sqlite(SqliteError {
2538
code,
39+
errstr: Self::errstr(db),
2640
context: Some(context.into()),
27-
}
41+
})
2842
.into()
2943
}
3044

@@ -93,32 +107,21 @@ impl PowerSyncError {
93107
/// Applies this error to a function result context, setting the error code and a descriptive
94108
/// text.
95109
pub fn apply_to_ctx(self, description: &str, ctx: *mut context) {
96-
let mut desc = self.description(ctx.db_handle());
110+
let mut desc = self.to_string();
97111
desc.insert_str(0, description);
98112
desc.insert_str(description.len(), ": ");
99113

100114
ctx.result_error(&desc);
101115
ctx.result_error_code(self.sqlite_error_code());
102116
}
103117

104-
/// Obtains a description of this error, fetching it from SQLite if necessary.
105-
pub fn description(&self, db: *mut sqlite3) -> String {
106-
if let RawPowerSyncError::Sqlite { .. } = &*self.inner {
107-
let message = db.errmsg().unwrap_or(String::from("Conversion error"));
108-
if message != "not an error" {
109-
return format!("{}, caused by: {message}", self.inner);
110-
}
111-
}
112-
113-
self.inner.to_string()
114-
}
115-
116118
pub fn sqlite_error_code(&self) -> ResultCode {
117119
use RawPowerSyncError::*;
118120

119121
match self.inner.as_ref() {
120-
Sqlite { code, .. } => *code,
121-
ArgumentError { .. } | StateError { .. } => ResultCode::MISUSE,
122+
Sqlite(desc) => desc.code,
123+
ArgumentError { .. } => ResultCode::CONSTRAINT_DATATYPE,
124+
StateError { .. } => ResultCode::MISUSE,
122125
MissingClientId
123126
| SyncProtocolError { .. }
124127
| DownMigrationDidNotUpdateVersion { .. } => ResultCode::ABORT,
@@ -134,6 +137,8 @@ impl Display for PowerSyncError {
134137
}
135138
}
136139

140+
impl Error for PowerSyncError {}
141+
137142
impl From<RawPowerSyncError> for PowerSyncError {
138143
fn from(value: RawPowerSyncError) -> Self {
139144
return PowerSyncError {
@@ -144,10 +149,11 @@ impl From<RawPowerSyncError> for PowerSyncError {
144149

145150
impl From<ResultCode> for PowerSyncError {
146151
fn from(value: ResultCode) -> Self {
147-
return RawPowerSyncError::Sqlite {
152+
return RawPowerSyncError::Sqlite(SqliteError {
148153
code: value,
154+
errstr: None,
149155
context: None,
150-
}
156+
})
151157
.into();
152158
}
153159
}
@@ -164,11 +170,8 @@ enum RawPowerSyncError {
164170
/// [PowerSyncError::description] to create a detailed error message.
165171
///
166172
/// This error should _never_ be created for anything but rethrowing underlying SQLite errors.
167-
#[error("internal SQLite call returned {code}")]
168-
Sqlite {
169-
code: ResultCode,
170-
context: Option<Cow<'static, str>>,
171-
},
173+
#[error("{0}")]
174+
Sqlite(SqliteError),
172175
/// A user (e.g. the one calling a PowerSync function, likely an SDK) has provided invalid
173176
/// arguments.
174177
///
@@ -205,6 +208,45 @@ enum RawPowerSyncError {
205208
Internal { cause: PowerSyncErrorCause },
206209
}
207210

211+
#[derive(Debug)]
212+
struct SqliteError {
213+
code: ResultCode,
214+
errstr: Option<String>,
215+
context: Option<Cow<'static, str>>,
216+
}
217+
218+
impl Display for SqliteError {
219+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
220+
if let Some(context) = &self.context {
221+
write!(f, "{}: ", context)?;
222+
}
223+
224+
write!(f, "internal SQLite call returned {}", self.code)?;
225+
if let Some(desc) = &self.errstr {
226+
write!(f, ": {}", desc)?
227+
}
228+
229+
Ok(())
230+
}
231+
}
232+
233+
pub trait PSResult<T> {
234+
fn into_db_result(self, db: *mut sqlite3) -> Result<T, PowerSyncError>;
235+
}
236+
237+
impl<T> PSResult<T> for Result<T, ResultCode> {
238+
fn into_db_result(self, db: *mut sqlite3) -> Result<T, PowerSyncError> {
239+
self.map_err(|code| {
240+
RawPowerSyncError::Sqlite(SqliteError {
241+
code,
242+
errstr: PowerSyncError::errstr(db),
243+
context: None,
244+
})
245+
.into()
246+
})
247+
}
248+
}
249+
208250
#[derive(Debug)]
209251
pub enum PowerSyncErrorCause {
210252
Json(serde_json::Error),

crates/core/src/migrations.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ CREATE TABLE IF NOT EXISTS ps_migration(id INTEGER PRIMARY KEY, down_migrations
5656
let rs = local_db.exec_safe(&sql);
5757
if let Err(code) = rs {
5858
return Err(PowerSyncError::from_sqlite(
59+
local_db,
5960
code,
6061
format!(
6162
"Down migration failed for {:} {:} {:}",
@@ -74,6 +75,7 @@ CREATE TABLE IF NOT EXISTS ps_migration(id INTEGER PRIMARY KEY, down_migrations
7475
let rc = current_version_stmt.step()?;
7576
if rc != ResultCode::ROW {
7677
return Err(PowerSyncError::from_sqlite(
78+
local_db,
7779
rc,
7880
"Down migration failed - could not get version",
7981
));

crates/core/src/operations_vtab.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,24 @@ extern "C" fn update(
9292

9393
if op == "save" {
9494
let result = insert_operation(db, args[3].text());
95-
vtab_result(vtab, db, result)
95+
vtab_result(vtab, result)
9696
} else if op == "sync_local" {
9797
let result = sync_local(&tab.state, db, &args[3]);
9898
if let Ok(result_row) = result {
9999
unsafe {
100100
*p_row_id = result_row;
101101
}
102102
}
103-
vtab_result(vtab, db, result)
103+
vtab_result(vtab, result)
104104
} else if op == "clear_remove_ops" {
105105
let result = clear_remove_ops(db, args[3].text());
106-
vtab_result(vtab, db, result)
106+
vtab_result(vtab, result)
107107
} else if op == "delete_pending_buckets" {
108108
let result = delete_pending_buckets(db, args[3].text());
109-
vtab_result(vtab, db, result)
109+
vtab_result(vtab, result)
110110
} else if op == "delete_bucket" {
111111
let result: Result<(), ResultCode> = delete_bucket(db, args[3].text());
112-
vtab_result(vtab, db, result)
112+
vtab_result(vtab, result)
113113
} else {
114114
ResultCode::MISUSE as c_int
115115
}

crates/core/src/vtab_util.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
extern crate alloc;
22

3+
use alloc::string::ToString;
34
use core::ffi::{c_char, c_int};
45

56
use sqlite::ResultCode;
@@ -65,13 +66,12 @@ pub extern "C" fn vtab_no_close(_cursor: *mut sqlite::vtab_cursor) -> c_int {
6566

6667
pub fn vtab_result<T, E: Into<PowerSyncError>>(
6768
vtab: *mut sqlite::vtab,
68-
db: *mut sqlite::sqlite3,
6969
result: Result<T, E>,
7070
) -> c_int {
7171
if let Err(error) = result {
7272
let error = error.into();
7373

74-
vtab.set_err(&error.description(db));
74+
vtab.set_err(&error.to_string());
7575
error.sqlite_error_code() as c_int
7676
} else {
7777
ResultCode::OK as c_int

dart/test/error_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'dart:convert';
2+
3+
import 'package:sqlite3/common.dart';
4+
import 'package:test/test.dart';
5+
6+
import 'utils/native_test_utils.dart';
7+
8+
void main() {
9+
group('error reporting', () {
10+
late CommonDatabase db;
11+
12+
setUp(() async {
13+
db = openTestDatabase();
14+
});
15+
16+
tearDown(() {
17+
db.dispose();
18+
});
19+
20+
test('contain inner SQLite descriptions', () {
21+
expect(
22+
() => db.execute('SELECT powersync_replace_schema(?)', [
23+
json.encode({
24+
// This fails because we're trying to json_extract from the string
25+
// in e.g. update_tables.
26+
'tables': ['invalid entry'],
27+
})
28+
]),
29+
throwsA(isSqliteException(
30+
1,
31+
'powersync_replace_schema: internal SQLite call returned ERROR',
32+
)),
33+
);
34+
});
35+
});
36+
}
37+
38+
Matcher isSqliteException(int code, String message) {
39+
return isA<SqliteException>()
40+
.having((e) => e.extendedResultCode, 'extendedResultCode', code)
41+
.having((e) => e.message, 'message', message);
42+
}

0 commit comments

Comments
 (0)