源码精读:OQL Parser — 从文本到执行计划
OQL(Ontology Query Language)是 coomia-dip 的自研查询语言,其解析器完全手写——没有使用 ANTLR 或 JavaCC 等解析器生成器。解析流水线分为三层:OQLParserService(入口门面)→ OQLLexer(词法分析)→ OQLParser(递归下降语法分析),最终输出 OQLQuery AST。词法分析器支持 30+ 关键字、6 种 Token 类型(标识符、字符串、整数、浮点数、运算符、参数占位符),语法分析器实现完整的 SELECT-FROM-WHERE-GROUP BY-ORDER BY-LIMIT 语法,并扩展了 SIMILARTO(向量搜索)、CONNECTEDTO(图遍历)、METRIC(指标引用)三种领域特定函数。本文将逐行剖析词法分析器的状态机、递归下降解析器的产生式实现、AST 的 Record 模型设计,以及错误恢复策略。
源码精读:OQL Parser — 从文本到执行计划
“系列:S9 源码精读 · 第 6 篇 | 难度:高级 | 阅读时间:25 分钟
#TL;DR
OQL(Ontology Query Language)是 coomia-dip 的自研查询语言,其解析器完全手写——没有使用 ANTLR 或 JavaCC 等解析器生成器。解析流水线分为三层:OQLParserService(入口门面)→ OQLLexer(词法分析)→ OQLParser(递归下降语法分析),最终输出 OQLQuery AST。词法分析器支持 30+ 关键字、6 种 Token 类型(标识符、字符串、整数、浮点数、运算符、参数占位符),语法分析器实现完整的 SELECT-FROM-WHERE-GROUP BY-ORDER BY-LIMIT 语法,并扩展了 SIMILAR_TO(向量搜索)、CONNECTED_TO(图遍历)、METRIC(指标引用)三种领域特定函数。本文将逐行剖析词法分析器的状态机、递归下降解析器的产生式实现、AST 的 Record 模型设计,以及错误恢复策略。
#目录
- 三层解析架构
- 入口门面:OQLParserService
- 词法分析器:OQLLexer
- Token 类型与关键字表
- 字符串字面量与转义处理
- 数值识别:整数与浮点的区分
- 运算符的多字符前瞻
- 注释跳过:行注释与块注释
- 递归下降解析器:OQLParser
- AST 模型:OQLQuery Record
- Key Takeaways
#1. 三层解析架构
OQL 文本 → OQLParserService → OQLLexer → List<TokenInfo>
↓
OQLParser → OQLQuery (AST)
↓
QueryOptimizer → PhysicalPlan
data-Layer/src/main/java/com/onto/data/query/
├── parser/
│ ├── OQLParserService.java # 入口门面:输入验证 + 组装
│ ├── OQLLexer.java # 手写词法分析器
│ ├── OQLParser.java # 递归下降语法分析器
│ ├── OQLToken.java # Token 枚举
│ └── OQLErrorListener.java # 错误监听器
├── ast/
│ ├── OQLQuery.java # AST 根节点(Record)
│ ├── SelectClause.java # SELECT 子句
│ ├── FromClause.java # FROM 子句
│ ├── WhereClause.java # WHERE 子句
│ ├── Projection.java # 投影(字段/聚合/相似度/指标)
│ ├── GroupByClause.java # GROUP BY 子句
│ ├── OrderByClause.java # ORDER BY 子句
│ └── LimitClause.java # LIMIT/OFFSET 子句
├── ast/condition/
│ ├── Condition.java # 条件接口
│ ├── ComparisonCondition.java # 比较条件(=, !=, >, <, >=, <=)
│ ├── LogicalCondition.java # 逻辑条件(AND, OR, NOT)
│ ├── InCondition.java # IN / NOT IN
│ ├── LikeCondition.java # LIKE 模式匹配
│ ├── NullCondition.java # IS NULL / IS NOT NULL
│ ├── RelationCondition.java # HAS 关系条件
│ ├── GraphCondition.java # CONNECTED_TO 图条件
│ └── VectorCondition.java # SIMILAR_TO 向量条件
└── exception/
├── OQLSyntaxException.java # 语法错误(含行列号)
└── OQLSemanticException.java # 语义错误
#2. 入口门面:OQLParserService
@ApplicationScoped
public class OQLParserService {
private static final int MAX_QUERY_LENGTH = 10000;
public OQLQuery parse(String oql) {
// 1. 输入验证
validateInput(oql);
// 2. 词法分析
var lexer = new OQLLexer(oql);
var tokens = lexer.tokenize();
// 3. 语法分析
var parser = new OQLParser(tokens);
var ast = parser.parse();
return ast;
}
private void validateInput(String oql) {
if (oql == null || oql.isBlank()) {
throw new OQLSyntaxException("OQL query cannot be null or empty");
}
if (oql.length() > MAX_QUERY_LENGTH) {
throw new OQLSyntaxException(
"OQL query exceeds maximum length of 10000 characters");
}
}
}
门面模式的价值:OQLParserService 隐藏了"先 Lexer 后 Parser"的两步细节。调用者只需 parser.parse(oql) 一行代码。MAX_QUERY_LENGTH = 10000 防止超长查询导致的 OOM 和 ReDoS 攻击。
#3. 词法分析器:OQLLexer
public class OQLLexer {
private final String input;
private int pos;
private int line;
private int column;
public OQLLexer(String input) {
this.input = input;
this.pos = 0;
this.line = 1;
this.column = 1;
}
public List<TokenInfo> tokenize() {
var tokens = new ArrayList<TokenInfo>();
while (pos < input.length()) {
skipWhitespaceAndComments();
if (pos >= input.length()) break;
var token = nextToken();
if (token != null) {
tokens.add(token);
}
}
tokens.add(new TokenInfo(OQLToken.EOF, "", line, column));
return tokens;
}
}
状态机设计:pos(当前位置)、line(行号)、column(列号)三个状态变量跟踪扫描进度。每个 Token 都记录了精确的行列位置,为语法错误的精确定位提供基础。
#3.1 Token 分发器
private TokenInfo nextToken() {
char c = current();
if (c == '\'') return readString(...);
if (Character.isDigit(c) || (c == '-' && ...)) return readNumber(...);
if (Character.isLetter(c) || c == '_') return readIdentifier(...);
if (c == '$') return readParameter(...);
return readOperatorOrSymbol(...);
}
五路分发基于首字符类型:单引号 → 字符串,数字/负号 → 数值,字母/下划线 → 标识符/关键字,$ → 参数占位符,其他 → 运算符/符号。
#4. Token 类型与关键字表
private static final Map<String, OQLToken> KEYWORDS = Map.ofEntries(
Map.entry("SELECT", OQLToken.SELECT),
Map.entry("DISTINCT", OQLToken.DISTINCT),
Map.entry("FROM", OQLToken.FROM),
Map.entry("WHERE", OQLToken.WHERE),
Map.entry("AND", OQLToken.AND),
Map.entry("OR", OQLToken.OR),
Map.entry("NOT", OQLToken.NOT),
Map.entry("IN", OQLToken.IN),
Map.entry("LIKE", OQLToken.LIKE),
Map.entry("IS", OQLToken.IS),
Map.entry("NULL", OQLToken.NULL),
// ... 共 30+ 关键字
Map.entry("CONNECTED_TO", OQLToken.CONNECTED_TO),
Map.entry("SIMILARITY", OQLToken.SIMILARITY),
Map.entry("METRIC", OQLToken.METRIC),
Map.entry("MATCH", OQLToken.MATCH),
Map.entry("SHORTEST_PATH", OQLToken.SHORTEST_PATH),
Map.entry("GRAPH_TABLE", OQLToken.GRAPH_TABLE)
);
关键字识别策略:先读取完整标识符,再用大写形式在 KEYWORDS Map 中查找。这意味着 OQL 是大小写不敏感的——select、SELECT、Select 都合法。
三类领域扩展关键字:
- 向量搜索:
SIMILARITY - 图遍历:
CONNECTED_TO,MATCH,SHORTEST_PATH,GRAPH_TABLE - 指标引用:
METRIC
#5. 字符串字面量与转义处理
private TokenInfo readString(int startLine, int startColumn) {
advance(); // skip opening quote
var sb = new StringBuilder();
while (pos < input.length()) {
char c = current();
if (c == '\'') {
if (pos + 1 < input.length() && input.charAt(pos + 1) == '\'') {
// 转义引号:'' → '
sb.append('\'');
advance();
advance();
} else {
// 字符串结束
advance();
break;
}
} else {
sb.append(c);
advance();
}
}
return new TokenInfo(OQLToken.STRING, sb.toString(), startLine, startColumn);
}
SQL 风格转义:OQL 使用 ''(两个单引号)表示字面量单引号,与 SQL 标准一致。例如 'O''Brien' 解析为字符串 O'Brien。
#6. 数值识别:整数与浮点的区分
private TokenInfo readNumber(int startLine, int startColumn) {
var sb = new StringBuilder();
if (current() == '-') { sb.append('-'); advance(); }
while (pos < input.length() && Character.isDigit(current())) {
sb.append(current()); advance();
}
// 检查小数点
if (pos < input.length() && current() == '.'
&& pos + 1 < input.length()
&& Character.isDigit(input.charAt(pos + 1))) {
sb.append('.'); advance();
while (pos < input.length() && Character.isDigit(current())) {
sb.append(current()); advance();
}
return new TokenInfo(OQLToken.NUMBER, sb.toString(), ...);
}
return new TokenInfo(OQLToken.INTEGER, sb.toString(), ...);
}
INTEGER vs NUMBER:词法分析器在 Token 级别就区分了整数和浮点数——42 是 INTEGER,3.14 是 NUMBER。这为后续语法分析中的类型推断提供了基础。
前瞻检查:current() == '.' && ... && Character.isDigit(input.charAt(pos + 1)) 确保 . 后面必须跟数字才算小数点——否则 42.field 中的 . 会被误判为小数点。
#7. 运算符的多字符前瞻
private TokenInfo readOperatorOrSymbol(int startLine, int startColumn) {
char c = current();
switch (c) {
case '<':
if (peek() == '=') { advance(); advance(); return tokenInfo(OQLToken.LTE); }
if (peek() == '>') { advance(); advance(); return tokenInfo(OQLToken.NEQ); }
advance(); return tokenInfo(OQLToken.LT);
case '>':
if (peek() == '=') { advance(); advance(); return tokenInfo(OQLToken.GTE); }
advance(); return tokenInfo(OQLToken.GT);
case '!':
if (peek() == '=') { advance(); advance(); return tokenInfo(OQLToken.NEQ); }
advance(); return tokenInfo(OQLToken.UNKNOWN);
case '.':
if (peek() == '.') { advance(); advance(); return tokenInfo(OQLToken.DOTDOT); }
advance(); return tokenInfo(OQLToken.DOT);
}
}
一字符前瞻:< 可能是 <(LT)、<=(LTE)或 <>(NEQ),需要前瞻一个字符才能确定。同样 . 可能是属性访问(DOT)或范围运算符(DOTDOT ..)。
#8. 注释跳过:行注释与块注释
private void skipWhitespaceAndComments() {
while (pos < input.length()) {
char c = current();
if (Character.isWhitespace(c)) { /* skip */ continue; }
// 行注释:-- 到行尾
if (c == '-' && pos + 1 < input.length() && input.charAt(pos + 1) == '-') {
while (pos < input.length() && current() != '\n') pos++;
continue;
}
// 块注释:/* ... */
if (c == '/' && pos + 1 < input.length() && input.charAt(pos + 1) == '*') {
pos += 2;
while (pos + 1 < input.length()) {
if (current() == '*' && input.charAt(pos + 1) == '/') {
pos += 2; break;
}
pos++;
}
continue;
}
break;
}
}
OQL 支持 SQL 风格的两种注释:--(行注释)和 /* */(块注释)。
#9. 递归下降解析器:OQLParser
#9.1 顶层语法
public OQLQuery parse() {
var builder = OQLQuery.builder();
builder.select(parseSelectClause()); // SELECT(必需)
builder.from(parseFromClause()); // FROM(必需)
if (check(OQLToken.WHERE)) builder.where(parseWhereClause());
if (check(OQLToken.GROUP)) builder.groupBy(parseGroupByClause());
if (check(OQLToken.ORDER)) builder.orderBy(parseOrderByClause());
if (check(OQLToken.LIMIT)) builder.limit(parseLimitClause());
expect(OQLToken.EOF, "end of query");
return builder.build();
}
语法结构:SELECT 和 FROM 是必需的,WHERE、GROUP BY、ORDER BY、LIMIT 是可选的——通过 check() 前瞻判断是否存在。
#9.2 条件解析:优先级层级
condition → parseOrCondition()
orCondition → andCondition (OR andCondition)*
andCondition → notCondition (AND notCondition)*
notCondition → NOT? primaryCondition
primaryCondition → LPAREN condition RPAREN
| HAS relationCondition
| CONNECTED_TO graphCondition
| SIMILARITY vectorCondition
| field IS [NOT] NULL
| field [NOT] IN (values)
| field LIKE pattern
| field op value
优先级通过调用层级实现:OR < AND < NOT < 基本条件。例如 a = 1 AND b = 2 OR c = 3 被解析为 (a = 1 AND b = 2) OR c = 3。
#9.3 投影解析:四种投影类型
private Projection parseProjection() {
if (check(OQLToken.STAR)) return Projection.wildcard();
if (isAggregateFunction(current())) return parseAggregateProjection();
if (check(OQLToken.SIMILARITY)) return parseSimilarityProjection();
if (check(OQLToken.METRIC)) return parseMetricProjection();
// 字段投影
String fieldPath = parseFieldPath();
String alias = check(OQLToken.AS) ? expect(IDENTIFIER).value() : null;
return Projection.field(fieldPath, alias);
}
| 投影类型 | 示例 | 解析方法 |
|---|---|---|
| 通配符 | * | Projection.wildcard() |
| 聚合函数 | COUNT(*), SUM(amount) | parseAggregateProjection() |
| 相似度 | SIMILARITY(embedding, [0.1, 0.2]) | parseSimilarityProjection() |
| 指标引用 | METRIC('revenue') AS rev | parseMetricProjection() |
| 字段 | name, attributes.role | Projection.field() |
#9.4 参数化查询
private Object parseValue() {
return switch (token.type()) {
case STRING -> token.value();
case NUMBER -> Double.parseDouble(token.value());
case INTEGER -> Long.parseLong(token.value());
case TRUE -> true;
case FALSE -> false;
case NULL -> null;
case PARAMETER -> new ParameterPlaceholder(token.value());
default -> throw error("value");
};
}
public record ParameterPlaceholder(String name) {}
$paramName 语法支持参数化查询,防止 OQL 注入。ParameterPlaceholder 在执行阶段被实际值替换。
#10. AST 模型:OQLQuery Record
public record OQLQuery(
SelectClause select,
FromClause from,
Optional<WhereClause> where,
Optional<GroupByClause> groupBy,
Optional<OrderByClause> orderBy,
Optional<LimitClause> limit
) {
public boolean isAggregateQuery() {
return select.hasAggregation() || groupBy.isPresent();
}
public OQLQuery withSelect(SelectClause newSelect) {
return new OQLQuery(newSelect, from, where, groupBy, orderBy, limit);
}
}
Record 的不可变性:Java Record 确保 AST 一旦构建就不可修改。withSelect() 方法返回新的 AST 实例——这是函数式不可变更新模式,用于 MetricProjectionRewriter 等查询重写场景。
聚合查询判定:isAggregateQuery() 检查 SELECT 子句中是否有聚合函数,或是否存在 GROUP BY——这影响后续的优化策略和缓存策略。
#11. Key Takeaways
- 手写词法分析器:不依赖 ANTLR/JavaCC,
OQLLexer使用状态机模式逐字符扫描,支持 30+ 关键字和精确行列位置追踪 - 递归下降解析器:
OQLParser通过调用层级实现运算符优先级(OR < AND < NOT < 基本条件),无需优先级表 - 大小写不敏感:标识符转大写后在关键字表中查找,
select和SELECT等价 - INTEGER vs NUMBER:词法级别区分整数和浮点数,为类型推断提供基础
- SQL 风格注释:支持
--(行注释)和/* */(块注释) - 参数化查询:
$paramName语法生成ParameterPlaceholder,防止 OQL 注入 - 不可变 AST:使用 Java Record +
Optional组合,withSelect()实现函数式更新 - 领域扩展:
SIMILAR_TO、CONNECTED_TO、METRIC三个领域特定函数扩展了标准 SQL 语法
#下一篇
S9-07:AnalyticsQueryService — 14 种聚合的实现。我们将深入 Data Layer 的分析查询服务,看它如何通过 SQL Builder 模式实现分组聚合、时间桶聚合、TopN 和分布查询。
Tags: #coomia-dip #source-code-reading #data-Layer #oql #lexer #parser #ast #recursive-descent #query-language