diff --git a/Cargo.lock b/Cargo.lock index c7b4549..0f3c7d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,7 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "powersync_core" -version = "0.3.13" +version = "0.3.14" dependencies = [ "bytes", "const_format", @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "powersync_loadable" -version = "0.3.13" +version = "0.3.14" dependencies = [ "powersync_core", "sqlite_nostd", @@ -260,7 +260,7 @@ dependencies = [ [[package]] name = "powersync_sqlite" -version = "0.3.13" +version = "0.3.14" dependencies = [ "cc", "powersync_core", @@ -375,7 +375,7 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "sqlite3" -version = "0.3.13" +version = "0.3.14" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index d5ce702..3506783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ inherits = "release" inherits = "wasm" [workspace.package] -version = "0.3.13" +version = "0.3.14" edition = "2021" authors = ["JourneyApps"] keywords = ["sqlite", "powersync"] diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 23c9772..7247336 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "co.powersync" -version = "0.3.13" +version = "0.3.14" description = "PowerSync Core SQLite Extension" repositories { diff --git a/android/src/prefab/prefab.json b/android/src/prefab/prefab.json index 8d0c818..165b579 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.3.13" + "version": "0.3.14" } diff --git a/crates/core/src/view_admin.rs b/crates/core/src/view_admin.rs index 9bbeb56..19c12ed 100644 --- a/crates/core/src/view_admin.rs +++ b/crates/core/src/view_admin.rs @@ -213,7 +213,7 @@ fn setup_internal_views(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { AS SELECT view.name name, view.sql sql, - ifnull(trigger1.sql, '') delete_trigger_sql, + ifnull(group_concat(trigger1.sql, ';\n' ORDER BY trigger1.name DESC), '') delete_trigger_sql, ifnull(trigger2.sql, '') insert_trigger_sql, ifnull(trigger3.sql, '') update_trigger_sql FROM sqlite_master view @@ -223,7 +223,8 @@ fn setup_internal_views(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> { ON trigger2.tbl_name = view.name AND trigger2.type = 'trigger' AND trigger2.name GLOB 'ps_view_insert*' LEFT JOIN sqlite_master trigger3 ON trigger3.tbl_name = view.name AND trigger3.type = 'trigger' AND trigger3.name GLOB 'ps_view_update*' - WHERE view.type = 'view' AND view.sql GLOB '*-- powersync-auto-generated'; + WHERE view.type = 'view' AND view.sql GLOB '*-- powersync-auto-generated' + GROUP BY view.name; CREATE TRIGGER IF NOT EXISTS powersync_views_insert INSTEAD OF INSERT ON powersync_views diff --git a/crates/core/src/views.rs b/crates/core/src/views.rs index ee547d7..dea0b2b 100644 --- a/crates/core/src/views.rs +++ b/crates/core/src/views.rs @@ -129,7 +129,7 @@ DELETE FROM {internal_name} WHERE id = OLD.id; INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'DELETE', 'type', {type_string}, 'id', OLD.id{old_fragment})); INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES({type_string}, OLD.id); INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, {MAX_OP_ID}); -END;" +END" ); // The DELETE statement can't include metadata for the delete operation, so we create @@ -137,6 +137,7 @@ END;" if table_info.flags.include_metadata() { let trigger_name = quote_identifier_prefixed("ps_view_delete2_", view_name); write!(&mut trigger, "\ +; CREATE TRIGGER {trigger_name} INSTEAD OF UPDATE ON {quoted_name} FOR EACH ROW @@ -146,7 +147,7 @@ DELETE FROM {internal_name} WHERE id = NEW.id; INSERT INTO powersync_crud_(data) VALUES(json_object('op', 'DELETE', 'type', {type_string}, 'id', NEW.id{old_fragment}, 'metadata', NEW._metadata)); INSERT OR IGNORE INTO ps_updated_rows(row_type, row_id) VALUES({type_string}, NEW.id); INSERT OR REPLACE INTO ps_buckets(name, last_op, target_op) VALUES('$local', 0, {MAX_OP_ID}); -END;" +END" ).expect("writing to string should be infallible"); } diff --git a/dart/test/schema_test.dart b/dart/test/schema_test.dart new file mode 100644 index 0000000..18a4515 --- /dev/null +++ b/dart/test/schema_test.dart @@ -0,0 +1,343 @@ +import 'dart:convert'; + +import 'package:sqlite3/common.dart'; +import 'package:test/test.dart'; + +import 'utils/native_test_utils.dart'; + +void main() { + late CommonDatabase db; + + setUp(() async { + db = openTestDatabase(); + }); + + tearDown(() { + db.dispose(); + }); + + group('Schema Tests', () { + test('Schema versioning', () { + // Test that powersync_replace_schema() is a no-op when the schema is not + // modified. + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema)]); + + final [versionBefore] = db.select('PRAGMA schema_version'); + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema)]); + final [versionAfter] = db.select('PRAGMA schema_version'); + + // No change + expect(versionAfter['schema_version'], + equals(versionBefore['schema_version'])); + + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema2)]); + final [versionAfter2] = db.select('PRAGMA schema_version'); + + // Updated + expect(versionAfter2['schema_version'], + greaterThan(versionAfter['schema_version'] as int)); + + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema3)]); + final [versionAfter3] = db.select('PRAGMA schema_version'); + + // Updated again (index) + expect(versionAfter3['schema_version'], + greaterThan(versionAfter2['schema_version'] as int)); + }); + + group('metadata', () { + // This is a special because we have two delete triggers when + // include_metadata is true (one for actual `DELETE` statements and one + // for `UPDATE ... SET _deleted = TRUE` that allows attaching metadata). + Object createSchema(bool withMetadata) { + return { + "tables": [ + { + "name": "customers", + "view_name": null, + "local_only": false, + "insert_only": false, + "include_metadata": withMetadata, + "columns": [ + {"name": "name", "type": "TEXT"}, + {"name": "email", "type": "TEXT"} + ], + "indexes": [] + }, + ] + }; + } + + test('enabling', () { + db.execute('SELECT powersync_replace_schema(?)', + [json.encode(createSchema(false))]); + expect( + db.select("select * from sqlite_schema where type = 'trigger' " + "AND tbl_name = 'customers' " + "AND name GLOB 'ps_view_delete*'"), + hasLength(1), + ); + + db.execute('SELECT powersync_replace_schema(?)', + [json.encode(createSchema(true))]); + expect( + db.select("select * from sqlite_schema where type = 'trigger' " + "AND tbl_name = 'customers' " + "AND name GLOB 'ps_view_delete*'"), + hasLength(2), + ); + }); + + test('unchanged', () { + final schema = createSchema(true); + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema)]); + + final [versionBefore] = db.select('PRAGMA schema_version'); + db.execute('SELECT powersync_replace_schema(?)', [json.encode(schema)]); + final [versionAfter] = db.select('PRAGMA schema_version'); + + expect(versionAfter['schema_version'], + equals(versionBefore['schema_version'])); + }); + + test('disabling', () { + db.execute('SELECT powersync_replace_schema(?)', + [json.encode(createSchema(true))]); + expect( + db.select("select * from sqlite_schema where type = 'trigger' " + "AND tbl_name = 'customers' " + "AND name GLOB 'ps_view_delete*'"), + hasLength(2), + ); + + db.execute('SELECT powersync_replace_schema(?)', + [json.encode(createSchema(false))]); + expect( + db.select("select * from sqlite_schema where type = 'trigger' " + "AND tbl_name = 'customers' " + "AND name GLOB 'ps_view_delete*'"), + hasLength(1), + ); + }); + }); + }); +} + +final schema = { + "tables": [ + { + "name": "assets", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "created_at", "type": "TEXT"}, + {"name": "make", "type": "TEXT"}, + {"name": "model", "type": "TEXT"}, + {"name": "serial_number", "type": "TEXT"}, + {"name": "quantity", "type": "INTEGER"}, + {"name": "user_id", "type": "TEXT"}, + {"name": "weight", "type": "REAL"}, + {"name": "description", "type": "TEXT"} + ], + "indexes": [ + { + "name": "makemodel", + "columns": [ + {"name": "make", "ascending": true, "type": "TEXT"}, + {"name": "model", "ascending": true, "type": "TEXT"} + ] + } + ] + }, + { + "name": "customers", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"}, + {"name": "email", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "logs", + "view_name": null, + "local_only": false, + "insert_only": true, + "columns": [ + {"name": "level", "type": "TEXT"}, + {"name": "content", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "credentials", + "view_name": null, + "local_only": true, + "insert_only": false, + "columns": [ + {"name": "key", "type": "TEXT"}, + {"name": "value", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "aliased", + "view_name": "test1", + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"} + ], + "indexes": [] + } + ] +}; + +final schema2 = { + "tables": [ + { + "name": "assets", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "created_at", "type": "TEXT"}, + {"name": "make", "type": "TEXT"}, + {"name": "model", "type": "TEXT"}, + {"name": "serial_number", "type": "TEXT"}, + {"name": "quantity", "type": "INTEGER"}, + {"name": "user_id", "type": "TEXT"}, + {"name": "weights", "type": "REAL"}, + {"name": "description", "type": "TEXT"} + ], + "indexes": [ + { + "name": "makemodel", + "columns": [ + {"name": "make", "ascending": true, "type": "TEXT"}, + {"name": "model", "ascending": true, "type": "TEXT"} + ] + } + ] + }, + { + "name": "customers", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"}, + {"name": "email", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "logs", + "view_name": null, + "local_only": false, + "insert_only": true, + "columns": [ + {"name": "level", "type": "TEXT"}, + {"name": "content", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "credentials", + "view_name": null, + "local_only": true, + "insert_only": false, + "columns": [ + {"name": "key", "type": "TEXT"}, + {"name": "value", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "aliased", + "view_name": "test1", + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"} + ], + "indexes": [] + } + ] +}; + +final schema3 = { + "tables": [ + { + "name": "assets", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "created_at", "type": "TEXT"}, + {"name": "make", "type": "TEXT"}, + {"name": "model", "type": "TEXT"}, + {"name": "serial_number", "type": "TEXT"}, + {"name": "quantity", "type": "INTEGER"}, + {"name": "user_id", "type": "TEXT"}, + {"name": "weights", "type": "REAL"}, + {"name": "description", "type": "TEXT"} + ], + "indexes": [ + { + "name": "makemodel", + "columns": [ + {"name": "make", "ascending": true, "type": "TEXT"}, + {"name": "model", "ascending": false, "type": "TEXT"} + ] + } + ] + }, + { + "name": "customers", + "view_name": null, + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"}, + {"name": "email", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "logs", + "view_name": null, + "local_only": false, + "insert_only": true, + "columns": [ + {"name": "level", "type": "TEXT"}, + {"name": "content", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "credentials", + "view_name": null, + "local_only": true, + "insert_only": false, + "columns": [ + {"name": "key", "type": "TEXT"}, + {"name": "value", "type": "TEXT"} + ], + "indexes": [] + }, + { + "name": "aliased", + "view_name": "test1", + "local_only": false, + "insert_only": false, + "columns": [ + {"name": "name", "type": "TEXT"} + ], + "indexes": [] + } + ] +}; diff --git a/powersync-sqlite-core.podspec b/powersync-sqlite-core.podspec index b32c921..44f4f95 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.3.13' + s.version = '0.3.14' s.summary = 'PowerSync SQLite Extension' s.description = <<-DESC PowerSync extension for SQLite. diff --git a/tool/build_xcframework.sh b/tool/build_xcframework.sh index a9c12a2..2a6334d 100755 --- a/tool/build_xcframework.sh +++ b/tool/build_xcframework.sh @@ -28,9 +28,9 @@ function createXcframework() { MinimumOSVersion 11.0 CFBundleVersion - 0.3.13 + 0.3.14 CFBundleShortVersionString - 0.3.13 + 0.3.14 EOF