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