Queries
Fluent, nested, typed reads and writes — and aggregates that fold server-side.
Every query goes through the generated client. There is no query string to build and no result to hand-map; the builder is typed against your schema and the rows come back as typed structs.
Reads
db, _ := tracker.Connect("rad://localhost:7237")
board, _, _ := db.Boards.Query().
IDEq(id).
IncludeTasks(func(t *tracker.TaskInclude) {
t.OrderByPriority().IncludeAssignee()
}).
First(ctx)
Includes return nested data — a board with its tasks, each task with its assignee — as one round trip. The result is shaped like your data, not flattened into a join you have to unpick.
Aggregations
Aggregates fold over matching rows on the server; not one row is shipped back.
open, _ := db.Tasks.Query().
BoardIDEq(id).
StatusNe("done").
Count(ctx) // int64
avg, _ := db.Tasks.Query().
BoardIDEq(id).
AvgEstimate(ctx) // *float64 — nil when there are no rows
Count is never null. sum, avg, min, and max return a pointer that is
nil when nothing matched — an empty average is nothing, not zero.
Writes and transactions
err := db.Tx(ctx, func(tx *tracker.Tx) error {
board, err := tx.Boards.Create(ctx, tracker.BoardCreate{Name: "Launch"})
if err != nil {
return err
}
_, err = tx.Tasks.Create(ctx, tracker.TaskCreate{
BoardID: board.ID, Title: "Ship it",
})
return err
})
Transactions are serializable. When two of them race, the loser fails with a
retryable conflict error — check tracker.IsConflict(err) and run the whole
transaction again.
The same, in TypeScript
const db = connect("rad://localhost:7237");
const board = await db.boards.query()
.idEq(id)
.includeTasks((t) => t.orderByPriority().includeAssignee())
.first();
const open = await db.tasks.query()
.boardIdEq(id).statusNe("done").count();
Same wire, same shapes, same loop — just generated for a different language.