news 2026/9/9 14:00:10

TiDB 中的 AST 还原 SQL 文本(Restore)机制解析:从设计提案到 parser 源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TiDB 中的 AST 还原 SQL 文本(Restore)机制解析:从设计提案到 parser 源码实现

TiDB 中的 AST 还原 SQL 文本(Restore)机制解析:从设计提案到 parser 源码实现

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

导读:当 TiDB 在 SQL 解析层拿到一棵 AST(抽象语法树)后,许多高级特性(如CREATE VIEW的列展开、Plan Cache、SQL Binding 归一化等)都要求能把 AST 节点重新拼接回等价的 SQL 文本。本文以 TiDB 官方设计提案 docs/design/2018-11-29-ast-to-sql-text.md 为主线,结合当前仓库pkg/parser中真实落地的实现代码,系统讲解ast.Node.Restore(ctx)接口、RestoreFlags/RestoreCtx设计、Writer 辅助方法与典型调用链。读完你既能理解这套还原机制的演进脉络,也能在自己的 SQL 工具链中复用其接口设计与格式控制方法。

为什么需要"从 AST 还原 SQL 文本"

TiDB 首先使用 parser 将 SQL 语句解析为一棵ast.Node组成的语法树,随后优化器、执行器会在树上做变换。很多功能需要把(可能被改写过的)AST 节点重新还原成 SQL 文本,最典型的场景正是提案中给出的例子:

CREATE VIEW v AS SELECT * FROM t;

在真正落库前,TiDB 需要把视图定义中的SELECT *展开成显式列名(例如SELECT test.t.col0, test.t.col1 FROM test.t),再重新生成完整的、可执行的CREATE VIEW语句文本。要做到这一点,就要求"任何 AST 节点都能被还原为 SQL 文本"——即select子节点被展开替换后,其父级、祖级节点乃至整个语句仍可重新序列化。

在引入 Restore 机制之前,ast.Node上只存在Text()/SetText()方法:parser 在解析过程中通过SetText()记录节点对应的原始文本片段,之后可通过Text()取回。但该机制并不完整——只有根节点的Text()能可靠工作;一旦 AST 被修改过,节点记录的原始文本便与新树不一致。设计文档因此明确指出:实现本提案时不应依赖node.Text()(同理也不应依赖exprNode.Format())。

每个 AST 节点本质是一棵嵌套树,其子节点与 SQL 片段一一对应,这正是 Restore 递归还原的基础。以CREATE USER语句为例,CreateUserStmt是一个语句级节点,它聚合了UserSpec、用户身份User、认证选项AuthOpt等子节点,整体构成如图所示的层次结构:

提案核心:给 ast.Node 增加 Restore() 方法

设计提案给出的接口定义如下(该方法现已实际进入pkg/parser/ast包):

type Node interface { // Restore AST to SQL text and append them to `ctx`. // return error when the AST is invalid. Restore(ctx *RestoreCtx) error // ... }

在 pkg/parser/ast/ast.go#L28 中可以看到当前Node接口的完整形态——Restore已是每个 AST 节点必须实现的能力,与Accept(Visitor 访问)、TextSetText等方法并列:

type Node interface { // Restore returns the sql text from ast tree Restore(ctx *format.RestoreCtx) error Accept(v Visitor) (node Node, proceed bool) AcceptInPlace(v InPlaceVisitor) (proceed bool) Text() string OriginalText() string SetText(enc charset.Encoding, text string) // ... }

注意Restore并不直接返回字符串,而是接收一个*format.RestoreCtx并将还原出的文本"追加写入"其内部的 writer。这样调用方可以自由选择输出目标(bytes.Bufferstrings.Builderos.Stdout等),也便于在遍历过程中控制格式。

RestoreFlags:控制输出格式的九面"旗标"

为了让还原出的 SQL 能以不同格式输出,提案引入RestoreFlags。最初的九面旗标构成如下互斥分组(同一分组内靠左的旗标优先级更高):

互斥分组作用
RestoreStringSingleQuotes/RestoreStringDoubleQuotes字符串字面量用单引号或双引号包裹
RestoreStringEscapeBackslash是否对反斜杠做转义处理
RestoreKeyWordUppercase/RestoreKeyWordLowercase关键字(SELECTCREATE等)大写或小写
RestoreNameUppercase/RestoreNameLowercase标识符(库名、表名等)大写或小写
RestoreNameDoubleQuotes/RestoreNameBackQuotes标识符用双引号或反引号`包裹

这些定义可以在 pkg/parser/format/format.go#L209-L250 中找到,其枚举声明与设计文档完全一致(文档中"左侧位置旗标优先级更高"的注释也原样保留在源码中)。例如在字符串引号分组中,RestoreStringSingleQuotes排在前,因此当两个旗标同时被置位时按单引号处理。

RestoreCtx:携带旗标与写入目标

还原过程的"上下文"由RestoreCtx承载。提案中的定义是:

// RestoreCtx is Restore context to hold flags and writer type RestoreCtx struct { Flags RestoreFlags In io.Writer } const DefaultRestoreFlags = RestoreStringSingleQuotes | RestoreKeyWordUppercase | RestoreNameBackQuotes

默认旗标组合意味着:字符串用单引号、关键字用大写、标识符用反引号。当前实现中RestoreCtx的结构已扩展得更丰富(见 pkg/parser/format/format.go#L378-L407):

type RestoreCtx struct { Flags RestoreFlags In RestoreWriter // io.Writer + io.StringWriter DefaultDB string ParentBinaryOp int // 供表达式还原判断优先级、决定括号去留 ParentBinarySide int InUnaryOperation bool CTERestorer // 记录/查询 CTE 名称 }

建议始终通过构造函数创建实例,它会把DefaultDB初始化为空串:

func NewRestoreCtx(flags RestoreFlags, in RestoreWriter) *RestoreCtx { return &RestoreCtx{Flags: flags, In: in, DefaultDB: ""} }

五个 Writer 辅助方法:还原输出的基本原语

Restore实现体本身不直接操作字符串拼接细节,而是调用RestoreCtx提供的语义化 Writer 方法,让"格式控制"集中收敛:

  • WriteKeyWord(keyWord):按旗标将关键字转为大写或小写后写入(见 format.go#L416-L424);
  • WriteString(str):按旗标选择单/双引号包裹字符串字面量,并处理'/"/\的转义(见 format.go#L453-L469);
  • WriteName(name):按旗标对标识符做大小写转换、并用反引号或双引号包裹(内部对反引号`做 `` 双重转义,见 format.go#L473-L494);
  • WritePlain(text)/WritePlainf(format, args...):原样写入,不做任何转换(见 format.go#L497-L504);
  • 此外还有用于 TiDB 扩展语法的WriteWithSpecialComments/WriteKeyWordWithSpecialComments,可在还原结果中生成/*T![feature] ... */形式的特殊注释。

之所以把"关键字/标识符/字符串"分开处理,是为了让同一棵 AST 在不同输出需求下(例如大写规范输出、不带任何引号的迁移脚本)无需改动节点实现。

还原原理:自顶向下递归、按层拼接

AST 是一棵"子节点即 SQL 片段"的树,因此还原算法非常朴素:从根节点开始,逐层调用各子节点的Restore(),按语法次序把子节点输出与字面量(关键字、符号、空格)拼接起来。设计文档以如下语句为例:

SELECT column0 FROM table0 UNION SELECT column1 FROM table1 WHERE a = 1

其语法树顶层是一个UnionStmt,下面挂两个UnionSel(子 SELECT);第二个 SELECT 内部又细分出FieldList(列名)、TableRefs(表引用)、ExprNodeWHERE a = 1表达式)等子结构。还原过程正是沿着这棵树的边逐层下行拼接:

每个节点只负责"自己那一层"的拼装,子句细节交给子节点递归完成。这种"子节点可插拔"的设计,让改写子节点后重新还原整句成为可能——这正是视图列展开、SQL 归一化等场景的前提。

兼容性约束:还原结果只需"AST 等价"

SQL 文本与 AST 是一对多关系:同一棵 AST 可以对应无数种写法(关键字大小写、空白、括号冗余、字符串引号风格不同等),因此不可能也不必还原出与原始输入逐字符相同的文本。提案确立的验收标准是:

由原始 SQL 语句解析出的 AST,与由还原出的 SQL 语句再次解析得到的 AST,两者应当相等

这也解释了为何需要RestoreFlags提供多种输出风格——不同消费方对"格式"的要求不同,但对"AST 等价"的要求完全一致。以默认旗标还原时,CREATE DATABASE db1这类语句会被稳定输出为规范形式,从而可作为 SQL 指纹/归一化文本参与缓存命中判断。

实现示例:从提案代码到今天仓库中的真实实现

设计文档选取了ast.CreateDatabaseStmtast.DropDatabaseStmt作为首批示例。回到 2018 年时该代码位于独立的pingcap/parser仓库;如今 parser 已经作为子模块内嵌在 TiDB 主仓库的pkg/parser目录中,这些Restore实现可以在 pkg/parser/ast/ddl.go 里直接读到。

先看子节点DatabaseOption.Restore,它按选项类型分发,用WriteKeyWord/WritePlain还原CHARACTER SETCOLLATE等数据库选项(ddl.go#L102-L140),其核心逻辑与提案代码保持一致:

// Restore implements Node interface. func (n *DatabaseOption) Restore(ctx *format.RestoreCtx) error { switch n.Tp { case DatabaseOptionCharset: ctx.WriteKeyWord("CHARACTER SET") ctx.WritePlain(" = ") ctx.WritePlain(n.Value) case DatabaseOptionCollate: ctx.WriteKeyWord("COLLATE") ctx.WritePlain(" = ") ctx.WritePlain(n.Value) case DatabaseOptionEncryption: ctx.WriteKeyWord("ENCRYPTION") ctx.WritePlain(" = ") ctx.WriteString(n.Value) case DatabaseOptionPlacementPolicy: placementOpt := PlacementOption{ Tp: PlacementOptionPolicy, UintValue: n.UintValue, StrValue: n.Value, } return placementOpt.Restore(ctx) // ... default: return errors.Errorf("invalid DatabaseOptionType: %d", n.Tp) } return nil }

可以看到,随着 TiDB 演进,源码在提案示例基础上又补充了ENCRYPTIONPLACEMENT POLICYSET TIFLASH REPLICA等新选项分支。这也说明Restore是一个随语法能力持续增长的方法集合。

父节点CreateDatabaseStmt.Restore则展示了"关键字 + 条件分支 + 子节点递归 + 错误标注"的通用骨架(ddl.go#L153-L167):

// Restore implements Node interface. func (n *CreateDatabaseStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("CREATE DATABASE ") if n.IfNotExists { ctx.WriteKeyWord("IF NOT EXISTS ") } ctx.WriteName(n.Name.O) for i, option := range n.Options { ctx.WritePlain(" ") err := option.Restore(ctx) if err != nil { return errors.Annotatef(err, "An error occurred while splicing CreateDatabaseStmt DatabaseOption: [%v]", i) } } return nil }

设计文档同时给出了DropDatabaseStmt的还原写法作为对照,同样遵循"先关键字、再可选子句、后名称"的顺序:

// Restore implements Node interface. func (n *DropDatabaseStmt) Restore(ctx *RestoreCtx) error { ctx.WriteKeyWord("DROP DATABASE ") if n.IfExists { ctx.WriteKeyWord("IF EXISTS ") } ctx.WriteName(n.Name) return nil }

与视图场景直接相关的 CreateViewStmt

回到文章开头那个CREATE VIEW场景:在 pkg/parser/ast/ddl.go#L1674-L1743 中,CreateViewStmt结构体用Select StmtNode字段保存视图查询的 AST,其Restore()在输出ALGORITHMDEFINERSQL SECURITY、视图名与列清单后,通过n.Select.Restore(ctx)递归还原查询体,最后追加WITH ... CHECK OPTION。视图定义里"SELECT 子节点可被展开替换后整体再序列化"的能力正是由这套递归接口提供的。

提案给出的实现注意点

  • 不要依赖exprNode.Format():旧 Format 机制并非为可逆还原设计;
  • 不要依赖node.Text():它只能忠实反映解析时记录的原文本,无法表达被改写后的 AST。

这些约定在今天的Node接口文档注释中仍能看到影响——Restore从"追加写入的上下文"出发,天然避免了这两个陷阱。

落地现状:Restore 在 TiDB 各模块中的典型调用

Restore 机制如今贯穿 TiDB 的多个子系统。从调用方式看,统一模式是先构造strings.Builder/bytes.Buffer,再format.NewRestoreCtx(flags, &buf),最后node.Restore(ctx)

  • 语义检查与报错信息构造:在 pkg/planner/core/preprocess.go#L2106-L2124 中,当CAST表达式的小数位/精度非法时,会先用NewRestoreCtx(format.DefaultRestoreFlags, &buf)把表达式还原成 SQL 文本,再拼进ErrMBiggerThanDErrTooBigPrecision等错误消息,让用户看到"是哪段表达式写错了";
  • 非预备计划缓存(Non-Prepared Plan Cache):在 pkg/planner/core/plan_cache_param.go#L44 中,使用RestoreForNonPrepPlanCache | RestoreStringWithoutCharset | RestoreStringSingleQuotes | RestoreNameBackQuotes组合旗标对 SQL 归一化,作为缓存键的一部分;
  • SQL Binding 归一化pkg/bindinfo相关代码同样依赖 Restore 生成规范化文本(见 pkg/bindinfo/binding.go);
  • 此外表达式还原还通过ParentBinaryOpRestoreSkipRedundantParentheses等机制在保留语义的前提下智能省略冗余括号,服务于规范化路径。

作为对照,仅用Text()无法支撑上述场景,因为它们消费的都是"经过改写/需要规范化"的 AST 而非原始输入。

RestoreFlags 的后续演进

最初九面旗标之外,当前 pkg/parser/format/format.go#L231-L249 已新增多面旗标,读者在阅读代码或自行调用时可留意:

旗标用途
RestoreSpacesAroundBinaryOperation/RestoreBracketAroundBinaryOperation二元运算两侧空格 / 强制加括号
RestoreStringWithoutCharset/RestoreStringWithoutDefaultCharset省略字符串上的 charset 前缀或 DEFAULT charset
RestoreTiDBSpecialComment将 TiDB 扩展语法包进/*T!...*/特殊注释
SkipPlacementRuleForRestore还原时跳过 placement 相关子句
RestoreWithTTLEnableOff还原 TTL 表时强制TTL_ENABLE='OFF'
RestoreWithoutSchemaName/RestoreWithoutTableName还原时省略库名 / 表名(按标识符层级裁剪输出)
RestoreForNonPrepPlanCache非预备计划缓存场景的归一化还原
RestoreBracketAroundBetweenExprBETWEEN表达式补括号
RestoreSkipRedundantParentheses规范化路径中省略不影响语义的冗余括号

工程实施与演进节奏

设计文档将整个改造划分为四个阶段推进:考虑到若干ast.Node之间存在依赖(例如语句节点依赖表达式节点、DDL 节点依赖名称节点),子任务按依赖顺序分批实现,避免一次性全量改造。第一批落地了CreateDatabaseStmt/DropDatabaseStmt等基础 DDL 语句作为样板,随后逐步覆盖到 DML、表达式、函数调用乃至 TTL、placement、CTE 等全部语法家族。

小结

从 2018 年这篇设计提案到今天的pkg/parser,"从 AST 还原 SQL 文本"已经成为 TiDB 的一项基础设施能力:

  • 接口层面Node.Restore(ctx *format.RestoreCtx) error让每类节点都能递归序列化自身;
  • 格式层面RestoreFlags+RestoreCtx以互斥分组、优先级语义和 Writer 辅助方法,把输出风格与语法拼装解耦;
  • 正确性层面:以"还原文本二次解析出的 AST 与原 AST 相等"为准则,规避了 AST 与文本一对多带来的歧义;
  • 应用层面:视图定义生成、表达式报错回显、非预备计划缓存、SQL Binding 归一化等场景都建立在这一机制之上。

对于想在自己的 SQL 工具链中做 AST 改写与回写的开发者,这套设计(互斥格式分组 + 上下文写入 + 递归还原 + AST 等价校验)是可直接借鉴的成熟范式。

相关代码与文档索引

  • 设计提案原文:docs/design/2018-11-29-ast-to-sql-text.md
  • AST 节点Node接口定义:pkg/parser/ast/ast.go#L28
  • RestoreFlagsRestoreCtx、Writer 辅助方法:pkg/parser/format/format.go#L209
  • CreateDatabaseStmt/DatabaseOption/CreateViewStmt的 Restore 实现:pkg/parser/ast/ddl.go#L102
  • 还原调用示例(报错回显 / 非预备计划缓存):pkg/planner/core/preprocess.go#L2114、pkg/planner/core/plan_cache_param.go#L44

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 13:57:03

Configure GitSync(ToolJet 工作区 Git 同步配置)

Configure GitSync(ToolJet 工作区 Git 同步配置) 【免费下载链接】ToolJet Open-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build…

作者头像 李华
网站建设 2026/9/9 13:55:11

SRResCycGAN真实图像超分:循环一致性GAN与残差卷积网络工程实践

简介:SRResCycGAN是ECCVW AIM2020真实图像超分辨率挑战赛赛道3的官方PyTorch实现。项目针对真实场景中低分辨率图像退化过程不符合双三次降采样假设的问题,借用CycleGAN思路构建深度循环生成对抗网络,保持LR与HR域间分布一致性,实…

作者头像 李华
网站建设 2026/9/9 13:54:44

Flask、Django、FastAPI、Tornado:Python Web框架选型与实战

任何人用Python写后端,迟早都会在Flask、Django、FastAPI、Tornado这四个名字里纠结一次。我在技术社区潜水多年,见过为选型开会吵三天的团队,也见过拍脑袋选了Django之后把接口开发周期拖到崩溃的小组;还有朋友用FastAPI重写旧接…

作者头像 李华