using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using VolPro.Builder.IServices; using VolPro.Core.EFDbContext; namespace VolPro.Builder.Services; public class SqlServerTableProvider : ITableDatabaseProvider { private readonly BaseDbContext _context; public SqlServerTableProvider(BaseDbContext context) { _context = context; } private static string NormalizeSqlServerDefault(string raw) { if (string.IsNullOrWhiteSpace(raw)) return null; var s = raw.Trim(); while (s.StartsWith("(") && s.EndsWith(")")) s = s[1..^1].Trim(); return string.IsNullOrWhiteSpace(s) ? null : s; } private static string FormatDefaultForSqlServer(string value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; var v = value.Trim(); var vLower = v.ToLower(); if (vLower is "getdate()" or "newid()" or "sysdatetime()") return $" DEFAULT {v}"; if (long.TryParse(v, out _) || decimal.TryParse(v, out _)) return $" DEFAULT {v}"; if (vLower == "true" || vLower == "1") return " DEFAULT 1"; if (vLower == "false" || vLower == "0") return " DEFAULT 0"; return $" DEFAULT N'{v.Replace("'", "''")}'"; } private static string BuildColumnDefinition(TableColumnDto column) { var definition = $"[{column.ColumnName}] "; var dtLower = (column.DataType ?? "").Trim().ToLower(); if (dtLower == "nvarchar(max)") { dtLower = "nvarchar"; } if (dtLower == "varchar(max)") { dtLower = "varchar"; } if (dtLower is "nvarchar" or "varchar") { var length = (column.DataType ?? "").Contains("max", StringComparison.OrdinalIgnoreCase) ? -1 : (column.Length ?? 255); definition += $"{dtLower}({(length == -1 ? "MAX" : length.ToString())})"; } else if (dtLower is "char" or "nchar") { var length = column.Length ?? 1; definition += $"{column.DataType}({length})"; } else if (dtLower is "decimal" or "numeric") { var precision = column.Length ?? 18; var scale = column.Scale ?? 0; definition += $"{column.DataType}({precision}, {scale})"; } else { definition += column.DataType ?? "nvarchar"; } if (column.IsIdentity) definition += " IDENTITY(1,1)"; if (!column.IsNullable) definition += " NOT NULL"; definition += FormatDefaultForSqlServer(column.DefaultValue); return definition; } /// /// SQL Server 不支持 ALTER COLUMN 修改 IDENTITY,通过临时表重建实现 /// private async Task RebuildTableForIdentityChangeAsync(UpdateTableRequest request, Dictionary existingByName) { var tableName = request.TableName; var tmpName = $"Tmp_{tableName}_{Guid.NewGuid().ToString("N")[..8]}"; var colDefs = request.Columns.OrderBy(c => c.Order).Select(BuildColumnDefinition).ToList(); var createTmpSql = $"CREATE TABLE [{tmpName}] (\n " + string.Join(",\n ", colDefs) + "\n)"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createTmpSql); var colsToCopy = request.Columns.Where(c => existingByName.ContainsKey(c.ColumnName)).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList(); var hasIdentity = request.Columns.Any(c => c.IsIdentity); var colList = string.Join(", ", colsToCopy.Select(c => $"[{c}]")); // IDENTITY_INSERT 是会话级,必须与 INSERT 在同一连接/同一批中执行 if (colsToCopy.Count > 0) { var insertBatch = hasIdentity ? $"SET IDENTITY_INSERT [{tmpName}] ON; INSERT INTO [{tmpName}] ({colList}) SELECT {colList} FROM [{tableName}]; SET IDENTITY_INSERT [{tmpName}] OFF" : $"INSERT INTO [{tmpName}] ({colList}) SELECT {colList} FROM [{tableName}]"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(insertBatch); } var dropPkSql = $@" DECLARE @pk NVARCHAR(200); SELECT @pk = name FROM sys.key_constraints WHERE type = 'PK' AND parent_object_id = OBJECT_ID('{tableName.Replace("'", "''")}'); IF @pk IS NOT NULL EXEC('ALTER TABLE [{tableName}] DROP CONSTRAINT [' + @pk + ']')"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropPkSql); await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"DROP TABLE [{tableName}]"); await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"EXEC sp_rename 'dbo.[{tmpName}]', '{tableName.Replace("'", "''")}', 'OBJECT'"); if (request.Columns.Any(c => c.IsPrimaryKey)) { var pkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => $"[{c.ColumnName}]"); var addPkSql = $"ALTER TABLE [{tableName}] ADD CONSTRAINT [PK_{tableName}] PRIMARY KEY ({string.Join(", ", pkCols)})"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addPkSql); } foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment))) { try { var commentSql = $@" EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'{column.Comment.Replace("'", "''")}', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{tableName.Replace("'", "''")}', @level2type = N'COLUMN', @level2name = N'{column.ColumnName.Replace("'", "''")}'"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql); } catch { /* 注释已存在时忽略 */ } } } public async Task TableExistsAsync(string tableName) { const string sql = "SELECT COUNT(*) as Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName"; var res = await _context.SqlSugarClient.Ado.GetScalarAsync(sql, new { tableName }); return Convert.ToInt32(res) > 0; } public async Task CreateTableAsync(CreateTableRequest request) { var columnDefinitions = new List(); foreach (var column in request.Columns.OrderBy(c => c.Order)) { var definition = $"[{column.ColumnName}] "; // Data type and length var dtLower = column.DataType.ToLower(); if (dtLower == "nvarchar" || dtLower == "varchar") { var length = column.Length ?? 255; definition += $"{column.DataType}({(length == -1 ? "MAX" : length.ToString())})"; } else if (dtLower == "char" || dtLower == "nchar") { var length = column.Length ?? 1; definition += $"{column.DataType}({length})"; } else if (dtLower == "decimal" || dtLower == "numeric") { var precision = column.Length ?? 18; var scale = column.Scale ?? 0; definition += $"{column.DataType}({precision}, {scale})"; } else { definition += column.DataType; } // Identity (Auto increment) if (column.IsIdentity) { definition += " IDENTITY(1,1)"; } // Nullable if (!column.IsNullable) { definition += " NOT NULL"; } // Default value definition += FormatDefaultForSqlServer(column.DefaultValue); columnDefinitions.Add(definition); } // Build CREATE TABLE statement var createTableSql = $"CREATE TABLE [{request.TableName}] (\n " + string.Join(",\n ", columnDefinitions) + "\n)"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createTableSql); // Add primary key constraint var pkColumns = request.Columns .Where(c => c.IsPrimaryKey) .OrderBy(c => c.Order) .Select(c => $"[{c.ColumnName}]"); var pkConstraintName = $"PK_{request.TableName}"; var pkSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{pkConstraintName}] PRIMARY KEY ({string.Join(", ", pkColumns)})"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(pkSql); // Add column comments (MS_Description) foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment))) { var commentSql = $@" EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'{column.Comment.Replace("'", "''")}', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{request.TableName}', @level2type = N'COLUMN', @level2name = N'{column.ColumnName}'"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql); } } public async Task> GetAllTablesAsync() { const string sql = "SELECT TABLE_NAME as Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME"; var rows = await _context.SqlSugarClient.Ado.SqlQueryAsync(sql); return rows ?? new List(); } public async Task GetTableInfoAsync(string tableName) { const string columnsSql = @" DECLARE @objId INT = OBJECT_ID(QUOTENAME('dbo') + '.' + QUOTENAME(@tableName)); SELECT c.COLUMN_NAME, c.DATA_TYPE, c.CHARACTER_MAXIMUM_LENGTH, c.IS_NULLABLE, c.ORDINAL_POSITION, c.NUMERIC_SCALE, c.NUMERIC_PRECISION, COLUMNPROPERTY(@objId, c.COLUMN_NAME, 'IsIdentity') AS IS_IDENTITY, CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PRIMARY_KEY, CAST(ep.value AS NVARCHAR(4000)) AS COMMENT, c.COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS c LEFT JOIN ( SELECT ku.TABLE_NAME, ku.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME ) pk ON c.TABLE_NAME = pk.TABLE_NAME AND c.COLUMN_NAME = pk.COLUMN_NAME LEFT JOIN sys.columns sc ON sc.object_id = @objId AND sc.name = c.COLUMN_NAME LEFT JOIN sys.extended_properties ep ON ep.major_id = sc.object_id AND ep.minor_id = sc.column_id AND ep.name = 'MS_Description' WHERE c.TABLE_SCHEMA = 'dbo' AND c.TABLE_NAME = @tableName ORDER BY c.ORDINAL_POSITION;"; var dt = await _context.SqlSugarClient.Ado.GetDataTableAsync(columnsSql, new { tableName }); var columnsWithComments = new List(); foreach (DataRow row in dt.Rows) { var length = row[2] == DBNull.Value || row[2] == null ? (int?)null : Convert.ToInt32(row[2]); var numericScale = row[5] == DBNull.Value || row[5] == null ? (int?)null : Convert.ToInt32(row[5]); var numericPrecision = row[6] == DBNull.Value || row[6] == null ? (int?)null : Convert.ToInt32(row[6]); var dataType = row[1]?.ToString() ?? ""; var isDecimal = dataType.Equals("decimal", StringComparison.OrdinalIgnoreCase) || dataType.Equals("numeric", StringComparison.OrdinalIgnoreCase); var colDefault = row[10] == DBNull.Value || row[10] == null ? null : NormalizeSqlServerDefault(row[10]?.ToString()); columnsWithComments.Add(new TableColumnDto { ColumnName = row[0]?.ToString() ?? "", DataType = dataType, Length = isDecimal ? numericPrecision : (length == -1 ? -1 : length), Scale = isDecimal ? numericScale : null, IsNullable = row[3]?.ToString() == "YES", Order = Convert.ToInt32(row[4]), IsIdentity = row[7] != DBNull.Value && row[7] != null && Convert.ToInt32(row[7]) == 1, IsPrimaryKey = row[8] != DBNull.Value && row[8] != null && Convert.ToInt32(row[8]) == 1, Comment = row[9]?.ToString(), DefaultValue = colDefault ?? string.Empty }); } return new TableInfoDto { TableName = tableName, Columns = columnsWithComments }; } public async Task UpdateTableAsync(UpdateTableRequest request) { var existingTable = await GetTableInfoAsync(request.TableName); if (existingTable == null) { throw new InvalidOperationException($"Could not retrieve table information for '{request.TableName}'."); } var existingByName = existingTable.Columns.ToDictionary(c => c.ColumnName, c => c, StringComparer.Ordinal); var newColumnNames = request.Columns.Select(c => c.ColumnName).ToHashSet(StringComparer.Ordinal); // 1) 识别并执行重命名字段(区分大小写) var renames = request.Columns .Where(c => c.IsDbField && !string.IsNullOrWhiteSpace(c.OriginalColumnName) && c.OriginalColumnName != c.ColumnName && existingByName.Keys.Any(k => string.Equals(k, c.OriginalColumnName, StringComparison.OrdinalIgnoreCase))) .ToList(); foreach (var r in renames) { var actualKey = existingByName.Keys.FirstOrDefault(k => string.Equals(k, r.OriginalColumnName, StringComparison.OrdinalIgnoreCase)); if (actualKey == null) continue; var renameSql = $"EXEC sp_rename 'dbo.[{request.TableName}].[{actualKey}]', '{r.ColumnName.Replace("'", "''")}', 'COLUMN';"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(renameSql); var oldCol = existingByName[actualKey]; existingByName.Remove(actualKey); existingByName[r.ColumnName] = new TableColumnDto { ColumnName = r.ColumnName, DataType = oldCol.DataType, Length = oldCol.Length, Scale = oldCol.Scale, IsNullable = oldCol.IsNullable, IsPrimaryKey = oldCol.IsPrimaryKey, IsIdentity = oldCol.IsIdentity, Comment = oldCol.Comment, DefaultValue = oldCol.DefaultValue ?? "", Order = oldCol.Order }; } var existingColumnNames = existingByName.Keys.ToHashSet(StringComparer.Ordinal); var columnsToDrop = existingColumnNames.Except(newColumnNames); foreach (var columnName in columnsToDrop) { var dropColumnSql = $"ALTER TABLE [{request.TableName}] DROP COLUMN [{columnName}]"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropColumnSql); } // 2) 新增列 var columnsToAdd = request.Columns.Where(c => !existingColumnNames.Contains(c.ColumnName, StringComparer.Ordinal)); foreach (var column in columnsToAdd.OrderBy(c => c.Order)) { var definition = $"[{column.ColumnName}] "; var dtLowerAdd = column.DataType.ToLower(); if (dtLowerAdd == "nvarchar" || dtLowerAdd == "varchar") { var length = column.Length ?? 255; definition += $"{column.DataType}({(length == -1 ? "MAX" : length.ToString())})"; } else if (dtLowerAdd == "char" || dtLowerAdd == "nchar") { var length = column.Length ?? 1; definition += $"{column.DataType}({length})"; } else if (dtLowerAdd == "decimal" || dtLowerAdd == "numeric") { var precision = column.Length ?? 18; var scale = column.Scale ?? 0; definition += $"{column.DataType}({precision}, {scale})"; } else { definition += column.DataType; } if (column.IsIdentity) { definition += " IDENTITY(1,1)"; } if (!column.IsNullable) { definition += " NOT NULL"; } definition += FormatDefaultForSqlServer(column.DefaultValue); var addColumnSql = $"ALTER TABLE [{request.TableName}] ADD {definition}"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addColumnSql); // 新列注释 if (!string.IsNullOrWhiteSpace(column.Comment)) { try { var commentSql = $@" EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'{column.Comment.Replace("'", "''")}', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{request.TableName.Replace("'", "''")}', @level2type = N'COLUMN', @level2name = N'{column.ColumnName.Replace("'", "''")}'"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql); } catch { /* 注释已存在时忽略 */ } } } // 3) Alter existing columns: 类型/长度/小数位/可空(existingByName 已在 rename 后更新) var columnsToAlter = request.Columns .Where(c => existingByName.ContainsKey(c.ColumnName)) .OrderBy(c => c.Order); foreach (var column in columnsToAlter) { var existing = existingByName[column.ColumnName]; var defaultChanged = (existing.DefaultValue ?? "").Trim() != (column.DefaultValue ?? "").Trim(); var newTypeRaw = column.DataType?.Trim() ?? ""; var newTypeLower = newTypeRaw.ToLower(); var oldTypeLower = (existing.DataType ?? "").Trim().ToLower(); // 兼容传入 nvarchar(max)/varchar(max) if (newTypeLower == "nvarchar(max)") { newTypeLower = "nvarchar"; column.Length = -1; } if (newTypeLower == "varchar(max)") { newTypeLower = "varchar"; column.Length = -1; } bool typeChanged = !string.Equals(oldTypeLower, newTypeLower, StringComparison.OrdinalIgnoreCase); bool nullableChanged = existing.IsNullable != column.IsNullable; bool lengthChanged = false; bool scaleChanged = false; if (newTypeLower is "nvarchar" or "varchar") { var oldLen = existing.Length ?? 255; var newLen = column.Length ?? 255; lengthChanged = oldLen != newLen; } else if (newTypeLower is "char" or "nchar") { var oldLen = existing.Length ?? 1; var newLen = column.Length ?? 1; lengthChanged = oldLen != newLen; } else if (newTypeLower is "decimal" or "numeric") { var oldPrec = existing.Length ?? 18; var newPrec = column.Length ?? 18; lengthChanged = oldPrec != newPrec; var oldScale = existing.Scale ?? 0; var newScale = column.Scale ?? 0; scaleChanged = oldScale != newScale; } bool isIdentityChanged = existing.IsIdentity != column.IsIdentity; // 仅当字段有实际修改时才执行更新脚本 if (!typeChanged && !nullableChanged && !lengthChanged && !scaleChanged && !defaultChanged && !isIdentityChanged) continue; // IsIdentity 变更:SQL Server 不支持 ALTER COLUMN 修改 IDENTITY,需通过临时表重建 if (isIdentityChanged) { await RebuildTableForIdentityChangeAsync(request, existingByName); var refreshed = await GetTableInfoAsync(request.TableName); foreach (var c in refreshed.Columns) existingByName[c.ColumnName] = c; break; } if (typeChanged || nullableChanged || lengthChanged || scaleChanged) { string typeSql; if (newTypeLower is "nvarchar" or "varchar") { var len = column.Length ?? 255; typeSql = $"{newTypeLower}({(len == -1 ? "MAX" : len.ToString())})"; } else if (newTypeLower is "char" or "nchar") { var len = column.Length ?? 1; typeSql = $"{newTypeLower}({len})"; } else if (newTypeLower is "decimal" or "numeric") { var precision = column.Length ?? 18; var scale = column.Scale ?? 0; typeSql = $"{newTypeLower}({precision}, {scale})"; } else { typeSql = newTypeRaw; } var nullSql = column.IsNullable ? "NULL" : "NOT NULL"; var alterSql = $"ALTER TABLE [{request.TableName}] ALTER COLUMN [{column.ColumnName}] {typeSql} {nullSql}"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(alterSql); } if (defaultChanged) { var dfName = $"DF_{request.TableName}_{column.ColumnName}"; var dropDfSql = $@"DECLARE @cn NVARCHAR(200); SELECT @cn = dc.name FROM sys.default_constraints dc INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id WHERE c.object_id = OBJECT_ID('{request.TableName.Replace("'", "''")}') AND c.name = N'{column.ColumnName.Replace("'", "''")}'; IF @cn IS NOT NULL EXEC('ALTER TABLE [{request.TableName}] DROP CONSTRAINT [' + @cn + ']')"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropDfSql); if (!string.IsNullOrWhiteSpace(column.DefaultValue)) { var defVal = column.DefaultValue.Trim(); var vLower = defVal.ToLower(); var innerExpr = vLower is "getdate()" or "newid()" or "sysdatetime()" ? defVal : long.TryParse(defVal, out _) || decimal.TryParse(defVal, out _) ? defVal : vLower == "true" || vLower == "1" ? "1" : vLower == "false" || vLower == "0" ? "0" : $"N'{defVal.Replace("'", "''")}'"; var addDfSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{dfName}] DEFAULT ({innerExpr}) FOR [{column.ColumnName}]"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addDfSql); } } } var existingPkCols = existingTable.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList(); var newPkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList(); var pkChanged = !existingPkCols.SequenceEqual(newPkCols); if (pkChanged) { // Drop existing PK constraint var dropPkSql = $@" DECLARE @pkConstraintName NVARCHAR(200); SELECT @pkConstraintName = name FROM sys.key_constraints WHERE type = 'PK' AND parent_object_id = OBJECT_ID('{request.TableName}'); IF @pkConstraintName IS NOT NULL EXEC('ALTER TABLE [{request.TableName}] DROP CONSTRAINT [' + @pkConstraintName + ']')"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropPkSql); if (newPkCols.Any()) { var pkConstraintName = $"PK_{request.TableName}"; var pkColumns = newPkCols.Select(c => $"[{c}]"); var addPkSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{pkConstraintName}] PRIMARY KEY ({string.Join(", ", pkColumns)})"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addPkSql); } } foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment))) { if (existingByName.TryGetValue(column.ColumnName, out var ec) && (ec.Comment ?? "").Trim() == (column.Comment ?? "").Trim()) continue; var escapedComment = column.Comment!.Replace("'", "''"); var tableNameEscaped = request.TableName.Replace("'", "''"); var columnNameEscaped = column.ColumnName.Replace("'", "''"); try { var updateCommentSql = $@" EXEC sp_updateextendedproperty @name = N'MS_Description', @value = N'{escapedComment}', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{tableNameEscaped}', @level2type = N'COLUMN', @level2name = N'{columnNameEscaped}'"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(updateCommentSql); } catch { try { var addCommentSql = $@" EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'{escapedComment}', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{tableNameEscaped}', @level2type = N'COLUMN', @level2name = N'{columnNameEscaped}'"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addCommentSql); } catch { /* 忽略 */ } } } } public async Task DeleteTableAsync(string tableName) { var dropTableSql = $"DROP TABLE [{tableName}]"; await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropTableSql); } }