|
|
@@ -0,0 +1,114 @@
|
|
|
+using System.Reflection;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess.Providers;
|
|
|
+using Xunit;
|
|
|
+
|
|
|
+namespace Admin.NET.Test.S8;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// S8-RULE01-SQL-MAPPING-GUARD-1:SQL 结果别名与 POCO 属性的映射契约。
|
|
|
+///
|
|
|
+/// <b>为什么需要单独一个文件</b>:本仓 <c>DbSettings.EnableUnderLine = false</c>,
|
|
|
+/// SqlSugar 不做 snake_case → PascalCase 换算,<c>Ado.SqlQueryAsync<T></c> 按名字直接匹配。
|
|
|
+/// 别名一旦写成 snake_case,映射会**静默失败**:
|
|
|
+/// · 不抛异常
|
|
|
+/// · 行数照常返回
|
|
|
+/// · 但每个属性都是 null / default
|
|
|
+/// 于是 evaluator 在 <c>due == null</c> 处逐行跳过,命中恒为 0 —— 表面像"当前没有超期单",
|
|
|
+/// 实际是取数根本没落到对象上。本地全链实验中真实发生过一次(36 候选 / 0 命中)。
|
|
|
+///
|
|
|
+/// <b>为什么既有测试抓不到</b>:Provider 映射契约测试是直接 <c>new PurchaseDeliveryRow{...}</c>
|
|
|
+/// 再喂 evaluator,整条 SqlSugar 映射被绕开;E2E 测试用的 stub Provider 同理。
|
|
|
+/// 两者都在"映射之后"开始验证,因此本文件必须从 <b>SQL 文本本身</b>入手。
|
|
|
+///
|
|
|
+/// 不接 DB、不接 DI:解析 SQL 常量 + 反射比对属性名。
|
|
|
+/// </summary>
|
|
|
+public class S8PurchaseDeliverySqlMappingTests
|
|
|
+{
|
|
|
+ /// <summary>取 SELECT 列表里所有 <c>AS <alias></c> 的别名。</summary>
|
|
|
+ private static string[] SelectAliases() =>
|
|
|
+ Regex.Matches(S8PurchaseDeliveryDataProvider.Sql, @"\bAS\s+([A-Za-z_][A-Za-z0-9_]*)", RegexOptions.IgnoreCase)
|
|
|
+ .Select(m => m.Groups[1].Value)
|
|
|
+ .ToArray();
|
|
|
+
|
|
|
+ private static string[] RowProperties() =>
|
|
|
+ typeof(S8PurchaseDeliveryDataProvider.PurchaseDeliveryRow)
|
|
|
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
|
+ .Select(p => p.Name)
|
|
|
+ .ToArray();
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 每个 SQL 别名都必须**逐字**等于某个属性名。
|
|
|
+ /// 用 Ordinal 而非忽略大小写:SqlSugar 的匹配行为不该靠"大概能对上"来赌。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void EverySelectAlias_MapsToRowProperty()
|
|
|
+ {
|
|
|
+ var props = RowProperties();
|
|
|
+ var aliases = SelectAliases();
|
|
|
+
|
|
|
+ Assert.NotEmpty(aliases);
|
|
|
+
|
|
|
+ var unmapped = aliases.Where(a => !props.Contains(a, StringComparer.Ordinal)).ToArray();
|
|
|
+ Assert.True(unmapped.Length == 0,
|
|
|
+ "以下 SQL 别名在 PurchaseDeliveryRow 上找不到同名属性,SqlSugar 会静默映射为 null:\n "
|
|
|
+ + string.Join("\n ", unmapped));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 反向:每个属性都必须被某个别名覆盖。
|
|
|
+ /// 漏掉一个属性同样是静默 null —— 例如漏掉 DueAt 就直接导致命中恒为 0。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void EveryRowProperty_IsProducedBySql()
|
|
|
+ {
|
|
|
+ var aliases = SelectAliases();
|
|
|
+ var missing = RowProperties().Where(p => !aliases.Contains(p, StringComparer.Ordinal)).ToArray();
|
|
|
+
|
|
|
+ Assert.True(missing.Length == 0,
|
|
|
+ "以下属性没有对应的 SQL 别名,取数后将恒为 null:\n " + string.Join("\n ", missing));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 别名不得含下划线。EnableUnderLine=false 时 snake_case 别名等于"注定映射不上",
|
|
|
+ /// 而失败形态是静默的,所以这条要单独钉死,而不是依赖上面两条的间接覆盖。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void NoAlias_UsesSnakeCase()
|
|
|
+ {
|
|
|
+ var snake = SelectAliases().Where(a => a.Contains('_', StringComparison.Ordinal)).ToArray();
|
|
|
+
|
|
|
+ Assert.True(snake.Length == 0,
|
|
|
+ "本仓 EnableUnderLine=false,snake_case 别名会静默映射失败:\n " + string.Join("\n ", snake));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 驱动判定的四个 canonical 字段必须齐备。
|
|
|
+ /// 其余诊断列缺失只是 payload 少信息,这四个缺一个就直接改变判定结果。
|
|
|
+ /// </summary>
|
|
|
+ [Theory]
|
|
|
+ [InlineData("SourceObjectId")]
|
|
|
+ [InlineData("RelatedObjectCode")]
|
|
|
+ [InlineData("DueAt")]
|
|
|
+ [InlineData("Status")]
|
|
|
+ public void CanonicalDrivingField_IsSelected(string alias)
|
|
|
+ {
|
|
|
+ Assert.Contains(alias, SelectAliases(), StringComparer.Ordinal);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// WHERE / FROM 仍必须用数据库真实列名(snake_case)——
|
|
|
+ /// 别名只改输出侧,改错这一侧会直接让 SQL 报「Unknown column」。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void PhysicalColumnNames_StayUnchangedInPredicates()
|
|
|
+ {
|
|
|
+ var sql = S8PurchaseDeliveryDataProvider.Sql;
|
|
|
+
|
|
|
+ Assert.Contains("d.tenant_id = @TenantId", sql, StringComparison.Ordinal);
|
|
|
+ Assert.Contains("d.stat_date =", sql, StringComparison.Ordinal);
|
|
|
+ Assert.Contains("d.po_no <> ''", sql, StringComparison.Ordinal);
|
|
|
+ Assert.Contains("d.po_line <> ''", sql, StringComparison.Ordinal);
|
|
|
+ Assert.Contains("FROM dwd_supplier_delivery d", sql, StringComparison.Ordinal);
|
|
|
+ }
|
|
|
+}
|