diff --git a/Cargo.lock b/Cargo.lock index cc2b9a4..510abc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,7 +242,7 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "powersync_core" -version = "0.4.3" +version = "0.4.4" dependencies = [ "bytes", "const_format", @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "powersync_loadable" -version = "0.4.3" +version = "0.4.4" dependencies = [ "powersync_core", "sqlite_nostd", @@ -267,7 +267,7 @@ dependencies = [ [[package]] name = "powersync_sqlite" -version = "0.4.3" +version = "0.4.4" dependencies = [ "cc", "powersync_core", @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "powersync_static" -version = "0.4.3" +version = "0.4.4" dependencies = [ "powersync_core", "sqlite_nostd", @@ -397,7 +397,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "sqlite3" -version = "0.4.3" +version = "0.4.4" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index 7bd4057..c9782c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ inherits = "release" inherits = "wasm" [workspace.package] -version = "0.4.3" +version = "0.4.4" edition = "2024" authors = ["JourneyApps"] keywords = ["sqlite", "powersync"] @@ -38,4 +38,3 @@ repository = "https://github.com/powersync-ja/powersync-sqlite-core" [workspace.dependencies] sqlite_nostd = { path="./sqlite-rs-embedded/sqlite_nostd" } - diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 14f033a..c9d1043 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -13,7 +13,7 @@ plugins { } group = "com.powersync" -version = "0.4.3" +version = "0.4.4" description = "PowerSync Core SQLite Extension" val localRepo = uri("build/repository/") diff --git a/android/src/prefab/prefab.json b/android/src/prefab/prefab.json index c934036..08eafd1 100644 --- a/android/src/prefab/prefab.json +++ b/android/src/prefab/prefab.json @@ -2,5 +2,5 @@ "name": "powersync_sqlite_core", "schema_version": 2, "dependencies": [], - "version": "0.4.3" + "version": "0.4.4" } diff --git a/crates/core/src/constants.rs b/crates/core/src/constants.rs index 3a9ed3d..7eff17a 100644 --- a/crates/core/src/constants.rs +++ b/crates/core/src/constants.rs @@ -9,6 +9,8 @@ pub const FULL_GIT_HASH: &'static str = env!("GIT_HASH"); // we're testing with the minimum version we claim to support. pub const MIN_SQLITE_VERSION_NUMBER: c_int = 3044000; +pub const SUBTYPE_JSON: u32 = 'J' as u32; + pub fn short_git_hash() -> &'static str { &FULL_GIT_HASH[..8] } diff --git a/crates/core/src/diff.rs b/crates/core/src/diff.rs index a32accf..f08efad 100644 --- a/crates/core/src/diff.rs +++ b/crates/core/src/diff.rs @@ -7,18 +7,20 @@ use sqlite::ResultCode; use sqlite_nostd as sqlite; use sqlite_nostd::{Connection, Context, Value}; -use serde_json as json; - +use crate::constants::SUBTYPE_JSON; use crate::create_sqlite_text_fn; use crate::error::PowerSyncError; +use serde_json as json; +use sqlite_nostd::bindings::SQLITE_RESULT_SUBTYPE; fn powersync_diff_impl( - _ctx: *mut sqlite::context, + ctx: *mut sqlite::context, args: &[*mut sqlite::value], ) -> Result { let data_old = args[0].text(); let data_new = args[1].text(); + ctx.result_subtype(SUBTYPE_JSON); diff_objects(data_old, data_new) } @@ -66,7 +68,7 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { db.create_function_v2( "powersync_diff", 2, - sqlite::UTF8 | sqlite::DETERMINISTIC, + sqlite::UTF8 | sqlite::DETERMINISTIC | SQLITE_RESULT_SUBTYPE, None, Some(powersync_diff), None, diff --git a/crates/core/src/json_merge.rs b/crates/core/src/json_util.rs similarity index 68% rename from crates/core/src/json_merge.rs rename to crates/core/src/json_util.rs index 273c88c..f3f3c1e 100644 --- a/crates/core/src/json_merge.rs +++ b/crates/core/src/json_util.rs @@ -3,19 +3,34 @@ extern crate alloc; use alloc::string::{String, ToString}; use core::ffi::c_int; +use crate::constants::SUBTYPE_JSON; +use crate::create_sqlite_text_fn; +use crate::error::PowerSyncError; use sqlite::ResultCode; use sqlite_nostd as sqlite; +use sqlite_nostd::bindings::{SQLITE_RESULT_SUBTYPE, SQLITE_SUBTYPE}; use sqlite_nostd::{Connection, Context, Value}; -use crate::create_sqlite_text_fn; -use crate::error::PowerSyncError; +extern "C" fn powersync_strip_subtype( + ctx: *mut sqlite::context, + argc: c_int, + argv: *mut *mut sqlite::value, +) { + if argc != 1 { + return; + } + + let arg = unsafe { *argv }; + ctx.result_value(arg); + ctx.result_subtype(0); +} /// Given any number of JSON TEXT arguments, merge them into a single JSON object. /// /// This assumes each argument is a valid JSON object, with no duplicate keys. /// No JSON parsing or validation is performed - this performs simple string concatenation. fn powersync_json_merge_impl( - _ctx: *mut sqlite::context, + ctx: *mut sqlite::context, args: &[*mut sqlite::value], ) -> Result { if args.is_empty() { @@ -42,6 +57,7 @@ fn powersync_json_merge_impl( // Close the outer brace result.push('}'); + ctx.result_subtype(SUBTYPE_JSON); Ok(result) } @@ -55,7 +71,7 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { db.create_function_v2( "powersync_json_merge", -1, - sqlite::UTF8 | sqlite::DETERMINISTIC, + sqlite::UTF8 | sqlite::DETERMINISTIC | SQLITE_RESULT_SUBTYPE, None, Some(powersync_json_merge), None, @@ -63,5 +79,16 @@ pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { None, )?; + db.create_function_v2( + "powersync_strip_subtype", + 1, + sqlite::UTF8 | sqlite::DETERMINISTIC | SQLITE_SUBTYPE | SQLITE_RESULT_SUBTYPE, + None, + Some(powersync_strip_subtype), + None, + None, + None, + )?; + Ok(()) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4fe0037..e300a5e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -23,7 +23,7 @@ mod diff; mod error; mod ext; mod fix_data; -mod json_merge; +mod json_util; mod kv; mod macros; mod migrations; @@ -73,7 +73,7 @@ fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> { crate::uuid::register(db)?; crate::diff::register(db)?; crate::fix_data::register(db)?; - crate::json_merge::register(db)?; + crate::json_util::register(db)?; crate::view_admin::register(db)?; crate::checkpoint::register(db)?; crate::kv::register(db)?; diff --git a/crates/core/src/sync/interface.rs b/crates/core/src/sync/interface.rs index ffdb0f7..4e1fcd4 100644 --- a/crates/core/src/sync/interface.rs +++ b/crates/core/src/sync/interface.rs @@ -1,6 +1,12 @@ use core::cell::RefCell; use core::ffi::{c_int, c_void}; +use super::streaming_sync::SyncClient; +use super::sync_status::DownloadSyncStatus; +use crate::constants::SUBTYPE_JSON; +use crate::error::PowerSyncError; +use crate::schema::Schema; +use crate::state::DatabaseState; use alloc::borrow::Cow; use alloc::boxed::Box; use alloc::rc::Rc; @@ -8,16 +14,10 @@ use alloc::sync::Arc; use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; use sqlite::{ResultCode, Value}; +use sqlite_nostd::bindings::SQLITE_RESULT_SUBTYPE; use sqlite_nostd::{self as sqlite, ColumnType}; use sqlite_nostd::{Connection, Context}; -use crate::error::PowerSyncError; -use crate::schema::Schema; -use crate::state::DatabaseState; - -use super::streaming_sync::SyncClient; -use super::sync_status::DownloadSyncStatus; - /// Payload provided by SDKs when requesting a sync iteration. #[derive(Default, Deserialize)] pub struct StartSyncStream { @@ -186,6 +186,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<( let formatted = serde_json::to_string(&instructions).map_err(PowerSyncError::internal)?; ctx.result_text_transient(&formatted); + ctx.result_subtype(SUBTYPE_JSON); Ok(()) })(); @@ -206,7 +207,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Arc) -> Result<( db.create_function_v2( "powersync_control", 2, - sqlite::UTF8 | sqlite::DIRECTONLY, + sqlite::UTF8 | sqlite::DIRECTONLY | SQLITE_RESULT_SUBTYPE, Some(Box::into_raw(controller).cast()), Some(control), None, diff --git a/crates/core/src/views.rs b/crates/core/src/views.rs index c6a267e..ea16433 100644 --- a/crates/core/src/views.rs +++ b/crates/core/src/views.rs @@ -415,26 +415,17 @@ fn json_object_fragment<'a>( let mut column_names_quoted: Vec = alloc::vec![]; while let Some(column) = columns.next() { let name = &*column.name; - let quoted = match &*column.type_name { - // We really want the individual columns here to appear as they show up in the database. - // For text columns however, it's possible that e.g. NEW.column was created by a JSON - // function, meaning that it has a JSON subtype active - causing the json_object() call - // we're about to emit to include it as a subobject instead of a string. - "TEXT" | "text" => format!( - "{:}, concat({:}.{:})", - QuotedString(name), - prefix, - quote_identifier(name) - ), - _ => format!( - "{:}, {:}.{:}", - QuotedString(name), - prefix, - quote_identifier(name) - ), - }; - column_names_quoted.push(quoted); + // We really want the individual columns here to appear as they show up in the database. + // For text columns however, it's possible that e.g. NEW.column was created by a JSON + // function, meaning that it has a JSON subtype active - causing the json_object() call + // we're about to emit to include it as a subobject instead of a string. + column_names_quoted.push(format!( + "{:}, powersync_strip_subtype({:}.{:})", + QuotedString(name), + prefix, + quote_identifier(name) + )); } // SQLITE_MAX_COLUMN - 1 (because of the id column) diff --git a/dart/test/crud_test.dart b/dart/test/crud_test.dart index bda2d02..fc9c9e9 100644 --- a/dart/test/crud_test.dart +++ b/dart/test/crud_test.dart @@ -662,7 +662,7 @@ void main() { expect(db.select('SELECT * FROM ps_crud'), hasLength(1)); }); - test('json values are included as text', () { + test('preserves values in text column', () { db ..execute('select powersync_replace_schema(?)', [ json.encode({ @@ -675,11 +675,44 @@ void main() { } ] }) - ]) - ..execute('INSERT INTO items (id, col) VALUES (uuid(), json_object())'); + ]); + + db.execute('INSERT INTO items (id, col) VALUES (uuid(), json_object())'); + final [insert] = db.select('SELECT data FROM ps_crud'); + expect(json.decode(insert['data']), containsPair('data', {'col': '{}'})); + db.execute('DELETE FROM ps_crud'); + db.execute('UPDATE items SET col = NULL;'); final [update] = db.select('SELECT data FROM ps_crud'); - expect(json.decode(update['data']), containsPair('data', {'col': '{}'})); + expect(json.decode(update['data']), containsPair('data', {'col': null})); + db.execute('DELETE FROM ps_crud'); + }); + + test('preserves mismatched type', () { + db + ..execute('select powersync_replace_schema(?)', [ + json.encode({ + 'tables': [ + { + 'name': 'items', + 'columns': [ + {'name': 'col', 'type': 'int'} + ], + } + ] + }) + ]) + ..execute('insert into items (id, col) values (uuid(), json_object())') + ..execute('insert into items (id, col) values (uuid(), null)') + ..execute('insert into items (id, col) values (uuid(), ?)', + ['not an integer']); + + final data = db.select('SELECT data FROM ps_crud'); + expect(data.map((row) => jsonDecode(row['data'])), [ + containsPair('data', {'col': '{}'}), + containsPair('data', {}), + containsPair('data', {'col': 'not an integer'}), + ]); }); }); } diff --git a/dart/test/utils/migration_fixtures.dart b/dart/test/utils/migration_fixtures.dart index 29a338c..972bd21 100644 --- a/dart/test/utils/migration_fixtures.dart +++ b/dart/test/utils/migration_fixtures.dart @@ -536,8 +536,8 @@ END THEN RAISE (FAIL, 'id should be text') END; INSERT INTO "ps_data__lists" - SELECT NEW.id, json_object('description', concat(NEW."description")); - INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff('{}', json_object('description', concat(NEW."description")))))); + SELECT NEW.id, json_object('description', powersync_strip_subtype(NEW."description")); + INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PUT', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff('{}', json_object('description', powersync_strip_subtype(NEW."description")))))); INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded) SELECT '$local', 1, @@ -557,9 +557,9 @@ BEGIN THEN RAISE (FAIL, 'Cannot update id') END; UPDATE "ps_data__lists" - SET data = json_object('description', concat(NEW."description")) + SET data = json_object('description', powersync_strip_subtype(NEW."description")) WHERE id = NEW.id; - INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff(json_object('description', concat(OLD."description")), json_object('description', concat(NEW."description")))))); + INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'PATCH', 'type', 'lists', 'id', NEW.id, 'data', json(powersync_diff(json_object('description', powersync_strip_subtype(OLD."description")), json_object('description', powersync_strip_subtype(NEW."description")))))); INSERT INTO ps_oplog(bucket, op_id, op, row_type, row_id, hash, superseded) SELECT '$local', 1, @@ -598,8 +598,8 @@ END WHEN (typeof(NEW.id) != 'text') THEN RAISE (FAIL, 'id should be text') END; - INSERT INTO "ps_data__lists" SELECT NEW.id, json_object('description', concat(NEW."description")); - INSERT INTO powersync_crud(op,id,type,data) VALUES ('PUT',NEW.id,'lists',json(powersync_diff('{}', json_object('description', concat(NEW."description"))))); + INSERT INTO "ps_data__lists" SELECT NEW.id, json_object('description', powersync_strip_subtype(NEW."description")); + INSERT INTO powersync_crud(op,id,type,data) VALUES ('PUT',NEW.id,'lists',json(powersync_diff('{}', json_object('description', powersync_strip_subtype(NEW."description"))))); END ;CREATE TRIGGER "ps_view_update_lists" INSTEAD OF UPDATE ON "lists" @@ -610,9 +610,9 @@ BEGIN THEN RAISE (FAIL, 'Cannot update id') END; UPDATE "ps_data__lists" - SET data = json_object('description', concat(NEW."description")) + SET data = json_object('description', powersync_strip_subtype(NEW."description")) WHERE id = NEW.id; - INSERT INTO powersync_crud(op,type,id,data,options) VALUES ('PATCH','lists',NEW.id,json(powersync_diff(json_object('description', concat(OLD."description")), json_object('description', concat(NEW."description")))),0); + INSERT INTO powersync_crud(op,type,id,data,options) VALUES ('PATCH','lists',NEW.id,json(powersync_diff(json_object('description', powersync_strip_subtype(OLD."description")), json_object('description', powersync_strip_subtype(NEW."description")))),0); END '''; @@ -636,8 +636,8 @@ END WHEN (typeof(NEW.id) != 'text') THEN RAISE (FAIL, 'id should be text') END; - INSERT INTO "ps_data__lists" SELECT NEW.id, json_object('description', concat(NEW."description")); - INSERT INTO powersync_crud(op,id,type,data) VALUES ('PUT',NEW.id,'lists',json(powersync_diff('{}', json_object('description', concat(NEW."description"))))); + INSERT INTO "ps_data__lists" SELECT NEW.id, json_object('description', powersync_strip_subtype(NEW."description")); + INSERT INTO powersync_crud(op,id,type,data) VALUES ('PUT',NEW.id,'lists',json(powersync_diff('{}', json_object('description', powersync_strip_subtype(NEW."description"))))); END ;CREATE TRIGGER "ps_view_update_lists" INSTEAD OF UPDATE ON "lists" @@ -648,8 +648,8 @@ BEGIN THEN RAISE (FAIL, 'Cannot update id') END; UPDATE "ps_data__lists" - SET data = json_object('description', concat(NEW."description")) + SET data = json_object('description', powersync_strip_subtype(NEW."description")) WHERE id = NEW.id; - INSERT INTO powersync_crud(op,type,id,data,options) VALUES ('PATCH','lists',NEW.id,json(powersync_diff(json_object('description', concat(OLD."description")), json_object('description', concat(NEW."description")))),0); + INSERT INTO powersync_crud(op,type,id,data,options) VALUES ('PATCH','lists',NEW.id,json(powersync_diff(json_object('description', powersync_strip_subtype(OLD."description")), json_object('description', powersync_strip_subtype(NEW."description")))),0); END '''; diff --git a/powersync-sqlite-core.podspec b/powersync-sqlite-core.podspec index b7d32e7..8167f8d 100644 --- a/powersync-sqlite-core.podspec +++ b/powersync-sqlite-core.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'powersync-sqlite-core' - s.version = '0.4.3' + s.version = '0.4.4' s.summary = 'PowerSync SQLite Extension' s.description = <<-DESC PowerSync extension for SQLite. diff --git a/sqlite-rs-embedded b/sqlite-rs-embedded index 40c77a0..f4bbd97 160000 --- a/sqlite-rs-embedded +++ b/sqlite-rs-embedded @@ -1 +1 @@ -Subproject commit 40c77a011e6143e02f89c922f65594827aac91c3 +Subproject commit f4bbd97a4714e5ab59062a60441978a00e3f87ad diff --git a/tool/build_xcframework.sh b/tool/build_xcframework.sh index 2bc2c06..e7e71a0 100755 --- a/tool/build_xcframework.sh +++ b/tool/build_xcframework.sh @@ -25,7 +25,7 @@ TARGETS=( aarch64-apple-tvos-sim x86_64-apple-tvos ) -VERSION=0.4.3 +VERSION=0.4.4 function generatePlist() { min_os_version=0