using SQLInsight.Core.DynamicsAx; namespace SQLInsight.Collectors.Tests; /// /// The SQL the AX collectors send, for every version adapter with a schema file: SQL Server 2005 syntax, bounded, /// read-only, run in the AX database without changing the session's database, and never NOLOCK. /// public sealed class DynamicsAxSqlTests { private static string Directory => Path.Combine(AppContext.BaseDirectory, "DynamicsAx"); public static TheoryData Adapters() => [.. AxSchemaAdapter.All.Where(a => AxSchemaFile.Find(Directory, a.SchemaFileName) is not null).Select(a => a.SchemaFileName)]; private static (AxSchemaAdapter Adapter, AxDatabase Database) Load(string fileName) { var adapter = AxSchemaAdapter.All.First(a => a.SchemaFileName == fileName); var schema = AxSchemaFile.Find(Directory, fileName)!; var database = AxDetection.Recognise("AXDB", schema.DetectionRows(), ["AXDB", "AXDB_model"])!; return (adapter, database with { Isolation = AxReadIsolation.Snapshot }); } [Theory] [MemberData(nameof(Adapters))] public void Every_ax_query_is_bounded_read_only_sql_server_2005_syntax(string fileName) { var (adapter, database) = Load(fileName); var queries = new[] { adapter.BatchJobs(database), adapter.BatchHistory(database, null), adapter.BatchHistory(database, 123_456), adapter.NumberSequences(database), adapter.RecIds(database), AxSchemaAdapter.Companies(database), }.Where(q => q is not null).Select(q => q!).ToList(); Assert.All(queries, q => Assert.True(q.IsMeasurable, q.NotMeasurable)); foreach (var sql in queries.Select(q => AxSql.InDatabase(database.Database, q.Sql!, database.Isolation)).Append(AxSql.InDatabase("AXDB", AxDetection.MetadataSql))) { Assert.DoesNotContain(DiscoveryTests.Sql2008Only, p => p.IsMatch(sql)); Assert.DoesNotContain("NOLOCK", sql, StringComparison.OrdinalIgnoreCase); // Only AX's own standard tables, never anything else. var tables = System.Text.RegularExpressions.Regex.Matches(sql, @"\[(?:bmssa|dbo)\]\.\[(?[A-Z_]+)\]").Select(m => m.Groups["t"].Value).ToList(); Assert.All(tables, t => Assert.Contains(t, AxDetection.KnownTables)); Assert.DoesNotContain("USE ", sql, StringComparison.Ordinal); Assert.DoesNotMatch(@"\b(INSERT|UPDATE|DELETE|MERGE|TRUNCATE|ALTER|CREATE|DROP|EXEC\s*\()\b", sql.Replace("sp_executesql", string.Empty, StringComparison.Ordinal)); Assert.StartsWith("EXEC [AXDB].sys.sp_executesql", sql, StringComparison.Ordinal); Assert.Contains("SET LOCK_TIMEOUT 2000", sql, StringComparison.Ordinal); } Assert.All(queries, q => Assert.Contains("SELECT TOP (", q.Sql!, StringComparison.Ordinal)); } [Fact] public void Reads_use_snapshot_isolation_when_the_database_allows_it_and_read_committed_otherwise() { Assert.Contains("ISOLATION LEVEL SNAPSHOT", AxSql.InDatabase("AX", "SELECT 1", AxReadIsolation.Snapshot), StringComparison.Ordinal); Assert.Contains("ISOLATION LEVEL READ COMMITTED; SELECT 1", AxSql.InDatabase("AX", "SELECT 1", AxReadIsolation.ReadCommittedSnapshot), StringComparison.Ordinal); // Afterwards the agent's own session settings again, so the next collector on the shared connection keeps them. Assert.EndsWith("SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;", AxSql.InDatabase("AX", "SELECT 1"), StringComparison.Ordinal); Assert.Contains(Core.Instances.AgentSession.LockTimeout, Core.Instances.AgentSession.Settings, StringComparison.Ordinal); Assert.Contains(Core.Instances.AgentSession.Isolation, Core.Instances.AgentSession.Settings, StringComparison.Ordinal); } }