Skip to content

[Feature] Class-based schema definition#41

Open
LucDeCaf wants to merge 20 commits intomainfrom
feat/schema-classes
Open

[Feature] Class-based schema definition#41
LucDeCaf wants to merge 20 commits intomainfrom
feat/schema-classes

Conversation

@LucDeCaf
Copy link
Contributor

@LucDeCaf LucDeCaf commented Feb 5, 2026

Discussion: #34

TODO:

  • Schema.WithOptions or similar - basically some mechanism for overriding options on schema creation
  • AttributeParser tests
  • Refactor other demos
  • Refactor tests

Class based schema definition

Provides a new syntax for defining the PowerSync schema using C# classes and attributes.

Example

using PowerSync.Common.DB.Schema;
using PowerSync.Common.DB.Schema.Attributes;

[
    Table("todos"),
    Index("list", ["list_id"]),
    Index("created_at", ["created_at"])
]
public class Todo
{
    [Column("id")]
    public string TodoId { get; set; }

    [Column("list_id")]
    public string ListId { get; set; }

    [Column("created_at")]
    public DateTime CreatedAt { get; set; }

    [Column("completed_at")]
    public DateTime? CompletedAt { get; set; }

    [Column("created_by")]
    public string CreatedBy { get; set; }

    [Column("completed_by")]
    public string? CompletedBy { get; set; }

    [Column("completed")]
    public bool Completed { get; set; }
}

[Table("lists")]
public class List
{
    [Column("id")]
    public string ListId { get; set; }

    [Column("created_at")]
    public string CreatedAt { get; set; }

    [Column("name")]
    public string Name { get; set; }

    [Column("owner_id")]
    public string OwnerId { get; set; }
}

public class AppSchema
{
    public static Schema PowerSyncSchema = new Schema(typeof(Todo), typeof(List));
}

// Usage
var list = db.Get<List>("SELECT * FROM lists WHERE owner_id = ? ORDER BY created_at LIMIT 1", [userId]);
var todos = db.GetAll<Todo>("SELECT * FROM todos WHERE list_id = ?", [list.ListId]);

This syntax allows you to define both a result type for queries and the PowerSync schema definitions from the same language construct.

Setting options

Options are set primarily in the Table attribute. Indexes are defined via the Index attribute, which can be defined multiple times for the same table. Column-specific settings (i.e. column aliases, ColumnType, and TrackPrevious) can be customised via the Column attribute. Properties can be excluded from defining SQLite columns via the Ignored attribute.

[
    Table(
        "logs",
        LocalOnly = true,
        InsertOnly = false,
        ViewName = "logs_local",
        TrackMetadata = false,
        IgnoreEmptyUpdates = true,
        // `TrackPreviousValues` is set via the `TrackPrevious` enum, since attributes can't accept arbitrary class instances.
        TrackPrevious = TrackPrevious.Columns | TrackPrevious.OnlyWhenChanged
    ),
    Index("created_at", ["created_at"])
]
public class Log
{
    // Change the name of the backing SQLite column
    [Column("id")]
    public string LogId { get; set; }

    // Manually set the column type (default is ColumnType.Inferred)
    [Column(ColumnType = ColumnType.Text)]
    public DateTime created_at { get; set; }

    // Track changes on this column
    [Column(TrackPrevious = true)]
    public string description { get; set; }

    // Combine multiple options
    [Column("log_level", ColumnType = ColumnType.Integer, TrackPrevious = true)]
    public int LogLevel { get; set; }

    // Exclude properties from creating SQLite columns
    [Ignored]
    public string LogLevelString { get { return LogLevel.ToString(); } }
}

The Table object also contains a number of ways to manually set the table options.

public static Table TodosTable = new Table(typeof(Todo));

// Create a new table from an existing Table object
Table TodosWithOptions = new Table(TodosTable, new TableOptions(/* ... */));

// OR: Create a new table from a [Table] class
Table TodosWithOptions = new Table(typeof(Todo), new TableOptions(/* ... */));

// OR: Set Table.Options directly
Table TodosWithOptions = new Table(typeof(Todo));
TodosWithOptions.Options = new TableOptions(/* ... */);

// OR: Use the old syntax for creating tables
Table TodosWithOptions = new Table("todos", columns: new Dictionary(/* ... */), options: new TableOptions(/* ... */));

@LucDeCaf LucDeCaf changed the title Class-based schema definition [Feature] Class-based schema definition Feb 5, 2026
class Demo
{
private record ListResult
using Spectre.Console;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like this because part of the formatting converted CRLF in some of the demos to LF

@@ -1,127 +1,127 @@
namespace CommandLine;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason as above for why the diff looks so bad

@LucDeCaf LucDeCaf marked this pull request as ready for review February 10, 2026 11:46
@LucDeCaf LucDeCaf requested a review from Chriztiaan February 10, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant