Skip to content
Open
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
7 changes: 7 additions & 0 deletions PowerSync/PowerSync.Common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# PowerSync.Common Changelog

## 0.0.9-alpha.1

- _Breaking:_ Further updated schema definition syntax.
- Renamed `Schema` and `Table` to `CompiledSchema` and `CompiledTable` and renamed `SchemaFactory` and `TableFactory` to `Schema` and `Table`.
- Made `CompiledSchema` and `CompiledTable` internal classes.
- These are the last breaking changes to schema definition before entering beta.

## 0.0.8-alpha.1

- Updated the syntax for defining the app schema to use a factory pattern.
Expand Down
17 changes: 9 additions & 8 deletions PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public interface IPowerSyncDatabase : IEventStream<PowerSyncDBEvent>
public class PowerSyncDatabase : EventStream<PowerSyncDBEvent>, IPowerSyncDatabase
{
public IDBAdapter Database { get; protected set; }
private Schema schema;
private CompiledSchema schema;

private static readonly int DEFAULT_WATCH_THROTTLE_MS = 30;
private static readonly Regex POWERSYNC_TABLE_MATCH = new Regex(@"(^ps_data__|^ps_data_local__)", RegexOptions.Compiled);
Expand Down Expand Up @@ -168,7 +168,7 @@ public PowerSyncDatabase(PowerSyncDatabaseOptions options)
Closed = false;
Ready = false;

schema = options.Schema;
schema = options.Schema.Compile();
SdkVersion = "";

remoteFactory = options.RemoteFactory ?? (connector => new Remote(connector));
Expand Down Expand Up @@ -223,7 +223,7 @@ public PowerSyncDatabase(PowerSyncDatabaseOptions options)
}
}, logger: Logger);

IsReadyTask = Initialize();
IsReadyTask = Initialize(options);
}

protected IBucketStorageAdapter generateBucketStorageAdapter()
Expand Down Expand Up @@ -307,11 +307,11 @@ public async Task WaitForStatus(Func<SyncStatus, bool> predicate, CancellationTo
await tcs.Task;
}

protected async Task Initialize()
protected async Task Initialize(PowerSyncDatabaseOptions options)
{
await BucketStorageAdapter.Init();
await LoadVersion();
await UpdateSchema(schema);
await UpdateSchema(options.Schema);
await ResolveOfflineSyncStatus();
await Database.Execute("PRAGMA RECURSIVE_TRIGGERS=TRUE");
Ready = true;
Expand Down Expand Up @@ -369,22 +369,23 @@ protected async Task ResolveOfflineSyncStatus()
/// </summary>
public async Task UpdateSchema(Schema schema)
{
CompiledSchema compiledSchema = schema.Compile();
if (syncStreamImplementation != null)
{
throw new Exception("Cannot update schema while connected");
}

try
{
schema.Validate();
compiledSchema.Validate();
}
catch (Exception ex)
{
Logger.LogWarning("Schema validation failed. Unexpected behavior could occur: {Exception}", ex);
}

this.schema = schema;
await Database.Execute("SELECT powersync_replace_schema(?)", [schema.ToJSON()]);
this.schema = compiledSchema;
await Database.Execute("SELECT powersync_replace_schema(?)", [compiledSchema.ToJSON()]);
await Database.RefreshSchema();
Emit(new PowerSyncDBEvent { SchemaChanged = schema });
}
Expand Down
41 changes: 41 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/CompiledSchema.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace PowerSync.Common.DB.Schema;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

class CompiledSchema(Dictionary<string, CompiledTable> tables)
{
private readonly Dictionary<string, CompiledTable> Tables = tables;

public void Validate()
{
foreach (var kvp in Tables)
{
var tableName = kvp.Key;
var table = kvp.Value;

if (CompiledTable.InvalidSQLCharacters.IsMatch(tableName))
{
throw new Exception($"Invalid characters in table name: {tableName}");
}

table.Validate();
}
}

public string ToJSON()
{
var jsonObject = new
{
tables = Tables.Select(kv =>
{
var json = JObject.Parse(kv.Value.ToJSON(kv.Key));
var orderedJson = new JObject { ["name"] = kv.Key };
orderedJson.Merge(json, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat });
return orderedJson;
}).ToList()
};

return JsonConvert.SerializeObject(jsonObject);
}
}
136 changes: 136 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/CompiledTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
namespace PowerSync.Common.DB.Schema;

using System.Collections.Generic;
using System.Text.RegularExpressions;

using Newtonsoft.Json;

class CompiledTable
{
public static readonly Regex InvalidSQLCharacters = new Regex(@"[""'%,.#\s\[\]]", RegexOptions.Compiled);

public string Name { get; init; } = null!;
protected TableOptions Options { get; init; } = null!;
public IReadOnlyDictionary<string, ColumnType> Columns { get; init; }
public IReadOnlyDictionary<string, List<string>> Indexes { get; init; }

private readonly ColumnJSON[] ColumnsJSON;
private readonly IndexJSON[] IndexesJSON;

public CompiledTable(string name, Dictionary<string, ColumnType> columns, TableOptions options)
{
ColumnsJSON =
columns
.Select(kvp => new ColumnJSON(new ColumnJSONOptions(kvp.Key, kvp.Value)))
.ToArray();

IndexesJSON =
(Options?.Indexes ?? [])
.Select(kvp =>
new IndexJSON(new IndexJSONOptions(
kvp.Key,
kvp.Value.Select(name =>
new IndexedColumnJSON(new IndexedColumnJSONOptions(
name.Replace("-", ""), !name.StartsWith("-")))
).ToArray()
))
)
.ToArray();

Name = name;
Columns = columns;
Options = options;
Indexes = Options?.Indexes ?? [];
}

public void Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
throw new Exception($"Table name is required.");
}

if (!string.IsNullOrWhiteSpace(Options.ViewName) && InvalidSQLCharacters.IsMatch(Options.ViewName!))
{
throw new Exception($"Invalid characters in view name: {Options.ViewName}");
}

if (Columns.Count > Table.MAX_AMOUNT_OF_COLUMNS)
{
throw new Exception(
$"Table has too many columns. The maximum number of columns is {Table.MAX_AMOUNT_OF_COLUMNS}.");
}

if (Options.TrackMetadata && Options.LocalOnly)
{
throw new Exception("Can't include metadata for local-only tables.");
}

if (Options.TrackPreviousValues != null && Options.LocalOnly)
{
throw new Exception("Can't include old values for local-only tables.");
}

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 kvp in Indexes)
{
var indexName = kvp.Key;
var indexColumns = kvp.Value;

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 trackPrevious = Options.TrackPreviousValues;

var jsonObject = new
{
view_name = Options.ViewName ?? Name,
local_only = Options.LocalOnly,
insert_only = Options.InsertOnly,
columns = ColumnsJSON.Select(c => c.ToJSONObject()).ToList(),
indexes = IndexesJSON.Select(i => i.ToJSONObject(this)).ToList(),

include_metadata = Options.TrackMetadata,
ignore_empty_update = Options.IgnoreEmptyUpdates,
include_old = (object)(trackPrevious switch
{
null => false,
{ Columns: null } => true,
{ Columns: var cols } => cols
}),
include_old_only_when_changed = trackPrevious?.OnlyWhenChanged ?? false
};

return JsonConvert.SerializeObject(jsonObject);
}
}
2 changes: 1 addition & 1 deletion PowerSync/PowerSync.Common/DB/Schema/IndexJSON.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class IndexJSON(IndexJSONOptions options)

public IndexedColumnJSON[] Columns => options.Columns ?? [];

public object ToJSONObject(Table table)
public object ToJSONObject(CompiledTable table)
{
return new
{
Expand Down
2 changes: 1 addition & 1 deletion PowerSync/PowerSync.Common/DB/Schema/IndexedColumnJSON.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class IndexedColumnJSON(IndexedColumnJSONOptions options)

protected bool Ascending { get; set; } = options.Ascending;

public string ToJSON(Table table)
public string ToJSON(CompiledTable table)
{
var colType = table.Columns.TryGetValue(Name, out var value) ? value : default;

Expand Down
41 changes: 11 additions & 30 deletions PowerSync/PowerSync.Common/DB/Schema/Schema.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,22 @@
namespace PowerSync.Common.DB.Schema;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Schema(Dictionary<string, Table> tables)
public class Schema
{
private readonly Dictionary<string, Table> Tables = tables;
private List<Table> _tables;

public void Validate()
public Schema(params Table[] tables)
{
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();
}
_tables = tables.ToList();
}

public string ToJSON()
internal CompiledSchema Compile()
{
var jsonObject = new
Dictionary<string, CompiledTable> tableMap = new();
foreach (Table table in _tables)
{
tables = Tables.Select(kv =>
{
var json = JObject.Parse(kv.Value.ToJSON(kv.Key));
var orderedJson = new JObject { ["name"] = kv.Key };
orderedJson.Merge(json, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Concat });
return orderedJson;
}).ToList()
};

return JsonConvert.SerializeObject(jsonObject);
var compiled = table.Compile();
tableMap[compiled.Name] = compiled;
}
return new CompiledSchema(tableMap);
}
}
26 changes: 0 additions & 26 deletions PowerSync/PowerSync.Common/DB/Schema/SchemaFactory.cs

This file was deleted.

Loading