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