gaseous-server/gaseous-lib/Classes/Database/DatabaseMigration.cs
2026-04-11 22:16:21 +10:00

878 lines
No EOL
48 KiB
C#

using System;
using System.Data;
using System.Reflection;
using System.Reflection.Metadata;
using System.Threading.Tasks;
using gaseous_server.Classes.Metadata;
using gaseous_server.Classes.Plugins.MetadataProviders;
using gaseous_server.Models;
namespace gaseous_server.Classes
{
public static class DatabaseMigration
{
public static List<int> BackgroundUpgradeTargetSchemaVersions = new List<int>();
/// <summary>
/// Safely adds or updates a key in a parameter dictionary without throwing on duplicate keys.
/// Use instead of dict.Add() everywhere in migration code to prevent "key already added" errors
/// when the same dbDict instance is reused across steps.
/// </summary>
public static void AddOrSet(Dictionary<string, object> dict, string key, object value)
{
dict[key] = value;
}
public static async Task PreUpgradeScript(int TargetSchemaVersion, Database.databaseType? DatabaseType)
{
// load resources
var assembly = Assembly.GetExecutingAssembly();
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
DataTable data;
Logging.LogKey(Logging.LogType.Information, "process.database", "database.checking_pre_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
switch (DatabaseType)
{
case Database.databaseType.MySql:
switch (TargetSchemaVersion)
{
case 1005:
Logging.LogKey(Logging.LogType.Information, "process.database", "database.running_pre_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
// there was a mistake at dbschema version 1004-1005
// the first preview release of v1.7 reused dbschema version 1004
// if table "Relation_Game_AgeRatings" exists - then we need to apply the gaseous-fix-1005.sql script before applying the standard 1005 script
sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = @dbname AND table_name = @tablename;";
dbDict.Add("dbname", Config.DatabaseConfiguration.DatabaseName);
dbDict.Add("tablename", "Relation_Game_AgeRatings");
data = await db.ExecuteCMDAsync(sql, dbDict);
if (data.Rows.Count == 0)
{
Logging.LogKey(Logging.LogType.Information, "process.database", "database.schema_version_requires_missing_table", null, new[] { TargetSchemaVersion.ToString() });
string resourceName = "gaseous_lib.Support.Database.MySQL.gaseous-fix-1005.sql";
string dbScript = "";
string[] resources = Assembly.GetExecutingAssembly().GetManifestResourceNames();
if (resources.Contains(resourceName))
{
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
using (StreamReader reader = new StreamReader(stream))
{
dbScript = await reader.ReadToEndAsync();
// apply schema!
Logging.LogKey(Logging.LogType.Information, "process.database", "database.applying_schema_version_fix_prior_to", null, new[] { "1005" });
await db.ExecuteCMDAsync(dbScript, dbDict, 180);
}
}
}
break;
case 1027:
Logging.LogKey(Logging.LogType.Information, "process.database", "database.running_pre_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
// create the basic relation tables
// this is a blocking task
await Storage.CreateRelationsTables<IGDB.Models.Game>();
await Storage.CreateRelationsTables<IGDB.Models.Platform>();
// drop source id from all metadata tables if it exists
var tablesToDropSourceId = new List<string>
{
"AgeGroup","AgeRating","AgeRatingContentDescription","AlternativeName","Artwork","Collection","Company","CompanyLogo","Cover","ExternalGame","Franchise","Game","GameMode","GameVideo","Genre","InvolvedCompany","MultiplayerMode","Platform","PlatformLogo","PlatformVersion","PlayerPerspective","ReleaseDate","Screenshot","Theme","GameLocalization","Region"
};
foreach (var table in tablesToDropSourceId)
{
// check if the column exists
sql = $"SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{Config.DatabaseConfiguration.DatabaseName}' AND TABLE_NAME = '{table}' AND COLUMN_NAME = 'SourceId';";
dbDict.Clear();
data = await db.ExecuteCMDAsync(sql, dbDict);
if (data.Rows.Count > 0)
{
// column exists, drop it
sql = $"ALTER TABLE {table} DROP COLUMN SourceId;"; // MySQL does not support IF EXISTS in ALTER TABLE
await db.ExecuteCMDAsync(sql, dbDict);
Logging.LogKey(Logging.LogType.Information, "process.database", "database.dropped_sourceid_column_from_table", null, new[] { table });
}
switch (table)
{
case "ReleaseDate":
// check if month and/or year columns exist
sql = $"SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '{Config.DatabaseConfiguration.DatabaseName}' AND TABLE_NAME = '{table}' AND COLUMN_NAME IN ('Month', 'Year');";
data = await db.ExecuteCMDAsync(sql, dbDict);
foreach (DataRow row in data.Rows)
{
sql = "";
if (row["COLUMN_NAME"].ToString() == "Month")
{
sql += "ALTER TABLE ReleaseDate DROP COLUMN Month, CHANGE `m` `Month` int(11) DEFAULT NULL;";
}
if (row["COLUMN_NAME"].ToString() == "Year")
{
sql += "ALTER TABLE ReleaseDate DROP COLUMN Year, CHANGE `y` `Year` int(11) DEFAULT NULL;";
}
if (!string.IsNullOrEmpty(sql))
{
await db.ExecuteCMDAsync(sql, dbDict);
Logging.LogKey(Logging.LogType.Information, "process.database", "database.dropped_column_from_releasedate_table", null, new[] { row["COLUMN_NAME"].ToString() ?? "" });
}
}
break;
}
}
break;
case 1031:
Logging.LogKey(Logging.LogType.Information, "process.database", "database.running_pre_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
// build tables for metadata storage
TableBuilder_1031.BuildTables_1031();
sql = "RENAME TABLE AgeGroup TO Metadata_AgeGroup; RENAME TABLE ClearLogo TO Metadata_ClearLogo;";
dbDict.Clear();
await db.ExecuteCMDAsync(sql, dbDict);
break;
case 1035:
Logging.LogKey(Logging.LogType.Information, "process.database", "database.running_pre_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
// ensure that the relation tables for games and platforms are built before we attempt to update the database schema
await Storage.CreateRelationsTables<IGDB.Models.Game>();
break;
}
break;
}
}
public static void PostUpgradeScript(int TargetSchemaVersion, Database.databaseType? DatabaseType)
{
var assembly = Assembly.GetExecutingAssembly();
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
DataTable data;
Logging.LogKey(Logging.LogType.Information, "process.database",
"database.running_post_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() });
switch (DatabaseType)
{
case Database.databaseType.MySql:
switch (TargetSchemaVersion)
{
case 1002:
// this is a safe background task
BackgroundUpgradeTargetSchemaVersions.Add(1002);
break;
case 1004:
// needs to run on start up
// copy root path to new libraries format
string oldRoot = Path.Combine(Config.LibraryConfiguration.LibraryRootDirectory, "Library");
sql = "INSERT INTO GameLibraries (Name, Path, DefaultLibrary, DefaultPlatform) VALUES (@name, @path, @defaultlibrary, @defaultplatform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);";
AddOrSet(dbDict, "name", "Default");
AddOrSet(dbDict, "path", oldRoot);
AddOrSet(dbDict, "defaultlibrary", 1);
AddOrSet(dbDict, "defaultplatform", 0);
data = db.ExecuteCMD(sql, dbDict);
// apply the new library id to the existing roms
sql = "UPDATE Games_Roms SET LibraryId=@libraryid;";
dbDict.Clear();
AddOrSet(dbDict, "libraryid", data.Rows[0][0]);
db.ExecuteCMD(sql, dbDict);
break;
case 1016:
// delete old format LastRun_* settings from settings table
sql = "DELETE FROM Settings WHERE Setting LIKE 'LastRun_%';";
db.ExecuteNonQuery(sql);
break;
case 1023:
// load country list
Logging.LogKey(Logging.LogType.Information, "process.database", "database.adding_country_lookup_table_contents");
string countryResourceName = "gaseous_lib.Support.Country.txt";
using (Stream stream = assembly.GetManifestResourceStream(countryResourceName))
using (StreamReader reader = new StreamReader(stream))
{
do
{
string[] line = reader.ReadLine().Split("|");
sql = "INSERT INTO Country (Code, Value) VALUES (@code, @value);";
dbDict = new Dictionary<string, object>{
{ "code", line[0] },
{ "value", line[1] }
};
db.ExecuteNonQuery(sql, dbDict);
} while (reader.EndOfStream == false);
}
// load language list
Logging.LogKey(Logging.LogType.Information, "process.database", "database.adding_language_lookup_table_contents");
string languageResourceName = "gaseous_lib.Support.Language.txt";
using (Stream stream = assembly.GetManifestResourceStream(languageResourceName))
using (StreamReader reader = new StreamReader(stream))
{
do
{
string[] line = reader.ReadLine().Split("|");
sql = "INSERT INTO Language (Code, Value) VALUES (@code, @value);";
dbDict = new Dictionary<string, object>{
{ "code", line[0] },
{ "value", line[1] }
};
db.ExecuteNonQuery(sql, dbDict);
} while (reader.EndOfStream == false);
}
break;
case 1024:
// attempt to re-import signature dats
// delete existing signature sources to allow re-import
Logging.LogKey(Logging.LogType.Information, "process.database", "database.deleting_existing_signature_sources");
sql = "DELETE FROM Signatures_Sources;";
db.ExecuteNonQuery(sql);
_ = MySql_1024_MigrateMetadataVersion();
break;
case 1027:
// create profiles for all existing users
sql = "SELECT * FROM Users;";
data = db.ExecuteCMD(sql);
foreach (DataRow row in data.Rows)
{
// get legacy avatar from UserAvatars table
sql = "SELECT Avatar FROM UserAvatars WHERE UserId = @userid;";
dbDict = new Dictionary<string, object>
{
{ "userid", row["Id"] }
};
DataTable avatarData = db.ExecuteCMD(sql, dbDict);
sql = "INSERT INTO UserProfiles (Id, UserId, DisplayName, Quip, Avatar, AvatarExtension, UnstructuredData) VALUES (@id, @userid, @displayname, @quip, @avatar, @avatarextension, @data);";
dbDict = new Dictionary<string, object>
{
{ "id", Guid.NewGuid() },
{ "userid", row["Id"] },
{ "displayname", row["Email"] },
{ "quip", "" },
{ "avatar", avatarData.Rows.Count > 0 ? avatarData.Rows[0]["Avatar"] : null },
{ "avatarextension", avatarData.Rows.Count > 0 ? ".jpg" : null },
{ "data", "{}" }
};
db.ExecuteNonQuery(sql, dbDict);
}
// update all rom paths to use the new format
sql = "SELECT * FROM GameLibraries;";
data = db.ExecuteCMD(sql);
foreach (DataRow row in data.Rows)
{
sql = "SELECT * FROM Games_Roms WHERE LibraryId = @libraryid;";
dbDict = new Dictionary<string, object>
{
{ "libraryid", row["Id"] }
};
DataTable romData = db.ExecuteCMD(sql, dbDict);
string libraryRootPath = (string)row["Path"];
if (libraryRootPath.EndsWith(Path.DirectorySeparatorChar.ToString()) == false)
{
libraryRootPath += Path.DirectorySeparatorChar;
}
bool GetLastThreeElements = false;
if ((int)row["DefaultLibrary"] == 1)
{
GetLastThreeElements = true;
}
foreach (DataRow romRow in romData.Rows)
{
string existingPath = (string)romRow["RelativePath"];
string newPath = "";
if (GetLastThreeElements == true)
{
// strip all but the last 3 elements from existingPath separated by directory separator
// this mode only works for the default library
string[] pathParts = existingPath.Split(Path.DirectorySeparatorChar);
if (pathParts.Length > 3)
{
newPath = Path.Combine(pathParts[pathParts.Length - 3], pathParts[pathParts.Length - 2], pathParts[pathParts.Length - 1]);
}
else
{
// Path does not have the expected 3-segment structure
// (platform/game/romfile). Using as-is; this may indicate
// a ROM that was placed outside the managed library structure.
if (pathParts.Length != 3)
{
Logging.LogKey(Logging.LogType.Warning, "process.database",
"database.rom_path_unexpected_segment_count",
null, new[] { existingPath, pathParts.Length.ToString() });
}
newPath = existingPath;
}
}
else
{
// strip the library root path from the existing path
if (existingPath.StartsWith(libraryRootPath))
{
newPath = existingPath.Substring(libraryRootPath.Length);
}
else
{
newPath = existingPath;
}
}
Logging.LogKey(Logging.LogType.Information, "process.database", "database.updating_rom_path_from_to", null, new[] { existingPath, newPath });
sql = "UPDATE Games_Roms SET RelativePath = @newpath WHERE Id = @id;";
dbDict = new Dictionary<string, object>
{
{ "newpath", newPath },
{ "id", romRow["Id"] }
};
db.ExecuteNonQuery(sql, dbDict);
}
}
// get all tables that have the prefix "Relation_" and drop them
sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = @dbname AND table_name LIKE 'Relation_%';";
dbDict = new Dictionary<string, object>
{
{ "dbname", Config.DatabaseConfiguration.DatabaseName }
};
data = db.ExecuteCMD(sql, dbDict);
foreach (DataRow row in data.Rows)
{
sql = "DROP TABLE " + (string)row["table_name"] + ";";
db.ExecuteNonQuery(sql);
}
// migrating metadata is a safe background task
BackgroundUpgradeTargetSchemaVersions.Add(1024);
break;
case 1031:
// update Metadata_Platform SourceId to 0
sql = "UPDATE Metadata_Platform SET SourceId = 0;";
db.ExecuteNonQuery(sql);
// update Gmes_Roms to MetadataId = 0
sql = "UPDATE Games_Roms SET GameId = 0;";
db.ExecuteNonQuery(sql);
DatabaseMigration.BackgroundUpgradeTargetSchemaVersions.Add(1031);
break;
case 1038:
MySql_1038_MigrateDateSettings();
break;
}
break;
}
}
public static void MySql_1038_MigrateDateSettings()
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = @"DELETE FROM Settings
WHERE ValueType = 0
AND (
`Setting` IN ('LastContentChange', 'LastLibraryChange', 'LastMetadataChange', 'LastMetadataRefresh')
OR `Setting` LIKE 'LastRun_%'
);";
int deletedCount = db.ExecuteNonQuery(sql);
Logging.LogKey(Logging.LogType.Information, "process.database", "database.migrated_legacy_date_settings_total", null, new[] { deletedCount.ToString() });
}
public static async Task UpgradeScriptBackgroundTasks()
{
Logging.LogKey(Logging.LogType.Information, "process.database", "database.starting_background_upgrade_tasks");
foreach (int TargetSchemaVersion in BackgroundUpgradeTargetSchemaVersions)
{
try
{
switch (TargetSchemaVersion)
{
case 1002:
MySql_1002_MigrateMetadataVersion();
break;
case 1031:
await MySql_1031_MigrateMetadataVersion();
break;
}
}
catch (Exception ex)
{
Logging.LogKey(Logging.LogType.Warning, "process.database", "database.error_during_background_upgrade_for_schema_version", null, new[] { TargetSchemaVersion.ToString() }, ex);
}
}
// perform any metadata table migrations that are needed
await gaseous_server.Classes.Metadata.Utility.MetadataTableBuilder.BuildTableFromType("gaseous", "Metadata", typeof(gaseous_server.Classes.Plugins.MetadataProviders.MetadataTypes.Game), "", "NameThe, AgeGroupId");
await gaseous_server.Classes.Metadata.Utility.MetadataTableBuilder.BuildTableFromType("gaseous", "Metadata", typeof(gaseous_server.Classes.Plugins.MetadataProviders.MetadataTypes.GameLocalization), "", "NameThe");
}
public static void MySql_1002_MigrateMetadataVersion()
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
// update signature roms to v2
sql = "SELECT Id, Flags, Attributes, IngestorVersion FROM Signatures_Roms WHERE IngestorVersion = 1";
DataTable data = db.ExecuteCMD(sql);
if (data.Rows.Count > 0)
{
Logging.LogKey(Logging.LogType.Information, "process.signature_ingest", "database.update_updating_database_entries_total", null, new[] { data.Rows.Count.ToString() });
int Counter = 0;
int LastCounterCheck = 0;
foreach (DataRow row in data.Rows)
{
List<string> Flags = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>((string)Common.ReturnValueIfNull(row["flags"], "[]"));
List<KeyValuePair<string, object>> Attributes = new List<KeyValuePair<string, object>>();
foreach (string Flag in Flags)
{
if (Flag.StartsWith("a"))
{
Attributes.Add(
new KeyValuePair<string, object>(
"a",
Flag
)
);
}
else
{
string[] FlagCompare = Flag.Split(' ');
switch (FlagCompare[0].Trim().ToLower())
{
case "cr":
// cracked
case "f":
// fixed
case "h":
// hacked
case "m":
// modified
case "p":
// pirated
case "t":
// trained
case "tr":
// translated
case "o":
// overdump
case "u":
// underdump
case "v":
// virus
case "b":
// bad dump
case "a":
// alternate
case "!":
// known verified dump
// -------------------
string shavedToken = Flag.Substring(FlagCompare[0].Trim().Length).Trim();
Attributes.Add(new KeyValuePair<string, object>(
FlagCompare[0].Trim().ToLower(),
shavedToken
));
break;
}
}
}
string AttributesJson;
if (Attributes.Count > 0)
{
AttributesJson = Newtonsoft.Json.JsonConvert.SerializeObject(Attributes);
}
else
{
AttributesJson = "[]";
}
string updateSQL = "UPDATE Signatures_Roms SET Attributes=@attributes, IngestorVersion=2 WHERE Id=@id";
dbDict = new Dictionary<string, object>();
AddOrSet(dbDict, "attributes", AttributesJson);
AddOrSet(dbDict, "id", (int)row["Id"]);
db.ExecuteCMD(updateSQL, dbDict);
Counter += 1;
if ((Counter - LastCounterCheck) >= 100 || Counter == data.Rows.Count)
{
LastCounterCheck = Counter;
Logging.LogKey(Logging.LogType.Information, "process.signature_ingest",
"database.update_updating_database_entries_progress",
null, new[] { Counter.ToString(), data.Rows.Count.ToString() });
}
}
}
}
public static async Task MySql_1024_MigrateMetadataVersion()
{
FileSignature fileSignature = new FileSignature();
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
// Check if the view exists before proceeding
string sql = "SELECT table_name FROM information_schema.views WHERE table_schema = @dbname AND table_name = 'view_Games_Roms';";
Dictionary<string, object> dbDict = new Dictionary<string, object>
{
{ "dbname", Config.DatabaseConfiguration.DatabaseName }
};
DataTable viewCheck = await db.ExecuteCMDAsync(sql, dbDict);
if (viewCheck.Rows.Count == 0)
{
// View doesn't exist, skip migration
Logging.LogKey(Logging.LogType.Information, "process.database", "database.view_does_not_exist_skipping_migration", null, new[] { "view_Games_Roms" });
return;
}
sql = "SELECT * FROM view_Games_Roms WHERE RomDataVersion = 1;";
DataTable data = await db.ExecuteCMDAsync(sql);
long count = 1;
foreach (DataRow row in data.Rows)
{
Logging.LogKey(Logging.LogType.Information, "process.database", "database.migration_updating_rom_table_for_rom", null, new[] { count.ToString(), data.Rows.Count.ToString(), (string)row["Name"] });
GameLibrary.LibraryItem library = await GameLibrary.GetLibrary((int)row["LibraryId"]);
HashObject hash = new HashObject()
{
md5hash = (string)row["MD5"],
sha1hash = (string)row["SHA1"]
};
FileSignature.FileHash fileHash = new FileSignature.FileHash()
{
Library = library,
Hash = hash,
FileName = (string)row["RelativePath"]
};
var (_, signature) = await fileSignature.GetFileSignatureAsync(
library,
fileHash
);
gaseous_server.Classes.Plugins.MetadataProviders.MetadataTypes.Platform platform = await Platforms.GetPlatform((long)row["PlatformId"]);
await ImportGame.StoreGame(library, hash, signature, platform, (string)row["Path"], (long)row["Id"]);
count += 1;
}
}
public static async Task MySql_1031_MigrateMetadataVersion()
{
// get the database migration task
foreach (ProcessQueue.QueueProcessor.QueueItem qi in ProcessQueue.QueueProcessor.QueueItems)
{
if (qi.ItemType == ProcessQueue.QueueItemType.BackgroundDatabaseUpgrade)
{
await qi.AddSubTask(ProcessQueue.QueueItemSubTasks.MetadataRefresh_Platform, "Platform Metadata", null, false);
await qi.AddSubTask(ProcessQueue.QueueItemSubTasks.MetadataRefresh_Signatures, "Signature Metadata", null, false);
await qi.AddSubTask(ProcessQueue.QueueItemSubTasks.MetadataRefresh_Game, "Game Metadata", null, false);
await qi.AddSubTask(ProcessQueue.QueueItemSubTasks.DatabaseMigration_1031, "Database Migration 1031", null, false);
}
}
}
public static async Task RunMigration1031()
{
// migrate favourites
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT * FROM Users;";
DataTable data = db.ExecuteCMD(sql);
foreach (DataRow row in data.Rows)
{
// get the user's favourites
sql = "SELECT * FROM Favourites WHERE UserId = @userid;";
Dictionary<string, object> dbDict = new Dictionary<string, object>
{
{ "userid", row["Id"] }
};
DataTable favouritesData = db.ExecuteCMD(sql, dbDict);
// copy the users favourites into an array of long
List<long> favourites = new List<long>();
foreach (DataRow favouriteRow in favouritesData.Rows)
{
favourites.Add((long)favouriteRow["GameId"]);
}
// delete the existing favourites
sql = "DELETE FROM Favourites WHERE UserId = @userid;";
dbDict.Clear();
dbDict.Add("userid", row["Id"]);
db.ExecuteNonQuery(sql, dbDict);
// lookup the metadata objects using the GameId, and add the metadataid as a new favourite
foreach (long gameId in favourites)
{
sql = "SELECT DISTINCT ParentMapId FROM MetadataMapBridge WHERE MetadataSourceType = 1 AND MetadataSourceId = @gameid;";
dbDict.Clear();
dbDict.Add("gameid", gameId);
DataTable metadataData = db.ExecuteCMD(sql, dbDict);
if (metadataData.Rows.Count > 0)
{
Favourites metadataFavourites = new Favourites();
metadataFavourites.SetFavourite((string)row["Id"], (long)metadataData.Rows[0]["ParentMapId"], true);
}
}
}
// migrate media groups
sql = "SELECT DISTINCT RomMediaGroup.Id, Games_Roms.MetadataMapId FROM RomMediaGroup_Members JOIN RomMediaGroup ON RomMediaGroup_Members.GroupId = RomMediaGroup.Id JOIN Games_Roms ON RomMediaGroup_Members.RomId = Games_Roms.Id;";
data = db.ExecuteCMD(sql);
foreach (DataRow row in data.Rows)
{
// set the media group for each media group
sql = "UPDATE RomMediaGroup SET GameId = @gameid WHERE Id = @id;";
Dictionary<string, object> dbDict = new Dictionary<string, object>
{
{ "gameid", row["MetadataMapId"] },
{ "id", row["Id"] }
};
db.ExecuteNonQuery(sql, dbDict);
}
}
public static class TableBuilder_1031
{
public static void BuildTables_1031()
{
BuildTableFromType(typeof(IGDB.Models.AgeRating));
BuildTableFromType(typeof(IGDB.Models.AgeRatingCategory));
BuildTableFromType(typeof(IGDB.Models.AgeRatingContentDescriptionV2));
BuildTableFromType(typeof(IGDB.Models.AgeRatingOrganization));
BuildTableFromType(typeof(IGDB.Models.AlternativeName));
BuildTableFromType(typeof(IGDB.Models.Artwork));
BuildTableFromType(typeof(IGDB.Models.Character));
BuildTableFromType(typeof(IGDB.Models.CharacterGender));
BuildTableFromType(typeof(IGDB.Models.CharacterMugShot));
BuildTableFromType(typeof(IGDB.Models.CharacterSpecies));
BuildTableFromType(typeof(IGDB.Models.Collection));
BuildTableFromType(typeof(IGDB.Models.CollectionMembership));
BuildTableFromType(typeof(IGDB.Models.CollectionMembershipType));
BuildTableFromType(typeof(IGDB.Models.CollectionRelation));
BuildTableFromType(typeof(IGDB.Models.CollectionRelationType));
BuildTableFromType(typeof(IGDB.Models.CollectionType));
BuildTableFromType(typeof(IGDB.Models.Company));
BuildTableFromType(typeof(IGDB.Models.CompanyLogo));
BuildTableFromType(typeof(IGDB.Models.CompanyStatus));
BuildTableFromType(typeof(IGDB.Models.CompanyWebsite));
BuildTableFromType(typeof(IGDB.Models.Cover));
BuildTableFromType(typeof(IGDB.Models.Event));
BuildTableFromType(typeof(IGDB.Models.EventLogo));
BuildTableFromType(typeof(IGDB.Models.EventNetwork));
BuildTableFromType(typeof(IGDB.Models.ExternalGame));
BuildTableFromType(typeof(IGDB.Models.ExternalGameSource));
BuildTableFromType(typeof(IGDB.Models.Franchise));
BuildTableFromType(typeof(IGDB.Models.Game));
BuildTableFromType(typeof(IGDB.Models.GameEngine));
BuildTableFromType(typeof(IGDB.Models.GameEngineLogo));
BuildTableFromType(typeof(IGDB.Models.GameLocalization));
BuildTableFromType(typeof(IGDB.Models.GameMode));
BuildTableFromType(typeof(IGDB.Models.GameReleaseFormat));
BuildTableFromType(typeof(IGDB.Models.GameStatus));
BuildTableFromType(typeof(IGDB.Models.GameTimeToBeat));
BuildTableFromType(typeof(IGDB.Models.GameType));
BuildTableFromType(typeof(IGDB.Models.GameVersion));
BuildTableFromType(typeof(IGDB.Models.GameVersionFeature));
BuildTableFromType(typeof(IGDB.Models.GameVersionFeatureValue));
BuildTableFromType(typeof(IGDB.Models.GameVideo));
BuildTableFromType(typeof(IGDB.Models.Genre));
BuildTableFromType(typeof(IGDB.Models.InvolvedCompany));
BuildTableFromType(typeof(IGDB.Models.Keyword));
BuildTableFromType(typeof(IGDB.Models.Language));
BuildTableFromType(typeof(IGDB.Models.LanguageSupport));
BuildTableFromType(typeof(IGDB.Models.LanguageSupportType));
BuildTableFromType(typeof(IGDB.Models.MultiplayerMode));
BuildTableFromType(typeof(IGDB.Models.NetworkType));
BuildTableFromType(typeof(IGDB.Models.Platform));
BuildTableFromType(typeof(IGDB.Models.PlatformFamily));
BuildTableFromType(typeof(IGDB.Models.PlatformLogo));
BuildTableFromType(typeof(IGDB.Models.PlatformVersion));
BuildTableFromType(typeof(IGDB.Models.PlatformVersionCompany));
BuildTableFromType(typeof(IGDB.Models.PlatformVersionReleaseDate));
BuildTableFromType(typeof(IGDB.Models.PlatformWebsite));
BuildTableFromType(typeof(IGDB.Models.PlayerPerspective));
BuildTableFromType(typeof(IGDB.Models.PopularityPrimitive));
BuildTableFromType(typeof(IGDB.Models.PopularityType));
BuildTableFromType(typeof(IGDB.Models.Region));
BuildTableFromType(typeof(IGDB.Models.ReleaseDate));
BuildTableFromType(typeof(IGDB.Models.ReleaseDateRegion));
BuildTableFromType(typeof(IGDB.Models.ReleaseDateStatus));
BuildTableFromType(typeof(IGDB.Models.Screenshot));
BuildTableFromType(typeof(IGDB.Models.Theme));
BuildTableFromType(typeof(IGDB.Models.Website));
BuildTableFromType(typeof(IGDB.Models.WebsiteType));
}
/// <summary>
/// Builds a table from a type definition, or modifies an existing table.
/// This is used to create or update tables in the database based on the properties of a class.
/// Updates are limited to adding new columns, as the table structure should not change once created.
/// If the table already exists, it will only add new columns that are not already present.
/// This is useful for maintaining a consistent schema across different versions of the application.
/// The method is generic and can be used with any type that has properties that can be mapped to database columns.
/// The method does not return any value, but it will throw an exception if there is an error during the table creation or modification process.
/// </summary>
/// <param name="type">The type definition of the class for which the table should be built.</param>
public static void BuildTableFromType(Type type)
{
// Get the table name from the class name
string tableName = type.Name;
// Start building the SQL command
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
// Use migration journal to track whether the rename step for this table has run.
// This replaces the old Config.ReadSetting("RenameMigration_{tableName}") approach,
// making the tracking schema-based rather than settings-based.
string renameStepName = $"RenameToMetadata_{tableName}";
if (!MigrationJournal.AlreadySucceeded(1031, MigrationJournal.StepType.PreUpgrade, renameStepName))
{
// rename the table if it exists
// Check if the table exists via information_schema (portable, no IF EXISTS needed)
string checkTableExistsQuery = $"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '{tableName}'";
var result = db.ExecuteCMD(checkTableExistsQuery);
if (Convert.ToInt32(result.Rows[0][0]) > 0)
{
// The table exists, so we will rename it
Logging.LogKey(Logging.LogType.Information, "process.database",
"database.renaming_table_to_metadata_prefix",
null, new[] { tableName, $"Metadata_{tableName}" });
string renameTableQuery = $"ALTER TABLE `{tableName}` RENAME TO `Metadata_{tableName}`";
db.ExecuteNonQuery(renameTableQuery);
}
// Record success in the journal so this step is skipped on any subsequent run
long jId = MigrationJournal.Start(1031, MigrationJournal.StepType.PreUpgrade, renameStepName);
MigrationJournal.Complete(jId);
}
// Update the table name to include the Metadata prefix
tableName = $"Metadata_{tableName}";
// Get the properties of the class
PropertyInfo[] properties = type.GetProperties();
// Create the table with the basic structure if it does not exist
string createTableQuery = $"CREATE TABLE IF NOT EXISTS `{tableName}` (`Id` BIGINT PRIMARY KEY, `dateAdded` DATETIME DEFAULT CURRENT_TIMESTAMP, `lastUpdated` DATETIME DEFAULT CURRENT_TIMESTAMP )";
db.ExecuteNonQuery(createTableQuery);
// Add the sourceId column if it does not exist
string addSourceIdQuery = $"ALTER TABLE `{tableName}` ADD COLUMN IF NOT EXISTS `SourceId` INT";
db.ExecuteNonQuery(addSourceIdQuery);
// Loop through each property to add it as a column in the table
foreach (PropertyInfo property in properties)
{
// Get the property name and type
string columnName = property.Name;
string columnType = "VARCHAR(255)"; // Default type, can be changed based on property type
// Convert the property type name to a string
string propertyTypeName = property.PropertyType.Name;
if (propertyTypeName == "Nullable`1")
{
// If the property is nullable, get the underlying type
propertyTypeName = property.PropertyType.GetGenericArguments()[0].Name;
}
// Determine the SQL type based on the property type
switch (propertyTypeName)
{
case "String":
columnType = "VARCHAR(255)";
break;
case "Int32":
columnType = "INT";
break;
case "Int64":
columnType = "BIGINT";
break;
case "Boolean":
columnType = "BOOLEAN";
break;
case "DateTime":
case "DateTimeOffset":
columnType = "DATETIME";
break;
case "Double":
columnType = "DOUBLE";
break;
case "IdentityOrValue`1":
columnType = "BIGINT";
break;
case "IdentitiesOrValues`1":
columnType = "LONGTEXT";
break;
}
// check if there is a column with the name of the property
string checkColumnQuery = $"SHOW COLUMNS FROM `{tableName}` LIKE '{columnName}'";
var result = db.ExecuteCMD(checkColumnQuery);
if (result.Rows.Count > 0)
{
// Column already exists, check if the type matches
string existingType = result.Rows[0]["Type"].ToString();
if (existingType.ToLower().Split("(")[0] != columnType.ToLower().Split("(")[0] && existingType != "text" && existingType != "longtext")
{
// Type mismatch: modify the column to expected type
Logging.LogKey(Logging.LogType.Information, "process.database",
"database.modifying_column_type",
null, new[] { columnName, tableName, existingType, columnType });
string alterColumnQuery = $"ALTER TABLE `{tableName}` MODIFY COLUMN `{columnName}` {columnType}";
try
{
db.ExecuteNonQuery(alterColumnQuery);
}
catch (Exception ex)
{
Logging.LogKey(Logging.LogType.Warning, "process.database",
"database.modify_column_type_failed",
null, new[] { columnName, tableName, ex.Message }, ex);
}
continue;
}
continue; // Skip this column as it already exists with the correct type
}
// Add the column to the table
Logging.LogKey(Logging.LogType.Information, "process.database",
"database.adding_column_to_table",
null, new[] { columnName, columnType, tableName });
string addColumnQuery = $"ALTER TABLE `{tableName}` ADD COLUMN IF NOT EXISTS `{columnName}` {columnType}";
db.ExecuteNonQuery(addColumnQuery);
}
}
}
}
}