Skip to content

chore: Schema validation and other small cleanup items #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public async Task UpdateSchema(Schema schema)

try
{
// schema.Validate();
schema.Validate();
}
catch (Exception ex)
{
Expand Down Expand Up @@ -356,12 +356,19 @@ public async Task Disconnect()
syncStreamStatusCts?.Cancel();
}

public async Task DisconnectAndClear()
/// <summary>
/// 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.
/// </summary>
public async Task DisconnectAndClear(bool clearLocal = true)
{
await Disconnect();
await WaitForReady();

bool clearLocal = true;

await Database.WriteTransaction(async tx =>
{
Expand All @@ -373,11 +380,23 @@ await Database.WriteTransaction(async tx =>
Emit(new PowerSyncDBEvent { StatusChanged = CurrentStatus });
}

/// <summary>
/// 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.
/// </summary>
public new async Task Close()
{
base.Close();
await WaitForReady();

if (Closed) return;


await Disconnect();
base.Close();
syncStreamImplementation?.Close();
BucketStorageAdapter?.Close();

Expand Down
16 changes: 16 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ public class Schema(Dictionary<string, Table> tables)
{
private readonly Dictionary<string, Table> 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
Expand Down
53 changes: 52 additions & 1 deletion PowerSync/PowerSync.Common/DB/Schema/Table.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace PowerSync.Common.DB.Schema;

using System.Text.RegularExpressions;
using Newtonsoft.Json;

public class TableOptions(
Expand All @@ -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<string, ColumnType> Columns;
public Dictionary<string, List<string>> Indexes;
Expand Down Expand Up @@ -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<string> { "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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -496,7 +496,7 @@ await Assert.ThrowsAsync<SqliteException>(async () =>
powersync = new PowerSyncDatabase(new PowerSyncDatabaseOptions
{
Database = new SQLOpenOptions { DbFilename = dbName },
Schema = TestSchema.appSchema,
Schema = TestSchema.AppSchema,
});
await powersync.Init();

Expand All @@ -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();
Expand Down Expand Up @@ -557,7 +557,7 @@ await Assert.ThrowsAsync<SqliteException>(async () =>
powersync = new PowerSyncDatabase(new PowerSyncDatabaseOptions
{
Database = new SQLOpenOptions { DbFilename = dbName },
Schema = TestSchema.appSchema,
Schema = TestSchema.AppSchema,
});
await powersync.Init();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
10 changes: 5 additions & 5 deletions Tests/PowerSync/PowerSync.Common.Tests/TestSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace PowerSync.Common.Tests;

public class TestSchema
{
public static Table assets = new Table(new Dictionary<string, ColumnType>
public static readonly Table Assets = new Table(new Dictionary<string, ColumnType>
{
{ "created_at", ColumnType.TEXT },
{ "make", ColumnType.TEXT },
Expand All @@ -19,15 +19,15 @@ public class TestSchema
Indexes = new Dictionary<string, List<string>> { { "makemodel", new List<string> { "make", "model" } } }
});

public static Table customers = new Table(new Dictionary<string, ColumnType>
public static readonly Table Customers = new Table(new Dictionary<string, ColumnType>
{
{ "name", ColumnType.TEXT },
{ "email", ColumnType.TEXT }
});

public static Schema appSchema = new Schema(new Dictionary<string, Table>
public static readonly Schema AppSchema = new Schema(new Dictionary<string, Table>
{
{ "assets", assets },
{ "customers", customers }
{ "assets", Assets },
{ "customers", Customers }
});
}