Skip to content

Commit 0ae8d98

Browse files
committed
Add local-only columns
1 parent 2570248 commit 0ae8d98

File tree

4 files changed

+72
-7
lines changed

4 files changed

+72
-7
lines changed

crates/core/src/schema/common.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use core::slice;
22

33
use alloc::{string::String, vec::Vec};
4+
use serde::Deserialize;
45

56
use crate::schema::{
67
Column, CommonTableOptions, RawTable, Table, raw_table::InferredTableStructure,
@@ -61,6 +62,7 @@ impl<'a> Iterator for SchemaTableColumnIterator<'a> {
6162
}
6263
}
6364

65+
#[derive(Default)]
6466
pub struct ColumnFilter {
6567
sorted_names: Vec<String>,
6668
}
@@ -82,3 +84,18 @@ impl ColumnFilter {
8284
.is_ok()
8385
}
8486
}
87+
88+
impl AsRef<Vec<String>> for ColumnFilter {
89+
fn as_ref(&self) -> &Vec<String> {
90+
&self.sorted_names
91+
}
92+
}
93+
94+
impl<'de> Deserialize<'de> for ColumnFilter {
95+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96+
where
97+
D: serde::Deserializer<'de>,
98+
{
99+
Ok(Self::from(Vec::<String>::deserialize(deserializer)?))
100+
}
101+
}

crates/core/src/schema/raw_table.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use core::fmt::{self, Formatter, from_fn};
1+
use core::fmt::{self, Formatter, Write, from_fn};
22

33
use alloc::{
44
format,
@@ -10,7 +10,7 @@ use powersync_sqlite_nostd::{Connection, Destructor, ResultCode};
1010

1111
use crate::{
1212
error::PowerSyncError,
13-
schema::{RawTable, SchemaTable},
13+
schema::{ColumnFilter, RawTable, SchemaTable},
1414
utils::{InsertIntoCrud, SqlBuffer, WriteType},
1515
views::table_columns_to_json_object,
1616
};
@@ -23,6 +23,7 @@ impl InferredTableStructure {
2323
pub fn read_from_database(
2424
table_name: &str,
2525
db: impl Connection,
26+
ignored_local_columns: &ColumnFilter,
2627
) -> Result<Option<Self>, PowerSyncError> {
2728
let stmt = db.prepare_v2("select name from pragma_table_info(?)")?;
2829
stmt.bind_text(1, table_name, Destructor::STATIC)?;
@@ -34,7 +35,7 @@ impl InferredTableStructure {
3435
let name = stmt.column_text(0)?;
3536
if name == "id" {
3637
has_id_column = true;
37-
} else {
38+
} else if !ignored_local_columns.matches(name) {
3839
columns.push(name.to_string());
3940
}
4041
}
@@ -63,7 +64,9 @@ pub fn generate_raw_table_trigger(
6364
return Err(PowerSyncError::argument_error("Table has no local name"));
6465
};
6566

66-
let Some(resolved_table) = InferredTableStructure::read_from_database(local_table_name, db)?
67+
let local_only_columns = &table.schema.local_only_columns;
68+
let Some(resolved_table) =
69+
InferredTableStructure::read_from_database(local_table_name, db, local_only_columns)?
6770
else {
6871
return Err(PowerSyncError::argument_error(format!(
6972
"Could not find {} in local schema",
@@ -80,7 +83,27 @@ pub fn generate_raw_table_trigger(
8083
buffer.create_trigger("", trigger_name);
8184
buffer.trigger_after(write, local_table_name);
8285
// Skip the trigger for writes during sync_local, these aren't crud writes.
83-
buffer.push_str("WHEN NOT powersync_in_sync_operation() BEGIN\n");
86+
buffer.push_str("WHEN NOT powersync_in_sync_operation()");
87+
88+
if write == WriteType::Update && !local_only_columns.as_ref().is_empty() {
89+
buffer.push_str(" AND\n(");
90+
// If we have local-only columns, we want to add additional WHEN clauses to ensure the
91+
// trigger runs for updates on synced columns.
92+
for (i, name) in as_schema_table.column_names().enumerate() {
93+
if i != 0 {
94+
buffer.push_str(" OR ");
95+
}
96+
97+
// Generate OLD."column" IS NOT NEW."column"
98+
buffer.push_str("OLD.");
99+
let _ = buffer.identifier().write_str(name);
100+
buffer.push_str(" IS NOT NEW.");
101+
let _ = buffer.identifier().write_str(name);
102+
}
103+
buffer.push_str(")");
104+
}
105+
106+
buffer.push_str(" BEGIN\n");
84107

85108
if table.schema.options.flags.insert_only() {
86109
if write != WriteType::Insert {

crates/core/src/schema/table_info.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub struct RawTableSchema {
3737
/// Currently, this is only used to generate `CREATE TRIGGER` statements for the raw table.
3838
#[serde(default)]
3939
pub table_name: Option<String>,
40+
#[serde(default)]
41+
pub local_only_columns: ColumnFilter,
4042
#[serde(flatten)]
4143
pub options: CommonTableOptions,
4244
}

dart/test/schema_test.dart

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,29 @@ END''',
280280
delete: '''
281281
CREATE TRIGGER "test_delete" AFTER DELETE ON "users" FOR EACH ROW WHEN NOT powersync_in_sync_operation() BEGIN
282282
INSERT INTO powersync_crud(op,id,type,old_values) VALUES ('DELETE', OLD.id, 'sync_type', json_object('email', powersync_strip_subtype(OLD."email"), 'email_verified', powersync_strip_subtype(OLD."email_verified")));
283+
END''',
284+
),
285+
// Local-only columns, should not be included in ps_crud.
286+
_RawTableTestCase(
287+
createTable:
288+
'CREATE TABLE users (id TEXT, synced_a TEXT, synced_b TEXT, local_a TEXT, local_b TEXT);',
289+
tableOptions: {
290+
'table_name': 'users',
291+
'local_only_columns': ['local_a', 'local_b'],
292+
},
293+
insert: '''
294+
CREATE TRIGGER "test_insert" AFTER INSERT ON "users" FOR EACH ROW WHEN NOT powersync_in_sync_operation() BEGIN
295+
INSERT INTO powersync_crud(op,id,type,data) VALUES ('PUT', NEW.id, 'sync_type', json(powersync_diff('{}', json_object('synced_a', powersync_strip_subtype(NEW."synced_a"), 'synced_b', powersync_strip_subtype(NEW."synced_b")))));
296+
END''',
297+
update: '''
298+
CREATE TRIGGER "test_update" AFTER UPDATE ON "users" FOR EACH ROW WHEN NOT powersync_in_sync_operation() AND
299+
(OLD."synced_a" IS NOT NEW."synced_a" OR OLD."synced_b" IS NOT NEW."synced_b") BEGIN
300+
SELECT CASE WHEN (OLD.id != NEW.id) THEN RAISE (FAIL, 'Cannot update id') END;
301+
INSERT INTO powersync_crud(op,id,type,data,options) VALUES ('PATCH', NEW.id, 'sync_type', json(powersync_diff(json_object('synced_a', powersync_strip_subtype(OLD."synced_a"), 'synced_b', powersync_strip_subtype(OLD."synced_b")), json_object('synced_a', powersync_strip_subtype(NEW."synced_a"), 'synced_b', powersync_strip_subtype(NEW."synced_b")))), 0);
302+
END''',
303+
delete: '''
304+
CREATE TRIGGER "test_delete" AFTER DELETE ON "users" FOR EACH ROW WHEN NOT powersync_in_sync_operation() BEGIN
305+
INSERT INTO powersync_crud(op,id,type) VALUES ('DELETE', OLD.id, 'sync_type');
283306
END''',
284307
),
285308
];
@@ -330,9 +353,9 @@ SELECT
330353
db.select("SELECT name, sql FROM sqlite_schema WHERE type = 'trigger'");
331354

332355
// Uncomment to help update expectations
333-
// for (final row in foundTriggers) {
356+
//for (final row in foundTriggers) {
334357
// print(row['sql']);
335-
// }
358+
//}
336359

337360
expect(foundTriggers, [
338361
{'name': 'test_insert', 'sql': insert},

0 commit comments

Comments
 (0)