Repository navigation
Persistence System
SparkEngine provides two independent persistence layers for different use cases:
- Save System -- ECS-aware game state serialization to compressed JSON files. Designed for single-player save slots, quicksave/quickload, and autosave rotation.
- AsyncDatabase (this page) -- TrinityCore-inspired async database layer with connection pooling, prepared statements, and transaction support. Designed for server-side and multiplayer persistence.
Source: SparkEngine/Source/Engine/Persistence/AsyncDatabase.h, AsyncDatabase.cpp
Namespace: Spark::Persistence
AsyncDatabase is modeled after TrinityCore's DatabaseWorkerPool. It provides:
- A thread pool where each worker owns a dedicated database connection
- Prepared statements registered by ID and replicated to all connections
- Async queries via
std::futureor main-thread callbacks - Atomic transaction batching (all-or-nothing)
- A synchronous query path for blocking operations
The default backend is a file-based key-value store (SQLiteConnection) that requires no external libraries. The IDatabaseConnection interface is designed to be swapped for real SQLite, MySQL, or PostgreSQL backends without changing calling code.
┌──────────────────────────────────────────────────────────────────────┐
│ Game / Server Code │
│ AsyncQuery() / AsyncQueryWithCallback() / SyncQuery() │
├──────────────────────────────────────────────────────────────────────┤
│ AsyncDatabasePool │
│ │
│ ┌─────────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Work Queue (mutex) │ │ Callback Queue (mutex) │ │
│ │ WorkItem { stmtId, │ │ CompletedCallback { callback, │ │
│ │ params, promise, │ │ result } │ │
│ │ callback } │ │ │ │
│ └────────────┬────────────┘ └──────────────┬───────────────────┘ │
│ │ │ │
│ ┌────────────▼────────────┐ ProcessCallbacks() dispatches on │
│ │ Worker Threads (N) │ main thread each frame │
│ │ ┌───────────────────┐ │ │
│ │ │ IDatabaseConnection│ │ │
│ │ │ (1 per thread) │ │ │
│ │ └───────────────────┘ │ │
│ └─────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ IDatabaseConnection │
│ (abstract: Open, Close, Execute, │
│ PrepareStatement, BeginTransaction) │
├──────────────────────────────────────────────────────────────────────┤
│ SQLiteConnection │
│ (file-based key-value fallback store) │
└──────────────────────────────────────────────────────────────────────┘
A variant representing a single column value from a query result:
using QueryValue = std::variant<std::monostate, int64_t, double, std::string, std::vector<uint8_t>>;std::monostate represents SQL NULL.
A single row of query results with typed accessors:
| Method | Description |
|---|---|
GetInt(col) |
Get column as int64_t. Throws std::bad_variant_access if wrong type. |
GetDouble(col) |
Get column as double. Throws std::bad_variant_access if wrong type. |
GetString(col) |
Get column as const std::string&. Throws std::bad_variant_access if wrong type. |
IsNull(col) |
Returns true if the column holds monostate (NULL) or is out of range. |
Result of a database query or transaction:
struct QueryResult
{
bool success = false;
std::string errorMessage;
std::vector<QueryRow> rows;
int affectedRows = 0;
int64_t lastInsertId = 0;
bool HasRows() const;
size_t RowCount() const;
};| Field | Description |
|---|---|
success |
Whether the query completed without error |
errorMessage |
Error description if success is false |
rows |
Result rows for SELECT-type queries |
affectedRows |
Number of rows affected by INSERT/UPDATE/DELETE |
lastInsertId |
Auto-increment ID of the last inserted row |
Handle for binding parameters to a prepared statement before execution:
struct PreparedStatementData
{
PreparedStatementID id = 0;
std::vector<PreparedStatementParam> params;
void SetInt(size_t index, int64_t value);
void SetDouble(size_t index, double value);
void SetString(size_t index, const std::string& value);
void SetNull(size_t index);
void ClearParams();
};Parameters auto-resize to accommodate the given index. Call ClearParams() to reuse the statement with new bindings.
A batch of queries executed atomically (all-or-nothing):
struct Transaction
{
std::vector<std::pair<PreparedStatementID, std::vector<PreparedStatementParam>>> queries;
void Append(PreparedStatementID stmtId, std::vector<PreparedStatementParam> params = {});
size_t Size() const;
};On execution, the pool wraps the batch in BEGIN / COMMIT. If any query fails, the entire transaction is rolled back.
Abstract interface for a single database connection. Implement this to add a new database backend:
| Method | Description |
|---|---|
Open(connectionString) |
Open the connection. Returns true on success. |
Close() |
Close the connection and release resources. |
IsOpen() |
Check whether the connection is currently open. |
PrepareStatement(id, sql) |
Register a prepared statement SQL string by ID. |
Execute(id, params) |
Execute a prepared statement with bound parameters. |
ExecuteRaw(sql) |
Execute a raw SQL string (no parameter binding). |
BeginTransaction() |
Start a transaction. |
CommitTransaction() |
Commit the current transaction. |
RollbackTransaction() |
Roll back the current transaction. |
File-based key-value fallback implementing IDatabaseConnection. This is not a real SQLite binding -- it stores data as tab-separated key-value pairs on disk and supports a minimal command set:
| Command | Syntax | Description |
|---|---|---|
SET |
SET key value |
Store a key-value pair |
GET |
GET key |
Retrieve a value by key |
DELETE |
DELETE key |
Remove a key-value pair |
KEYS |
KEYS [prefix] |
List all keys, optionally filtered by prefix |
Parameters in prepared statements use ?0, ?1, ?2 placeholders, substituted by string replacement before execution. Transaction support uses an in-memory snapshot for rollback.
The SQLiteConnection is intended as a zero-dependency fallback. For production use, replace it with a real SQLite, MySQL, or PostgreSQL implementation of IDatabaseConnection.
Thread-pool based async database with connection pooling. This is the primary API surface for game and server code.
| Method | Description |
|---|---|
Open(connectionString, poolSize) |
Create poolSize worker threads, each with its own database connection. Returns true if all connections opened successfully. Minimum pool size is 1. |
Close() |
Signal all workers to stop, join threads, close all connections. |
PrepareStatement(id, sql) |
Register a prepared statement by ID. Replicates to all live connections. Call during initialization before issuing queries. |
AsyncQuery(id, params) |
Enqueue a query and return a std::future<QueryResult>. |
AsyncQueryWithCallback(id, params, callback) |
Enqueue a query with a completion callback. The callback is dispatched on the main thread via ProcessCallbacks(). |
AsyncTransaction(transaction) |
Execute a Transaction atomically and asynchronously. Returns a std::future<QueryResult>. |
SyncQuery(id, params) |
Execute a query synchronously on the calling thread. Uses connection[0] guarded by a dedicated mutex. Blocks the caller. |
ProcessCallbacks() |
Dispatch completed async callbacks on the calling (main) thread. Call once per frame from the main loop. |
IsOpen() |
Check whether the pool is open (atomic). |
GetPendingQueryCount() |
Number of queries currently in the work queue (atomic). |
GetPoolSize() |
Number of worker threads. |
| Mechanism | Protection | Notes |
|---|---|---|
Work queue (m_workQueue) |
std::mutex + std::condition_variable
|
Workers wait on the CV; producers notify on enqueue |
Callback results (m_completedCallbacks) |
std::mutex |
Workers push; main thread swaps and dispatches in ProcessCallbacks()
|
Sync query (m_syncConnection) |
std::mutex (m_syncMutex) |
Guards connection[0] for SyncQuery() calls from the main thread |
Pending count (m_pendingCount) |
std::atomic<int> |
Incremented on enqueue, decremented after execution |
| Open/stopping flags | std::atomic<bool> |
Lock-free read from any thread |
Key rule: ProcessCallbacks() must be called on the main thread each frame. Callback functions execute on the main thread, so they can safely access game state without additional synchronization.
using namespace Spark::Persistence;
AsyncDatabasePool dbPool;
// Open with 2 worker threads
dbPool.Open("data/server.db", 2);
// Register prepared statements by ID
constexpr PreparedStatementID STMT_LOAD_CHARACTER = 1;
constexpr PreparedStatementID STMT_SAVE_CHARACTER = 2;
constexpr PreparedStatementID STMT_DELETE_CHARACTER = 3;
dbPool.PrepareStatement(STMT_LOAD_CHARACTER, "GET character_?0");
dbPool.PrepareStatement(STMT_SAVE_CHARACTER, "SET character_?0 ?1");
dbPool.PrepareStatement(STMT_DELETE_CHARACTER, "DELETE character_?0");// Build parameters
std::vector<PreparedStatementParam> params;
params.push_back({int64_t{42}}); // character ID
// Fire async query
auto future = dbPool.AsyncQuery(STMT_LOAD_CHARACTER, std::move(params));
// ... later, check the result ...
if (future.wait_for(std::chrono::milliseconds(0)) == std::future_status::ready)
{
QueryResult result = future.get();
if (result.success && result.HasRows())
{
const std::string& data = result.rows[0].GetString(0);
// Deserialize character from data
}
}std::vector<PreparedStatementParam> params;
params.push_back({int64_t{42}});
dbPool.AsyncQueryWithCallback(STMT_LOAD_CHARACTER, std::move(params),
[](QueryResult result)
{
// This runs on the main thread via ProcessCallbacks()
if (result.success && result.HasRows())
{
const std::string& data = result.rows[0].GetString(0);
// Safe to access game state here
}
});Transaction txn;
// Save multiple character fields atomically
std::vector<PreparedStatementParam> nameParams;
nameParams.push_back({std::string{"42"}});
nameParams.push_back({std::string{"{\"name\":\"Hero\",\"level\":10}"}});
txn.Append(STMT_SAVE_CHARACTER, std::move(nameParams));
std::vector<PreparedStatementParam> inventoryParams;
inventoryParams.push_back({std::string{"inv_42"}});
inventoryParams.push_back({std::string{"{\"slots\":[...]}"}});
txn.Append(STMT_SAVE_CHARACTER, std::move(inventoryParams));
// Execute atomically -- if any query fails, all are rolled back
auto future = dbPool.AsyncTransaction(std::move(txn));// In the main game/server loop
void GameLoop()
{
// ... update game state ...
// Dispatch completed database callbacks on the main thread
dbPool.ProcessCallbacks();
}// On shutdown, close the pool (joins all worker threads)
dbPool.Close();AsyncDatabase and SaveSystem are independent persistence layers serving different purposes:
| Aspect | SaveSystem | AsyncDatabase |
|---|---|---|
| Purpose | Single-player save/load | Server-side / multiplayer persistence |
| Data model | ECS world snapshots | Arbitrary key-value or relational data |
| I/O model | Synchronous file I/O | Async thread pool with futures/callbacks |
| Format | Compressed JSON (miniz) | Backend-dependent (file-based KV store by default) |
| Thread safety | Main thread only | Thread-safe work queue; callbacks on main thread |
| Integration | Fully wired into engine | Available but not wired into engine startup |
Neither system depends on or communicates with the other. A game can use both simultaneously -- for example, SaveSystem for local save files and AsyncDatabase for leaderboards or server-side character storage.
Note: AsyncDatabase is currently available but not initialized at engine startup. To use it, create and manage an
AsyncDatabasePoolinstance in your game or server code directly.
| File | Lines | Description |
|---|---|---|
SparkEngine/Source/Engine/Persistence/AsyncDatabase.h |
288 | All type definitions and class declarations (QueryValue, QueryRow, QueryResult, PreparedStatementData, Transaction, IDatabaseConnection, SQLiteConnection, AsyncDatabasePool) |
SparkEngine/Source/Engine/Persistence/AsyncDatabase.cpp |
614 | Connection pool implementation, worker thread loop, query dispatch, SQLiteConnection file-based KV store |
| Scenario | Behavior |
|---|---|
Open() fails on any connection |
Returns false; no workers are started |
Open() called when already open |
Returns false (no-op) |
| Unknown prepared statement ID |
Execute() returns QueryResult with success = false and error message |
| Transaction query fails mid-batch | Remaining queries skipped, transaction rolled back, error propagated |
SyncQuery() when pool is closed |
Returns QueryResult with success = false
|
Close() with pending work items |
Workers drain the queue before exiting |
| Worker thread exception | Not caught internally; undefined behavior. Ensure connection backends do not throw. |
GetInt()/GetDouble()/GetString() type mismatch |
Throws std::bad_variant_access
|
| Column index out of range |
GetInt()/GetDouble()/GetString() throw std::out_of_range; IsNull() returns true |
To add a real SQLite, MySQL, or PostgreSQL backend:
- Create a new class implementing
IDatabaseConnection - Implement all pure virtual methods (
Open,Close,PrepareStatement,Execute,ExecuteRaw,BeginTransaction,CommitTransaction,RollbackTransaction) - Modify
AsyncDatabasePool::Open()to construct your connection type instead ofSQLiteConnection - The rest of the pool machinery (threading, queuing, callbacks) works unchanged
class MySQLConnection : public IDatabaseConnection
{
public:
bool Open(const std::string& connectionString) override { /* mysql_real_connect() */ }
void Close() override { /* mysql_close() */ }
// ... implement remaining methods ...
};- Save System -- ECS-aware game state serialization
- Networking -- Multiplayer networking layer that may use AsyncDatabase for persistence
- Entity Component System -- Components persisted by SaveSystem
- Area Server Architecture -- Server architecture where AsyncDatabase fits naturally
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers