Repository navigation
SQL Databases
Five providers, one model. Birko creates your tables from attributes, so there is no schema file to maintain to get started.
| Provider | Store class | Settings |
|---|---|---|
| SQLite |
SQLiteStore<T> / AsyncSQLiteStore<T>
|
SqLiteSettings(location, name) |
| PostgreSQL | PostgreSQLStore<T> |
PostgreSqlSettings |
| MySQL | MySQLStore<T> |
MySqlSettings |
| SQL Server | MSSqlStore<T> |
MSSqlSettings |
| TimescaleDB | TimescaleDBStore<T> |
TimescaleDBSettings |
All settings classes live in their provider's namespace, e.g. Birko.Data.SQL.SqLite.Stores.
using Birko.Data.Models;
using Birko.Data.SQL.Attributes;
[Table("Products")]
[CompositeIndex("ux_product_sku", nameof(TenantGuid), nameof(Sku), IsUnique = true)]
public class Product : AbstractDatabaseModel
{
[MaxLengthField(200), RequiredField]
public string Name { get; set; } = string.Empty;
[MaxLengthField(50), IndexedField("ix_product_sku")]
public string Sku { get; set; } = string.Empty;
[PrecisionField(18), ScaleField(2)]
public decimal Price { get; set; }
[UtcField]
public DateTime PublishedAt { get; set; }
public Guid TenantGuid { get; set; }
[IgnoreField]
public string DisplayLabel => $"{Sku} — {Name}";
}| Attribute | Effect |
|---|---|
[Table("name")] |
table name |
[MaxLengthField(n)] |
VARCHAR(n) rather than unbounded text |
[PrecisionField(p)] + [ScaleField(s)]
|
DECIMAL(p,s) |
[RequiredField] |
NOT NULL |
[PrimaryField] |
primary key |
[UniqueField] |
unique constraint |
[IncrementField] |
auto-increment |
[IndexedField("ix_name")] |
single-column index — the name is required |
[CompositeIndex(name, …props, IsUnique =)] |
multi-column index, declared on the class |
[NamedField("col")] |
map to a different column name |
[UtcField] |
store a true instant; reads back as Kind = Utc
|
[IgnoreField] |
do not persist |
[PrecisionField] without [ScaleField] gives scale 0 on SQL Server and MySQL — money
truncated to whole units — and REAL on SQLite. Always use both.
DateTime is a wall clock, not an instant. It stores the components you give it and
reads back as Unspecified. If you mean a moment in time, use [UtcField].
CREATE TABLE IF NOT EXISTS runs on first use; changing
a model afterwards does not change an existing table. Use a migration, or drop the table in
development. Birko.Health.Data.SQL's schema-drift check reports the mismatch.
using var uow = SqlUnitOfWork.FromStore(store);
await uow.BeginAsync();
try
{
await store.CreateAsync(a);
await store.CreateAsync(b);
await uow.CommitAsync();
}
catch { await uow.RollbackAsync(); throw; }Stores inside the boundary join it automatically, including bulk writes.
💡 MySQL commits implicitly around DDL. If a store's first ever call happens inside your
transaction, its CREATE TABLE commits the transaction on MySQL. Touch your stores once at startup
if that matters.
For controlled schema change, Birko.Data.Migrations.SQL:
using Birko.Data.Migrations;
using Birko.Data.Migrations.Context;
using Birko.Data.Patterns.Schema;
public class AddProductSku : IMigration
{
public long Version => 202601010001;
public string Name => "AddProductSku";
public string Description => "Adds the Sku column to Products";
public DateTime CreatedAt => new(2026, 1, 1);
public void Up(IMigrationContext ctx) =>
ctx.Schema.CreateCollection("Products")
.WithField("Guid", FieldType.Guid, isPrimary: true)
.WithField("Name", FieldType.String, maxLength: 200, isRequired: true)
.WithField("Price", FieldType.Decimal, precision: 18, scale: 2)
.Build();
public void Down(IMigrationContext ctx) { /* drop, if your backend supports it */ }
}All six members are required: Version, Name, Description, CreatedAt, Up and Down.
IMigrationContext is in Birko.Data.Migrations.Context, and FieldType in Birko.Data.Patterns.Schema.
ISchemaBuilder offers CreateCollection and CreateIndex — there is no AlterCollection.
The runner records applied versions in its own table. Migration backends also exist for MongoDB, ElasticSearch, RavenDB, CosmosDB, InfluxDB and TimescaleDB.
FieldDescriptor without maxLength
produces unbounded text; without precision and scale, a bare DECIMAL.
Birko.Data.SQL.View builds SQL views from attribute-mapped view classes, including aggregates.
Persistent views are created in the database; on-the-fly views are composed per query.
| Note | |
|---|---|
| PostgreSQL | folds unquoted identifiers to lower case — Birko handles this, but it is why identifier handling looks fussy in the source |
| MySQL | no partial indexes; commits implicitly around DDL |
| SQL Server | a limited read needs an ORDER BY; Birko synthesises one when you don't supply it |
| SQLite | runs in WAL mode by default (set JournalMode to change); serialises writes at the file level |
| TimescaleDB | set TimeColumn in settings and tables become hypertables automatically |
- Data and Stores — the non-SQL backends
- Core Concepts — stores, settings, filters
Start here
Data
Services
UI