using System.Text.Json; using System.Text.RegularExpressions; using SQLInsight.Core.Instances; namespace SQLInsight.Collectors.Tests; /// /// SQL Server 2005 cannot run in CI (no container image), and its T-SQL lacks much of 2008's. Every collector that can /// run on 2005 (all its required capabilities exist from 9.0) is checked for 2008+ syntax in its SQL, and so is every /// community rule that does not ask for 10.0 or later. A collector with version branches must keep 2008+ syntax in the /// branch for newer versions; those few are listed with what they gate. /// public sealed partial class Sql2005CompatibilityTests { /// 2008+ constructs that make a whole batch fail on 2005, whatever branch it was meant for. private static readonly (string Name, Regex Pattern)[] Sql2008Only = [ ("DECLARE med startværdi", new Regex(@"DECLARE\s+@\w+\s+\w+(\s*\([\d\s,]+\))?\s*=", RegexOptions.IgnoreCase)), ("+= / -= på variabel", new Regex(@"@\w+\s*[+\-*/]=", RegexOptions.IgnoreCase)), ("MERGE", new Regex(@"\bMERGE\s", RegexOptions.IgnoreCase)), ("CONCAT/IIF/CHOOSE/FORMAT/TRY_*", new Regex(@"\b(CONCAT|IIF|CHOOSE|FORMAT|TRY_CONVERT|TRY_CAST|TRY_PARSE|EOMONTH|DATEFROMPARTS|STRING_AGG|STRING_SPLIT)\s*\(", RegexOptions.IgnoreCase)), ("OFFSET … FETCH", new Regex(@"\bOFFSET\s+\S+\s+ROWS\b", RegexOptions.IgnoreCase)), ("date/time/datetime2/datetimeoffset", new Regex(@"\bAS\s+(date|time|datetime2|datetimeoffset)\b|\b(datetime2|datetimeoffset)\s*\(", RegexOptions.IgnoreCase)), ("VALUES med flere rækker", new Regex(@"\bVALUES\s*\([^)]*\)\s*,\s*\(", RegexOptions.IgnoreCase)), ]; /// /// Collectors that run on 2005 but deliberately contain 2008+ syntax in a branch that 2005 never takes. /// Only patterns from listed here are tolerated, and only in that file. /// private static readonly Dictionary VersionBranched = new(StringComparer.Ordinal) { // Query Store queries only run when Capability.QueryStore is present (2016+). ["QueryStoreCollector"] = ["DECLARE med startværdi", "date/time/datetime2/datetimeoffset"], }; [Fact] public void Collectors_that_run_on_sql_server_2005_use_only_2005_syntax() { var problems = new List(); foreach (var collector in BuiltInCollectors.Create()) { var runsOn2005 = collector.RequiredCapabilities.All(c => InstanceCapabilities.MinimumVersionFor(c) is not { Major: > 9 }); if (!runsOn2005) { continue; } var name = collector.GetType().Name; var source = SourceOf(name); VersionBranched.TryGetValue(name, out var tolerated); foreach (Match literal in RawString().Matches(source)) { foreach (var (pattern, regex) in Sql2008Only) { if (regex.IsMatch(literal.Value) && tolerated?.Contains(pattern) != true) { problems.Add($"{name}: {pattern}"); } } } } Assert.True(problems.Count == 0, "SQL Server 2008+-syntaks i SQL, der også køres på 2005: " + string.Join("; ", problems.Distinct())); } [Fact] public void Community_rules_for_sql_server_2005_use_only_2005_syntax() { var problems = new List(); foreach (var file in Directory.EnumerateFiles(Path.Combine(RepositoryRoot(), "rules", "community"), "*.json")) { using var document = JsonDocument.Parse(File.ReadAllText(file), new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); var rules = document.RootElement.ValueKind == JsonValueKind.Array ? document.RootElement.EnumerateArray().ToList() : [document.RootElement]; foreach (var rule in rules) { var minimum = rule.TryGetProperty("minimumVersion", out var v) ? v.GetString() : "9.0"; if (SqlServerVersion.Parse(minimum + ".0.0").Major > 9) { continue; } var query = rule.GetProperty("query").GetString()!; problems.AddRange(Sql2008Only.Where(p => p.Pattern.IsMatch(query)).Select(p => $"{rule.GetProperty("id").GetString()}: {p.Name}")); } } Assert.True(problems.Count == 0, "SQL Server 2008+-syntaks i regler uden minimumVersion 10.0: " + string.Join("; ", problems)); } [Fact] public void Plan_analysis_and_top_sql_identify_statements_without_query_hash_on_2005() { var sql2005 = new[] { Performance.PlanAnalysisCollector.BuildSql(memoryColumns: false, queryHash: false), Performance.QueryStatsCollector.SqlFor(SqlServerVersion.Parse("9.0.5057.0"), new DateTime(2026, 10, 1)), }; Assert.All(sql2005, sql => Assert.DoesNotContain("query_hash", sql, StringComparison.OrdinalIgnoreCase)); Assert.All(sql2005, sql => Assert.Contains("sys.fn_varbintohexstr(", sql, StringComparison.Ordinal)); Assert.Contains("query_hash", Performance.PlanAnalysisCollector.BuildSql(memoryColumns: true), StringComparison.Ordinal); } [Theory] [InlineData("DECLARE @since int = CONVERT(int, 1);", true)] [InlineData("DECLARE @x nvarchar(60) = N'a';", true)] [InlineData("DECLARE @since int;\nSET @since = 1;", false)] [InlineData("SET @n += 1", true)] [InlineData("SELECT CONVERT(datetime, '2026-10-01', 126)", false)] public void The_check_itself_recognises_2008_syntax(string sql, bool expected) => Assert.Equal(expected, Sql2008Only.Any(p => p.Pattern.IsMatch(sql))); private static string SourceOf(string typeName) { var folder = Path.Combine(RepositoryRoot(), "src", "SQLInsight.Collectors"); var file = Directory.EnumerateFiles(folder, typeName + ".cs", SearchOption.AllDirectories).SingleOrDefault(); return file is null ? throw new InvalidOperationException($"{typeName}.cs ikke fundet") : File.ReadAllText(file); } private static string RepositoryRoot() { var root = AppContext.BaseDirectory; while (!File.Exists(Path.Combine(root, "SQLInsight.slnx"))) { root = Path.GetDirectoryName(root) ?? throw new InvalidOperationException("SQLInsight.slnx ikke fundet."); } return root; } // C# raw string literals ("""…""" and $"""…"""): where the collectors keep their SQL. [GeneratedRegex("\"\"\"(.*?)\"\"\"", RegexOptions.Singleline)] private static partial Regex RawString(); }