Skip to content

Commit 0aa3e7f

Browse files
Merge remote-tracking branch 'origin/main' into fix/add-dysm-for-frameworks
2 parents 681ca99 + 87b638b commit 0aa3e7f

File tree

5 files changed

+404
-48
lines changed

5 files changed

+404
-48
lines changed

Diff for: crates/core/src/diff.rs

+49-16
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
extern crate alloc;
22

3-
43
use alloc::format;
54
use alloc::string::{String, ToString};
65
use core::ffi::c_int;
76
use core::slice;
87

9-
use sqlite::{ResultCode};
8+
use sqlite::ResultCode;
109
use sqlite_nostd as sqlite;
1110
use sqlite_nostd::{Connection, Context, Value};
1211

@@ -26,7 +25,6 @@ fn powersync_diff_impl(
2625
}
2726

2827
pub fn diff_objects(data_old: &str, data_new: &str) -> Result<String, SQLiteError> {
29-
3028
let v_new: json::Value = json::from_str(data_new)?;
3129
let v_old: json::Value = json::from_str(data_old)?;
3230

@@ -81,7 +79,6 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
8179
Ok(())
8280
}
8381

84-
8582
#[cfg(test)]
8683
mod tests {
8784
use super::*;
@@ -91,17 +88,53 @@ mod tests {
9188
assert_eq!(diff_objects("{}", "{}").unwrap(), "{}");
9289
assert_eq!(diff_objects(r#"{"a": null}"#, "{}").unwrap(), "{}");
9390
assert_eq!(diff_objects(r#"{}"#, r#"{"a": null}"#).unwrap(), "{}");
94-
assert_eq!(diff_objects(r#"{"b": 1}"#, r#"{"a": null, "b": 1}"#).unwrap(), "{}");
95-
assert_eq!(diff_objects(r#"{"b": 1}"#, r#"{"a": null, "b": 2}"#).unwrap(), r#"{"b":2}"#);
96-
assert_eq!(diff_objects(r#"{"a": 0, "b": 1}"#, r#"{"a": null, "b": 2}"#).unwrap(), r#"{"a":null,"b":2}"#);
97-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{"a": null}"#).unwrap(), r#"{"a":null}"#);
98-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{}"#).unwrap(), r#"{"a":null}"#);
99-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{"a": 2}"#).unwrap(), r#"{"a":2}"#);
100-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{"a": "1"}"#).unwrap(), r#"{"a":"1"}"#);
101-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{"a": 1.0}"#).unwrap(), r#"{"a":1.0}"#);
102-
assert_eq!(diff_objects(r#"{"a": 1.00}"#, r#"{"a": 1.0}"#).unwrap(), r#"{}"#);
103-
assert_eq!(diff_objects(r#"{}"#, r#"{"a": 1.0}"#).unwrap(), r#"{"a":1.0}"#);
104-
assert_eq!(diff_objects(r#"{}"#, r#"{"a": [1,2,3]}"#).unwrap(), r#"{"a":[1,2,3]}"#);
105-
assert_eq!(diff_objects(r#"{"a": 1}"#, r#"{"a": [1,2,3]}"#).unwrap(), r#"{"a":[1,2,3]}"#);
91+
assert_eq!(
92+
diff_objects(r#"{"b": 1}"#, r#"{"a": null, "b": 1}"#).unwrap(),
93+
"{}"
94+
);
95+
assert_eq!(
96+
diff_objects(r#"{"b": 1}"#, r#"{"a": null, "b": 2}"#).unwrap(),
97+
r#"{"b":2}"#
98+
);
99+
assert_eq!(
100+
diff_objects(r#"{"a": 0, "b": 1}"#, r#"{"a": null, "b": 2}"#).unwrap(),
101+
r#"{"a":null,"b":2}"#
102+
);
103+
assert_eq!(
104+
diff_objects(r#"{"a": 1}"#, r#"{"a": null}"#).unwrap(),
105+
r#"{"a":null}"#
106+
);
107+
assert_eq!(
108+
diff_objects(r#"{"a": 1}"#, r#"{}"#).unwrap(),
109+
r#"{"a":null}"#
110+
);
111+
assert_eq!(
112+
diff_objects(r#"{"a": 1}"#, r#"{"a": 2}"#).unwrap(),
113+
r#"{"a":2}"#
114+
);
115+
assert_eq!(
116+
diff_objects(r#"{"a": 1}"#, r#"{"a": "1"}"#).unwrap(),
117+
r#"{"a":"1"}"#
118+
);
119+
assert_eq!(
120+
diff_objects(r#"{"a": 1}"#, r#"{"a": 1.0}"#).unwrap(),
121+
r#"{"a":1.0}"#
122+
);
123+
assert_eq!(
124+
diff_objects(r#"{"a": 1.00}"#, r#"{"a": 1.0}"#).unwrap(),
125+
r#"{}"#
126+
);
127+
assert_eq!(
128+
diff_objects(r#"{}"#, r#"{"a": 1.0}"#).unwrap(),
129+
r#"{"a":1.0}"#
130+
);
131+
assert_eq!(
132+
diff_objects(r#"{}"#, r#"{"a": [1,2,3]}"#).unwrap(),
133+
r#"{"a":[1,2,3]}"#
134+
);
135+
assert_eq!(
136+
diff_objects(r#"{"a": 1}"#, r#"{"a": [1,2,3]}"#).unwrap(),
137+
r#"{"a":[1,2,3]}"#
138+
);
106139
}
107140
}

Diff for: crates/core/src/json_merge.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
extern crate alloc;
2+
3+
use alloc::format;
4+
use alloc::string::{String, ToString};
5+
use core::ffi::c_int;
6+
use core::slice;
7+
8+
use sqlite::ResultCode;
9+
use sqlite_nostd as sqlite;
10+
use sqlite_nostd::{Connection, Context, Value};
11+
12+
use crate::create_sqlite_text_fn;
13+
use crate::error::SQLiteError;
14+
15+
/// Given any number of JSON TEXT arguments, merge them into a single JSON object.
16+
///
17+
/// This assumes each argument is a valid JSON object, with no duplicate keys.
18+
/// No JSON parsing or validation is performed - this performs simple string concatenation.
19+
fn powersync_json_merge_impl(
20+
_ctx: *mut sqlite::context,
21+
args: &[*mut sqlite::value],
22+
) -> Result<String, SQLiteError> {
23+
if args.is_empty() {
24+
return Ok("{}".to_string());
25+
}
26+
let mut result = String::from("{");
27+
for arg in args {
28+
let chunk = arg.text();
29+
if chunk.is_empty() || !chunk.starts_with('{') || !chunk.ends_with('}') {
30+
return Err(SQLiteError::from(ResultCode::MISMATCH));
31+
}
32+
33+
// Strip outer braces
34+
let inner = &chunk[1..(chunk.len() - 1)];
35+
36+
// If this is not the first chunk, insert a comma
37+
if result.len() > 1 {
38+
result.push(',');
39+
}
40+
41+
// Append the inner content
42+
result.push_str(inner);
43+
}
44+
45+
// Close the outer brace
46+
result.push('}');
47+
Ok(result)
48+
}
49+
50+
create_sqlite_text_fn!(
51+
powersync_json_merge,
52+
powersync_json_merge_impl,
53+
"powersync_json_merge"
54+
);
55+
56+
pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
57+
db.create_function_v2(
58+
"powersync_json_merge",
59+
-1,
60+
sqlite::UTF8 | sqlite::DETERMINISTIC,
61+
None,
62+
Some(powersync_json_merge),
63+
None,
64+
None,
65+
None,
66+
)?;
67+
68+
Ok(())
69+
}

Diff for: crates/core/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod diff;
1818
mod error;
1919
mod ext;
2020
mod fix035;
21+
mod json_merge;
2122
mod kv;
2223
mod macros;
2324
mod migrations;
@@ -55,6 +56,7 @@ fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
5556
crate::views::register(db)?;
5657
crate::uuid::register(db)?;
5758
crate::diff::register(db)?;
59+
crate::json_merge::register(db)?;
5860
crate::view_admin::register(db)?;
5961
crate::checkpoint::register(db)?;
6062
crate::kv::register(db)?;

Diff for: crates/core/src/views.rs

+54-32
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use core::ffi::c_int;
77
use core::slice;
88

99
use sqlite::{Connection, Context, ResultCode, Value};
10-
use sqlite_nostd as sqlite;
10+
use sqlite_nostd::{self as sqlite, ManagedStmt};
1111

1212
use crate::create_sqlite_text_fn;
1313
use crate::error::{PSResult, SQLiteError};
@@ -143,16 +143,7 @@ fn powersync_trigger_insert_sql_impl(
143143
let local_db = ctx.db_handle();
144144
let stmt2 = local_db.prepare_v2("select json_extract(e.value, '$.name') as name from json_each(json_extract(?, '$.columns')) e")?;
145145
stmt2.bind_text(1, table, sqlite::Destructor::STATIC)?;
146-
147-
let mut column_names_quoted: Vec<String> = alloc::vec![];
148-
while stmt2.step()? == ResultCode::ROW {
149-
let name = stmt2.column_text(0)?;
150-
151-
let foo: String = format!("{:}, NEW.{:}", quote_string(name), quote_identifier(name));
152-
column_names_quoted.push(foo);
153-
}
154-
155-
let json_fragment = column_names_quoted.join(", ");
146+
let json_fragment = json_object_fragment("NEW", &stmt2)?;
156147

157148
return if !local_only && !insert_only {
158149
let trigger = format!("\
@@ -165,8 +156,8 @@ fn powersync_trigger_insert_sql_impl(
165156
THEN RAISE (FAIL, 'id is required')
166157
END;
167158
INSERT INTO {:}
168-
SELECT NEW.id, json_object({:});
169-
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff('{{}}', json_object({:})))));
159+
SELECT NEW.id, {:};
160+
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff('{{}}', {:}))));
170161
INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES({:}, NEW.id);
171162
INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, {:});
172163
END", trigger_name, quoted_name, internal_name, json_fragment, type_string, json_fragment, type_string, MAX_OP_ID);
@@ -178,7 +169,7 @@ fn powersync_trigger_insert_sql_impl(
178169
INSTEAD OF INSERT ON {:}
179170
FOR EACH ROW
180171
BEGIN
181-
INSERT INTO {:} SELECT NEW.id, json_object({:});
172+
INSERT INTO {:} SELECT NEW.id, {:};
182173
END",
183174
trigger_name, quoted_name, internal_name, json_fragment
184175
);
@@ -189,7 +180,7 @@ fn powersync_trigger_insert_sql_impl(
189180
INSTEAD OF INSERT ON {:}
190181
FOR EACH ROW
191182
BEGIN
192-
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', {}, 'id', NEW.id, 'data', json(powersync_diff('{{}}', json_object({:})))));
183+
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', {}, 'id', NEW.id, 'data', json(powersync_diff('{{}}', {:}))));
193184
END", trigger_name, quoted_name, type_string, json_fragment);
194185
Ok(trigger)
195186
} else {
@@ -224,20 +215,9 @@ fn powersync_trigger_update_sql_impl(
224215
let db = ctx.db_handle();
225216
let stmt2 = db.prepare_v2("select json_extract(e.value, '$.name') as name from json_each(json_extract(?, '$.columns')) e").into_db_result(db)?;
226217
stmt2.bind_text(1, table, sqlite::Destructor::STATIC)?;
227-
228-
let mut column_names_quoted_new: Vec<String> = alloc::vec![];
229-
let mut column_names_quoted_old: Vec<String> = alloc::vec![];
230-
while stmt2.step()? == ResultCode::ROW {
231-
let name = stmt2.column_text(0)?;
232-
233-
let foo_new: String = format!("{:}, NEW.{:}", quote_string(name), quote_identifier(name));
234-
column_names_quoted_new.push(foo_new);
235-
let foo_old: String = format!("{:}, OLD.{:}", quote_string(name), quote_identifier(name));
236-
column_names_quoted_old.push(foo_old);
237-
}
238-
239-
let json_fragment_new = column_names_quoted_new.join(", ");
240-
let json_fragment_old = column_names_quoted_old.join(", ");
218+
let json_fragment_new = json_object_fragment("NEW", &stmt2)?;
219+
stmt2.reset()?;
220+
let json_fragment_old = json_object_fragment("OLD", &stmt2)?;
241221

242222
return if !local_only && !insert_only {
243223
let trigger = format!("\
@@ -250,9 +230,9 @@ BEGIN
250230
THEN RAISE (FAIL, 'Cannot update id')
251231
END;
252232
UPDATE {:}
253-
SET data = json_object({:})
233+
SET data = {:}
254234
WHERE id = NEW.id;
255-
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff(json_object({:}), json_object({:})))));
235+
INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', {:}, 'id', NEW.id, 'data', json(powersync_diff({:}, {:}))));
256236
INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES({:}, NEW.id);
257237
INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, {:});
258238
END", trigger_name, quoted_name, internal_name, json_fragment_new, type_string, json_fragment_old, json_fragment_new, type_string, MAX_OP_ID);
@@ -269,7 +249,7 @@ BEGIN
269249
THEN RAISE (FAIL, 'Cannot update id')
270250
END;
271251
UPDATE {:}
272-
SET data = json_object({:})
252+
SET data = {:}
273253
WHERE id = NEW.id;
274254
END",
275255
trigger_name, quoted_name, internal_name, json_fragment_new
@@ -335,3 +315,45 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
335315

336316
Ok(())
337317
}
318+
319+
/// Given a query returning column names, return a JSON object fragment for a trigger.
320+
///
321+
/// Example output with prefix "NEW": "json_object('id', NEW.id, 'name', NEW.name, 'age', NEW.age)".
322+
fn json_object_fragment(prefix: &str, name_results: &ManagedStmt) -> Result<String, SQLiteError> {
323+
// floor(SQLITE_MAX_FUNCTION_ARG / 2).
324+
// To keep databases portable, we use the default limit of 100 args for this,
325+
// and don't try to query the limit dynamically.
326+
const MAX_ARG_COUNT: usize = 50;
327+
328+
let mut column_names_quoted: Vec<String> = alloc::vec![];
329+
while name_results.step()? == ResultCode::ROW {
330+
let name = name_results.column_text(0)?;
331+
332+
let quoted: String = format!(
333+
"{:}, {:}.{:}",
334+
quote_string(name),
335+
prefix,
336+
quote_identifier(name)
337+
);
338+
column_names_quoted.push(quoted);
339+
}
340+
341+
// SQLITE_MAX_COLUMN - 1 (because of the id column)
342+
if column_names_quoted.len() > 1999 {
343+
return Err(SQLiteError::from(ResultCode::TOOBIG));
344+
} else if column_names_quoted.len() <= MAX_ARG_COUNT {
345+
// Small number of columns - use json_object() directly.
346+
let json_fragment = column_names_quoted.join(", ");
347+
return Ok(format!("json_object({:})", json_fragment));
348+
} else {
349+
// Too many columns to use json_object directly.
350+
// Instead, we build up the JSON object in chunks,
351+
// and merge using powersync_json_merge().
352+
let mut fragments: Vec<String> = alloc::vec![];
353+
for chunk in column_names_quoted.chunks(MAX_ARG_COUNT) {
354+
let sub_fragment = chunk.join(", ");
355+
fragments.push(format!("json_object({:})", sub_fragment));
356+
}
357+
return Ok(format!("powersync_json_merge({:})", fragments.join(", ")));
358+
}
359+
}

0 commit comments

Comments
 (0)