ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PostgreSQL语法解析:移进规约算法与gram.y实战解析

PostgreSQL语法解析:移进规约算法与gram.y实战解析 如果你在搜索引擎里敲下“pg语法解析”或“移进规约”大概率会先看到一堆跟数据库无关的东西甚至还有不少娱乐向的干扰词。这里先明确一下这篇聊的PG是PostgreSQL不是任何游戏模拟器或者外挂工具。PostgreSQL作为开源数据库里语法体系最庞大、最贴近SQL标准的重器之一它的语法解析器几乎是每个想做数据库内核、中间件或者对SQL执行流程好奇的人绕不开的第一站。而“移进规约”这四个字就是PostgreSQL解析SQL使用的核心算法机制。很多人在看PG源码时会直接跳到优化器、执行器觉得PL/pgSQL和executor才够劲。但我的经验是真正在业务里碰到疑难杂症时往往是解析阶段埋下的雷——比如一条SQL在特定写法下报错或者你发现PG对某个语法的支持跟其他数据库不一样这些问题的根子几乎都在gram.y里。这篇分享会从最基础的解析器组成讲起结合一条真实SQL把移进规约的运作过程完整推演一遍再落到如何修改PG语法扩展自定义语句以及排查parser报错时我实际用过的调试手段。1. 一条SQL在PostgreSQL中要过的第一道关解析器组件与职责边界很多人第一次看PG源码时会以为解析器做完了从字符到执行计划的全部事情。这是最容易产生的误解。实际上PostgreSQL把这条链路切得很清楚词法分析、语法分析、语义分析和优化执行是四个完全独立的阶段。语法解析阶段只负责把用户输入的文本变成一颗“裸语法树”也就是Raw Parse Tree它不关心表名是否存在、列名是否合法、权限够不够这些全部留给后面的parse_analyze和rewrite阶段处理。1.1 从字符串到RawStmt解析器只做“分句建裸树”你可以把PG的raw_parser理解成一个翻译官输入是一整段SQL字符串输出是一个List列表里每个元素都是一个RawStmt结构。RawStmt里面除了语法树节点本身的指针还记录了这条语句在原始输入字符串中的起始位置和长度。为什么要记录位置因为扩展查询协议和psql的命令补全都需要知道每条语句从哪里开始、到哪里结束。看源码的时候建议直接打开src/backend/parser/gram.y在文件比较靠后的位置能找到raw_parser的定义。它的实现非常简单核心就一句调用base_yyparse()。base_yyparse就是Bison根据gram.y生成出来的解析函数。这个函数会驱动整个移进规约过程最终把你输入的SQL逐步规约成起始非终结符并且在每一条顶层规则里调用你写好的动作代码构造出SelectStmt、InsertStmt、CreateStmt这些节点。我做内核调试时经常在raw_parser入口下断点。这个函数不依赖任何数据库连接和事务上下文你甚至可以写一个独立小程序只调用raw_parser来解析一条SQL字符串看看输出的裸语法树长什么样。这比在psql里反复试错要快得多。1.2 gram.y不是PG独有的语法文件而是Bison的输入看到gram.y这个后缀很多后端开发会愣一下。这个文件不是PG自创的格式它是GNU Bison的输入文件。Bison的用法和flex很像你写语法规则Bison帮你生成C代码实现一个解析器。PG的gram.y体量非常大成千上万行光%token声明就有几百个各种产生式规则更是密密麻麻。PG 14之后把原来的gram.y拆成了两个文件core.y和gram.y。core.y负责存放表达式、目标列表、限定名这些基础语法gram.y负责存放各类SQL语句的顶层规则。这么拆分不是因为算法变了纯粹是为了让文件更容易维护编译的时候两者还是会一起被Bison处理后合并成一个parser。如果你打开这两个文件会看到大量的规则形如simple_select: SELECT opt_all_clause opt_target_list into_clause from_clause where_clause group_clause having_clause window_clause ...每一段冒号后面都是一个产生式右部花括号里是配套的C动作代码。这就是Bison处理的核心语法描述。1.3 为什么不手写递归下降这个问题几乎每个接触PG解析器的人都会问一遍。PostgreSQL的SQL语法极其庞大用递归下降手写当然可以很多现代数据库也确实是这么做的。但SQL文法有一个让人头大的特点表达式层面的规则天然是左递归的。左递归在递归下降里是大忌。比如expr: expr expr如果你按这个规则直接写递归下降函数进入expr之后第一步又调用expr直接无限循环。解决左递归需要手动改写成右递归或者引入循环机制这会让优先级处理变得非常繁琐还得为加法、乘法、比较、逻辑运算这些不同优先级各写一层函数整个parser代码会暴涨。Bison处理左递归是强项。它生成的解析器基于LALR(1)分析表通过状态栈自动处理左递归结构你只需要把语法规则原样写出来然后为不同运算符声明优先级和结合性即可。表达式规则的写法可以保持最直观的形式expr: expr expr | expr * expr | NUMBER这让PG能用一个相对统一的框架承载整个SQL标准语法。代价也很明显Bison生成的parser代码读起来很痛苦而且一旦出现冲突你面对的不再是“递归调用栈”而是一张张状态表。所以理解移进规约原理其实就是拿到了阅读和修改gram.y的钥匙。2. 词法分析先走一步flex如何把SQL文本变成token流语法解析器处理的不是字符串而是token流。这个转换由flex扫描器完成PG对应的文件是src/backend/parser/scan.l。flex的职责很纯粹把字符序列切分成一个一个有意义的单词并判断每个单词的类型。比如看到一串数字就给出ICONST或者FCONST token看到一串字母就要进一步判断它是一个普通标识符还是一个SQL关键字。2.1 扫描器的输出不只是单词还带位置与类型scan.l输出的每个token其实是一个二元组token类型和语义值。token类型对应gram.y里%token声明的那些名字比如SELECT_P、IDENT、ICONST语义值则是一个union里面可能存了整数值、浮点值、字符串指针或者关键字枚举值。这里有一个经常被忽略的点token还会携带位置信息。PG的parser在报错时能精确告诉你“syntax error at or near xxxx”的字符位置就是靠词法分析阶段记录的cursorpos。gram.y里的parser_errposition函数就是拿这些位置信息去生成错误上下文。如果你在gdb里给base_yylex打断点能清楚地看到这个函数每次调用只做一件事从当前扫描位置开始匹配scan.l里定义的正则规则返回一个int类型的token编号同时通过全局变量或参数把语义值和位置传出去。Bison生成的parser在需要lookahead token时就会调用这个函数去拿下一个token这就是LR分析里的“向前看”。2.2 关键字表为什么很多token叫SELECT_P而不叫SELECT第一次看gram.y的%token声明你肯定会被那些带_P后缀的token搞得一头雾水SELECT_P、VALUES_P、DEFAULT_P、UNION_P。这些token不是拼写错误而是为了避免和C语言里的符号冲突。比如C标准库里有select系统调用如果你的token名字直接叫SELECT生成的C代码里会出现#define SELECT之类的东西很可能把系统头文件搞炸。所以PostgreSQL给很多容易撞名的关键字token加了一个_P后缀这个P就是“关键字”的Protection标记。关键字的识别逻辑在src/include/parser/kwlist.h里。这个文件维护了一张全量关键字表每一行都是一个PG_KEYWORD宏三个参数分别表示SQL文本、token名、关键字分类。比如PG_KEYWORD(select, SELECT_P, RESERVED_KEYWORD)flex扫描器识别到一个完整的字母串之后会先转成小写然后在这张表里做二分查找。如果命中了就返回对应的token类型比如SELECT_P如果没有命中就返回IDENT标记为普通标识符。这个文件还决定了哪些关键字是保留的。RESERVED_KEYWORD意味着你没法拿它当表名、列名而UNRESERVED_KEYWORD则可以在很多位置作为标识符使用。修改PG语法时如果你新增了一个关键字却忘记往kwlist.h里插入对应条目那新语法永远不可能被触发因为扫描器只会把这个词当普通标识符吐出去。2.3 未加引号的标识符会自动小写词法阶段的“隐藏规约”词法阶段还悄悄做了一件事把不带双引号的标识符全部折叠成小写。PostgreSQL的SQL标准行为是SELECT Name FROM Users和SELECT name FROM users可以完全等价因为扫描器在返回IDENT之前会调用downcase_truncate_identifier把大小写统一掉。只有写成Name这种带双引号的形式大小写才会被保留。这个行为看起来简单但直接影响后面语法分析和语义分析的设计。你说select * from Users解析到表达式时拿到的标识符字符串已经是被折叠成小写的parse_analyze阶段再拿它去和系统表里的真实表名做对比。所以如果你在gram.y里写规则时想去匹配某个固定列名永远不要假设用户输入是什么大小写统一按小写处理才是靠谱的。3. 移进规约的完整推演拿一条SELECT语句逐栈走一遍现在进入正题。Bison生成的解析器是一个下推自动机运行原理可以用“移进”和“规约”两个动作概括。虽然具体实现里还有接受动作和报错动作但大部分步骤都是在这两个动作之间来回切换。3.1 状态栈与前瞻token决定一切解析器内部维护一个状态栈每个状态代表“当前已经识别到文法中的哪些位置”。你可以把状态理解为文法分析里的“进度标记”。符号栈则记录已经读入的终结符和非终结符。解析器每次从词法接口拿一个lookahead token然后查分析表根据栈顶状态和这个token决定下一步动作移进把当前token压入符号栈同时压入一个新的状态表示“我已经接受了一个新符号”。规约如果栈顶的若干个符号恰好匹配某条产生式的右部就把它们全部弹出把产生式左部的非终结符压回符号栈同时执行这条规则对应的动作代码。接受整个输入被规约成起始符号解析成功。报错既不能移进也不能规约抛出语法错误。为了更直观我用一个简化文法举例。实际PG的expr规则比这复杂得多但核心逻辑完全一致。query: SELECT expr FROM id ; expr: NUMBER | expr expr | expr * expr ; id: IDENT ;输入SQLSELECT 1 2 * 3 FROM t整个分析过程大致如下表。这里把状态栈省略了只记录符号栈和剩余token的变化步骤动作符号栈剩余Token1移进SELECTSELECT1 2 * 3 FROM t2移进1SELECT 1 2 * 3 FROM t3规约expr - NUMBERSELECT expr 2 * 3 FROM t4移进SELECT expr 2 * 3 FROM t5移进2SELECT expr 2* 3 FROM t6规约expr - NUMBERSELECT expr expr* 3 FROM t7移进*SELECT expr expr *3 FROM t8移进3SELECT expr expr * 3FROM t9规约expr - expr * exprSELECT expr exprFROM t10规约expr - expr exprSELECT exprFROM t11移进FROMSELECT expr FROMt12移进tSELECT expr FROM t(空)13规约id - IDENTSELECT expr FROM id(空)14规约query - SELECT expr FROM idquery(空)15接受query(空)注意第7步是关键。当符号栈是SELECT expr expr、lookahead是*时解析器理论上既可以按expr expr先规约也可以把*移进栈里。如果在这里选择规约那么后续表达式会先算加法2会被当成(12)的右操作数结果就是(12)*3如果选择移进后续就会先算2*3再加1结果符合四则运算的常识。Bison遇上这种冲突时默认动作是“移进优先”。也就是说如果文法没有声明任何优先级1 2 * 3会被解析成1 (2 * 3)吗并不会这么简单——默认移进会让1 - 2 - 3也变成1 - (2 - 3)这就是右结合的效果。在实际的编程语言语法里加减法需要的是左结合所以你必须通过%left、%right声明来告诉Bison遇到冲突时应当规约还是继续移进。这个话题后面展开。3.2 状态栈解析器的“当前位置”上面把状态栈省掉了但理解状态栈有助于看懂gram.output里的冲突报告。每个状态本质上是一组LR(0)项目比如某个状态可能包含项目expr: expr expr .那个点表示“我已经识别到了expr expr这个右部的前三段现在期待更多输入或者可以准备归约”。当解析器决定移进时会根据当前状态和输入token转移到新状态决定规约时则按照产生式右部长度弹出若干状态然后根据弹出后露出的栈顶状态和左部非终结符切换到另一个新状态。这个过程可以类比成走迷宫状态是迷宫房间分析表是房间里的路牌token是你手中的钥匙移进是往深处走归约是退回岔路口然后换一条路继续前进。很多从递归下降parser转过来的人会问我看状态栈干嘛但在Bison里调试冲突和报错时的路径全靠这些状态。后面讲冲突排查时你会看到“State 123 conflicts: 1 shift/reduce”这样的报告那就直接对应一个具体状态内部的两难选择。3.3 没有优先级声明时表达式会怎样回到实际操作。PG的gram.y里对加减乘除都有类似声明%left - %left * / % %precedence UMINUS优先级从低到高排列%left表示左结合%right表示右结合%precedence表示这个优先级只用来决定谁先谁后不关心结合方向。为什么must声明还是以expr: expr expr为例当栈顶是expr expr、lookahead是时如果不声明%leftBison默认移进结果就是1 (2 3)数学上结果一样但如果换成减号就是1 - (2 - 3) 2完全错误。声明了%left -之后Bison就会在栈顶状态已经可以按expr expr归约、而下一个token是的情况下选择归约从而实现左结合得到(1 - 2) - 3。一元负号是另一个经典。PG里一元负号没有对应的expr: - exprtoken优先级可以借用直接写会产生一大堆冲突。解决办法是在规则末尾加%prec UMINUS人为指定这条规则使用UMINUS声明的优先级这样它就能压过一切二元运算符保证-1 * 2被解析成(-1) * 2而不是-(1 * 2)。4. gram.y里那些容易被忽略的设计空规则、优先级和瞬时二义性PG的语法规则读起来有一种独特感觉跟教科书上的迷你文法差别很大。很多规则都带optional前缀比如opt_all_clause、opt_target_list、into_clause这些看起来“可有可无”的规则实际上是降低冲突数量和保持规则可读性的关键。4.1 空产生式用“什么都不写”表达可选部分在Bison文法里如果某个语法成分允许为空最简单的写法是直接不写任何终结符用一个空右部表达。旧式PG代码里通常写成/* EMPTY */注释占位可读性更强Bison新版本也支持%empty关键字。实例如下opt_all_clause: ALL_P | %empty { $$ NIL; } ;解析器读到SELECT ALL name时会走ALL_P分支读到SELECT name时语法分析器会调用lexer拿下一个token发现不是ALL之后就选择空产生式完成规约这时opt_all_clause被规约成一个空值规则里的动作代码把$$设成NIL。空产生式非常有价值因为它让复杂语句的可选部分被拆成独立规则文法结构更像积木而不是一条规则里塞满各种[some_optional]。代价是解析器可能需要多进行几次规约实际上对性能影响很小因为空产生式不会触发任何语义动作。4.2 %prec和优先级优先级规则不只是token属性Bison的优先级规则是“作用于产生式”的。一个产生式如果右侧最后一个终结符有声明优先级那么规则就继承该优先级如果最后一个终结符没有声明或者你想覆盖它就可以用%prec显式指定。这个方法在PG里用得尤其多。比如a_expr ISNULL这类后缀条件或者a_expr BETWEEN b_expr AND b_expr右侧最后一个终结符AND有优先级但实际含义显然不是AND操作符的优先级。PG在规则上写%prec ISNULL之类来修正。你看源码时如果看到某条规则不自然先找它结尾有没有%prec这往往就是设计者刻意处理过的点。层级的拆分同样重要。PG把表达式拆成a_expr、b_expr、c_expr、func_expr等层次a_expr能接受最广的类型c_expr只接受最小粒度的常量、列引用、括号表达式。这等于把很多优先级关系直接固化在文法结构里而不是全依赖%left。两种方式各有取舍全依赖优先级声明会减少规则数量但冲突难以控制靠层级拆分规则更冗长但冲突更少、更可控。PG是两者并用你会在core.y里看到大量这种分层设计。4.3 瞬时二义性DISTINCT ON这类语法怎么不打架LR解析里最难处理的是那种“前两个token相同后面突然分道扬镳”的语法。比如SELECT DISTINCT a和SELECT DISTINCT ON (a) a两个语句都以SELECT DISTINCT开头如果文法把DISTINCT和DISTINCT ON放在同一个产生式里解析器在读完DISTINCT时需要立即决定是准备接受ON还是直接进入target list可它还没有看到ON这就会制造冲突。PG的解法是把它们拆成两条并列规则一条匹配SELECT DISTINCT target_list另一条匹配SELECT DISTINCT ON (expr_list) target_list。LR分析表在读到DISTINCT之后的状态里同时保留了这两种可能的进度等lookahead token变成ON时自然选择对应的路径变成普通标识符或*时选择另一条路径。这就是为什么我强调“移进规约”不只是一个理论概念——DISTINCT ON这类语法之所以能正确工作全靠状态栈和lookahead token的协同判断。同类场景还有LEFT JOIN与LEFT SEMI JOIN、GROUP BY与GROUP BY ROLLUP处理思路都类似。你写新语法时如果遇到“以相同前缀开始的不同分支”千万不要硬塞进同一个产生式照着PG这种拆分法来冲突会少很多。5. 冲突不是Bug但必须看懂检查shift/reduce conflict的方法Bison在生成解析器时会统计冲突数量。冲突分两类shift/reduce conflict和reduce/reduce conflict。很多没接触过Yacc系工具的人一看到conflict就慌以为自己的语法写错了。其实shift/reduce conflict是家常便饭只要你能看懂它出现在哪里、是不是预期的就不会有问题。5.1 用bison -v生成gram.output定位冲突修改PG语法后如果想要一份冲突报告可以在parser目录下手动跑bison加上-v参数cd src/backend/parser bison -v -d -o gram.c gram.y这条命令会生成gram.c、gram.h和gram.output。gram.output里每个状态的反引号都能看到。看到类似这种片段State 98 expr: expr expr . [$end, , *] expr: expr . * expr [*] * shift, and go to state 45 * [reduce using rule 21 (expr)] $default reduce using rule 21 (expr) state 98 conflicts: 1 shift/reduce这里的意思很明确状态98下解析器已经可以规约expr expr同时lookahead如果是*它也可以选择把*移进。Bison默认选择shift然后输出一个conflict提示。如果你声明了%left 和%left *Bison会在优先级对比后决定这里应当shift因为*优先级高于但报告中依然会保留一行conflict说明只是最终动作符合预期。实际操作时我习惯在修改gram.y之前先生成一份baseline的gram.output保存起来改完再生成一份用diff对比出现的新冲突。如果一份新语法引入了几十个conflict直接看完整报告会头大diff之后剩下的才是你的嫌疑对象。5.2 reduce/reduce conflict为什么更危险shift/reduce conflict可以用优先级和结合性消解而且默认动作是移进通常语义上还能接受。reduce/reduce conflict则完全不同它表示在同一个状态下存在两条产生式都可以完成规约解析器不知道该用哪一条。举个例子如果你同时写了expr: IDENT ; typename: IDENT ;某个状态下栈顶是一个IDENTlookahead又恰好指向下一个普通的标识符解析器就不知道这个IDENT应该被规约成expr还是typename。Bison默认选择文法规约中靠前的那一条但这纯属“破财消灾”很容易产生跟你预期不符的语义树。reduce/reduce conflict不能用优先级解决因为优先级比较的是“规约规则”和“移进token”这里两个候选都是规约没有可比对象。唯一的解决办法是改写文法让两条规则的左部能在一个更早的节点分流或者增加一个中间非终结符让决策延后。PG经历了二十多年演进gram.y里基本很少有正经的reduce/reduce conflict。如果你加了新语法后看到这类冲突多半是你在两条独立的规则里写了相同的右部结构需要重新设计。5.3 PG如何控制冲突总量PG的gram.y不是零冲突的但整体冲突数量保持在很低的水平。控制冲突主要靠三件事一是大量使用%left/%right/%nonassoc声明运算符优先级二是把表达式拆成多层非终结符三是把可选的SQL子句全部抽成opt_开头的规则避免规则里出现过长且含糊的token串。还有一个容易忽略的因素bison版本。不同版本的Bison对文法的默认处理有细微差别冲突数量会有波动。PG在官方文档里会写明推荐的最低bison版本如果你升级了系统的bison然后重新编译PGgram.output里的冲突数可能跟官方版本不完全一致。我在实际编译中就踩过这个坑所以每次切环境都会先确认bison版本再决定要不要重新生成gram.c。6. 动手实践给PG追加一个PRAGMA语句把改语法变成操作题理论聊再多不如实际改一次语法。这里以给PostgreSQL加一个简单的顶层语句PRAGMA name为例走一遍完整流程。注意这只是教学骨架生产环境要加的语句通常复杂得多但这个流程能帮你建立改gram.y的手感。6.1 从%token到kwlist.h再到规则的完整步骤第一步在gram.y的%token声明区域比如%token keyword PRAGMA_P。这里的keyword是语义值类型PRAGMA_P是token名后面跟的_P是为了防止跟C语言符号冲突。第二步打开src/include/parser/kwlist.h在按字母排序的正确位置插入一行PG_KEYWORD(pragma, PRAGMA_P, RESERVED_KEYWORD)记得必须按字母顺序插PG源码里明确要求这张表保持有序否则二分查找会出问题。第三步定义语法规则。为了简单我们把它跟顶层语句列表关联起来。找到stmtmulti相关的规则新增一个分支。同时定义一个pragmastmt规则大致长这样pragmastmt: PRAGMA_P qualified_name { PragmaStmt *n makeNode(PragmaStmt); n-item $2; $$ (Node *) n; } ;还需要在%type声明里给pragmastmt注册节点类型否则Bison不允许把它传给$$。如果你改的是core.y里的基础表达式则需要对应添加%type。第四步创建PragmaStmt这个节点结构。PG节点一般定义在src/include/nodes/parsenodes.h或专门的头文件里。你至少要给NodeTag加一个枚举值然后定义结构体并把头文件include进gram.y。这一步特别容易漏漏了就是编译报错。第五步在stmtmulti的产生式里注册新语句让raw_parser能接受它以分号结尾的顶层形式。完成之后整个语法才算真正接入了解析入口。6.2 重新生成gram.c与重编PG改完gram.y之后理论上PG的构建系统会检测到依赖变化并自动重新生成gram.c但我的习惯是显式执行make -C src/backend/parser gram.c这样能确保万无一失。如果bison报错或冲突数量出现异常这一步就会直接暴露不会等到后面编译时才炸。生成完gram.c之后正常走编译和安装make -C src/backend make install装好之后需要重启PostgreSQL服务然后进psql执行PRAGMA myname。如果一切顺利你会看到它被解析成功虽然因为没有对应的执行逻辑可能在实际运行时报“unrecognized statement type”之类的错误但至少语法解析阶段已经通过了。6.3 改语法时最容易翻车的三类报错第一类是bison直接报conflict数增加。这种情况下你觉得规则写得很自然但新的规则跟现有规则产生了歧义。思路就是跑bison -v生成gram.outputdiff新增冲突看状态报告再决定是加%prec还是改规则结构。第二类是C编译错误。比如makeNode(PragmaStmt)能编译过但报“unknown type name”或者NodeTag枚举值没加。这类错误定位比较简单include头文件就行但容易反复出现尤其是同时改了多个文件时。第三类最坑运行时报syntax error at or near pragma。这往往不是gram.y规则写错了而是kwlist.h里忘了加条目。扫描器不认识pragma这个新词就把它当成IDENT语法规则里的PRAGMA_P永远匹配不上。遇到“我明明加了规则却完全不生效”的情况优先检查kwlist.h。7. 排查解析问题时的几条实用经验即便你不动PG源码日常也会遇到需要排查SQL语法错误的时候。PostgreSQL的报错信息虽然相对友好但复杂SQL嵌套多了错误位置不一定能一眼看出问题。这里分享几个我在实际中验证过好用的排错手段。7.1 打开debug_print_parse看原始解析树PostgreSQL提供了一个很有用的配置参数debug_print_parse。把它设为on再重新加载配置日志里就会打印每条SQL对应生成的解析树结构。这个输出比你在SQL客户端里看到的执行计划早了好几个阶段能看到语法分析器到底把这条SQL理解成了什么形状。有时候一条SQL在planner阶段报错你以为是优化器的问题但一看解析树才发现原来某个表达式在语法层就被解析成了完全不同的结构。查这类问题时debug_print_parse比EXPLAIN更接近根因。7.2 看解析器内部状态yydebug开关如果你真的想看到移进规约的实时过程最硬核的办法是打开Bison的trace输出。这需要在编译时启用对应宏或者把gram.y里的%debug打开然后在运行时设置yydebug变量为1。开启后解析器会在标准错误流里输出每一步动作包括当前状态、lookahead token、是选择移进还是规约、规约的是哪条规则。生产环境肯定不能开这个但在一台开发机上自己编译一个debug版PG做实验效果非常直观。你看着trace输出再对照gram.output就能把之前纸上推演的内容真正印在脑子里。7.3 我自己总结的一套排查顺序遇到一条SQL在PG里报语法错误我按照这个顺序排查基本能覆盖90%的情况先确认token识别是否正确尤其当涉及新关键字或者用户自定义类型名时。把报错位置的词单独拿出来想扫描器会不会把它当成了IDENT。然后看规则匹配找到报错位置附近最可能命中的产生式判断用户输入是否符合该产生式的右部结构。如果不符合继续往上层找看有没有其他产生式能匹配。最后看语义分析阶段是否被误判成语法错误。有些报错信息是syntax error但根本原因是某个类型或函数不存在导致parse_analyze阶段在建设节点时失败。看报错里有没有“function ... does not exist”这种带有语义色彩的补充信息如果有就别再盯着语法规则死磕了。7.4 顺手再分享一个改代码前的习惯我会在启动任何gram.y修改前先把当前bison版本记录下来然后把gram.y、kwlist.h、core.y复制一份做备份。改完之后如果解析结果不对我第一件事不是改回代码而是用diff检查自己到底改了什么再决定要不要回滚。PostgreSQL语法文件环环相扣一个看似无害的改动可能会让几十条语句的解析路径发生变化这种基础工作做扎实了能省下大量排查时间。
返回列表