diff --git a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs index d9c693a..60cd033 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,23 @@ await Database.WriteTransaction(async tx => Emit(new PowerSyncDBEvent { StatusChanged = CurrentStatus }); } + /// + /// 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 new async Task Close() { - base.Close(); await WaitForReady(); + if (Closed) return; + + + 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..3b60b4e 100644 --- a/PowerSync/PowerSync.Common/DB/Schema/Schema.cs +++ b/PowerSync/PowerSync.Common/DB/Schema/Schema.cs @@ -7,6 +7,22 @@ public class Schema(Dictionary tables) { private readonly Dictionary Tables = tables; + public void Validate() + { + 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(); + } + } + 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..5c26379 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; } + public 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/PowerSyncDatabaseTransactionTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTransactionTests.cs index d9a1614..f56cbc3 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTransactionTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTransactionTests.cs @@ -12,7 +12,7 @@ public async Task InitializeAsync() db = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new SQLOpenOptions { DbFilename = "powersyncDataBaseTransactions.db" }, - Schema = TestSchema.appSchema, + Schema = TestSchema.AppSchema, }); await db.Init(); } 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