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();
}