mirror of
https://github.com/gaseous-project/gaseous-server
synced 2026-04-21 21:37:17 +00:00
947 lines
31 KiB
C#
947 lines
31 KiB
C#
|
|
using System.Data;
|
|
using System.Data.SqlClient;
|
|
using System.Diagnostics;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using MySqlConnector;
|
|
|
|
namespace gaseous_server.Classes
|
|
{
|
|
/// <summary>
|
|
/// Provides methods for interacting with the database, including schema management, command execution, and transactions.
|
|
/// </summary>
|
|
public class Database
|
|
{
|
|
private static int _schema_version { get; set; } = 0;
|
|
/// <summary>
|
|
/// Gets or sets the current schema version of the database.
|
|
/// </summary>
|
|
public static int schema_version
|
|
{
|
|
get
|
|
{
|
|
//Logging.Log(Logging.LogType.Information, "Database Schema", "Schema version is " + _schema_version);
|
|
return _schema_version;
|
|
}
|
|
set
|
|
{
|
|
//Logging.Log(Logging.LogType.Information, "Database Schema", "Setting schema version " + _schema_version);
|
|
_schema_version = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Database"/> class.
|
|
/// </summary>
|
|
public Database()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Database"/> class with the specified database type and connection string.
|
|
/// </summary>
|
|
/// <param name="Type">The type of database connector to use.</param>
|
|
/// <param name="ConnectionString">The connection string to use for the database connection.</param>
|
|
public Database(databaseType Type, string ConnectionString)
|
|
{
|
|
_ConnectorType = Type;
|
|
_ConnectionString = ConnectionString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies the type of database connector being used.
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Represents a MySQL database type.
|
|
/// </summary>
|
|
public enum databaseType
|
|
{
|
|
/// <summary>
|
|
/// MySQL database.
|
|
/// </summary>
|
|
MySql
|
|
}
|
|
|
|
string _ConnectionString = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the connection string used to connect to the database.
|
|
/// </summary>
|
|
public string ConnectionString
|
|
{
|
|
get
|
|
{
|
|
return _ConnectionString;
|
|
}
|
|
set
|
|
{
|
|
_ConnectionString = value;
|
|
}
|
|
}
|
|
|
|
databaseType? _ConnectorType = null;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the type of database connector being used.
|
|
/// </summary>
|
|
public databaseType? ConnectorType
|
|
{
|
|
get
|
|
{
|
|
return _ConnectorType;
|
|
}
|
|
set
|
|
{
|
|
_ConnectorType = value;
|
|
}
|
|
}
|
|
|
|
private static MemoryCache DatabaseMemoryCache = new MemoryCache();
|
|
|
|
/// <summary>
|
|
/// Initializes the database, creates schema version table if missing, and applies schema upgrades.
|
|
/// Takes a backup before the first migration step, retries timed-out steps with a larger timeout,
|
|
/// and logs restore instructions on failure before terminating.
|
|
/// </summary>
|
|
public async Task InitDB()
|
|
{
|
|
// load resources
|
|
var assembly = Assembly.GetExecutingAssembly();
|
|
|
|
DatabaseMemoryCacheOptions? CacheOptions = new DatabaseMemoryCacheOptions(false);
|
|
|
|
Config.DatabaseConfiguration.UpgradeInProgress = true;
|
|
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
// check if the database exists first - first run must have permissions to create a database
|
|
string sql = "CREATE DATABASE IF NOT EXISTS `" + Config.DatabaseConfiguration.DatabaseName + "`;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
Logging.LogKey(Logging.LogType.Information, "process.database", "database.creating_database_if_not_exists");
|
|
ExecuteCMD(sql, dbDict, CacheOptions, 30, "server=" + Config.DatabaseConfiguration.HostName + ";port=" + Config.DatabaseConfiguration.Port + ";userid=" + Config.DatabaseConfiguration.UserName + ";password=" + Config.DatabaseConfiguration.Password);
|
|
|
|
// check if schema version table is in place - if not, create the schema version table
|
|
sql = "SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '" + Config.DatabaseConfiguration.DatabaseName + "' AND TABLE_NAME = 'schema_version';";
|
|
DataTable SchemaVersionPresent = ExecuteCMD(sql, dbDict, CacheOptions);
|
|
if (SchemaVersionPresent.Rows.Count == 0)
|
|
{
|
|
// no schema table present - create it
|
|
Logging.LogKey(Logging.LogType.Information, "process.database", "database.schema_version_table_missing_creating");
|
|
sql = "CREATE TABLE `schema_version` (`schema_version` INT NOT NULL, PRIMARY KEY (`schema_version`)); INSERT INTO `schema_version` (`schema_version`) VALUES (0);";
|
|
ExecuteCMD(sql, dbDict, CacheOptions);
|
|
}
|
|
|
|
// ensure migration journal table exists before any migration steps run
|
|
MigrationJournal.EnsureTable();
|
|
|
|
sql = "SELECT schema_version FROM schema_version;";
|
|
dbDict = new Dictionary<string, object>();
|
|
DataTable SchemaVersion = ExecuteCMD(sql, dbDict, CacheOptions);
|
|
int OuterSchemaVer = (int)SchemaVersion.Rows[0][0];
|
|
if (OuterSchemaVer == 0)
|
|
{
|
|
OuterSchemaVer = 1000;
|
|
}
|
|
|
|
// --- PREFLIGHT: scan for all migration resources that need to be applied ---
|
|
// If a contiguous sequence exists and a resource is missing in that sequence,
|
|
// fail early before touching any data. This prevents partial migrations caused
|
|
// by a missing embedded SQL file making it to production.
|
|
{
|
|
string[] allResources = Assembly.GetExecutingAssembly().GetManifestResourceNames();
|
|
int preflight = OuterSchemaVer;
|
|
bool foundAny = false;
|
|
while (true)
|
|
{
|
|
string rn = "gaseous_lib.Support.Database.MySQL.gaseous-" + preflight + ".sql";
|
|
if (!allResources.Contains(rn)) break;
|
|
foundAny = true;
|
|
preflight++;
|
|
}
|
|
// If we found at least one pending resource, verify version sequence is
|
|
// contiguous up to the last one found.
|
|
if (foundAny)
|
|
{
|
|
// preflight now points to the first missing version after a run of
|
|
// present versions — nothing more to check, sequence is intact.
|
|
Logging.LogKey(Logging.LogType.Information, "process.database",
|
|
"database.preflight_migration_resources_verified",
|
|
null, new[] { OuterSchemaVer.ToString(), (preflight - 1).ToString() });
|
|
}
|
|
}
|
|
|
|
// --- BACKUP: take a backup before the first migration is applied ---
|
|
bool backupTaken = false;
|
|
string backupFilePath = "";
|
|
{
|
|
string[] allResources = Assembly.GetExecutingAssembly().GetManifestResourceNames();
|
|
bool hasPendingMigration = allResources.Contains(
|
|
"gaseous_lib.Support.Database.MySQL.gaseous-" + OuterSchemaVer + ".sql");
|
|
|
|
if (hasPendingMigration)
|
|
{
|
|
try
|
|
{
|
|
backupFilePath = DatabaseBackup.GenerateBackupPath();
|
|
DatabaseBackup.Backup(backupFilePath);
|
|
backupTaken = true;
|
|
}
|
|
catch (Exception backupEx)
|
|
{
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database",
|
|
"database.backup_failed_aborting_migration",
|
|
null, new[] { backupEx.Message }, backupEx);
|
|
System.Environment.Exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = OuterSchemaVer; i < 10000; i++)
|
|
{
|
|
string resourceName = "gaseous_lib.Support.Database.MySQL.gaseous-" + i + ".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 = reader.ReadToEnd();
|
|
|
|
// apply script
|
|
sql = "SELECT schema_version FROM schema_version;";
|
|
dbDict = new Dictionary<string, object>();
|
|
SchemaVersion = ExecuteCMD(sql, dbDict, CacheOptions);
|
|
if (SchemaVersion.Rows.Count == 0)
|
|
{
|
|
// something is broken here... where's the table?
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database", "database.schema_table_missing_should_not_happen");
|
|
throw new Exception("schema_version table is missing!");
|
|
}
|
|
else
|
|
{
|
|
int SchemaVer = (int)SchemaVersion.Rows[0][0];
|
|
Logging.LogKey(Logging.LogType.Information, "process.database", "database.schema_version_is", null, new[] { SchemaVer.ToString() });
|
|
// update schema version variable
|
|
Database.schema_version = SchemaVer;
|
|
if (SchemaVer < i)
|
|
{
|
|
// Step timeouts: first attempt uses 360s, retry doubles up to 1440s (24 min)
|
|
int[] timeoutSequence = { 360, 720, 1440 };
|
|
bool stepSucceeded = false;
|
|
Exception? lastException = null;
|
|
|
|
foreach (int stepTimeout in timeoutSequence)
|
|
{
|
|
try
|
|
{
|
|
long preJournalId = MigrationJournal.Start(i, MigrationJournal.StepType.PreUpgrade, "PreUpgradeScript");
|
|
try
|
|
{
|
|
await DatabaseMigration.PreUpgradeScript(i, _ConnectorType);
|
|
MigrationJournal.Complete(preJournalId);
|
|
}
|
|
catch (Exception preEx) when (preEx.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
MigrationJournal.Fail(preJournalId, preEx.Message);
|
|
throw; // let outer handler retry
|
|
}
|
|
catch (Exception preEx)
|
|
{
|
|
MigrationJournal.Fail(preJournalId, preEx.Message);
|
|
throw;
|
|
}
|
|
|
|
long sqlJournalId = MigrationJournal.Start(i, MigrationJournal.StepType.SqlScript, resourceName);
|
|
try
|
|
{
|
|
Logging.LogKey(Logging.LogType.Information, "process.database",
|
|
"database.updating_schema_to_version",
|
|
null, new[] { i.ToString(), stepTimeout.ToString() });
|
|
await ExecuteCMDAsync(dbScript, dbDict, stepTimeout);
|
|
MigrationJournal.Complete(sqlJournalId);
|
|
}
|
|
catch (Exception sqlEx) when (sqlEx.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
MigrationJournal.Fail(sqlJournalId, sqlEx.Message);
|
|
throw; // let outer handler retry
|
|
}
|
|
catch (Exception sqlEx)
|
|
{
|
|
MigrationJournal.Fail(sqlJournalId, sqlEx.Message);
|
|
throw;
|
|
}
|
|
|
|
// increment schema version
|
|
sql = "UPDATE schema_version SET schema_version=@schemaver";
|
|
dbDict = new Dictionary<string, object>();
|
|
dbDict.Add("schemaver", i);
|
|
await ExecuteCMDAsync(sql, dbDict, CacheOptions);
|
|
|
|
// run post-upgrade code
|
|
long postJournalId = MigrationJournal.Start(i, MigrationJournal.StepType.PostUpgrade, "PostUpgradeScript");
|
|
try
|
|
{
|
|
DatabaseMigration.PostUpgradeScript(i, _ConnectorType);
|
|
MigrationJournal.Complete(postJournalId);
|
|
}
|
|
catch (Exception postEx)
|
|
{
|
|
MigrationJournal.Fail(postJournalId, postEx.Message);
|
|
throw;
|
|
}
|
|
|
|
// run validation checks for this schema version
|
|
if (!DatabaseMigrationValidator.ValidateVersion(i))
|
|
{
|
|
throw new Exception($"Post-migration validation failed for schema version {i}. Check logs for details.");
|
|
}
|
|
|
|
// update schema version variable
|
|
Database.schema_version = i;
|
|
stepSucceeded = true;
|
|
break; // no need to retry
|
|
}
|
|
catch (Exception ex) when (ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
lastException = ex;
|
|
if (stepTimeout == timeoutSequence[timeoutSequence.Length - 1])
|
|
{
|
|
// exhausted all retries
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database",
|
|
"database.schema_upgrade_timed_out_all_retries",
|
|
null, new[] { i.ToString(), stepTimeout.ToString() }, ex);
|
|
}
|
|
else
|
|
{
|
|
Logging.LogKey(Logging.LogType.Warning, "process.database",
|
|
"database.schema_upgrade_timed_out_retrying",
|
|
null, new[] { i.ToString(), stepTimeout.ToString() }, ex);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastException = ex;
|
|
break; // non-timeout errors are not retried
|
|
}
|
|
}
|
|
|
|
if (!stepSucceeded)
|
|
{
|
|
string reason = lastException?.Message ?? "Unknown error";
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database",
|
|
"database.schema_upgrade_failed_unable_to_continue",
|
|
null, null, lastException);
|
|
|
|
if (backupTaken)
|
|
{
|
|
DatabaseBackup.LogRestoreInstructions(backupFilePath, i, reason);
|
|
}
|
|
System.Environment.Exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Logging.LogKey(Logging.LogType.Information, "process.database", "database.setup_complete");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a SQL script into individual statements, ignoring semicolons inside strings and comments.
|
|
/// </summary>
|
|
private static List<string> SplitSqlStatements(string sqlScript)
|
|
{
|
|
var statements = new List<string>();
|
|
var sb = new System.Text.StringBuilder();
|
|
bool inSingleQuote = false;
|
|
bool inDoubleQuote = false;
|
|
bool inLineComment = false;
|
|
bool inBlockComment = false;
|
|
|
|
for (int i = 0; i < sqlScript.Length; i++)
|
|
{
|
|
char c = sqlScript[i];
|
|
char next = i < sqlScript.Length - 1 ? sqlScript[i + 1] : '\0';
|
|
|
|
// Handle entering/exiting comments
|
|
if (!inSingleQuote && !inDoubleQuote)
|
|
{
|
|
if (!inBlockComment && c == '-' && next == '-')
|
|
{
|
|
inLineComment = true;
|
|
}
|
|
else if (!inBlockComment && c == '/' && next == '*')
|
|
{
|
|
inBlockComment = true;
|
|
i++; // skip '*'
|
|
continue;
|
|
}
|
|
else if (inBlockComment && c == '*' && next == '/')
|
|
{
|
|
inBlockComment = false;
|
|
i++; // skip '/'
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (inLineComment)
|
|
{
|
|
if (c == '\n')
|
|
{
|
|
inLineComment = false;
|
|
sb.Append(c);
|
|
}
|
|
continue;
|
|
}
|
|
if (inBlockComment)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Handle entering/exiting string literals
|
|
if (c == '\'' && !inDoubleQuote)
|
|
{
|
|
inSingleQuote = !inSingleQuote;
|
|
}
|
|
else if (c == '"' && !inSingleQuote)
|
|
{
|
|
inDoubleQuote = !inDoubleQuote;
|
|
}
|
|
|
|
// Split on semicolon if not inside a string
|
|
if (c == ';' && !inSingleQuote && !inDoubleQuote)
|
|
{
|
|
var statement = sb.ToString().Trim();
|
|
if (!string.IsNullOrEmpty(statement))
|
|
statements.Add(statement);
|
|
sb.Clear();
|
|
}
|
|
else
|
|
{
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
|
|
// Add any remaining statement
|
|
var last = sb.ToString().Trim();
|
|
if (!string.IsNullOrEmpty(last))
|
|
statements.Add(last);
|
|
|
|
return statements;
|
|
}
|
|
|
|
#region Synchronous Database Access
|
|
public DataTable ExecuteCMD(string Command)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMD(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public DataTable ExecuteCMD(string Command, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMD(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public DataTable ExecuteCMD(string Command, Dictionary<string, object> Parameters)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public DataTable ExecuteCMD(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public DataTable ExecuteCMD(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public DataTable ExecuteCMD(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command, Dictionary<string, object> Parameters)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public List<Dictionary<string, object>> ExecuteCMDDict(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
#endregion Synchronous Database Access
|
|
|
|
#region Asynchronous Database Access
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMD(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMD(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command, Dictionary<string, object> Parameters)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public async Task<DataTable> ExecuteCMDAsync(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
return _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteCMDDict(Command, dbDict, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command, Dictionary<string, object> Parameters)
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions)
|
|
{
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, 30, "");
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
DatabaseMemoryCacheOptions? CacheOptions = null;
|
|
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
|
|
public async Task<List<Dictionary<string, object>>> ExecuteCMDDictAsync(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
return _ExecuteCMDDict(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
}
|
|
#endregion Asynchronous Database Access
|
|
|
|
|
|
private List<Dictionary<string, object>> _ExecuteCMDDict(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
DataTable dataTable = _ExecuteCMD(Command, Parameters, CacheOptions, Timeout, ConnectionString);
|
|
|
|
// convert datatable to dictionary
|
|
List<Dictionary<string, object?>> rows = new List<Dictionary<string, object?>>();
|
|
|
|
foreach (DataRow dataRow in dataTable.Rows)
|
|
{
|
|
Dictionary<string, object?> row = new Dictionary<string, object?>();
|
|
for (int i = 0; i < dataRow.Table.Columns.Count; i++)
|
|
{
|
|
string columnName = dataRow.Table.Columns[i].ColumnName;
|
|
if (dataRow[i] == System.DBNull.Value)
|
|
{
|
|
row.Add(columnName, null);
|
|
}
|
|
else
|
|
{
|
|
row.Add(columnName, dataRow[i].ToString());
|
|
}
|
|
}
|
|
rows.Add(row);
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
private DataTable _ExecuteCMD(string Command, Dictionary<string, object> Parameters, DatabaseMemoryCacheOptions? CacheOptions, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
string CacheKey = Command + string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value)));
|
|
if (CacheOptions?.CacheKey != null)
|
|
{
|
|
CacheKey = CacheOptions.CacheKey;
|
|
}
|
|
|
|
if (CacheOptions is object && CacheOptions.CacheEnabled)
|
|
{
|
|
object? CachedData = DatabaseMemoryCache.GetCacheObject(CacheKey);
|
|
if (CachedData is object)
|
|
{
|
|
return (DataTable)CachedData;
|
|
}
|
|
}
|
|
|
|
// purge cache if command contains "INSERT", "UPDATE", "DELETE", or "ALTER"
|
|
if (
|
|
Command.Contains("INSERT", StringComparison.InvariantCultureIgnoreCase) ||
|
|
Command.Contains("UPDATE", StringComparison.InvariantCultureIgnoreCase) ||
|
|
Command.Contains("DELETE", StringComparison.InvariantCultureIgnoreCase) ||
|
|
Command.Contains("ALTER", StringComparison.InvariantCultureIgnoreCase)
|
|
)
|
|
{
|
|
// exclude logging events from purging the cache
|
|
if (!Command.StartsWith("INSERT INTO SERVERLOGS", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
DatabaseMemoryCache.ClearCache();
|
|
}
|
|
}
|
|
|
|
if (ConnectionString == "") { ConnectionString = _ConnectionString; }
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
MySQLServerConnector conn = new MySQLServerConnector(ConnectionString);
|
|
DataTable RetTable = conn.ExecCMD(Command, Parameters, Timeout);
|
|
if (CacheOptions is object && CacheOptions.CacheEnabled)
|
|
{
|
|
DatabaseMemoryCache.SetCacheObject(CacheKey, RetTable, CacheOptions.ExpirationSeconds);
|
|
}
|
|
return RetTable;
|
|
default:
|
|
return new DataTable();
|
|
}
|
|
}
|
|
|
|
public int ExecuteNonQuery(string Command)
|
|
{
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>();
|
|
return _ExecuteNonQuery(Command, dbDict, 30, "");
|
|
}
|
|
|
|
public int ExecuteNonQuery(string Command, Dictionary<string, object> Parameters)
|
|
{
|
|
return _ExecuteNonQuery(Command, Parameters, 30, "");
|
|
}
|
|
|
|
public int ExecuteNonQuery(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
return _ExecuteNonQuery(Command, Parameters, Timeout, ConnectionString);
|
|
}
|
|
|
|
private int _ExecuteNonQuery(string Command, Dictionary<string, object> Parameters, int Timeout = 30, string ConnectionString = "")
|
|
{
|
|
if (ConnectionString == "") { ConnectionString = _ConnectionString; }
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
MySQLServerConnector conn = new MySQLServerConnector(ConnectionString);
|
|
int retVal = conn.ExecNonQuery(Command, Parameters, Timeout);
|
|
return retVal;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public void ExecuteTransactionCMD(List<SQLTransactionItem> CommandList, int Timeout = 60)
|
|
{
|
|
object conn;
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
{
|
|
var commands = new List<Dictionary<string, object>>();
|
|
foreach (SQLTransactionItem CommandItem in CommandList)
|
|
{
|
|
var nCmd = new Dictionary<string, object>();
|
|
nCmd.Add("sql", CommandItem.SQLCommand);
|
|
nCmd.Add("values", CommandItem.Parameters);
|
|
commands.Add(nCmd);
|
|
}
|
|
|
|
conn = new MySQLServerConnector(_ConnectionString);
|
|
((MySQLServerConnector)conn).TransactionExecCMD(commands, Timeout);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public int GetDatabaseSchemaVersion()
|
|
{
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
string sql = "SELECT schema_version FROM schema_version;";
|
|
DataTable SchemaVersion = ExecuteCMD(sql);
|
|
if (SchemaVersion.Rows.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return (int)SchemaVersion.Rows[0][0];
|
|
}
|
|
|
|
default:
|
|
return 0;
|
|
|
|
}
|
|
}
|
|
|
|
public bool TestConnection()
|
|
{
|
|
switch (_ConnectorType)
|
|
{
|
|
case databaseType.MySql:
|
|
MySQLServerConnector conn = new MySQLServerConnector(_ConnectionString);
|
|
return conn.TestConnection();
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public class SQLTransactionItem
|
|
{
|
|
public SQLTransactionItem()
|
|
{
|
|
|
|
}
|
|
|
|
public SQLTransactionItem(string SQLCommand, Dictionary<string, object> Parameters)
|
|
{
|
|
this.SQLCommand = SQLCommand;
|
|
this.Parameters = Parameters;
|
|
}
|
|
|
|
public string? SQLCommand;
|
|
public Dictionary<string, object>? Parameters = new Dictionary<string, object>();
|
|
}
|
|
|
|
private partial class MySQLServerConnector
|
|
{
|
|
private string DBConn = "";
|
|
|
|
public MySQLServerConnector(string ConnectionString)
|
|
{
|
|
DBConn = ConnectionString;
|
|
}
|
|
|
|
public DataTable ExecCMD(string SQL, Dictionary<string, object> Parameters, int Timeout)
|
|
{
|
|
DataTable RetTable = new DataTable();
|
|
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.connecting_to_database", null, null, null, true);
|
|
using (MySqlConnection conn = new MySqlConnection(DBConn))
|
|
{
|
|
conn.Open();
|
|
|
|
MySqlCommand cmd = new MySqlCommand
|
|
{
|
|
Connection = conn,
|
|
CommandText = SQL,
|
|
CommandTimeout = Timeout
|
|
};
|
|
|
|
foreach (string Parameter in Parameters.Keys)
|
|
{
|
|
cmd.Parameters.AddWithValue(Parameter, Parameters[Parameter]);
|
|
}
|
|
|
|
try
|
|
{
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.executing_sql", null, new[] { SQL }, null, true);
|
|
if (Parameters.Count > 0)
|
|
{
|
|
string dictValues = string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value)));
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.parameters", null, new[] { dictValues }, null, true);
|
|
}
|
|
RetTable.Load(cmd.ExecuteReader());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database", "database.error_executing_sql", null, new[] { SQL }, ex);
|
|
#if DEBUG
|
|
if (Parameters.Count > 0)
|
|
{
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database", "database.parameters");
|
|
foreach (string param in Parameters.Keys)
|
|
{
|
|
string typeName = Parameters[param]?.GetType().ToString() ?? "unknown";
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database", param + " = " + Parameters[param] + " (" + typeName + ")");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.closing_database_connection", null, null, null, true);
|
|
conn.Close();
|
|
}
|
|
|
|
return RetTable;
|
|
}
|
|
|
|
public int ExecNonQuery(string SQL, Dictionary<string, object> Parameters, int Timeout)
|
|
{
|
|
int result = 0;
|
|
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.connecting_to_database", null, null, null, true);
|
|
using (MySqlConnection conn = new MySqlConnection(DBConn))
|
|
{
|
|
conn.Open();
|
|
|
|
MySqlCommand cmd = new MySqlCommand
|
|
{
|
|
Connection = conn,
|
|
CommandText = SQL,
|
|
CommandTimeout = Timeout
|
|
};
|
|
|
|
foreach (string Parameter in Parameters.Keys)
|
|
{
|
|
cmd.Parameters.AddWithValue(Parameter, Parameters[Parameter]);
|
|
}
|
|
|
|
try
|
|
{
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.executing_sql", null, new[] { SQL }, null, true);
|
|
if (Parameters.Count > 0)
|
|
{
|
|
string dictValues = string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value)));
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.parameters", null, new[] { dictValues }, null, true);
|
|
}
|
|
result = cmd.ExecuteNonQuery();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logging.LogKey(Logging.LogType.Critical, "process.database", "database.error_executing_sql", null, new[] { SQL }, ex);
|
|
Trace.WriteLine("Error executing " + SQL);
|
|
Trace.WriteLine("Full exception: " + ex.ToString());
|
|
}
|
|
|
|
Logging.LogKey(Logging.LogType.Debug, "process.database", "database.closing_database_connection", null, null, null, true);
|
|
conn.Close();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public void TransactionExecCMD(List<Dictionary<string, object>> Parameters, int Timeout)
|
|
{
|
|
using (MySqlConnection conn = new MySqlConnection(DBConn))
|
|
{
|
|
conn.Open();
|
|
var command = conn.CreateCommand();
|
|
MySqlTransaction transaction;
|
|
transaction = conn.BeginTransaction();
|
|
command.Connection = conn;
|
|
command.Transaction = transaction;
|
|
foreach (Dictionary<string, object> Parameter in Parameters)
|
|
{
|
|
var cmd = buildcommand(conn, Parameter["sql"].ToString(), (Dictionary<string, object>)Parameter["values"], Timeout);
|
|
cmd.Transaction = transaction;
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
transaction.Commit();
|
|
conn.Close();
|
|
}
|
|
}
|
|
|
|
private MySqlCommand buildcommand(MySqlConnection Conn, string SQL, Dictionary<string, object> Parameters, int Timeout)
|
|
{
|
|
var cmd = new MySqlCommand();
|
|
cmd.Connection = Conn;
|
|
cmd.CommandText = SQL;
|
|
cmd.CommandTimeout = Timeout;
|
|
{
|
|
var withBlock = cmd.Parameters;
|
|
if (Parameters is object)
|
|
{
|
|
if (Parameters.Count > 0)
|
|
{
|
|
foreach (string param in Parameters.Keys)
|
|
withBlock.AddWithValue(param, Parameters[param]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
public bool TestConnection()
|
|
{
|
|
using (MySqlConnection conn = new MySqlConnection(DBConn))
|
|
{
|
|
try
|
|
{
|
|
conn.Open();
|
|
conn.Close();
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|