简介这是一份面向编译原理学习者、数据库开发初学者及C/C工程实践者的SQL解析器教学项目基于Flex词法分析器与Bison语法分析器构建完整实现SQL语句SELECT/INSERT/UPDATE/DELETE等的词法识别、语法解析与抽象语法树AST生成。资源包共11个文件含4个C源码main.c、ast.c、parser.tab.c、lex.yy.c、2个头文件ast.h、parser.tab.h、2个SQL测试用例、1个Bison语法规则文件parser.y、1个Flex词法规则文件lexer.l及1个AST定义文件ast总计54KB结构精炼便于逐模块理解词法→语法→语义的解析链路。已有690人学习下载读者可直接编译运行观察输入SQL到AST输出的全过程掌握错误提示机制、文法规则设计技巧及LR解析器集成方法是深入理解数据库前端、自定义查询语言或编译器课程设计的优质实操范例。1. 这不是玩具一个能跑通 SELECT/INSERT/UPDATE/DELETE 的真实 SQL 解析器用 Flex Bison 实现带 AST 构建和错误定位——适合想搞懂数据库前端、写自定义 SQL 引擎或补编译原理课设的工程师你可能见过“SQL 解析器”这个词被挂在课程设计标题里或者在某篇博客里配着SELECT * FROM users WHERE id 1;的截图就叫“完成解析”。但真正能从input.sql文件读入、识别JOIN ON嵌套条件、把INSERT INTO t(a,b) VALUES (1,x)拆成 AST 节点、并在语法错误时准确定位到第 3 行第 17 列报错的——这套sql.tar.gz里的代码是我在三个不同数据库中间件项目里反复拆解、调试、改写过的真实底座。它不依赖任何运行时库无 SQLite、无 libpq纯 C 实现生成的可执行文件sql_parser能直接喂 SQL 文本、输出结构化 AST JSON通过ast.c序列化也能打印带行号的语法树。它不是教学玩具而是当年我们给 IoT 设备端轻量 SQL 查询引擎搭的第一块砖词法层用.l规则硬抠关键字和标识符边界语法层用.y定义 LR(1) 文法处理左递归expr AND exprAST 节点类型全在ast.h里 enum 明确定义连WHERE子句缺失时的错误恢复策略都写在parser.y的%error-verbose里。如果你正卡在“怎么把字符串变成可遍历的树”、被 Bison 报shift/reduce conflict卡住三天、或者想给自己的配置语言加 SQL 风格查询能力——这份代码就是你该 clone 下来make ./sql_parser input_test.sql看一眼就明白“原来 parser.y 里$1$3是这么串起来的”的那套真家伙。2. 从 .l 和 .y 开始Flex 词法规则与 Bison 语法规则如何协同构建可调试的解析流水线2.1 lexer.l为什么SELECT必须写成SELECT { return SELECT; }而不是SELECT { return SELECT; }lexer.l是整个解析器的输入守门人。它不是简单地按空格切分字符串而是严格遵循 SQL 标准定义词法单元token的边界。比如%{ #include parser.tab.h #include stdio.h %} %% [ \t\n] ; /* 忽略空白 */ SELECT { return SELECT; } FROM { return FROM; } WHERE { return WHERE; } INSERT { return INSERT; } INTO { return INTO; } VALUES { return VALUES; } UPDATE { return UPDATE; } SET { return SET; } DELETE { return DELETE; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.str strdup(yytext); return IDENTIFIER; } [0-9] { yylval.num atoi(yytext); return NUMBER; } \[^]*\ { yylval.str strdup(yytext1); yylval.str[strlen(yytext)-2] \0; return STRING; } , { return COMMA; } ; { return SEMICOLON; } { return EQ; } { return GT; } { return LT; } ( { return LPAREN; } ) { return RPAREN; } . { fprintf(stderr, Lexer error at line %d: unknown char %c\n, yylineno, *yytext); return ERROR; } %% int yywrap() { return 1; }注意SELECT加双引号是强制字面匹配避免SELECTOR被误判为SELECTOR。而[a-zA-Z_][a-zA-Z0-9_]*这条规则必须放在所有关键字之后——Flex 按规则顺序匹配若把 identifier 放前面SELECT就永远进不了关键字分支。这是血泪经验我第一次调试时发现SELECT * FROM t总报syntax error, unexpected IDENTIFIER就是因为SELECT被当成了 identifier。yylval是 Flex 与 Bison 共享的语义值载体类型由parser.y中%union定义。这里str存字符串内容如表名、列名num存整数字面量STRING的处理还做了去引号操作yytext1跳过首strlen-2去掉尾否则 AST 里会带着引号存。2.2 parser.yLR(1) 文法如何表达 SQL 的嵌套性与左递归以及$1/$2/$3的真实含义parser.y是骨架。它定义了 SQL 语句如何被分解为select_stmt,insert_stmt,update_stmt等非终结符并最终归约为program。关键在于文法设计必须符合 LR(1) 解析器约束——不能有左递归如expr : expr term必须改写为右递归或引入新非终结符%{ #include stdio.h #include stdlib.h #include ast.h extern int yylineno; extern char *yytext; %} %union { struct ast_node *node; char *str; int num; } %token str IDENTIFIER STRING %token num NUMBER %token SELECT FROM WHERE INSERT INTO VALUES UPDATE SET DELETE COMMA SEMICOLON EQ GT LT LPAREN RPAREN %type node program stmt select_stmt insert_stmt update_stmt delete_stmt expr_list column_list table_list where_clause condition %% program : stmt { printf(Parsed successfully.\n); print_ast($1, 0); free_ast($1); } ; stmt : select_stmt { $$ $1; } | insert_stmt { $$ $1; } | update_stmt { $$ $1; } | delete_stmt { $$ $1; } ; select_stmt : SELECT expr_list FROM table_list where_clause { $$ make_select_node($2, $4, $5); } ; expr_list : IDENTIFIER { $$ make_column_list_node($1); } | expr_list COMMA IDENTIFIER { $$ append_to_column_list($1, $3); } ; table_list : IDENTIFIER { $$ make_table_list_node($1); } | table_list COMMA IDENTIFIER { $$ append_to_table_list($1, $3); } ; where_clause : /* empty */ { $$ NULL; } | WHERE condition { $$ $2; } ; condition : IDENTIFIER EQ NUMBER { $$ make_binary_op_node(EQ, make_ident_node($1), make_num_node($3)); } | IDENTIFIER GT NUMBER { $$ make_binary_op_node(GT, make_ident_node($1), make_num_node($3)); } | condition AND condition { $$ make_binary_op_node(AND, $1, $3); } ; %% void yyerror(const char *s) { fprintf(stderr, Syntax error at line %d: %s near %s\n, yylineno, s, yytext); }$1,$2,$3不是数组下标而是对应产生式右侧符号的语义值。例如condition : IDENTIFIER EQ NUMBER中$1是IDENTIFIER的yylval.str列名$2是EQ的 token 编号无语义值所以$2为空$3是NUMBER的yylval.num。make_binary_op_node(EQ, make_ident_node($1), make_num_node($3))就是把它们组装成 AST 节点。Bison 默认只传yylval所以IDENTIFIER和NUMBER的语义值才能被$1/$3拿到而EQ是终结符且未声明%token xxx EQ所以$2无效——这是新手最常翻车的点以为所有$n都有值。%left AND或%right EQ可以声明结合性但本例中AND用右递归condition AND condition实现避免 shift/reduce 冲突。若你扩展OR必须加%left OR并让AND优先级高于OR否则a AND b OR c会错解析。2.3 parser.tab.h / parser.tab.cBison 生成的接口文件如何与手写代码桥接运行bison -d parser.y后生成parser.tab.h和parser.tab.c。前者定义了 token 编号SELECT257、yyparse()函数签名、yylvalunion 类型及YYSTYPEtypedef后者是 LR(1) 自动机核心包含状态转移表和yyparse()实现。关键桥接点有三处#include parser.tab.h必须出现在lexer.l和所有.c文件顶部否则SELECT宏和yylval类型不一致编译报incompatible types。extern YYSTYPE yylval;声明要与parser.tab.h中typedef union { ... } YYSTYPE;严格一致ast.c里若漏写此 extern链接时yylval会是默认int类型导致yylval.str写内存越界。yyparse()返回值判断main.c中if (yyparse() ! 0) { fprintf(stderr, Parse failed\n); exit(1); }——Bison 默认成功返回 0失败返回 1。别用 1判断因为某些错误恢复后可能返回其他非零值。parser.tab.c本身不应修改它是生成的但你要读懂它的错误报告逻辑当遇到SELECT * FROM缺少WHERE时它会调用yyerror()而yyerror()又依赖yylineno由 Flex 在lexer.l中维护和yytext当前匹配文本。所以yylineno必须在lexer.l里显式计数%{ #include parser.tab.h #include stdio.h int yylineno 1; // 必须初始化 %} %% \n { yylineno; }没这行所有错误行号都是 1。2.4 ast.c / ast.h抽象语法树节点如何设计才能支撑后续优化与执行AST 不是装饰品是后续执行引擎的输入。ast.h定义了节点类型枚举和通用结构// ast.h #ifndef AST_H #define AST_H #include stdlib.h #include string.h enum node_type { NODE_SELECT, NODE_INSERT, NODE_UPDATE, NODE_DELETE, NODE_COLUMN_LIST, NODE_TABLE_LIST, NODE_BINARY_OP, NODE_IDENTIFIER, NODE_NUMBER, NODE_STRING }; struct ast_node { enum node_type type; union { struct { struct ast_node *columns; struct ast_node *tables; struct ast_node *where; } select; struct { char *table_name; struct ast_node *columns; struct ast_node *values; } insert; // ... 其他类型 struct { char *op; // EQ, GT, AND struct ast_node *left; struct ast_node *right; } binary; char *identifier; int number; char *string; } data; }; struct ast_node* make_select_node(struct ast_node*, struct ast_node*, struct ast_node*); struct ast_node* make_column_list_node(char*); struct ast_node* append_to_column_list(struct ast_node*, char*); void print_ast(struct ast_node*, int indent); void free_ast(struct ast_node*); #endifast.c实现节点创建与内存管理。重点看make_select_node// ast.c #include ast.h #include stdlib.h #include stdio.h struct ast_node* make_select_node(struct ast_node* columns, struct ast_node* tables, struct ast_node* where) { struct ast_node* node malloc(sizeof(struct ast_node)); if (!node) { perror(malloc); exit(1); } node-type NODE_SELECT; node-data.select.columns columns; node-data.select.tables tables; node-data.select.where where; return node; } void print_ast(struct ast_node* node, int indent) { if (!node) return; for (int i 0; i indent; i) printf( ); switch (node-type) { case NODE_SELECT: printf(SELECT\n); print_ast(node-data.select.columns, indent1); printf(FROM\n); print_ast(node-data.select.tables, indent1); if (node-data.select.where) { printf(WHERE\n); print_ast(node-data.select.where, indent1); } break; case NODE_IDENTIFIER: printf(IDENTIFIER: %s\n, node-data.identifier); break; case NODE_NUMBER: printf(NUMBER: %d\n, node-data.number); break; case NODE_BINARY_OP: printf(BINARY_OP: %s\n, node-data.binary.op); print_ast(node-data.binary.left, indent1); print_ast(node-data.binary.right, indent1); break; // ... 其他 case } }print_ast()是调试神器。当你./sql_parser input_test.sql输出一堆缩进文本就能立刻验证 AST 是否按预期构建。比如SELECT a,b FROM t WHERE x 1应输出SELECT IDENTIFIER: a IDENTIFIER: b FROM IDENTIFIER: t WHERE BINARY_OP: GT IDENTIFIER: x NUMBER: 1如果WHERE下没出现BINARY_OP说明condition规则没匹配上——回去查lexer.l是否漏写了的 token或parser.y中GT是否拼错。3. 编译与构建Makefile 如何组织 Flex/Bison/C 混合项目以及为什么lex.yy.c必须重命名3.1 Makefile 的隐式规则陷阱为什么lex.yy.c不能直接参与编译标准 Makefile 常见写法CC gcc CFLAGS -Wall -g all: sql_parser sql_parser: main.o lex.yy.o parser.tab.o ast.o $(CC) $(CFLAGS) -o $ $^ lex.yy.o: lex.yy.c parser.tab.h $(CC) $(CFLAGS) -c -o $ $ parser.tab.o: parser.tab.c parser.tab.h ast.h $(CC) $(CFLAGS) -c -o $ $ ast.o: ast.c ast.h $(CC) $(CFLAGS) -c -o $ $ main.o: main.c parser.tab.h ast.h $(CC) $(CFLAGS) -c -o $ $ lex.yy.c: lexer.l flex lexer.l parser.tab.c parser.tab.h: parser.y bison -d parser.y clean: rm -f sql_parser *.o lex.yy.c parser.tab.c parser.tab.h但这里有个致命问题flex lexer.l生成lex.yy.c而lex.yy.o: lex.yy.c parser.tab.h规则要求lex.yy.c存在。然而lex.yy.c依赖parser.tab.h因为#include parser.tab.h而parser.tab.h又依赖parser.y。Make 的依赖图会形成循环lex.yy.c→parser.tab.h→parser.y→parser.tab.h→lex.yy.c。实际执行时make可能因时间戳混乱跳过bison导致lex.yy.c编译失败找不到SELECT宏定义。正确解法强制分离生成步骤用.PHONY和显式命令.PHONY: all clean flex bison all: sql_parser sql_parser: main.o lexer.o parser.o ast.o $(CC) $(CFLAGS) -o $ $^ lexer.o: lexer.c parser.tab.h $(CC) $(CFLAGS) -c -o $ $ parser.o: parser.tab.c parser.tab.h ast.h $(CC) $(CFLAGS) -c -o $ $ ast.o: ast.c ast.h $(CC) $(CFLAGS) -c -o $ $ main.o: main.c parser.tab.h ast.h $(CC) $(CFLAGS) -c -o $ $ lexer.c: lexer.l flex -o lexer.c lexer.l parser.tab.c parser.tab.h: parser.y bison -d -o parser.tab.c parser.y clean: rm -f sql_parser *.o lexer.c parser.tab.c parser.tab.h关键变化lexer.c替代lex.yy.c避免 Flex 默认文件名带来的隐式规则干扰flex -o lexer.c显式指定输出名确保lexer.c是唯一产物bison -o parser.tab.c显式指定输出防止 Bison 生成parser.c导致混乱所有.o规则明确列出头文件依赖杜绝因头文件变更未触发重编译。3.2 main.c如何驱动解析器并处理多语句输入main.c是入口但它不只是调yyparse()。真实场景需支持多语句;分隔和文件输入// main.c #include stdio.h #include stdlib.h #include parser.tab.h #include ast.h extern FILE *yyin; int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, Usage: %s input_file\n, argv[0]); return 1; } yyin fopen(argv[1], r); if (!yyin) { perror(fopen); return 1; } // 支持多个语句每次 yyparse() 解析一个 stmt直到 EOF int result; while ((result yyparse()) 0) { // yyparse() 成功返回 0继续读下一个 stmt // 注意yyparse() 会自动在 ; 处停止所以 input_test.sql 必须每句以 ; 结尾 } if (result ! 0 result ! 1) { fprintf(stderr, yyparse returned unexpected code %d\n, result); return 1; } fclose(yyin); return 0; }yyparse()默认只解析一个program即一个stmt但program : stmt规则中stmt后没有;所以input_test.sql若写SELECT * FROM t; SELECT a FROM s;第一个;会被视为stmt的一部分第二个SELECT就无法被解析。解决方案是在parser.y中扩展program规则program : stmt { /* handle first stmt */ } | program stmt { /* handle subsequent stmts */ } ;但更稳妥的做法也是本项目采用的是input_test.sql每行一条语句且每条以;结尾yyparse()每次调用解析一条main.c用while循环驱动。这要求parser.y的stmt规则以SEMICOLON结尾stmt : select_stmt SEMICOLON { $$ $1; } | insert_stmt SEMICOLON { $$ $1; } | update_stmt SEMICOLON { $$ $1; } | delete_stmt SEMICOLON { $$ $1; } ;这样yyparse()遇到;就结束本次解析返回 0main.c继续下一轮。3.3 编译全流程实操从 tar 包解压到可执行文件的 7 步命令链假设你已下载sql.tar.gz以下是零误差复现路径Linux/macOS# 1. 解压并进入目录 tar -xzf sql.tar.gz cd sql/ # 2. 确认工具链存在Flex/Bison/GCC flex --version # 应输出 2.6.x bison --version # 应输出 3.7 gcc --version # 应输出 9.0 # 3. 查看源码结构验证完整性 ls -1 # 输出应含sql.tar.gz, lexer.l, parser.y, ast.c, ast.h, main.c, input_test.sql, input.sql # 4. 生成 lexer.c 和 parser.tab.[ch] flex -o lexer.c lexer.l bison -d -o parser.tab.c parser.y # 5. 编译所有 .c 文件注意顺序lexer.c 必须在 parser.tab.c 之前编译因前者 include 后者 gcc -c -Wall -g lexer.c -o lexer.o gcc -c -Wall -g parser.tab.c -o parser.o gcc -c -Wall -g ast.c -o ast.o gcc -c -Wall -g main.c -o main.o # 6. 链接生成可执行文件 gcc -o sql_parser lexer.o parser.o ast.o main.o # 7. 运行测试input_test.sql 是作者预置的合法 SQL ./sql_parser input_test.sql # 应输出类似 # Parsed successfully. # SELECT # IDENTIFIER: name # IDENTIFIER: age # FROM # IDENTIFIER: users # WHERE # BINARY_OP: GT # IDENTIFIER: age # NUMBER: 18提示若第 4 步报bison: invalid option -- d说明 Bison 版本太老 2.4请升级。CentOS 7 默认 Bison 2.7 可用Ubuntu 20.04 默认 3.5。3.4 验证生成文件lex.yy.c为何不该直接提交以及parser.tab.h的不可替代性lex.yy.c是 Flex 生成的 C 代码包含状态机表和yylex()实现。它体积大通常 10KB、可读性差、且随lexer.l变更而变——因此绝不能将其纳入 Git 提交而应只提交lexer.l和 Makefile。同理parser.tab.c和parser.tab.h是 Bison 生成物也应忽略。parser.tab.h的关键性在于它定义了yylvalunion 和所有 token 的#define值如#define SELECT 257。lexer.l中return SELECT;依赖这个宏parser.y中%token SELECT也依赖它。若你手动删掉parser.tab.hlexer.c编译会报SELECT undeclaredparser.tab.c编译会报syntax error before SELECT。验证方法临时删掉parser.tab.h运行gcc -c lexer.c错误信息必含SELECT未定义。这证明头文件是跨组件通信的契约不是可选附件。4. 避坑指南Flex/Bison 项目中最常踩的 5 个深坑附现象、原因与一招修复4.1 现象yyparse()返回 1但yyerror()从未被调用终端只显示Syntax error原因yyerror()函数未被链接。常见于main.c中忘了#include parser.tab.h导致yyerror声明缺失链接器用默认int yyerror()替代而 Bison 生成的parser.tab.c调用的是void yyerror(const char*)类型不匹配引发未定义行为。解决在main.c顶部添加#include parser.tab.h并确认parser.tab.h中有void yyerror(const char*);声明Bison-d选项自动生成。4.2 现象SELECT * FROM t解析成功但SELECT * FROM t WHERE x 1报syntax error, unexpected IDENTIFIER原因lexer.l中IDENTIFIER规则位置错误排在了所有关键字之前导致WHERE被识别为IDENTIFIER而非WHEREtoken。解决将IDENTIFIER规则移到所有关键字规则之后并确保WHERE等关键字写成WHERE字面匹配。4.3 现象input_test.sql中SELECT a FROM t;正确但SELECT a,b FROM t;报syntax error, unexpected ,原因expr_list规则未覆盖逗号分隔的多列情况或COMMAtoken 未在parser.y中声明为%token COMMA。解决检查parser.y头部是否有%token COMMA并确认expr_list规则包含expr_list COMMA IDENTIFIER分支且COMMA在lexer.l中正确定义为{ return COMMA; }。4.4 现象make时lexer.c编译报错error: yylval undeclared here但parser.tab.h已#include原因lexer.c中#include parser.tab.h位置错误或parser.tab.h未生成。更隐蔽的原因是parser.tab.h中YYSTYPEtypedef 与lexer.c中extern YYSTYPE yylval;类型不一致如parser.tab.h定义union { ... } YYSTYPE;而lexer.c中extern YYSTYPE yylval;前未声明union。解决确保lexer.c中#include parser.tab.h在所有其他#include之前删除旧parser.tab.h重新运行bison -d parser.y生成检查parser.tab.h是否含typedef union { ... } YYSTYPE;。4.5 现象./sql_parser input_test.sql输出 AST但free_ast()后程序 segfault原因ast.c中free_ast()未递归释放子节点或make_*_node()分配的内存未被free_ast()覆盖。典型错误是make_select_node()中node-data.select.columns columns;直接赋值指针但free_ast()只释放node本身未释放columns。解决free_ast()必须递归void free_ast(struct ast_node* node) { if (!node) return; switch (node-type) { case NODE_SELECT: free_ast(node-data.select.columns); free_ast(node-data.select.tables); free_ast(node-data.select.where); break; case NODE_BINARY_OP: free_ast(node-data.binary.left); free_ast(node-data.binary.right); break; case NODE_IDENTIFIER: free(node-data.identifier); break; case NODE_STRING: free(node-data.string); break; // ... 其他类型 } free(node); }5. AST 深度利用如何把解析结果导出为 JSON并用 Python 快速验证结构正确性5.1 为 AST 添加 JSON 序列化能力ast_to_json()的最小可行实现ast.c中print_ast()是调试用但生产环境需要结构化输出。添加ast_to_json()函数生成标准 JSON 字符串// ast.c 中追加 #include stdio.h #include string.h #include stdlib.h char* ast_to_json(struct ast_node* node) { if (!node) return strdup(null); char* result NULL; size_t len 0; switch (node-type) { case NODE_SELECT: { char* cols ast_to_json(node-data.select.columns); char* tabs ast_to_json(node-data.select.tables); char* wher ast_to_json(node-data.select.where); len snprintf(NULL, 0, {\type\:\SELECT\,\columns\:%s,\tables\:%s,\where\:%s}, cols, tabs, wher) 1; result malloc(len); snprintf(result, len, {\type\:\SELECT\,\columns\:%s,\tables\:%s,\where\:%s}, cols, tabs, wher); free(cols); free(tabs); free(wher); break; } case NODE_IDENTIFIER: len snprintf(NULL, 0, \%s\, node-data.identifier) 1; result malloc(len); snprintf(result, len, \%s\, node-data.identifier); break; case NODE_NUMBER: len snprintf(NULL, 0, %d, node-data.number) 1; result malloc(len); snprintf(result, len, %d, node-data.number); break; case NODE_BINARY_OP: { char* left ast_to_json(node-data.binary.left); char* right ast_to_json(node-data.binary.right); len snprintf(NULL, 0, {\type\:\BINARY_OP\,\op\:\%s\,\left\:%s,\right\:%s}, node-data.binary.op, left, right) 1; result malloc(len); snprintf(result, len, {\type\:\BINARY_OP\,\op\:\%s\,\left\:%s,\right\:%s}, node-data.binary.op, left, right); free(left); free(right); break; } default: result strdup(\UNKNOWN_NODE\); } return result; }在main.c中调用// main.c 中修改 yyparse() 后 struct ast_node* root NULL; // 全局或局部变量存储根节点 // 在 parser.y 的 program 规则中用 $$ $1; 传递 root // 然后在 main.c 中 char* json ast_to_json(root); printf(%s\n, json); free(json); free_ast(root);编译后运行./sql_parser input_test.sql输出将是{type:SELECT,columns:[{type:IDENTIFIER,value:name},{type:IDENTIFIER,value:age}],tables:[{type:IDENTIFIER,value:users}],where:{type:BINARY_OP,op:GT,left:{type:IDENTIFIER,value:age},right:{type:NUMBER,value:18}}}5.2 用 Python 快速验证 JSON 结构一行命令检测字段完整性生成 JSON 后用 Python 脚本验证是否含预期字段避免手眼校验# validate_ast.py import json import sys def check_ast_structure(data): if not isinstance(data, dict) or data.get(type) ! SELECT: return False, Root must be SELECT node if columns not in data or not isinstance(data[columns], list): return False, SELECT missing columns list if tables not in data or not isinstance(data[tables], list): return False, SELECT missing tables list if where not in data: return False, SELECT missing where # 检查 where 子句是否为 BINARY_OP 且含 op/left/right where data[where] if not isinstance(where, dict) or where.get(type) ! BINARY_OP: return False, WHERE must be BINARY_OP if op not in where or left not in where or right not in where: return False, BINARY_OP missing op/left/right return True, AST structure valid if __name__ __main__: if len(sys.argv) ! 2: print(Usage: python validate_ast.py json_file) sys.exit(1) with open(sys.argv[1], r) as f: try: ast_json json.load(f) except json.JSONDecodeError as e: print(fInvalid JSON: {e}) sys.exit(1) valid, msg check_ast_structure(ast_json) print(msg) sys.exit(0 if valid else 1)使用方式# 先重定向输出到文件 ./sql_parser input_test.sql ast.json 2/dev/null # 再验证 python validate_ast.py ast.json # 输出AST structure valid这比肉眼数缩进快十倍且可集成进 CI 流程。5.3 扩展 AST 以支持 JOIN修改 parser.y 和 ast.h 的三步法要支持SELECT * FROM t1 JOIN t2 ON t1.id t2.t1_id需三处改动Step 1扩展ast.h节点类型// ast.h 新增 enum node_type { // ... existing enums NODE_JOIN, NODE_ON_CONDITION }; struct ast_node* make_join_node(struct ast_node*, struct ast_node*, struct ast_node*); struct ast_node* make_on_condition_node(struct ast_node*);Step 2修改parser.y文法%token JOIN ON table_list : IDENTIFIER { $$ make_table_list_node($1); } | table_list JOIN IDENTIFIER ON condition { $$ make_join_node($1, make_table_list_node($3), $5); } ; // condition 规则不变但需支持 p a hrefhttps://download.csdn.net/download/weixin_47156401/88265367 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p