namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
///
/// S0 主数据维度清单 —— **本模块唯一的业务知识落点**。
///
/// 新增一个维度 = 在此追加一个 ,
/// 不得在 Materializer / Reconciler 中出现按 Key 分支的逻辑。
///
/// 🔴 JsonPath 一律是**源表物理列名**,不是实体属性名。改动前先核对 SHOW CREATE TABLE。
///
public static class S0DimCatalog
{
/// 本库样板源(mdp_source.source_code),入站为只读 SELECT。
public const string LocalSourceSystem = "AIDOPDEV_MYSQL";
///
/// 工作中心。源 WorkCtrMaster(无 company/factory 列,故 dim 也不设 —— 不为统一而发明数据)。
///
public static readonly S0DimDefinition WorkCenter = new(
Key: "WORK_CENTER",
EntityCode: "S0_WORK_CENTER",
SourceTable: "WorkCtrMaster",
SourceSystem: LocalSourceSystem,
StagingTable: "mdp_stg_s0_work_center",
DimTable: "dim_work_center",
BusinessKeyColumns: ["tenant_id", "domain_code", "work_center_code"],
SourceBizKeyColumns: ["Domain", "WorkCtr"],
Columns:
[
new("tenant_id", S0DimValueKind.TenantIdColumn),
new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
new("work_center_code", S0DimValueKind.Str, "WorkCtr", Required: true),
new("work_center_name", S0DimValueKind.Str, "Descr"),
new("department_code", S0DimValueKind.Str, "Department"),
new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
]);
///
/// 部门。源 DepartmentMaster。
/// company/factory 仅作属性透传:实测同一 (tenant, Domain) 下 factory 同时存在 legacy 数值与 SysOrg 雪花两种值,
/// 与身份正交,本阶段不做组织域治理。
///
public static readonly S0DimDefinition Department = new(
Key: "DEPARTMENT",
EntityCode: "S0_DEPARTMENT",
SourceTable: "DepartmentMaster",
SourceSystem: LocalSourceSystem,
StagingTable: "mdp_stg_s0_department",
DimTable: "dim_department",
BusinessKeyColumns: ["tenant_id", "domain_code", "department_code"],
SourceBizKeyColumns: ["Domain", "Department"],
Columns:
[
new("tenant_id", S0DimValueKind.TenantIdColumn),
new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
new("department_code", S0DimValueKind.Str, "Department", Required: true),
new("department_name", S0DimValueKind.Str, "Descr"),
new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
// 源侧 238/239 非空:UATDEMO 的「未分配」行 UpdateTime 恒 NULL。
// 本 definition 走 FULL(mdp_entity.incr_column = NULL),不生成 "UpdateTime > cursor" 谓词,
// 因此该行每轮都能进 stg 与 dim;切 INCR 前必须先落 keyset NullTimePhase 两阶段。
new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
]);
///
/// 库位。源 LocationMaster —— **dual-column 复刻表**。
///
/// 🔴 以下 JsonPath 全部是 PascalCase 真列,同表存在同名 snake_case 影子列且**恒空/已发散**,
/// 取错即静默得到空值:
/// Domain(真) vs domain_code(132 行全空串) ·
/// IsActive(真) vs is_active(132 行中仅 1 行与真列相等) ·
/// PhysicalAddress(42 非空) vs physical_address(0 非空) ·
/// CreateUser(119 非空) vs create_user(0 非空) ·
/// RecID(真主键) vs rec_id(恒 0)。
///
/// 身份取 (tenant, domain_code, location_code):与子表父引用一致,且在源侧索引口径变化时仍成立;
/// 另用 镜像当前源 UK (tenant_id, location)。
///
public static readonly S0DimDefinition Location = new(
Key: "LOCATION",
EntityCode: "S0_LOCATION",
SourceTable: "LocationMaster",
SourceSystem: LocalSourceSystem,
StagingTable: "mdp_stg_s0_location",
DimTable: "dim_location",
BusinessKeyColumns: ["tenant_id", "domain_code", "location_code"],
SourceBizKeyColumns: ["Domain", "location"],
MirrorUniqueColumns: ["tenant_id", "location_code"],
Columns:
[
new("tenant_id", S0DimValueKind.TenantIdColumn),
new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
new("location_code", S0DimValueKind.Str, "location", Required: true),
new("location_name", S0DimValueKind.Str, "descr"),
new("location_type", S0DimValueKind.Str, "typed"),
new("storer", S0DimValueKind.Str, "storer"),
new("physical_address", S0DimValueKind.Str, "PhysicalAddress"),
new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
]);
///
/// 货架(库位子维度)。源 LocationShelfMaster。
///
/// 与 共用 mdp_stg_s0_location,靠 source_table 隔离,
/// 两者是**两个独立 definition**,绝不合并成一次转换。
///
/// 本表**全列 snake_case,没有影子列** —— 不可套用 LocationMaster 的禁用规则。
/// 另:源 update_time 0/30 非空(无水位)、主键 rec_id 不在通用行标识候选列表中
/// 且每次父表 FULL Replace 都会变,故物化只认 sync_batch_id,不依赖 source_row_id 稳定性。
/// 无 is_active —— 源表没有该列,不发明。
///
public static readonly S0DimDefinition LocationShelf = new(
Key: "LOCATION_SHELF",
EntityCode: "S0_LOCATION_SHELF",
SourceTable: "LocationShelfMaster",
SourceSystem: LocalSourceSystem,
StagingTable: "mdp_stg_s0_location",
DimTable: "dim_location_shelf",
BusinessKeyColumns: ["tenant_id", "domain_code", "location_code", "shelf_code"],
SourceBizKeyColumns: ["domain_code", "location", "inv_shelf"],
ParentDimTable: "dim_location",
ParentKeyColumns: ["domain_code", "location_code"],
Columns:
[
new("tenant_id", S0DimValueKind.TenantIdColumn),
new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
new("domain_code", S0DimValueKind.Str, "domain_code", Required: true),
new("location_code", S0DimValueKind.Str, "location", Required: true),
new("shelf_code", S0DimValueKind.Str, "inv_shelf", Required: true),
new("shelf_name", S0DimValueKind.Str, "descr"),
new("area", S0DimValueKind.Str, "area"),
// 源恒 NULL(0/30),保留列以便源将来启用;不得为了“看起来有值”而回填
new("source_updated_at", S0DimValueKind.DateTimeSec, "update_time")
]);
/// 全部维度,按依赖顺序(父在前,子在后 —— 孤儿判定需要父维度先就绪)。
public static readonly IReadOnlyList All = [WorkCenter, Department, Location, LocationShelf];
/// 按 Key 取维度(大小写不敏感);不存在返回 null。
public static S0DimDefinition? Find(string? key) =>
string.IsNullOrWhiteSpace(key)
? null
: All.FirstOrDefault(d => string.Equals(d.Key, key.Trim(), StringComparison.OrdinalIgnoreCase));
/// 校验全部声明;任一不合法即抛出(供启动自检与单测使用)。
public static void ValidateAll()
{
foreach (var d in All) d.Validate();
var dupDim = All.GroupBy(d => d.DimTable, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
if (dupDim is not null)
throw new InvalidOperationException(
$"dim 表被多个 definition 共用:{dupDim.Key} —— FULL Replace 按 tenant 整表删除,会互相清空");
var dupEntity = All.GroupBy(d => d.EntityCode, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
if (dupEntity is not null)
throw new InvalidOperationException($"entity_code 重复:{dupEntity.Key}");
// 同一 staging 被多个 definition 共用时,source_table 必须互异,否则 purge/物化会互相污染
foreach (var g in All.GroupBy(d => d.StagingTable, StringComparer.Ordinal).Where(g => g.Count() > 1))
{
var tables = g.Select(d => d.SourceTable).ToList();
if (tables.Distinct(StringComparer.Ordinal).Count() != tables.Count)
throw new InvalidOperationException($"staging {g.Key} 被多个 definition 共用且 source_table 重复");
}
}
}