MySQL Query Span Omni 迁移实战:从 ANTLR 解析树遍历到 omni MySQL AST 的无回退重构
2026/9/14 20:32:12 网站建设 项目流程

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:谓词中涉及的列及文档数据库的字段路径;
  • NotFoundErrorFunctionNotSupportedErrorUnresolvedColumnsError:错误恢复与脱敏相关语义。

提取器对外通过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.yamlquery_type.yamlcase_insensitive.yamlstarrocks.yaml四个文件。

非协商约束:无回退、无开关、一次性切机

迁移计划开篇就用"Non-Negotiables"明确了红线,这是整个迁移工程纪律的核心,任何实现决策都不能违反:

  1. 无运行时特性开关(No runtime feature flag);
  2. 无环境切换(No environment switch);
  3. 切机后不允许从 omni 回退到 ANTLR(No fallback from omni to ANTLR after cutover);
  4. 不做部分生产 rollout(No partial production rollout);
  5. ANTLR 路径在切机前只允许作为开发参考存在
  6. 只有当 omni 路径在当前 fixture 语料上达到零 parity diff 时才允许切机
  7. 切机时既有 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共享提取器状态与字符串/元数据辅助函数(getAllTableColumnSourcesgetFieldColumnSourcefilterClusterNamefindTableSchemagetColumnsForViewisMixedQueryisSystemResource等),以及嵌入其中的omniQuerySpanExtractor
query_type.goANTLRqueryTypeListenerclassifyOmniQueryType取代
omni.goParseMySQL(基于mysqlparser.Parse)、GetOmniNodeByteOffsetToRunePosition等 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要求的行为
查询类型SELECTTABLEVALUESEXPLAINEXPLAIN ANALYZE SELECTSHOWSET、DDL、DML、全系统表 SELECT、遗留 DML 根(CALLDOHANDLER
访问表顶层 FROM、连接表、派生表体表、CTE 体表、SELECT/WHERE 中的子查询、系统表抑制
目标列表常量、裸列、限定列、别名、表达式名、*table.*
表达式列引用、字面量、二元/一元运算、函数、聚合、CASECASTBETWEENIN值列表、IN子查询、LIKEISEXISTS、标量子查询、CONVERTCOLLATEMATCHROWMEMBER OFINTERVAL、窗口参数
FROMTableRef、别名、逗号连接、JoinClauseONUSING、嵌套连接
派生表子查询表源、派生别名、派生列别名列表、嵌套派生表
集合运算UNIONUNION ALL、递归 CTE union、按位置合并源列
CTE非递归 CTE、嵌套 CTE、显式 CTE 列列表、递归 CTE anchor/recursive 合并
JSON_TABLEJSON 表列、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 引擎的特殊性(如filterClusterNamedatabase:cluster形式的处理,见 query_span_extractor.go)。

ANTLR 到 omni 的逐项映射

迁移不是重写,而是"翻译":既有手写提取器的每个 ANTLR 上下文处理函数都映射到一个 omni AST 处理函数。这是计划中信息密度最高的一张表,也是理解最终代码结构的关键:

当前 ANTLR 项最终 omni 项
getQuerySpangetOmniQuerySpan在切机时改回getQuerySpan
queryTypeListenerclassifyOmniQueryType(ast.Node, allSystems)
selectOnlyListenergetOmniQuerySpan中的直接根分发
extractContext直接extractFromSelectStmt/ 语句类型 switch
extractSelectStatementextractFromSelectStmt(*ast.SelectStmt)
extractQueryExpressionextractFromSelectStmt(含 CTE + 集合运算处理)
extractQueryExpressionParens不需要;omni AST 已归一化
extractQueryExpressionBodyextractFromSetOp或简单 select 体处理
extractQueryPrimary按需extractFromSelectStmtextractTableStmtextractValuesStmt
extractExplicitTableresolveTableRef/extractTableStmt
extractTableValueConstructorfixture 需要时extractValuesStmt
extractQuerySpecificationextractFromSelectStmt简单 select 分支
extractSelectItemListextractTargetList([]ast.ExprNode, fromSources)
extractSelectItemextractTarget
extractSourceColumnSetFromExprresolveExpression(ast.ExprNode)
extractSourceColumnSetFromExprListmergeExpressionSources(...ast.ExprNode)
extractTableWildexpandStar(*ast.ColumnRef)
extractTableSourcesFromFromClauseextractFromClause([]ast.TableExpr)
extractTableReferenceListextractFromClause
extractTableReferenceextractTableSource(ast.TableExpr)
extractJoinedTableextractJoin(*ast.JoinClause)
extractTableFactorextractTableSource类型 switch
extractTableFunctionextractJsonTable(*ast.JsonTableExpr)
extractTableReferenceListParens除非 omni 暴露嵌套表列表,否则不需要
extractSubqueryextractSubqueryAsPseudo(*ast.SelectStmt)
extractDerivedTableextractDerivedTable(*ast.SubqueryExpr)
extractSingleTableresolveTableRef(*ast.TableRef)
extractSingleTableParens不需要
extractCommonTableExpressionprocessCTEs([]*ast.CommonTableExpr)
extractRecursiveCTEextractRecursiveCTE(*ast.CommonTableExpr)
extractNonRecursiveCTEextractNonRecursiveCTE(*ast.CommonTableExpr)
recursiveCTEExtractListener直接递归 CTE anchor/recursive 分支提取
getAccessTables/accessTableListenercollectOmniAccessTables(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.SourceColumnSet

cloneForSubquery值得单独说明:标量/关联子查询在解析时需要独立的querySpanExtractor实例(携带独立的ctesouterTableSources等作用域状态),这与getColumnsForView中"新起一个提取器解析视图定义"的模式一脉相承。

逐阶段执行:三层测试驱动的迁移流水线

计划的执行哲学是纯 TDD:每个任务都按"先写失败测试 → 运行确认按预期失败 → 实现最小行为 → 跑聚焦测试 → 跑黄金测试记录 matched/diff → 跑既有TestGetQuerySpan保护参考路径"六步循环推进。完整阶段如下:

Phase 0:探测与脚手架(Probe And Scaffold)

产出四个文件:query_span_omni_probe_test.goquery_span_extractor_omni.goquery_span_extractor_omni_test.goquery_span_omni_parity_test.go(仓库中部分文件在切机后已改名/合并进query_span_extractor.goquery_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,包含FixtureParseCoverageStructuralInvariants两个子测试。前者遍历四个 YAML fixture 中的每一条语句,用ParseMySQL验证 omni 能成功解析并产出非空语句列表——当前仓库中 MySQL 全部 30 条 fixture 语句均解析成功。

Phase 1:简单 SELECT 结果列

目标:SELECT 1SELECT a FROM tSELECT a AS x FROM tSELECT a, t.b, db.t.c FROM tSELECT * FROM tSELECT *, a FROM t等基础目标列表 fixture 对齐。

实现:extractFromSelectStmt简单分支、extractTargetListresolveExpression(覆盖*ast.ColumnRef、字面量、*ast.ResTarget)、expandStar

Phase 2:表达式源合并

目标:算术(a-b AS c1)、比较(a=b AS c2)、函数(MAX(a))、嵌套函数参数、CASECASTBETWEENIN值列表、LIKEIS NULL、窗口函数参数等非子查询表达式的血缘与命名对齐。

实现:扩展resolveExpression、新增mergeExpressionSources,并保持IsPlainField语义

  • 裸列引用:true;
  • 字面量常量:true(匹配当前 MySQL 对SELECT 1的行为);
  • 由列/函数/子查询构成的表达式:false。

Phase 3:FROM、JOIN、别名与作用域

目标:FROM t AS xx.aJOIN ... ONJOIN ... USING(a)、逗号连接、嵌套连接、表别名与物理名之间的别名遮蔽。

实现:extractFromClauseextractTableSourceextractJoin,并保证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 ...)

实现:cloneForSubqueryextractDerivedTableextractSubqueryAsPseudo,并按 parity 要求将子查询源列合并到结果集或访问表集合;同时修复collectOmniAccessTables,使其覆盖目标列表与谓词中的子查询(而不仅限于 FROM)。

Phase 5:集合运算

目标:SELECT a FROM t UNION SELECT b FROM t2UNION ALL、列数不匹配行为、派生表内的集合运算。

实现:extractFromSetOp按位置合并结果源列,保留 anchor 侧的结果名。

Phase 6:CTE

目标:简单 CTE、嵌套 CTE、显式 CTE 列别名、带显式别名的递归 CTE、不带别名的递归 CTE。

实现:processCTEsextractNonRecursiveCTEextractRecursiveCTE,并保持既有遮蔽行为:当未指定 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 表列伪结果,并严格按当前提取器方式传递priorTableInFromstandard.yaml的开头就是完整的 JSON_TABLE fixture(含JSON_ARRAYAGG+REPLACE嵌套表达式),其期望结果展示了isplainfield: truesourcecolumns指向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_schemaperformance_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:评审后的系统性修正

这一阶段是计划中最有价值的复盘,直接回答了"为什么三层测试都过了还会漏"。四条根因:

  1. 迁移从 ANTLR 递归解析树遍历转向 omni 显式类型 switch 后,部分 omni 子字段没有被接入血缘提取
  2. 探测测试只验证了 AST 形状可用,没有验证提取器行为
  3. 切机后的黄金测试一度以GetQuerySpan为参考,但此时GetQuerySpan已经委托 omni,形成自我比较
  4. 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:为EXISTSCONVERTCOLLATEMATCHROWMEMBER OFINTERVALDEFAULT添加显式表达式处理器;
  • 对不支持的 omni 表达式节点类型返回错误,而不是静默返回空血缘——这是最重要的防线,避免"解析成功但血缘丢失"的静默错误;
  • legacy_dml_roots_stay_dmlCALLDOHANDLER根语句分类为 DML;
  • table_and_values_roots_return_select_resultsTABLEVALUES根语句提取为 select 家族结果。

Phase 11:切机(Cutover)

步骤:公共GetQuerySpan路径切换为 omni 提取器 →omniQuerySpanExtractor改名为querySpanExtractor或与之合并 → 删除 query-span 代码中的 ANTLR 上下文方法 → 删除selectOnlyListeneraccessTableListenerresourceExtractListenerrecursiveCTEExtractListener→ 删除或保留 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/mysql

PR 前的仓库门禁:

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 六项全绿才允许合入分支:

  1. 探测测试通过;
  2. 聚焦 omni 提取器测试通过;
  3. 严格黄金测试报告 0 diffs;
  4. 切换到 omni 后既有TestGetQuerySpan原样通过;
  5. query-span 生产代码对 ANTLR 零依赖;
  6. golangci-lint通过,服务端构建通过。

从本次迁移中可以复用的工程方法

最后把这次迁移沉淀为可复用的工程纪律,供其他解析器基础设施迁移参考:

  1. 影子实现 + 一次性切机:新实现先在包内与旧实现并行,用行为等价证明换取切机权限,而不是用特性开关长期双轨运行。这避免了"新旧两套代码永远共存"的维护税。
  2. 三层测试各司其职:探测测试验证"新 AST 形状可用",聚焦行为测试验证"每个 bucket 语义正确",黄金测试验证"全部既有期望零回归"。三者缺一不可,探测测试永远不能替代行为测试。
  3. 黄金测试必须有独立参考:切机后黄金测试必须直接对比内部 omni 路径与 YAML 期望,绝不能拿"已经指向 omni 的公共入口"当参考去验证 omni 自身。
  4. 语料要覆盖长尾:只覆盖常见 SELECT 的语料会让回归测试漏掉CALL/DO/HANDLER/TABLE/VALUES这类长尾根语句与长尾表达式节点。
  5. 静默错误比报错更危险:新实现遇到不支持的节点类型时宁可返回显式错误,也不要静默返回空血缘——后者会让脱敏等下游消费者在不知情的情况下丢失保护范围。
  6. 保留行为语义字段IsPlainFieldNotFoundError、系统表抑制、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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询