maui314159/navigatsql
GitHub: maui314159/navigatsql
一款 .NET 静态分析工具,利用 ScriptDom 和 Roslyn 提取 T-SQL 存储过程与 C# 代码之间的跨层数据流依赖关系,输出为知识图谱边数据。
Stars: 0 | Forks: 0
# navigaT-SQL
[](https://www.nuget.org/packages/navigatsql)
[](https://github.com/maui314159/navigatsql/actions/workflows/ci.yml)
[](LICENSE)
一个小巧的 .NET 工具,它使用 Microsoft 的 **ScriptDom** 解析 **T-SQL**,并提取代码库中存储过程与表以及彼此之间的 **资源 / 数据流关系** —— 这是调用图和 tree-sitter 工具未建模的层级。
它以图 **edges** 的形式发出关系,并标记有 trusty-tools 可扩展知识图(`EdgeKind::Custom`)所接受的精确 `custom:` 字符串,因此输出可以直接被摄取,而无需在可扩展 edge 工作之外进行任何核心更改。
## 为什么会有这个项目
纵观成熟的代码智能工具,**没有人对跨层数据库依赖图进行建模**(已针对 GitNexus 和 codebase-memory-mcp 进行验证):GitNexus 根本没有独立的 SQL;codebase-memory-mcp 使用通用的 tree-sitter 语法解析 SQL,但**完全丢弃了存储过程**,并且没有 SQL 数据流 edges。tree-sitter SQL 语法对于真实的 T-SQL 来说太弱了——这就是为什么本项目使用 ScriptDom,它是 Microsoft MIT 许可的权威 T-SQL 解析器。
宿主语言(C#)端**已经由 trusty-search 索引**,因此真正全新的工作是 (1) T-SQL 关系图(本工具)和 (2) C# → proc 桥接(路线图)。
## 它提取的内容
对于每个 `CREATE` / `ALTER PROCEDURE`(proc 之外的语句归属于 ``):
| 关系 | T-SQL 构造 | Edge 标签 |
|---|---|---|
| `reads_table` | `FROM` / `JOIN` 源;`MERGE … USING` 源 | `custom:reads_table` |
| `writes_table` | `INSERT` / `UPDATE` / `DELETE` / `MERGE` 目标(`UPDATE p … FROM dbo.T AS p` 别名会被解析) | `custom:writes_table` |
| `calls_proc` | `EXEC procname` | `custom:calls_proc` |
| `calls_function` | 标量 UDF 调用(`dbo.fn_X(…)`);`FROM` 中的表值 UDF(`dbo.tvf_X(…)`)—— 跳过内置函数 | `custom:calls_function` |
| `references` | 外键(`CREATE`/`ALTER TABLE … FOREIGN KEY`)—— `table -> table` | `custom:references` |
| `dynamic_sql` | `sp_executesql`, `sp_execute`, `EXEC(@sql)` —— **被标记,绝不静默丢弃** | `custom:dynamic_sql` |
`CREATE`/`ALTER FUNCTION` 和 `CREATE`/`ALTER VIEW` 主体也是作用域(`FromKind: function` / `view`),因此它们自己的 `reads_table`/`writes_table` 会扩展传递的 `proc → function/view → table` 图(视图展开)。
**C# 传递 (Roslyn):** 对于每个 C# 成员,来自数据访问调用点处存储过程名字符串字面量(`usp_*`/`sp_*`/`qry_*`)的 `csharp_method --calls_proc--> proc`。将这些与 T-SQL 的 `proc -> table` edges 链接起来,可以重建任何 tree-sitter 工具都无法建模的跨层 **method -> proc -> table** 依赖。已在包含约 1,000 个存储过程的实际生产代码库上验证:**96%** 的 C# 存储过程引用解析为提取出的 proc;**882** 个 C# 方法可传递地触达 **3,010** 个不同的 method→table 对。
**嵌入式 SQL (Dapper / 原始查询):** 看起来像 SQL 的字符串字面量会通过 T-SQL 传递进行解析,其表读/写操作直接归属于外层的 C# 方法 —— `csharp_method → reads_table/writes_table → table`。这恢复了存在于 C# 字符串中,而不是 `.sql` 文件或存储过程调用中的数据访问(这是 Dapper 风格代码库中的主要模式)。
## 安装
navigaT-SQL 作为已发布的 [.NET 全局工具](https://www.nuget.org/packages/navigatsql) 发布,并提供了源码安装和独立运行选项。
### 从 NuGet 安装(推荐)
在任何装有 .NET 10 运行时的机器上:
```
dotnet tool install --global navigatsql
navigatsql --trusty-setup # how to wire into trusty-search
navigatsql --emit kggraph path/to/repo > repo-kggraph.json
```
使用 `dotnet tool update --global navigatsql` 更新,或使用 `dotnet tool uninstall --global navigatsql` 移除。该命令会存放在 `~/.dotnet/tools` 目录下;如果尚未添加到 `PATH` 中,请手动添加(`export PATH="$PATH:$HOME/.dotnet/tools"`)。
### 从源码安装
从克隆的代码中构建并安装 —— 需要 .NET 10 SDK:
```
git clone https://github.com/maui314159/navigatsql
cd navigatsql
dotnet pack -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg navigatsql
navigatsql --trusty-setup # how to wire into trusty-search
```
### 独立二进制文件(任意服务器,无需 .NET 运行时)
类似于 trusty-tools 的预编译 Homebrew 二进制文件。生成一个捆绑了运行时的独立可执行文件 —— 将其复制到任何具有相同 OS/架构的服务器上即可运行;**目标机器上无需安装 .NET**:
```
# 选择目标的 runtime identifier (RID):
# linux-x64 · linux-arm64 · win-x64 · osx-arm64 · osx-x64
dotnet publish -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true -o ./publish
# ./publish/navigatsql 是一个单文件 —— 将其放入服务器的 PATH 中:
scp ./publish/navigatsql server:/usr/local/bin/
ssh server navigatsql --trusty-setup # how to wire into trusty-search
ssh server navigatsql --emit kggraph /srv/app > app-kggraph.json
```
### 构建与测试(开发)
```
dotnet build # build the exe (net10.0), no install
dotnet test tests/NavigaTSql.Tests # full xUnit suite
dotnet run -- # run straight from the repo
```
`dotnet run -- `(本 README 中全程使用)与已安装的 `navigatsql ` 命令可以互换使用。请参阅 [CLAUDE.md](./CLAUDE.md#development-workflow) 了解完整的 CI 门禁工作流(格式化、警告视为错误、500 行代码上限)。
## 用法
```
dotnet run -- # single file
dotnet run -- path/to/repo # directory, recursive *.sql (T-SQL) + *.cs (C#)
dotnet run -- a.sql b.sql dir/ # any mix
# stdout 上输出干净的 JSON,stderr 上输出人类可读的摘要 —— 可以将 edges 通过管道传输到任何地方:
dotnet run -- ~/src/myapp > myapp-edges.json
# 将每个 .sql 文件的名称视为其 database context(对于 whole-database dumps,
# 例如 SALES_LIVE.sql -> sales_live.dbo.t);默认关闭:
dotnet run -- --db-from-filename path/to/db-dumps
# ingest wire shape(去重后的 nodes+edges+facts,ADR-0009):
dotnet run -- --emit kggraph path/to/repo > repo-kggraph.json
```
- **stdout** 输出纯净的 JSON(一个 edges 数组),因此可以直接管道传输到摄取步骤。
- **stderr** 输出摘要 + 每种关系的直方图。**动态 SQL 计数是判断静态提取器最多能看到代码库多少内容的唯一最重要指标**。
- **错误恢复:** 报告并跳过单个文件的解析错误(ScriptDom 的部分树仍会被挖掘)—— 一个损坏的脚本绝不会导致整个运行崩溃。完成扫描后退出代码为 `0`,仅在出现使用错误 / 未找到文件时为 `2`。
### 输出 schema
```
[
{
"File": "Procedures/usp_GetProperty.sql",
"From": "dbo.usp_GetProperty",
"FromKind": "proc",
"To": "dbo.Property",
"ToKind": "table",
"Relation": "reads_table",
"EdgeKindTag": "custom:reads_table"
}
]
```
`File` 是源路径(相对于工作目录),因此多文件输出仍可追溯。`EdgeKindTag` 是 trusty-tools 的摄取键。
### `--emit kggraph` —— 摄取文档(trusty-tools ADR-0009 线上格式)
默认的 `edges` 模式针对**每次出现**发出一条记录。`--emit kggraph` 则为 trusty-tools 的 `POST /indexes/{id}/graph` (ADR-0009) 发送去重后的 **摄取文档**:包含节点 + edges + facts,并遵循幂等契约:*节点即其 id;edge 即 `(from, to, relation)`*。
```
{
"schema": "navigatsql/kggraph@2",
"producer": "navigatsql", // required by the ingest endpoint (400 if missing)
"producerVersion": "0.1.0", // optional; this tool's assembly version
"gitSha": "dbfd800…", // optional; HEAD of the scanned tree (omitted off-repo)
"nodes": [ { "id": "dbo.property", "kind": "table" } ],
"edges": [
{
"from": "dbo.usp_GetProperty", "fromKind": "proc",
"to": "dbo.property", "toKind": "table",
"kind": "reads", // coarse #817-aligned kind
"relation": "reads_table", // fine-grained original
"tag": "custom:reads_table", // Custom(String) escape-hatch key
"provenance": ["Procedures/usp_GetProperty.sql"],
"linkedServer": "srv" // only when referenced via [srv].db.dbo.t
}
],
"facts": [
{ "subject": "dbo.usp_X", "predicate": "dynamic_sql", "object": "",
"provenance": ["Procedures/usp_X.sql"] }
]
}
```
- **生产者信封:** `producer` 是 trusty-search ADR-0009 端点要求的常量 `"navigatsql"`;`producerVersion` 和 `gitSha` 是可选的,并遵循 `linkedServer` 约定(驼峰命名,为 null 时省略)。`gitSha` 是扫描树的 HEAD(`git -C rev-parse HEAD`),能够进行低成本的陈旧度检查(叠加 SHA 与仓库 HEAD 对比)。它在每次运行中**仅解析一次**,绝不是每个文件解析一次,并且在 git 仓库之外会退化为省略(绝不会报错)。跨越多个仓库的**多根扫描**会省略 `gitSha` 而不是靠猜;共享同一个 HEAD 的多个根会保留它。`schema` 为 `@2`,因为信封添加了始终存在的 `producer` 字段 —— 此更改是向后兼容的(`@1` 消费者仍可解析它),但版本号的提升让消费者能够区分带信封的输出与信封化之前的输出。
- **去重 + 溯源:** 来自多个文件的相同 `(from, to, relation)` 断言会折叠成一条 edge,其 `provenance` 会列出每个源文件。
- **Facts 拆分:** `dynamic_sql` 标记不是 node→node 关系 —— 它们作为 FactStore 形状的三元组发出,且绝不生成 `` 节点。
- **确定性:** 输出在所有地方都按序号排序,信封不包含时间戳或机器名;相同的输入(以任何顺序、同一棵树)会产生逐字节相同的文档,因此重新摄取是一个无操作的合并。
## 喂养图(trusty-tools 集成)
`--emit kggraph` 不仅仅是一种转储格式 —— 它是 trusty-search 贡献图端点的**原生摄取体**。没有转换器,也没有垫片:该文档直接按原样 POST。使用哪条路径仅取决于你是否运行了 trusty-search。
### 1. 摄取到 trusty-search 中(预定路径)
trusty-search 公开了 `POST /indexes/{id}/graph` —— 这是一个持久的**贡献图叠加层**,能够在重启*和*重新索引后存活,并独立于块派生图进行存储。navigaT-SQL 的 `--emit kggraph` 文档正是该端点的线上格式,因此你可以直接将其通过管道喂给端点:
```
# daemon 默认地址为 http://127.0.0.1:7878;{id} 是一个现有的 index id
# (使用 trusty-search `list_indexes` MCP 工具,或通过 GET /indexes 来列出它们)。
ID=my-index
navigatsql --emit kggraph path/to/repo \
| curl -sS -X POST "http://127.0.0.1:7878/indexes/$ID/graph" \
-H 'content-type: application/json' --data-binary @-
```
JSON 响应会报告已落地的数据以及合并后的图统计总数:
```
{ "producer": "navigatsql", "replaced": true,
"nodes_received": 412, "edges_received": 3010,
"graph_nodes": 18233, "graph_edges": 51904,
"unknown_edge_tags_dropped": 0 }
```
- **按生产者替换。** 每次摄取都会以原子方式替换 navigaT-SQL 对该索引的*先前*贡献 —— 因此从代码库中删除的表/proc 绝不会留下过期的 edges;只需在源代码更改后重新运行即可。索引的派生图和任何其他生产者的贡献均不受影响。
- **身份是自包含的。** 贡献的规范 ID(`db.schema.table`、schema 限定的例程、`Class.Method`)作为它们自己的节点存储,绝不会合并到 trusty-search 的裸名称代码符号中,因此无论宿主图如何,跨层的 `method → proc → table` 链都能保持可靠。
- **schema 仅被记录而不被强制执行** —— `navigatsql/kggraph@2` 可以正常摄取;字段集即契约。端点会读取 `nodes` + `edges` 并忽略 `facts` 数组(见下文)。错误:如果 `producer` 为空则返回 `400`,如果索引 id 未知则返回 `404`。
然后使用 `GET /indexes/{id}/graph/neighbors`(`node`,`direction=in|out|both`,`edge_kinds=writes,reads,…`,`max_hops=1..4`)或 trusty-search 的 `search_kg` MCP 工具遍历合并后的图 —— 例如,*“什么写入表 X”* 对应的查询是 `?node=db.dbo.x&direction=in&edge_kinds=writes`。
**动态 SQL facts(可选)。** 图端点会忽略 `facts` 数组 —— 动态 SQL 标记不是 node→node edges。如果你希望它们可查询,请通过 trusty-analyze 的 `upsert_fact` MCP 工具将每个 `(subject, predicate, object)` 三元组加载到其 FactStore 中。这是可选的充实;没有它图也是完整的。
### 2. 独立(无 trusty-search)
`--emit edges`(按出现次数)和 `--emit kggraph`(去重后)都是 stdout 上的纯 JSON。直接使用它们 —— 加载到任何图/存储中,对比两次运行,或使用 `jq` 查询。`EdgeKindTag`(`custom:`)是你在其他地方摄取时的稳定密钥。
一个封装了上述 `curl` 命令的一键式 `navigatsql push` 子命令是目前路线图上仅有的待开发便利功能;线上契约本身已经完成并在今日投入使用。
## 范围与限制(实事求是)
会被标记出来,而不是静默解决:
- **动态 SQL** (`sp_executesql` / `EXEC(@sql)`) —— 被检测并计数,但不进行解析。
- **CTE / 临时表 / 表变量** 名称显示为其别名,而不是底层表。
- `FROM` 中的 **视图 (Views)** 作为表读取发出,而不进行递归展开。
- **数据库上下文 (Database context)** 来自 `USE ` 语句,或者 —— 仅当使用 `--db-from-filename` 时 —— 来自每个 `.sql` 文件的名称。如果两者都没有,1/2 部分名称将被规范化为 `schema.table`(没有数据库限定符),而不是靠猜测。
## 测试
```
dotnet test tests/NavigaTSql.Tests
```
201 个 xUnit 测试覆盖了 T-SQL 传递(读/写/调用/UDF//动态/FK)、规范化表标识 + 噪声排除、C# proc 桥接 + 嵌入式 SQL、EF 提取器以及 kggraph 输出(去重/溯源/facts/确定性)。CI 在每次 push 时运行它们(`.github/workflows/ci.yml`)。
## 路线图
**已构建:** 文件 SQL、嵌入式 SQL (Dapper) 和 EF/ORM(`--ef`)提取;规范化的表标识 + 噪声排除;C#→proc 桥接。
**剩余工作:**
- 针对未声明 FK 的数据库进行 **JOIN 推断关系**([#4](https://github.com/maui314159/navigatsql/issues/4))。
- 在 kggraph 中**构建概念节点**(`construct:*` + `custom:uses_*`),以便“X 在哪里使用?”成为一次调用图查询([#5](https://github.com/maui314159/navigatsql/issues/5))。
- **列级血缘** —— 下一个高天花板目标。
- 强化启发式规则:插值/拼接的 SQL、`.Set()`、Fluent-API `HasForeignKey`。
- 一个封装了摄取 `curl` 命令的一键式 **`push` 子命令**。线上契约已经完成并投入运行:`--emit kggraph` 按原样 POST 到 trusty-search 的 `POST /indexes/{id}/graph` 端点(ADR-0009,位于 trusty-search ≥ 0.24.5,通过 [trusty-tools#1129](https://github.com/bobmatnyc/trusty-tools/pull/1129) 实现)—— 参见[喂养图](#feeding-the-graph-trusty-tools-integration)。该子命令只是在已记录管道之上提供的纯粹便利。
## 许可证
MIT —— 参见 [LICENSE](./LICENSE)。根据部署情况可选:没有 T-SQL 的代码库永远不会运行它。
标签:T-SQL, 云安全监控, 代码分析, 凭证管理, 多人体追踪, 静态分析