Godot DataWizard SQLite Toolkit
Godot DataWizard SQLite Toolkit
Declare a class, call save_to_db(). DataWizard maps your script variables to SQLite columns, creates and migrates the tables, keeps writes off the main thread, and hands the rows back as your own types — no SQL, no schema file, no serialisation code. There is no autoload to add and no plugin to enable.
It is a real SQLite database, not a save format. Windows, macOS, Linux and the browser.
The code you do not write
Saving an NPC and its inventory, by hand and with DataWizard. Both do the same thing.
By hand
# Open, and hope the schema matches what shipped last patch
db.query("CREATE TABLE IF NOT EXISTS npc (...)")
db.query("CREATE TABLE IF NOT EXISTS item (...)")
# Flatten the object yourself
var row := {
"name": npc.name,
"age": npc.age,
"pos": var_to_str(npc.position),
"stats": JSON.stringify(npc.stats),
}
db.insert_or_update("npc", row, "name")
# Now the children, and the link back to the parent
for it in npc.inventory:
db.insert_or_update("item", {
"owner": npc.name, "name": it.name, "qty": it.qty
}, "name")
# ...then all of it again, backwards, to load.
# ...on the main thread, so the frame waits.
# ...and again next patch, when you add a field.
With DataWizard
npc.save_to_db()
Every script variable is a column. Nested records go to their own table and come back with their parent. The write is queued on a background thread, so the call returns in microseconds and your frame never waits on the disk.
Loading is the mirror image:
var npcs := DataWiz.select(Npc.new())
# inventory is already populated
It never stalls your frame
Writes go to a dedicated writer thread and are batched into transactions. Reads lease a connection from a pool. Values are copied off your object on the calling thread, so the writer never touches a Godot object and you can keep mutating it the moment the call returns.
When a read is big enough to matter, select_async() moves it off the main thread and hands the rows to a callback: in our benchmark a 2,000-row load that blocked for 28 ms returned in 0.03 ms and delivered six frames later, with the game still running underneath.
It survives your next patch
The hard part of shipping a save system is not the first version — it is the second. Add a field to a class players already have data for and DataWizard adds the column on next use, then tells you through a schema_changed signal so you can backfill a default.
For anything bigger there is a versioned migration hook: get_user_version(), set_user_version() and migrate(target, step).
It is a real database
SQLite 3, bundled and tuned — WAL journaling, prepared-statement caching, pragma tuning. Not an opaque save blob.
That means you can open the file in any SQLite browser, write a tool against it, query it with DataWiz.query() when you want the full power of SQL, and hand a support ticket's save file to a script instead of a debugger.
Measured, not claimed
2,000 records, 500 of them carrying two nested children each, on a desktop Linux build.
Rule of thumb: a few hundred records is free, and past a few thousand — or a few hundred with nested children — reach for select_async() so the load never shows.
Quick start
Drop the GDExtension into res://addons/datawiz/ and define your records. There is no plugin to enable and no autoload to add — the addon registers a DataWiz singleton and opens the database named in Project Settings the first time anything uses it.
1. Nothing to boot
The database opens by itself. Connect sql_error wherever you like, and open a file yourself only when you want save slots.
func _ready() -> void:
DataWiz.sql_error.connect(func(context, message):
push_error("[DataWiz] %s: %s" % [context, message]))
print(DataWiz.db_connected()) # already true, unless auto_open is off
# Save slots: turn datawiz/database/auto_open off, then
func load_slot(n: int) -> void:
DataWiz.open("user://slot_%d.db" % n, 4) # path, reader pool size
2. Model your data
Use DWRecord resources to declare columns, primary keys, and unique constraints.
class_name InventoryRecord
extends DWRecord
# Convention variables configure storage and are not stored as columns.
var table_name := "inventory"
var unique_fields := ["slot"] # defaults to the generated guid
var index_fields := ["item_id"]
# Every other script variable becomes a column, exported or not.
# Prefix one with _ to keep it out of the database.
var slot: int = 0
var item_id: String = ""
var quantity: int = 1
3. Load records with select()
Pull rows back as live resource instances, nested records and all—no manual hydration needed.
# select() hands back Array[DWRecord]; the objects in it are your class.
var npcs := DataWiz.select(NPC.new(), "is_alive = 1")
for n in npcs:
print(n.name, " hp:", n.hp)
# Bind your values, never format them into the string.
var guards := DataWiz.select(NPC.new(), "faction = ?", ["Guard"])
# Want a statically typed array? assign() converts it.
var typed: Array[NPC] = []
typed.assign(DataWiz.select(NPC.new()))
4. Persist and iterate
Save a record and fetch it back—write SQL only when you need full control.
var record := InventoryRecord.new()
record.slot = 1
record.item_id = "healing_potion"
record.save_to_db() # queued; pass true to wait for the commit
# select(record, where, params, limit, order_by, offset)
var inventory := DataWiz.select(record, "", [], -1, "slot ASC")
for entry in inventory:
print(entry.slot, entry.item_id)
Everywhere your game goes
Windows, Linux, macOS (universal) and WebAssembly, against the Godot 4.5 extension API and every later 4.x.
The browser build is real: threads, WAL journaling and a database that survives a page reload. It needs cross-origin isolation headers so the browser will hand out threads; persistence to browser storage is handled for you — the platforms page has the details.
Explore the building blocks
Dive into focused component pages for the service singleton, schema resources, Autoload helpers, and utility functions. Each page highlights common usage patterns and the most important APIs.
DWService Singleton
Threaded SQLite for Godot 4.5+, registered for you
Owns the SQLite connection pool, the write queue and the schema cache. The addon registers it as the DataWiz singleton, so there is no autoload to add.
View referenceDWRecord Resource
A Resource that knows how to store itself
Subclass it and every script variable becomes a column. Nested records are stored in their own table and come back with their parent.
View referenceDWAutoLoad Node
A Node that persists itself as a single row
Base node for global state that should round-trip through SQLite using the same schema metadata as DWRecord.
View reference