-
Notifications
You must be signed in to change notification settings - Fork 226
Add SQLite database support #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds persistent SQLite database support to the Sandbox engine, allowing games to store data locally using SQL queries. The implementation provides both a static API for convenience and direct database instances for more control, with separate databases for server and client contexts.
- Introduces SQLite integration via Microsoft.Data.Sqlite package (version 10.0.1)
- Provides three-tier API: static
Sqlclass for convenience,SqlDatabasefor direct connections, andSqlBuilderfor fluent query construction - Implements transaction support, error handling with
LastErrorproperty, and helper methods for common operations
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| engine/Sandbox.Engine/Sandbox.Engine.csproj | Adds Microsoft.Data.Sqlite package reference for SQLite functionality |
| engine/Sandbox.Engine/Utility/Sql/Sql.cs | Static API providing convenient access to server/client databases with error handling and helper methods |
| engine/Sandbox.Engine/Utility/Sql/SqlDatabase.cs | Core database wrapper managing connections, transactions, and native library initialization |
| engine/Sandbox.Engine/Utility/Sql/SqlBuilder.cs | Fluent query builder for constructing dynamic SQL queries with parameterization |
| engine/Sandbox.Test.Unit/Sql/SqlTest.cs | Comprehensive unit tests covering database operations, transactions, and the query builder |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public async Task<List<Dictionary<string, object>>> QueryAsync( string query, object parameters = null ) | ||
| { | ||
| ThrowIfDisposed(); | ||
|
|
||
| // Note: We acquire the lock synchronously to avoid deadlocks. | ||
| // The actual database operation is async. | ||
| lock ( _lock ) | ||
| { | ||
| // Create command inside lock to ensure thread safety | ||
| var cmd = CreateCommand( query, parameters ); | ||
| return QueryAsyncInternal( cmd ).GetAwaiter().GetResult(); | ||
| } | ||
| } | ||
|
|
||
| private async Task<List<Dictionary<string, object>>> QueryAsyncInternal( SqliteCommand cmd ) | ||
| { | ||
| using ( cmd ) | ||
| using ( var reader = await cmd.ExecuteReaderAsync() ) | ||
| { | ||
| var results = new List<Dictionary<string, object>>(); | ||
|
|
||
| while ( await reader.ReadAsync() ) | ||
| { | ||
| var row = new Dictionary<string, object>( StringComparer.OrdinalIgnoreCase ); | ||
|
|
||
| for ( int i = 0; i < reader.FieldCount; i++ ) | ||
| { | ||
| var name = reader.GetName( i ); | ||
| var value = reader.IsDBNull( i ) ? null : reader.GetValue( i ); | ||
| row[name] = value; | ||
| } | ||
|
|
||
| results.Add( row ); | ||
| } | ||
|
|
||
| return results; | ||
| } | ||
| } | ||
|
|
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using GetAwaiter().GetResult() blocks the thread synchronously, defeating the purpose of async code and potentially causing deadlocks. Consider making the entire method chain async or use synchronous operations inside the lock instead.
| public async Task<List<Dictionary<string, object>>> QueryAsync( string query, object parameters = null ) | |
| { | |
| ThrowIfDisposed(); | |
| // Note: We acquire the lock synchronously to avoid deadlocks. | |
| // The actual database operation is async. | |
| lock ( _lock ) | |
| { | |
| // Create command inside lock to ensure thread safety | |
| var cmd = CreateCommand( query, parameters ); | |
| return QueryAsyncInternal( cmd ).GetAwaiter().GetResult(); | |
| } | |
| } | |
| private async Task<List<Dictionary<string, object>>> QueryAsyncInternal( SqliteCommand cmd ) | |
| { | |
| using ( cmd ) | |
| using ( var reader = await cmd.ExecuteReaderAsync() ) | |
| { | |
| var results = new List<Dictionary<string, object>>(); | |
| while ( await reader.ReadAsync() ) | |
| { | |
| var row = new Dictionary<string, object>( StringComparer.OrdinalIgnoreCase ); | |
| for ( int i = 0; i < reader.FieldCount; i++ ) | |
| { | |
| var name = reader.GetName( i ); | |
| var value = reader.IsDBNull( i ) ? null : reader.GetValue( i ); | |
| row[name] = value; | |
| } | |
| results.Add( row ); | |
| } | |
| return results; | |
| } | |
| } | |
| public Task<List<Dictionary<string, object>>> QueryAsync( string query, object parameters = null ) | |
| { | |
| ThrowIfDisposed(); | |
| // Delegate to the synchronous implementation, which handles locking/thread safety. | |
| return Task.FromResult( Query( query, parameters ) ); | |
| } |
| /// </remarks> | ||
| public static List<Dictionary<string, object>> GetTableColumns( string tableName ) | ||
| { | ||
| return Query( $"PRAGMA table_info({tableName})" ); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Direct string interpolation of table name creates SQL injection vulnerability despite the comment warning. Consider validating the table name against a whitelist or using sqlite_master to verify it exists before using it in the PRAGMA statement.
| /// <code> | ||
| /// if ( !Sql.TableExists( "users" ) ) | ||
| /// { | ||
| /// Sql.Query( "CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT )" ); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected spelling of 'Query' to 'Execute' in the documentation example.
| /// Sql.Query( "CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT )" ); | |
| /// Sql.Execute( "CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT )" ); |
| /// { | ||
| /// for ( int i = 0; i < 1000; i++ ) | ||
| /// { | ||
| /// Sql.Query( "INSERT INTO data (value) VALUES (@v)", new { v = i } ); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected spelling of 'Query' to 'Execute' in the documentation example.
| /// Sql.Query( "INSERT INTO data (value) VALUES (@v)", new { v = i } ); | |
| /// Sql.Execute( "INSERT INTO data (value) VALUES (@v)", new { v = i } ); |
|
|
||
| if ( providerType == null || rawType == null ) | ||
| { | ||
| throw new InvalidOperationException( "SQLitePCL assemblies not found. Did you check if Microsoft.Data.Sqlite is referenced?" ); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message contains a confusing rhetorical question. Consider rephrasing to a more direct statement like "SQLitePCL assemblies not found. Ensure Microsoft.Data.Sqlite is referenced."
| throw new InvalidOperationException( "SQLitePCL assemblies not found. Did you check if Microsoft.Data.Sqlite is referenced?" ); | |
| throw new InvalidOperationException( "SQLitePCL assemblies not found. Ensure Microsoft.Data.Sqlite is referenced." ); |
|
I struggle with this. I struggle whether it's a good idea or not, whether we should be thinking more advanced. Should we be adding a layer above this, like entity framework, that means that the database can be sqlite, mssql, mysql? Should we be having people write raw sql queries? |
I wrote this at first because I wanted to enable people to do simple SQL queries like you would in GMod, but an entity framework would be an interesting idea to expand upon. I'm fine with being limited to SQLite though |

This resolves Facepunch/sbox-issues#9262.
Adds persistent SQLite storage via
Microsoft.Data.Sqlite. Databases are stored in the game's data folder (sv.db for server, cl.db for client)New classes
Sql- Static APISqlDatabase- SQLite connection wrapperSqlBuilder- Fluent query builder for dynamic SQL constructionSqlTest- Unit testsUsage
Error handling
Methods return
null(Query),-1(Execute), ordefault(QueryValue) on failure. CheckSql.LastErrorfor the error message.