diff --git a/Dashboard/Services/DatabaseService.FinOps.cs b/Dashboard/Services/DatabaseService.FinOps.cs index 752e02e..f593d22 100644 --- a/Dashboard/Services/DatabaseService.FinOps.cs +++ b/Dashboard/Services/DatabaseService.FinOps.cs @@ -1852,19 +1852,51 @@ public async Task> GetFinOpsRecommendationsAsync(deci try { using var editionCmd = new SqlCommand( - "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))", connection); + "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " + + "CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)", connection); editionCmd.CommandTimeout = 30; - var edition = (string?)await editionCmd.ExecuteScalarAsync() ?? ""; + using var editionReader = await editionCmd.ExecuteReaderAsync(); + string edition = ""; + int majorVersion = 0; + if (await editionReader.ReadAsync()) + { + edition = editionReader.IsDBNull(0) ? "" : editionReader.GetString(0); + majorVersion = editionReader.IsDBNull(1) ? 0 : editionReader.GetInt32(1); + } if (edition.Contains("Enterprise", StringComparison.OrdinalIgnoreCase)) { - /* - sys.dm_db_persisted_sku_features is database-scoped on all versions. - Query across all online user databases for TDE usage — the only feature - still Enterprise-only since 2016 SP1 (Compression, Partitioning, - ColumnStoreIndex are all available in Standard). - */ - using var featCmd = new SqlCommand(@" + // SQL Server 2019 (major version 15) moved TDE to Standard Edition. + // On 2019+, dm_db_persisted_sku_features won't report TDE since it's + // no longer Enterprise-restricted — so we skip the TDE-specific check + // and give version-appropriate guidance instead. + if (majorVersion >= 15) + { + // 2019+: Most features that were Enterprise-only moved to Standard + // in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only + // features remain (e.g., certain HA configurations). + recommendations.Add(new FinOpsRecommendation + { + Category = "Licensing", + Severity = "High", + Confidence = "Medium", + Finding = "Enterprise Edition may not be required", + Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " + + "(including TDE, compression, partitioning, and columnstore) are available " + + "in Standard Edition. Review whether remaining Enterprise-only features " + + "(such as Always On availability groups with multiple secondaries) are in use " + + "before considering a downgrade to Standard Edition.", + EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null + }); + } + else + { + /* + Pre-2019: TDE is the only commonly-used feature still restricted + to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features + to detect it — the DMV correctly reports TDE on these versions. + */ + using var featCmd = new SqlCommand(@" DECLARE @sql nvarchar(max) = N''; @@ -1883,42 +1915,42 @@ IF @sql <> N'' SET @sql = LEFT(@sql, LEN(@sql) - 10); EXEC sys.sp_executesql @sql; END;", connection); - featCmd.CommandTimeout = 30; + featCmd.CommandTimeout = 30; - var tdeDbNames = new List(); - using var featReader = await featCmd.ExecuteReaderAsync(); - while (await featReader.ReadAsync()) - { - if (!featReader.IsDBNull(0)) - tdeDbNames.Add(featReader.GetString(0)); - } + var tdeDbNames = new List(); + using var featReader = await featCmd.ExecuteReaderAsync(); + while (await featReader.ReadAsync()) + { + if (!featReader.IsDBNull(0)) + tdeDbNames.Add(featReader.GetString(0)); + } - if (tdeDbNames.Count == 0) - { - recommendations.Add(new FinOpsRecommendation + if (tdeDbNames.Count == 0) { - Category = "Licensing", - Severity = "High", - Confidence = "High", - Finding = "Enterprise Edition with no Enterprise-only features detected", - Detail = "No databases use Transparent Data Encryption (TDE), the only feature " + - "still restricted to Enterprise Edition since SQL Server 2016 SP1. " + - "Review whether Standard Edition would meet workload requirements for potential license savings.", - EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null - }); - } - else - { - recommendations.Add(new FinOpsRecommendation + recommendations.Add(new FinOpsRecommendation + { + Category = "Licensing", + Severity = "High", + Confidence = "High", + Finding = "Enterprise Edition with no Enterprise-only features detected", + Detail = "No databases use Transparent Data Encryption (TDE), the only feature " + + "still restricted to Enterprise Edition since SQL Server 2016 SP1. " + + "Review whether Standard Edition would meet workload requirements for potential license savings.", + EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null + }); + } + else { - Category = "Licensing", - Severity = "Low", - Confidence = "High", - Finding = "TDE in use — Enterprise Edition downgrade blocker", - Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" + - (tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") + - ". TDE must be removed before downgrading to Standard Edition." - }); + recommendations.Add(new FinOpsRecommendation + { + Category = "Licensing", + Severity = "Low", + Confidence = "High", + Finding = "TDE in use — Enterprise Edition downgrade blocker", + Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" + + (tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") + + ". TDE must be removed before downgrading to Standard Edition." + }); // Check 10: License cost impact estimate (only when features ARE in use) using var cpuInfoCmd = new SqlCommand( @@ -1941,6 +1973,7 @@ IF @sql <> N'' }); } } + } } } catch (Exception ex) diff --git a/Lite/Services/LocalDataService.FinOps.cs b/Lite/Services/LocalDataService.FinOps.cs index c2290f0..163cdf8 100644 --- a/Lite/Services/LocalDataService.FinOps.cs +++ b/Lite/Services/LocalDataService.FinOps.cs @@ -1578,19 +1578,51 @@ public async Task> GetRecommendationsAsync(int serverId, await sqlConn.OpenAsync(); using var editionCmd = new SqlCommand( - "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))", sqlConn); + "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " + + "CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)", sqlConn); editionCmd.CommandTimeout = 30; - var edition = (string?)await editionCmd.ExecuteScalarAsync() ?? ""; + using var editionReader = await editionCmd.ExecuteReaderAsync(); + string edition = ""; + int majorVersion = 0; + if (await editionReader.ReadAsync()) + { + edition = editionReader.IsDBNull(0) ? "" : editionReader.GetString(0); + majorVersion = editionReader.IsDBNull(1) ? 0 : editionReader.GetInt32(1); + } if (edition.Contains("Enterprise", StringComparison.OrdinalIgnoreCase)) { - /* - sys.dm_db_persisted_sku_features is database-scoped on all versions. - Query across all online user databases for TDE usage — the only feature - still Enterprise-only since 2016 SP1 (Compression, Partitioning, - ColumnStoreIndex are all available in Standard). - */ - using var featCmd = new SqlCommand(@" + // SQL Server 2019 (major version 15) moved TDE to Standard Edition. + // On 2019+, dm_db_persisted_sku_features won't report TDE since it's + // no longer Enterprise-restricted — so we skip the TDE-specific check + // and give version-appropriate guidance instead. + if (majorVersion >= 15) + { + // 2019+: Most features that were Enterprise-only moved to Standard + // in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only + // features remain (e.g., certain HA configurations). + recommendations.Add(new RecommendationRow + { + Category = "Licensing", + Severity = "High", + Confidence = "Medium", + Finding = "Enterprise Edition may not be required", + Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " + + "(including TDE, compression, partitioning, and columnstore) are available " + + "in Standard Edition. Review whether remaining Enterprise-only features " + + "(such as Always On availability groups with multiple secondaries) are in use " + + "before considering a downgrade to Standard Edition.", + EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null + }); + } + else + { + /* + Pre-2019: TDE is the only commonly-used feature still restricted + to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features + to detect it — the DMV correctly reports TDE on these versions. + */ + using var featCmd = new SqlCommand(@" DECLARE @sql nvarchar(max) = N''; @@ -1609,62 +1641,63 @@ IF @sql <> N'' SET @sql = LEFT(@sql, LEN(@sql) - 10); EXEC sys.sp_executesql @sql; END;", sqlConn); - featCmd.CommandTimeout = 30; + featCmd.CommandTimeout = 30; - var tdeDbNames = new List(); - using var featReader = await featCmd.ExecuteReaderAsync(); - while (await featReader.ReadAsync()) - { - if (!featReader.IsDBNull(0)) - tdeDbNames.Add(featReader.GetString(0)); - } - - if (tdeDbNames.Count == 0) - { - recommendations.Add(new RecommendationRow - { - Category = "Licensing", - Severity = "High", - Confidence = "High", - Finding = "Enterprise Edition with no Enterprise-only features detected", - Detail = "No databases use Transparent Data Encryption (TDE), the only feature " + - "still restricted to Enterprise Edition since SQL Server 2016 SP1. " + - "Review whether Standard Edition would meet workload requirements for potential license savings.", - EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null - }); - } - else - { - recommendations.Add(new RecommendationRow + var tdeDbNames = new List(); + using var featReader = await featCmd.ExecuteReaderAsync(); + while (await featReader.ReadAsync()) { - Category = "Licensing", - Severity = "Low", - Confidence = "High", - Finding = "TDE in use — Enterprise Edition downgrade blocker", - Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" + - (tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") + - ". TDE must be removed before downgrading to Standard Edition." - }); + if (!featReader.IsDBNull(0)) + tdeDbNames.Add(featReader.GetString(0)); + } - // Check 10: License cost impact estimate (only when features ARE in use) - using var cpuInfoCmd = new SqlCommand( - "SELECT cpu_count FROM sys.dm_os_sys_info", sqlConn); - cpuInfoCmd.CommandTimeout = 30; - var cpuCountObj = await cpuInfoCmd.ExecuteScalarAsync(); - var coreLicenseCount = cpuCountObj != null ? Convert.ToInt32(cpuCountObj) : 0; - if (coreLicenseCount > 0) + if (tdeDbNames.Count == 0) + { + recommendations.Add(new RecommendationRow + { + Category = "Licensing", + Severity = "High", + Confidence = "High", + Finding = "Enterprise Edition with no Enterprise-only features detected", + Detail = "No databases use Transparent Data Encryption (TDE), the only feature " + + "still restricted to Enterprise Edition since SQL Server 2016 SP1. " + + "Review whether Standard Edition would meet workload requirements for potential license savings.", + EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null + }); + } + else { - var monthlySavings = coreLicenseCount * 5000m / 12m; recommendations.Add(new RecommendationRow { Category = "Licensing", Severity = "Low", - Confidence = "Low", - Finding = $"Enterprise to Standard would save ~${monthlySavings:N0}/mo at list pricing ({coreLicenseCount} cores)", - Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " + - "Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers.", - EstMonthlySavings = monthlySavings + Confidence = "High", + Finding = "TDE in use — Enterprise Edition downgrade blocker", + Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" + + (tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") + + ". TDE must be removed before downgrading to Standard Edition." }); + + // Check 10: License cost impact estimate (only when features ARE in use) + using var cpuInfoCmd = new SqlCommand( + "SELECT cpu_count FROM sys.dm_os_sys_info", sqlConn); + cpuInfoCmd.CommandTimeout = 30; + var cpuCountObj = await cpuInfoCmd.ExecuteScalarAsync(); + var coreLicenseCount = cpuCountObj != null ? Convert.ToInt32(cpuCountObj) : 0; + if (coreLicenseCount > 0) + { + var monthlySavings = coreLicenseCount * 5000m / 12m; + recommendations.Add(new RecommendationRow + { + Category = "Licensing", + Severity = "Low", + Confidence = "Low", + Finding = $"Enterprise to Standard would save ~${monthlySavings:N0}/mo at list pricing ({coreLicenseCount} cores)", + Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " + + "Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers.", + EstMonthlySavings = monthlySavings + }); + } } } }