Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/designs/graph-table-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- 支持有向图和无向图的基础建模。
- 在生成期发现重复节点、非法边引用和不符合项目规则的环。
- 生成强类型图查询 API,支持节点集合、入边、出边、关联边、前驱、后继、两点边、路径和遍历。
- 通过注册 parser、validator、writer 和模板实现,不在 `DataTableProcessor` 主流程中硬编码 graph 分支。
- 通过统一表类型描述符注册 parser、materializer、validator、serialization plan 和模板,不在 `DataTableProcessor` 主流程中硬编码 graph 分支。

## 推荐 Excel 格式

Expand Down Expand Up @@ -77,6 +77,8 @@ DTLevelGraph.TryGetEdge(context, "E001", out var edge);
- 项目声明为无环图时检测到环。
- 业务字段或 JSON payload 不能按声明类型解析。

普通导出与 `ValidateOnly` 会基于同一份 materialized snapshot 执行这些生成期规则;校验模式不会写出代码、bytes 或 manifest。

### 可以警告

- 节点没有任何入边或出边。
Expand Down Expand Up @@ -124,8 +126,8 @@ DTLevelGraph.TryGetEdge(context, "E001", out var edge);
## 实现步骤建议

1. 已新增 `GraphTableParser`,基于普通行表解析边列表并注册内建索引。
2. 已新增生成期行校验,检查 `EdgeId`、`From`、`To` 和可选 `Weight`。
3. 已复用普通表二进制写出流程,运行时通过内建索引构建邻接查询。
2. 已新增独立的生成期行验证阶段,检查 `EdgeId`、`From`、`To` 和可选 `Weight`。
3. 已复用通用 materialized snapshot 和 serialization plan,运行时通过内建索引构建邻接查询。
4. 已新增 graph 代码生成模板,输出节点集合、边、邻接、前驱/后继、两点边、路径和 BFS 遍历 API。
5. 已为 `DTGen=graph` 注册 parser 和模板。
6. 已增加 parser 与模板测试;后续可继续补充节点 Sheet、环检测和路径 API 测试。
Expand Down
23 changes: 13 additions & 10 deletions docs/designs/tree-table-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
## 目标

- 用表格方式表达稳定的父子层级关系。
- 在生成期发现缺失必需字段、非法字段类型和普通行表可发现的数据错误;孤儿节点、循环引用和非法根节点校验可作为后续增强
- 在生成期发现缺失必需字段、非法字段类型、孤儿节点、循环引用和普通行表可发现的数据错误
- 生成强类型查询 API,支持按节点、父节点、子节点和根节点查询。
- 支持客户端或服务端通过预热提前加载树表;完整结构校验可作为后续增强
- 通过已注册的 parser 和模板实现,不在 `DataTableProcessor` 主流程中硬编码 tree 分支。
- 支持客户端或服务端通过预热提前加载树表;完整父子引用与循环校验在写出前完成
- 通过统一表类型描述符注册 parser、materializer、validator、serialization plan 和模板,不在 `DataTableProcessor` 主流程中硬编码 tree 分支。

## 推荐 Excel 格式

Expand All @@ -22,7 +22,7 @@
推荐约定:

- `Id` 必填、唯一,并且作为树节点主键。
- `ParentId` 为空表示根节点;当前解析器会按 `ParentId` 建立分组索引,但不在解析阶段检查非空 `ParentId` 是否引用已存在的 `Id`。
- `ParentId` 为空表示根节点;生成期关系校验会检查非空 `ParentId` 是否引用已存在的 `Id`。
- `Order` 可选;字段类型留空时会默认为 `int`。当前生成的 `GetChildren` 返回分组索引顺序,不额外按 `Order` 排序。
- `Name`、`Type`、`Payload` 为业务字段,可以按项目需要扩展。
- `Comment` 可选,只用于说明,不进入运行时 payload。
Expand Down Expand Up @@ -68,16 +68,19 @@ foreach (var node in DTQuestTree.TraverseDepthFirst(context, "Root"))

- 缺少 `Id` 或 `ParentId` 字段。
- `Id` 或 `ParentId` 显式声明为非 `string` 类型。
- `Id` 重复会通过内建唯一索引写出/加载流程报错。
- `Id` 为空或重复。
- 非空 `ParentId` 引用不存在。
- 检测到父子循环引用,并输出完整循环路径。
- `Order` 存在且值不能按 `int` 或显式声明类型解析。
- 业务字段不能按声明类型解析。

### 后续可增强

- 校验非空 `ParentId` 是否引用已存在的 `Id`。
- 校验循环引用和根节点数量。
- 校验根节点数量。
- 对同一父节点下重复 `Order`、过深节点等输出项目级警告。

普通导出与 `ValidateOnly` 消费同一份 materialized snapshot,并执行相同的 tree 验证阶段;`ValidateOnly` 不通过模拟文件写出来触发校验,也不会产生任何代码或数据文件。

## 诊断信息

后续结构化诊断可进一步包含:
Expand Down Expand Up @@ -113,9 +116,9 @@ foreach (var node in DTQuestTree.TraverseDepthFirst(context, "Root"))

## 后续实现建议

1. 新增更完整的 `TreeTableValidator`,实现引用完整性、循环检测和项目级根节点规则
2. 如有需要,新增 tree serialization plan 或 writer,写出节点数据和关系索引。
3. 增加正常树、多根树、孤儿节点、重复节点和循环引用测试
1. 增加项目级根节点数量、同级 `Order` 和最大深度规则,并通过审计模式评估兼容性
2. 如有需要,新增 tree 专用 serialization plan,写出节点数据和关系索引。
3. 继续补充正常树、多根树、孤儿节点、重复节点和循环引用的端到端 fixture

## 非目标

Expand Down
2 changes: 2 additions & 0 deletions docs/guides/table-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

当前代码中的默认解析器注册了 `table`、`matrix`、`column`、`kv`、`tree` 和 `graph`;代码模板也为这些类型提供生成器。

所有已支持类型都通过同一份表类型描述符组合 parser、数据 materializer、分阶段 validator、serialization plan 和代码 renderer。普通导出与 `ValidateOnly` 会读取并转换同一份不可变数据快照;`ValidateOnly` 会执行数据类型以及 tree/graph 结构校验,但不会写出代码、bytes 或增量 manifest。诊断 metrics 会记录 snapshot 的逻辑行数和近似字节量,供大型 matrix 或全量校验评估内存预算。

所有类型的生成静态查询都是 context-first。默认表把 `DataTableContext` 放在第一参数;命名表把名称放在第二参数:

```csharp
Expand Down

This file was deleted.

20 changes: 13 additions & 7 deletions src/DataTables.GeneratorCore/DataTableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ namespace DataTables.GeneratorCore;

public sealed class DataTableGenerator
{
private static readonly CodeTemplateRendererRegistry s_CodeTemplateRendererRegistry = CodeTemplateRendererRegistry.CreateDefault();

private readonly ConcurrentDictionary<string, object> m_Locks;

public DataTableGenerator()
Expand Down Expand Up @@ -265,7 +263,7 @@ await Parallel.ForEachAsync(filePaths, (pair, _) =>

foreach (var m in metricsList)
{
logger($" - [{m.File}/{m.Sheet}] Ignored={m.IgnoredFieldCount}, TagFiltered={m.TagFilteredFieldCount}, SkippedCols={m.SkippedColumnCount}, MatrixSkipped={m.MatrixDefaultSkippedCount}, ParseMs={m.ParseElapsedMs}, GenMs={m.GenerateElapsedMs}");
logger($" - [{m.File}/{m.Sheet}] Ignored={m.IgnoredFieldCount}, TagFiltered={m.TagFilteredFieldCount}, SkippedCols={m.SkippedColumnCount}, MatrixSkipped={m.MatrixDefaultSkippedCount}, SnapshotRows={m.SnapshotRowCount}, SnapshotBytes~={m.SnapshotEstimatedBytes}, ParseMs={m.ParseElapsedMs}, GenMs={m.GenerateElapsedMs}");
}
}

Expand Down Expand Up @@ -688,6 +686,14 @@ private async Task GenerateExcel(string filePath, string usingNamespace, string
continue;
}

// 数据只读取和类型转换一次。ValidateOnly 与实际导出共享同一份规范快照和验证阶段。
var dataSnapshot = processor.MaterializeData(sheet);
if (!processor.ValidateData(dataSnapshot))
{
context.Failed = true;
continue;
}

var codeContent = renderCode ? RenderCodeFile(context) : null;
var codeContentHash = codeContent == null ? null : ComputeContentHash(codeContent);
if (!outputClaims.TryReserve(context, filePath, codeContentHash, out var writeCode, out var conflict))
Expand All @@ -705,10 +711,10 @@ private async Task GenerateExcel(string filePath, string usingNamespace, string
GenerateCodeFile(context, codeOutputDir, finalCodeOutputDir, codeContent!, forceOverwrite, logger);
}

// 生成数据文件。ValidateOnly 模式只解析、校验并渲染代码模板,不写任何产物
// 生成数据文件。ValidateOnly 已完成相同的数据快照验证,但不写任何产物
if (!validateOnly)
{
processor.GenerateDataFile(dataOutputDir, finalDataOutputDir, forceOverwrite, sheet, logger);
processor.GenerateDataFile(dataOutputDir, finalDataOutputDir, forceOverwrite, dataSnapshot, logger);
}

if (context.Failed)
Expand Down Expand Up @@ -766,12 +772,12 @@ private static bool ValidSheet(ISheet? sheet)

private static string RenderCodeFile(GenerationContext context)
{
if (!s_CodeTemplateRendererRegistry.TryGetRenderer(context.DataSetType, out var renderer))
if (!TableTypeDescriptorRegistry.Default.TryGetDescriptor(context.DataSetType, out var descriptor))
{
throw new InvalidOperationException($"未注册 DTGen={context.DataSetType} 的代码生成模板。");
}

return renderer.TransformText(context);
return descriptor.CodeRenderer.TransformText(context);
}

void GenerateCodeFile(GenerationContext context, string outputDir, string comparisonOutputDir, string content, bool forceOverwrite, ILogger logger)
Expand Down
105 changes: 42 additions & 63 deletions src/DataTables.GeneratorCore/DataTableProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using NPOI.SS.UserModel;

namespace DataTables.GeneratorCore;
Expand Down Expand Up @@ -62,43 +61,6 @@ public void CreateGenerationContext(ISheet sheet)
m_FirstDataRowIndex = new TableSchemaService(m_Context, m_Options, m_Diagnostics).CreateGenerationContext(sheet);
}

// 是否有效行
private bool IgnoreDataRow(IRow? row)
{
if (row == null || row.FirstCellNum < 0)
{
return false;
}

// 此行是否被注释
foreach (var field in m_Context.Fields)
{
if (!field.IsComment)
{
continue;
}

var cellString = GetCellString(row.GetCell(field.Index));
if (!string.IsNullOrEmpty(cellString) && cellString == "#")
{
return false;
}
}

// 是否空行
foreach (var field in m_Context.Fields)
{
if (field.IsIgnore) { continue; }

if (!string.IsNullOrEmpty(GetCellString(row.GetCell(field.Index))))
{
return true;
}
}

return false;
}

private string GetCellString(ICell? cell)
{
if (cell == null)
Expand Down Expand Up @@ -199,16 +161,54 @@ private void ValidateFormulaCellString(ICell cell, string value)
}


internal void GenerateDataFile(string outputDir, string comparisonOutputDir, bool forceOverwrite, ISheet sheet, ILogger logger)
internal TableDataSnapshot MaterializeData(ISheet sheet)
{
new DataTableBinaryWriter(m_Context, WriteDataRows).GenerateDataFile(outputDir, comparisonOutputDir, forceOverwrite, sheet, logger);
var descriptor = GetTableTypeDescriptor();
var snapshot = descriptor.DataMaterializer.Materialize(new TableDataMaterializationContext(
m_Context,
sheet,
m_FirstDataRowIndex,
m_Diagnostics,
GetCellString));
var metrics = m_Diagnostics.GetMetrics(m_Context.FileName, m_Context.SheetName);
metrics.SnapshotRowCount = snapshot.Rows.Count;
metrics.SnapshotEstimatedBytes = snapshot.EstimatedSizeInBytes;
return snapshot;
}

internal int WriteDataRows(ISheet sheet, BinaryWriter writer)
internal bool ValidateData(TableDataSnapshot snapshot)
{
return new DataRowBinarySerializer(m_Context, m_FirstDataRowIndex, m_Diagnostics, GetCellString, IgnoreDataRow).WriteDataRows(sheet, writer);
var descriptor = GetTableTypeDescriptor();
var validationContext = new TableDataValidationContext(m_Context, snapshot, m_Diagnostics);
var errorCount = m_Diagnostics.ErrorCount;
foreach (var validationPass in descriptor.ValidationPasses)
{
validationPass.Validate(validationContext);
if (m_Diagnostics.ErrorCount > errorCount)
{
return false;
}
}

return true;
}

internal void GenerateDataFile(string outputDir, string comparisonOutputDir, bool forceOverwrite, TableDataSnapshot snapshot, ILogger logger)
{
var descriptor = GetTableTypeDescriptor();
new DataTableBinaryWriter(m_Context, descriptor.SerializationPlan)
.GenerateDataFile(outputDir, comparisonOutputDir, forceOverwrite, snapshot, logger);
}

private TableTypeDescriptor GetTableTypeDescriptor()
{
if (!TableTypeDescriptorRegistry.Default.TryGetDescriptor(m_Context.DataSetType, out var descriptor))
{
throw new InvalidOperationException($"未注册 DTGen={m_Context.DataSetType} 的表类型描述符。");
}

return descriptor;
}

internal static DataProcessor GetDataProcessorForSerialization(string type)
{
Expand Down Expand Up @@ -243,27 +243,6 @@ public static string GetLanguageValue(XField field, string text)
return DataProcessorUtility.GetDataProcessor(field.TypeName).GenerateTypeValue(text);
}

private static string GetRowColString(int row, int col)
{
return string.Format("{0}{1}", ConvertToDigit(col), row + 1);
}

private static string ConvertToDigit(int num)
{
if (num < 0)
{
throw new ArgumentOutOfRangeException(nameof(num));
}
else if (num < 26)
{
return Convert.ToString((char)('A' + num));
}
else
{
return ConvertToDigit(num / 26 - 1) + ConvertToDigit(num % 26);
}
}

/// <summary>
/// 计算7BitEncodedInt编码后的字节数
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;

namespace DataTables.GeneratorCore;

Expand Down Expand Up @@ -34,6 +35,8 @@ public sealed class DiagnosticsCollector

public IReadOnlyList<Diagnostic> Items => m_Diagnostics;

public int ErrorCount => m_Diagnostics.Count(item => item.Severity == DiagnosticSeverity.Error);

public DiagnosticsMetrics GetMetrics(string file, string sheet)
{
var key = file + "|" + sheet;
Expand Down Expand Up @@ -74,6 +77,8 @@ public DiagnosticsMetrics(string file, string sheet)
public int TagFilteredFieldCount { get; set; }
public int SkippedColumnCount { get; set; }
public int MatrixDefaultSkippedCount { get; set; }
public int SnapshotRowCount { get; set; }
public long SnapshotEstimatedBytes { get; set; }
public long ParseElapsedMs { get; set; }
public long GenerateElapsedMs { get; set; }
}
Expand Down
10 changes: 5 additions & 5 deletions src/DataTables.GeneratorCore/Schema/TableSchemaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace DataTables.GeneratorCore;

public sealed class TableSchemaService : ITableSchemaService
{
private static readonly ITableSchemaParserRegistry s_TableSchemaParserRegistry = TableSchemaParserRegistry.CreateDefault();
private static readonly TableTypeDescriptorRegistry s_TableTypeDescriptorRegistry = TableTypeDescriptorRegistry.Default;

private readonly GenerationContext m_Context;
private readonly ParseOptions m_Options;
Expand Down Expand Up @@ -60,10 +60,10 @@ public int CreateGenerationContext(ISheet sheet)
}
}

if (!s_TableSchemaParserRegistry.TryGetParser(m_Context.DataSetType, out var parser))
if (!s_TableTypeDescriptorRegistry.TryGetDescriptor(m_Context.DataSetType, out var descriptor))
{
var supportedTypes = string.Join(", ", s_TableSchemaParserRegistry.SupportedDataSetTypes);
var reservedTypes = string.Join(", ", s_TableSchemaParserRegistry.ReservedDataSetTypes);
var supportedTypes = string.Join(", ", s_TableTypeDescriptorRegistry.SupportedDataSetTypes);
var reservedTypes = string.Join(", ", s_TableTypeDescriptorRegistry.ReservedDataSetTypes);
m_Diagnostics.Error(
m_Context.FileName,
m_Context.SheetName,
Expand All @@ -72,7 +72,7 @@ public int CreateGenerationContext(ISheet sheet)
return -1;
}

int nextIndex = parser.Parse(new NpoiSheetReader(sheet), m_Context, m_Options, m_Diagnostics);
int nextIndex = descriptor.SchemaParser.Parse(new NpoiSheetReader(sheet), m_Context, m_Options, m_Diagnostics);
if (nextIndex == -1)
{
m_Diagnostics.Warn(m_Context.FileName, m_Context.SheetName, string.Empty, "表头或字段信息不完整,解析终止");
Expand Down
Loading
Loading