├── .gitignore ├── DataAccess.uplugin ├── LICENSE ├── README.md ├── Resources └── Icon128.png └── Source └── DataAccess ├── DataAccess.Build.cs ├── Private ├── DataAccess.cpp ├── DataAccessPrivatePCH.h ├── Sqlite │ ├── SqliteDataHandler.cpp │ └── SqliteDataResource.cpp └── Tests │ ├── SqliteTest.cpp │ ├── TestObject.cpp │ └── TestObject.h └── Public ├── IDataAccess.h ├── IDataHandler.h ├── IDataResource.h └── Sqlite ├── SqliteDataHandler.h └── SqliteDataResource.h /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | ThirdParty/ 4 | Binaries/ -------------------------------------------------------------------------------- /DataAccess.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion" : 3, 3 | 4 | "FriendlyName" : "Data Access Module", 5 | "Version" : 1, 6 | "VersionName" : "1.0", 7 | "CreatedBy" : "afuzzyllama", 8 | "CreatedByURL" : "http://www.pixelsforglory.com", 9 | "EngineVersion" : "4.6.1", 10 | "Description" : "A generic interface that can support saving to different data sources", 11 | "Category" : "Pixels For Glory.Data Access", 12 | "EnabledByDefault" : true, 13 | 14 | "Modules" : 15 | [ 16 | { 17 | "Name" : "DataAccess", 18 | "Type" : "Runtime" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 afuzzyllama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Unreal® is a trademark or registered trademark of Epic Games, Inc. in the United States of America and elsewhere 24 | Unreal® Engine, Copyright 1998 – 2014, Epic Games, Inc. All rights reserved. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DataAccess 2 | ========== 3 | 4 | Data Access Module for the Unreal Engine which saves UObjects. Intended to be used with SQLite, but could be modified to support a variety of other databases if the interfaces are implemented correctly. 5 | 6 | Overview 7 | ======== 8 | 9 | While working with Unreal, I wanted to be able to save UObjects to a local database, sqlite, but I didn't want to have to have to write custom code every time I created a new object. Looking at a modern web framework like the Entity Framework, you can apply attributes to a class that will allow the framework to access meta data about your object and save it. The team at Epic have built in a sweet reflection interface for developers and I decided to see if I could use it in a similar fashion. 10 | 11 | Later I found that having a way to query my data like with Laravel would also be useful, so I threw some code together that mimics that system loosely. 12 | 13 | I have no idea if this should be used in production code, so consider this more of a proof of concept :) 14 | 15 | The plugin in its current state can save a UObject to an sqlite database if it meets these requirements: 16 | 17 | 1. It uses basic data types or a TArray. 18 | 2. It contains a property `int32 Id`, `int32 CreateTimestamp`, and `int32 LastUpdateTimestamp` 19 | 3. All property that are desired to be saved to the database, including property that are required, contain the meta data of SaveToDatabase = "true". 20 | 4. The sqlite database used contains a table that matches the class name of the object 21 | 22 | The meta data requirement will allow UObjects that are inherited from core UObjects to save specific marked property instead of trying to save all properties returned by the reflection system. 23 | 24 | Here is an example class: 25 | ``` 26 | UCLASS() 27 | class UTestObject : public UObject 28 | { 29 | GENERATED_UCLASS_BODY() 30 | 31 | public: 32 | UPROPERTY(meta = (SaveToDatabase = "true")) 33 | int32 Id; 34 | 35 | UPROPERTY(meta = (SaveToDatabase = "true")) 36 | int32 TestInt; 37 | 38 | UPROPERTY(meta = (SaveToDatabase = "true")) 39 | float TestFloat; 40 | 41 | UPROPERTY(meta = (SaveToDatabase = "true")) 42 | bool TestBool; 43 | 44 | UPROPERTY(meta = (SaveToDatabase = "true")) 45 | FString TestString; 46 | 47 | UPROPERTY(meta = (SaveToDatabase = "true")) 48 | TArray TestArray; 49 | 50 | UPROPERTY() 51 | FString TestIgnore; 52 | 53 | UPROPERTY(meta = (SaveToDatabase = "true")) 54 | int32 CreateTimestamp; 55 | 56 | UPROPERTY(meta = (SaveToDatabase = "true")) 57 | int32 LastUpdateTimestamp; 58 | }; 59 | ``` 60 | 61 | That class would have the following table and triggers created in sqlite: 62 | ``` 63 | CREATE TABLE TestObject ( 64 | Id INTEGER PRIMARY KEY AUTOINCREMENT, 65 | TestInt INTEGER, 66 | TestFloat REAL, 67 | TestBool NUMERIC, 68 | TestString TEXT, 69 | TestArray BLOB, 70 | CreateTimestamp INTEGER, 71 | LastUpdateTimestamp INTEGER 72 | ); 73 | 74 | CREATE TRIGGER TestObject_Insert 75 | AFTER INSERT ON TestObject 76 | BEGIN 77 | UPDATE TestObject 78 | SET CreateTimestamp = strftime('%s','now'), 79 | LastUpdateTimestamp = strftime('%s','now') 80 | WHERE Id = new.Id; 81 | END; 82 | 83 | CREATE TRIGGER TestObject_Update 84 | AFTER UPDATE ON TestObject FOR EACH ROW 85 | BEGIN 86 | UPDATE TestObject 87 | SET LastUpdateTimestamp = strftime('%s','now') 88 | WHERE Id = new.Id; 89 | END; 90 | ``` 91 | Usage 92 | ===== 93 | 94 | Here is a snippet of how to use the code: 95 | ``` 96 | TSharedPtr DataResource = MakeShareable(new SqliteDataResource(FString(FPaths::GameDir() + "/Data/Test.db"))); 97 | DataResource->Acquire(); 98 | TSharedPtr DataHandler = MakeShareable(new SqliteDataHandler(DataResource)); 99 | 100 | UTestObject* TestObj = NewObject(); 101 | 102 | // Create a record 103 | DataHandler->Source(UTestObject::StaticClass()).Create(TestObj); 104 | 105 | // Read a record 106 | DatHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).First(TestObj); 107 | 108 | // Read all records. Not the most ideal setup, but the array should match the amount of records returned. 109 | TArray Results; 110 | int32 Count; 111 | DatHandler->Source(UTestObject::StaticClass()).Count(Count); 112 | for(int32 i = 0; i < Count; ++i) 113 | { 114 | Results.Add(NewObject()); 115 | } 116 | DatHandler->Source(UTestObject::StaticClass()).Get(Results); 117 | 118 | // Update a record 119 | TestObj->SomeProperty = "some value"; 120 | DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).Update(TestObj); 121 | 122 | // Delete a record 123 | DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).Delete(TestObj); 124 | 125 | // Manually run a query that returns Unreal's JSON object implementation 126 | TArray< TSharedPtr > JsonArray; 127 | DataHandler->ExecuteQuery("SELECT Id FROM TestObject", JsonArray); 128 | 129 | // This shouldn't be necessary since this should be run when the TSharedPtr runs out of references 130 | DataResource->Release(); 131 | 132 | 133 | ``` 134 | 135 | 136 | Installation 137 | ============ 138 | 139 | Epic has included a way to add SQLiteSupport in the engine. Look in `Engine/Source/ThirdParty/sqlite` for more information on how to include support. This plugin will not compile without it! 140 | 141 | Notes 142 | ===== 143 | 144 | Here are some interesting points of the project: 145 | 146 | - I used the testing framework that is in the Unreal Engine. See [SqliteTest.cpp](https://github.com/afuzzyllama/DataAccess/blob/master/Source/DataAccess/Private/Tests/SqliteTest.cpp) if you are interested in looking at an example of that. To run the rest in the editor, add a sqlite database at `$(PROJECT DIR)/Data/Test.db` with the `TestObject` table inside of it. 147 | - TArrays are stored as byte arrays in the database. In theory this should work with anything you can throw at it, but I haven't tried pushing the limits too hard. 148 | - This has only been slightly tested with sqlite 3.8.6 149 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afuzzyllama/DataAccessUE4/b31ef7a8d9c1f5a7f21af0c9b9775fd0a8b55541/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/DataAccess/DataAccess.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | using System.IO; 3 | 4 | namespace UnrealBuildTool.Rules 5 | { 6 | public class DataAccess : ModuleRules 7 | { 8 | public DataAccess(TargetInfo Target) 9 | { 10 | PrivateIncludePaths.AddRange( 11 | new string[] { 12 | "DataAccess/Private" 13 | } 14 | ); 15 | 16 | PrivateDependencyModuleNames.AddRange( 17 | new string[] { 18 | "Core", 19 | "CoreUObject", 20 | "Json", 21 | "SQLiteSupport" 22 | } 23 | ); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Source/DataAccess/Private/DataAccess.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #include "DataAccessPrivatePCH.h" 4 | 5 | DEFINE_LOG_CATEGORY(LogDataAccess); 6 | 7 | class FDataAccess : public IDataAccess 8 | { 9 | /** IModuleInterface implementation */ 10 | virtual void StartupModule(); 11 | virtual void ShutdownModule(); 12 | }; 13 | 14 | IMPLEMENT_MODULE( FDataAccess, DataAccess ) 15 | 16 | void FDataAccess::StartupModule() 17 | { 18 | // This code will execute after your module is loaded into memory (but after global variables are initialized, of course.) 19 | } 20 | 21 | 22 | void FDataAccess::ShutdownModule() 23 | { 24 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 25 | // we call this function before unloading the module. 26 | } 27 | -------------------------------------------------------------------------------- /Source/DataAccess/Private/DataAccessPrivatePCH.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "IDataAccess.h" 5 | #include "CoreUObject.h" 6 | #include "Json.h" 7 | 8 | #include "sqlite3.h" 9 | 10 | // You should place include statements to your module's private header files here. You only need to 11 | // add includes for headers that are used in most of your module's source files though. 12 | 13 | DECLARE_LOG_CATEGORY_EXTERN(LogDataAccess, Log, All); -------------------------------------------------------------------------------- /Source/DataAccess/Private/Sqlite/SqliteDataHandler.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #include "DataAccessPrivatePCH.h" 3 | #include "SqliteDataResource.h" 4 | #include "SqliteDataHandler.h" 5 | 6 | SqliteDataHandler::SqliteDataHandler(TSharedPtr DataResource) 7 | : DataResource(DataResource) 8 | , QueryStarted(false) 9 | , SourceClass(nullptr) 10 | 11 | { 12 | QueryParts.Empty(); 13 | QueryParameters.Empty(); 14 | } 15 | 16 | SqliteDataHandler::~SqliteDataHandler() 17 | { 18 | QueryParts.Empty(); 19 | QueryParameters.Empty(); 20 | DataResource.Reset(); 21 | } 22 | 23 | IDataHandler& SqliteDataHandler::Source(UClass* Source) 24 | { 25 | check(Source); 26 | UIntProperty* IdProperty = FindFieldChecked(Source, "Id"); 27 | check(IdProperty); 28 | 29 | QueryStarted = true; 30 | SourceClass = Source; 31 | QueryParts.Empty(); 32 | QueryParameters.Empty(); 33 | 34 | return *this; 35 | } 36 | 37 | IDataHandler& SqliteDataHandler::Where(FString FieldName, EDataHandlerOperator Operator, FString Condition) 38 | { 39 | check(QueryStarted == true); 40 | bool bFound = false; 41 | 42 | UClass* FieldType = nullptr; 43 | // Terrible search, could be implemented better 44 | for(TFieldIterator Itr(SourceClass); Itr; ++Itr) 45 | { 46 | UProperty* Property = *Itr; 47 | 48 | if(Property->GetName() == FieldName) 49 | { 50 | bFound = true; 51 | FieldType = Property->GetClass(); 52 | break; 53 | } 54 | } 55 | 56 | if(!bFound) 57 | { 58 | UE_LOG(LogDataAccess, Error, TEXT("Where: FieldName \"%s\" does not exist in UClass \"%s\". Clause not added"), *(FieldName), *(SourceClass->GetName())); 59 | return *this; 60 | } 61 | 62 | QueryParts.Add(FieldName); 63 | 64 | switch(Operator) 65 | { 66 | case GreaterThan: 67 | QueryParts.Add(">"); 68 | break; 69 | case LessThan: 70 | QueryParts.Add("<"); 71 | break; 72 | case Equals: 73 | QueryParts.Add("="); 74 | break; 75 | case LessThanOrEqualTo: 76 | QueryParts.Add("<="); 77 | break; 78 | case GreaterThanOrEqualTo: 79 | QueryParts.Add(">="); 80 | break; 81 | case NotEqualTo: 82 | QueryParts.Add("<>"); 83 | break; 84 | } 85 | 86 | QueryParts.Add("?"); 87 | 88 | TPair NewPair; 89 | NewPair.Key = FieldType; 90 | NewPair.Value = Condition; 91 | 92 | QueryParameters.Add(NewPair); 93 | 94 | return *this; 95 | } 96 | 97 | IDataHandler& SqliteDataHandler::Or() 98 | { 99 | check(QueryStarted == true); 100 | QueryParts.Add("OR"); 101 | return *this; 102 | } 103 | 104 | IDataHandler& SqliteDataHandler::And() 105 | { 106 | check(QueryStarted == true); 107 | QueryParts.Add("AND"); 108 | return *this; 109 | } 110 | 111 | IDataHandler& SqliteDataHandler::BeginNested() 112 | { 113 | check(QueryStarted == true); 114 | QueryParts.Add("("); 115 | return *this; 116 | } 117 | 118 | IDataHandler& SqliteDataHandler::EndNested() 119 | { 120 | check(QueryStarted == true); 121 | QueryParts.Add(")"); 122 | return *this; 123 | } 124 | 125 | bool SqliteDataHandler::Create(UObject* const Obj) 126 | { 127 | check(Obj); 128 | check(QueryStarted == true); 129 | // Check that our object exists and that it had an Id property 130 | check(Obj->GetClass()->GetName() == SourceClass->GetName()); 131 | 132 | // Build column names and values for the insert 133 | FString Columns("("); 134 | FString Values("("); 135 | 136 | for(TFieldIterator Itr(Obj->GetClass()); Itr; ++Itr) 137 | { 138 | UProperty* Property = *Itr; 139 | 140 | if (Property->GetName() == "Id" || Property->GetName() == "CreateTimestamp" || Property->GetName() == "LastUpdateTimestamp" || !Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 141 | { 142 | continue; 143 | } 144 | 145 | Columns += FString::Printf(TEXT("%s,"), *(Property->GetName())); 146 | Values += "?,"; 147 | } 148 | Columns.RemoveFromEnd(",", ESearchCase::IgnoreCase); 149 | Values.RemoveFromEnd(",", ESearchCase::IgnoreCase); 150 | Columns += ")"; 151 | Values += ")"; 152 | 153 | FString SqlStatement(FString::Printf(TEXT("INSERT INTO %s %s VALUES %s;"), *(Obj->GetClass()->GetName()), *Columns, *Values)); 154 | 155 | // Create a prepared statement and bind the UObject to it 156 | sqlite3_stmt* SqliteStatement; 157 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 158 | { 159 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 160 | sqlite3_finalize(SqliteStatement); 161 | ClearQuery(); 162 | return false; 163 | } 164 | 165 | if(!BindObjectToStatement(Obj, SqliteStatement)) 166 | { 167 | UE_LOG(LogDataAccess, Error, TEXT("Create: error binding sqlite statement.")); 168 | sqlite3_finalize(SqliteStatement); 169 | ClearQuery(); 170 | return false; 171 | } 172 | 173 | // Execute 174 | if(sqlite3_step(SqliteStatement) != SQLITE_DONE) 175 | { 176 | UE_LOG(LogDataAccess, Error, TEXT("Create: error executing insert statement.. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 177 | sqlite3_finalize(SqliteStatement); 178 | ClearQuery(); 179 | return false; 180 | } 181 | sqlite3_finalize(SqliteStatement); 182 | 183 | 184 | // Get last id, create, and update timestamps and update the UObject 185 | int32 LastId = sqlite3_last_insert_rowid(DataResource->Get()); 186 | UIntProperty* IdProperty = FindFieldChecked(Obj->GetClass(), "Id"); 187 | IdProperty->SetPropertyValue_InContainer(Obj, LastId); 188 | 189 | SqlStatement = FString::Printf(TEXT("SELECT CreateTimestamp, LastUpdateTimestamp FROM %s WHERE Id = ?;"), *(Obj->GetClass()->GetName())); 190 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*(SqlStatement)), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 191 | { 192 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot prepare sqlite statement for timestamps. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 193 | sqlite3_finalize(SqliteStatement); 194 | ClearQuery(); 195 | return false; 196 | } 197 | 198 | if(sqlite3_bind_int(SqliteStatement, 1, LastId)) 199 | { 200 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot bind class name to sqlite statement for seq. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 201 | sqlite3_finalize(SqliteStatement); 202 | ClearQuery(); 203 | return false; 204 | } 205 | 206 | if(sqlite3_step(SqliteStatement) != SQLITE_ROW) 207 | { 208 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot step sqlite statement for seq. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 209 | sqlite3_finalize(SqliteStatement); 210 | ClearQuery(); 211 | return false; 212 | } 213 | 214 | UIntProperty* CreateTimestampProperty = FindFieldChecked(Obj->GetClass(), "CreateTimestamp"); 215 | CreateTimestampProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, 0)); 216 | 217 | UIntProperty* LastUpdateTimestampProperty = FindFieldChecked(Obj->GetClass(), "LastUpdateTimestamp"); 218 | LastUpdateTimestampProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, 1)); 219 | 220 | sqlite3_finalize(SqliteStatement); 221 | ClearQuery(); 222 | return true; 223 | } 224 | 225 | 226 | bool SqliteDataHandler::Update(UObject* const Obj) 227 | { 228 | check(Obj); 229 | check(QueryStarted == true); 230 | check(Obj->GetClass()->GetName() == SourceClass->GetName()); 231 | 232 | // Build set commands for the update 233 | FString Sets; 234 | int32 PropertyCount = 0; 235 | for(TFieldIterator Itr(Obj->GetClass()); Itr; ++Itr) 236 | { 237 | UProperty* Property = *Itr; 238 | if (Property->GetName() == "Id" || Property->GetName() == "CreateTimestamp" || Property->GetName() == "LastUpdateTimestamp" || !Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 239 | { 240 | continue; 241 | } 242 | 243 | ++PropertyCount; 244 | Sets += FString::Printf(TEXT("%s = ?,"), *(Property->GetName())); 245 | } 246 | Sets.RemoveFromEnd(",", ESearchCase::IgnoreCase); 247 | 248 | FString SqlStatement(FString::Printf(TEXT("UPDATE %s SET %s %s;"), *(SourceClass->GetName()), *Sets, *(GenerateWhereClause()))); 249 | 250 | // Prepare a statement and bind the UObject to it. Also bind the update Id. 251 | sqlite3_stmt* SqliteStatement; 252 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 253 | { 254 | UE_LOG(LogDataAccess, Error, TEXT("Update: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 255 | sqlite3_finalize(SqliteStatement); 256 | ClearQuery(); 257 | return false; 258 | } 259 | 260 | if(!BindObjectToStatement(Obj, SqliteStatement)) 261 | { 262 | UE_LOG(LogDataAccess, Error, TEXT("Update: error binding sqlite statement.")); 263 | sqlite3_finalize(SqliteStatement); 264 | ClearQuery(); 265 | return false; 266 | } 267 | 268 | // Bind Where Paramters 269 | if(!BindWhereToStatement(SqliteStatement, PropertyCount + 1)) 270 | { 271 | UE_LOG(LogDataAccess, Error, TEXT("Update: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 272 | sqlite3_finalize(SqliteStatement); 273 | ClearQuery(); 274 | return false; 275 | } 276 | 277 | // Execute 278 | if(sqlite3_step(SqliteStatement) != SQLITE_DONE) 279 | { 280 | UE_LOG(LogDataAccess, Error, TEXT("Update: error executing update statement.. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 281 | sqlite3_finalize(SqliteStatement); 282 | ClearQuery(); 283 | return false; 284 | } 285 | 286 | if(sqlite3_changes(DataResource->Get()) == 0) 287 | { 288 | UE_LOG(LogDataAccess, Log, TEXT("Update: Nothing to update")); 289 | sqlite3_finalize(SqliteStatement); 290 | ClearQuery(); 291 | return false; 292 | } 293 | sqlite3_finalize(SqliteStatement); 294 | 295 | // Get create and update timestamps and update the UObject 296 | SqlStatement = FString::Printf(TEXT("SELECT DISTINCT LastUpdateTimestamp FROM %s %s;"), *(Obj->GetClass()->GetName()), *(GenerateWhereClause())); 297 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*(SqlStatement)), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 298 | { 299 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot prepare sqlite statement for timestamp. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 300 | sqlite3_finalize(SqliteStatement); 301 | ClearQuery(); 302 | return false; 303 | } 304 | 305 | if(!BindWhereToStatement(SqliteStatement, 1)) 306 | { 307 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot bind where clause for timestamp. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 308 | sqlite3_finalize(SqliteStatement); 309 | ClearQuery(); 310 | return false; 311 | } 312 | 313 | if(sqlite3_step(SqliteStatement) != SQLITE_ROW) 314 | { 315 | UE_LOG(LogDataAccess, Error, TEXT("Create: cannot step sqlite statement for seq. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 316 | sqlite3_finalize(SqliteStatement); 317 | ClearQuery(); 318 | return false; 319 | } 320 | 321 | UIntProperty* LastUpdateTimestampProperty = FindFieldChecked(Obj->GetClass(), "LastUpdateTimestamp"); 322 | LastUpdateTimestampProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, 0)); 323 | 324 | sqlite3_finalize(SqliteStatement); 325 | ClearQuery(); 326 | return true; 327 | } 328 | 329 | bool SqliteDataHandler::Delete() 330 | { 331 | check(QueryStarted == true); 332 | 333 | FString SqlStatement(FString::Printf(TEXT("DELETE FROM %s %s"), *(SourceClass->GetName()), *(GenerateWhereClause()))); 334 | 335 | // Prepare a statement and bind the Id to it 336 | sqlite3_stmt* SqliteStatement; 337 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 338 | { 339 | UE_LOG(LogDataAccess, Error, TEXT("Delete: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 340 | sqlite3_finalize(SqliteStatement); 341 | ClearQuery(); 342 | return false; 343 | } 344 | 345 | // Bind Where Paramters 346 | if(!BindWhereToStatement(SqliteStatement)) 347 | { 348 | UE_LOG(LogDataAccess, Error, TEXT("Delete: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 349 | sqlite3_finalize(SqliteStatement); 350 | ClearQuery(); 351 | return false; 352 | } 353 | 354 | // Execute 355 | if(sqlite3_step(SqliteStatement) != SQLITE_DONE) 356 | { 357 | UE_LOG(LogDataAccess, Error, TEXT("Delete: error executing delete statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 358 | sqlite3_finalize(SqliteStatement); 359 | ClearQuery(); 360 | return false; 361 | } 362 | 363 | if(sqlite3_changes(DataResource->Get()) == 0) 364 | { 365 | UE_LOG(LogDataAccess, Log, TEXT("Delete: Nothing to delete")); 366 | sqlite3_finalize(SqliteStatement); 367 | ClearQuery(); 368 | return false; 369 | } 370 | 371 | sqlite3_finalize(SqliteStatement); 372 | ClearQuery(); 373 | return true; 374 | } 375 | 376 | bool SqliteDataHandler::Count(int32& OutCount) 377 | { 378 | check(QueryStarted == true); 379 | 380 | OutCount = 0; 381 | FString SqlStatement(FString::Printf(TEXT("SELECT COUNT(Id) FROM %s %s;"), *(SourceClass->GetName()), *(GenerateWhereClause()))); 382 | 383 | // Preare statement and bind Id to it 384 | sqlite3_stmt* SqliteStatement; 385 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 386 | { 387 | UE_LOG(LogDataAccess, Error, TEXT("Count: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 388 | sqlite3_finalize(SqliteStatement); 389 | ClearQuery(); 390 | return false; 391 | } 392 | 393 | // Bind Where Paramters 394 | if(!BindWhereToStatement(SqliteStatement)) 395 | { 396 | UE_LOG(LogDataAccess, Error, TEXT("Count: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 397 | sqlite3_finalize(SqliteStatement); 398 | ClearQuery(); 399 | return false; 400 | } 401 | 402 | // Execute 403 | int32 ResultCode = sqlite3_step(SqliteStatement); 404 | if(ResultCode == SQLITE_DONE) 405 | { 406 | OutCount = 0; 407 | } 408 | else if(ResultCode != SQLITE_ROW) 409 | { 410 | UE_LOG(LogDataAccess, Error, TEXT("Count: error executing select statement.")); 411 | sqlite3_finalize(SqliteStatement); 412 | ClearQuery(); 413 | return false; 414 | } 415 | else 416 | { 417 | // Bind the results to the passed in object 418 | OutCount = sqlite3_column_int(SqliteStatement, 0); 419 | } 420 | 421 | sqlite3_finalize(SqliteStatement); 422 | ClearQuery(); 423 | return true; 424 | } 425 | 426 | bool SqliteDataHandler::First(UObject* const OutObj) 427 | { 428 | check(OutObj); 429 | check(QueryStarted == true); 430 | 431 | // Build columns for select statement 432 | FString Columns; 433 | for(TFieldIterator Itr(SourceClass); Itr; ++Itr) 434 | { 435 | UProperty* Property = *Itr; 436 | 437 | if (!Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 438 | { 439 | continue; 440 | } 441 | 442 | Columns += FString::Printf(TEXT("%s,"), *(Property->GetName())); 443 | } 444 | Columns.RemoveFromEnd(",", ESearchCase::IgnoreCase); 445 | 446 | FString SqlStatement(FString::Printf(TEXT("SELECT %s FROM %s %s;"), *Columns, *(SourceClass->GetName()), *(GenerateWhereClause()))); 447 | 448 | // Preare statement and bind Id to it 449 | sqlite3_stmt* SqliteStatement; 450 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 451 | { 452 | UE_LOG(LogDataAccess, Error, TEXT("First: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 453 | sqlite3_finalize(SqliteStatement); 454 | ClearQuery(); 455 | return false; 456 | } 457 | 458 | // Bind Where Paramters 459 | if(!BindWhereToStatement(SqliteStatement)) 460 | { 461 | UE_LOG(LogDataAccess, Error, TEXT("First: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 462 | sqlite3_finalize(SqliteStatement); 463 | ClearQuery(); 464 | return false; 465 | } 466 | 467 | // Execute 468 | int32 ResultCode = sqlite3_step(SqliteStatement); 469 | 470 | if(ResultCode == SQLITE_DONE) 471 | { 472 | //UE_LOG(LogDataAccess, Log, TEXT("First: nothing selected.")); 473 | sqlite3_finalize(SqliteStatement); 474 | ClearQuery(); 475 | return false; 476 | } 477 | else if(ResultCode != SQLITE_ROW) 478 | { 479 | UE_LOG(LogDataAccess, Error, TEXT("First: error executing select statement.")); 480 | sqlite3_finalize(SqliteStatement); 481 | ClearQuery(); 482 | return false; 483 | } 484 | 485 | // Bind the results to the passed in object 486 | if(!BindStatementToObject(SqliteStatement, OutObj)) 487 | { 488 | UE_LOG(LogDataAccess, Error, TEXT("First: error binding results.")); 489 | sqlite3_finalize(SqliteStatement); 490 | ClearQuery(); 491 | return false; 492 | } 493 | 494 | sqlite3_finalize(SqliteStatement); 495 | ClearQuery(); 496 | return true; 497 | } 498 | 499 | bool SqliteDataHandler::Get(TArray& OutObjs) 500 | { 501 | check(QueryStarted == true); 502 | 503 | if(OutObjs.Num() <= 0) 504 | { 505 | UE_LOG(LogDataAccess, Error, TEXT("Get: cannot get with an empty array")); 506 | OutObjs.Empty(); 507 | ClearQuery(); 508 | return false; 509 | } 510 | 511 | // Build columns for select statement 512 | FString Columns; 513 | for(TFieldIterator Itr(SourceClass); Itr; ++Itr) 514 | { 515 | UProperty* Property = *Itr; 516 | 517 | // All properties to save to the database require the SaveToDatabase attribute 518 | if (!Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 519 | { 520 | continue; 521 | } 522 | 523 | Columns += FString::Printf(TEXT("%s,"), *(Property->GetName())); 524 | } 525 | Columns.RemoveFromEnd(",", ESearchCase::IgnoreCase); 526 | 527 | FString SqlStatement(FString::Printf(TEXT("SELECT %s FROM %s %s;"), *Columns, *(SourceClass->GetName()), *(GenerateWhereClause()))); 528 | 529 | // Preare statement and bind Id to it 530 | sqlite3_stmt* SqliteStatement; 531 | if(sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*SqlStatement), FCString::Strlen(*SqlStatement), &SqliteStatement, nullptr) != SQLITE_OK) 532 | { 533 | UE_LOG(LogDataAccess, Error, TEXT("Get: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 534 | sqlite3_finalize(SqliteStatement); 535 | OutObjs.Empty(); 536 | ClearQuery(); 537 | return false; 538 | } 539 | 540 | // Bind Where Paramters 541 | if(!BindWhereToStatement(SqliteStatement)) 542 | { 543 | UE_LOG(LogDataAccess, Error, TEXT("Get: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 544 | sqlite3_finalize(SqliteStatement); 545 | OutObjs.Empty(); 546 | ClearQuery(); 547 | return false; 548 | } 549 | 550 | // Execute 551 | int32 ResultCode = sqlite3_step(SqliteStatement); 552 | if(ResultCode == SQLITE_DONE) 553 | { 554 | //UE_LOG(LogDataAccess, Log, TEXT("Get: nothing selected.")); 555 | sqlite3_finalize(SqliteStatement); 556 | OutObjs.Empty(); 557 | ClearQuery(); 558 | return false; 559 | } 560 | else if(ResultCode != SQLITE_ROW) 561 | { 562 | UE_LOG(LogDataAccess, Error, TEXT("Get: error executing select statement.")); 563 | sqlite3_finalize(SqliteStatement); 564 | OutObjs.Empty(); 565 | ClearQuery(); 566 | return false; 567 | } 568 | 569 | int32 CurrentIndex = 0; 570 | // Bind the results to the passed in object 571 | while(ResultCode != SQLITE_DONE) 572 | { 573 | if(CurrentIndex == OutObjs.Num()) 574 | { 575 | UE_LOG(LogDataAccess, Warning, TEXT("Get: Passed array not large enough to handle all objects. %i objected returned"), CurrentIndex); 576 | break; 577 | } 578 | 579 | if(!BindStatementToObject(SqliteStatement, OutObjs[CurrentIndex])) 580 | { 581 | UE_LOG(LogDataAccess, Error, TEXT("Get: error binding results.")); 582 | sqlite3_finalize(SqliteStatement); 583 | ClearQuery(); 584 | OutObjs.Empty(); 585 | return false; 586 | } 587 | ResultCode = sqlite3_step(SqliteStatement); 588 | ++CurrentIndex; 589 | } 590 | 591 | sqlite3_finalize(SqliteStatement); 592 | ClearQuery(); 593 | return true; 594 | } 595 | 596 | bool SqliteDataHandler::ExecuteQuery(FString Query, TArray< TSharedPtr >& JsonArray) 597 | { 598 | // A query cannot be started before a manual query execution 599 | check(QueryStarted == false); 600 | 601 | JsonArray.Empty(); 602 | 603 | // Preare statement and bind Id to it 604 | sqlite3_stmt* SqliteStatement; 605 | if (sqlite3_prepare_v2(DataResource->Get(), TCHAR_TO_UTF8(*Query), FCString::Strlen(*Query), &SqliteStatement, nullptr) != SQLITE_OK) 606 | { 607 | UE_LOG(LogDataAccess, Error, TEXT("Get: cannot prepare sqlite statement. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 608 | sqlite3_finalize(SqliteStatement); 609 | ClearQuery(); 610 | return false; 611 | } 612 | 613 | // Bind Where Paramters 614 | if (!BindWhereToStatement(SqliteStatement)) 615 | { 616 | UE_LOG(LogDataAccess, Error, TEXT("Get: cannot bind where clause. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 617 | sqlite3_finalize(SqliteStatement); 618 | ClearQuery(); 619 | return false; 620 | } 621 | 622 | // Execute 623 | int32 ResultCode = sqlite3_step(SqliteStatement); 624 | if (ResultCode == SQLITE_DONE) 625 | { 626 | //UE_LOG(LogDataAccess, Log, TEXT("Get: nothing selected.")); 627 | sqlite3_finalize(SqliteStatement); 628 | ClearQuery(); 629 | return false; 630 | } 631 | else if (ResultCode != SQLITE_ROW) 632 | { 633 | UE_LOG(LogDataAccess, Error, TEXT("Get: error executing select statement.")); 634 | sqlite3_finalize(SqliteStatement); 635 | ClearQuery(); 636 | return false; 637 | } 638 | 639 | int32 CurrentIndex = 0; 640 | // Bind the results to the passed in object 641 | while (ResultCode != SQLITE_DONE) 642 | { 643 | TSharedPtr< FJsonValue > JsonValue; 644 | if (!BindStatementToArray(SqliteStatement, JsonValue)) 645 | { 646 | UE_LOG(LogDataAccess, Error, TEXT("Get: error binding results.")); 647 | sqlite3_finalize(SqliteStatement); 648 | JsonArray.Empty(); 649 | ClearQuery(); 650 | return false; 651 | } 652 | JsonArray.Add(JsonValue); 653 | 654 | ResultCode = sqlite3_step(SqliteStatement); 655 | ++CurrentIndex; 656 | } 657 | 658 | sqlite3_finalize(SqliteStatement); 659 | ClearQuery(); 660 | return true; 661 | } 662 | 663 | void SqliteDataHandler::ClearQuery() 664 | { 665 | QueryStarted = false; 666 | SourceClass = nullptr; 667 | QueryParts.Empty(); 668 | QueryParameters.Empty(); 669 | } 670 | 671 | FString SqliteDataHandler::GenerateWhereClause() 672 | { 673 | FString WhereClause(""); 674 | if(QueryParts.Num() > 0) 675 | { 676 | WhereClause = "WHERE " + FString::Join(QueryParts, TEXT(" ")); 677 | } 678 | 679 | return WhereClause; 680 | } 681 | 682 | bool SqliteDataHandler::BindWhereToStatement(sqlite3_stmt* const SqliteStatement, int32 ParameterIndex) 683 | { 684 | bool bSuccess = true; 685 | // Binding index is 1 based not 0 based 686 | for(auto Itr = QueryParameters.CreateConstIterator(); Itr; ++Itr) 687 | { 688 | const TPair CurrentParameter = *Itr; 689 | 690 | if(CurrentParameter.Key->GetName() == UByteProperty::StaticClass()->GetName()) 691 | { 692 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 693 | { 694 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind byte. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 695 | bSuccess = false; 696 | } 697 | } 698 | else if(CurrentParameter.Key->GetName() == UInt8Property::StaticClass()->GetName()) 699 | { 700 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 701 | { 702 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind int8. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 703 | bSuccess = false; 704 | } 705 | } 706 | else if(CurrentParameter.Key->GetName() == UInt16Property::StaticClass()->GetName()) 707 | { 708 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 709 | { 710 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind int16. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 711 | bSuccess = false; 712 | } 713 | } 714 | else if(CurrentParameter.Key->GetName() == UIntProperty::StaticClass()->GetName()) 715 | { 716 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 717 | { 718 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind int32. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 719 | bSuccess = false; 720 | } 721 | } 722 | else if(CurrentParameter.Key->GetName() == UInt64Property::StaticClass()->GetName()) 723 | { 724 | if(sqlite3_bind_int64(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 725 | { 726 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind int64. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 727 | bSuccess = false; 728 | } 729 | } 730 | else if(CurrentParameter.Key->GetName() == UUInt16Property::StaticClass()->GetName()) 731 | { 732 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 733 | { 734 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind uint16. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 735 | bSuccess = false; 736 | } 737 | } 738 | else if(CurrentParameter.Key->GetName() == UUInt32Property::StaticClass()->GetName()) 739 | { 740 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 741 | { 742 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind uint32. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 743 | bSuccess = false; 744 | } 745 | } 746 | else if(CurrentParameter.Key->GetName() == UUInt64Property::StaticClass()->GetName()) 747 | { 748 | if(sqlite3_bind_int64(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 749 | { 750 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind uint64. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 751 | bSuccess = false; 752 | } 753 | } 754 | else if(CurrentParameter.Key->GetName() == UFloatProperty::StaticClass()->GetName()) 755 | { 756 | if(sqlite3_bind_double(SqliteStatement, ParameterIndex, FCString::Atof(*CurrentParameter.Value)) != SQLITE_OK) 757 | { 758 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind float. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 759 | bSuccess = false; 760 | } 761 | } 762 | else if(CurrentParameter.Key->GetName() == UDoubleProperty::StaticClass()->GetName()) 763 | { 764 | if(sqlite3_bind_double(SqliteStatement, ParameterIndex, FCString::Atof(*CurrentParameter.Value)) != SQLITE_OK) 765 | { 766 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind double. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 767 | bSuccess = false; 768 | } 769 | } 770 | else if(CurrentParameter.Key->GetName() == UBoolProperty::StaticClass()->GetName()) 771 | { 772 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, FCString::Atoi(*CurrentParameter.Value)) != SQLITE_OK) 773 | { 774 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind bool. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 775 | bSuccess = false; 776 | } 777 | } 778 | else if(CurrentParameter.Key->GetName() == UStrProperty::StaticClass()->GetName()) 779 | { 780 | if(sqlite3_bind_text(SqliteStatement, ParameterIndex, TCHAR_TO_UTF8(*CurrentParameter.Value), FCString::Strlen(*CurrentParameter.Value), SQLITE_TRANSIENT) != SQLITE_OK) 781 | { 782 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: cannot bind string. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 783 | bSuccess = false; 784 | } 785 | } 786 | else 787 | { 788 | UE_LOG(LogDataAccess, Error, TEXT("BindWhereToStatement: Data type %s is not supported"), *(CurrentParameter.Key->GetName())); 789 | bSuccess = false; 790 | } 791 | ++ParameterIndex; 792 | } 793 | 794 | return bSuccess; 795 | } 796 | 797 | bool SqliteDataHandler::BindObjectToStatement(UObject* const Obj, sqlite3_stmt* const SqliteStatement) 798 | { 799 | check(SqliteStatement); 800 | int32 ParameterIndex = 1; 801 | bool bSuccess = true; 802 | 803 | // Iterate through all of the UObject properties and bind values to the passed in prepared statement. 804 | // Since the query is built off the same query of the object. The ordering should be the same. 805 | for(TFieldIterator Itr(Obj->GetClass()); Itr; ++Itr) 806 | { 807 | UProperty* Property = *Itr; 808 | if (Property->GetName() == "Id" || Property->GetName() == "CreateTimestamp" || Property->GetName() == "LastUpdateTimestamp" || !Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 809 | { 810 | continue; 811 | } 812 | 813 | if(Property->IsA(UByteProperty::StaticClass())) 814 | { 815 | UByteProperty* ByteProperty = CastChecked(Property); 816 | uint8 CurrentValue = ByteProperty->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 817 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 818 | { 819 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind byte. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 820 | bSuccess = false; 821 | } 822 | } 823 | else if(Property->IsA(UInt8Property::StaticClass())) 824 | { 825 | UInt8Property* Int8Property = CastChecked(Property); 826 | int8 CurrentValue = Int8Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 827 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 828 | { 829 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind int8. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 830 | bSuccess = false; 831 | } 832 | } 833 | else if(Property->IsA(UInt16Property::StaticClass())) 834 | { 835 | UInt16Property* Int16Property = CastChecked(Property); 836 | int16 CurrentValue = Int16Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 837 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 838 | { 839 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind int16. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 840 | bSuccess = false; 841 | } 842 | } 843 | else if(Property->IsA(UIntProperty::StaticClass())) 844 | { 845 | UIntProperty* Int32Property = CastChecked(Property); 846 | int32 CurrentValue = Int32Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 847 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 848 | { 849 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind int32. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 850 | bSuccess = false; 851 | } 852 | } 853 | else if(Property->IsA(UInt64Property::StaticClass())) 854 | { 855 | UInt64Property* Int64Property = CastChecked(Property); 856 | int64 CurrentValue = Int64Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 857 | if(sqlite3_bind_int64(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 858 | { 859 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind int64. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 860 | bSuccess = false; 861 | } 862 | } 863 | else if(Property->IsA(UUInt16Property::StaticClass())) 864 | { 865 | UUInt16Property* UInt16Property = CastChecked(Property); 866 | uint16 CurrentValue = UInt16Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 867 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 868 | { 869 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind uint16. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 870 | bSuccess = false; 871 | } 872 | } 873 | else if(Property->IsA(UUInt32Property::StaticClass())) 874 | { 875 | UUInt32Property* UInt32Property = CastChecked(Property); 876 | uint32 CurrentValue = UInt32Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 877 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 878 | { 879 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind uint32. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 880 | bSuccess = false; 881 | } 882 | } 883 | else if(Property->IsA(UUInt64Property::StaticClass())) 884 | { 885 | UUInt64Property* UInt64Property = CastChecked(Property); 886 | uint64 CurrentValue = UInt64Property->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 887 | if(sqlite3_bind_int64(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 888 | { 889 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind uint64. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 890 | bSuccess = false; 891 | } 892 | } 893 | else if(Property->IsA(UFloatProperty::StaticClass())) 894 | { 895 | UFloatProperty* FloatProperty = CastChecked(Property); 896 | float CurrentValue = FloatProperty->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 897 | if(sqlite3_bind_double(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 898 | { 899 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind float. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 900 | bSuccess = false; 901 | } 902 | } 903 | else if(Property->IsA(UDoubleProperty::StaticClass())) 904 | { 905 | UDoubleProperty* DoubleProperty = CastChecked(Property); 906 | double CurrentValue = DoubleProperty->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 907 | if(sqlite3_bind_double(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 908 | { 909 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind double. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 910 | bSuccess = false; 911 | } 912 | } 913 | else if(Property->IsA(UBoolProperty::StaticClass())) 914 | { 915 | UBoolProperty* BoolProperty = CastChecked(Property); 916 | bool CurrentValue = BoolProperty->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 917 | if(sqlite3_bind_int(SqliteStatement, ParameterIndex, CurrentValue) != SQLITE_OK) 918 | { 919 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind bool. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 920 | bSuccess = false; 921 | } 922 | } 923 | else if(Property->IsA(UStrProperty::StaticClass())) 924 | { 925 | UStrProperty* StrProperty = CastChecked(Property); 926 | FString CurrentValue = StrProperty->GetPropertyValue(Property->ContainerPtrToValuePtr(Obj)); 927 | if(sqlite3_bind_text(SqliteStatement, ParameterIndex, TCHAR_TO_UTF8(*CurrentValue), FCString::Strlen(*CurrentValue), SQLITE_TRANSIENT) != SQLITE_OK) 928 | { 929 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind string. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 930 | bSuccess = false; 931 | } 932 | } 933 | else if(Property->IsA(UArrayProperty::StaticClass())) 934 | { 935 | UArrayProperty* ArrayProperty = CastChecked(Property); 936 | FScriptArrayHelper_InContainer ArrayHelper(ArrayProperty, Obj); 937 | if(sqlite3_bind_blob(SqliteStatement, ParameterIndex, ArrayHelper.GetRawPtr(), ArrayHelper.Num() * ArrayProperty->Inner->ElementSize, SQLITE_TRANSIENT) != SQLITE_OK) 938 | { 939 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: cannot bind array. Error message \"%s\""), UTF8_TO_TCHAR(sqlite3_errmsg(DataResource->Get()))); 940 | bSuccess = false; 941 | } 942 | } 943 | else 944 | { 945 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: Data type on UPROPERTY() %s is not supported"), *(Property->GetName())); 946 | bSuccess = false; 947 | } 948 | ++ParameterIndex; 949 | } 950 | 951 | return bSuccess; 952 | } 953 | 954 | bool SqliteDataHandler::BindStatementToObject(sqlite3_stmt* const SqliteStatement, UObject* const Obj) 955 | { 956 | check(SqliteStatement); 957 | 958 | int32 ColumnIndex = 0; 959 | bool bSuccess = true; 960 | 961 | // Iterator through all of the UObject properties and the passed in prepared statement to it 962 | // Since the query is built off the same query of the object. The ordering should be the same. 963 | for(TFieldIterator Itr(Obj->GetClass()); Itr; ++Itr) 964 | { 965 | UProperty* Property = *Itr; 966 | 967 | // All properties to save to the database require the SaveToDatabase attribute 968 | if (!Property->HasMetaData("SaveToDatabase") || !Property->GetMetaData("SaveToDatabase").ToUpper().Equals("TRUE")) 969 | { 970 | continue; 971 | } 972 | 973 | if(Property->IsA(UByteProperty::StaticClass())) 974 | { 975 | UByteProperty* ByteProperty = CastChecked(Property); 976 | ByteProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 977 | } 978 | else if(Property->IsA(UInt8Property::StaticClass())) 979 | { 980 | UInt8Property* Int8Property = CastChecked(Property); 981 | Int8Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 982 | } 983 | else if(Property->IsA(UInt16Property::StaticClass())) 984 | { 985 | UInt16Property* Int16Property = CastChecked(Property); 986 | Int16Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 987 | } 988 | else if(Property->IsA(UIntProperty::StaticClass())) 989 | { 990 | UIntProperty* Int32Property = CastChecked(Property); 991 | Int32Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 992 | } 993 | else if(Property->IsA(UInt64Property::StaticClass())) 994 | { 995 | UInt64Property* Int64Property = CastChecked(Property); 996 | Int64Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int64(SqliteStatement, ColumnIndex)); 997 | } 998 | else if(Property->IsA(UUInt16Property::StaticClass())) 999 | { 1000 | UUInt16Property* UInt16Property = CastChecked(Property); 1001 | UInt16Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 1002 | } 1003 | else if(Property->IsA(UUInt32Property::StaticClass())) 1004 | { 1005 | UUInt32Property* UInt32Property = CastChecked(Property); 1006 | UInt32Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int(SqliteStatement, ColumnIndex)); 1007 | } 1008 | else if(Property->IsA(UUInt64Property::StaticClass())) 1009 | { 1010 | UUInt64Property* UInt64Property = CastChecked(Property); 1011 | UInt64Property->SetPropertyValue_InContainer(Obj, sqlite3_column_int64(SqliteStatement, ColumnIndex)); 1012 | } 1013 | else if(Property->IsA(UFloatProperty::StaticClass())) 1014 | { 1015 | UFloatProperty* FloatProperty = CastChecked(Property); 1016 | FloatProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_double(SqliteStatement, ColumnIndex)); 1017 | } 1018 | else if(Property->IsA(UDoubleProperty::StaticClass())) 1019 | { 1020 | UDoubleProperty* DoubleProperty = CastChecked(Property); 1021 | DoubleProperty->SetPropertyValue_InContainer(Obj, sqlite3_column_double(SqliteStatement, ColumnIndex)); 1022 | } 1023 | else if(Property->IsA(UBoolProperty::StaticClass())) 1024 | { 1025 | UBoolProperty* BoolProperty = CastChecked(Property); 1026 | BoolProperty->SetPropertyValue_InContainer(Obj, (sqlite3_column_int(SqliteStatement, ColumnIndex) == 0 ? false : true)); 1027 | } 1028 | else if(Property->IsA(UStrProperty::StaticClass())) 1029 | { 1030 | UStrProperty* StrProperty = CastChecked(Property); 1031 | StrProperty->SetPropertyValue_InContainer(Obj, UTF8_TO_TCHAR(sqlite3_column_text(SqliteStatement, ColumnIndex))); 1032 | } 1033 | else if(Property->IsA(UArrayProperty::StaticClass())) 1034 | { 1035 | UArrayProperty* ArrayProperty = CastChecked(Property); 1036 | const uint8* SrcRaw = static_cast(sqlite3_column_blob(SqliteStatement, ColumnIndex)); 1037 | int32 SrcCount = sqlite3_column_bytes(SqliteStatement, ColumnIndex); 1038 | 1039 | // Copy values internal is really looking for two void* pointers to TArrays. Therefore, here we should make a TArray of the data from sqlite. 1040 | TArray Src; 1041 | Src.AddUninitialized(SrcCount); 1042 | 1043 | for(int32 i = 0; i < SrcCount; ++i) 1044 | { 1045 | Src[i] = SrcRaw[i]; 1046 | } 1047 | 1048 | void* Dest = ArrayProperty->ContainerPtrToValuePtr(Obj); 1049 | 1050 | ArrayProperty->CopyValuesInternal(Dest, static_cast(&Src), 1); 1051 | 1052 | // We need to resize the array to represent the element size and not the byte size 1053 | FScriptArrayHelper_InContainer ArrayHelper(ArrayProperty, Obj); 1054 | ArrayHelper.Resize( SrcCount / ArrayProperty->Inner->ElementSize ); 1055 | } 1056 | else 1057 | { 1058 | UE_LOG(LogDataAccess, Error, TEXT("BindParameters: Data type on UPROPERTY() %s is not supported"), *(Property->GetName())); 1059 | bSuccess = false; 1060 | } 1061 | ++ColumnIndex; 1062 | } 1063 | 1064 | return bSuccess; 1065 | } 1066 | 1067 | bool SqliteDataHandler::BindStatementToArray(sqlite3_stmt* const SqliteStatement, TSharedPtr< FJsonValue >& JsonValue) 1068 | { 1069 | check(SqliteStatement) 1070 | 1071 | int32 ColumnCount = sqlite3_column_count(SqliteStatement); 1072 | 1073 | TSharedPtr< FJsonObject > JsonObject(new FJsonObject()); 1074 | for (int32 i = 0; i < ColumnCount; ++i) 1075 | { 1076 | int32 ColumnTypeCode = sqlite3_column_type(SqliteStatement, i); 1077 | 1078 | FString ColumnName = UTF8_TO_TCHAR(sqlite3_column_name(SqliteStatement, i)); 1079 | 1080 | // Fundamental Datatypes 1081 | // --------------------- 1082 | // SQLITE_INTEGER 1 - 64 - bit signed integer 1083 | // SQLITE_FLOAT 2 - 64 - bit IEEE floating point number 1084 | // SQLITE3_TEXT 3 - string 1085 | // SQLITE_BLOB 4 - BLOB 1086 | // SQLITE_NULL 5 - NULL 1087 | 1088 | FString ColumnValue; 1089 | TSharedPtr< FJsonValue > FieldJsonValue; 1090 | if (ColumnTypeCode == SQLITE_INTEGER) 1091 | { 1092 | FieldJsonValue = MakeShareable(new FJsonValueString(FString::FromInt(sqlite3_column_int(SqliteStatement, i)))); 1093 | JsonObject->SetField(ColumnName, FieldJsonValue); 1094 | } 1095 | else if (ColumnTypeCode == SQLITE_FLOAT) 1096 | { 1097 | FieldJsonValue = MakeShareable(new FJsonValueNumber(sqlite3_column_double(SqliteStatement, i))); 1098 | JsonObject->SetField(ColumnName, FieldJsonValue); 1099 | } 1100 | else if (ColumnTypeCode == SQLITE3_TEXT) 1101 | { 1102 | FieldJsonValue = MakeShareable(new FJsonValueString(UTF8_TO_TCHAR(sqlite3_column_text(SqliteStatement, i)))); 1103 | JsonObject->SetField(ColumnName, FieldJsonValue); 1104 | } 1105 | else if (ColumnTypeCode == SQLITE_BLOB) 1106 | { 1107 | UE_LOG(LogDataAccess, Error, TEXT("BindStatementToArray: Column '%s' is BLOB and not supported yet"), *ColumnName); 1108 | } 1109 | else if (ColumnTypeCode == SQLITE_NULL) 1110 | { 1111 | FieldJsonValue = MakeShareable(new FJsonValueNull()); 1112 | JsonObject->SetField(ColumnName, FieldJsonValue); 1113 | } 1114 | else 1115 | { 1116 | UE_LOG(LogDataAccess, Error, TEXT("BindStatementToArray: Column '%s' could not be bound"), *ColumnName); 1117 | } 1118 | } 1119 | JsonValue = MakeShareable(new FJsonValueObject(JsonObject)); 1120 | 1121 | return true; 1122 | } -------------------------------------------------------------------------------- /Source/DataAccess/Private/Sqlite/SqliteDataResource.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #include "DataAccessPrivatePCH.h" 3 | #include "SqliteDataResource.h" 4 | 5 | SqliteDataResource::SqliteDataResource(FString DatabaseFileLocation) 6 | : DatabaseFileLocation(DatabaseFileLocation) 7 | {} 8 | 9 | SqliteDataResource::~SqliteDataResource() 10 | { 11 | if(!DatabaseResource) 12 | { 13 | return; 14 | } 15 | 16 | Release(); 17 | } 18 | 19 | bool SqliteDataResource::Acquire() 20 | { 21 | int32 ReturnCode = sqlite3_open(TCHAR_TO_UTF8(*DatabaseFileLocation), &DatabaseResource); 22 | 23 | if(ReturnCode != SQLITE_OK) 24 | { 25 | UE_LOG(LogDataAccess, Error, TEXT("Acquire: Cannot open a database connection with %s with error %s"), *DatabaseFileLocation, UTF8_TO_TCHAR(sqlite3_errmsg(DatabaseResource))); 26 | sqlite3_close(DatabaseResource); 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | bool SqliteDataResource::Release() 34 | { 35 | if(!DatabaseResource) 36 | { 37 | return true; 38 | } 39 | 40 | if(sqlite3_close(DatabaseResource) != SQLITE_OK) 41 | { 42 | UE_LOG(LogDataAccess, Error, TEXT("Release: Cannot close a database connection with %s with error %s"), *DatabaseFileLocation, UTF8_TO_TCHAR(sqlite3_errmsg(DatabaseResource))); 43 | } 44 | DatabaseResource = nullptr; 45 | return true; 46 | } 47 | 48 | sqlite3* SqliteDataResource::Get() const 49 | { 50 | return DatabaseResource; 51 | } 52 | -------------------------------------------------------------------------------- /Source/DataAccess/Private/Tests/SqliteTest.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #include "DataAccessPrivatePCH.h" 3 | #include "AutomationTest.h" 4 | #include "SqliteDataResource.h" 5 | #include "SqliteDataHandler.h" 6 | #include "TestObject.h" 7 | 8 | IMPLEMENT_SIMPLE_AUTOMATION_TEST(FSqliteDataAccessTest, "DataAccess.Sqlite", EAutomationTestFlags::ATF_ApplicationMask) 9 | 10 | bool FSqliteDataAccessTest::RunTest(const FString& Parameters) 11 | { 12 | AddLogItem(TEXT("Creating SqliteDataResource")); 13 | if(!FPlatformFileManager::Get().GetPlatformFile().FileExists(*FString(FPaths::GameDir() + "/Data/Test.db"))) 14 | { 15 | AddError(TEXT("Test database does not exist at \"" + FPaths::GameDir() + "/Data/Test.db\"")); 16 | return false; 17 | } 18 | 19 | TSharedPtr DataResource = MakeShareable(new SqliteDataResource(FString(FPaths::GameDir() + "/Data/Test.db"))); 20 | if(!DataResource->Acquire()) 21 | { 22 | AddError(TEXT("Test database resource could not be acquired")); 23 | return false; 24 | } 25 | AddLogItem(TEXT("Successfully created SqliteDataResource")); 26 | 27 | 28 | AddLogItem(TEXT("Creating SqliteDataHandler")); 29 | TSharedPtr DataHandler = MakeShareable(new SqliteDataHandler(DataResource)); 30 | AddLogItem(TEXT("Successfully created SqliteDataHandler")); 31 | 32 | 33 | AddLogItem(TEXT("Creating test object")); 34 | UTestObject* TestObj = nullptr; 35 | TestObj = NewObject(); 36 | TestObj->TestInt = 42; 37 | TestObj->TestFloat = 42.f; 38 | TestObj->TestBool = true; 39 | TestObj->TestString = "Test String"; 40 | TestObj->TestArray.Add(42); 41 | 42 | if(!DataHandler->Source(UTestObject::StaticClass()).Create(TestObj)) 43 | { 44 | AddError(TEXT("Error creating a new record")); 45 | return false; 46 | } 47 | 48 | if(TestObj->Id == -1) 49 | { 50 | AddError(TEXT("TestObj not correctly set after creating record")); 51 | return false; 52 | } 53 | AddLogItem(TEXT("Successfully created test object")); 54 | 55 | 56 | AddLogItem(TEXT("Reading created test object")); 57 | UTestObject* TestObj2 = nullptr; 58 | TestObj2 = NewObject(); 59 | TestObj2->TestInt = 0; 60 | TestObj2->TestFloat = 0.f; 61 | TestObj2->TestBool = false; 62 | TestObj2->TestString = ""; 63 | TestObj2->TestArray.Empty(); 64 | if(!DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).First(TestObj2)) 65 | { 66 | AddError(TEXT("Error reading created record")); 67 | return false; 68 | } 69 | 70 | if(!(TestObj->TestInt == TestObj2->TestInt && 71 | TestObj->TestFloat == TestObj2->TestFloat && 72 | TestObj->TestBool == TestObj2->TestBool && 73 | TestObj->TestString == TestObj2->TestString && 74 | TestObj->TestArray.Num() == TestObj2->TestArray.Num() && 75 | TestObj->TestArray[0] == TestObj2->TestArray[0])) 76 | { 77 | AddError(TEXT("Created object and read object do not match")); 78 | return false; 79 | } 80 | AddLogItem(TEXT("Read test object matched returned test object")); 81 | AddLogItem(TEXT("Successfully created and read test object")); 82 | 83 | AddLogItem(TEXT("Updating test object")); 84 | TestObj->TestInt = 43; 85 | TestObj->TestFloat = 43.f; 86 | TestObj->TestBool = false; 87 | TestObj->TestString = "Another Test String"; 88 | TestObj->TestArray.Add(43); 89 | if(!DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).Update(TestObj)) 90 | { 91 | AddError(TEXT("Error updating record")); 92 | return false; 93 | } 94 | 95 | TestObj2 = NewObject(); 96 | TestObj2->TestInt = 0; 97 | TestObj2->TestFloat = 0.f; 98 | TestObj2->TestBool = false; 99 | TestObj2->TestString = ""; 100 | TestObj2->TestArray.Empty(); 101 | if(!DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).First(TestObj2)) 102 | { 103 | AddError(TEXT("Error reading updated record")); 104 | return false; 105 | } 106 | 107 | if(!(TestObj->TestInt == TestObj2->TestInt && 108 | TestObj->TestFloat == TestObj2->TestFloat && 109 | TestObj->TestBool == TestObj2->TestBool && 110 | TestObj->TestString == TestObj2->TestString && 111 | TestObj->TestArray.Num() == TestObj2->TestArray.Num() && 112 | TestObj->TestArray[0] == TestObj2->TestArray[0] && 113 | TestObj->TestArray[1] == TestObj2->TestArray[1])) 114 | { 115 | AddError(TEXT("Updated object and read object do not match")); 116 | return false; 117 | } 118 | AddLogItem(TEXT("Updated test object matched return test object")); 119 | AddLogItem(TEXT("Successfully updated and read test object")); 120 | 121 | 122 | AddLogItem(TEXT("Testing manual query")); 123 | TArray< TSharedPtr > JsonArray; 124 | if (!DataHandler->ExecuteQuery(FString::Printf(TEXT("SELECT Id, TestInt, TestFloat, TestBool, TestString FROM TestObject WHERE Id = %s"), *(FString::FromInt(TestObj->Id))), JsonArray)) 125 | { 126 | AddError(TEXT("Error executing manual query")); 127 | return false; 128 | } 129 | 130 | if (JsonArray.Num() != 1) 131 | { 132 | AddError(TEXT("Manual query did not return expected 1 result")); 133 | return false; 134 | } 135 | 136 | int32 JsonTestInt = static_cast(JsonArray[0]->AsObject()->GetNumberField("TestInt")); 137 | float JsonTestFloat = static_cast(JsonArray[0]->AsObject()->GetNumberField("TestFloat")); 138 | bool JsonTestBool = JsonArray[0]->AsObject()->GetBoolField("TestBool"); 139 | FString JsonTestString = JsonArray[0]->AsObject()->GetStringField("TestString"); 140 | 141 | if (!(TestObj->TestInt == JsonTestInt && 142 | TestObj->TestFloat == JsonTestFloat && 143 | TestObj->TestBool == JsonTestBool && 144 | TestObj->TestString == JsonTestString)) 145 | { 146 | AddError(TEXT("Json values do not match TestObj values")); 147 | return false; 148 | } 149 | AddLogItem(TEXT("Successfully executing manual query")); 150 | 151 | 152 | AddLogItem(TEXT("Creating second test object")); 153 | if(!DataHandler->Source(UTestObject::StaticClass()).Create(TestObj2)) 154 | { 155 | AddError(TEXT("Error creating a second record")); 156 | return false; 157 | } 158 | 159 | 160 | AddLogItem(TEXT("Getting count")); 161 | int32 Count; 162 | if(!DataHandler->Source(UTestObject::StaticClass()).Count(Count)) 163 | { 164 | AddError(TEXT("Error getting count")); 165 | return false; 166 | } 167 | 168 | if(Count != 2) 169 | { 170 | AddError(TEXT("Count incorrect")); 171 | } 172 | AddLogItem(TEXT("Successfully got count")); 173 | 174 | 175 | AddLogItem(TEXT("Getting all test object")); 176 | TArray ReturnedObjects; 177 | for(int32 i = 0; i < Count; ++i) 178 | { 179 | ReturnedObjects.Add(NewObject()); 180 | } 181 | if(!DataHandler->Source(UTestObject::StaticClass()).Get(ReturnedObjects)) 182 | { 183 | AddError(TEXT("Error fetching all records")); 184 | return false; 185 | } 186 | 187 | if(ReturnedObjects.Num() != 2) 188 | { 189 | AddError(TEXT("Error getting all test object")); 190 | } 191 | AddLogItem(TEXT("Successfully got all test object")); 192 | 193 | 194 | AddLogItem(TEXT("Deleting test object")); 195 | if(!DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, FString::FromInt(TestObj->Id)).Delete()) 196 | { 197 | AddError(TEXT("Error deleting record")); 198 | return false; 199 | } 200 | AddLogItem(TEXT("Successfully deleted test object")); 201 | 202 | 203 | AddLogItem(TEXT("Testing expected fail cases")); 204 | TestObj = NewObject(); 205 | if(DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, "-1").First(TestObj)) 206 | { 207 | AddError(TEXT("Read success, but object doesn't exist")); 208 | return false; 209 | } 210 | 211 | if(DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, "-1").Update(TestObj)) 212 | { 213 | AddError(TEXT("Update success, but object doesn't exist")); 214 | return false; 215 | } 216 | 217 | if(DataHandler->Source(UTestObject::StaticClass()).Where("Id", EDataHandlerOperator::Equals, "-1").Delete()) 218 | { 219 | AddError(TEXT("Delete success, but object doesn't exist")); 220 | return false; 221 | } 222 | 223 | AddLogItem(TEXT("Deleting all test objects")); 224 | if(!DataHandler->Source(UTestObject::StaticClass()).Delete()) 225 | { 226 | AddError(TEXT("Problems cleaning up")); 227 | return false; 228 | } 229 | AddLogItem(TEXT("Successfully tested fail cases")); 230 | 231 | TestObj->ConditionalBeginDestroy(); 232 | TestObj = nullptr; 233 | TestObj2->ConditionalBeginDestroy(); 234 | TestObj2 = nullptr; 235 | 236 | return true; 237 | } 238 | -------------------------------------------------------------------------------- /Source/DataAccess/Private/Tests/TestObject.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #include "DataAccessPrivatePCH.h" 4 | #include "TestObject.h" 5 | 6 | UTestObject::UTestObject(const FObjectInitializer& ObjectInitializer) 7 | : Super(ObjectInitializer) 8 | { 9 | Id = -1; 10 | } 11 | -------------------------------------------------------------------------------- /Source/DataAccess/Private/Tests/TestObject.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "TestObject.generated.h" 6 | /* Sqlite: 7 | CREATE TABLE TestObject ( Id INTEGER PRIMARY KEY AUTOINCREMENT, TestInt INTEGER, TestFloat REAL, TestBool NUMERIC, TestString TEXT, TestArray BLOB, CreateTimestamp INTEGER, LastUpdateTimestamp INTEGER ); 8 | CREATE TRIGGER TestObject_Insert AFTER INSERT ON TestObject BEGIN UPDATE TestObject SET CreateTimestamp = strftime('%s','now'), LastUpdateTimestamp = strftime('%s','now') WHERE Id = new.Id; END; 9 | CREATE TRIGGER TestObject_Update AFTER UPDATE ON TestObject FOR EACH ROW BEGIN UPDATE TestObject SET LastUpdateTimestamp = strftime('%s','now') WHERE Id = new.Id; END; 10 | */ 11 | 12 | UCLASS() 13 | class UTestObject : public UObject 14 | { 15 | GENERATED_BODY() 16 | 17 | private: 18 | 19 | UTestObject(const FObjectInitializer& ObjectInitializer); 20 | 21 | UPROPERTY(meta = (SaveToDatabase = "true")) 22 | int32 Id; 23 | 24 | UPROPERTY(meta = (SaveToDatabase="true")) 25 | int32 TestInt; 26 | 27 | UPROPERTY(meta = (SaveToDatabase = "true")) 28 | float TestFloat; 29 | 30 | UPROPERTY(meta = (SaveToDatabase = "true")) 31 | bool TestBool; 32 | 33 | UPROPERTY(meta = (SaveToDatabase = "true")) 34 | FString TestString; 35 | 36 | UPROPERTY(meta = (SaveToDatabase = "true")) 37 | TArray TestArray; 38 | 39 | UPROPERTY() 40 | FString TestIgnore; 41 | 42 | UPROPERTY(meta = (SaveToDatabase = "true")) 43 | int32 CreateTimestamp; 44 | 45 | UPROPERTY(meta = (SaveToDatabase = "true")) 46 | int32 LastUpdateTimestamp; 47 | 48 | friend class FSqliteDataAccessTest; 49 | }; -------------------------------------------------------------------------------- /Source/DataAccess/Public/IDataAccess.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "ModuleManager.h" 6 | 7 | #include "SqliteDataResource.h" 8 | #include "SqliteDataHandler.h" 9 | 10 | /** 11 | * The public interface to this module. In most cases, this interface is only public to sibling modules 12 | * within this plugin. 13 | */ 14 | class DATAACCESS_API IDataAccess : public IModuleInterface 15 | { 16 | 17 | public: 18 | 19 | /** 20 | * Singleton-like access to this module's interface. This is just for convenience! 21 | * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. 22 | * 23 | * @return Returns singleton instance, loading the module on demand if needed 24 | */ 25 | static inline IDataAccess& Get() 26 | { 27 | return FModuleManager::LoadModuleChecked< IDataAccess >( "DataAccess" ); 28 | } 29 | 30 | /** 31 | * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. 32 | * 33 | * @return True if the module is loaded and ready to use 34 | */ 35 | static inline bool IsAvailable() 36 | { 37 | return FModuleManager::Get().IsModuleLoaded( "DataAccess" ); 38 | } 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /Source/DataAccess/Public/IDataHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | enum EDataHandlerOperator 6 | { 7 | GreaterThan, 8 | LessThan, 9 | Equals, 10 | LessThanOrEqualTo, 11 | GreaterThanOrEqualTo, 12 | NotEqualTo 13 | }; 14 | 15 | /** 16 | * Interface that facilities saving data. Will utilizie Unreal's reflection to save data 17 | */ 18 | class DATAACCESS_API IDataHandler 19 | { 20 | public: 21 | virtual ~IDataHandler(){} 22 | 23 | virtual IDataHandler& Source(UClass* Source) = 0; 24 | 25 | virtual IDataHandler& Where(FString FieldName, EDataHandlerOperator Operator, FString Condition) = 0; 26 | virtual IDataHandler& Or() = 0; 27 | virtual IDataHandler& And() = 0; 28 | virtual IDataHandler& BeginNested() = 0; 29 | virtual IDataHandler& EndNested() = 0; 30 | 31 | virtual bool Create(UObject* const Obj) = 0; 32 | virtual bool Update(UObject* const Obj) = 0; 33 | virtual bool Delete() = 0; 34 | virtual bool Count(int32& OutCount) = 0; 35 | virtual bool First(UObject* const OutObj) = 0; 36 | virtual bool Get(TArray& OutObjs) = 0; 37 | 38 | virtual bool ExecuteQuery(FString Query, TArray< TSharedPtr >& JsonArray) = 0; 39 | 40 | }; -------------------------------------------------------------------------------- /Source/DataAccess/Public/IDataResource.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | /** 6 | * Interface that facilities create a data resource. 7 | */ 8 | template 9 | class DATAACCESS_API IDataResource 10 | { 11 | public: 12 | virtual ~IDataResource(){} 13 | 14 | /** 15 | * Acquire the data resource 16 | */ 17 | virtual bool Acquire() = 0; 18 | 19 | /** 20 | * Release the data resource 21 | */ 22 | virtual bool Release() = 0; 23 | 24 | /** 25 | * Get the resource 26 | */ 27 | virtual T* Get() const = 0; 28 | }; -------------------------------------------------------------------------------- /Source/DataAccess/Public/Sqlite/SqliteDataHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "IDataHandler.h" 5 | 6 | // forward declaration 7 | class SqliteDataResource; 8 | typedef struct sqlite3_stmt sqlite3_stmt; 9 | 10 | /** 11 | * Implementation of the IDataHandler for Sqlite 12 | */ 13 | class DATAACCESS_API SqliteDataHandler : public IDataHandler 14 | { 15 | public: 16 | /** 17 | * Construct a new sqlite data handler. Assumes a successfully created data resource is passed in 18 | * 19 | * @param DataResource sqlite data resource 20 | */ 21 | SqliteDataHandler(TSharedPtr DataResource); 22 | virtual ~SqliteDataHandler(); 23 | 24 | // IDataHandler interface 25 | virtual IDataHandler& Source(UClass* Source); 26 | 27 | virtual IDataHandler& Where(FString FieldName, EDataHandlerOperator Operator, FString Condition); 28 | virtual IDataHandler& Or(); 29 | virtual IDataHandler& And(); 30 | virtual IDataHandler& BeginNested(); 31 | virtual IDataHandler& EndNested(); 32 | 33 | virtual bool Create(UObject* const Obj); 34 | virtual bool Update(UObject* const Obj); 35 | virtual bool Delete(); 36 | virtual bool Count(int32& OutCount); 37 | virtual bool First(UObject* const OutObj); 38 | virtual bool Get(TArray& OutObjs); 39 | 40 | virtual bool ExecuteQuery(FString Query, TArray< TSharedPtr >& JsonArray); 41 | // End of IDataHandler interface 42 | 43 | private: 44 | TSharedPtr DataResource; 45 | 46 | bool QueryStarted; 47 | UClass* SourceClass; 48 | TArray QueryParts; 49 | TArray> QueryParameters; 50 | 51 | void ClearQuery(); 52 | FString GenerateWhereClause(); 53 | 54 | /** 55 | * 56 | */ 57 | bool BindWhereToStatement(sqlite3_stmt* const SqliteStatement, int32 ParameterIndex = 1); 58 | 59 | /** 60 | * Bind parameters to the passed in sqlite statement 61 | * 62 | * @param Obj object that contains the data we want to bind 63 | * @param SqliteStatement sqlite statement to bind to 64 | * @return true if successful, false otherwise 65 | */ 66 | bool BindObjectToStatement(UObject* const Obj, sqlite3_stmt* const SqliteStatement); 67 | 68 | /** 69 | * Bind result to UObject 70 | * 71 | * @param SqliteStatement statement to bind from 72 | * @param Obj object to bind to 73 | * @return true if successful, false otherwise 74 | */ 75 | bool BindStatementToObject(sqlite3_stmt* const SqliteStatement, UObject* const Obj); 76 | 77 | /** 78 | * Bind result to JsonValue 79 | * 80 | * @param SqliteStatement statement to bind from 81 | * @param JsonValue JsonValue to bind to 82 | * @return true if successful, false otherwise 83 | */ 84 | bool BindStatementToArray(sqlite3_stmt* const SqliteStatement, TSharedPtr& JsonValue); 85 | }; 86 | -------------------------------------------------------------------------------- /Source/DataAccess/Public/Sqlite/SqliteDataResource.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015 afuzzyllama. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "IDataResource.h" 5 | 6 | typedef struct sqlite3 sqlite3; 7 | 8 | /** 9 | * Implementation of IDataResource for Sqlite 10 | */ 11 | class DATAACCESS_API SqliteDataResource : public IDataResource 12 | { 13 | public: 14 | SqliteDataResource(FString DatabaseFileLocation); 15 | virtual ~SqliteDataResource(); 16 | 17 | virtual bool Acquire(); 18 | virtual bool Release(); 19 | virtual sqlite3* Get() const; 20 | 21 | private: 22 | FString DatabaseFileLocation; 23 | sqlite3* DatabaseResource; 24 | }; 25 | --------------------------------------------------------------------------------