Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions crates/core/src/sync_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ impl<'a> SyncOperation<'a> {
let parsed: serde_json::Value = serde_json::from_str(data)
.map_err(PowerSyncError::json_local_error)?;
stmt.bind_for_put(id, &parsed)?;
stmt.stmt.exec()?;
stmt.exec(self.db, type_name, id, Some(&parsed))?;
}
Err(_) => {
let stmt = raw.delete_statement(self.db)?;
stmt.bind_for_delete(id)?;
stmt.stmt.exec()?;
stmt.exec(self.db, type_name, id, None)?;
}
}
} else {
Expand Down Expand Up @@ -598,4 +598,26 @@ impl<'a> PreparedPendingStatement<'a> {

Ok(())
}

/// Executes the prepared statement, contextualizing errors with the id / data that we've tried
/// to insert.
pub fn exec(
&self,
db: *mut sqlite::sqlite3,
table: &str,
id: &str,
data: Option<&serde_json::Value>,
) -> Result<(), PowerSyncError> {
match self.stmt.exec() {
Ok(_) => Ok(()),
Err(rc) => {
let context = match data {
None => format!("deleting from {table}, id = {id}"),
Some(data) => format!("replacing into {table}, id = {id}, data = {data}"),
};

Err(PowerSyncError::from_sqlite(db, rc, context))
}
}
}
}
171 changes: 119 additions & 52 deletions dart/test/sync_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1021,35 +1021,65 @@ END;
});

group('raw tables', () {
syncTest('smoke test', (_) {
db.execute(
'CREATE TABLE users (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL) STRICT;');

invokeControl(
'start',
json.encode({
'schema': {
'raw_tables': [
{
'name': 'users',
'put': {
'sql':
'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?);',
'params': [
'Id',
{'Column': 'name'}
],
},
'delete': {
'sql': 'DELETE FROM users WHERE id = ?',
'params': ['Id'],
},
}
const schema = {
'raw_tables': [
{
'name': 'users',
'put': {
'sql': 'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?);',
'params': [
'Id',
{'Column': 'name'}
],
'tables': [],
},
}),
);
'delete': {
'sql': 'DELETE FROM users WHERE id = ?',
'params': ['Id'],
},
}
],
'tables': [],
};

void setupRawTables() {
db.execute('''
CREATE TABLE users (id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL) STRICT;

CREATE TRIGGER users_insert
AFTER INSERT ON users
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'users', json_object(
'name', NEW.name
));
END;

CREATE TRIGGER users_update
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
SELECT CASE
WHEN (OLD.id != NEW.id)
THEN RAISE (FAIL, 'Cannot update id')
END;

INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'users', json_object(
'name', NEW.name
));
END;

CREATE TRIGGER users_delete
AFTER DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'users');
END;
''');
}

syncTest('smoke test', (_) {
setupRawTables();
invokeControl('start', json.encode({'schema': schema}));

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

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

invokeControl(
'start',
json.encode({
'schema': {
'raw_tables': [
{
'name': 'users',
'put': {
'sql': "INSERT INTO powersync_crud_(data) VALUES (?);",
'params': [
{'Column': 'name'}
],
},
'delete': {
'sql': 'DELETE FROM users WHERE id = ?',
'params': ['Id'],
},
}
],
'tables': [],
},
}),
pushCheckpoint(buckets: [bucketDescription('a')]);
pushSyncData(
'a',
'1',
'my_user',
'PUT',
{},
objectType: 'users',
);

expect(
pushCheckpointComplete,
throwsA(
isSqliteException(
1299,
'powersync_control: replacing into users, id = my_user, data = {}: '
'internal SQLite call returned CONSTRAINT_NOTNULL: '
'NOT NULL constraint failed: users.name',
),
),
);
});

test('crud vtab', () {
// This is mostly a test for the triggers, validating the suggestions we
// give on https://docs.powersync.com/usage/use-case-examples/raw-tables#capture-local-writes-with-triggers
setupRawTables();

db.execute('''
BEGIN;
INSERT INTO users (id, name) VALUES ('test-id', 'test user');
UPDATE users SET name = name || '2';
DELETE FROM users;
END;
''');

expect(db.select('SELECT * FROM ps_crud'), [
{
'id': 1,
'data':
'{"op":"PUT","id":"test-id","type":"users","data":{"name":"test user"}}',
'tx_id': 1
},
{
'id': 2,
'data':
'{"op":"PATCH","id":"test-id","type":"users","data":{"name":"test user2"}}',
'tx_id': 1
},
{
'id': 3,
'data': '{"op":"DELETE","id":"test-id","type":"users"}',
'tx_id': 1
},
]);
});

test("crud vtab is no-op during sync", () {
setupRawTables();
invokeControl('start', json.encode({'schema': schema}));

// Insert
pushCheckpoint(buckets: [bucketDescription('a')]);
pushSyncData(
Expand Down
Loading