Code/Sandbank.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SandbankDatabase;
public static class Sandbank
{
/// <summary>
/// Copy the saveable data from one class to another. This is useful for when you load
/// data from the database and you want to put it in a component or something like that.
/// </summary>
public static void CopySavedData<T>(T sourceClass, T destinationClass)
{
Cloning.CopyClassData<T>( sourceClass, destinationClass );
}
/// <summary>
/// Insert a document into the database. The document will have its ID set
/// if it is empty.
/// </summary>
public static void Insert<T>( string collection, T document ) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, true );
Document newDocument = new( document, typeof(T), true, collection );
relevantCollection.InsertDocument( newDocument );
}
/// <summary>
/// Insert a document into the database. The document will have its ID set if it is empty.
///
/// For internal use.
/// </summary>
internal static void Insert( string collection, object document, Type documentType )
{
var relevantCollection = Cache.GetCollectionByName( collection, true, documentType );
Document newDocument = new( document, documentType, true, collection );
relevantCollection.InsertDocument( newDocument );
}
/// <summary>
/// Insert multiple documents into the database. The documents will have their IDs
/// set if they are empty.
/// </summary>
public static void InsertMany<T>( string collection, IEnumerable<T> documents ) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, true );
foreach (var document in documents)
{
Document newDocument = new Document( document, typeof(T), true, collection );
relevantCollection.InsertDocument( newDocument );
}
}
/// <summary>
/// Fetch a single document from the database where selector evaluates to true.
/// </summary>
public static T SelectOne<T>( string collection, Func<T, bool> selector ) where T : class, new()
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return null;
foreach ( var pair in relevantCollection.CachedDocuments )
{
if ( selector.Invoke( (T)pair.Value.Data ) )
return ObjectPool.CloneObject( (T)pair.Value.Data, relevantCollection.DocumentClassType.FullName );
}
return null;
}
/// <summary>
/// The same as SelectOne except faster since we can look it up by ID.
/// </summary>
public static T SelectOneWithID<T>( string collection, string uid ) where T : class, new()
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return null;
relevantCollection.CachedDocuments.TryGetValue(uid, out Document document);
return document == null ?
null
: ObjectPool.CloneObject( (T)document.Data, relevantCollection.DocumentClassType.FullName );
}
/// <summary>
/// Select all documents from the database where selector evaluates to true.
/// </summary>
public static List<T> Select<T>( string collection, Func<T, bool> selector ) where T : class, new()
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
List<T> output = new();
if ( relevantCollection == null )
return output;
foreach ( var pair in relevantCollection.CachedDocuments )
{
if ( selector.Invoke( (T)pair.Value.Data ) )
output.Add( ObjectPool.CloneObject( (T)pair.Value.Data, relevantCollection.DocumentClassType.FullName ) );
}
return output;
}
/// <summary>
/// DO NOT USE THIS FUNCTION UNLESS YOU FULLY UNDERSTAND THE BELOW, AS THERE IS
/// A RISK YOU COULD CORRUPT YOUR DATA. <br/>
/// <br/>
/// This does the exact same thing as Select, except it is about 9x faster.
/// They work differently, however. <br/>
/// <br/>
/// Select copies the data from the cache into new objects and then gives those
/// new objects to you. That means that any changes you make to those new objects
/// don't affect anything else - you're free to do what you want with them. The
/// downside to this is that there is an overhead invovled in creating all those
/// new objects. <br/>
/// <br/>
/// SelectUnsafeReferences on the other hand will give you a reference to the data
/// that is stored in the cache. This is faster because it means no new copy has to
/// be made. However, because it's giving you a reference, this means that ANY CHANGES
/// YOU MAKE TO THE RETURNED OBJECTS WILL BE REFLECTED IN THE CACHE, AND THEREFORE MAY
/// CHANGE THE VALUES IN THE DATABASE UNEXEPECTEDLY!!! You should therefore not modify
/// the returned objects in any way, only read them.<br/>
/// <br/>
/// You are guaranteed that the cache will not change the object after you have requested
/// it (because all inserts are new objects).
/// </summary>
public static List<T> SelectUnsafeReferences<T>( string collection, Func<T, bool> selector ) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
List<T> output = new();
if ( relevantCollection == null )
return output;
foreach ( var pair in relevantCollection.CachedDocuments )
{
if ( selector.Invoke( (T)pair.Value.Data ) )
output.Add( (T)pair.Value.Data );
}
return output;
}
/// <summary>
/// Delete all documents from the database where selector evaluates to true.
/// </summary>
public static void Delete<T>( string collection, Predicate<T> selector ) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return;
List<string> idsToDelete = new();
foreach ( var pair in relevantCollection.CachedDocuments )
{
if ( selector.Invoke( (T)pair.Value.Data ) )
idsToDelete.Add( pair.Key );
}
foreach ( var id in idsToDelete )
{
relevantCollection.CachedDocuments.TryRemove( id, out _ );
int attempt = 0;
var error = "";
while ( true )
{
if ( attempt++ >= 10 )
throw new SandbankException( $"failed to delete document from collection \"{collection}\" after 10 tries: " + error );
error = FileController.DeleteDocument( collection, id );
if ( error == null)
break;
}
}
}
/// <summary>
/// The same as Delete except faster since we can look it up by ID.
/// </summary>
public static void DeleteWithID<T>( string collection, string id) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return;
relevantCollection.CachedDocuments.TryRemove( id, out _ );
int attempt = 0;
while ( true )
{
if ( attempt++ >= 10 )
throw new SandbankException( $"failed to delete document from collection \"{collection}\" after 10 tries - is the file in use by something else?" );
if ( FileController.DeleteDocument( collection, id ) == null )
break;
}
}
/// <summary>
/// Return whether there are any documents in the datbase where selector evalutes
/// to true.
/// </summary>
public static bool Any<T>( string collection, Func<T, bool> selector ) where T : class
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return false;
foreach ( var pair in relevantCollection.CachedDocuments )
{
if ( selector.Invoke( (T)pair.Value.Data ) )
return true;
}
return false;
}
/// <summary>
/// The same as Any except faster since we can look it up by ID.
/// </summary>
public static bool AnyWithID<T>( string collection, string id )
{
var relevantCollection = Cache.GetCollectionByName<T>( collection, false );
if ( relevantCollection == null )
return false;
return relevantCollection.CachedDocuments.ContainsKey( id );
}
public static void DeleteAllBackups()
{
try
{
FileController.WipeBackups();
}
catch ( Exception e )
{
Logging.Warn( $"failed deleting all backups: {Logging.ExtractExceptionString( e )}" );
}
}
/// <summary>
/// Deletes everything, forever. Does not delete backups.
/// </summary>
public static void DeleteAllData()
{
Cache.WipeStaticFields();
int attempt = 0;
string error = null;
while ( true )
{
if ( attempt++ >= 10 )
throw new SandbankException( $"failed to wipe data after 10 tries: {error}" );
error = FileController.WipeFilesystem();
if ( error == null )
return;
}
}
/// <summary>
/// Call this to gracefully shut-down the database. It is recommended to call this
/// when your server is shutting down to make sure all recently-changed data is saved,
/// if that's important to you.
/// <br/> <br/>
/// Any operations ongoing at the time Shutdown is called are not guaranteed to be
/// written to disk.
/// <br/> <br/>
/// Shutdown takes some time to complete, so you should await this until it's done.
/// </summary>
public static async Task Shutdown()
{
while ( true )
{
lock ( InitialisationController.DatabaseStateLock )
{
if ( InitialisationController.CurrentDatabaseState == DatabaseState.Uninitialised )
break;
// This will signal to the ticker to kill the background threads and complete the shutdown.
InitialisationController.CurrentDatabaseState = DatabaseState.ShuttingDown;
}
await Task.Delay( 10 );
}
}
}