From a32817de931e2d17c74214a8d3b778033694230a Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 27 Mar 2025 15:28:06 +0200 Subject: [PATCH 1/3] Added schema validation. Fleshed out options for DisconnectAndClear() and Close(). --- .../Client/PowerSyncDatabase.cs | 32 +++++++++-- .../PowerSync.Common/DB/Schema/Schema.cs | 8 +++ PowerSync/PowerSync.Common/DB/Schema/Table.cs | 53 ++++++++++++++++++- .../Client/Sync/BucketStorageTests.cs | 8 +-- .../Client/Sync/CRUDTests.cs | 2 +- .../PowerSync.Common.Tests/TestSchema.cs | 10 ++-- 6 files changed, 97 insertions(+), 16 deletions(-) diff --git a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs index 5d28379..cfe1678 100644 --- a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs +++ b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs @@ -265,7 +265,7 @@ public async Task UpdateSchema(Schema schema) try { - // schema.Validate(); + schema.Validate(); } catch (Exception ex) { @@ -356,12 +356,19 @@ public async Task Disconnect() syncStreamStatusCts?.Cancel(); } - public async Task DisconnectAndClear() + /// + /// Disconnect and clear the database. + /// Use this when logging out. + /// The database can still be queried after this is called, but the tables + /// would be empty. + /// + /// To preserve data in local-only tables, set clearLocal to false. + /// + public async Task DisconnectAndClear(bool clearLocal = true) { await Disconnect(); await WaitForReady(); - bool clearLocal = true; await Database.WriteTransaction(async tx => { @@ -373,11 +380,26 @@ await Database.WriteTransaction(async tx => Emit(new PowerSyncDBEvent { StatusChanged = CurrentStatus }); } - public new async Task Close() + /// + /// Close the database, releasing resources. + /// + /// Also disconnects any active connection. + /// + /// Once close is called, this connection cannot be used again - a new one + /// must be constructed. + /// + public async Task Close(bool disconnect = true) { - base.Close(); await WaitForReady(); + if (Closed) return; + + if (disconnect) + { + await Disconnect(); + } + + base.Close(); syncStreamImplementation?.Close(); BucketStorageAdapter?.Close(); diff --git a/PowerSync/PowerSync.Common/DB/Schema/Schema.cs b/PowerSync/PowerSync.Common/DB/Schema/Schema.cs index 0742088..67b459f 100644 --- a/PowerSync/PowerSync.Common/DB/Schema/Schema.cs +++ b/PowerSync/PowerSync.Common/DB/Schema/Schema.cs @@ -7,6 +7,14 @@ public class Schema(Dictionary tables) { private readonly Dictionary Tables = tables; + public void Validate() + { + foreach (var table in Tables.Values) + { + table.Validate(); + } + } + public string ToJSON() { var jsonObject = new diff --git a/PowerSync/PowerSync.Common/DB/Schema/Table.cs b/PowerSync/PowerSync.Common/DB/Schema/Table.cs index b9785d2..ade1e2b 100644 --- a/PowerSync/PowerSync.Common/DB/Schema/Table.cs +++ b/PowerSync/PowerSync.Common/DB/Schema/Table.cs @@ -1,5 +1,6 @@ namespace PowerSync.Common.DB.Schema; +using System.Text.RegularExpressions; using Newtonsoft.Json; public class TableOptions( @@ -19,7 +20,10 @@ public class TableOptions( public class Table { - protected TableOptions Options { get; set; } + private static readonly Regex InvalidSQLCharacters = new Regex(@"[""'%,.#\s\[\]]", RegexOptions.Compiled); + + + protected TableOptions Options { get; init; } = null!; public Dictionary Columns; public Dictionary> Indexes; @@ -48,6 +52,53 @@ [.. kv.Value.Select(name => Indexes = Options?.Indexes ?? []; } + public void Validate() + { + if (!string.IsNullOrWhiteSpace(Options.ViewName) && InvalidSQLCharacters.IsMatch(Options.ViewName!)) + { + throw new Exception($"Invalid characters in view name: {Options.ViewName}"); + } + + if (Columns.Count > Column.MAX_AMOUNT_OF_COLUMNS) + { + throw new Exception($"Table has too many columns. The maximum number of columns is {Column.MAX_AMOUNT_OF_COLUMNS}."); + } + + var columnNames = new HashSet { "id" }; + + foreach (var columnName in Columns.Keys) + { + if (columnName == "id") + { + throw new Exception("An id column is automatically added, custom id columns are not supported"); + } + + if (InvalidSQLCharacters.IsMatch(columnName)) + { + throw new Exception($"Invalid characters in column name: {columnName}"); + } + + columnNames.Add(columnName); + } + + foreach (var (indexName, indexColumns) in Indexes) + { + + if (InvalidSQLCharacters.IsMatch(indexName)) + { + throw new Exception($"Invalid characters in index name: {indexName}"); + } + + foreach (var indexColumn in indexColumns) + { + if (!columnNames.Contains(indexColumn)) + { + throw new Exception($"Column {indexColumn} not found for index {indexName}"); + } + } + } + } + public string ToJSON(string Name = "") { var jsonObject = new diff --git a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/BucketStorageTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/BucketStorageTests.cs index 7025038..b19727f 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/BucketStorageTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/BucketStorageTests.cs @@ -70,7 +70,7 @@ public async Task InitializeAsync() db = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = "powersync.db" }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await db.Init(); bucketStorage = new SqliteBucketStorage(db.Database, createLogger()); @@ -496,7 +496,7 @@ await Assert.ThrowsAsync(async () => powersync = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = dbName }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await powersync.Init(); @@ -515,7 +515,7 @@ public async Task ShouldRemoveTypes() var powersync = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = dbName }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await powersync.Init(); @@ -557,7 +557,7 @@ await Assert.ThrowsAsync(async () => powersync = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = dbName }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await powersync.Init(); diff --git a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CRUDTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CRUDTests.cs index 6f31ee4..965cd13 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CRUDTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CRUDTests.cs @@ -20,7 +20,7 @@ public async Task InitializeAsync() db = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = dbName }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await db.Init(); } diff --git a/Tests/PowerSync/PowerSync.Common.Tests/TestSchema.cs b/Tests/PowerSync/PowerSync.Common.Tests/TestSchema.cs index b7e37af..1998f45 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/TestSchema.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/TestSchema.cs @@ -4,7 +4,7 @@ namespace PowerSync.Common.Tests; public class TestSchema { - public static Table assets = new Table(new Dictionary + public static readonly Table Assets = new Table(new Dictionary { { "created_at", ColumnType.TEXT }, { "make", ColumnType.TEXT }, @@ -19,15 +19,15 @@ public class TestSchema Indexes = new Dictionary> { { "makemodel", new List { "make", "model" } } } }); - public static Table customers = new Table(new Dictionary + public static readonly Table Customers = new Table(new Dictionary { { "name", ColumnType.TEXT }, { "email", ColumnType.TEXT } }); - public static Schema appSchema = new Schema(new Dictionary + public static readonly Schema AppSchema = new Schema(new Dictionary { - { "assets", assets }, - { "customers", customers } + { "assets", Assets }, + { "customers", Customers } }); } \ No newline at end of file From 2d0d013d3b262a456fcf5d563dea78cd32ee3c3e Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 27 Mar 2025 15:40:56 +0200 Subject: [PATCH 2/3] Validating table names. --- PowerSync/PowerSync.Common/DB/Schema/Schema.cs | 10 +++++++++- PowerSync/PowerSync.Common/DB/Schema/Table.cs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/PowerSync/PowerSync.Common/DB/Schema/Schema.cs b/PowerSync/PowerSync.Common/DB/Schema/Schema.cs index 67b459f..3b60b4e 100644 --- a/PowerSync/PowerSync.Common/DB/Schema/Schema.cs +++ b/PowerSync/PowerSync.Common/DB/Schema/Schema.cs @@ -9,8 +9,16 @@ public class Schema(Dictionary tables) public void Validate() { - foreach (var table in Tables.Values) + foreach (var kvp in Tables) { + var tableName = kvp.Key; + var table = kvp.Value; + + if (Table.InvalidSQLCharacters.IsMatch(tableName)) + { + throw new Exception($"Invalid characters in table name: {tableName}"); + } + table.Validate(); } } diff --git a/PowerSync/PowerSync.Common/DB/Schema/Table.cs b/PowerSync/PowerSync.Common/DB/Schema/Table.cs index ade1e2b..5c26379 100644 --- a/PowerSync/PowerSync.Common/DB/Schema/Table.cs +++ b/PowerSync/PowerSync.Common/DB/Schema/Table.cs @@ -20,7 +20,7 @@ public class TableOptions( public class Table { - private static readonly Regex InvalidSQLCharacters = new Regex(@"[""'%,.#\s\[\]]", RegexOptions.Compiled); + public static readonly Regex InvalidSQLCharacters = new Regex(@"[""'%,.#\s\[\]]", RegexOptions.Compiled); protected TableOptions Options { get; init; } = null!; From 95360378af5a26f01ccc88c9743df5976033295f Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Thu, 27 Mar 2025 16:50:19 +0200 Subject: [PATCH 3/3] Always disconnecting on close. --- PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs index b0e332a..60cd033 100644 --- a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs +++ b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs @@ -388,17 +388,14 @@ await Database.WriteTransaction(async tx => /// Once close is called, this connection cannot be used again - a new one /// must be constructed. /// - public async Task Close(bool disconnect = true) + public new async Task Close() { await WaitForReady(); if (Closed) return; - if (disconnect) - { - await Disconnect(); - } + await Disconnect(); base.Close(); syncStreamImplementation?.Close(); BucketStorageAdapter?.Close();