Skip to content

Commit 410cd77

Browse files
authored
Merge pull request #66 from powersync-ja/skip-empty-updates
Add table option to skip empty updates
2 parents bf13bc1 + 0d51ce6 commit 410cd77

File tree

9 files changed

+114
-12
lines changed

9 files changed

+114
-12
lines changed

Cargo.lock

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ num-derive = "0.3"
2020
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
2121
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
2222
streaming-iterator = { version = "0.1.9", default-features = false, features = ["alloc"] }
23+
const_format = "0.2.34"
2324

2425
[dependencies.uuid]
2526
version = "1.4.1"

crates/core/src/crud_vtab.rs

+28-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ extern crate alloc;
22

33
use alloc::boxed::Box;
44
use alloc::string::String;
5+
use const_format::formatcp;
56
use core::ffi::{c_char, c_int, c_void};
67

78
use sqlite::{Connection, ResultCode, Value};
@@ -11,10 +12,11 @@ use sqlite_nostd::ResultCode::NULL;
1112

1213
use crate::error::SQLiteError;
1314
use crate::ext::SafeManagedStmt;
15+
use crate::schema::TableInfoFlags;
1416
use crate::vtab_util::*;
1517

1618
// Structure:
17-
// CREATE TABLE powersync_crud_(data TEXT);
19+
// CREATE TABLE powersync_crud_(data TEXT, options INT HIDDEN);
1820
//
1921
// This is a insert-only virtual table. It generates transaction ids in ps_tx, and inserts data in
2022
// ps_crud(tx_id, data).
@@ -39,7 +41,10 @@ extern "C" fn connect(
3941
vtab: *mut *mut sqlite::vtab,
4042
_err: *mut *mut c_char,
4143
) -> c_int {
42-
if let Err(rc) = sqlite::declare_vtab(db, "CREATE TABLE powersync_crud_(data TEXT);") {
44+
if let Err(rc) = sqlite::declare_vtab(
45+
db,
46+
"CREATE TABLE powersync_crud_(data TEXT, options INT HIDDEN);",
47+
) {
4348
return rc as c_int;
4449
}
4550

@@ -70,7 +75,16 @@ extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int {
7075
fn begin_impl(tab: &mut VirtualTable) -> Result<(), SQLiteError> {
7176
let db = tab.db;
7277

73-
let insert_statement = db.prepare_v3("INSERT INTO ps_crud(tx_id, data) VALUES (?1, ?2)", 0)?;
78+
const SQL: &str = formatcp!(
79+
"\
80+
WITH insertion (tx_id, data) AS (VALUES (?1, ?2))
81+
INSERT INTO ps_crud(tx_id, data)
82+
SELECT * FROM insertion WHERE (NOT (?3 & {})) OR data->>'op' != 'PATCH' OR data->'data' != '{{}}';
83+
",
84+
TableInfoFlags::IGNORE_EMPTY_UPDATE
85+
);
86+
87+
let insert_statement = db.prepare_v3(SQL, 0)?;
7488
tab.insert_statement = Some(insert_statement);
7589

7690
// language=SQLite
@@ -107,7 +121,11 @@ extern "C" fn rollback(vtab: *mut sqlite::vtab) -> c_int {
107121
ResultCode::OK as c_int
108122
}
109123

110-
fn insert_operation(vtab: *mut sqlite::vtab, data: &str) -> Result<(), SQLiteError> {
124+
fn insert_operation(
125+
vtab: *mut sqlite::vtab,
126+
data: &str,
127+
flags: TableInfoFlags,
128+
) -> Result<(), SQLiteError> {
111129
let tab = unsafe { &mut *(vtab.cast::<VirtualTable>()) };
112130
if tab.current_tx.is_none() {
113131
return Err(SQLiteError(
@@ -123,6 +141,7 @@ fn insert_operation(vtab: *mut sqlite::vtab, data: &str) -> Result<(), SQLiteErr
123141
.ok_or(SQLiteError::from(NULL))?;
124142
statement.bind_int64(1, current_tx)?;
125143
statement.bind_text(2, data, sqlite::Destructor::STATIC)?;
144+
statement.bind_int(3, flags.0 as i32)?;
126145
statement.exec()?;
127146

128147
Ok(())
@@ -144,7 +163,11 @@ extern "C" fn update(
144163
} else if rowid.value_type() == sqlite::ColumnType::Null {
145164
// INSERT
146165
let data = args[2].text();
147-
let result = insert_operation(vtab, data);
166+
let flags = match args[3].value_type() {
167+
sqlite_nostd::ColumnType::Null => TableInfoFlags::default(),
168+
_ => TableInfoFlags(args[3].int() as u32),
169+
};
170+
let result = insert_operation(vtab, data, flags);
148171
vtab_result(vtab, result)
149172
} else {
150173
// UPDATE - not supported

crates/core/src/diff.rs

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ extern crate alloc;
33
use alloc::format;
44
use alloc::string::{String, ToString};
55
use core::ffi::c_int;
6-
use core::slice;
76

87
use sqlite::ResultCode;
98
use sqlite_nostd as sqlite;

crates/core/src/schema/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ mod table_info;
33

44
use sqlite::ResultCode;
55
use sqlite_nostd as sqlite;
6-
pub use table_info::{ColumnInfo, ColumnNameAndTypeStatement, DiffIncludeOld, TableInfo};
6+
pub use table_info::{
7+
ColumnInfo, ColumnNameAndTypeStatement, DiffIncludeOld, TableInfo, TableInfoFlags,
8+
};
79

810
pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
911
management::register(db)

crates/core/src/schema/table_info.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ impl TableInfo {
2828
json_extract(?1, '$.insert_only'),
2929
json_extract(?1, '$.include_old'),
3030
json_extract(?1, '$.include_metadata'),
31-
json_extract(?1, '$.include_old_only_when_changed')",
31+
json_extract(?1, '$.include_old_only_when_changed'),
32+
json_extract(?1, '$.ignore_empty_update')",
3233
)?;
3334
statement.bind_text(1, data, sqlite::Destructor::STATIC)?;
3435

@@ -44,6 +45,7 @@ impl TableInfo {
4445
let insert_only = statement.column_int(3) != 0;
4546
let include_metadata = statement.column_int(5) != 0;
4647
let include_old_only_when_changed = statement.column_int(6) != 0;
48+
let ignore_empty_update = statement.column_int(7) != 0;
4749

4850
let mut flags = TableInfoFlags::default();
4951
flags = flags.set_flag(TableInfoFlags::LOCAL_ONLY, local_only);
@@ -53,7 +55,7 @@ impl TableInfo {
5355
TableInfoFlags::INCLUDE_OLD_ONLY_WHEN_CHANGED,
5456
include_old_only_when_changed,
5557
);
56-
58+
flags = flags.set_flag(TableInfoFlags::IGNORE_EMPTY_UPDATE, ignore_empty_update);
5759
flags
5860
};
5961

@@ -98,13 +100,14 @@ pub enum DiffIncludeOld {
98100

99101
#[derive(Clone, Copy)]
100102
#[repr(transparent)]
101-
pub struct TableInfoFlags(u32);
103+
pub struct TableInfoFlags(pub u32);
102104

103105
impl TableInfoFlags {
104106
pub const LOCAL_ONLY: u32 = 1;
105107
pub const INSERT_ONLY: u32 = 2;
106108
pub const INCLUDE_METADATA: u32 = 4;
107109
pub const INCLUDE_OLD_ONLY_WHEN_CHANGED: u32 = 8;
110+
pub const IGNORE_EMPTY_UPDATE: u32 = 16;
108111

109112
pub const fn local_only(self) -> bool {
110113
self.0 & Self::LOCAL_ONLY != 0

crates/core/src/views.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ fn powersync_trigger_update_sql_impl(
339339
""
340340
};
341341

342+
let flags = table_info.flags.0;
343+
342344
let trigger = format!("\
343345
CREATE TRIGGER {trigger_name}
344346
INSTEAD OF UPDATE ON {quoted_name}
@@ -351,7 +353,7 @@ BEGIN
351353
UPDATE {internal_name}
352354
SET data = {json_fragment_new}
353355
WHERE id = NEW.id;
354-
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff({:}, {:})){:}{:}));
356+
INSERT INTO powersync_crud_(data, options) VALUES(json_object('op', 'PATCH', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff({:}, {:})){:}{:}), {flags});
355357
INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES({type_string}, NEW.id);
356358
INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, {MAX_OP_ID});
357359
END", type_string, json_fragment_old, json_fragment_new, old_fragment, metadata_fragment);

dart/test/crud_test.dart

+45
Original file line numberDiff line numberDiff line change
@@ -473,5 +473,50 @@ void main() {
473473
expect(op['metadata'], 'custom delete');
474474
});
475475
});
476+
477+
test('includes empty updates by default', () {
478+
db
479+
..execute('select powersync_replace_schema(?)', [
480+
json.encode({
481+
'tables': [
482+
{
483+
'name': 'items',
484+
'columns': [
485+
{'name': 'col', 'type': 'text'}
486+
],
487+
}
488+
]
489+
})
490+
])
491+
..execute(
492+
'INSERT INTO items (id, col) VALUES (uuid(), ?)', ['new item'])
493+
..execute('UPDATE items SET col = LOWER(col)');
494+
495+
// Should record insert and update operation.
496+
expect(db.select('SELECT * FROM ps_crud'), hasLength(2));
497+
});
498+
499+
test('can ignore empty updates', () {
500+
db
501+
..execute('select powersync_replace_schema(?)', [
502+
json.encode({
503+
'tables': [
504+
{
505+
'name': 'items',
506+
'columns': [
507+
{'name': 'col', 'type': 'text'}
508+
],
509+
'ignore_empty_update': true,
510+
}
511+
]
512+
})
513+
])
514+
..execute(
515+
'INSERT INTO items (id, col) VALUES (uuid(), ?)', ['new item'])
516+
..execute('UPDATE items SET col = LOWER(col)');
517+
518+
// The update which didn't change any rows should not be recorded.
519+
expect(db.select('SELECT * FROM ps_crud'), hasLength(1));
520+
});
476521
});
477522
}

dart/test/utils/migration_fixtures.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ BEGIN
558558
UPDATE "ps_data__lists"
559559
SET data = json_object('description', NEW."description")
560560
WHERE id = NEW.id;
561-
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff(json_object('description', OLD."description"), json_object('description', NEW."description")))));
561+
INSERT INTO powersync_crud_(data, options) VALUES(json_object('op', 'PATCH', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff(json_object('description', OLD."description"), json_object('description', NEW."description")))), 0);
562562
INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES('lists', NEW.id);
563563
INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, 9223372036854775807);
564564
END

0 commit comments

Comments
 (0)