Documentation

API Reference

Complete reference for every public type and method in GameSchema.

Generated Classes

GameSchema's code generator produces two files per table. You never touch them by hand — regenerate after any schema change.

  • Data class (e.g. EnemyData) — a plain C# class whose public fields map 1-to-1 to database columns. Fields are named after columns (case-insensitive). Supports fields and properties equally.
  • Static query class (e.g. Enemies) — contains typed Column<T> references for every column, plus convenience methods and query builder entry points. This is the only class you call in game code.
Generated output (abbreviated)
// Generated by GameSchema — do not edit
public static class Enemies
{
    public const string TableName = "enemies";

    // One typed Column<T> per column. Use these in every query —
    // no raw strings, full refactoring support.
    public static readonly Column<int>    Id       = new("id");
    public static readonly Column<string> Name     = new("name");
    public static readonly Column<int>    Level    = new("level");
    public static readonly Column<int>    WeaponId = new("weapon_id");

    // Simple one-liners
    public static EnemyData           Get(int id)                   { ... }
    public static List<EnemyData>     GetAll(Condition filter = null){ ... }
    public static int                 Count(Condition filter = null) { ... }

    // Full query builder
    public static TypedQuery<EnemyData> Query()      { ... }
    public static TypedQuery<EnemyData> From(int id) { ... }
}

// Generated data class
[Table("enemies")]
public class EnemyData
{
    [PrimaryKey]            public int    Id;
    [NotNull]               public string Name;
                            public int    Level;
    [ForeignKey("weapons")] public int    WeaponId;
}
Note: The Column<T> fields (Enemies.Level, Enemies.Name, …) are the heart of the type-safe API. Prefer them over any raw string wherever possible.

Simple Query Methods

For the most common access patterns you don't need a full query chain. These three methods cover the majority of game-code queries.

// Get a single row by primary key (returns null if not found)
EnemyData boss = Enemies.Get(42);

// Get all rows — no filter means every row
List<EnemyData> all = Enemies.GetAll();

// Get all rows matching a condition
List<EnemyData> tough = Enemies.GetAll(Enemies.Level > 5);

// Compound condition in one call
List<EnemyData> elite = Enemies.GetAll(
    (Enemies.Level > 10) & Enemies.Name.Like("%boss%")
);

// Count without allocating a list
int n = Enemies.Count(Enemies.Level > 5);
MethodReturnsDescription
Get(id)T or nullFetches a single row by primary key. Returns null if not found.
GetAll(filter?)List<T>Fetches all matching rows. Omit the filter to return every row.
Count(filter?)intReturns the count of matching rows without fetching data.

TypedQuery Builder

When you need more control — multiple conditions, ordering, paging — use the full query chain. Start with Enemies.Query() (all rows) or Enemies.From(id) (rows seeded by a PK condition). All builder methods return this for chaining, and the chain is lazy until you call a terminal method.

// Entry point: Enemies.Query() for all rows, .From(id) for a PK seed
var results = Enemies.Query()
    .Where(Enemies.Level > 5)
    .Where(Enemies.Name.Like("%goblin%"))   // AND-chained automatically
    .OrderBy(Enemies.Name)
    .Limit(10)
    .Offset(20)
    .FetchAll();                            // List<EnemyData>

// FetchOne — returns first match or default
EnemyData first = Enemies.Query()
    .Where(Enemies.Level > 5)
    .OrderByDesc(Enemies.Level)
    .FetchOne();

// FetchCount — no row allocation
int count = Enemies.Query()
    .Where(Enemies.Level > 5)
    .FetchCount();

// FetchExists — cheapest existence check
bool any = Enemies.Query()
    .Where(Enemies.Level > 100)
    .FetchExists();

Builder Methods

MethodDescription
Where(condition)Adds an AND-chained WHERE condition.
OrderBy(column)ORDER BY column ASC.
OrderByDesc(column)ORDER BY column DESC.
Limit(n)LIMIT n rows.
Offset(n)OFFSET n rows (use with Limit for paging).

Terminal Methods

MethodReturnsDescription
FetchAll()List<T>Execute and return all matching rows.
FetchOne()TExecute and return the first match, or default(T).
FetchCount()intReturn the count without fetching data.
FetchExists()boolReturn true if at least one row matches.
Fetch(column)TValReturn a single column value from the first match (see FK Navigation).
FetchAll(column)List<TVal>Return a column value from every matching row.
Fetch<TTarget>()TTargetReturn a full object mapped from the first match.
FetchAll<TTarget>()List<TTarget>Return a list of full objects from all matches.

FK Navigation

GameSchema offers two ways to traverse foreign key relationships. Choose the one that fits your situation — see FK Nav vs JOINs for a detailed comparison.

Option A — Follow Chain (single SQL query)

.Follow(fkColumn, refTable)adds a JOIN for each hop. The chain resolves in a single round-trip to the database. Ideal for reading a value you won't store, or when you need exactly one path through the schema.

Read a scalar value across two hops
// Traverse two FK hops in a single SQL query — no extra round-trips
string effectName = Enemies.From(42)
    .Follow(Enemies.WeaponId, "weapons")
    .Follow(Weapons.StatusEffectId, "status_effects")
    .Fetch(StatusEffects.Name);            // returns string
Fetch a full object at the end of a chain
// Fetch the full object at the end of a chain
WeaponData weapon = Enemies.From(42)
    .Follow(Enemies.WeaponId, "weapons")
    .Fetch<WeaponData>();                  // full mapped object
Follow a reverse FK (one-to-many)
// Follow a reverse relation (one → many)
// "All loot entries that reference this enemy's loot table"
List<LootEntryData> loot = Enemies.From(42)
    .Follow(Enemies.LootTableId, "loot_entries", "loot_table_id")
    .FetchAll<LootEntryData>();
MethodDescription
Follow(fkCol, refTable, refCol='id')Adds a JOIN from the current table to refTable using the FK column. Advances the chain to refTable.
Fetch(column)Terminal: returns a single typed value from the final table.
FetchAll(column)Terminal: returns a list of typed values from the final table.
Fetch<TTarget>()Terminal: returns a full mapped object from the final table.
FetchAll<TTarget>()Terminal: returns a list of mapped objects from the final table.

Option B — Navigation Properties (lazy loading)

When FK relationships are declared with [ForeignKey], the code generator can optionally emit lazy-loading navigation properties on the data class. Each property issues a separate query on first access and caches the result.

Generated navigation properties
// Generated lazy-loading navigation properties
// (requires FK attributes on the data class)
EnemyData boss = Enemies.Get(42);

WeaponData     weapon = boss.Weapon;           // lazy-loaded on first access
StatusEffect   effect = boss.Weapon.StatusEffect;
string         name   = boss.Weapon.StatusEffect.Name;
Note: Navigation properties are convenient but each uninitialized access is a separate SQL query. If you need multiple FK values from many rows, prefer the Follow chain or a JOIN to avoid N+1 queries. See Best Practices.

Column References & Operators

Every Column<T> field on a generated class supports C# operator overloads that produce a Condition object. Conditions are the type that Where(), GetAll(), and Count() accept.

// Equality
Enemies.Level == 5          // WHERE level = 5
Enemies.Level != 5          // WHERE level != 5

// Comparison
Enemies.Level >  5          // WHERE level > 5
Enemies.Level >= 5          // WHERE level >= 5
Enemies.Level <  5          // WHERE level < 5
Enemies.Level <= 5          // WHERE level <= 5

// Pattern matching
Enemies.Name.Like("%goblin%")   // WHERE name LIKE '%goblin%'
Enemies.Name.Like("boss_%")     // WHERE name LIKE 'boss_%'

// Range
Enemies.Level.Between(5, 10)    // WHERE level BETWEEN 5 AND 10

// Set membership
Enemies.Level.In(1, 5, 10)      // WHERE level IN (1, 5, 10)
Enemies.Level.NotIn(1, 2, 3)    // WHERE level NOT IN (1, 2, 3)

// Null checks
Enemies.WeaponId.IsNull()       // WHERE weapon_id IS NULL
Enemies.WeaponId.IsNotNull()    // WHERE weapon_id IS NOT NULL
ExpressionSQL producedNotes
col == valuecol = ?
col != valuecol != ?
col > valuecol > ?
col >= valuecol >= ?
col < valuecol < ?
col <= valuecol <= ?
col.Like(pattern)col LIKE ?Use % and _ wildcards.
col.Between(min, max)col BETWEEN ? AND ?Inclusive on both ends.
col.In(v1, v2, …)col IN (?, ?, …)
col.NotIn(v1, v2, …)col NOT IN (?, ?, …)
col.IsNull()col IS NULL
col.IsNotNull()col IS NOT NULL
Note: All parameters are always bound as prepared-statement placeholders (?). GameSchema never interpolates values directly into SQL strings, so SQL injection is not possible through the typed API.

Combining Conditions

Condition objects can be combined with & (AND) and | (OR). Standard C# operator precedence applies — use parentheses to group as needed.

// & → AND
var c = Enemies.Level > 5 & Enemies.Name.Like("%boss%");

// | → OR
var c = Enemies.Level > 20 | Enemies.Name == "Dragon King";

// Compound: (Level > 5 AND Name LIKE '%goblin%') OR Level > 20
var c = (Enemies.Level > 5 & Enemies.Name.Like("%goblin%"))
      | Enemies.Level > 20;

// Use in GetAll, Count, or Where
var results = Enemies.GetAll(c);
int n       = Enemies.Count(c);
var q       = Enemies.Query().Where(c).OrderBy(Enemies.Level).FetchAll();
Tip: A single Where() call always ANDs with previous ones. Use | only when you need a real OR — otherwise multiple .Where() calls are cleaner.

C# Data Model Attributes

Attributes in the GameSchema.Attributesnamespace let you control how the editor reads your classes and generates schemas. They're optional — the query mapper always works by matching member names to column names case-insensitively.

using GameSchema.Attributes;

// Maps the class to a specific table name.
// Omit to use the lowercase class name ("enemydata" → always specify this).
[Table("enemies")]
public class EnemyData
{
    // Marks this field as the PRIMARY KEY column
    [PrimaryKey]
    public int Id;

    // Maps to a differently-named column (e.g. "display_name" in the DB)
    [Column("display_name")]
    public string Name;

    // Adds NOT NULL to the schema
    [NotNull]
    public int Level;

    // Sets a default value in the schema
    [DefaultValue(1)]
    public int Tier;

    // Declares a FK relationship; the editor shows a dropdown of valid values
    [ForeignKey("weapons", "id")]
    public int WeaponId;

    // Same, but shows "name" column in the editor dropdown instead of the raw ID
    [ForeignKey("weapons", "id", DisplayColumn = "name")]
    public int SecondaryWeaponId;

    // Column stores an Addressable address; editor shows an asset picker
    [AssetRefColumn(typeof(Sprite))]
    public AssetRef<Sprite> Icon;

    // Excluded from DB mapping entirely — won't be read or written
    [Ignore]
    public string RuntimeOnlyCache;
}
AttributeTargetDescription
[Table(name)]class / structMaps the type to a named table. Omit to use the lowercased class name.
[Column(name)]field / propertyMaps the member to a differently-named column.
[PrimaryKey]field / propertyMarks the primary key column. Used by Get() and From().
[NotNull]field / propertyAdds NOT NULL to the column schema.
[DefaultValue(val)]field / propertySets the column default in the schema.
[ForeignKey(table, col)]field / propertyDeclares a FK reference. The editor renders a dropdown of valid values. Set DisplayColumn to show a friendly column instead of the raw ID.
[AssetRefColumn(typeof(T))]field / propertyMarks the column as an Addressable asset reference. The editor renders a drag-and-drop asset picker.
[Ignore]field / propertyExcludes the member from DB mapping entirely.

AssetRef<T>

AssetRef<T> is a lightweight struct that wraps an Addressable address string. The database stores only the string; the struct provides strongly-typed loading helpers so you never have to cast or manage AsyncOperationHandle manually.

Loading helpers require the GAMESCHEMA_ADDRESSABLES module. Enable it in Window → GameSchema → Welcome → Modules.

// AssetRef<T> stores the Addressable address string in the DB.
// The GAMESCHEMA_ADDRESSABLES module must be enabled for the loading helpers.
// Enable it via Window > GameSchema > Welcome > Modules.

// In your data class:
public AssetRef<Sprite>     Icon;
public AssetRef<GameObject> Prefab;
public AssetRef<AudioClip>  SoundEffect;

// ── async / await ──────────────────────────────────────────────────────────
Sprite icon = await enemy.Icon.LoadAsync();
myRenderer.sprite = icon;

// ── Callback (no async overhead) ───────────────────────────────────────────
enemy.Icon.Load(sprite => myImage.sprite = sprite);

// With error handling:
enemy.Icon.Load(
    onLoaded: sprite => myImage.sprite = sprite,
    onFailed: err    => Debug.LogError(err)
);

// ── Coroutine ──────────────────────────────────────────────────────────────
yield return enemy.Prefab.LoadCoroutine(go => Instantiate(go));

// ── Blocking (only if asset is cached locally) ─────────────────────────────
Material mat = enemy.Material.LoadSync();

// ── Instantiate a prefab ───────────────────────────────────────────────────
GameObject go = await enemy.Prefab.InstantiateAsync(parentTransform);
enemy.Prefab.Instantiate(go => go.name = "SpawnedEnemy");

// ── Release when done ──────────────────────────────────────────────────────
enemy.Icon.Release(icon);
Addressables.ReleaseInstance(go);

// ── Without the module: use the address string directly ───────────────────
if (enemy.Icon.IsValid)
    Addressables.LoadAssetAsync<Sprite>(enemy.Icon.Address);
MethodDescription
AddressThe raw Addressable address string.
IsValidTrue if Address is non-null and non-empty.
LoadAsync()async Task<T> — preferred for async/await code.
Load(onLoaded, onFailed?)Callback-based, no async overhead.
LoadCoroutine(onLoaded)IEnumerator for use inside a coroutine.
LoadSync()Blocking load — only use if asset is already cached locally.
InstantiateAsync(parent?)async Task<GameObject> — for AssetRef<GameObject> only.
Instantiate(onInstantiated, parent?)Callback version of InstantiateAsync.
Release(asset)Returns a loaded asset to the Addressables pool.
ReleaseInstance(go)Releases an instantiated GameObject.

Low-Level API

The generated classes cover nearly every game-code use case. Reach for the low-level API when you need raw JOINs, custom SQL, or direct database access. Avoid it in hot paths — the typed API is faster to write and safer to refactor.

QueryBuilder (string-based)

Accessed via DB.Table("tableName"). Accepts raw column name strings and the Op enum for comparison operators. Supports all JOIN types not available on the typed API.

// Start a QueryBuilder directly (accepts raw strings — use sparingly)
var results = DB.Table("enemies")
    .Where("level", Op.Gt, 5)
    .Where("name", Op.Like, "%goblin%")
    .OrderBy("name")
    .Limit(10)
    .Get<EnemyData>();

// JOINs (only available on low-level QueryBuilder)
var joined = DB.Table("enemies")
    .JoinFK("weapon_id", "weapons")                   // LEFT JOIN weapons ON enemies.weapon_id = weapons.id
    .Join("loot_tables", "enemies.loot_id", "loot_tables.id") // INNER JOIN
    .Select("enemies.id", "enemies.name", "weapons.damage")
    .Where("enemies.level", Op.Gt, 5)
    .Get<EnemyCombatData>();

// Convenience shorthands
DB.Table("enemies").WhereEquals("name", "Goblin King").Get<EnemyData>();
DB.Table("enemies").WhereIn("level", 1, 5, 10).Get<EnemyData>();
DB.Table("enemies").WhereBetween("level", 5, 10).Get<EnemyData>();

GameDatabase (direct SQL)

Accessed via DB.Main. Use for write operations, raw scalar queries, or any SQL the higher APIs don't support.

// Direct access to the underlying GameDatabase (escape hatch)
GameDatabase db = DB.Main;

// Raw execute (INSERT / UPDATE / DELETE)
db.Execute("UPDATE enemies SET level = level + 1 WHERE id = ?", 42);

// Raw query → List<Dictionary<string, object>>
var rows = db.Query("SELECT * FROM enemies WHERE level > ?", 5);

// Scalar — first column of first row
int count = (int)(long)db.Scalar("SELECT COUNT(*) FROM enemies");

// Find by PK (low-level)
var enemy = DB.Find<EnemyData>("enemies", 42);
Note: DB.Main opens the database read-only at runtime. Write operations (Execute) work only in the Editor or in builds where you've opened the DB read-write. Runtime game data should be read-only by design.

Op enum reference

OpSQLNotes
Op.Eq=
Op.Neq!=
Op.Gt>
Op.Gte>=
Op.Lt<
Op.Lte<=
Op.LikeLIKE
Op.InIN (...)Pass value as object[]
Op.NotInNOT IN (...)Pass value as object[]
Op.BetweenBETWEEN ? AND ?Pass min as value, max as value2
Op.IsNullIS NULLvalue not required
Op.IsNotNullIS NOT NULLvalue not required