Seen on a customer's SQL Server 2005 SP4 (legacy ODBC): - agent-jobs declared @since with an initial value (2008+): "Incorrect syntax near CONVERT" and SQLI-JOB-001/002 never ran. Now DECLARE; SET. - plan-analysis used query_hash (2008+) and failed every 30 minutes for SQLI-PLAN-001/002/003. On 2005 a statement is now identified by sql_handle + offsets, as top SQL does. It needs sys.dm_exec_text_query_plan, so the new capability TextQueryPlan (2005 SP2, 9.0.3042) is required. - COM-DB-001 used OBJECT_SCHEMA_NAME (2005 SP2+); now joins sys.objects and sys.schemas. Audit of all collector SQL and community rules without minimumVersion 10.0: the rest is 2005 syntax or behind a version/capability check. Sql2005CompatibilityTests (2005 cannot run in CI): every collector that can run on 2005 is scanned for 2008+ syntax in its SQL (inline DECLARE, +=, MERGE, CONCAT/IIF/FORMAT/TRY_*, OFFSET/FETCH, date/time types, multi-row VALUES), version-branched files listed explicitly; the same for community rules; and the 2005 variants of plan analysis and top SQL must not use query_hash. Verified to fail on the old agent-jobs SQL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
135 lines
6.6 KiB
C#
135 lines
6.6 KiB
C#
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using SQLInsight.Core.Instances;
|
|
|
|
namespace SQLInsight.Collectors.Tests;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed partial class Sql2005CompatibilityTests
|
|
{
|
|
/// <summary>2008+ constructs that make a whole batch fail on 2005, whatever branch it was meant for.</summary>
|
|
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)),
|
|
];
|
|
|
|
/// <summary>
|
|
/// Collectors that run on 2005 but deliberately contain 2008+ syntax in a branch that 2005 never takes.
|
|
/// Only patterns from <see cref="Sql2008Only"/> listed here are tolerated, and only in that file.
|
|
/// </summary>
|
|
private static readonly Dictionary<string, string[]> 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<string>();
|
|
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<string>();
|
|
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();
|
|
}
|