MySQL Query Span Omni 迁移实战:从 ANTLR 解析树遍历到 omni MySQL AST 的无回退重构
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
本篇文章以 docs/plans/2026-04-27-mysql-query-span-omni-migration.md 为骨架,结合 backend/plugin/parser/mysql 目录下的实际实现与测试代码整理而成。该实现计划在仓库中标记为全部阶段已完成(Status: done),当前仓库状态即切机(cutover)后的最终形态。
导读
Query Span 是 Bytebase 在数据库治理场景中的核心抽象:给定一条 SQL 语句,它回答"这条语句访问了哪些表、哪些列、结果集由哪些来源列构成、语句属于哪种查询类型(SELECT/DML/DDL 等)",是脱敏(masking)、细粒度访问控制、SQL 审核与数据血缘分析的基础。本文以 MySQL 引擎的 Query Span 提取器从 ANTLR 解析树遍历迁移到 omni MySQL AST 的全过程为线索,完整讲解迁移目标、非协商约束、逐阶段的 TDD 执行方式、ANTLR 到 omni 的逐项映射、最终的提取器 API 形态以及严格的切机门禁,并结合当前仓库源码给出每一阶段的可验证证据。
读完本文,你将掌握:Bytebase MySQL Query Span 提取器的内部结构与调用链、omni AST 与 ANTLR 上下文在提取逻辑上的对应关系、如何用"探测测试 + 行为测试 + 全语料黄金测试"三层测试体系保障无回退迁移,以及 Bytebase 团队在重构解析器基础设施时遵循的工程纪律。
背景:为什么要把 Query Span 提取从 ANTLR 迁移到 omni
Query Span 是什么
在 backend/plugin/parser/base/span.go 中,QuerySpan结构体承载了语句的完整血缘信息:
Type:查询类型,区分纯查询(SELECT 家族)与 DML(改变表数据的语句)等;Results:结果列,每列带有来源列(SourceColumns)、是否为纯字段(IsPlainField)等信息;SourceColumns:整个语句(含 WHERE 条件、JOIN 条件等)涉及的全部来源列集合;PredicateColumns/PredicatePaths:谓词中涉及的列及文档数据库的字段路径;NotFoundError、FunctionNotSupportedError、UnresolvedColumnsError:错误恢复与脱敏相关语义。
提取器对外通过base.RegisterGetQuerySpan注册。在 query_span.go 中,MySQL、MariaDB、OceanBase 三个引擎共用同一套提取实现:
func init() { base.RegisterGetQuerySpan(storepb.Engine_MYSQL, GetQuerySpan) base.RegisterGetQuerySpan(storepb.Engine_MARIADB, GetQuerySpan) base.RegisterGetQuerySpan(storepb.Engine_OCEANBASE, GetQuerySpan) } func GetQuerySpan(ctx context.Context, gCtx base.GetQuerySpanContext, stmt base.Statement, database, _ string, ignoreCaseSensitive bool) (*base.QuerySpan, error) { q := newQuerySpanExtractor(database, gCtx, ignoreCaseSensitive) querySpan, err := q.getQuerySpan(ctx, stmt.Text) if err != nil { return nil, convertOmniError(err, stmt) } return querySpan, nil }为什么换掉 ANTLR 路径
原 MySQL Query Span 提取器建立在 ANTLR 语法解析产生的上下文对象(context)之上,通过一系列 Listener 遍历解析树:
queryTypeListener判定查询类型;selectOnlyListener判定是否纯 SELECT;accessTableListener收集访问表;resourceExtractListener提取表引用;recursiveCTEExtractListener处理递归 CTE。
ANTLR 解析树是扁平的、节点类型海量的语法树,提取逻辑必须处理大量上下文类型,遍历顺序也依赖语法的递归结构。Bytebase 正在推进的 omni 解析器(github.com/bytebase/omni/mysql/ast)提供了归一化、类型明确的 AST,更适合做结构化的血缘提取。
计划的迁移目标是明确的:把 MySQL Query Span 提取从 ANTLR 解析树遍历迁移到 omni MySQL AST,并且不做运行时回退(without runtime fallback)。注意计划明确排除了 PostgreSQL 的 catalog-analyzer 迁移模型——因为 MySQL 已经有一份手写的提取器,其行为可以直接平移映射到 omni AST 上,无需重新设计分析器。
技术栈与既有资产
- Go 语言;
- omni MySQL AST:
github.com/bytebase/omni/mysql/ast; - Query Span 数据模型:
backend/plugin/parser/base; - 既有 YAML 测试夹具:
backend/plugin/parser/mysql/test-data/query-span/,包含standard.yaml、query_type.yaml、case_insensitive.yaml、starrocks.yaml四个文件。
非协商约束:无回退、无开关、一次性切机
迁移计划开篇就用"Non-Negotiables"明确了红线,这是整个迁移工程纪律的核心,任何实现决策都不能违反:
- 无运行时特性开关(No runtime feature flag);
- 无环境切换(No environment switch);
- 切机后不允许从 omni 回退到 ANTLR(No fallback from omni to ANTLR after cutover);
- 不做部分生产 rollout(No partial production rollout);
- ANTLR 路径在切机前只允许作为开发参考存在;
- 只有当 omni 路径在当前 fixture 语料上达到零 parity diff 时才允许切机;
- 切机时既有 YAML fixture 期望保持不变。
这套约束意味着:omni 提取器必须先在包内(package-internal)以"影子实现"的方式与 ANTLR 提取器并行存在,通过三层测试证明行为等价,然后一次性替换公共入口,最后删除 ANTLR 相关代码。当前仓库状态已经完全符合这些约束——query_span_extractor.go 中的getQuerySpan已经直接委托给omniQuerySpanExtractor.getOmniQuerySpan,生产路径不再存在 ANTLR 分支。
最终形态:单一 omni 提取器与文件归属
计划用一张清晰的调用链图定义了最终实现形态:
GetQuerySpan -> newQuerySpanExtractor(...) -> q.getQuerySpan(ctx, stmt.Text) -> ParseMySQL(stmt) -> collectOmniAccessTables(root) -> isMixedQuery(...) -> classifyOmniQueryType(root, allSystems) -> if non-SELECT: return type + access tables -> extractFromSelectRoot(*ast.SelectStmt | *ast.TableStmt | *ast.ValuesStmt) -> processCTEs -> extractFromSetOp -> extractFromClause -> collect predicate/source tables as legacy behavior requires -> extractTargetList -> return QuerySpan对应到最终文件归属,当前仓库的实现与计划完全一致:
| 文件 | 职责 |
|---|---|
| query_span.go | 公共注册入口与GetQuerySpan包装,调用唯一的 omni 后端querySpanExtractor.getQuerySpan |
| query_span_extractor.go | 共享提取器状态与字符串/元数据辅助函数(getAllTableColumnSources、getFieldColumnSource、filterClusterName、findTableSchema、getColumnsForView、isMixedQuery、isSystemResource等),以及嵌入其中的omniQuerySpanExtractor |
| query_type.go | ANTLRqueryTypeListener被classifyOmniQueryType取代 |
| omni.go | ParseMySQL(基于mysqlparser.Parse)、GetOmniNode、ByteOffsetToRunePosition等 omni 基础设施 |
在 query_span_extractor.go 中可以看到切机后的合并结果:omniQuerySpanExtractor通过嵌入*querySpanExtractor复用共享的元数据与标识符解析辅助函数,querySpanExtractor.getQuerySpan则直接委托:
func (q *querySpanExtractor) getQuerySpan(ctx context.Context, stmt string) (*base.QuerySpan, error) { return (&omniQuerySpanExtractor{querySpanExtractor: q}).getOmniQuerySpan(ctx, stmt) }querySpanExtractor结构体的状态字段反映了血缘提取的核心作用域模型(query_span_extractor.go):
ctes:当前 SELECT 作用域可见的公共表表达式(PseudoTable 列表);outerTableSources:用于解析关联子查询列引用的外层表源;tableSourceFrom:当前 FROM 子句可见的表源集合;priorTableInFrom:用于解析JSON_TABLE文档表达式对前置 FROM 项的引用;viewResolutionStack:视图递归解析的防环栈。
覆盖率矩阵:fixture 语料是行为等价的最低门槛
迁移计划规定:当前 fixture 语料是最低门槛,实现每个 bucket 之前必须先为该 bucket 添加聚焦测试。完整覆盖率矩阵如下:
| Bucket | 要求的行为 |
|---|---|
| 查询类型 | SELECT、TABLE、VALUES、EXPLAIN、EXPLAIN ANALYZE SELECT、SHOW、SET、DDL、DML、全系统表 SELECT、遗留 DML 根(CALL、DO、HANDLER) |
| 访问表 | 顶层 FROM、连接表、派生表体表、CTE 体表、SELECT/WHERE 中的子查询、系统表抑制 |
| 目标列表 | 常量、裸列、限定列、别名、表达式名、*、table.* |
| 表达式 | 列引用、字面量、二元/一元运算、函数、聚合、CASE、CAST、BETWEEN、IN值列表、IN子查询、LIKE、IS、EXISTS、标量子查询、CONVERT、COLLATE、MATCH、ROW、MEMBER OF、INTERVAL、窗口参数 |
| FROM | TableRef、别名、逗号连接、JoinClause、ON、USING、嵌套连接 |
| 派生表 | 子查询表源、派生别名、派生列别名列表、嵌套派生表 |
| 集合运算 | UNION、UNION ALL、递归 CTE union、按位置合并源列 |
| CTE | 非递归 CTE、嵌套 CTE、显式 CTE 列列表、递归 CTE anchor/recursive 合并 |
| JSON_TABLE | JSON 表列、priorTableInFrom、来自 JSON 表达式属主的源血缘 |
| 视图 | getColumnsForView通过 omni 提取器递归并应用视图输出列 |
| 大小写敏感性 | 保持既有ignoreCaseSensitive行为 |
| 未找到 | 缺失库/表/列错误映射到QuerySpan.NotFoundError |
| StarRocks | 当前两个 StarRocks fixture 持续匹配 |
矩阵中的每一行在仓库中都有对应落点:standard.yaml覆盖目标列表、表达式、FROM、派生表、集合运算、CTE、JSON_TABLE 等常规场景;query_type.yaml覆盖查询类型判定;case_insensitive.yaml覆盖ignoreCaseSensitive行为;starrocks.yaml覆盖 StarRocks 引擎的特殊性(如filterClusterName对database:cluster形式的处理,见 query_span_extractor.go)。
ANTLR 到 omni 的逐项映射
迁移不是重写,而是"翻译":既有手写提取器的每个 ANTLR 上下文处理函数都映射到一个 omni AST 处理函数。这是计划中信息密度最高的一张表,也是理解最终代码结构的关键:
| 当前 ANTLR 项 | 最终 omni 项 |
|---|---|
getQuerySpan | getOmniQuerySpan在切机时改回getQuerySpan |
queryTypeListener | classifyOmniQueryType(ast.Node, allSystems) |
selectOnlyListener | getOmniQuerySpan中的直接根分发 |
extractContext | 直接extractFromSelectStmt/ 语句类型 switch |
extractSelectStatement | extractFromSelectStmt(*ast.SelectStmt) |
extractQueryExpression | extractFromSelectStmt(含 CTE + 集合运算处理) |
extractQueryExpressionParens | 不需要;omni AST 已归一化 |
extractQueryExpressionBody | extractFromSetOp或简单 select 体处理 |
extractQueryPrimary | 按需extractFromSelectStmt、extractTableStmt、extractValuesStmt |
extractExplicitTable | resolveTableRef/extractTableStmt |
extractTableValueConstructor | fixture 需要时extractValuesStmt |
extractQuerySpecification | extractFromSelectStmt简单 select 分支 |
extractSelectItemList | extractTargetList([]ast.ExprNode, fromSources) |
extractSelectItem | extractTarget |
extractSourceColumnSetFromExpr | resolveExpression(ast.ExprNode) |
extractSourceColumnSetFromExprList | mergeExpressionSources(...ast.ExprNode) |
extractTableWild | expandStar(*ast.ColumnRef) |
extractTableSourcesFromFromClause | extractFromClause([]ast.TableExpr) |
extractTableReferenceList | extractFromClause |
extractTableReference | extractTableSource(ast.TableExpr) |
extractJoinedTable | extractJoin(*ast.JoinClause) |
extractTableFactor | extractTableSource类型 switch |
extractTableFunction | extractJsonTable(*ast.JsonTableExpr) |
extractTableReferenceListParens | 除非 omni 暴露嵌套表列表,否则不需要 |
extractSubquery | extractSubqueryAsPseudo(*ast.SelectStmt) |
extractDerivedTable | extractDerivedTable(*ast.SubqueryExpr) |
extractSingleTable | resolveTableRef(*ast.TableRef) |
extractSingleTableParens | 不需要 |
extractCommonTableExpression | processCTEs([]*ast.CommonTableExpr) |
extractRecursiveCTE | extractRecursiveCTE(*ast.CommonTableExpr) |
extractNonRecursiveCTE | extractNonRecursiveCTE(*ast.CommonTableExpr) |
recursiveCTEExtractListener | 直接递归 CTE anchor/recursive 分支提取 |
getAccessTables/accessTableListener | collectOmniAccessTables(ast.Node, defaultDatabase) |
extractTableRefs/resourceExtractListener | 访问表的显式表源遍历 |
这张表揭示了迁移的核心理念:ANTLR 的 Listener 机制(隐式的树遍历回调)被替换为 omni AST 上的显式类型 switch 分发;ANTLR 括号包裹的语法层级(Parens系列)在归一化 AST 中不再存在;每个extractXxx方法都有明确对应的 omni 节点类型。
目标 API:最终提取器的方法签名
计划给出了切机后提取器的目标 API。这些方法在迁移期间可以挂在*omniQuerySpanExtractor上,切机时合并或改名,最终只保留一个生产提取器类型:
func (q *querySpanExtractor) getQuerySpan(ctx context.Context, stmt string) (*base.QuerySpan, error) func (q *querySpanExtractor) extractFromSelectStmt(sel *ast.SelectStmt) (*base.PseudoTable, error) func (q *querySpanExtractor) extractFromSetOp(sel *ast.SelectStmt) (*base.PseudoTable, error) func (q *querySpanExtractor) processCTEs(ctes []*ast.CommonTableExpr) error func (q *querySpanExtractor) extractTableSource(expr ast.TableExpr) ([]base.TableSource, error) func (q *querySpanExtractor) resolveTableRef(ref *ast.TableRef) (base.TableSource, error) func (q *querySpanExtractor) resolveExpression(expr ast.ExprNode) (base.QuerySpanResult, error) func (q *querySpanExtractor) cloneForSubquery() *querySpanExtractor func collectOmniAccessTables(root ast.Node, defaultDatabase string) base.SourceColumnSetcloneForSubquery值得单独说明:标量/关联子查询在解析时需要独立的querySpanExtractor实例(携带独立的ctes、outerTableSources等作用域状态),这与getColumnsForView中"新起一个提取器解析视图定义"的模式一脉相承。
逐阶段执行:三层测试驱动的迁移流水线
计划的执行哲学是纯 TDD:每个任务都按"先写失败测试 → 运行确认按预期失败 → 实现最小行为 → 跑聚焦测试 → 跑黄金测试记录 matched/diff → 跑既有TestGetQuerySpan保护参考路径"六步循环推进。完整阶段如下:
Phase 0:探测与脚手架(Probe And Scaffold)
产出四个文件:query_span_omni_probe_test.go、query_span_extractor_omni.go、query_span_extractor_omni_test.go、query_span_omni_parity_test.go(仓库中部分文件在切机后已改名/合并进query_span_extractor.go与query_span_parity_test.go)。
验证命令:
go test -v -count=1 github.com/bytebase/bytebase/backend/plugin/parser/mysql -run '^(TestMySQLOmniQuerySpanMigrationProbe|TestOmniQuerySpanScaffold_QueryTypesAndAccessTables|TestMySQLOmniQuerySpanGoldenHarness|TestGetQuerySpan)$'探测测试 query_span_probe_test.go 现在的形态是TestMySQLOmniQuerySpanMigrationProbe,包含FixtureParseCoverage与StructuralInvariants两个子测试。前者遍历四个 YAML fixture 中的每一条语句,用ParseMySQL验证 omni 能成功解析并产出非空语句列表——当前仓库中 MySQL 全部 30 条 fixture 语句均解析成功。
Phase 1:简单 SELECT 结果列
目标:SELECT 1、SELECT a FROM t、SELECT a AS x FROM t、SELECT a, t.b, db.t.c FROM t、SELECT * FROM t、SELECT *, a FROM t等基础目标列表 fixture 对齐。
实现:extractFromSelectStmt简单分支、extractTargetList、resolveExpression(覆盖*ast.ColumnRef、字面量、*ast.ResTarget)、expandStar。
Phase 2:表达式源合并
目标:算术(a-b AS c1)、比较(a=b AS c2)、函数(MAX(a))、嵌套函数参数、CASE、CAST、BETWEEN、IN值列表、LIKE、IS NULL、窗口函数参数等非子查询表达式的血缘与命名对齐。
实现:扩展resolveExpression、新增mergeExpressionSources,并保持IsPlainField语义:
- 裸列引用:true;
- 字面量常量:true(匹配当前 MySQL 对
SELECT 1的行为); - 由列/函数/子查询构成的表达式:false。
Phase 3:FROM、JOIN、别名与作用域
目标:FROM t AS x、x.a、JOIN ... ON、JOIN ... USING(a)、逗号连接、嵌套连接、表别名与物理名之间的别名遮蔽。
实现:extractFromClause、extractTableSource、extractJoin,并保证tableSourceFrom的顺序与既有 ANTLR 行为一致。仓库中 query_span_extractor.go 的joinTableSources展示了连接合并的细节:ON/USING连接与 NATURAL 连接对重复列的处理不同,USING字段会从右侧结果集中剔除(大小写不敏感),NATURAL 连接则按同名列合并。
Phase 4:派生表与子查询
目标:(SELECT a,b FROM t) AS x、(SELECT a,b FROM t) AS x(c1,c2)、标量子查询常量、来自表的标量子查询、关联标量子查询、WHERE a IN (SELECT a FROM t)、EXISTS (SELECT 1 FROM t WHERE ...)。
实现:cloneForSubquery、extractDerivedTable、extractSubqueryAsPseudo,并按 parity 要求将子查询源列合并到结果集或访问表集合;同时修复collectOmniAccessTables,使其覆盖目标列表与谓词中的子查询(而不仅限于 FROM)。
Phase 5:集合运算
目标:SELECT a FROM t UNION SELECT b FROM t2、UNION ALL、列数不匹配行为、派生表内的集合运算。
实现:extractFromSetOp,按位置合并结果源列,保留 anchor 侧的结果名。
Phase 6:CTE
目标:简单 CTE、嵌套 CTE、显式 CTE 列别名、带显式别名的递归 CTE、不带别名的递归 CTE。
实现:processCTEs、extractNonRecursiveCTE、extractRecursiveCTE,并保持既有遮蔽行为:当未指定 database 时,最近的 CTE 优先于物理表。这一规则在findTableSchema中可以看到——databaseName == ""时先倒序扫描q.ctes匹配(query_span_extractor.go)。
Phase 7:JSON_TABLE
目标:当前 JSON_TABLE fixture 对齐;JSON 表列从属主 JSON 表达式派生;priorTableInFrom解析JSON_TABLE(t.doc, ...)中的t.doc。
实现:extractJsonTable、JSON 表列伪结果,并严格按当前提取器方式传递priorTableInFrom。standard.yaml的开头就是完整的 JSON_TABLE fixture(含JSON_ARRAYAGG+REPLACE嵌套表达式),其期望结果展示了isplainfield: true、sourcecolumns指向db.products.product_info的精确血缘形状。
Phase 8:视图与元数据递归
目标:保持视图派生列在 omni 递归下正常工作——既有 StarRocks 视图 fixture、带SELECT *的 MySQL 视图元数据、带别名的视图。
实现:getColumnsForView在所需阶段就绪后改调 omni 提取器。当前实现(query_span_extractor.go)会为视图解析创建一个全新的提取器实例,并通过viewResolutionStack检测循环视图引用,把视图定义解析出的span.Results作为视图的输出列。
Phase 9:NotFound 与系统/用户表混合行为
目标:保留错误恢复与脱敏相关行为——缺失表、缺失列、mysql.user与用户表混合查询、全系统表查询返回SelectInfoSchema类型且源列为空。
实现:将ResourceNotFoundError路由到QuerySpan.NotFoundError;保留MixUserSystemTablesError(定义于 base/span.go,错误信息为 "cannot access user and system tables at the same time");确保全系统访问表在返回的 span 中被抑制。
系统表判定的具体规则在 query_span_extractor.go:information_schema、performance_schema视为保留系统库(大小写不敏感),mysql视为磁盘系统库(受ignoreCaseSensitive影响)。isMixedQuery则根据源列集合中系统表与用户表的共存情况,区分"纯系统查询"与"混合查询"两类结果。
Phase 10:严格黄金门禁
把黄金测试改为存在 diff 即失败,遍历全部 fixture 修正剩余 diff;切机后该测试继续作为黄金检查,但不得再让GetQuerySpan与同一 omni 内部路径互相比较(否则测试失去独立性)。
go test -v -count=1 github.com/bytebase/bytebase/backend/plugin/parser/mysql -run '^TestMySQLOmniQuerySpanGoldenHarness$'当前仓库结果:30/30 matched, 0 diffs。
黄金测试 query_span_parity_test.go 的实现印证了上述要求:它遍历mysqlOmniProbeFixturePaths四个 YAML,对每条语句调用包内 omni 路径newOmniQuerySpanExtractor(...).getOmniQuerySpan(...),再与 YAML 中querySpan期望做reflect.DeepEqual比较,最后require.Empty(t, diffs)强制零差异。
Phase 10.5:评审后的系统性修正
这一阶段是计划中最有价值的复盘,直接回答了"为什么三层测试都过了还会漏"。四条根因:
- 迁移从 ANTLR 递归解析树遍历转向 omni 显式类型 switch 后,部分 omni 子字段没有被接入血缘提取;
- 探测测试只验证了 AST 形状可用,没有验证提取器行为;
- 切机后的黄金测试一度以
GetQuerySpan为参考,但此时GetQuerySpan已经委托 omni,形成自我比较; - fixture 语料太小,覆盖不到旧提取器的长尾根语句与表达式节点。
对应的回归测试与实现修正:
derived_table_column_aliases_are_applied:对SubqueryExpr.Columns应用派生表伪列(带长度校验);in_subquery_sources_are_part_of_result_lineage:将InExpr.Select的结果源合并进表达式血缘;explicit_expression_nodes_do_not_drop_lineage:为EXISTS、CONVERT、COLLATE、MATCH、ROW、MEMBER OF、INTERVAL、DEFAULT添加显式表达式处理器;- 对不支持的 omni 表达式节点类型返回错误,而不是静默返回空血缘——这是最重要的防线,避免"解析成功但血缘丢失"的静默错误;
legacy_dml_roots_stay_dml:CALL、DO、HANDLER根语句分类为 DML;table_and_values_roots_return_select_results:TABLE、VALUES根语句提取为 select 家族结果。
Phase 11:切机(Cutover)
步骤:公共GetQuerySpan路径切换为 omni 提取器 →omniQuerySpanExtractor改名为querySpanExtractor或与之合并 → 删除 query-span 代码中的 ANTLR 上下文方法 → 删除selectOnlyListener、accessTableListener、resourceExtractListener、recursiveCTEExtractListener→ 删除或保留 ANTLRqueryTypeListener(仅当 query-span 之外的代码仍在使用)→ParseMySQL仅为仍需它的模块保留 → 用 grep 验证无 ANTLR 依赖:
rg 'ParseMySQL\(|GetANTLRAST|antlr4-go|github.com/bytebase/parser/mysql' backend/plugin/parser/mysql/query_span*.go预期结果:query-span 生产代码零 ANTLR 依赖,仅测试中的 parity 代码可删除或排除。
Phase 12:清理
决定query_span_omni_probe_test.go是否保留为常驻的解析器形状回归测试;保留query_span_omni_parity_test.go作为黄金测试;更新本计划的状态;运行完整 Go 检查。
全局验证与切机门禁
迁移全程贯穿三级验证命令:
聚焦 query-span 验证:
go test -v -count=1 github.com/bytebase/bytebase/backend/plugin/parser/mysql -run '^(TestGetQuerySpan|TestMySQLOmniQuerySpanMigrationProbe|TestOmniQuerySpan|TestMySQLOmniQuerySpanGoldenHarness)'包级验证:
go test -v -count=1 github.com/bytebase/bytebase/backend/plugin/parser/mysqlPR 前的仓库门禁:
gofmt -w <modified go files> golangci-lint run --allow-parallel-runners go build -ldflags "-w -s" -p=16 -o ./bytebase-build/bytebase ./backend/bin/server/main.go最终的 Cutover Gate 六项全绿才允许合入分支:
- 探测测试通过;
- 聚焦 omni 提取器测试通过;
- 严格黄金测试报告 0 diffs;
- 切换到 omni 后既有
TestGetQuerySpan原样通过; - query-span 生产代码对 ANTLR 零依赖;
golangci-lint通过,服务端构建通过。
从本次迁移中可以复用的工程方法
最后把这次迁移沉淀为可复用的工程纪律,供其他解析器基础设施迁移参考:
- 影子实现 + 一次性切机:新实现先在包内与旧实现并行,用行为等价证明换取切机权限,而不是用特性开关长期双轨运行。这避免了"新旧两套代码永远共存"的维护税。
- 三层测试各司其职:探测测试验证"新 AST 形状可用",聚焦行为测试验证"每个 bucket 语义正确",黄金测试验证"全部既有期望零回归"。三者缺一不可,探测测试永远不能替代行为测试。
- 黄金测试必须有独立参考:切机后黄金测试必须直接对比内部 omni 路径与 YAML 期望,绝不能拿"已经指向 omni 的公共入口"当参考去验证 omni 自身。
- 语料要覆盖长尾:只覆盖常见 SELECT 的语料会让回归测试漏掉
CALL/DO/HANDLER/TABLE/VALUES这类长尾根语句与长尾表达式节点。 - 静默错误比报错更危险:新实现遇到不支持的节点类型时宁可返回显式错误,也不要静默返回空血缘——后者会让脱敏等下游消费者在不知情的情况下丢失保护范围。
- 保留行为语义字段:
IsPlainField、NotFoundError、系统表抑制、CTE 遮蔽优先级这类"行为细节"是迁移中最容易在结构平移中丢失的部分,必须作为一等公民写入测试断言。
对 Bytebase 而言,这次迁移的意义不止于 MySQL:backend/plugin/parser/mysql是 omni 解析器全面接管各引擎解析基础设施的样板间。文中验证命令与测试路径均来自当前仓库,读者可以直接在仓库中复跑 query_span_parity_test.go 与 query_span_probe_test.go 验证30/30 matched, 0 diffs与 30/30 解析覆盖的实际状态。
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考