上一篇文章,分析了语法解析,而实现"语义校验"是 SQL 质量管控中比"语法校验"更进一步的关键步骤。语法校验只能保证 SQL "写得对"(结构正确),而语义校验能保证 SQL "写得通"(逻辑合理、对象存在、权限足够)。
实现方案分为两大类:基于数据库内核的预编译(Prepare) 和 基于元数据的 Schema-Aware 解析。
方案一:基于数据库内核的预编译 (Prepare/Execute)
这是最准确、最"偷懒"的方法。利用数据库自带的优化器和元数据,让数据库引擎自己去检查 SQL 是否合法。
核心原理
在数据库连接上执行 PREPARE 命令(或使用编程语言中的 PrepareStatement),数据库会执行以下流程:
- 词法/语法解析:检查 SQL 结构。
- 语义解析:去元数据字典(data dictionary)中查找表是否存在、列是否存在、类型是否匹配、用户是否有权限。
- 生成执行计划 :但不执行它。
实现方式 (以 Java/Python 为例)
1. Java (JDBC)
Connection conn = dataSource.getConnection();
String sql = "SELECT name FROM users WHERE id = ?";
try {
// 这一步会触发数据库的解析和语义校验
// 如果表不存在或字段不存在,这里会抛出 SQLException
PreparedStatement pstmt = conn.prepareStatement(sql);
System.out.println("SQL 语义校验通过 (通过 Prepare)");
pstmt.close();
} catch (SQLException e) {
System.err.println("SQL 校验失败: " + e.getMessage());
}
2. Python (PyMySQL/MySQL-Client)
import pymysql
conn = pymysql.connect(host='localhost', user='root', passwd='', db='test')
cursor = conn.cursor()
sql = "SELECT name FROM non_exist_table WHERE id = %s"
try:
# 使用 cursor.mogrify 或直接 execute 在某些驱动中会先解析
# 更稳妥的是尝试创建 PreparedStatement (具体取决于驱动实现)
# 或者直接执行 EXPLAIN (见下文)
cursor.execute(f"EXPLAIN {sql}")
print("语法及基本语义校验通过")
except Exception as e:
print(f"校验失败: {e}")
finally:
conn.close()
3. 使用 EXPLAIN 命令
如果不支持 Prepare 或为了只读校验,可以在 SQL 前面加上 EXPLAIN(或 EXPLAIN FORMAT=JSON)。
-- 数据库会解析这条语句,检查表和字段,生成执行计划,但不真正去磁盘读数据。
EXPLAIN SELECT name FROM users WHERE id = 1;
- 优点:绝对准确,涵盖了数据库引擎所有的校验逻辑(包括权限、视图展开等)。
- 缺点:必须连接到真实的数据库实例(哪怕是测试库),有轻微的性能开销,且如果是 DML(INSERT/UPDATE),需要确保在事务中回滚,避免污染测试数据。
方案二:基于元数据的 Schema-Aware 解析 (离线校验)
如果你不想连接数据库,或者需要在代码提交阶段(Pre-Commit)就拦截错误,就需要构建一个"知道表结构"的解析器。
核心原理
- 获取元数据 :从数据字典(如
information_schema)导出表结构,或者读取建表语句(DDL)。 - 绑定上下文:将这些元数据(Schema)注入到 SQL 解析器中。
- 执行校验:解析 SQL 生成 AST,并遍历 AST,检查引用的表/字段是否在元数据中存在。
实现步骤与工具
1. 使用 ShardingSphere (推荐,基于 Java)
ShardingSphere 支持加载规则和元数据,可以进行离线语义校验。
// 1. 定义元数据 (模拟从数据库读取的表结构)
ColumnMetaData idColumn = new ColumnMetaData("id", Types.INTEGER, true); // 字段名, 类型, 是否主键
ColumnMetaData nameColumn = new ColumnMetaData("name", Types.VARCHAR, false);
TableMetaData userTable = new TableMetaData("users", Arrays.asList(idColumn, nameColumn));
// 2. 注册到元数据资源中
MetaDataLoader metaLoader = new MetaDataLoader() {
@Override
public Collection<TableMetaData> load() {
return Arrays.asList(userTable);
}
};
// 3. 使用解析引擎 (前面提到的 SQLParserEngine)
// 在解析 SQL 时,解析器会去查找 userTable,如果 SQL 里写了 "age" 字段而元数据里没有,就会报错。
2. 使用 SQLGlot (Python/通用,推荐)
SQLGlot 是一个现代的 SQL 解析器和编译器,支持多方言,且可以绑定 Catalog(元数据)。
from sqlglot import parse_one, exp
from sqlglot.catalog import Catalog
# 1. 构建元数据 (Catalog)
catalog = Catalog()
# 假设定义了一个表 users,有字段 id 和 name
catalog.add_table("users", {"id": "INT", "name": "TEXT"})
# 2. 解析并校验
sql = "SELECT name, age FROM users" # 注意:这里写了不存在的 'age'
try:
# 使用 transform 或 parse 并传入 catalog
# SQLGlot 可以遍历 AST,检查每一个 Column 是否在 Table 的 Schema 中
expression = parse_one(sql)
# 手动遍历或使用其提供的校验器检查列引用
for column in expression.find_all(exp.Column):
table_name = column.table
column_name = column.name
# 伪代码:检查 column_name 是否在 catalog.get_table(table_name).columns 中
if column_name not in catalog.get_columns(table_name):
raise ValueError(f"列 {column_name} 不存在于表 {table_name} 中")
except Exception as e:
print(e)
3. 自建规则引擎 (基于 ANTLR)
如果你使用 ANTLR (如之前的讨论),你可以通过监听器(Listener)模式实现:
- 步骤 :
- 解析 SQL 得到 AST。
- 遍历 AST 中的
TableIdentifier和ColumnIdentifier。 - 对照内存中加载的 JSON/YAML 格式的表结构定义。
- 如果找不到对应对象,抛出
SemanticException。
方案三:混合模式 (IDEA 插件或 CI/CD 工具)
结合上述两种方法,构建更强大的校验工具:
-
连接测试库自动补全与校验:
- 像 DataGrip 或 VS Code 的 SQL 插件那样,连接到测试环境数据库。
- 实时获取表结构元数据。
- 在编辑器中实时高亮不存在的字段(红波浪线)。
-
CI/CD 流水线中的校验:
- 阶段 1 (静态):使用 SQLGlot 或 JSqlParser 检查基本语法和风格。
- 阶段 2 (语义) :启动一个轻量级的数据库实例(如 Testcontainers 启动 MySQL),导入最新的 DDL 脚本,然后使用
Prepare模式校验所有待上线的 SQL。
总结与选型建议
| 方案 | 实现方式 | 准确度 | 依赖环境 | 推荐场景 |
|---|---|---|---|---|
| 数据库 Prepare | PREPARE 或 EXPLAIN |
⭐⭐⭐⭐⭐ (最高) | 需要连接测试库 | 发布审核、Web SQL 工单平台 |
| Schema-Aware 解析 | ShardingSphere / SQLGlot | ⭐⭐⭐⭐ (高) | 仅需元数据文件/内存 | 代码提交钩子 (Git Hook)、IDEA 插件 |
| 纯语法解析 | JSqlParser (无元数据) | ⭐⭐ (低) | 无 | 仅检查关键字拼写,无法检查表是否存在 |
建议路径:
- 如果你是开发一个Web 审核平台 ,直接使用 Prepare/Explain,连接测试库执行,这是最稳妥的。
- 如果你是开发一个本地代码检查工具 ,使用 SQLGlot 或 ShardingSphere 结合导出的元数据文件进行校验。