sqlinsight-pro/tests/SQLInsight.Collectors.Tests/DynamicsAxSqlTests.cs
Søren Eggert Lundsteen Olsen 6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
dynamics-ax (hourly) lists the monitored AX databases with version, schema,
companies and read access; discovery and the AX community rules (now marked
dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH
(3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run
history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and
SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads
run through [db].sys.sp_executesql under read committed with a 2 s lock
timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never
written. Blocking now captures CONTEXT_INFO for the AX user mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:53:34 +02:00

56 lines
3.2 KiB
C#

using SQLInsight.Core.DynamicsAx;
namespace SQLInsight.Collectors.Tests;
/// <summary>
/// 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.
/// </summary>
public sealed class DynamicsAxSqlTests
{
private static string Directory => Path.Combine(AppContext.BaseDirectory, "DynamicsAx");
public static TheoryData<string> 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);
Assert.DoesNotContain("PXI_", sql, StringComparison.OrdinalIgnoreCase);
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);
Assert.EndsWith("SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;", AxSql.InDatabase("AX", "SELECT 1"), StringComparison.Ordinal);
}
}