Quests & Progress
Progress tracking is a lookup-by-owner workload with a simple state flip. It shows how a declared index keeps per-player reads fast and how a command performs the write.
The QuestProgress table here is the same one introduced in
Build Your First Game; this page adds the GetOpenQuests
query and the CompleteQuest command on top of it.
Schema
table QuestProgress(plural: QuestProgressRows, persistence: local, capacity: 2048, type_id: 3) {
Id: int @id
PlayerId: int
QuestId: int
Completed: bool
@@index(fields: [PlayerId], name: "QuestProgress_ByPlayer", kind: lookup)
}
Query the player's open quests
query GetOpenQuests(playerId: int) -> QuestProgress[] {
from QuestProgress q
| filter q.PlayerId == @playerId && !q.Completed
| sort q.QuestId
}
The lookup index on PlayerId means this filter is a direct index probe, not a table
scan — even with thousands of rows. The query never names the index: the planner proves
the filter matches it and chooses the probe.
Complete a quest with a command
module Quests {
type CompleteQuestResult {
questId: int
}
command CompleteQuest(playerId: int, questId: int) -> CompleteQuestResult
kind: local
{
upsert QuestProgress
| key { PlayerId = @playerId, QuestId = @questId }
| set { Completed = true }
return { questId = @questId }
}
}
Count what is left
A scalar reduction is an aggregate with one unnamed aggregate, and the signature says it
returns a non-nullable long:
query OpenQuestCount(playerId: int) -> long {
from QuestProgress q
| filter q.PlayerId == @playerId && !q.Completed
| aggregate count()
}
Call it from C#
using ConjureDB;
using var ctx = DbContextBuilder<GameDbContext>.Create()
.WithDataDirectory("./data")
.WithDefaultSnapshot()
.Build();
var open = ctx.QuestProgressRows.GetOpenQuests(playerId: 1);
See Indexing for how to choose lookup vs sorted_set / sorted_list
vs unique indexes for each access pattern.