|
|
@@ -161,7 +161,7 @@ public sealed class MdpDbPullExecutor : IMdpSourcePullExecutor, ITransient
|
|
|
var scopeMismatch = IsScopeMismatch(
|
|
|
table.Rows.Count, ctx, tenantCol, factoryCol,
|
|
|
sourceHasRowsOutsideScope: ShouldProbeScope(table.Rows.Count, ctx, tenantCol, factoryCol)
|
|
|
- && await SourceHasRowsOutsideScopeAsync(scope, entity, ctx, isSqlServer, useKeyset, parameters, cancellationToken));
|
|
|
+ && await SourceHasRowsOutsideScopeAsync(scope, entity, ctx, isSqlServer, useKeyset, parameters, tenantCol, cancellationToken));
|
|
|
|
|
|
var diagnostic = scopeMismatch
|
|
|
? $"{ScopeMismatchCode}: 源表 {entity.SourceTableName} 在 tenantId={ctx.TenantId}/factoryId={ctx.FactoryId} 作用域外仍有数据,"
|
|
|
@@ -211,13 +211,14 @@ public sealed class MdpDbPullExecutor : IMdpSourcePullExecutor, ITransient
|
|
|
private static async Task<bool> SourceHasRowsOutsideScopeAsync(
|
|
|
ISqlSugarClient scope, MdpEntity entity, MdpPullContext ctx,
|
|
|
bool isSqlServer, bool useKeyset, List<SugarParameter> parameters,
|
|
|
- CancellationToken cancellationToken)
|
|
|
+ string? tenantCol, CancellationToken cancellationToken)
|
|
|
{
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
try
|
|
|
{
|
|
|
- var probeSql = BuildScopeProbeSql(entity, isSqlServer, ctx, useKeyset);
|
|
|
- var probe = await scope.Ado.GetDataTableAsync(probeSql, StripScopeParameters(parameters));
|
|
|
+ var keepTenant = !string.IsNullOrWhiteSpace(tenantCol);
|
|
|
+ var probeSql = BuildScopeProbeSql(entity, isSqlServer, ctx, useKeyset, tenantCol);
|
|
|
+ var probe = await scope.Ado.GetDataTableAsync(probeSql, StripScopeParameters(parameters, keepTenant));
|
|
|
return probe.Rows.Count > 0;
|
|
|
}
|
|
|
catch
|
|
|
@@ -227,17 +228,26 @@ public sealed class MdpDbPullExecutor : IMdpSourcePullExecutor, ITransient
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>探针复用正式查询的参数,但必须剔除作用域参数(探针 SQL 不再引用它们)。</summary>
|
|
|
- internal static List<SugarParameter> StripScopeParameters(List<SugarParameter> parameters)
|
|
|
+ /// <summary>
|
|
|
+ /// 探针复用正式查询的参数,剔除探针 SQL 不再引用的作用域参数。
|
|
|
+ /// <para><paramref name="keepTenantParameter"/> 必须与
|
|
|
+ /// <see cref="BuildScopeProbeSql"/> 是否发射了租户谓词保持一致:
|
|
|
+ /// 探针带租户谓词时要留下 <c>@scopeTenantId</c>,否则参数会缺失;
|
|
|
+ /// 不带时要剔除,否则会多绑一个 SQL 里不存在的参数。</para>
|
|
|
+ /// <para><c>@scopeFactoryId</c> 恒剔除 —— 探针的全部意义就是"去掉工厂口径再看一眼"。</para>
|
|
|
+ /// </summary>
|
|
|
+ internal static List<SugarParameter> StripScopeParameters(
|
|
|
+ List<SugarParameter> parameters, bool keepTenantParameter = false)
|
|
|
=> parameters
|
|
|
- .Where(p => !IsScopeParameter(p.ParameterName))
|
|
|
+ .Where(p => !IsStrippedScopeParameter(p.ParameterName, keepTenantParameter))
|
|
|
.ToList();
|
|
|
|
|
|
- private static bool IsScopeParameter(string? name)
|
|
|
+ private static bool IsStrippedScopeParameter(string? name, bool keepTenantParameter)
|
|
|
{
|
|
|
var n = (name ?? "").TrimStart('@', ':', '?');
|
|
|
- return string.Equals(n, "scopeTenantId", StringComparison.OrdinalIgnoreCase)
|
|
|
- || string.Equals(n, "scopeFactoryId", StringComparison.OrdinalIgnoreCase);
|
|
|
+ if (string.Equals(n, "scopeFactoryId", StringComparison.OrdinalIgnoreCase)) return true;
|
|
|
+ return !keepTenantParameter
|
|
|
+ && string.Equals(n, "scopeTenantId", StringComparison.OrdinalIgnoreCase);
|
|
|
}
|
|
|
|
|
|
private static string RequireSourceTable(MdpEntity entity)
|
|
|
@@ -361,17 +371,30 @@ public sealed class MdpDbPullExecutor : IMdpSourcePullExecutor, ITransient
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 作用域探针:与正式查询完全同一组增量/窗口/keyset 谓词,唯独**去掉**租户/工厂作用域谓词。
|
|
|
- /// 正式查询 0 行而探针有行 ⇒ 差异只可能来自作用域过滤,可据此判定"全量被作用域过滤掉"。
|
|
|
+ /// 作用域探针:与正式查询同一组增量/窗口/keyset 谓词,**保留租户谓词、只去掉工厂谓词**。
|
|
|
+ /// 正式查询 0 行而探针有行 ⇒ 差异只可能来自工厂口径,可据此判定"本租户的数据被工厂过滤掉了"。
|
|
|
/// 只做存在性判断(TOP 1 / LIMIT 1),不取数据。
|
|
|
+ ///
|
|
|
+ /// <para><b>租户谓词必须保留</b>。此前两个作用域谓词一起剥掉,探针退化成
|
|
|
+ /// <c>SELECT 1 FROM 源表 LIMIT 1</c> —— 在多租户共享同一张源表的模型下,
|
|
|
+ /// 它对**任何**租户都会命中别的租户的行,于是每一个合法空租户都被误报成 SCOPE_MISMATCH。
|
|
|
+ /// 实测该误报在共享库上累计 291 条,且全部落在源表本就 0 行的租户上,
|
|
|
+ /// 而真正被工厂谓词滤空的那两个租户反而一条都没报出来 —— 信号方向是反的。</para>
|
|
|
+ ///
|
|
|
+ /// <para>探针要回答的是「同一租户内,是否因工厂口径而取空」,
|
|
|
+ /// 不是「整张表有没有数据」。丢掉租户维度,这个问题就问不出来了。</para>
|
|
|
/// </summary>
|
|
|
- internal static string BuildScopeProbeSql(MdpEntity entity, bool isSqlServer, MdpPullContext ctx, bool useKeyset)
|
|
|
+ internal static string BuildScopeProbeSql(
|
|
|
+ MdpEntity entity, bool isSqlServer, MdpPullContext ctx, bool useKeyset, string? tenantCol = null)
|
|
|
{
|
|
|
var table = RequireSourceTable(entity);
|
|
|
var predicates = useKeyset
|
|
|
? BuildKeysetPredicates(isSqlServer, ctx, out _)
|
|
|
: BuildIncrementalPredicates(entity, isSqlServer, ctx, out _);
|
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(tenantCol))
|
|
|
+ predicates.Add($"{QuoteIdent(tenantCol, isSqlServer)} = @scopeTenantId");
|
|
|
+
|
|
|
var where = predicates.Count > 0 ? " WHERE " + string.Join(" AND ", predicates) : "";
|
|
|
return isSqlServer
|
|
|
? $"SELECT TOP 1 1 AS probe FROM {table}{where}"
|