基于ANTLR4的大数据SQL编辑器解析引擎实践|得物技术

一、背景

随着得物离线业务的快速增长,为了脱离全托管服务的一些限制和享受技术发展带来的成本优化,公司提出了大数据Galaxy开源演进项目,将离线业务从全托管且封闭的环境迁移到一个开源且自主可控的生态系统中,而离线开发治理套件是Galaxy自研体系中一个核心的项目,在数据开发IDE中最核心的就是SQL编辑器,我们需要一个SQL解析引擎在SQL编辑提供适配得物自研Spark引擎的语法定义,实时语法解析,语法补全,语法校验等能力,结合业内dataworks和dataphin的实践,我们最终选用ANTLR作为SQL解析引擎底座。

二、ANTLR4 简介

ANTLR(一种语法解析引擎工具)是一个功能强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。它广泛用于构建语言、工具和框架。ANTLR可以根据语法规则文件生成一个可以构建和遍历解析树的解析器。

ANTLR4 特性

ANTLR4 是一个强大的工具,适合用于语言处理、编译器构建、代码分析等多种场景。它的易用性、灵活性和强大的特性使得它成为开发者的热门选择。

  • 强大的文法定义:ANTLR4 允许用户使用简单且易读的文法语法来定义语言的结构。这使得创建和维护语言解析器变得更加直观,同时在复杂文法构造上支持左递归文法、嵌套结构以及其他复杂的文法构造,使得能够解析更复杂的语言结构。
  • 抽象语法树遍历:ANTLR4 可以生成抽象语法树,使得在解析源代码时能够更容易地进行分析和变换。AST 是编译器和解释器的核心组件。同时提供了简单的 API 来遍历生成的语法树,使得实现代码分析、转换等操作变得简单
  • 自动语法错误处理:ANTLR4 提供了内置的错误处理机制,可以在解析过程中自动处理语法错误,并且可以自定义错误消息和处理逻辑
  • 可扩展性:ANTLR4 允许用户扩展和自定义生成的解析器的行为。例如,您可以自定义解析器的方法、错误处理以及其他功能。
  • 工具&社区生态:ANTLR4 提供了丰富的工具支持,包括命令行工具、集成开发环境插件和可视化工具,可以帮助您更轻松地开发和调试解析器。同时拥有活跃的社区,提供了大量的文档、示例和支持。这使得新用户能够快速上手,并得到必要的帮助。

ANTLR4 的应用场景

Apache Spark: 流行的大数据处理框架,使用ANTLR作为其SQL解析器的一部分,支持SQL查询。
Twitter: Twitter 使用ANTLR来解析和分析用户的查询语言,这有助于他们的搜索和分析功能。
IBM: IBM使用ANTLR来支持一些其产品和工具中的DSL(领域特定语言)解析需求,例如,在其企业集成解决方案中。

ANTLR4入门

ANTLR元语言

为了实现一门计算机编程语言,我们需要构建一个程序来读取输入语句,对其中的词组和符号进行识别处理,即我们需要语法解释器或者翻译器来识别出一门特定语言的所有词组,子词组,语句。我们将语法分析过程拆分为两个独立的阶段则为词法分析和语法分析。

antlr4入门.jpeg

ANTLR语法遵循了一种专门用来描述其他语言的语法,我们称之为ANTLR元语言(ANTLR’s meta-language)。ANTLR元语句是一个强大的工具,可以用来定义编程语言的语法。通过定义词法和语法规则,可以基于antlr生成解析器和词法分析器。

1、自顶向下
在语言结构中,整体的辨识都是从最粗的粒度开始,一直进行到最详细的层次,并把它们编写成为语法规则,ANTLR4就是采用自顶向下的,词法语法分离,上下文无关的语法框架来描述语言。

// MyGLexer.g4
lexer grammar MyGLexer;

SEMICOLON: ';';
LEFT_PAREN: '(';
RIGHT_PAREN: ')';
COMMA: ',';
DOT: '.';
LEFT_BRACKET: '[';
RIGHT_BRACKET: ']';
LEFT_BRACES: '{';
RIGHT_RACES: '}';
EQ: '=';

FUNCTOM: 'FUNCTION';
LET: 'LET';
CONST: 'CONST';
VAR: 'VAR';
IF: 'IF';
ELSE: 'ELSE';
WHILE: 'WHILE';
FOR: 'FOR';
RETURN: 'RETURN';
// MyGParser.g4
parser grammar MyGParser;

options {
  tokenVocab = MyGLexer;
}

// 入口规则
program: statement* EOF;

statement:
  variableDeclaration
  | functionDeclaration
  | expressionStatement
  | blockStatement
  | ifStatement
  | whileStatement
  | forStatement
  | returnStatement;
  ......

2、语言模式

计算机语言常见4种语言模式:序列(sequence)、选择(choice)、词法符号依赖 (token dependency),以及嵌套结构(nested phrase)。以下是ANTLR对4种模式的语法规则描述。

语言模式.jpeg

3、语法歧义

在自顶向下的语法和手工编写的递归下降语法分析器中,处理表达式都是一件相当棘手的事情,这首先是因为大多数语法都存在歧义,其次是因为大多数语言的规范使用了一种特殊的递归方式,称为左递归。

expr : expr '*' expr
     | expr '+' expr
     | INT
     ;

我们举个运算符优先级带来的语法歧义问题,同样的规则可以匹配多个输入字符流。

匹配多个输入字符流.jpeg

在其他语法工具中,通常通过指定额外的标记来指定运算符优先级。而在ANTLR4中通过备选分支的排序来指定优先级,越靠前优先级越高。

代码自动生成

ANTLR可以根据lexer.g4和parser.g4自动生成词法分析器,语法分析器,监听器,访问器等。

antlr4ng -Dlanguage=TypeScript -visitor -listener -Xexact-output-dir -o ./src/lib ./src/grammar/*.g

代码自动生成.jpeg

语法解析与业务逻辑解耦

在ANTLR4中语法解析和业务逻辑的高度解耦是一个重要的设计理念,优点就是同一个 AST 结构能够在不同的业务逻辑实现之间实现复用。不同的业务逻辑(如执行、转换、优化等)可以对同一个 AST 进行不同的处理,而不需要关心解析过程。核心几个设计方案如下:

  • 访问者模式:ANTLR4通过访问者模式支持业务代码可访问特定“词法”或“语法”节点执行自定义的操作,通过这个方式完全解耦AST(抽象语法树)生成和业务逻辑,词法分析器和解释器专注于AST生成,而业务可以通过访问器的扩展支持业务定制化诉求。
  • 语法和语义的独立性:ANTLR4中可以独立进行语法解析和语义分析,可以在 AST 中进行语义检查和业务逻辑处理。这种分离使得开发者可以更灵活地处理输入的语法和语义。
  • AST生成:ANRL4通过语法解析器生成结构化AST(抽象语法树),不同业务逻辑可以不断复用同一个AST。
  • 上下文模式:解析器在处理输入数据时,上下文会在解析树中传递信息。每当进入一个新的语法规则时,都会创建一个新的上下文实例上下文可以存储解析过程中需要的临时信息,例如变量的值、数据类型等。上下文信息主要结合访问器模式进行使用,同时也解决了在解析复杂语句如多层嵌套结构的层级调用问题。

三、SparkSQL介绍

Spark SQL 是 Apache Spark 的一个模块,专门用于处理结构化数据,Spark SQL 的特点包括:

  1. 高效的查询执行:通过 Catalyst 优化器和 Tungsten 执行引擎,Spark SQL 能够优化查询执行计划,提升查询性能。
  2. 与 Hive 的兼容性:Spark SQL 支持 HiveQL 语法,使得用户可以轻松迁移现有的 Hive 查询。
  3. 支持多种数据源:Spark SQL 可以从多种数据源读取数据,包括 HDFS、Parquet、ORC、JDBC 等。

四、技术实现

语法设计

在Aparch Spark源码中就是使用ANTLR4来解析和处理SQL语句,以下为Apach Spark中基于ANTLR元语言定义的词法分析器和语法分析器,在语法定义上我们只需要基于这套标准的SparkSQL语法去适配得物自研引擎的能力,做能力对齐。

Lexer.g4

https://github.com/apache/spark/blob/master/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4

Parser.g4

https://github.com/apache/spark/blob/master/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4

语法补全

以下我们以字段补全场景为例解析从语法定义,语法解析,语法补全,上下文信息采集各个流程节点剖析最后完成的表字段信息精准推荐。在下列语法场景中,存在多层Select语法嵌套,同时表du_emr_test.empsalary tableB和表du_emr_test.hujh_type_tk AS tableB设置了同一别名, 如图在父子查询中都使用了同一个表别名(tableB),当用户在父子查询中分别输入**tableB.**时,这时候需要结合当前上下文语境,对tableB别名推荐不同表的字段。

SELECT 
    tableB.c1
 FROM
    (
       SELECT
            tableB.empno,
            tableC.department
        FROM
                du_emr_test.empsalary as tableB
        LEFT JOIN du_emr_test.employees AS tableC
        WHERE tableC.department = tableB.depname

    ) AS tableA
LEFT JOIN du_emr_test.hujh_type_tk AS tableB
WHERE tableB.c1 = tableA.dename

语法补全1.jpeg
语法补全2.jpeg
语法补全3.jpeg
语法补全4.jpeg

在子查询中我们期望推荐tableB来自du_emr_test.empsalary tableB的字段信息,而在最外层中我们期望的是du_emr_test.hujh_type_tk的字段,如上图。

基于以上场景我们核心要解决2个问题:

问题1:当前光标应该提示哪些推荐语法类型

目前,开源方案ANTLR-C3引擎就能完美解决我们问题,用户在编辑器实时输入时,获取当前光标位置,实时做语法解析,然后基于开源的ANTLR-C3引擎能力结合ANTLR 生成的AST即可获取当前光标位置所需要的语法规则。

问题2: 获取当前上下文信息以实现精准推荐

根据不同业务场景需要采集的上下文信息不同,基于字段推荐的场景,我们需要获取当前光标位置处可以推荐的表信息,表别名信息,结合编辑器能力实时获取表对应的字段信息进行字段推荐补全,而上下文信息的采集,我们可以通过ANTLR生成的监听器来实现。

语法定义

以下我们用ANTLR元语言实现一段简化版的SQL查询场景的语法规则(QueryStatment),方便我们理解。

lexer grammar SqlLexer;

// 基础词法
COMMA: ',';
LEFT_PAREN: '(';
RIGHT_PAREN: ')';
IDENTIFY: (LETTER | DIGIT | '_' | '.')+;
fragment DIGIT: [0-9];
fragment LETTER: [A-Z];
SEMICOLON: ';';

parser grammar SqlParser;

program: statment* EOF;

statment: queryStatment SEMICOLON?;
// 查询语句
queryStatment:
  SELECT columnNames FROM (
    tableName
    | (LEFT_PAREN queryStatment LEFT_PAREN)
  ) whereExpression? relationsExpresssion? SEMICOLON?;

// 字段
columnNames: columnName (COMMA columnName)*;

tableName: IDENTIFY AS? tableAlis;

tableAlis: IDENTIFY;

columnName: IDENTIFY AS? columnAlis;

columnAlis: IDENTIFY;

whereExpression: WHERE booleanExpression;

booleanExpression: (NOT | BANG) booleanExpression           # logicalBinary
  | left = booleanExpression operator = AND right = booleanExpression # logicalBinary
  | left = booleanExpression operator = OR right = booleanExpression  # logicalBinary;

relationsExpresssion:
  LEFT JOIN tableName whereExpression?
  | RIGHT JOIN tableName whereExpression?;

代码生成

代码生成1.jpeg
代码生成2.jpeg

以下是部分生成代码:

1、词法分析器

 // SqlLexer.ts
    
    public static readonly COMMA = 1;
    public static readonly LEFT_PAREN = 2;
    public static readonly RIGHT_PAREN = 3;
    public static readonly IDENTIFY = 4;
    public static readonly SEMICOLON = 5;

    // 词法分析器可以使用的通道
    public static readonly channelNames = [
        "DEFAULT_TOKEN_CHANNEL", "HIDDEN"
    ];
    // 包含了所有字面量记号的名称
    public static readonly literalNames = [
        null, "','", "'('", "')'", null, "';'"
    ];
    // 包含为每个记号分配的符号名,这些符号在生成解析器时用于标识记号
    public static readonly symbolicNames = [
        null, "COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "SEMICOLON"
    ];
    
    //  ANTLR 生成的类中的一个字段,列出了所有定义的规则
    public static readonly ruleNames = [
        "COMMA", "LEFT_PAREN", "RIGHT_PAREN", "IDENTIFY", "DIGIT", "LETTER", 
        "SEMICOLON",
    ];

2、语法分析器

ANTLR自动为每个规则生成了一个解析方法,以下是tableName的 ANTLR 中的解析器方法,具备了处理标识符、可选的别名和错误处理的能力。

// SQLParse.ts
// ANTLR自动生成了一个解析 SQL 表名的 ANTLR 中的解析器方法,具备了处理标识符、可选的别名和错误处理的能力
public tableName(): TableNameContext {
        let localContext = new TableNameContext(this.context, this.state);
        this.enterRule(localContext, 8, SqlParser.RULE_tableName);
        let _la: number;
        try {
            this.enterOuterAlt(localContext, 1);
            {
            this.state = 60;
            this.match(SqlParser.IDENTIFY);
            this.state = 62;
            this.errorHandler.sync(this);
            _la = this.tokenStream.LA(1);
            if (_la === 8) {
                {
                this.state = 61;
                this.match(SqlParser.AS);
                }
            }

            this.state = 64;
            this.tableAlis();
            }
        }
        catch (re) {
            if (re instanceof antlr.RecognitionException) {
                this.errorHandler.reportError(this, re);
                this.errorHandler.recover(this, re);
            } else {
                throw re;
            }
        }
        finally {
            this.exitRule();
        }
        return localContext;
    }

自动补全

ANTLR4代码补全核心(antlr4-c3) 是一个开创性的工具,它为ANTLR4生成的解析器提供了一个通用的代码补全解决方案。无论你的项目是处理哪种编程语言或领域特定语言(DSL),只要是基于ANTLR就能够利用这个库实现精准的代码建议和自动补全,极大地增强开发体验。通过antlr4-c3 能力我们通过手动配置需要收集的语法规则,获取在当前光标处需要推荐的语法规则类型。

1、语法规则

通过ANTLR4工具我们可以自动生成Sqllexer.ts词法解析器,SqlParser.ts语法解析器,SqlParserLister.ts访问器,SqlParseVisitor.ts监听器,在SqlParser 语法解析器自动生成了我们在语法定义中的语法规则。

preferredRules = new Set([
        SqlParser.RULE_tableName,
        SqlParser.RULE_columnName,
]);

2、代码补全

以下我们实现一套简化版的代码补全能力。

当用户在编辑器实时输入时,调用getSuggestionAtCaretPosition获取当前语境中需要推荐的信息,包含语法规则,关键词,上下文信息,在结合业务层数据做自动补全,其中包含5个核心步骤:

  1. 获取当前语法解析器实例。
  2. 获取当前光标位置对应的Token。
  3. 生成AST。
  4. 获取当前语境上下文信息。
  5. 通过ANTLR-C3获取当前位置候选语法规则。
public getSuggestionAtCaretPosition(
        sqlContent: string,
        caretPosition: CaretPosition
        preferredRules: Set
    ): Suggestions | null {
        
        // 1、 使用SqlParse解析器获取
        const sqlParserIns = new SqlParse(sqlContent)
        
        // 2、获取当前光标处token
        const charStreams = CharStreams.fromString(sqlContent);
        const lexer = new SqlLexer(charStreams);
        const tokenStream = new CommonTokenStream(lexer);
        tokenStream.fill()
        const allTokens = tokenStream.getTokens(); 
        let caretTokenIndex = findCaretToken(caretPosition, allTokens); 

        // 3、获取AST抽象语法树
        const parseTree = sqlParserIns.program()
        
        // 4、通过监听器采集上下文表信息(下面上下文分析部分阐述细节)
        const tableEntity = getTableEntitys()
        
         // 异常场景兼容存在多条sql, 获取有效最小SQL范围给到antlr4-c3做推荐。
        const statementCount = splitListener.statementsContext?.length;
        const statementsContext = splitListener.statementsContext; 

        // 5、antlr4-c3接入获取推荐语法规则
        let tokenIndexOffset: number = 0;
        const core = new CodeCompletionCore(sqlParserIns);
        // 推荐规则 来自SQLparse解析器的规则(元语言定义)
        core.preferredRules = preferredRules; 
        // 通过AST和当前光标Token获取推荐类型
        const candidates = core.collectCandidates(caretTokenIndex, parseTree); 
        
        // ruleType -> preferredRules 
        // const [rules, tokens] = candidate;
        const rules = [];
        const keywords = [
                
        for (let candidate of candidates.rules) {
        const [ruleType] = candidate;
        let synContextType;
        switch (ruleType) {
            case SqlParser.RULE_tableName: {
                syntaxContextType = 'table';
                break;
            }
            case SqlParser.RULE_columnName: {
                syntaxContextType = 'column';
                break;
            }
            default:
                break;
        }
        if (synContextType) {
            rules.push(syntaxContextType)
        }
    }

    // 获取对应keywords
    for (let candidate of candidates.tokens) {
        const displayName = sqlParserIns.vocabulary.getDisplayName(candidate[0]);
        const keyword = displayName.startsWith("'") && displayName.endsWith("'")
                ? displayName.slice(1, -1)
                : displayName
        keywords.push(keyword);
    }

    return {
        rules,
        keywords,
        tableEntity
    };
  }

在这里我们简化了流程,忽略了很多异常case的处理,自动补全的前提是在当前语法规则正确,而在多级子查询嵌套场景我们需要考虑到过滤异常QueryStatment, 在当前光标出最小范围有效的QueryStatment做补全。这时候需要配合监听器去做上下文采集做容错性更高的自动补全。

上下文分析

上下文分析.jpeg

如图:每个table都归属于一个QueryStatment表达式, 查询中又存在子层级查询的嵌套。我们需要通过上下文收集以下信息:

  1. 每个查询语句的信息,包含Position位置信息,记录当前的查询开始行,结束行,开始列,结束列。
  2. 查询语句的关联关系,即记录当前查询语句父级查询语句对象。
  3. 表实体信息包含表名,表位置信息,表别名信息,当前表归属于那个查询语句。

则我们需要监听3个语法规则包含QueryStatment, TableName,TableAlias, 采集QueryStatment信息,Table信息同时将table与当前归属的QueryStatment做关联, 还有与别名信息作配对关联。这就要求在不同监听器之间的信息需要做共享,上下文信息需要做传递和保留。ANTLR常用的3种信息共享方案包含:

  • 使用访问器方法来返回值,
  • 使用类成员在事件方法之间共享数据,
  • 在语法定义中使用树标记来存储信息。

在这里我们使用第二种(在这里我们简化了SQL的语法定义,在实际场景中语法层级深度和复杂度远比当前高,这也使得方案1和3实际操作起来更麻烦,规则嵌套层级深使得方案一和方案三开发成本和维护成本更高)

1、监听器(SqlParserLister)
通过ANTLR4工具我们可以自动生成SqlParserLister.ts监听器进行自定义扩展。

// SqlParserListener.ts
export class QueryStatmentContext extends antlr.ParserRuleContext {
   public override enterRule(listener: SqlParserListener): void {
        if(listener.enterQueryStatment) {
             listener.enterQueryStatment(this);
        }
    }
    public override exitRule(listener: SqlParserListener): void {
        if(listener.exitQueryStatment) {
             listener.exitQueryStatment(this);
        }
    }
 }
 
 export class TableNameContext extends antlr.ParserRuleContext {
     public override enterRule(listener: SparkSqlParserListener): void {
        if(listener.enterTableName) {
             listener.enterTableName(this);
        }
    }
    public override exitRule(listener: SparkSqlParserListener): void {
        if(listener.exitTableName) {
             listener.exitTableName(this);
        }
    }
 }
// ....

export class TableAliasContext extends antlr.ParserRuleContext {
    public KW_AS(): antlr.TerminalNode | null {
        return this.getToken(SparkSqlParser.KW_AS, 0);
    }
    public override enterRule(listener: SparkSqlParserListener): void {
        if(listener.enterTableAlias) {
             listener.enterTableAlias(this);
        }
    }
    public override exitRule(listener: SparkSqlParserListener): void {
        if(listener.exitTableAlias) {
             listener.exitTableAlias(this);
        }
    }
}

2、自定义监听器扩展

通过SqlParserListener我们可以自定义采集上下文信息。在

  1. 监听进入QueryStatment表达式采集当前表达式信息到_queryStmtsStack。
  2. 监听退出TableNameToken时采集当前Table信息,并关联当前QueryStatment。
  3. 监听退出TableAliasToken时采集信息,并关联到Table实体。
  4. 监听退出QueryStatment表达式推出_queryStmtsStack
// tableEntityCollect
 export class SqlEntityCollector implements SqlParserListener {
     super() {
         this._tableEntitiesSet = new Set();
         this._queryStmtsStack = [];
         this._tableAliasStack = [];
         this._currentTable = '';
     }
     
     enterQueryStatment(ctx: QueryStatmentContext) {
        this.pushQueryStmt(ctx);
    }

    exitQueryStatment(ctx: QueryStatmentContext) {
        this.popQueryStmt(); 
    }

     exitTableName(ctx: TableNameContext) {
        this.pushTableEntity(ctx);
        this.setCurrentTable(ctx);
     }
     
     exitTableAlias(ctx: TableAliasContext) {
        this.pushTableEntity(ctx);
     }
     
     pushQueryStmt() {}   // 采集QueryStmt信息
     
     popQueryStmt() {}    // 推出当前QueryStmt,进入下个同级Stmt
     
     pushTableEntity() {} // 采集当前表信息,关联当前Stmt
     
     pushTableEntity() {} // 采集关联表
     
     enterProgram() {}    // 清空重置
     
     getTableEntity() {
         return this.TableEntity(ctx)
     }
    
 }

在这里我们简化了语法定义的规则便于讲解,但在实际中语法规则的整体嵌套层级是很深的,从以下的SparkSql语法定义中我们可以看到右侧聚合的表达式高达200+个,单个表达式的备选分支最多高达140+,这也加大了上下文分析采集的复杂度,即我们无法简单的从QueryStmt当前QueryStatmentContext中获取全量信息。

获取全量信息.jpeg

3、触发监听器采集上下文信息

getTableEntitys() {
    const collectListener = new SqlEntityCollector(sqlContent, caretTokenIndex);
    const parse = new SqlParse(sqlContent);
    const parseTree= sqlParserIns.program();
    ParseTreeWalker.DEFAULT.walk(collectListener, parseTree); 
    return collectListener.getTableEntity()
}

语法校验

ANRLR在生成语法分析器中内置了自动错误报告和恢复策略,能够在遇到句法错误时自动产生错误消息,为每个句法错误产生一条错误消息。

词法错误

常见的词法错误包含字符遗漏,词法错误。举个例子,在spark标准语法定义中 tableName规则不支持表变量场景(${variable}),如果要兼容这里词法,就需要在语法定义中变更tableName的语法规则定义。

语法错误.jpeg

以下是语法定义变更:

  1. 新增词法规则$, {, }。
  2. 新增语法规则identifyVar支持变量模式。
SqlLexer.g4
// 新增词法
LEFT_BRACE    : '{';
RIGHT_BRACE   : '}';

VARIABLE    : '$';

SqlParse.g4
// before tableName: IDENTIFY AS? tableAlis; 
tableName: identifyVar AS? tableAlis; 

identifyVar
    : IDENTIFY // odps_table_a
    | IDENTIFY? VARIABLE LEFT_BRACE IDENTIFY RIGHT_BRACE IDENTIFY? // odps_table_a_${variable} odps_table_a_${prefix_variable}_abs

自动恢复机制

语法分析器不应该在遇到非法的成员定义时结束,而是应尽最大可能匹配到一个合法的类定义,ANRTL4自动错误恢复机制能在语法分析器在发现语法错误后还能继续进行尝试语法解析和自动恢复。

1、异常捕获

ANRLT自动生成的语法解析器中自动为每个规则包裹异常捕获能力,并在catch中尝试错误恢复。

异常补货.jpeg

2、恢复策略

一般情况下,语法分析器在遇到无法匹配的错误时会尝试最简单的符号补全和移除来尝试解析,都不管用时,这时候就会用更高阶的策略来进行恢复。包括扫描后续词法符号来恢复,从不匹配的词法符号中恢复,从子规则的错误中恢复,捕获失败的语义判定。

虽然ANTLR提供了很多策略来进行错误恢复,但在实际业务场景中,需要结合考虑语法、语境的复杂度去权衡性能与更友好的错误提示之间的抉择。在复杂场景中ANTLR表现并不理想,在一些复杂语法和语境的情况下解析器在检测错误时难以做出合理的决策,例如:递归和嵌套结构中会使得错误恢复变得很复杂,导致解析器无法做出合理决策。还有在上下文敏感的语境中,错误恢复机制基本无法提供有效恢复。

性能

在 ANTLR 4 中,语法复杂度、语法歧义、语法规则嵌套深度与预测算法的选择都会显著影响解析器的性能和准确性。Spark SQL语法规则达200+,备选分支最高达140, 嵌套深度达20+,同时又存在负责循环嵌套场景, 这也意味着在整个语法解析,语法错误的处理过程是很复杂的,当遇到复杂大SQL量和一片狼籍的语法错误SQL,会导致语法解析过程变得缓慢引发性能问题。目前在性能优化上,有以下几个方向。

缓存优化

在antlr4中词法解析和语法解析能力和业务是完全解耦的,这也意味着底层基于同个SQL内容解析出来的tokens和parserTree都是可以在不同业务逻辑应用里复用。我们可以通过缓存tokens,parseTree减少词法解析和语法解析的损耗。

语法优化

通过减少语法树的层级和优化表达式减少解析过程中“二义性”的次数,可以加速语法解析的速度,优化AST生成性能。合理使用语法定义中用法,例如树标记(用于上下文通信数据共享),在语法解析过程中会为每个标记生成上下文,这也意味着每个局部结果都会保留,会有更大的内存消耗。

预测模型选择

在语法解析中不同预测模型的选择对解析性能有显著影响,针对不同的场景需要评估时效性与正确性之间的衡量。

ANTLR4预测模型:

https://www.antlr.org/api/Java/org/antlr/v4/runtime/atn/PredictionMode.html

预测模型.jpeg

我们可以选择性价比更高的SLL预测模型作为语法分析策略,结合定制化的错误监听器做错误纠正。

编辑器应用

编辑器集成

与MonacoEditor集成流程可查看此文章 https://blog.shizhuang-inc.com/article/MTUzNzY?fromType=personal_blog

辅助编程

1、信息项提示(表,函数,字段)

信息项提示1.jpeg
信息项提示2.jpeg
信息项提示3.jpeg

2、自动补全(库,表,字段,语法)

自动补全1.jpeg
自动补全2.jpeg
自动补全3.jpeg

五、大模型下的SQL编辑器应用

随着大模型的蓬勃发展,在数据产品中的应用也逐步得到了验证和落地,目前,Galaxy还没有接入Copilot, 内部暂时还没有基于SQL的Copilot。业界较成熟的是阿里云的Dataworks, DataWorks于2023年推出了Copilot 产品, 核心2个方向,一个方向是智能 SQL 编程助手,辅助 SQL 编程,支持 NL2SQL 及 SQL 代码补全;另一个方向是 AI Agent,提供 LUI(自然语言用户界面),以提升产品功能操作的便捷性和用户体验。

NL2SQL应用场景

基于SQL的Copilot一般在以下几个应用场景比较深入和广泛的落地效果:简单数据查询,SQL 优化与转换,SQL 语法查询与讲解, 函数查询,功能咨询,注释生成,SQL 解释,SQL 一键纠错。

NL2SQL自动补全

代码补全是编程类 Copilot 的主要场景和能力,单市场上主流的编程类 Copilot 对 SQL 支持的好的并不多见。众所周知,SQL 代码补全比其他高级语言的代码补全更具挑战性,主要原因有以下几个方面:

  • 上下文和环境的依赖性:SQL 代码不是独立存在的,而是依赖于数据表的元数据信息以及表与表之间的关联关系。
  • SQL 语义多样性:实现同一种查询结果,可以有多种 SQL 写法,如何实现“最佳”写法存在挑战。
  • 语法简洁但高度专业化:SQL 语法简洁但每一个关键字、函数或语法都有特定的含义,大模型要准确理解这些得通过针对性的训练学习。
  • 执行计划和性能考量: 这跟数据库底层的执行计划有关,需要考虑如何书写才能使 SQL 的性能最优。
  • 数据库特异性:市面上不同的数据库往往存在不同的 SQL 方言,存在差异,针对这种差异性我们要投入大量时间做 SQL 数据集准备、数据标注、模型微调。
  • 高度业务相关性:SQL 语句通常与特定业务高度相关,比如一个指标存在特定的计算口径,这是与公司业务相关,通用的大模型也无法提前学习。

目前较成熟的代码补全核心场景主要在有规律的代码连续推荐场景(例如:字段、字段别名推荐,注释推荐、分区字段推荐、Group by 字段推荐,上下文自动联想推荐等)。

六、总结

通过SQL引擎能力建设我们在Galaxy数据研发IDE上支持了个性化词法规则定制能力,包含字段别名支持中文, 表变量等场景, 同时通过语法解析和监听器能力,支持实时识别各类的语法规则,包含表,函数,字段等做辅助编程提示和做精准化的库,表,字段代码补全和推荐。

后续我们仍面临很大的挑战,在非专业的数据开发背景、复杂的业务定制需求、语言定义的复杂性和嵌套深度等因素共同导致了解析器的开发难度。目前,在语法校验自动纠错提示上,虽然ANTLR的提供了自动错误恢复机制但整体表现并不理想,后续2个方向,第一,接入大模型的能力。第二,从基础语法定义上进行重构,减少语法歧义和层级优化。为了应对这些挑战,我们需要加强对 ANTLR 和 Spark SQL语言,数据处理的理解,以便顺利使用和扩展解析器。

参考资料

  • ANTLR
  • ANTLR4-C3
  • DataWorks Copilot:大模型时代数据开发的新范式
  • ANTLR4权威指南 - [美] 特恩斯·帕尔 著

文 / 吴所谓(Ethan)

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

相关推荐

压测AI Agent 治理与评测调优实践|得物技术

本文介绍智能分析Agent在压测调试中上线后的治理闭环建设:通过Langfuse记录完整执行链路,结合用户反馈与Trace还原问题根因;利用CLI Mock固化证据数据集,实现相同场景下的回测验证。基于固定案例与五档评分机制,持续评估方案质量、延迟与成本,确保修复有效且无新问题引入,推动Agent能力迭代优化。

SmartCodeTech的博客 310

从中心调度到自驱协作多 Agent 架构的三次反转|得物技术

本文探讨多Agent系统中的控制权分配问题,提出三大协作架构:Master Agent(中心调度)、任务看板(共享状态)与Advisor(轻量咨询)。通过划清上下文边界与模型边界,强调应以信息相关性而非职业标签划分Agent。三种模式分别将决策权集中于中心、下放至共享状态、再下沉至执行者,形成控制权逐步下沉的演进路径。案例显示,合理设计可提升复杂任务的并行性、弹性与可维护性。

SmartCodeTech的博客 420

Jev 实战:为AI工作流引入轻量决策层 x10倍提速|得物技术

完成这三个场景后,我对适用边界有了更明确的认识:它们都可以先定义答案范围,再根据判断结果驱动下一步操作。接入时,我会优先明确输入、选项、采纳条件和兜底路径,再评估Jev是否适合承担这一环节。结合这些实践,我更容易理解Jev(TypeSafe System One)作为专用结构化决策模型不生成自然语言,而是返回带概率的判断结果。后续接入时,我会继续从脚本中边界明确的判断任务开始,分别验证准确性、耗时和费用,再决定是否扩大使用范围。下面整理了相关能力、限制和接入要点,便于查阅。

SmartCodeTech的博客 345

国产算力·昇腾 910C 大模型推理优化(下):源码修改与算子优化,提速30%|得物技术

本文延续上篇,深入vllm-ascend源码优化:通过多流重叠实现MoE共享专家并行计算,消除冗余事件开销,并解除与FUSED_MC2的互斥限制,实现c=1下TPOT从8.2ms降至8.0ms;优化decode阶段host侧开销,引入状态跟踪跳过冗余拷贝;设计Block Table CPU快路径,规避小batch下NPU kernel启动延迟;针对GDN层融合L2Norm至recurrent kernel,减少算子调用与内存往返。综合优化后推理性能显著提升,形成可复用的昇腾910C大模型推理优化方法论。

SmartCodeTech的博客 245

国产算力·昇腾 910C 大模型推理优化(上):镜像更新与参数调优,提速 40%|得物技术

文中详细给出每一项优化的动机、实现(含具体源码改动路径与关键代码片段)、验证数据与踩坑记录(包括十余项实测无效或负向的尝试),希望为同样在国产算力上做推理优化的团队提供一份可直接复用的工程参考。

SmartCodeTech的博客 225

动态规则的 Agent 实践:用确定性工程驾驭 LLM 的资产智能验收

面对财务验收中规则多变、材料非结构化、核对复杂等痛点,传统规则引擎难以应对。系统采用“规则即配置”思路,由大语言模型(LLM)负责理解合同、扫描件等非结构化材料并执行语义比对,工程系统则保障流程编排与可追溯性。通过异步处理、幂等设计、三类流量隔离及双通道(文本/图像)处理,实现高效稳定运行。结合用户反馈构建自动标准答案,辅以试跑、回测与发布门禁机制,形成闭环质量体系。底层采用确定性工作流架构,确保行为可预期、可测试,真正实现灵活规则的快速迭代与安全上线。

SmartCodeTech的博客 344

发一张图,它居然“跃“出屏幕了?一文读懂得物 3D 空间图片技术实践

3D 空间图片功能的落地,涉及模型生成、内容分发、客户端渲染之间的一系列取舍。本文重点分享如何生产出高质量、小体积的模型,以及如何在手机上流畅运行。

SmartCodeTech的博客 555

别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?

你搜"af1",它得给你"空军一号"。这一年,得物交易搜索把召回从"做选择题"改成了"写答案"——不再从商品池里"找"最像的,而是让模型自己"生成"你真正想要的。从 LLM 语义索引到 HLLM、语义 ID,搜索是怎么从"检索"走向"生成"的?

SmartCodeTech的博客 594

指标平台:从语义底座到智能消费的实践路径|得物技术

这篇文章想做的,不是在本体化的宏大叙事上再叠加一层理论,而是把我们在真实落地中验证过的路径讲清楚:语义底座怎么从零建立起来、怎么保证质量不失控、怎么支撑 Text2SQL 和 AI Coding 两条消费主线,以及沿着这条路径往前走,下一步该往哪个方向发力。

SmartCodeTech的博客 317

企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术

基于 AgentScope Java 的企业级 MultiAgent 平台,为研发、数据分析和知识问答等场景提供统一的模型调用、工具接入、任务编排、流式输出与运行追踪能力。

SmartCodeTech的博客 302

得物小摊 AI Native 演进实录:用 Harness 构建可控 AI 交付

AI Coding 一个人用很顺手,进了团队就漏:指令没人审、经验难沉淀、过程看不见、链路不闭环。作者团队基于 Multica 做了 EP-Harness,把 Agent 当成员管,给它套一层可执行、可观察、可治理的 Harness,并让反馈闭环。成果:可追溯决策、多 Agent 协作,高频异常从每 4 小时 2400 多条降到个位数,对得物技术补上团队级 AI 协作关键一环。

SmartCodeTech的博客 543

企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术

在 MultiAgent 平台中,Agent 的"记忆"能力是实现智能对话和个性化服务的核心资产。本文深入剖析 Agent 记忆系统的设计理念与实现细节,重点解析长期记忆架构的写入与加载流程,涵盖 LLM 智能判断、语义检索、Token 预算控制、冲突处理等关键技术,为开发者提供完整的技术实现参考。

SmartCodeTech的博客 502

EP-Harness:从个人 AI Coding 到团队级 Agent 工作流|得物技术

AI Coding 个人用得爽,一到团队就崩。EP-Harness 把 Agent 从聊天工具变成团队协作成员——可管理、可追溯、可闭环。落地后高频异常从 2400+/4h 收敛到个位数。

SmartCodeTech的博客 694

得物知识问答:复合检索 Agent 的系统设计实践

从"搜到什么用什么"到"搜→筛→挖→验"自主决策闭环——我们造了一个有判断力的 RAG Agent,不是套壳。

SmartCodeTech的博客 734

实战从零开始构建一个Coding Agent:Violin |得物技术

理解了 coding agent 的构建原理,也就掌握了理解其他 agent 的一把钥匙。笔者对每天都在使用的 agent 原理很感兴趣,于是决定自己写一个:violin。

SmartCodeTech的博客 927

AI Native 交易核心系统的研发范式|得物技术

如何改造旧研发流程适配 AI 编码?本文提出五道标准化关口治理思路,依托 Claude Code Spec-Driven 插件打通需求澄清、方案设计、TDD 落地、门禁管控与埋点监控,构建带人工确认门控的完整工作流,以五层自底向上架构实现 AI 编码规范化落地。

SmartCodeTech的博客 975

推荐系统体验的数字化突破:得物自动化评测平台的技术实践|AICon 文章整理

「得物推荐 AI Harness 工程化实践系列」的末篇内容,本系列共三篇连载。本篇(末篇)详解得物推荐评测平台的完整技术体系:从主观体验量化到大模型自动化评测流水线,涵盖多维度指标设计、人机协作提示词校验闭环、分布式任务调度与成本精控等核心工程实现,以及单周百万级审计需求下实现 91% 资源成本优化的工业级落地实战细节。

SmartCodeTech的博客 1494

RAG 核心概念与原理:Chunking、Embedding、相似度、HNSW 与多路召回|得物技术

决定 RAG 效果的关键不在大模型,而是文本切块、查询改写、多路召回与精排。合理切块存好知识库内容,向量匹配抓语义、BM25 匹配关键词互补。先优化用户提问,快速召回一批相关文档,再用精排模型精细筛选,整套检索流程配合到位,问答准确度才会明显提升。

SmartCodeTech的博客 1079

从“机械应答“到“服务伙伴“:得物高可控智能客服的 Agent 工程实践|AICon 演讲整理

本次分享围绕以下核心技术展开:客服 Agent 挑战与架构设计(Single-Agent→Multi-Agent→Harness);高可控性数据飞轮(PE 自动化 + DPO 训练);RL 策略训练和人工经验对齐;多轮半双工消息流控制。

SmartCodeTech的博客 750

得物推荐系统诊断 Agent:从 “调接口” 到 “会思考”|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。AICon上海2026_全球人工智能开发与应用大会暨大模型应用生态展_InfoQ技术大会「得物推荐 AI Harness 工程化实践系列」的中篇内容,本系列共三篇连载。本篇(中篇)进一步详解得物推荐复杂业务场景下,AI代码从生成、防护校验到安全上线的完整技术体系、自研AI Harness整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。

SmartCodeTech的博客 1364

得物 OceanBase 落地实践

得物选型OceanBase多模数据库应对业务挑战,通过验证其高可用架构、存储压缩技术及混合负载处理能力。测试显示OceanBase在复杂查询场景性能较MySQL提升130-200倍,在高并发TP/AP混合场景表现优异,存储成本降低40%+。通过DBA内部报表业务试点验证其稳定性后,逐步推广至核心业务场景,优化数据库技术栈,实现性能与成本的双重收益。

SmartCodeTech的博客 1383

AI UITester:AI Native 的 UI 自动化测试新范式|得物技术

ai_uitester 代表了 AI Native 的 UI 自动化测试新范式:这不仅是工具的升级,而是测试范式的转变 ------ 从 "代码驱动" 转向 "视觉驱动",从 "人工调试" 转向 "AI 自愈",从 "三端分离" 转向 "统一抽象"。

SmartCodeTech的博客 779

从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理

本文是得物技术专家在 AICon 上海演讲整理的技术实录。「得物推荐 AI Harness 工程化实践系列」的开篇内容,本系列共三篇连载。将系统拆解得物推荐复杂业务场景下,AI 代码从生成、防护校验到安全上线的完整技术体系,逐一详解自研 AI Harness 整体架构框架、全流程安全防护机制、混合智能体核心算法实现,以及工业级工程落地的实战细节。本篇(上篇):将整体介绍团队自研的 AI Harness 体系,包括建设思路、全生命周期防护机制、混合智能体架构及落地效果。

SmartCodeTech的博客 1082

从表单到 Agent:得物社区活动搭建的 AI 实践之路

一场营销活动从策划到上线,运营要在三个系统间跳转 10 + 次、填写 40 + 个字段。我们用 AI 重新设计了这条链路 —— 从 “AI 帮你填表单” 到 “两阶段 Agent + 聚合工作台”。这篇文章记录的不是技术细节,而是这条路上的选择和反思。

SmartCodeTech的博客 744

从埋点需求到规则资产:Hermes Agent 重构得物数仓工作流

埋点和指标需求中,消耗数据承接方的不是生成答案,而是补齐证据链、对齐历史口径、识别下游影响,并在生产写入前留下可追溯的确认记录。本文数据承接方指承接业务需求的数据同学或业务合作伙伴;后文讨论内部研发、元数据、查询和门禁能力在流程中的角色。

SmartCodeTech的博客 657

让 Claude Code 拥有自我进化和记忆系统|得物技术

本文记录了为 Claude Code 构建的一套持久化记忆与自我学习系统的设计思路与实现细节。这套系统让 AI 助手能够跨越会话边界记住用户习惯,并在每次对话结束后自动提炼行为规律和系统信息,通过向量检索快速提取关键信息,下一次对话时主动应用,逐步进化为更懂你的编程伙伴。

SmartCodeTech的博客 994

用 LLM Agent 重构告警排查流程|得物技术

告警排查需频繁切换日志、APM、链路追踪等多个平台,耗时 10~30 分钟,且高度依赖个人经验。我们做了 Troubleshooter——用 LLM Agent 自动完成数据采集、根因分析和处置建议生成。上线后中位数排查耗时从 20 分钟降到 4.4 分钟,覆盖 11 个服务和 10+ 种告警类型。

SmartCodeTech的博客 1008

HorizonVault 技术深潜:如何在 HDD 上做出 100GB/s+ 级大吞吐分布式存储|得物技术

HorizonVault是一款面向Kafka远程存储的高吞吐分布式存储引擎,通过分布式并行写入和状态感知调度解决冷数据存储的三大核心问题:吞吐瓶颈、性能抖动和恢复能力。系统采用分层架构,Meta负责全局资源调度,Broker处理请求路由,Store实现本地磁盘管理,形成完整的写入治理链路。通过将HDD磁盘提升为一等资源,实现顺序追加写入、多盘并行和慢盘隔离,在保证高吞吐的同时控制延迟。存储结构采用DiskStore组织磁盘资源,LogSegment顺序写入数据,IndexSegment定位读取,Change

SmartCodeTech的博客 892

Claude Code Harness 工程:数仓侧落地方案|得物技术

Harness 工程通过职责分层实现研发流水线可靠性提升。Claude 负责需求理解与代码生成等语义层工作;hooks 负责规范检查等确定性执行;Subagents 隔离血缘查询、自测等高 token 操作;Memory 承载跨会话状态持久化。四层协同使数仓 AI 开发从"对话式辅助"转变为"规则驱动的自动化流程"。

SmartCodeTech的博客 1101

BP Claw 破解 AI 编码输入难题 ——FlinkSpec 需求智能化实践|得物技术

本文为 FlinkSpec 系列之开篇,亦是这场工程化变革的序章。BP Claw 所立之处,不过链路之源。FlinkSpec 所图,乃以 AI 之力,将实时数仓从需求落地至验收上线的全程工序,熔铸为一套精密自洽、生生不息的智能工程体系。宏图未竟,后续系列将逐章揭幕,敬请期待!

SmartCodeTech的博客 1039
上一篇: LSM-TREE从入门到入魔:从零开始实现一个高性能键值存储 | 得物技术
下一篇: 得物 Android Crash 治理实践
得物技术
得物技术 得物技术官方账号 得物技术官方账号
博客等级 码龄6年 2492粉丝 408原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值