diff options
Diffstat (limited to 'docker/crupest-api/CrupestApi/CrupestApi.Commons')
5 files changed, 149 insertions, 148 deletions
diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs index 13d9d6d..e8d3c2e 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/ColumnInfo.cs @@ -222,7 +222,7 @@ public class ColumnInfo else if (IsNotNull) { result.Append(' '); - result.Append(" NOT NULL"); + result.Append("NOT NULL"); } if (IsAutoIncrement) diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs index 5e00b28..1e881d3 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/CrudService.cs @@ -27,25 +27,14 @@ public class CrudService<TEntity> : IDisposable where TEntity : class _shouldDisposeConnection = dbConnectionFactory.ShouldDisposeConnection; _migrator = migrator; _logger = loggerFactory.CreateLogger<CrudService<TEntity>>(); + } - if (migrator.NeedMigrate(_dbConnection, _table)) + protected virtual void EnsureDatabase() + { + if (_migrator.NeedMigrate(_dbConnection, _table)) { _logger.LogInformation($"Entity {_table.TableName} needs migration."); - if (migrator.CanAutoMigrate(_dbConnection, _table)) - { - _logger.LogInformation($"Entity {_table.TableName} can be auto migrated."); - migrator.AutoMigrate(_dbConnection, _table); - AfterMigrate(_dbConnection, _table, loggerFactory); - } - else - { - _logger.LogInformation($"Entity {_table.TableName} can not be auto migrated."); - throw new Exception($"Entity {_table.TableName} needs migration but can not be auto migrated."); - } - } - else - { - _logger.LogInformation($"Entity {_table.TableName} does not need migration."); + _migrator.AutoMigrate(_dbConnection, _table); } } @@ -54,7 +43,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class return typeof(TEntity).Name; } - protected virtual void AfterMigrate(IDbConnection dbConnection, TableInfo tableInfo, ILoggerFactory loggerFactory) + protected virtual void AfterMigrate(IDbConnection dbConnection, TableInfo tableInfo) { } @@ -67,18 +56,21 @@ public class CrudService<TEntity> : IDisposable where TEntity : class public List<TEntity> GetAll() { + EnsureDatabase(); var result = _table.Select<TEntity>(_dbConnection, null); return result; } public int GetCount() { + EnsureDatabase(); var result = _table.SelectCount(_dbConnection); return result; } public TEntity GetByKey(object key) { + EnsureDatabase(); var result = _table.Select<TEntity>(_dbConnection, null, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)).SingleOrDefault(); if (result is null) { @@ -100,6 +92,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class public object Create(TEntity entity) { + EnsureDatabase(); var insertClause = ConvertEntityToInsertClauses(entity); _table.Insert(_dbConnection, insertClause, out var key); return key; @@ -121,6 +114,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class // Return new key. public object UpdateByKey(object key, TEntity entity, UpdateBehavior behavior = UpdateBehavior.None) { + EnsureDatabase(); var affectedCount = _table.Update(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key), ConvertEntityToUpdateClauses(entity, behavior), out var newKey); if (affectedCount == 0) @@ -132,6 +126,7 @@ public class CrudService<TEntity> : IDisposable where TEntity : class public bool DeleteByKey(object key) { + EnsureDatabase(); return _table.Delete(_dbConnection, WhereClause.Create().Eq(_table.KeyColumn.ColumnName, key)) == 1; } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs index 3d59c21..e5ef05d 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/DatabaseMigrator.cs @@ -2,44 +2,24 @@ using System.Data; namespace CrupestApi.Commons.Crud.Migrations; -public class TableColumn : IEquatable<TableColumn> +public class TableColumn { - public TableColumn(string name, string type, bool isNullable, int primaryKey) + public TableColumn(string name, string type, bool notNull, int primaryKey) { Name = name.ToLowerInvariant(); Type = type.ToLowerInvariant(); - IsNullable = isNullable; + NotNull = notNull; PrimaryKey = primaryKey; } public string Name { get; set; } public string Type { get; set; } - public bool IsNullable { get; set; } + public bool NotNull { get; set; } /// <summary> /// 0 if not primary key. 1-based index if in primary key. /// </summary> public int PrimaryKey { get; set; } - - bool IEquatable<TableColumn>.Equals(TableColumn? other) - { - if (other is null) - { - return false; - } - - return Name == other.Name && Type == other.Type && IsNullable == other.IsNullable && PrimaryKey == other.PrimaryKey; - } - - public override bool Equals(object? obj) - { - return Equals(obj as TableColumn); - } - - public override int GetHashCode() - { - return HashCode.Combine(Name, Type, IsNullable, PrimaryKey); - } } public class Table @@ -53,14 +33,21 @@ public class Table public List<TableColumn> Columns { get; set; } = new List<TableColumn>(); } +public class MigrationRecord +{ + public string TableName { get; set; } = default!; + public int Version { get; set; } + public Table Structure { get; set; } = default!; +} + public interface IDatabaseMigrator { - Table GetTable(IDbConnection dbConnection, string name); + List<MigrationRecord> GetRecords(IDbConnection dbConnection, string tableName); + + Table? GetTable(IDbConnection dbConnection, string tableName); Table ConvertTableInfoToTable(TableInfo tableInfo); string GenerateCreateTableColumnSqlSegment(TableColumn column); string GenerateCreateTableSql(string tableName, IEnumerable<TableColumn> columns); - bool TableExists(IDbConnection connection, string tableName); bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo); - bool CanAutoMigrate(IDbConnection dbConnection, TableInfo tableInfo); void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo); } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs index 762e95d..83b360b 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Crud/Migrations/SqliteDatabaseMigrator.cs @@ -1,5 +1,6 @@ using System.Data; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using Dapper; @@ -15,24 +16,90 @@ public class SqliteDatabaseMigrator : IDatabaseMigrator } } - public Table GetTable(IDbConnection dbConnection, string name) + private const string MigrationHistoryTableName = "migration_history"; + + private class MigrationRecordEntity { - CheckTableName(name); + public string TableName { get; set; } = string.Empty; + public int Version { get; set; } + public string Structure { get; set; } = string.Empty; + } - var table = new Table(name); - var queryColumns = dbConnection.Query<dynamic>($"PRAGMA table_info({name})"); + private void EnsureHistoryDatabase(IDbConnection dbConnection) + { + var exist = dbConnection.Query<int>($"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='{MigrationHistoryTableName}';").Single() == 1; + if (!exist) + { + dbConnection.Execute($@" + CREATE TABLE {MigrationHistoryTableName} ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + TableName TEXT NOT NULL, + Version INT NOT NULL, + Structure TEXT NOT NULL + ); + "); + } + } - foreach (var column in queryColumns) + public List<MigrationRecord> GetRecords(IDbConnection dbConnection, string tableName) + { + CheckTableName(tableName); + EnsureHistoryDatabase(dbConnection); + + var recordEntities = dbConnection.Query<MigrationRecordEntity>( + $"SELECT * FROM {MigrationHistoryTableName} WHERE TableName = @TableName ORDER BY Version ASC;", + new { TableName = tableName } + ).ToList(); + + var records = recordEntities.Select(entity => { - var columnName = (string)column.name; - var columnType = (string)column.type; - var isNullable = (bool)column.notnull; - var primaryKey = (long)column.pk; + var structure = JsonSerializer.Deserialize<Table>(entity.Structure); + if (structure is null) throw new Exception("Migration record is corrupted. Failed to convert structure."); + return new MigrationRecord + { + TableName = entity.TableName, + Version = entity.Version, + Structure = structure + }; + }).ToList(); + + return records; + } - table.Columns.Add(new TableColumn(columnName, columnType, isNullable, (int)primaryKey)); + + public Table? GetTable(IDbConnection dbConnection, string tableName) + { + CheckTableName(tableName); + + var count = dbConnection.QuerySingle<int>( + "SELECT count(*) FROM sqlite_schema WHERE type = 'table' AND tbl_name = @TableName;", + new { TableName = tableName }); + if (count == 0) + { + return null; + } + else if (count > 1) + { + throw new Exception($"More than 1 table has name {tableName}. What happened?"); } + else + { - return table; + var table = new Table(tableName); + var queryColumns = dbConnection.Query<dynamic>($"PRAGMA table_info({tableName})"); + + foreach (var column in queryColumns) + { + var columnName = (string)column.name; + var columnType = (string)column.type; + var isNullable = Convert.ToBoolean(column.notnull); + var primaryKey = Convert.ToInt32(column.pk); + + table.Columns.Add(new TableColumn(columnName, columnType, isNullable, primaryKey)); + } + + return table; + } } public Table ConvertTableInfoToTable(TableInfo tableInfo) @@ -42,42 +109,28 @@ public class SqliteDatabaseMigrator : IDatabaseMigrator foreach (var columnInfo in tableInfo.Columns) { table.Columns.Add(new TableColumn(columnInfo.ColumnName, columnInfo.ColumnType.GetSqlTypeString(), - !columnInfo.IsNotNull, columnInfo.IsPrimaryKey ? 1 : 0)); + columnInfo.IsNotNull, columnInfo.IsPrimaryKey ? 1 : 0)); } return table; } - public bool CanAutoMigrate(IDbConnection dbConnection, TableInfo tableInfo) + public string GenerateCreateTableColumnSqlSegment(TableColumn column) { - if (!TableExists(dbConnection, tableInfo.TableName)) return true; - - var databaseTable = GetTable(dbConnection, tableInfo.TableName); - var wantedTable = ConvertTableInfoToTable(tableInfo); - var databaseTableColumns = new HashSet<TableColumn>(databaseTable.Columns); - var wantedTableColumns = new HashSet<TableColumn>(wantedTable.Columns); - - if (databaseTableColumns.IsSubsetOf(wantedTableColumns)) + StringBuilder result = new StringBuilder(); + result.Append(column.Name); + result.Append(' '); + result.Append(column.Type); + if (column.PrimaryKey is not 0) { - var addColumns = wantedTableColumns.Except(databaseTableColumns); - foreach (var column in addColumns) - { - if (tableInfo.GetColumn(column.Name) is not null) - { - var columnInfo = tableInfo.GetColumn(column.Name); - if (!columnInfo.CanBeGenerated) - { - return false; - } - } - - } - return true; + result.Append(" PRIMARY KEY AUTOINCREMENT"); } - else + else if (column.NotNull) { - return false; + result.Append(" NOT NULL"); } + + return result.ToString(); } public string GenerateCreateTableSql(string tableName, IEnumerable<TableColumn> columns) @@ -87,7 +140,7 @@ public class SqliteDatabaseMigrator : IDatabaseMigrator var columnSql = string.Join(",\n", columns.Select(GenerateCreateTableColumnSqlSegment)); var sql = $@" -CREATE TABLE {tableName}( +CREATE TABLE {tableName} ( {columnSql} ); "; @@ -98,30 +151,36 @@ CREATE TABLE {tableName}( public void AutoMigrate(IDbConnection dbConnection, TableInfo tableInfo) { - if (!CanAutoMigrate(dbConnection, tableInfo)) + var tableName = tableInfo.TableName; + var databaseTable = GetTable(dbConnection, tableName); + var wantedTable = ConvertTableInfoToTable(tableInfo); + var databaseTableColumnNames = databaseTable is null ? new List<string>() : databaseTable.Columns.Select(column => column.Name).ToList(); + var wantedTableColumnNames = wantedTable.Columns.Select(column => column.Name).ToList(); + + var notChangeColumns = wantedTableColumnNames.Where(column => databaseTableColumnNames.Contains(column)).ToList(); + var addColumns = wantedTableColumnNames.Where(column => !databaseTableColumnNames.Contains(column)).ToList(); + + if (databaseTable is not null && dbConnection.Query<int>($"SELECT count(*) FROM {tableName}").Single() > 0) { - throw new Exception("The table can't be auto migrated."); + foreach (var columnName in addColumns) + { + var columnInfo = tableInfo.GetColumn(columnName); + if (!columnInfo.CanBeGenerated) + { + throw new Exception($"Column {columnName} cannot be generated. So we can't auto-migrate."); + } + } } // We are sqlite, so it's a little bit difficult. using var transaction = dbConnection.BeginTransaction(); - var tableName = tableInfo.TableName; - - var wantedTable = ConvertTableInfoToTable(tableInfo); - var wantedTableColumns = new HashSet<TableColumn>(wantedTable.Columns); - - var exist = TableExists(dbConnection, tableName); - if (exist) + if (databaseTable is not null) { - var databaseTable = GetTable(dbConnection, tableName); - var databaseTableColumns = new HashSet<TableColumn>(databaseTable.Columns); - var addColumns = wantedTableColumns.Except(databaseTableColumns); - var tempTableName = tableInfo.TableName + "_temp"; dbConnection.Execute($"ALTER TABLE {tableName} RENAME TO {tempTableName}", new { TableName = tableName, tempTableName }); - var createTableSql = GenerateCreateTableSql(tableName, wantedTableColumns.ToList()); + var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); dbConnection.Execute(createTableSql); // Copy old data to new table. @@ -130,19 +189,18 @@ CREATE TABLE {tableName}( { var parameters = new DynamicParameters(); - var originalColumnNames = originalRow.Keys.ToList(); - foreach (var columnName in originalColumnNames) + foreach (var columnName in notChangeColumns) { parameters.Add(columnName, originalRow[columnName]); } - var addColumnNames = addColumns.Select(c => c.Name).ToList(); - foreach (var columnName in addColumnNames) + + foreach (var columnName in addColumns) { parameters.Add(columnName, tableInfo.GetColumn(columnName).GenerateDefaultValue()); } - string columnSql = string.Join(", ", wantedTableColumns.Select(c => c.Name)); - string valuesSql = string.Join(", ", wantedTableColumns.Select(c => "@" + c.Name)); + string columnSql = string.Join(", ", wantedTableColumnNames); + string valuesSql = string.Join(", ", wantedTableColumnNames.Select(c => "@" + c)); string sql = $"INSERT INTO {tableName} ({columnSql}) VALUES {valuesSql})"; dbConnection.Execute(sql, parameters); @@ -153,7 +211,7 @@ CREATE TABLE {tableName}( } else { - var createTableSql = GenerateCreateTableSql(tableName, wantedTableColumns.ToList()); + var createTableSql = GenerateCreateTableSql(tableName, wantedTable.Columns); dbConnection.Execute(createTableSql); } @@ -161,52 +219,13 @@ CREATE TABLE {tableName}( transaction.Commit(); } - public string GenerateCreateTableColumnSqlSegment(TableColumn column) - { - StringBuilder result = new StringBuilder(); - result.Append(column.Name); - result.Append(' '); - result.Append(column.Type); - if (column.PrimaryKey is not 0) - { - result.Append(" PRIMARY KEY AUTOINCREMENT"); - } - else if (!column.IsNullable) - { - result.Append(" NOT NULL"); - } - - return result.ToString(); - } - public bool NeedMigrate(IDbConnection dbConnection, TableInfo tableInfo) { - if (!TableExists(dbConnection, tableInfo.TableName)) return true; - var tableName = tableInfo.TableName; var databaseTable = GetTable(dbConnection, tableName); var wantedTable = ConvertTableInfoToTable(tableInfo); - var databaseTableColumns = new HashSet<TableColumn>(databaseTable.Columns); - var wantedTableColumns = new HashSet<TableColumn>(wantedTable.Columns); - return databaseTableColumns != wantedTableColumns; - } - - public bool TableExists(IDbConnection connection, string tableName) - { - var count = connection.QuerySingle<int>( - "SELECT count(*) FROM sqlite_schema WHERE type = 'table' AND tbl_name = @TableName;", - new { TableName = tableName }); - if (count == 0) - { - return false; - } - else if (count > 1) - { - throw new Exception($"More than 1 table has name {tableName}. What happened?"); - } - else - { - return true; - } + var databaseTableColumns = databaseTable is null ? new HashSet<string>() : new HashSet<string>(databaseTable.Columns.Select(c => c.Name)); + var wantedTableColumns = new HashSet<string>(wantedTable.Columns.Select(c => c.Name)); + return !databaseTableColumns.SetEquals(wantedTableColumns); } } diff --git a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs index b8a1bbe..c693d8d 100644 --- a/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs +++ b/docker/crupest-api/CrupestApi/CrupestApi.Commons/Secrets/SecretService.cs @@ -14,11 +14,11 @@ public class SecretService : CrudService<SecretInfo>, ISecretService _logger = loggerFactory.CreateLogger<SecretService>(); } - protected override void AfterMigrate(IDbConnection connection, TableInfo table, ILoggerFactory loggerFactory) + protected override void AfterMigrate(IDbConnection connection, TableInfo table) { if (table.SelectCount(connection) == 0) { - loggerFactory.CreateLogger<SecretService>().LogInformation("No secrets found, insert default secrets."); + _logger.LogInformation("No secrets found, insert default secrets."); using var transaction = connection.BeginTransaction(); var insertClause = InsertClause.Create() .Add(nameof(SecretInfo.Key), SecretsConstants.SecretManagementKey) |