Skip to content
Open
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
namespace PowerSync.Common.DB.Schema;

using Newtonsoft.Json;

public enum ColumnType
{
TEXT,
INTEGER,
REAL
Text,
Integer,
Real
}

public class ColumnOptions(string Name, ColumnType? Type)
class ColumnJSONOptions(string Name, ColumnType? Type)
{
public string Name { get; set; } = Name;
public ColumnType? Type { get; set; } = Type;
}

public class Column(ColumnOptions options)
class ColumnJSON(ColumnJSONOptions options)
{
public const int MAX_AMOUNT_OF_COLUMNS = 1999;

public string Name { get; set; } = options.Name;

public ColumnType Type { get; set; } = options.Type ?? ColumnType.TEXT;
public ColumnType Type { get; set; } = options.Type ?? ColumnType.Text;

public string ToJSON()
public object ToJSONObject()
{
return JsonConvert.SerializeObject(new
return new
{
name = Name,
type = Type.ToString()
});
};
}
}
25 changes: 0 additions & 25 deletions PowerSync/PowerSync.Common/DB/Schema/Index.cs

This file was deleted.

23 changes: 23 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/IndexJSON.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace PowerSync.Common.DB.Schema;

class IndexJSONOptions(string name, IndexedColumnJSON[]? columns = null)
{
public string Name { get; set; } = name;
public IndexedColumnJSON[]? Columns { get; set; } = columns ?? [];
}

class IndexJSON(IndexJSONOptions options)
{
public string Name { get; set; } = options.Name;

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

public object ToJSONObject(Table table)
{
return new
{
name = Name,
columns = Columns.Select(column => column.ToJSON(table)).ToList()
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ namespace PowerSync.Common.DB.Schema;

using Newtonsoft.Json;

public class IndexColumnOptions(string Name, bool Ascending = true)
class IndexedColumnJSONOptions(string Name, bool Ascending = true)
{
public string Name { get; set; } = Name;
public bool Ascending { get; set; } = Ascending;
}

public class IndexedColumn(IndexColumnOptions options)
class IndexedColumnJSON(IndexedColumnJSONOptions options)
{
protected string Name { get; set; } = options.Name;

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

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

Expand Down
26 changes: 26 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/SchemaFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace PowerSync.Common.DB.Schema;

public class SchemaFactory
{
private List<Table> _tables;

public SchemaFactory(params Table[] tables)
{
_tables = tables.ToList();
}

public SchemaFactory(params TableFactory[] tableFactories)
{
_tables = tableFactories.Select((f) => f.Create()).ToList();
}

public Schema Create()
{
Dictionary<string, Table> tableMap = new();
foreach (Table table in _tables)
{
tableMap[table.Name] = table;
}
return new Schema(tableMap);
}
}
60 changes: 34 additions & 26 deletions PowerSync/PowerSync.Common/DB/Schema/Table.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace PowerSync.Common.DB.Schema;

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

using Newtonsoft.Json;

Expand Down Expand Up @@ -62,54 +63,61 @@ public class TrackPreviousOptions

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

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; }

public Dictionary<string, ColumnType> Columns;
public Dictionary<string, List<string>> Indexes;

private readonly List<Column> ConvertedColumns;
private readonly List<Index> ConvertedIndexes;
private readonly ColumnJSON[] ColumnsJSON;
private readonly IndexJSON[] IndexesJSON;

public Table(Dictionary<string, ColumnType> columns, TableOptions? options = null)
public Table(string name, Dictionary<string, ColumnType> columns, TableOptions? options = null)
{
ConvertedColumns = [.. columns.Select(kv => new Column(new ColumnOptions(kv.Key, kv.Value)))];

ConvertedIndexes =
[
.. (Options?.Indexes ?? [])
.Select(kv =>
new Index(new IndexOptions(
kv.Key,
[
.. kv.Value.Select(name =>
new IndexedColumn(new IndexColumnOptions(
name.Replace("-", ""), !name.StartsWith("-")))
)
]
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();

Options = options ?? new TableOptions();

Name = name;
Columns = columns;
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 > Column.MAX_AMOUNT_OF_COLUMNS)
if (Columns.Count > MAX_AMOUNT_OF_COLUMNS)
{
throw new Exception(
$"Table has too many columns. The maximum number of columns is {Column.MAX_AMOUNT_OF_COLUMNS}.");
$"Table has too many columns. The maximum number of columns is {MAX_AMOUNT_OF_COLUMNS}.");
}

if (Options.TrackMetadata && Options.LocalOnly)
Expand Down Expand Up @@ -168,8 +176,8 @@ public string ToJSON(string Name = "")
view_name = Options.ViewName ?? Name,
local_only = Options.LocalOnly,
insert_only = Options.InsertOnly,
columns = ConvertedColumns.Select(c => JsonConvert.DeserializeObject<object>(c.ToJSON())).ToList(),
indexes = ConvertedIndexes.Select(e => JsonConvert.DeserializeObject<object>(e.ToJSON(this))).ToList(),
columns = ColumnsJSON.Select(c => c.ToJSONObject()).ToList(),
indexes = IndexesJSON.Select(i => i.ToJSONObject(this)).ToList(),

include_metadata = Options.TrackMetadata,
ignore_empty_update = Options.IgnoreEmptyUpdates,
Expand Down
56 changes: 56 additions & 0 deletions PowerSync/PowerSync.Common/DB/Schema/TableFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace PowerSync.Common.DB.Schema;

using System.Collections;

public class TableFactory()
{
public ColumnMap Columns { get; set; } = new();
public IndexMap Indexes { get; set; } = new();

public string Name { get; set; } = null!;
public bool LocalOnly { get; set; } = false;
public bool InsertOnly { get; set; } = false;
string? ViewName { get; set; }
bool? TrackMetadata { get; set; }
TrackPreviousOptions? TrackPreviousValues { get; set; }
bool? IgnoreEmptyUpdates { get; set; }

public Table Create()
{
if (string.IsNullOrWhiteSpace(Name))
{
throw new Exception("Table name is required.");
}
TableOptions options = new(
indexes: Indexes.Indexes,
localOnly: LocalOnly,
insertOnly: InsertOnly,
viewName: ViewName,
trackMetadata: TrackMetadata,
trackPreviousValues: TrackPreviousValues,
ignoreEmptyUpdates: IgnoreEmptyUpdates
);
return new Table(Name, Columns.Columns, options);
}
}

public class ColumnMap : IEnumerable
{
public Dictionary<string, ColumnType> Columns { get; } = new();

public void Add(string key, ColumnType value) => Columns.Add(key, value);

public ColumnType this[string name] { set { Columns[name] = value; } }
public IEnumerator GetEnumerator() => Columns.GetEnumerator();
}

public class IndexMap : IEnumerable
{
public Dictionary<string, List<string>> Indexes { get; } = new();

public void Add(string key, List<string> value) => Indexes.Add(key, value);

public List<string> this[string name] { set { Indexes[name] = value; } }
public IEnumerator GetEnumerator() => Indexes.GetEnumerator();
}

50 changes: 28 additions & 22 deletions Tests/PowerSync/PowerSync.Common.IntegrationTests/TestSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,36 @@ namespace PowerSync.Common.IntegrationTests;

public class TestSchema
{
public static Table Todos = new Table(new Dictionary<string, ColumnType>
public static TableFactory Todos = new TableFactory()
{
{ "list_id", ColumnType.TEXT },
{ "created_at", ColumnType.TEXT },
{ "completed_at", ColumnType.TEXT },
{ "description", ColumnType.TEXT },
{ "created_by", ColumnType.TEXT },
{ "completed_by", ColumnType.TEXT },
{ "completed", ColumnType.INTEGER }
}, new TableOptions
{
Indexes = new Dictionary<string, List<string>> { { "list", new List<string> { "list_id" } } }
});
Name = "todos",
Columns =
{
["list_id"] = ColumnType.Text,
["created_at"] = ColumnType.Text,
["completed_at"] = ColumnType.Text,
["description"] = ColumnType.Text,
["created_by"] = ColumnType.Text,
["completed_by"] = ColumnType.Text,
["completed"] = ColumnType.Integer,
},
Indexes =
{
["list"] = ["list_id"],
}
};

public static Table Lists = new Table(new Dictionary<string, ColumnType>
{
{ "created_at", ColumnType.TEXT },
{ "name", ColumnType.TEXT },
{ "owner_id", ColumnType.TEXT }
});

public static Schema PowerSyncSchema = new Schema(new Dictionary<string, Table>
public static TableFactory Lists = new TableFactory()
{
{ "todos", Todos },
{ "lists", Lists }
});
Name = "lists",
Columns =
{
["created_at"] = ColumnType.Text,
["name"] = ColumnType.Text,
["owner_id"] = ColumnType.Text
}
};

public static Schema PowerSyncSchema = new SchemaFactory(Todos, Lists).Create();
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,10 @@ public async Task DeleteTest()
[Fact]
public async Task InsertOnlyTablesTest()
{
var logs = new Table(new Dictionary<string, ColumnType>
var logs = new Table("logs", new Dictionary<string, ColumnType>
{
{ "level", ColumnType.TEXT },
{ "content", ColumnType.TEXT },
{ "level", ColumnType.Text },
{ "content", ColumnType.Text },
}, new TableOptions
{
InsertOnly = true
Expand Down
Loading