diff --git a/.gitignore b/.gitignore index 1d33522..b5e3a4f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ obj .vscode temp-build-check Output -RELEASE* \ No newline at end of file +RELEASE* +database/systemgamemanager.db +*.db-wal +*.db-shm +*.db-journal \ No newline at end of file diff --git a/database/DatabaseController.cs b/database/DatabaseController.cs index d61abdc..0c7e492 100644 --- a/database/DatabaseController.cs +++ b/database/DatabaseController.cs @@ -1,5 +1,6 @@ namespace Krassheiten.SystemGameManager.Controller; +using System.Reflection; using Microsoft.Data.Sqlite; using Krassheiten.SystemGameManager.Service; using Krassheiten.SystemGameManager.Entity; @@ -45,14 +46,215 @@ public void ShowTable(string tableName) dump(rows); } + private static readonly object _dbInitLock = new(); + protected static SqliteConnection GetSqlConnection() { - string dbPath = "Data Source=database/systemgamemanager.db"; - var connection = new SqliteConnection(dbPath); + const string dbFile = "database/systemgamemanager.db"; + const string templateFile = "database/template-systemgamemanager.db"; + + lock (_dbInitLock) + { + if (File.Exists(templateFile)) + { + SyncEntitiesToTemplate(templateFile); + } + + if (!File.Exists(dbFile) && File.Exists(templateFile)) + { + File.Copy(templateFile, dbFile); + } + else if (File.Exists(dbFile) && File.Exists(templateFile)) + { + SyncSchemaFromTemplate(dbFile, templateFile); + } + } + + var connection = new SqliteConnection($"Data Source={dbFile}"); connection.Open(); + + using var pragma = connection.CreateCommand(); + pragma.CommandText = "PRAGMA journal_mode=DELETE;"; + pragma.ExecuteNonQuery(); + return connection; } + private static void SyncEntitiesToTemplate(string templateFile) + { + var entityTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.Name == "Record" && t.IsClass && t.DeclaringType != null) + .Select(t => new + { + RecordType = t, + TableName = t.DeclaringType! + .GetField("TABLE_NAME", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + ?.GetRawConstantValue() as string + }) + .Where(x => !string.IsNullOrWhiteSpace(x.TableName)) + .ToList(); + + if (entityTypes.Count == 0) return; + + using var conn = new SqliteConnection($"Data Source={templateFile}"); + conn.Open(); + + using var pragma = conn.CreateCommand(); + pragma.CommandText = "PRAGMA journal_mode=DELETE;"; + pragma.ExecuteNonQuery(); + + foreach (var entity in entityTypes) + { + var properties = entity.RecordType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite) + .OrderBy(p => p.MetadataToken) + .ToArray(); + + if (properties.Length == 0) continue; + + var columns = new List { "Id INTEGER PRIMARY KEY AUTOINCREMENT" }; + foreach (var property in properties) + { + var colType = GetEntitySqlType(property.PropertyType); + var colDef = property.Name.Equals("Name", StringComparison.OrdinalIgnoreCase) + ? $"[{property.Name}] {colType} NOT NULL UNIQUE" + : $"[{property.Name}] {colType}"; + columns.Add(colDef); + } + + using (var cmd = conn.CreateCommand()) + { + var columnDefinitions = string.Join(", ", columns); + cmd.CommandText = $"CREATE TABLE IF NOT EXISTS [{entity.TableName}] ({columnDefinitions});"; + cmd.ExecuteNonQuery(); + } + + var existingColumns = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA table_info([{entity.TableName}]);"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + existingColumns.Add(reader.GetString(1)); + } + } + + foreach (var property in properties) + { + if (existingColumns.Contains(property.Name)) continue; + + var colType = GetEntitySqlType(property.PropertyType); + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"ALTER TABLE [{entity.TableName}] ADD COLUMN [{property.Name}] {colType};"; + cmd.ExecuteNonQuery(); + } + } + } + + private static string GetEntitySqlType(Type propertyType) + { + var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + if (type == typeof(bool) || type == typeof(byte) || type == typeof(sbyte) || + type == typeof(short) || type == typeof(ushort) || type == typeof(int) || + type == typeof(uint) || type == typeof(long) || type == typeof(ulong)) + { + return "INTEGER"; + } + + if (type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + { + return "REAL"; + } + + return "TEXT"; + } + + private static void SyncSchemaFromTemplate(string dbFile, string templateFile) + { + var templateSchema = new Dictionary Columns)>(StringComparer.OrdinalIgnoreCase); + + using (var templateConn = new SqliteConnection($"Data Source={templateFile};Mode=ReadOnly")) + { + templateConn.Open(); + + var tableNames = new List<(string Name, string Sql)>(); + using (var cmd = templateConn.CreateCommand()) + { + cmd.CommandText = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + tableNames.Add((reader.GetString(0), reader.GetString(1))); + } + } + + foreach (var (name, sql) in tableNames) + { + var columns = new List<(string Name, string Type)>(); + using var colCmd = templateConn.CreateCommand(); + colCmd.CommandText = $"PRAGMA table_info([{name}]);"; + using var colReader = colCmd.ExecuteReader(); + while (colReader.Read()) + { + columns.Add((colReader.GetString(1), colReader.GetString(2))); + } + templateSchema[name] = (sql, columns); + } + } + + if (templateSchema.Count == 0) return; + + using var mainConn = new SqliteConnection($"Data Source={dbFile}"); + mainConn.Open(); + + var existingTables = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var cmd = mainConn.CreateCommand()) + { + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + existingTables.Add(reader.GetString(0)); + } + } + + foreach (var (tableName, (createSql, templateColumns)) in templateSchema) + { + if (!existingTables.Contains(tableName)) + { + using var cmd = mainConn.CreateCommand(); + cmd.CommandText = createSql; + cmd.ExecuteNonQuery(); + } + else + { + var existingColumns = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var cmd = mainConn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA table_info([{tableName}]);"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + existingColumns.Add(reader.GetString(1)); + } + } + + foreach (var (colName, colType) in templateColumns) + { + if (existingColumns.Contains(colName)) continue; + + using var cmd = mainConn.CreateCommand(); + cmd.CommandText = $"ALTER TABLE [{tableName}] ADD COLUMN [{colName}] {colType};"; + cmd.ExecuteNonQuery(); + } + } + } + } + public DatabaseService GetDatabaseService() { return new DatabaseService(); diff --git a/database/systemgamemanager.db b/database/template-systemgamemanager.db similarity index 78% rename from database/systemgamemanager.db rename to database/template-systemgamemanager.db index 81de567..39b6755 100644 Binary files a/database/systemgamemanager.db and b/database/template-systemgamemanager.db differ