☰
Flex+Bison实现SQL解析器:从词法分析到AST构建
2026/10/13 11:49:27 网站建设 项目流程

简介:这是一份面向编译原理学习者、数据库开发初学者及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 expr,AST 节点类型全在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(yytext+1); 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被误判为SELECT+OR。而[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的处理还做了去引号操作(yytext+1跳过首',strlen-2去掉尾'),否则 AST 里会带着引号存。

2.2 parser.y:LR(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.c:Bison 生成的接口文件如何与手写代码桥接

运行bison -d parser.y后生成parser.tab.h和parser.tab.c。前者定义了 token 编号(SELECT=257)、yyparse()函数签名、yylvalunion 类型及YYSTYPEtypedef;后者是 LR(1) 自动机核心,包含状态转移表和yyparse()实现。关键桥接点有三处:

  1. #include "parser.tab.h"必须出现在lexer.l和所有.c文件顶部:否则SELECT宏和yylval类型不一致,编译报incompatible types。
  2. extern YYSTYPE yylval;声明要与parser.tab.h中typedef union { ... } YYSTYPE;严格一致:ast.c里若漏写此 extern,链接时yylval会是默认int类型,导致yylval.str写内存越界。
  3. 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*); #endif

ast.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, indent+1); printf("FROM\n"); print_ast(node->data.select.tables, indent+1); if (node->data.select.where) { printf("WHERE\n"); print_ast(node->data.select.where, indent+1); } 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, indent+1); print_ast(node->data.binary.right, indent+1); 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()遇到;就结束本次解析,返回 0,main.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.h,lexer.c编译会报SELECT undeclared,parser.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(f"Invalid 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 href="https://download.csdn.net/download/weixin_47156401/88265367" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

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

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

立即咨询