区分 sql 语法中的函数名和函数参数

Differentiate function name and function arguments in sql grammar

有了这个 grammar ,我试图从 sql 查询中提取用户编写的表达式。

例如,我想从此查询中提取 FNAME、LName 和名称。

SELECT TRIM(CONCAT(FNAME , LNAME)) AS `name`FROM  CLIENTS;

[解析树]

我可以用我的听众提取 'name' :

public void enterSelectSingle(ksqlParser.SelectSingleContext ctx) {
    super.enterSelectSingle(ctx);
    System.out.println(ctx.identifier().getText());
}

但是当我尝试使用 ctx.expression().getText(). 提取“FNAME,LNAME”时,我得到 TRIM(CONCAT(FNAME,LNAME))

我如何区分 CONCAT、TRIM、(,) 和 , 与 FNAME 和 LNAME,因为它们都被识别为标识符并隐藏在语法中的表达式后面?

如果您查看语法,您可以看到以下“primaryExpression”的解析器规则

(在你的问题的树图中引用了它):

primaryExpression
    : literal                                                                             #literalExpression
    | identifier STRING                                                                   #typeConstructor
    | CASE valueExpression whenClause+ (ELSE elseExpression=expression)? END              #simpleCase
    | CASE whenClause+ (ELSE elseExpression=expression)? END                              #searchedCase
    | CAST '(' expression AS type ')'                                                     #cast
    | ARRAY '[' (expression (',' expression)*)? ']'                                       #arrayConstructor
    | MAP '(' (expression ASSIGN expression (',' expression ASSIGN expression)*)? ')'     #mapConstructor
    | STRUCT '(' (identifier ASSIGN expression (',' identifier ASSIGN expression)*)? ')'  #structConstructor
    | identifier '(' ASTERISK ')'                                                           #functionCall
    | identifier '(' (functionArgument (',' functionArgument)* (',' lambdaFunction)*)? ')' #functionCall
    | value=primaryExpression '[' index=valueExpression ']'                               #subscript
    | identifier                                                                          #columnReference
    | identifier '.' identifier                                                           #qualifiedColumnReference
    | base=primaryExpression STRUCT_FIELD_REF fieldName=identifier                        #dereference
    | '(' expression ')'                                                                  #parenthesizedExpression
    ;

与您的列引用匹配的备选方案是 #columnReference 备选方案。

这意味着,在您的侦听器中,您可以覆盖 enterColumnReference 方法,就像这样(以匹配 ONLY parseExpression 规则的备选方案) .它只有一个 identifier 规则成员,因此您只需引用它:

    @Override
    public void enterColumnReference(SqlBaseParser.ColumnReferenceContext ctx) {
        System.out.println(ctx.identifier().getText());
    }

使用该覆盖(除了你的覆盖),我得到以下输出:

`name`
FNAME
LNAME

此语法标记了解析器规则中的所有备选方案(至少据我所知)。这使得拦截(或监听)非常具体的解析器规则替代方案变得相对容易。每个标记的备选方案都有自己的 enter*exit* 方法以及特定的 *Context class 以非常简单地访问规则成员。

在您的情况下,您似乎只关心那个替代方案 IF 它是 functionArgument。如果是这种情况,您可以在侦听器中引入一些状态管理来跟踪它:

public class MyListener extends SqlBaseBaseListener {
    private boolean isFunctionArgument = false;

    @Override
    public void enterSelectSingle(SqlBaseParser.SelectSingleContext ctx) {
        System.out.println(ctx.identifier().getText());
    }

    @Override
    public void enterFunctionArgument(SqlBaseParser.FunctionArgumentContext ctx) {
        isFunctionArgument = true;
    }

    @Override
    public void exitListFunctions(SqlBaseParser.ListFunctionsContext ctx) {
        isFunctionArgument = true;
    }

    @Override
    public void enterColumnReference(SqlBaseParser.ColumnReferenceContext ctx) {
        if (isFunctionArgument) {
            System.out.println(ctx.identifier().getText());
        }
    }
}

在您的示例中,这不会更改输出,但应该让您了解如何 select 更具体的使用上下文。

在“你可以从这里到达那里”的意义上,它是可能的:

    @Override
    public void enterSelectSingle(SqlBaseParser.SelectSingleContext ctx) {
        System.out.println(ctx.identifier().getText());
        SqlBaseParser.BooleanDefaultContext bdc = (SqlBaseParser.BooleanDefaultContext) ctx.expression().booleanExpression();
        SqlBaseParser.ValueExpressionDefaultContext cev = (SqlBaseParser.ValueExpressionDefaultContext) bdc.predicated().valueExpression();
        SqlBaseParser.FunctionCallContext fc = (SqlBaseParser.FunctionCallContext) cev.primaryExpression();
        for (SqlBaseParser.FunctionArgumentContext fa : fc.functionArgument()) {
            SqlBaseParser.BooleanDefaultContext bdcfa = (SqlBaseParser.BooleanDefaultContext) fa.expression().booleanExpression();
            SqlBaseParser.ValueExpressionDefaultContext cevfa = (SqlBaseParser.ValueExpressionDefaultContext) bdcfa.predicated().valueExpression();
            SqlBaseParser.FunctionCallContext fc2 = (SqlBaseParser.FunctionCallContext) cevfa.primaryExpression();
            for (SqlBaseParser.FunctionArgumentContext fa2 : fc2.functionArgument()) {
                SqlBaseParser.BooleanDefaultContext bdcfa2 = (SqlBaseParser.BooleanDefaultContext) fa2.expression().booleanExpression();
                SqlBaseParser.ValueExpressionDefaultContext cevfa2 = (SqlBaseParser.ValueExpressionDefaultContext) bdcfa2.predicated().valueExpression();
                SqlBaseParser.ColumnReferenceContext cr = (SqlBaseParser.ColumnReferenceContext) cevfa2.primaryExpression();
                System.out.println(cr.identifier().getText());
            }
        }
    }

这是一条艰难的道路(我只是直接在您绝对应该进行 instanceof 测试的地方进行类型转换。)

它也很脆。任何微小的结构变化都会破坏此代码。因此,您需要大量逻辑来导航 singleSelect

下 parseTree 的所有可能排列

更好的方法(利用听众为您做的事情):注意:我将 enterSelectSingle 更改为 exitSelectSingle。你需要等到你已经听完子节点才能收集参数并打印出来。

import java.util.ArrayList;

public class MyListener extends SqlBaseBaseListener {
    private boolean isFunctionArgument = false;
    private final ArrayList<String> args = new ArrayList<>();

    @Override
    public void exitSelectSingle(SqlBaseParser.SelectSingleContext ctx) {
        System.out.println(ctx.identifier().getText());
        for (String arg : args) {
            System.out.println(arg);
        }
        args.clear();
    }

    @Override
    public void enterFunctionArgument(SqlBaseParser.FunctionArgumentContext ctx) {
        isFunctionArgument = true;
    }

    @Override
    public void exitListFunctions(SqlBaseParser.ListFunctionsContext ctx) {
        isFunctionArgument = true;
    }

    @Override
    public void enterColumnReference(SqlBaseParser.ColumnReferenceContext ctx) {
        if (isFunctionArgument) {
            args.add(ctx.identifier().getText());
        }
    }
}

注意:即使是这段代码(为了简单起见)也不处理嵌套函数调用(为此你需要创建一个 ArrayList 堆栈,然后 push/pop 当你 enter/exit 函数。

我通常编写代码来访问我的 parseTree,创建我想在我的程序中处理的简化内部树,但这是一个比这更长的答案,已经很长了,回复。

简而言之(为时已晚),它可能很复杂,您必须处理这种复杂性,除非您知道您只需要处理一个更简单的案例子集。