Browse Source

Add `DBConnection` class

core_refactor
Anton Tarasenko 2 years ago
parent
commit
90abd8f80e
  1. 1099
      sources/Data/Database/Connection/DBCache.uc
  2. 791
      sources/Data/Database/Connection/DBConnection.uc
  3. 40
      sources/Data/Database/Connection/Events/DBConnection_EditResult_Signal.uc
  4. 41
      sources/Data/Database/Connection/Events/DBConnection_EditResult_Slot.uc
  5. 43
      sources/Data/Database/Connection/Events/DBConnection_StateChanged_Signal.uc
  6. 44
      sources/Data/Database/Connection/Events/DBConnection_StateChanged_Slot.uc
  7. 389
      sources/Data/Database/Connection/Tests/TEST_DBConnection.uc
  8. 50
      sources/Data/Database/DBAPI.uc
  9. 7
      sources/Manifest.uc

1099
sources/Data/Database/Connection/DBCache.uc

File diff suppressed because it is too large Load Diff

791
sources/Data/Database/Connection/DBConnection.uc

@ -0,0 +1,791 @@
/**
* Auxiliary object for simplifying working with databases.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection extends AcediaObject
dependson(Database)
dependson(DBCache);
/**
* # `DBConnection`
*
* Auxiliary object for simplifying working with databases.
* `Database` class has a rather simple interface and there are several issues
* that constantly arise when trying to use it:
*
* 1. If one tries to read/write data from/to specific location in
* the database, then `JSONPointer` has to be kept in addition to
* the `Database` reference at all times;
* 2. One has to perform initial checks about whether database can even be
* connected to, if at desired location there is a proper data
* structure, etc.. If one also wants to start using data before
* database's response or after its failure, then the same work of
* duplication that data locally must be performed.
* 3. Instead of immediate operations, database operations are delayed and
* user has to handle their results asynchronously in separate methods.
*
* `DBConnection` takes care of these issues by providing you synchronous
* methods for accessing cached version of the data at the given location that
* is duplicated to the database as soon as possible (and even if its no longer
* possible in the case of a failure).
* `DBConnection` makes immediate changes on the local cache and reports
* about possible failures with database later through signals `OnEditResult()`
* (reports about success of writing operations) and `OnStateChanged()`
* (reports about state changes of connected database, including complete
* failures).
* The only reading of database's values occurs at the moment of connecting
* to it, after that all the data is read from the local cache.
* Possible `DBConnection` states include:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method:
*
* * `FDE_None` - no error has yet occurred;
* * `FDE_CannotReadRootData` - root data couldn't be read from
* the database, most likely because of the invalid `JSONPointer`
* for the root value;
* * `FDE_UnexpectedRootData` - root data was read from the database,
* but has an unexpected format. `DBConnection` expects either
* JSON object or array (can be specified which one) and this error
* occurs if its not found at specified database's location;
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result
* for one of the queries. This is likely to happen when database
* is damaged. More precise details depend on the implementation,
* but result boils down to database being unusable.
*
* ## Usage
*
* Usage is straightforward:
*
* 1. Initialize with appropriate database by calling `Initialize()`;
* 2. Start connecting to it by calling `Connect()`;
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into
* the connected database;
*
* You can use it transparently even if database connection fails, but if you
* need to handle such failure - connect to the `OnStateChanged()` signal for
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking
* success of writing operations.
*
* ## Implementation
*
* The brunt of work is done by `DBCache` and most of the logic in this
* class is for tracking state of the connection to the database and then
* reporting these changes through its own signals.
* The most notable hidden functionality is tracking requests by ID -
* `DBConnection` makes each request with unique ID and then stores them inside
* `requestIDs` (and in case of write requests - along with corresponding
* `JSONPointer` inside `queuedPointers`). This is necessary because:
*
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is
* concerned it is still the same object, so the responses we no longer
* care about will still arrive. Keeping track of the IDs that interest
* us inside `requestIDs` allows us to filter out responses we no
* longer care about;
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us
* to know responses to which writing requests we've received.
*
* ## Remarks
*
* Currently `DBConnection` doesn't support important feature of *incrementing*
* data that allows several sources to safely change the same value
* asynchronously. We're skipping on it right now to save time as its not
* really currently needed, however it will be added in the future.
*/
enum DBConnectionState
{
// `DBConnection` was created, but didn't yet attempt to connect
// to database
DBCS_Idle,
// `DBConnection` is currently connecting
DBCS_Connecting,
// `DBConnection` has already connected without errors
DBCS_Connected,
// `DBConnection` was manually disconnected
DBCS_Disconnected,
// `DBConnection` was disconnected because of the database error,
// @see `FaultyDatabaseError` for more.
DBCS_FaultyDatabase
};
// Current connection state
var private DBConnectionState currentState;
enum FaultyDatabaseError
{
// No error has occurred yet
FDE_None,
// Root data isn't available
FDE_CannotReadRootData,
// Root data has incorrect format
FDE_UnexpectedRootData,
// Some internal error in database has occurred
FDE_Unknown
};
// Reason for why current state is in `DBCS_FaultyDatabase` state;
// `FDE_None` if `DBConnection` is in any other state.
var private FaultyDatabaseError dbFailureReason;
// Keeps track whether root value read from the database was of the correct
// type. Only relevant after database tried connecting (i.e. it is in states
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`).
// This variable helps us determine whether error should be
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`.
var private bool rootIsOfExpectedType;
// `Database` + `JSONPointer` combo that point at the data we want to
// connect to
var private Database dbInstance;
var private JSONPointer rootPointer;
// Local, cached version of that data
var private DBCache localCache;
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking
// database requests of interest
var private array<int> requestIDs;
var private array<JSONPointer> queuedPointers;
// Next usable ID. `DBConnection` is expected to always use unique IDs.
var private int nextRequestID;
var private DBConnection_StateChanged_Signal onStateChangedSignal;
var private DBConnection_EditResult_Signal onEditResultSignal;
var private LoggerAPI.Definition errDoubleInitialization;
/**
* Signal that will be emitted whenever `DBConnection` changes state.
* List of the available states:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method.
*
* This method *is not* called when `DBConnection` is deallocated.
*
* [Signature]
* void <slot>(
* DBConnection instance,
* DBConnectionState oldState,
* DBConnectionState newState)
*
* @param instance Instance of the `DBConnection` that has changed state.
* @param oldState State it was previously in.
* @param oldState New state.
*/
/* SIGNAL */
public final function DBConnection_StateChanged_Slot OnStateChanged(
AcediaObject receiver)
{
return DBConnection_StateChanged_Slot(onStateChangedSignal
.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever `DBConnection` receives response from
* connected database about success of writing operation.
*
* Responses to old requests can still be received even if database got
* disconnected.
*
* Any emissions of this signal when the database's state is `DBCS_Connecting`
* correspond to reapplying edits made prior connection was established.
*
* [Signature]
* void <slot>(JSONPointer editLocation, bool isSuccessful)
*
* @param editLocation Location of the writing operation this is
* a response to.
* @param isSuccessful Whether writing operation ended in the success.
*/
/* SIGNAL */
public final function DBConnection_EditResult_Slot OnEditResult(
AcediaObject receiver)
{
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver));
}
protected function Constructor()
{
localCache = DBCache(_.memory.Allocate(class'DBCache'));
onStateChangedSignal = DBConnection_StateChanged_Signal(
_.memory.Allocate(class'DBConnection_StateChanged_Signal'));
onEditResultSignal = DBConnection_EditResult_Signal(
_.memory.Allocate(class'DBConnection_EditResult_Signal'));
}
protected function Finalizer()
{
rootIsOfExpectedType = false;
currentState = DBCS_Idle;
_.memory.Free(dbInstance);
_.memory.Free(rootPointer);
_.memory.Free(localCache);
dbInstance = none;
rootPointer = none;
localCache = none;
_.memory.FreeMany(queuedPointers);
queuedPointers.length = 0;
requestIDs.length = 0;
// Free signals
_.memory.Free(onStateChangedSignal);
_.memory.Free(onEditResultSignal);
onStateChangedSignal = none;
onEditResultSignal = none;
}
/**
* Initializes `DBConnection` with database and location to which it must be
* connected.
*
* For the initialization to be successful `DBConnection` must not yet be
* initialized and `initDatabase` be not `none`.
*
* To check whether caller `DBConnection` is initialized
* @see `IsInitialized()`.
*
* @param initDatabase Database with data we want to connect to.
* @param initRootPointer Location of said data in the given database.
* If `none` is specified, uses root object of the database.
* @return `true` if initialization was successful and `false` otherwise.
*/
public final function bool Initialize(
Database initDatabase,
optional JSONPointer initRootPointer)
{
if (IsInitialized()) return false;
if (initDatabase == none) return false;
if (!initDatabase.IsAllocated()) return false;
dbInstance = initDatabase;
dbInstance.NewRef();
if (initRootPointer != none) {
rootPointer = initRootPointer.Copy();
}
else {
rootPointer = _.json.Pointer();
}
return true;
}
/**
* Reads data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual returned data's location in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always read from the local cache and, therefore, we can
* read data we've written via `DBConnection` even without actually connecting
* to the database.
*
* @param pointer Location from which to read the data.
* @return Data recorded for the given `JSONPointer`. `none` if it is missing.
*/
public final function AcediaObject ReadDataByJSON(JSONPointer pointer)
{
return localCache.Read(pointer);
}
/**
* Writes given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for writing data in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always also written into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot write any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location into which to write the data.
* @param data Data to write into the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the writing database request to be made.
*/
public final function bool WriteDataByJSON(
JSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Write(pointer, data))
{
ModifyDataInDatabase(pointer, data, false);
return true;
}
return false;
}
/**
* Increments given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for incrementing data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also incremented into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot increment any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to increment the data.
* @param data Data with which to increment value inside the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the incrementing database request to be made.
*/
public final function bool IncrementDataByJSON(
JSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Increment(pointer, data))
{
ModifyDataInDatabase(pointer, data, true);
return true;
}
return false;
}
/**
* Removes data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location at which to remove data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also removed from the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*.
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to remove data.
* @return `true` on success and `false` on failure. `true` is required for
* the removal database request to be made.
*/
public final function bool RemoveDataByJSON(JSONPointer pointer)
{
if (pointer == none) {
return false;
}
if (localCache.Remove(pointer))
{
RemoveDataInDatabase(pointer);
return true;
}
return false;
}
private final function ModifyDataInDatabase(
JSONPointer pointer,
AcediaObject data,
bool increment)
{
local JSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
if (increment)
{
dbInstance
.IncrementData(
dataPointer,
data,
RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
else
{
dbInstance
.WriteData(dataPointer, data, RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
}
private final function RemoveDataInDatabase(JSONPointer pointer)
{
local JSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
dbInstance
.RemoveData(dataPointer, RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
/**
* Checks caller `DBConnection` was successfully initialized.
*
* @return `true` if caller `DBConnection` was initialized and `false`
* otherwise.
*/
public final function bool IsInitialized()
{
return (dbInstance != none);
}
/**
* Returns current state of the connection of `DBConnection` to the database
* it was initialized with.
*
* @see `OnStateChanged()` for more information about connection states.
* @return Current connection state.
*/
public final function DBConnectionState GetConnectionState()
{
return currentState;
}
/**
* Checks whether caller `DBConnection` is currently connected without errors
* to the database it was initialized with.
*
* @return `true` if caller `DBConnection` is connected to the database and
* `false` otherwise.
*/
public final function bool IsConnected()
{
return (currentState == DBCS_Connected);
}
/**
* Checks whether an error has occurred with connection to the database.
*
* `DBConnection` can get disconnected from database manually and without
* any errors, so, if you simply want to check whether connection exists,
* @see `IsConnected()` or @see `GetConnectionState()`.
* To obtain more detailed information @see `GetError()`.
*
* @return `true` if there were no error thus far and `false` otherwise.
*/
public final function bool IsOk()
{
return (dbFailureReason == FDE_None);
}
/**
* Returns error that has occurred during connection.
*
* @return Error that has occurred during connection to the database,
* `FDE_None` if there was no errors.
*/
public final function FaultyDatabaseError GetError()
{
return dbFailureReason;
}
private final function ChangeState(DBConnectionState newState)
{
local DBConnectionState oldState;
oldState = currentState;
currentState = newState;
onStateChangedSignal.Emit(self, oldState, newState);
}
/**
* Attempts connection to the database caller `DBConnection` was initialized
* with. Result isn't immediate and can be tracked with `OnStateChanged()`
* signal.
*
* Connection checks whether data by the initialization address can be read and
* has proper type (by default JSON object, but JSON array can be used
* instead).
*
* Whether connection is successfully established isn't known at the moment
* this function returns. User `OnStateChanged()` to track that.
*
* @param expectArray Set this to `true` if the expected root value is
* JSON array.
*/
public final function Connect(optional bool expectArray)
{
local Collection incrementObject;
if (!IsInitialized()) return;
if (currentState != DBCS_Idle) return;
if (expectArray) {
incrementObject = _.collections.EmptyArrayList();
}
else {
incrementObject = _.collections.EmptyHashTable();
}
dbInstance.IncrementData(
rootPointer,
incrementObject,
RegisterNextRequestID()).connect = IncrementCheckHandler;
incrementObject.FreeSelf();
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()`
// method
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy()))
.connect = InitialLoadingHandler;
ChangeState(DBCS_Connecting);
}
/**
* Disconnects `DBConnection` from its database, preventing its further
* updates.
*
* Database can only be disconnected if connection was at least initialized
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't
* `DBCS_FaultyDatabase`).
*
* @return `true` if `DBConnection` was disconnected from the database and
* `false` otherwise (including if it already was disconnected).
*/
public final function bool Disconnect()
{
if ( currentState != DBCS_FaultyDatabase
&& currentState != DBCS_Idle
&& currentState != DBCS_Disconnected)
{
ChangeState(DBCS_Disconnected);
return true;
}
return false;
}
private final function int RegisterNextRequestID(
optional /*take*/ JSONPointer relativePointer)
{
if (relativePointer != none) {
queuedPointers[queuedPointers.length] = relativePointer;
}
else {
queuedPointers[queuedPointers.length] = _.json.Pointer();
}
requestIDs[requestIDs.length] = nextRequestID;
nextRequestID += 1;
return (nextRequestID - 1);
}
private final function JSONPointer FetchRequestPointer(int requestID)
{
local int i;
local JSONPointer result;
while (i < requestIDs.length)
{
if (requestIDs[i] < requestID)
{
// We receive all requests in order, so if `requestID` is higher
// than IDs of some other requests - it means that they are older,
// lost requests
_.memory.Free(queuedPointers[i]);
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
}
if (requestIDs[i] == requestID)
{
result = queuedPointers[i];
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
return result;
}
i += 1;
}
return none;
}
private final function bool FetchIfRequestStillValid(int requestID)
{
local JSONPointer result;
result = FetchRequestPointer(requestID);
if (result != none)
{
_.memory.Free(result);
return true;
}
return false;
}
private final function IncrementCheckHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
if (!FetchIfRequestStillValid(requestID)) {
return;
}
// If we could successfully increment value with appropriate JSON value,
// then its type is correct
rootIsOfExpectedType = (result == DBR_Success);
}
private final function InitialLoadingHandler(
Database.DBQueryResult result,
/*take*/ AcediaObject data,
Database source,
int requestID)
{
local int i;
local array<DBCache.PendingEdit> completedEdits;
if (!FetchIfRequestStillValid(requestID))
{
_.memory.Free(data);
return;
}
if (HandleInitializationError(result))
{
_.memory.Free(data);
return;
}
completedEdits = localCache.SetRealData(data);
for (i = 0; i < completedEdits.length; i += 1)
{
if (completedEdits[i].successful)
{
if (completedEdits[i].type == DBCET_Remove) {
RemoveDataInDatabase(completedEdits[i].location);
}
else
{
ModifyDataInDatabase(
completedEdits[i].location,
completedEdits[i].data,
completedEdits[i].type == DBCET_Increment);
}
}
else {
onEditResultSignal.Emit(completedEdits[i].location, false);
}
_.memory.Free(completedEdits[i].location);
_.memory.Free(completedEdits[i].data);
}
_.memory.Free(data);
ChangeState(DBCS_Connected);
}
// Return `true` if further initialization must be stopped.
private final function bool HandleInitializationError(
Database.DBQueryResult result)
{
// Get disconnected before even response has even arrived
if (currentState == DBCS_Disconnected) {
return true;
}
if (currentState == DBCS_Connected)
{
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText());
return true;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (result != DBR_Success)
{
dbFailureReason = FDE_CannotReadRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (!rootIsOfExpectedType)
{
dbFailureReason = FDE_UnexpectedRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
return false;
}
private final function EditDataHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
local JSONPointer relatedPointer;
relatedPointer = FetchRequestPointer(requestID);
if (relatedPointer == none) {
return;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
relatedPointer.FreeSelf();
return;
}
if (result == DBR_Success) {
onEditResultSignal.Emit(relatedPointer, true);
}
else {
onEditResultSignal.Emit(relatedPointer, false);
}
relatedPointer.FreeSelf();
}
defaultproperties
{
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.")
}

40
sources/Data/Database/Connection/Events/DBConnection_EditResult_Signal.uc

@ -0,0 +1,40 @@
/**
* Signal class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Signal extends Signal
dependson(DBConnection);
public final function Emit(JSONPointer editLocation, bool isSuccessful)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_EditResult_Slot(nextSlot)
.connect(editLocation, isSuccessful);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_EditResult_Slot'
}

41
sources/Data/Database/Connection/Events/DBConnection_EditResult_Slot.uc

@ -0,0 +1,41 @@
/**
* Slot class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Slot extends Slot
dependson(DBConnection);
delegate connect(JSONPointer editLocation, bool isSuccessful)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

43
sources/Data/Database/Connection/Events/DBConnection_StateChanged_Signal.uc

@ -0,0 +1,43 @@
/**
* Signal class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Signal extends Signal
dependson(DBConnection);
public final function Emit(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_StateChanged_Slot(nextSlot)
.connect(instance, oldState, newState);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_StateChanged_Slot'
}

44
sources/Data/Database/Connection/Events/DBConnection_StateChanged_Slot.uc

@ -0,0 +1,44 @@
/**
* Slot class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Slot extends Slot
dependson(DBConnection);
delegate connect(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

389
sources/Data/Database/Connection/Tests/TEST_DBConnection.uc

@ -0,0 +1,389 @@
/**
* Set of tests for `DBConnection` and `DBCache` classes.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_DBConnection extends TestCase
abstract;
var DBCache cache;
protected static function TESTS()
{
Context("Testing how `DBCache` handles data writes and reads when"
@ "the actual database's data is yet to be downloaded.");
Test_DBCache_BasicWriteRead_DuringLoading();
Test_DBCache_AllOperations_DuringLoading();
Context("Testing how `DBCache` handles applying pending writes after real"
@ "data was set.");
Test_DBCache_ApplyingPendingEdits();
Test_DBCache_ApplyingPendingEdit_AllOperations();
Context("Testing how `DBCache` handles data writes and reads when"
@ "the actual database's data is already setup.");
Test_DBCache_BasicWriteRead_AfterLoading();
Test_DBCache_AllOperations_AfterLoading();
}
/* Creates following object:
{
"A": "simpleValue",
"B": {
"A": [true, {
"A": "simpleValue",
"B": 11.12,
"": [true, null, "huh"]
}, "huh"],
"B": -13.95
},
"C": -5,
"D": []
} */
protected static function HashTable MakeTestJSONObject()
{
local ArrayList outerArray, innerArray;
local HashTable result, outerObject, innerObject;
innerArray = __().collections.EmptyArrayList();
innerArray.AddBool(true).AddItem(none).AddString("huh");
innerObject = __().collections.EmptyHashTable();
innerObject.SetString(P("A"), "simpleValue");
innerObject.SetFloat(P("B"), 11.12).SetItem(P(""), innerArray);
outerArray = __().collections.EmptyArrayList();
outerArray.AddBool(true).AddItem(innerObject).AddString("huh");
outerObject = __().collections.EmptyHashTable();
outerObject.SetItem(P("A"), outerArray).SetFloat(P("B"), -13.95);
result = __().collections.EmptyHashTable();
result.SetString(P("A"), "simpleValue");
result.SetItem(P("B"), outerObject);
result.SetInt(P("C"), -5);
result.SetItem(P("D"), __().collections.EmptyArrayList());
return result;
}
protected static function CheckLocation(string pointer, string expectedResult)
{
local AcediaObject value;
value = default.cache
.Read(__().json.Pointer(__().text.FromString(pointer)));
if (BaseText(value) != none)
{
TEST_ExpectTrue(
__().json.Print(value).ToString()
== ("\"" $ expectedResult $ "\""));
}
else {
TEST_ExpectTrue(__().json.Print(value).ToString() == expectedResult);
}
}
// This is a rather extensive and long test.
// Despite its length, I've chosen to not break it up into smaller parts,
// since the same value `default.cache` is changed and built up all the way
// through this method.
// Trying to break it up into smaller and simpler tests would only mean
// putting less stress of `DBCache` than we currently do.
protected static function Test_DBCache_BasicWriteRead_DuringLoading()
{
local AcediaObject complexData;
Issue("Simple write/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away")),
complexData);
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near")),
complexData);
CheckLocation("/just/another/place/not/near/A", "simpleValue");
CheckLocation("/just/some/place/far/away/B/A/2", "huh");
CheckLocation("/just/another/place/not/near/B/A/1//2", "huh");
CheckLocation("/just/some/place/far/away/B/A/1/", "[true,null,\"huh\"]");
Issue("Data is read incorrectly after being overridden.");
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near/B/A/1//1")),
__().box.bool(false));
CheckLocation(
"/just/another/place/not/near/B/A/1/",
"[true,false,\"huh\"]");
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near/B/A")),
__().box.int(121));
CheckLocation("/just/another/place/not/near/B/A", "121");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/B/A/1/B")),
complexData);
CheckLocation("/just/some/place/far/away/B/A/1/B/B/A/0", "true");
default.cache.Write(
__().json.Pointer(
P("/just/some/place/far/away/C/inside_the_number!/hey")),
__().box.float(1.1));
CheckLocation("/just/some/place/far/away/C/inside_the_number!/hey", "null");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/7/hey")),
__().box.int(-345));
CheckLocation("/just/some/place/far/away/D", "[]");
CheckLocation("/just/some/place/far/away/D/7/hey", "-345");
default.cache.Write(
__().json.Pointer(
P("/just/some/place/far/away/D/inside_the_array!/hey")),
__().box.float(1.1));
CheckLocation("/just/some/place/far/away/D/inside_the_array!/hey", "null");
CheckLocation("/just/some/place/far/away/D", "[]");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.bool(true));
CheckLocation("/just/some/place/far/away/D", "[true]");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/7")),
__().box.bool(true));
CheckLocation(
"/just/some/place/far/away/D",
"[true,null,null,null,null,null,null,true]");
Issue("Writing at the end of a JSON array several times doesn't correctly"
@ "keep all values.");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.int(13524));
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")), none);
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.int(121));
CheckLocation(
"/just/some/place/far/away/D",
"[true,null,null,null,null,null,null,true,13524,null,121]");
}
protected static function Test_DBCache_AllOperations_DuringLoading()
{
local AcediaObject complexData;
local ArrayList newArrayData;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45);
newArrayData = newArrayData.AddItem(none);
newArrayData = newArrayData.AddString("lol");
Issue("Increment/remove/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
default.cache.Increment(__().json.Pointer(P("")), complexData);
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi"));
default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!"));
// Override all increments!
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
CheckLocation("/B/A",
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
CheckLocation("/C", "29.5");
CheckLocation("/D", "[45,null,\"lol\"]");
}
protected static function Test_DBCache_ApplyingPendingEdits()
{
SubTest_DBCache_ApplyingPendingEdits_Simple();
SubTest_DBCache_ApplyingPendingEdits_Complex();
}
protected static function SubTest_DBCache_ApplyingPendingEdits_Simple()
{
local int i;
local array<DBCache.PendingEdit> result;
Issue("Pending writes successfully apply in simple JSON object case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(
__().json.Pointer(P("")),
__().collections.EmptyHashTable());
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
default.cache.Write(__().json.Pointer(P("/hope")), none);
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
result = default.cache.SetRealData(__().text.FromString("woah"));
for (i = 0; i < result.length; i += 1) {
TEST_ExpectTrue(result[i].successful);
}
CheckLocation("/hey", "476");
CheckLocation("/hope", "null");
CheckLocation("/-", "324.3");
Issue("Pending don't fail in a simple case of writing into JSON string.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
default.cache.Write(__().json.Pointer(P("/hope")), none);
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
result = default.cache.SetRealData(__().text.FromString("woah"));
for (i = 0; i < result.length; i += 1) {
TEST_ExpectFalse(result[i].successful);
}
CheckLocation("/hey", "null");
CheckLocation("/hope", "null");
CheckLocation("/-", "null");
}
protected static function SubTest_DBCache_ApplyingPendingEdits_Complex()
{
local array<DBCache.PendingEdit> result;
Issue("Pending writes incorrectly apply in complex case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
default.cache.Write(
__().json.Pointer(P("/new")),
__().collections.EmptyHashTable());
default.cache.Write(
__().json.Pointer(P("/new/sub")),
__().text.FromString("!SubString!"));
default.cache.Write(
__().json.Pointer(P("/D/impossiburu")),
__().text.FromString("!SubString!"));
result = default.cache.SetRealData(MakeTestJSONObject());
CheckLocation("/B/A/1/B", "777");
CheckLocation("/B/A/3", "true");
CheckLocation("/D/5", "1.1");
CheckLocation("/D/2", "null");
CheckLocation("/new", "{\"sub\":\"!SubString!\"}");
TEST_ExpectTrue(result[0].successful);
TEST_ExpectTrue(result[1].successful);
TEST_ExpectTrue(result[2].successful);
TEST_ExpectTrue(result[3].successful);
TEST_ExpectTrue(result[4].successful);
TEST_ExpectFalse(result[5].successful);
}
protected static function Test_DBCache_ApplyingPendingEdit_AllOperations()
{
local ArrayList newArrayData;
local array<DBCache.PendingEdit> result;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");
Issue("Pending increments and removals incorrectly apply in complex case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi"));
default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Remove(__().json.Pointer(P("/B/A/Y")));
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!"));
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
result = default.cache.SetRealData(MakeTestJSONObject());
TEST_ExpectTrue(result.length == 9);
TEST_ExpectTrue(result[0].successful);
TEST_ExpectTrue(result[1].successful);
TEST_ExpectTrue(result[2].successful);
TEST_ExpectTrue(result[3].successful);
TEST_ExpectTrue(result[4].successful);
TEST_ExpectFalse(result[5].successful);
TEST_ExpectFalse(result[6].successful);
TEST_ExpectTrue(result[7].successful);
TEST_ExpectTrue(result[8].successful);
}
protected static function Test_DBCache_BasicWriteRead_AfterLoading()
{
local AcediaObject complexData;
Issue("Simple write/read sequence not working.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
TEST_ExpectTrue(default.cache.SetRealData(complexData).length == 0);
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
default.cache.Write(
__().json.Pointer(P("/new")),
__().collections.EmptyHashTable());
default.cache.Write(
__().json.Pointer(P("/new/sub")),
__().text.FromString("!SubString!"));
default.cache.Write(
__().json.Pointer(P("/D/impossiburu")),
__().text.FromString("!SubString!"));
CheckLocation("/B/A/1/B", "777");
CheckLocation("/B/A/3", "true");
CheckLocation("/D/5", "1.1");
CheckLocation("/new", "{\"sub\":\"!SubString!\"}");
Issue("Simple write/read are affecting data when they shouldn't.");
CheckLocation("/D/2", "null");
default.cache.Write(__().json.Pointer(P("")), __().box.float(1.1));
default.cache.Write(__().json.Pointer(P("/hm?")), __().box.float(2.2));
CheckLocation("", "1.1");
}
protected static function Test_DBCache_AllOperations_AfterLoading()
{
local ArrayList newArrayData;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");
Issue("Increment/remove/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.SetRealData(MakeTestJSONObject());
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi")));
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))));
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/"))));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A")),
newArrayData));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/C")),
__().box.float(34.5)));
TEST_ExpectFalse(default.cache.Increment(__().json.Pointer(P("/C")),
__().box.bool(true)));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
newArrayData));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
newArrayData));
TEST_ExpectFalse(default.cache.Remove(__().json.Pointer(P("/B/A/Y"))));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!")));
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
CheckLocation("/B/A",
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
CheckLocation("/C", "29.5");
CheckLocation("/D", "[45,null,\"lol\"]");
}
defaultproperties
{
caseGroup = "Database"
caseName = "DBConnection related tests"
}

50
sources/Data/Database/DBAPI.uc

@ -111,6 +111,56 @@ public final function JSONPointer GetPointer(BaseText databaseLink)
return result;
}
/**
* Opens a new `DBConnection` to the data referred to by the database link.
*
* Opened `DBConnection` doesn't automatically start a connection, so you
* need to call its `Connect()` method.
*
* @param databaseLink Database link to the data we want to connect to.
* @return Initialized `DBConnection` in case given link is valid and `none`
* otherwise.
*/
public final function DBConnection OpenConnection(BaseText databaseLink)
{
local DBConnection result;
local Parser parser;
local Database databaseToConnect;
local JSONPointer locationToConnect;
local MutableText databaseName, textPointer;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
textPointer = parser
.R()
.MUntil(databaseName, _.text.GetCharacter(":"))
.MatchS(":")
.GetRemainderM();
if (parser.Ok())
{
databaseToConnect = LoadLocal(databaseName);
locationToConnect = _.json.Pointer(textPointer);
result = DBConnection(_.memory.Allocate(class'DBConnection'));
result.Initialize(databaseToConnect, locationToConnect);
_.memory.Free(databaseToConnect);
_.memory.Free(locationToConnect);
}
parser.FreeSelf();
_.memory.Free(databaseName);
_.memory.Free(textPointer);
if (result != none && !result.IsInitialized())
{
result.FreeSelf();
result = none;
}
return result;
}
/**
* Creates new local database with name `databaseName`.
*

7
sources/Manifest.uc

@ -53,7 +53,8 @@ defaultproperties
testCases(25) = class'TEST_BigInt'
testCases(26) = class'TEST_DatabaseCommon'
testCases(27) = class'TEST_LocalDatabase'
testCases(28) = class'TEST_AcediaConfig'
testCases(29) = class'TEST_UTF8EncoderDecoder'
testCases(30) = class'TEST_AvariceStreamReader'
testCases(28) = class'TEST_DBConnection'
testCases(29) = class'TEST_AcediaConfig'
testCases(30) = class'TEST_UTF8EncoderDecoder'
testCases(31) = class'TEST_AvariceStreamReader'
}
Loading…
Cancel
Save