Skip to content
Search

    Introspecting CDS QL Statements

    API to introspect CDS QL statements in Java

    Content

    Introduction

    Handlers of CQN-based services often need to understand the incoming CQN statements.

    The statement analysis can be done in two different ways. Depending on the complexity of the statement it can be done using:

    • CQN Analyzer: A specialized API to extract element values from filter predicates of queries and to analyze the structure and filters of references
    • CQN Visitor: A general purpose API, to traverse CQN token trees such as expressions, predicates, values etc.

    CqnAnalyzer vs. CqnVisitor

    The CqnAnalyzer allows for analysis and extraction of element values for most of the queries, but it comes with some limitations. The main rule here is:

    The value of an element reference in a where and filter predicate must be unambiguously identified.

    This implies the following:

    • The operator of comparison predicate must be either eq or is:
    Select.from("bookshop.Book").where(b -> b.get("ID").eq(42));
    
    • Only the conjunction and is used to connect predicates:
    Select.from("bookshop.Book")
    	.where(b -> b.get("ID").eq(42).and(b.get("title").is("Capire"));
    

    This rule also applies to all segments of all references of the query, be it simple query or the one with path expression:

    Select.from("bookshop.Book",
    	b -> b.filter(b.get("ID").eq(41))
    		.to("author").filter(a -> a.get("Id").eq(1)));
    

    When to Use What

    Use CqnAnalyzer when element references of the query are:

    • Unambiguously identified by: eq or is operator of comparison predicate or used in byId or matching clause of the query
    • Used in conjunction (and) predicates

    Use CqnVisitor when element references of the query are:

    • Compared with lt, gt, le, ge, ne, isNot operator
    • Used within in
    • Negated with not
    • Used in search
    • Used in functions
    • Used in subqueries
    • Referencing elements of an associated entity

    CqnAnalyzer

    The CDS QL introspection API allows to analyze CDS QL statements and extract values and information on the CDS entities in references.

    The CqnAnalyzer can be constructed from a CDS model:

    import com.sap.cds.ql.cqn.CqnAnalyzer;
    
    CdsModel cdsModel = context.getModel();
    CqnAnalyzer cqnAnalyzer = CqnAnalyzer.create(cdsModel);
    

    Furthermore, the static isCountQuery(cqn) method can be used to check if a CDS QL query only returns a single count:

    // cqn: Select.from("Books").columns(CQL.count().as("bookCount"));
    boolean isCount = CqnAnalyzer.isCountQuery(cqn);  // true
    

    Usage

    Given the following CDS model and CQL query:

    entity Orders {
      key OrderNo : String;
      Items       : Composition of many OrderItems on Items.parent = $self;
      ...
    }
    entity OrderItems {
      key ID : Integer;
      book   : Association to Books;
      ...
    }
    

    Find this source also in cap/samples.

    --CQL query
    SELECT from Orders[OrderNo = '42'].items[ID = 1]
    

    the corresponding CQN statement can be analyzed using the analyze method of the CqnAnalyzer:

    CqnStatement cqn = context.getCqn();
    
    AnalysisResult result = cqnAnalyzer.analyze(cqn.ref());
    

    Resolving CDS Entities

    Based on the AnalysisResult, information on the CDS entities can be accessed through the Reflection API:

    CdsEntity order = result.rootEntity();   // Orders
    CdsEntity item  = result.targetEntity(); // OrderItems
    

    Extracting Filter Values

    Extracting filter values from CQN statements is supported for noncomplex predicates where the values can be unambiguously determined.

    The key values of the entities can be extracted as a map using the rootKeys and targetKeys method of the AnalysisResult object:

    Map<String, Object> rootKeys = result.rootKeys();
    String orderNo = (String) rootKeys.get("OrderNo"); // 42
    
    Map<String, Object> targetKeys  = result.targetKeys();
    Integer itemId = (Integer) targetKeys.get("ID");   // 1
    

    To extract all filter values of the target entity including nonkey values, the targetValues method can be used:

    Map<String, Object> filterValues = result.targetValues();
    

    For CqnSelect, CqnUpdate, and CqnDelete, values can also be extracted from the statement’s where condition:

    --CQL query
    SELECT from Orders[OrderNo = '42'].items where ID = 3 and status = 'open'
    
    CqnSelect select = context.getCqn();
    AnalysisResult result = cqnAnalyzer.analyze(select);
    
    Map<String, Object> targetKeys = result.targetKeys();
    Integer itemId = (Integer) targetKeys.get("ID");   // 3
    
    Map<String, Object> filterValues = result.targetValues();
    String status = (String) filterValues.get("status");   // 'open'
    

    Using the Iterator

    The methods prefixed with root and target access the first respectively last segment of the CQN statement’s reference. If the reference has more than two segments, such as:

    --CQL query
    SELECT from Orders[OrderNo = '42'].items[ID = 1].book
    

    the segment items can be analyzed using an iterator:

    Iterator<ResolvedSegment> iterator = result.iterator();
    CdsEntity order = iterator.next().entity();
    CdsEntity item  = iterator.next().entity();
    CdsEntity book  = iterator.next().entity();
    

    or a reverse iterator starting from the last segment:

    Iterator<ResolvedSegment> iterator = result.reverse();
    CdsEntity book  = iterator.next().entity();
    CdsEntity item  = iterator.next().entity();
    CdsEntity order = iterator.next().entity();
    

    In the same way, also the filter values for each segment can be extracted using the values and keys method instead of the entity method.

    CqnVisitor

    CqnVisitor interface is part of a public API, which allows to traverse CQN token trees such as expressions, predicates, values etc. It follows the Visitor design pattern.

    When a visitor is passed to a token’s accept method, it is traversed through the token’s expression tree. Generally the accept methods of the token’s children are called first (depth-first). Afterwards the visit method that is most specific to the token is invoked. Classes implementing the CqnVisitor interface may override the default visit method to perform arbitrary operations.

    Fields of Application

    It is a powerful tool, which can be handy to introspect the complex queries and its compound parts. It can be used to analyze the information about:

    • Element references
    • Expand associations
    • Connective predicates (and, or)
    • Comparison predicates with binary (gt, lt, ne, etc.) and unary (not) operators
    • search and in predicates
    • Functions and expressions
    • Literals and parameters

    Usage

    In the following example, the CqnVisitor is used to evaluate whether the data matches a given filter expression.

    Data

    List<Map<String, Object>> books = new ArrayList<>();
    books.add(ImmutableMap.of("title", "Catweazle", "stock", 3));
    books.add(ImmutableMap.of("title", "The Raven", "stock", 42));
    books.add(ImmutableMap.of("title", "Dracula", "stock", 66));
    

    Filter

    Predicate titles = CQL.get("title").in("Catweazle", "The Raven");
    Predicate stock = CQL.get("stock").gt(10);
    
    // title IN ('Catweazle', 'The Raven') AND stock > 10
    Predicate filter = CQL.and(titles, stock);
    

    The filter consists of three predicates, substituting the following tree:

                                           AND
                        ┌───────────────────┴───────────────────┐
                        IN                                      GT
            ┌───────────┴───────────┐                   ┌───────┴───────┐
          title        ['Catweazle', 'The Raven']     stock             10
    

    which corresponds to the following CQN token tree (numbers in brackets show the visit order):

                                CqnConnectivePredicate (8)
                      ┌───────────────────┴───────────────────┐
                CqnInPredicate (4)                  CqnComparisonPredicate (7)
          ┌───────────┴───────────┐               ┌───────────┴───────────┐
    CqnElementRef (1)     CqnLiteral (2, 3)  CqnElementRef (5)      CqnLiteral (6)
    

    Visitor

    As already mentioned, the CqnAnalyzer is not suitable to analyze such a predicate, as neither the element title nor stock is uniquely restricted to a single value.

    To overcome this issue a CqnVisitor is to be implemented to evaluate whether the data meets the filter expression. The visitor has access to the data that is checked. To respect the depth-first traversal order, it uses a stack to store intermediate results:

    class CheckDataVisitor implements CqnVisitor {
        private final Map<String, Object> data;
        private final Deque<Object> stack = new ArrayDeque<>();
    
        CheckDataVisitor(Map<String, Object> data) {
            this.data = data;
        }
    
        boolean matches() {
            return (Boolean) stack.pop();
        }
        ...
    

    On the leaf-level, the stack is used to store the concrete values from both data payload and filter expression:

    @Override
    public void visit(CqnElementRef ref) {
        Object dataValue = data.get(ref.displayName());
        stack.push(dataValue);
    }
    
    @Override
    public void visit(CqnLiteral<?> literal) {
        stack.push(literal.value());
    }
    

    When visiting the predicates, the values are popped from the stack and evaluated based on the predicate type and comparison operator. The Boolean result of the evaluation is pushed to the stack:

    @Override
    public void visit(CqnInPredicate in) {
        List<Object> values = in.values().stream()
                .map(v -> stack.pop()).collect(toList());
        Object value = stack.pop();
        stack.push(values.stream().anyMatch(value::equals));
    }
    
    @Override
    public void visit(CqnComparisonPredicate comparison) {
        Comparable rhs = (Comparable) stack.pop();
        Comparable lhs = (Comparable) stack.pop();
        int cmp = lhs.compareTo(rhs);
        switch (comparison.operator()) {
        case EQ:
            stack.push(cmp == 0);
            break;
        case GT:
            stack.push(cmp > 0);
            break;
        // ...
        }
    }
    

    The visit method of the CqnConnectivePredicate pops the Boolean evaluation results from the stack, applies the corresponding logical operator, and pushes the result to the stack:

    @Override
    public void visit(CqnConnectivePredicate connect) {
        Boolean rhs = (Boolean) stack.pop();
        Boolean lhs = (Boolean) stack.pop();
        switch (connect.operator()) {
        case AND:
            stack.push(lhs && rhs);
            break;
        case OR:
            stack.push(lhs || rhs);
            break;
        }
    }
    

    The whole process can be considered as a reduce operation when traversing the tree from bottom to top.

    To evaluate whether given data matches the filter expression, an instance v of the visitor is created. Afterwards the filter’s accept method traverses its expression tree with the visitor, which evaluates the expression during the traversal:

    for (Map<String, Object> book : books) {
        CheckDataVisitor v = new CheckDataVisitor(book);
        filter.accept(v);
        System.out.println(book.get("title") + " " +
                (v.matches() ? "match" : "no match"));
    }
    

    The output will be:

    Catweazle	no match
    The Raven	match
    Dracula		no match