# mysql-lineage-parser **Repository Path**: jjfinal/mysql-lineage-parser ## Basic Information - **Project Name**: mysql-lineage-parser - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-30 - **Last Updated**: 2026-09-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # mysql-lineage-parser > **MySQL 列级数据血缘解析器,不依赖 Hadoop,可嵌入 Spring Boot。** > 解析任意 MySQL SQL 脚本,告诉你每个输出字段到底来自哪些物理表、哪些字段——即使穿过嵌套子查询和多级临时表。 [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) [![Java](https://img.shields.io/badge/Java-8%2B-orange.svg)](https://www.oracle.com/java/) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-2.6.13-brightgreen.svg)](https://spring.io/projects/spring-boot) [![Druid](https://img.shields.io/badge/Druid-1.2.20-red.svg)](https://github.com/alibaba/druid) **[English README](README-en.md)** | **[技术博客(深度解读)](docs/intro-article-v2.md)** --- ## 为什么做这个项目? 现有 SQL 血缘工具通常有两个痛点: 1. **依赖太重**——绑定 Hive / Hadoop 生态,仅仅为了解析 SQL 就得搭一整套 metastore 2. **解析太浅**——血缘停在派生表别名或临时表上,给你的结果是 `tmp1.col` 而不是真实的物理源列 **mysql-lineage-parser** 走了另一条路: - ✅ **轻量**:只依赖 Alibaba Druid SQL Parser + MySQL,笔记本就能跑 - ✅ **深度穿透**:穿透嵌套派生表、标量子查询、多级 `CREATE TEMPORARY TABLE` 链,一路追到物理源列 - ✅ **Spring Boot 原生**:`@Autowired` 即用,无外部服务、无 metastore - ✅ **Java 生态**:在 Python 工具(sqllineage、OpenLineage)难以嵌入的场景填补空白 --- ## 30 秒看效果 ### 场景一:嵌套派生表,血缘不卡在中间别名 ```sql SELECT x FROM (SELECT a AS x FROM (SELECT id AS a FROM user_profile) inner_sub) outer_sub; ``` 很多工具到这里就停了,告诉你 `x` 来自 `a` 就算完事。这个解析器会穿透两层派生表别名,直接给出物理源: ``` - x derived from user_profile.id via: direct mapping ``` ### 场景二:标量子查询也能穿透 ```sql SELECT id, (SELECT name FROM t2 WHERE t2.id = t1.id) AS t2_name FROM t1; ``` ``` - id derived from t1.id via: direct mapping - t2_name derived from t2.name via: scalar subquery ``` `t2_name` 没有停留在"标量子查询表达式"这个黑盒上,而是解析到了 `t2.name`。 ### 场景三:多语句脚本——临时表链自动追到物理源 数仓 ETL 的经典场景: ```sql CREATE TEMPORARY TABLE tmp_user AS SELECT id AS uid, name FROM users; CREATE TEMPORARY TABLE tmp_final AS SELECT uid, name FROM tmp_user; SELECT uid, name FROM tmp_final; ``` 末尾 SELECT 的列沿临时表链一路回溯,结果里**完全没有 `tmp_user`、`tmp_final` 的影子**: ``` Input Tables: [default.users] - uid derived from default.users.id via: direct mapping - name derived from default.users.name via: direct mapping ``` --- ## 和同类工具对比 | 特性 | sqllineage (Python) | OpenLineage | DataHub | **mysql-lineage-parser** | |---|---|---|---|---| | **语言/生态** | Python | 多语言 SDK | Python | **Java / Spring Boot** | | **部署依赖** | 仅 sqlparse | 需 Marquez 后端 | 完整平台 | **仅 Druid,零外部服务** | | **临时表穿透** | ❌ 停在临时表 | ❌ | ❌ | ✅ 追到物理源表 | | **子查询穿透** | 部分支持 | 部分支持 | 依赖执行日志 | ✅ 完整支持 | | **Spring Boot 集成** | 难 | 难 | 难 | **原生 `@Autowired`** | | **适合场景** | 通用 SQL 分析 | 企业级血缘采集 | 数据目录平台 | **轻量嵌入、字段级审计原型** | **定位很清晰**:不是替代它们,而是**填补空白**——在 Java 技术栈里,想要一个**轻量、可嵌入、能穿透临时表**的列级血缘工具,目前没有太好的选择。 --- ## 核心功能 ### 表级血缘 自动识别 SQL 的输入表 / 输出表,覆盖 `FROM`、各种 `JOIN`、`UNION`,以及嵌套子查询里层的物理表。 ### 列级血缘 - **源列 → 输出列映射**:完整支持表别名 + 列别名解析 - **表达式与函数跟踪**:`salary * 1.1`、`CONCAT(...)`、`SUM(amount)`——直接映射的列标记为 `direct mapping` - **过滤条件记录**:`WHERE` / `HAVING` 条件关联到对应列的 lineage ### 派生表穿透(核心能力) 血缘不会停在派生表的输出别名上,而是逐层往回追到物理源列。支持: - 多层嵌套派生表 - 派生表 + JOIN + 聚合同时穿透 - 标量子查询穿透(`SELECT (SELECT ...) AS c`) ### 多语句脚本血缘(临时表穿透) 面向"一段含多条语句的脚本"——典型场景是 `CREATE TEMPORARY TABLE ... AS SELECT ...` 链 + 末尾 SELECT。末尾 SELECT 的列级 lineage 沿临时表链一路往回追溯,只暴露物理源表和源列。临时表不出现在 `inputTables` 里。 ### 通配符列展开 `SELECT *` / `t.*` / `db.table.*`——元数据可用时展开为物理表的所有列;不可用时保留 `*` 本身。 ### HTTP API 一个 POST 接口暴露脚本血缘能力: ```bash curl -X POST http://localhost:8080/api/lineage/script \ -H 'Content-Type: application/json' \ -d '{"sql":"CREATE TEMPORARY TABLE t1 AS SELECT id AS uid FROM users;\nSELECT uid FROM t1"}' ``` 响应: ```json { "code": 200, "message": "success", "data": { "inputTables": ["default.users"], "outputTables": [], "columnLineages": [ { "outputColumn": "uid", "sourceColumns": [{"table": "default.users", "columnName": "id"}], "transformation": "direct mapping" } ] } } ``` --- ## 快速开始 ```bash git clone https://github.com/zzydonteatsweet/mysql-lineage-parser.git cd mysql-lineage-parser mvn test -Dtest=TempTableScriptParserTest ``` 先跑核心能力的测试(临时表脚本穿透),确认能构建。 ### Java API(Spring Boot 内) ```java @Autowired private SqlLineageParser parser; // 单语句 LineageResult r1 = parser.parse("SELECT u.id, u.name FROM users u"); // 多语句脚本(临时表穿透) String script = "CREATE TEMPORARY TABLE t1 AS SELECT id AS uid FROM users;\n" + "SELECT uid FROM t1"; LineageResult r2 = parser.parseScript(script, ParseContext.createDefault()); System.out.println(r2.generateReport()); ``` 输出: ``` === SQL Lineage Report === SQL Type: UNKNOWN Input Tables: [default.users] Output Tables: [] Column Lineages: - uid derived from default.users.id via: direct mapping ``` ### 独立使用(不依赖 Spring) ```java List strategies = Arrays.asList( new CreateTableParseStrategy(), new SelectParseStrategy(null) // null 元数据 = 纯 AST 解析 ); SqlLineageParser parser = new MysqlLineageParserImpl(new ParseStrategyFactory(strategies)); LineageResult result = parser.parse("SELECT u.id, u.name FROM users u"); System.out.println(result.generateReport()); ``` --- ## 架构 ```mermaid flowchart TB subgraph Input["输入 SQL 脚本"] SQL["CREATE TEMPORARY TABLE t1 AS SELECT id AS uid FROM users;
CREATE TEMPORARY TABLE t2 AS SELECT uid FROM t1;
SELECT uid FROM t2;"] end subgraph Parse["解析层 (Druid SQL Parser)"] AST["MySQL AST
(抽象语法树)"] end subgraph Strategy["策略分发层"] CreateStrategy["CreateTableParseStrategy
处理 CREATE TABLE"] SelectStrategy["SelectParseStrategy
处理 SELECT"] TempScript["TempTableScriptParser
编排多语句脚本"] end subgraph Visitor["Visitor 访问者层"] CreateVisitor["CreateLineageVisitor
提取表元数据"] SelectVisitor["SelectLineageVisitor
提取列级血缘
(FROM → SELECT → WHERE)"] end subgraph Core["核心穿透机制"] Scope["QueryScopeCache
作用域缓存
(子查询穿透)"] Registry["TempTableRegistry
临时表注册表
(跨语句穿透)"] MetaSupport["TableMetaSupport
物理元数据
(SELECT * 展开)"] end subgraph Output["输出结果"] LineageResult["LineageResult
━━━━━━━━━━━━
inputTables: [users]
columnLineages:
uid → users.id"] end SQL --> AST AST --> TempScript TempScript --> CreateStrategy TempScript --> SelectStrategy CreateStrategy --> CreateVisitor SelectStrategy --> SelectVisitor SelectVisitor --> Scope SelectVisitor --> Registry SelectVisitor --> MetaSupport TempScript -.注册临时表.-> Registry Registry -.穿透复用.-> Scope Scope --> LineageResult Registry --> LineageResult style Input fill:#E6F1FB,stroke:#378ADD style Parse fill:#FAEEDA,stroke:#BA7517 style Strategy fill:#FBEAF0,stroke:#D4537E style Visitor fill:#EEEDFE,stroke:#7F77DD style Core fill:#E1F5EE,stroke:#1D9E75 style Output fill:#EAF3DE,stroke:#639922 ``` ### 两个真正的技术难点 用 Visitor 遍历 AST 本身不难,真正难的是这两个: #### 难点一:列引用是局部解析的,血缘却要全局追溯 SQL 里 `t.col` 的 `t` 只在当前查询块内有意义,但血缘要求把每个输出列一路追到物理源表。你不能看到 `t.col` 就记 `t.col`——`t` 很可能是派生表或子查询,`col` 背后还套着好几层。 **解法:作用域(Scope)** 每个查询块都是一个作用域,列引用只在本块内解析。进入内层查询块时,把外层缓存快照、清空,让内层用自己的,用完再恢复: ```java Map outer = new LinkedHashMap<>(tableSourceCacheMap); tableSourceCacheMap.clear(); // ... 解析内层查询块 ... tableSourceCacheMap.clear(); tableSourceCacheMap.putAll(outer); // 恢复外层缓存 ``` 穿透的关键:**每一层登记的都是"已到物理"的 lineage**。父作用域引用 `sub.col` 时,拿到的不是"派生表的输出别名",而是内层已经追到物理列的 lineage——再把它的 `sourceColumns` 直接 flatten 过来。N 层嵌套就是 N 次 flatten,链式一路拼到物理列。 #### 难点二:跨语句复用列血缘 临时表脚本是多条语句,每条独立解析,但下游 SELECT 用到 `t1`/`t2` 时得知道这些临时表的每一列到底追到了哪。 **解法:临时表注册表(TempTableRegistry)** 每条 `CREATE TEMPORARY TABLE` 解析出的 scope(其 `outputColumnMap` 即"该临时表每一列追到的物理源列")按表名登记: ```java registry.register(tempTableName, scope); ``` 最后那条 SELECT 的 visitor 注入共享的 registry,`FROM` 命中已登记的临时表名时,不当物理表处理,而是把缓存的 scope 注入进来复用列血缘: ```java if (registry.contains(tableInfo.getTableName())) { QueryScopeCache tempScope = registry.lookup(tableInfo.getTableName()); cache.copyOutputColumnsFrom(tempScope); } ``` 下游引用临时表的列时,走的是和"作用域"完全一样的穿透路径——只不过这个 scope 不是当场解析出来的,而是从 registry 里取的。 --- ## 当前局限(诚实说明) **INSERT / UPDATE / DELETE** 三类语句目前是占位策略、尚未实现——这不是"失败的测试",而是"还没写的功能"。 核心能力——子查询穿透、临时表脚本血缘、HTTP 接口——的测试全部通过,可安全用于生产环境的 SELECT 血缘分析。 --- ## 适用场景 这个工具解决的是一个窄而明确的问题:**给你一段 MySQL SQL,告诉你每列数据从哪里来、经过了什么操作。** 适合用在: - ETL 脚本的数据血缘审计 - 数据地图 / 数据目录的字段级溯源 - 数据质量影响分析(修改某张源表会影响哪些下游输出列) - 只是想理解一段复杂 SQL 做了什么 不适合做:实时血缘采集、Hive/SparkSQL 解析(虽然 Druid 支持多方言,但 Visitor 是针对 MySQL 语法树的)、大规模血缘图谱的存储和查询。 --- ## 技术栈 | 分类 | 选型 | |---|---| | 语言 / 运行时 | Java 8 | | 框架 | Spring Boot 2.6.13 | | 构建 | Maven | | SQL 解析 | Alibaba Druid SQL Parser 1.2.20 | | 元数据访问 | MyBatis Spring Boot Starter 2.2.2 + MySQL Connector/J | | 样板代码 | Lombok | | 静态检查 | PMD / SpotBugs / Checkstyle | --- ## 贡献 欢迎贡献!目前需要做的事: - `INSERT` / `UPDATE` / `DELETE` 策略实现(当前是占位) - `CASE WHEN` 列引用追踪 - `HAVING` 聚合表达式的过滤条件记录 - `WHERE` 子查询过滤跟踪(`EXISTS`、`NOT EXISTS`、关联 `IN`) 欢迎开 issue 讨论场景,或提交 PR。 --- ## 如果你感兴趣 - ⭐ **Star 这个仓库**支持一下 - 💬 开 issue 讨论你的使用场景 - 🔀 提交 PR 实现 INSERT / UPDATE / DELETE 策略 - 📖 阅读[技术博客(深度解读)](docs/intro-article-v2.md)了解完整设计思路 --- ## License Apache License 2.0