using System.Data; using LinqToDB.Data; using LinqToDB.Mapping; namespace Azaion.Common.Database; public static class SchemaMigrator { public static void EnsureSchemaUpdated(DataConnection dbConnection, params Type[] entityTypes) { var connection = dbConnection.Connection; var mappingSchema = dbConnection.MappingSchema; if (connection.State == ConnectionState.Closed) { connection.Open(); } foreach (var type in entityTypes) { var entityDescriptor = mappingSchema.GetEntityDescriptor(type); var tableName = entityDescriptor.Name.Name; var existingColumns = GetTableColumns(connection, tableName); foreach (var column in entityDescriptor.Columns) { if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase)) continue; var columnDefinition = GetColumnDefinition(column); dbConnection.Execute($"ALTER TABLE {tableName} ADD COLUMN {columnDefinition}"); } } } private static HashSet GetTableColumns(IDbConnection connection, string tableName) { var columns = new HashSet(StringComparer.OrdinalIgnoreCase); using var cmd = connection.CreateCommand(); cmd.CommandText = $"PRAGMA table_info({tableName})"; using var reader = cmd.ExecuteReader(); while (reader.Read()) columns.Add(reader.GetString(1)); // "name" is in the second column return columns; } private static string GetColumnDefinition(ColumnDescriptor column) { var type = column.MemberType; var underlyingType = Nullable.GetUnderlyingType(type) ?? type; var sqliteType = GetSqliteType(underlyingType); var defaultClause = GetSqlDefaultValue(type, underlyingType); return $"\"{column.ColumnName}\" {sqliteType} {defaultClause}"; } private static string GetSqliteType(Type type) => type switch { _ when type == typeof(int) || type == typeof(long) || type == typeof(bool) || type.IsEnum => "INTEGER", _ when type == typeof(double) || type == typeof(float) || type == typeof(decimal) => "REAL", _ when type == typeof(byte[]) => "BLOB", _ => "TEXT" }; private static string GetSqlDefaultValue(Type originalType, Type underlyingType) { var isNullable = originalType.IsClass || Nullable.GetUnderlyingType(originalType) != null; if (isNullable) return "NULL"; var defaultValue = Activator.CreateInstance(underlyingType); if (underlyingType == typeof(bool)) return $"NOT NULL DEFAULT {(Convert.ToBoolean(defaultValue) ? 1 : 0)}"; if (underlyingType.IsEnum) return $"NOT NULL DEFAULT {(int)defaultValue}"; if (underlyingType.IsValueType && defaultValue is IFormattable f) return $"NOT NULL DEFAULT {f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)}"; return $"NOT NULL DEFAULT '{defaultValue}'"; } }