当前位置: 首页 > news >正文

在MiniOB源码中学习使用Flex与Bison解析SQL语句-第一节

前言

阅读本篇文章要求了解编译原理的基础知识,如:文法、词法分析、语法分析、语义分析等概念,需要对这些概念有基本的认识。

SQL语句解析的大致流程

像是C语言的编译,其大致流程为:

  1. 通过词法分析、语法分析和语义分析,得出四元式列表。
  2. 分析四元式列表,进行代码优化。
  3. 将四元式列表变为具体的汇编语言,使用汇编器将汇编文件转化为二进制文件。

我们可以将这个流程抽象成:

  1. 通过词法分析、语法分析和语义分析,得出可以存在于解析器程序中的数据结构,比如:四元式列表可能就是一个(list<Item>)。
  2. 分析数据结构,进行某种优化。
  3. 执行数据结构。如:将数据结构转化成二进制文件(让CPU读取指令并执行);将数据结构的数据提取出来,调用程序的方法来执行。

我们再将其套用到SQL语句的解析流程上:

  1. 通过词法分析、语法分析和语义分析,得出一个代码级别的数据结构。
  2. 分析语数据结构中关系代数相关的部分,进行关系代数优化。
  3. 优化后的语法分析树中有数据,那么这些数据就可以指导代码去执行查询、插入、建表等操作。

本篇文章,我们要了解的重点是步骤1:通过词法分析、语法分析和语义分析,得出一个代码级别的数据结构。

语法分析树,它确实是一棵树,但它不是代码数据结构层面的树,是语法分析过程的形象表述。

代码级别的数据结构

如果我们想要自己实现一个SQL语句的解析器,那么这个代码级别的数据结构应该是什么怎么样的呢?

假设这个SQL语句的解析器只需要解析CREATE TABLE语句,那么这个代码级别的数据结构可能是这样的:

enum class SqlCommandType {CREATE_TABLE
};struct SqlCommand {CommandType command_type;string table_name;vector<AttributeInfo> attributes;
};struct AttributeInfo {string attr_name;string attr_type;// ...
};

解析的时候将Command中的command_typetable_nameattributes设置上即可。

由于建表语句不涉及查询操作,因此不需要进行优化,直接进入执行步骤,数据库会提供一个create_table方法给你调用:

void create_table(string table_name, vector<AttributeInfo> attributes);

SQL解析器需要解析出什么样的数据结构,是这样被决定的:当前正在解析的命令的类型,决定需要调用哪些数据库的接口,数据库提供的接口需要的参数决定了SQL解析器必须解析出什么数据结构。

在解析出来了必要的数据结构之后,这些数据结构需要以什么样的方式存在是没有做规定的。我们可以根据需求进行设计。

比如,SQL解析器不只是需要解析一个命令,那么我们可以将存储解析结果的数据结构设计为:

enum class SqlCommandType {CREATE_TABLE,INSERT,SELECT,UPDATE
};struct SqlCommand {SqlCommandType command_type;
}struct CreateTableCommand : SqlCommand {string table_name;vector<AttributeInfo> attributes;
};struct InsertCommand : SqlCommand {// ... 暂时不知道要什么数据结构
};struct SelectCommand : SqlCommand {// ... 暂时不知道要什么数据结构
};struct UpdateCommand : SqlCommand {// ... 暂时不知道要什么数据结构
};

通过继承来组织这些必要的数据结构,这样方便后续的代码编写。比如:我们使用SqlCommand*指向任意类型的SQL命令,通过判断类型知道它具体是什么类型后,再将其转换回相应类型后使用。也可以在不转换类型的前提下,判断SQL命令是否应该执行优化操作。

MiniOB存储SQL语句解析结果的数据结构

ParsedSqlNode是MiniOB存放SQL语句解析结果的数据结构,我们可以直接查看其源码,看看它由什么构成:

/*** @brief 表示一个SQL语句* @ingroup SQLParser*/
class ParsedSqlNode
{
public:enum SqlCommandFlag flag;ErrorSqlNode        error;          // 代表SQL语句解析错误,存放错误的位置已经错误的信息CalcSqlNode         calc;           // SelectSqlNode       selection;InsertSqlNode       insertion;DeleteSqlNode       deletion;UpdateSqlNode       update;CreateTableSqlNode  create_table;DropTableSqlNode    drop_table;AnalyzeTableSqlNode analyze_table;CreateIndexSqlNode  create_index;DropIndexSqlNode    drop_index;DescTableSqlNode    desc_table;LoadDataSqlNode     load_data;ExplainSqlNode      explain;SetVariableSqlNode  set_variable;public:ParsedSqlNode();explicit ParsedSqlNode(SqlCommandFlag flag);
};

存放Create Table语句解析结果的数据结构

可以看到,MiniOB中并非采用继承来表示SQL语句的解析结果。

而是将所有代表语句解析结果的类作为成员变量放在了ParsedSqlNode中,SqlCommandFlag存放着语句类型,是哪个语句类型,对应的代表SQL语句解析结果的成员变量就会有效。

我猜你现在一定很好奇这些SqlNode里面到底存了什么。就看看CreateTableSqlNode吧:

/*** @brief 描述一个create table语句* @ingroup SQLParser* @details 这里也做了很多简化。*/
struct CreateTableSqlNode
{string                  relation_name;  // Relation namevector<AttrInfoSqlNode> attr_infos;     // attributesvector<string>          primary_keys;   // primary keys// TODO: integrate to CreateTableOptionsstring storage_format;  // storage formatstring storage_engine;  // storage engine
};

我们看到这里面的信息更我们前面所描述的差不多,主键方面有所偏差(我前面的例子有点欠考虑了)。

storage_formatstorage_engine应该是用来决定什么样的存储格式或者存储引擎的。

这些值将会怎么样被设置呢?这就需要我们查看bison文件:

create_table_stmt:    /*create table 语句的语法解析树*/CREATE TABLE ID LBRACE attr_def_list primary_key RBRACE storage_format{$$ = new ParsedSqlNode(SCF_CREATE_TABLE);CreateTableSqlNode &create_table = $$->create_table;create_table.relation_name = $3;//free($3);create_table.attr_infos.swap(*$5);delete $5;if ($6 != nullptr) {create_table.primary_keys.swap(*$6);delete $6;}if ($8 != nullptr) {create_table.storage_format = $8;}};

解析Create Table语句的过程

在bison中,产生式可以用左部: 右部1 {语义计算1}| 右部2 {语义计算2}| 右部3 {语义计算3};来进行表示,这里的create_table_stmt是一个左部(即非终结符)。

CREATE TABLE ID LBRACE attr_def_list primary_key RBRACE storaged_format是一个右部,它的组成为:

  1. CREATE终结符,是大小写不敏感的create
  2. TABLE终结符,是大小写不敏感的table
  3. ID终结符,标识符,其语义值是一个字符串,$3可以引用对应右部从左往右数第三个符号的语义值,在这里$3代表的就是ID终结符的语义值。
  4. LBRACE终结符,即左括号(
  5. attr_def_list非终结符,解析出属性信息,其语义值大概是一个属性信息的列表。
  6. primary_key非终结符,解析出主键信息,其语义值大概是一个主键名列表。
  7. RBRACE终结符,即右括号)
  8. storaged_format非终结符,语义值是一个字符串,代表存储格式。

这里我们发现没有解析storaged engine的语义计算部分,CreateTableSqlNode描述的一样,这部分的内容是TODO有待实现。

其语义计算部分干了的事情:

  1. 申请一个ParsedSqlNode并将其flag设置为SCF_CREATE_TABLE,代表这是一个CREATE TABLE命令。$$ = 这个赋值操作的含义是:设置产生式右部的语义值为指向一个ParsedSqlNode的指针。
  2. 然后为这个刚申请的ParsedSqlNode里的CreateTableSqlNode设置变量,relation_name是表名,3号位是表名的位置,而$3引用了它。
  3. 以此类推,将attr_infosprimary_keysstorage_format均设置上了。

如果要进一步深究,那就可以去研究attr_def_list产生式的语义计算如何得出attr_infos这个语义值的以及primary_key产生式是如何得出primary_keys这个语义值的。

我们先来看AttrInfoSqlNode是什么,才能知道attr_def_list要解析什么出来:

/*** @brief 描述一个属性* @ingroup SQLParser* @details 属性,或者说字段(column, field)*/
struct AttrInfoSqlNode
{AttrType type;    ///< Type of attributestring   name;    ///< Attribute namesize_t   length;  ///< Length of attribute
};

接下来我们就看看attr_def_list产生式中,单个AttrInfoSqlNode是如何被解析出来的,然后看看多个又是怎么处理的:

attr_def:ID type LBRACE number RBRACE {$$ = new AttrInfoSqlNode;$$->type = (AttrType)$2;$$->name = $1;$$->length = $4;}| ID type{$$ = new AttrInfoSqlNode;$$->type = (AttrType)$2;$$->name = $1;$$->length = 4;};
number:NUMBER {$$ = $1;};
type:INT_T      { $$ = static_cast<int>(AttrType::INTS); }| STRING_T { $$ = static_cast<int>(AttrType::CHARS); }| FLOAT_T  { $$ = static_cast<int>(AttrType::FLOATS); }| VECTOR_T { $$ = static_cast<int>(AttrType::VECTORS); };

type其实就是(大小写不敏感):

  • 单词是int就将其语义值设置成AttrType::INTS
  • 单词是char就将其语义值设置成AttrType::CHARS
  • 单词是float就将其语义值设置成AttrType::FLOATS
  • 单词是vector就将其语义值设置成AttrType::VECTORS

number很好理解,语义值就是一个数字。

attr_def有两种产生式:

  • ID type LBRACE number RBRACE,比如name char(10),这种的话AttrInfoSqlNode就将length设置为括号里面的数字。
  • ID type,比如age int或者salary floatlength就默认为4。

从这我们就可以发现attr_def的语义值是一个指向AttrInfoSqlNode的指针,里面已经存放好了解析完成的属性信息。

attr_def_list则将这些指针集中成一个数组:

attr_def_list:attr_def{$$ = new vector<AttrInfoSqlNode>;$$->emplace_back(*$1);delete $1;}| attr_def_list COMMA attr_def{$$ = $1;$$->emplace_back(*$3);delete $3;};

产生式推导到最后一定是一个attr_def_list: attr_def结尾,此时是构造数组的时机,因此这里构造一个数组,并将attr_def的语义值插入,最后释放指针(因为已经值已经存到数组中了)。

attr_def_list: attr_def_list COMMA attr_def,则是通过不断规约,然后将attr_def指向的AttrInfoSqlNode添加到数组中。

最后的结果就是attr_def_list的语义值就是括号内声明的所有属性的信息。

通过类似的方式去研究,primary_key:

primary_key:/* empty */{$$ = nullptr;}| COMMA PRIMARY KEY LBRACE attr_list RBRACE{$$ = $5;};
attr_list:ID {$$ = new vector<string>();$$->push_back($1);}| ID COMMA attr_list {if ($3 != nullptr) {$$ = $3;} else {$$ = new vector<string>;}$$->insert($$->begin(), $1);};

attr_list分析的大致思路和attr_def_list的思路差不多,只不过从attr_def变成了string,最终attr_list的语义值为一个string的列表,每个string都是一个属性名。

primary_key则是在attr_list的外面套了一层产生式,应该是为了添加语法规则,明确主键的声明方式。

storaged_format产生式比较简单,和primary_key一样是为了添加语法规则而存在的(不然的话直接放个ID就行了):

storage_format:/* empty */{$$ = nullptr;}| STORAGE FORMAT EQ ID{$$ = $4;};

在这个过程中,由flex生成的代码提供词法分析的能力,由bison生成的代码提供语法分析(从定义的产生式可以得到)和语义计算的能力(语义计算的代码由我们自己写)。

可以发现,我们只需要真正需要我们花时间的是语义计算部分该怎么写,我们只需要定义好产生式和语义计算的代码,而无需关心语法分析怎么搞(该采用LR分析法还是LL分析法,使用LR分析法怎么把状态机搞出来),也需要关心词法分析怎么搞(只需要定义匹配到某些字符串作出什么动作)。

如果你曾经编写过编译器(比如学校课程设计要求你写编译器)并且没有使用到flex和bison,你应该能够理解flex和bison所带来的便利。

下一节将尝试使用flex和bison实现自己的SQL解析器解析CREATE TABLE语句。

http://www.dtcms.com/a/351813.html

相关文章:

  • Rust 环境搭建与 SeekStorm 项目编译部署(支持中文)
  • Robrain V2.0正式登场:落地人形机器人,引爆智能进化革命
  • Ubuntu操作系统下使用mysql、mongodb、redis
  • [特殊字符] CentOS 7 升级 OpenSSH 10.0p2 完整教程(含 Telnet 备份)
  • 如果 我退休了
  • 汽车域控中Hypervisor方案极致安全原理与弊端
  • APP UI自动化测试的思路总结
  • 破解豆瓣Ajax动态加载:Python爬取完整长评论和短评
  • Java面试实战系列【JVM篇】- JVM内存结构与运行时数据区详解(私有区域)
  • 数据结构:链式队列尝试;0826
  • poi生成word固定表格列宽
  • Spring - 文件上传与下载:真正的企业开发高频需求——Spring Boot文件上传与下载全场景实践指南
  • 位运算卡常技巧详解
  • Charles抓包微信小程序请求响应数据
  • 信号无忧,转决千里:耐达讯自动化PROFIBUS集线器与编码器连接术
  • 快速了解卷积神经网络
  • springweb项目中多线程使用详解
  • 问:单证硕士含金量是否不足?
  • 【Linux 进程】进程程序替换
  • 【GitHub】使用SSH与GitHub交互
  • 工业大模型五层架构全景解析:从算力底座到场景落地的完整链路
  • PyCharm注释详解:TODO、文档注释、注释
  • MySQL 索引:结构、对比与操作实践指南
  • 【合适新人】预测图片教程——如何随机抽取验证集图片进行可视化推理!(附完整代码)
  • DigitalOcean GPU 选型指南(三):中端AI GPU性价比之王 RTX 4000 Ada、A4000、A5000
  • 无人机航拍数据集|第33期 无人机树冠目标检测YOLO数据集5842张yolov11/yolov8/yolov5可训练
  • 【HZ-T536开发板免费体验】无需死记 Linux 命令!用 CangjieMagic 在 HZ-T536 开发板上搭建 MCP 服务器,自然语言轻松控板
  • Java大厂面试全真模拟:从Spring Boot到微服务架构实战
  • 文本转语音TTS工具合集(下)
  • 【强化学习】区分理解: 时序差分(TD)、蒙特卡洛(MC)、动态规划(DP)