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 typedColumn<T>references for every column, plus convenience methods and query builder entry points. This is the only class you call in game code.
// 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;
}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);| Method | Returns | Description |
|---|---|---|
| Get(id) | T or null | Fetches 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?) | int | Returns 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
| Method | Description |
|---|---|
| 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
| Method | Returns | Description |
|---|---|---|
| FetchAll() | List<T> | Execute and return all matching rows. |
| FetchOne() | T | Execute and return the first match, or default(T). |
| FetchCount() | int | Return the count without fetching data. |
| FetchExists() | bool | Return true if at least one row matches. |
| Fetch(column) | TVal | Return 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>() | TTarget | Return a full object mapped from the first match. |
| FetchAll<TTarget>() | List<TTarget> | Return a list of full objects from all matches. |
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| Expression | SQL produced | Notes |
|---|---|---|
| col == value | col = ? | |
| col != value | col != ? | |
| col > value | col > ? | |
| col >= value | col >= ? | |
| col < value | col < ? | |
| col <= value | col <= ? | |
| 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 |
?). 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();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;
}| Attribute | Target | Description |
|---|---|---|
| [Table(name)] | class / struct | Maps the type to a named table. Omit to use the lowercased class name. |
| [Column(name)] | field / property | Maps the member to a differently-named column. |
| [PrimaryKey] | field / property | Marks the primary key column. Used by Get() and From(). |
| [NotNull] | field / property | Adds NOT NULL to the column schema. |
| [DefaultValue(val)] | field / property | Sets the column default in the schema. |
| [ForeignKey(table, col)] | field / property | Declares 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 / property | Marks the column as an Addressable asset reference. The editor renders a drag-and-drop asset picker. |
| [Ignore] | field / property | Excludes 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);| Method | Description |
|---|---|
| Address | The raw Addressable address string. |
| IsValid | True 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);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
| Op | SQL | Notes |
|---|---|---|
| Op.Eq | = | |
| Op.Neq | != | |
| Op.Gt | > | |
| Op.Gte | >= | |
| Op.Lt | < | |
| Op.Lte | <= | |
| Op.Like | LIKE | |
| Op.In | IN (...) | Pass value as object[] |
| Op.NotIn | NOT IN (...) | Pass value as object[] |
| Op.Between | BETWEEN ? AND ? | Pass min as value, max as value2 |
| Op.IsNull | IS NULL | value not required |
| Op.IsNotNull | IS NOT NULL | value not required |