# Declaring table types

Declare a column shape once with [TableType], bind the table name at runtime, and get fully typed results. The right tool for per-tenant, sharded, and time-partitioned tables.

Some tables are created at runtime: per tenant, per period, or sharded (`audit_2026_06`, `tenant_42_orders`). Their **names** are not known at build time, but their **column shape** usually is. A `[TableType]` class lets you declare that shape once and bind the **name at runtime**, returning the **typed** entity. Only the name is dynamic, so the whole feature stays NativeAOT-safe.

## Declaring a table type

```csharp
using Socigy.OpenSource.DB.Attributes;

[TableType]
public partial class AuditEntry          // the generator implements IDbTableType<AuditEntry>
{
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public required string Action { get; set; }
    public int Amount { get; set; }
    public DateTime At { get; set; }
}
```

Columns use the same attributes as `[Table]` (`[PrimaryKey]`, `[Column]`, `[JsonColumn]`, `[Encrypted]`, and the rest). A class may carry **both** `[Table("default")]` and `[TableType]`: a fixed default name plus a runtime override.

The generator emits two static entry points on the class:

| Method | Returns | Use |
|--------|---------|-----|
| `AuditEntry.WithTableName(string name)` | `DynamicTable<AuditEntry>` | bind a runtime name |
| `AuditEntry.MapTypeAsync(string name, DbConnection conn, bool force = false, CancellationToken ct = default)` | `Task<DynamicTable<AuditEntry>>` | bind and auto-discover extra columns (see [Runtime operations](/database/0.3.2/dynamic-tables/runtime-operations)) |

## Querying and writing without a context

Bind the runtime name with `WithTableName`, attach a connection, then use the full typed API. `DynamicTable<T>` lives in `Socigy.OpenSource.DB.Core.Dynamic`.

```csharp
await using var conn = factory.Create();
await conn.OpenAsync();

var t = AuditEntry.WithTableName("audit_2026_06");

List<AuditEntry> rows = await t.WithConnection(conn).Query(x => x.UserId == id).ToListAsync();
AuditEntry? first    = await t.WithConnection(conn).Query(x => x.Amount > 100).FirstOrDefaultAsync();
long count           = await t.WithConnection(conn).Query(x => x.Action == "login").CountAsync();
int sum              = await t.WithConnection(conn).SumAsync<int>(x => x.Amount) ?? 0;

await t.WithConnection(conn).InsertAsync(new AuditEntry { UserId = id, Action = "login", At = DateTime.UtcNow });
await t.WithConnection(conn).InsertMultipleAsync(batch);                 // batched multi-row INSERT
await t.WithConnection(conn).UpdateAsync(changed, x => x.Id == changed.Id);
await t.WithConnection(conn).DeleteAsync(x => x.At < cutoff);
```

> **NOTE** Each `WithTableName(...)` returns a fresh handle. Build one per query rather than reusing a handle that already has a predicate.

The handle exposes the full surface: `Query`/`Where`, `OrderBy`, `Limit`/`Offset`, `WithCustomColumns`; the readers `ExecuteAsync` (stream), `ToListAsync`, `FirstOrDefaultAsync`, `ExistsAsync`, `CountAsync`; the aggregates `SumAsync`/`AvgAsync`/`MinAsync`/`MaxAsync`/`ScalarAsync`; the writers `InsertAsync`/`InsertMultipleAsync`/`UpdateAsync`/`DeleteAsync`; and the lifecycle methods covered in [Runtime operations](/database/0.3.2/dynamic-tables/runtime-operations).

## Through the context

The generated context exposes a generic `DynamicTable<T>(name)` that joins the unit-of-work scope, so it enlists the ambient transaction and shares the connection:

```csharp
await db.ExecuteTransactionAsync(async d =>
{
    await d.DynamicTable<AuditEntry>("audit_2026_06").InsertAsync(entry);
    long count = await d.DynamicTable<AuditEntry>("audit_2026_06")
        .Query(x => x.UserId == id)
        .CountAsync();
});
```

> **WARNING** The runtime table name is a SQL identifier you supply (quoted automatically). Treat it like a `[Table]` name, never as untrusted user input.
