Skip to content

Commit c923154

Browse files
committed
Improve quality of error messages for raw tables
1 parent e15ce73 commit c923154

File tree

2 files changed

+143
-54
lines changed

2 files changed

+143
-54
lines changed

crates/core/src/sync_local.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,12 @@ impl<'a> SyncOperation<'a> {
153153
let parsed: serde_json::Value = serde_json::from_str(data)
154154
.map_err(PowerSyncError::json_local_error)?;
155155
stmt.bind_for_put(id, &parsed)?;
156-
stmt.stmt.exec()?;
156+
stmt.exec(self.db, type_name, id, Some(&parsed))?;
157157
}
158158
Err(_) => {
159159
let stmt = raw.delete_statement(self.db)?;
160160
stmt.bind_for_delete(id)?;
161-
stmt.stmt.exec()?;
161+
stmt.exec(self.db, type_name, id, None)?;
162162
}
163163
}
164164
} else {
@@ -598,4 +598,26 @@ impl<'a> PreparedPendingStatement<'a> {
598598

599599
Ok(())
600600
}
601+
602+
/// Executes the prepared statement, contextualizing errors with the id / data that we've tried
603+
/// to insert.
604+
pub fn exec(
605+
&self,
606+
db: *mut sqlite::sqlite3,
607+
table: &str,
608+
id: &str,
609+
data: Option<&serde_json::Value>,
610+
) -> Result<(), PowerSyncError> {
611+
match self.stmt.exec() {
612+
Ok(_) => Ok(()),
613+
Err(rc) => {
614+
let context = match data {
615+
None => format!("deleting from {table}, id = {id}"),
616+
Some(data) => format!("replacing into {table}, id = {id}, data = {data}"),
617+
};
618+
619+
Err(PowerSyncError::from_sqlite(db, rc, context))
620+
}
621+
}
622+
}
601623
}

dart/test/sync_test.dart

Lines changed: 119 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,35 +1021,65 @@ END;
10211021
});
10221022

10231023
group('raw tables', () {
1024-
syncTest('smoke test', (_) {
1025-
db.execute(
1026-
'CREATE TABLE users (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL) STRICT;');
1027-
1028-
invokeControl(
1029-
'start',
1030-
json.encode({
1031-
'schema': {
1032-
'raw_tables': [
1033-
{
1034-
'name': 'users',
1035-
'put': {
1036-
'sql':
1037-
'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?);',
1038-
'params': [
1039-
'Id',
1040-
{'Column': 'name'}
1041-
],
1042-
},
1043-
'delete': {
1044-
'sql': 'DELETE FROM users WHERE id = ?',
1045-
'params': ['Id'],
1046-
},
1047-
}
1024+
const schema = {
1025+
'raw_tables': [
1026+
{
1027+
'name': 'users',
1028+
'put': {
1029+
'sql': 'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?);',
1030+
'params': [
1031+
'Id',
1032+
{'Column': 'name'}
10481033
],
1049-
'tables': [],
10501034
},
1051-
}),
1052-
);
1035+
'delete': {
1036+
'sql': 'DELETE FROM users WHERE id = ?',
1037+
'params': ['Id'],
1038+
},
1039+
}
1040+
],
1041+
'tables': [],
1042+
};
1043+
1044+
void setupRawTables() {
1045+
db.execute('''
1046+
CREATE TABLE users (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL) STRICT;
1047+
1048+
CREATE TRIGGER users_insert
1049+
AFTER INSERT ON users
1050+
FOR EACH ROW
1051+
BEGIN
1052+
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'users', json_object(
1053+
'name', NEW.name
1054+
));
1055+
END;
1056+
1057+
CREATE TRIGGER users_update
1058+
AFTER UPDATE ON users
1059+
FOR EACH ROW
1060+
BEGIN
1061+
SELECT CASE
1062+
WHEN (OLD.id != NEW.id)
1063+
THEN RAISE (FAIL, 'Cannot update id')
1064+
END;
1065+
1066+
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'users', json_object(
1067+
'name', NEW.name
1068+
));
1069+
END;
1070+
1071+
CREATE TRIGGER users_delete
1072+
AFTER DELETE ON users
1073+
FOR EACH ROW
1074+
BEGIN
1075+
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'users');
1076+
END;
1077+
''');
1078+
}
1079+
1080+
syncTest('smoke test', (_) {
1081+
setupRawTables();
1082+
invokeControl('start', json.encode({'schema': schema}));
10531083

10541084
// Insert
10551085
pushCheckpoint(buckets: [bucketDescription('a')]);
@@ -1083,34 +1113,71 @@ END;
10831113
expect(db.select('SELECT * FROM users'), isEmpty);
10841114
});
10851115

1086-
test("crud vtab is no-op during sync", () {
1087-
db.execute(
1088-
'CREATE TABLE users (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL) STRICT;');
1116+
test('reports errors from underlying statements', () {
1117+
setupRawTables();
1118+
invokeControl('start', json.encode({'schema': schema}));
10891119

1090-
invokeControl(
1091-
'start',
1092-
json.encode({
1093-
'schema': {
1094-
'raw_tables': [
1095-
{
1096-
'name': 'users',
1097-
'put': {
1098-
'sql': "INSERT INTO powersync_crud_(data) VALUES (?);",
1099-
'params': [
1100-
{'Column': 'name'}
1101-
],
1102-
},
1103-
'delete': {
1104-
'sql': 'DELETE FROM users WHERE id = ?',
1105-
'params': ['Id'],
1106-
},
1107-
}
1108-
],
1109-
'tables': [],
1110-
},
1111-
}),
1120+
pushCheckpoint(buckets: [bucketDescription('a')]);
1121+
pushSyncData(
1122+
'a',
1123+
'1',
1124+
'my_user',
1125+
'PUT',
1126+
{},
1127+
objectType: 'users',
11121128
);
11131129

1130+
expect(
1131+
pushCheckpointComplete,
1132+
throwsA(
1133+
isSqliteException(
1134+
1299,
1135+
'powersync_control: replacing into users, id = my_user, data = {}: '
1136+
'internal SQLite call returned CONSTRAINT_NOTNULL: '
1137+
'NOT NULL constraint failed: users.name',
1138+
),
1139+
),
1140+
);
1141+
});
1142+
1143+
test('crud vtab', () {
1144+
// This is mostly a test for the triggers, validating the suggestions we
1145+
// give on https://docs.powersync.com/usage/use-case-examples/raw-tables#capture-local-writes-with-triggers
1146+
setupRawTables();
1147+
1148+
db.execute('''
1149+
BEGIN;
1150+
INSERT INTO users (id, name) VALUES ('test-id', 'test user');
1151+
UPDATE users SET name = name || '2';
1152+
DELETE FROM users;
1153+
END;
1154+
''');
1155+
1156+
expect(db.select('SELECT * FROM ps_crud'), [
1157+
{
1158+
'id': 1,
1159+
'data':
1160+
'{"op":"PUT","id":"test-id","type":"users","data":{"name":"test user"}}',
1161+
'tx_id': 1
1162+
},
1163+
{
1164+
'id': 2,
1165+
'data':
1166+
'{"op":"PATCH","id":"test-id","type":"users","data":{"name":"test user2"}}',
1167+
'tx_id': 1
1168+
},
1169+
{
1170+
'id': 3,
1171+
'data': '{"op":"DELETE","id":"test-id","type":"users"}',
1172+
'tx_id': 1
1173+
},
1174+
]);
1175+
});
1176+
1177+
test("crud vtab is no-op during sync", () {
1178+
setupRawTables();
1179+
invokeControl('start', json.encode({'schema': schema}));
1180+
11141181
// Insert
11151182
pushCheckpoint(buckets: [bucketDescription('a')]);
11161183
pushSyncData(

0 commit comments

Comments
 (0)