Implement SQLite database file initialization and migration
This commit is contained in:
parent
92c2670b0b
commit
f0b5b080ba
8 changed files with 296 additions and 11 deletions
|
|
@ -11,6 +11,14 @@ namespace MHServerEmu.Core.Helpers
|
|||
public static readonly string ServerRoot = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
public static readonly string DataDirectory = Path.Combine(ServerRoot, "Data");
|
||||
|
||||
/// <summary>
|
||||
/// Returns a path relative to server root directory.
|
||||
/// </summary>
|
||||
public static string GetRelativePath(string filePath)
|
||||
{
|
||||
return Path.GetRelativePath(ServerRoot, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a <typeparamref name="T"/> from a JSON file located at the specified path.
|
||||
/// </summary>
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,72 @@
|
|||
-- Initialize a new database file using the current schema version
|
||||
|
||||
PRAGMA user_version = 1;
|
||||
|
||||
CREATE TABLE "Account" (
|
||||
"Id" INTEGER NOT NULL UNIQUE,
|
||||
"Email" TEXT NOT NULL UNIQUE,
|
||||
"PlayerName" TEXT NOT NULL UNIQUE,
|
||||
"PasswordHash" BLOB NOT NULL,
|
||||
"Salt" BLOB NOT NULL,
|
||||
"UserLevel" INTEGER NOT NULL,
|
||||
"IsBanned" INTEGER NOT NULL,
|
||||
"IsArchived" INTEGER NOT NULL,
|
||||
"IsPasswordExpired" INTEGER NOT NULL,
|
||||
PRIMARY KEY("Id")
|
||||
);
|
||||
|
||||
CREATE TABLE "Player" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ArchiveData" BLOB,
|
||||
"StartTarget" INTEGER,
|
||||
"StartTargetRegionOverride" INTEGER,
|
||||
"AOIVolume" INTEGER,
|
||||
FOREIGN KEY("DbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "Avatar" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "TeamUp" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "Item" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Avatar"("DbGuid") ON DELETE CASCADE,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "TeamUp"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "ControlledEntity" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Avatar"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
60
src/MHServerEmu.DatabaseAccess/Data/SQLite/Migrations/0.sql
Normal file
60
src/MHServerEmu.DatabaseAccess/Data/SQLite/Migrations/0.sql
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
-- Delete old placeholder tables while keeping existing account records
|
||||
DROP TABLE Avatar;
|
||||
DROP TABLE Player;
|
||||
|
||||
-- Initialize new tables for storing persistent entities
|
||||
CREATE TABLE "Player" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ArchiveData" BLOB,
|
||||
"StartTarget" INTEGER,
|
||||
"StartTargetRegionOverride" INTEGER,
|
||||
"AOIVolume" INTEGER,
|
||||
FOREIGN KEY("DbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "Avatar" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "TeamUp" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "Item" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Player"("DbGuid") ON DELETE CASCADE,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Avatar"("DbGuid") ON DELETE CASCADE,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "TeamUp"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
||||
CREATE TABLE "ControlledEntity" (
|
||||
"DbGuid" INTEGER NOT NULL UNIQUE,
|
||||
"ContainerDbGuid" INTEGER,
|
||||
"InventoryProtoGuid" INTEGER,
|
||||
"Slot" INTEGER,
|
||||
"EntityProtoGuid" INTEGER,
|
||||
"ArchiveData" BLOB,
|
||||
FOREIGN KEY("ContainerDbGuid") REFERENCES "Avatar"("DbGuid") ON DELETE CASCADE,
|
||||
PRIMARY KEY("DbGuid")
|
||||
);
|
||||
|
|
@ -35,7 +35,7 @@ namespace MHServerEmu.DatabaseAccess
|
|||
public bool UpdateAccount(DBAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the Player and Avatar tables in the database with the data from the provided <see cref="DBAccount"/>.
|
||||
/// Updates persistent game data for the provided <see cref="DBAccount"/>.
|
||||
/// </summary>
|
||||
public bool UpdateAccountData(DBAccount account);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Data\Account.db">
|
||||
<None Update="Data\SQLite\InitializeDatabase.sql">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Data\SQLite\Migrations\0.sql">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="SQLite.Interop.dll">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
{
|
||||
public class SQLiteDBManager : IDBManager
|
||||
{
|
||||
private const int CurrentSchemaVersion = 1;
|
||||
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
private string _connectionString;
|
||||
|
|
@ -19,10 +21,20 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
public bool Initialize()
|
||||
{
|
||||
string dbPath = Path.Combine(FileHelper.DataDirectory, "Account.db");
|
||||
if (File.Exists(dbPath) == false) return Logger.FatalReturn(false, $"Initialize(): {dbPath} not found");
|
||||
|
||||
_connectionString = $"Data Source={dbPath}";
|
||||
Logger.Info("Established database connection");
|
||||
|
||||
if (File.Exists(dbPath) == false)
|
||||
{
|
||||
if (InitializeDatabaseFile(dbPath) == false)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MigrateDatabaseFileToCurrentSchema(dbPath) == false)
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Info($"Initialized database");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +56,8 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
|
||||
public bool QueryIsPlayerNameTaken(string playerName)
|
||||
{
|
||||
using SQLiteConnection connection = new(_connectionString);
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
// This check is case insensitive (COLLATE NOCASE)
|
||||
var results = connection.Query<string>("SELECT PlayerName FROM Account WHERE PlayerName = @PlayerName COLLATE NOCASE", new { PlayerName = playerName });
|
||||
return results.Any();
|
||||
|
|
@ -52,8 +65,7 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
|
||||
public bool InsertAccount(DBAccount account)
|
||||
{
|
||||
using SQLiteConnection connection = new(_connectionString);
|
||||
connection.Open();
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -70,7 +82,7 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
|
||||
public bool UpdateAccount(DBAccount account)
|
||||
{
|
||||
using SQLiteConnection connection = new(_connectionString);
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -87,8 +99,7 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
|
||||
public bool UpdateAccountData(DBAccount account)
|
||||
{
|
||||
using SQLiteConnection connection = new(_connectionString);
|
||||
connection.Open();
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
// Use a transaction to make sure all data is saved
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
|
|
@ -143,6 +154,103 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and opens a new <see cref="SQLiteConnection"/>.
|
||||
/// </summary>
|
||||
private SQLiteConnection GetConnection()
|
||||
{
|
||||
SQLiteConnection connection = new(_connectionString);
|
||||
connection.Open();
|
||||
return connection;
|
||||
}
|
||||
|
||||
private bool InitializeDatabaseFile(string dbPath)
|
||||
{
|
||||
// Create a new database file if it does not exist
|
||||
string initializationScript = SQLiteScripts.GetInitializationScript();
|
||||
if (initializationScript == string.Empty)
|
||||
return Logger.ErrorReturn(false, "InitializeDatabaseFile(): Failed to get database initialization script");
|
||||
|
||||
SQLiteConnection.CreateFile(dbPath);
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
connection.Execute(initializationScript);
|
||||
|
||||
Logger.Info($"Initialized a new database file at {Path.GetRelativePath(FileHelper.ServerRoot, dbPath)} using schema version {CurrentSchemaVersion}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MigrateDatabaseFileToCurrentSchema(string dbPath)
|
||||
{
|
||||
// Migrate existing database if needed
|
||||
int schemaVersion = GetSchemaVersion();
|
||||
if (schemaVersion > CurrentSchemaVersion)
|
||||
return Logger.ErrorReturn(false, $"Initialize(): Existing database file uses unsupported schema version {schemaVersion} (current = {CurrentSchemaVersion})");
|
||||
|
||||
Logger.Info($"Found existing database file with schema version {schemaVersion} (current = {CurrentSchemaVersion})");
|
||||
|
||||
if (schemaVersion == CurrentSchemaVersion)
|
||||
return true;
|
||||
|
||||
// Create a back to fall back to if something goes wrong
|
||||
string backupDbPath = $"{dbPath}.backup";
|
||||
File.Copy(dbPath, backupDbPath);
|
||||
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
bool success = true;
|
||||
|
||||
while (schemaVersion < CurrentSchemaVersion)
|
||||
{
|
||||
Logger.Info($"Migrating version {schemaVersion} => {schemaVersion + 1}...");
|
||||
|
||||
string migrationScript = SQLiteScripts.GetMigrationScript(schemaVersion);
|
||||
if (migrationScript == string.Empty)
|
||||
{
|
||||
Logger.Error($"MigrateDatabaseFileToCurrentSchema(): Failed to get database migration script for version {schemaVersion}");
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
connection.Execute(migrationScript);
|
||||
connection.Execute($"PRAGMA user_version = {++schemaVersion}");
|
||||
}
|
||||
|
||||
success &= GetSchemaVersion() == CurrentSchemaVersion;
|
||||
|
||||
if (success == false)
|
||||
{
|
||||
// Restore backup
|
||||
File.Delete(dbPath);
|
||||
File.Move(backupDbPath, dbPath);
|
||||
return Logger.ErrorReturn(false, "MigrateDatabaseFileToCurrentSchema(): Migration failed, backup restored");
|
||||
}
|
||||
|
||||
Logger.Info($"Successfully migrated to schema version {CurrentSchemaVersion}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user_version value of the current database.
|
||||
/// </summary>
|
||||
private int GetSchemaVersion()
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
|
||||
var queryResult = connection.Query<int>("PRAGMA user_version");
|
||||
if (queryResult.Any())
|
||||
return queryResult.First();
|
||||
|
||||
return Logger.WarnReturn(-1, "GetSchemaVersion(): Failed to query user_version from the DB");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user_version value of the current database.
|
||||
/// </summary>
|
||||
private void SetSchemaVersion(int version)
|
||||
{
|
||||
using SQLiteConnection connection = GetConnection();
|
||||
connection.Execute($"PRAGMA user_version = {version}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads account data for the specified <see cref="DBAccount"/> and maps relations.
|
||||
/// </summary>
|
||||
|
|
@ -177,12 +285,18 @@ namespace MHServerEmu.DatabaseAccess.SQLite
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads <see cref="DBEntity"/> instances belonging to the specified container from the specified table.
|
||||
/// </summary>
|
||||
private static IEnumerable<DBEntity> LoadEntitiesFromTable(SQLiteConnection connection, string tableName, long containerDbGuid)
|
||||
{
|
||||
var @params = new { ContainerDbGuid = containerDbGuid };
|
||||
return connection.Query<DBEntity>($"SELECT * FROM {tableName} WHERE ContainerDbGuid = @ContainerDbGuid", @params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="DBEntity"/> instances belonging to the specified container in the specified table using the provided <see cref="DBEntityCollection"/>.
|
||||
/// </summary>
|
||||
private static void UpdateEntityTable(SQLiteConnection connection, SQLiteTransaction transaction, string tableName,
|
||||
long containerDbGuid, DBEntityCollection dbEntityCollection)
|
||||
{
|
||||
|
|
|
|||
28
src/MHServerEmu.DatabaseAccess/SQLite/SQLiteScripts.cs
Normal file
28
src/MHServerEmu.DatabaseAccess/SQLite/SQLiteScripts.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using MHServerEmu.Core.Helpers;
|
||||
using MHServerEmu.Core.Logging;
|
||||
|
||||
namespace MHServerEmu.DatabaseAccess.SQLite
|
||||
{
|
||||
public static class SQLiteScripts
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.CreateLogger();
|
||||
|
||||
public static string GetInitializationScript()
|
||||
{
|
||||
string filePath = Path.Combine(FileHelper.DataDirectory, "SQLite", "InitializeDatabase.sql");
|
||||
if (File.Exists(filePath) == false)
|
||||
return Logger.WarnReturn(string.Empty, $"GetDatabaseInitializationScript(): Initialization script file not found at {FileHelper.GetRelativePath(filePath)}");
|
||||
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
public static string GetMigrationScript(int currentVersion)
|
||||
{
|
||||
string filePath = Path.Combine(FileHelper.DataDirectory, "SQLite", "Migrations", $"{currentVersion}.sql");
|
||||
if (File.Exists(filePath) == false)
|
||||
return Logger.WarnReturn(string.Empty, $"GetMigrationScript(): Migration script for version {currentVersion} not found at {FileHelper.GetRelativePath(filePath)}");
|
||||
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue