Skip to content

Commit

Permalink
feat: create translator from query expr to SQL query and ES query
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad Luthfi Fahlevi committed Aug 8, 2024
1 parent 573ecac commit 1bc3990
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 0 deletions.
364 changes: 364 additions & 0 deletions pkg/translator/query_expr_translator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
package translator

import (
"encoding/json"
"errors"
"fmt"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/parser"
"log"
"strconv"
"strings"
)

type QueryExprTranslator struct {
QueryExpr string
SqlQuery strings.Builder
EsQuery map[string]interface{}
}

type ExprParam map[string]interface{}

func (q *QueryExprTranslator) getTreeNodeFromQueryExpr() *ast.Node {
parsed, err := parser.Parse(q.QueryExpr)
if err != nil {
log.Fatalf("Error parsing expression: %v", err)
}

return &parsed.Node
}

func (q *QueryExprTranslator) ConvertToSQL() (string, error) {
q.SqlQuery = strings.Builder{}
q.TranslateToSQL(q.getTreeNodeFromQueryExpr(), q)
return q.SqlQuery.String(), nil
}

func (q *QueryExprTranslator) TranslateToSQL(node *ast.Node, translator *QueryExprTranslator) {
if *node == nil {
return
}
switch n := (*node).(type) {
case *ast.BinaryNode:

Check failure on line 43 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 43 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 43 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 43 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 43 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)
translator.SqlQuery.WriteString("(")
q.TranslateToSQL(&n.Left, translator)

// write operator
operator := q.operatorToSQL(n)
translator.SqlQuery.WriteString(fmt.Sprintf(" %s ", strings.ToUpper(operator)))

q.TranslateToSQL(&n.Right, translator)
translator.SqlQuery.WriteString(")")
case *ast.NilNode:
translator.SqlQuery.WriteString(fmt.Sprintf("%s", "NULL"))
case *ast.IdentifierNode:
translator.SqlQuery.WriteString(n.Value)
case *ast.IntegerNode:
translator.SqlQuery.WriteString(strconv.FormatInt(int64(n.Value), 10))
case *ast.FloatNode:
translator.SqlQuery.WriteString(strconv.FormatFloat(n.Value, 'f', -1, 64))
case *ast.BoolNode:
translator.SqlQuery.WriteString(strconv.FormatBool(n.Value))
case *ast.StringNode:
translator.SqlQuery.WriteString(fmt.Sprintf("'%s'", n.Value))
case *ast.ConstantNode:
translator.SqlQuery.WriteString(fmt.Sprintf("%s", n.Value))
case *ast.UnaryNode:
switch n.Operator {
case "not":
binaryNode, ok := (n.Node).(*ast.BinaryNode)
if ok && binaryNode.Operator == "in" {
ast.Patch(&n.Node, &ast.BinaryNode{
Operator: "not in",
Left: binaryNode.Left,
Right: binaryNode.Right,
})
}
case "!":
switch nodeV := n.Node.(type) {
case *ast.BoolNode:
ast.Patch(&n.Node, &ast.BoolNode{
Value: !nodeV.Value,
})
// adjust other type if needed
}
}
q.TranslateToSQL(&n.Node, translator)
case *ast.BuiltinNode:
result, err := q.getQueryExprResult(n.String())
if err != nil {
return
}
translator.SqlQuery.WriteString(fmt.Sprintf("%s", result))
case *ast.ArrayNode:
translator.SqlQuery.WriteString("(")
for i := range n.Nodes {
q.TranslateToSQL(&n.Nodes[i], translator)
if i != len(n.Nodes)-1 {
translator.SqlQuery.WriteString(", ")
}
}
translator.SqlQuery.WriteString(")")
case *ast.ConditionalNode:
result, err := q.getQueryExprResult(n.String())
if err != nil {
return
}
if nodeV, ok := result.(ast.Node); ok {
q.TranslateToSQL(&nodeV, translator)
}
case *ast.ChainNode:
case *ast.MemberNode:
case *ast.SliceNode:
case *ast.CallNode:
case *ast.ClosureNode:
case *ast.PointerNode:
case *ast.VariableDeclaratorNode:
case *ast.MapNode:
case *ast.PairNode:
default:
panic(fmt.Sprintf("undefined node type (%T)", node))
}
}

func (q *QueryExprTranslator) getQueryExprResult(fn string) (any, error) {
env := make(ExprParam)
compile, err := expr.Compile(fn)
if err != nil {
return nil, fmt.Errorf("failed to compile function '%s': %w", fn, err)
}

result, err := expr.Run(compile, env)
if err != nil {
return nil, fmt.Errorf("failed to evaluate function '%s': %w", fn, err)
}

return result, nil
}

func (q *QueryExprTranslator) operatorToSQL(bn *ast.BinaryNode) string {
switch {
case bn.Operator == "&&":
return "AND"
case bn.Operator == "||":
return "OR"
case bn.Operator == "!=":
if _, ok := bn.Right.(*ast.NilNode); ok {
return "IS NOT"
}
case bn.Operator == "==":
if _, ok := bn.Right.(*ast.NilNode); ok {
return "IS"
} else {
return "="
}
}

return bn.Operator
}

func (q *QueryExprTranslator) ConvertToEsQuery() (string, error) {
esQueryInterface := q.TranslateToEsQuery(q.getTreeNodeFromQueryExpr())
esQuery, ok := esQueryInterface.(map[string]interface{})
if !ok {
return "", errors.New("failed to generate Elasticsearch query")
}
q.EsQuery = map[string]interface{}{"query": esQuery}

// Convert to JSON
queryJSON, err := json.Marshal(q.EsQuery)
if err != nil {
return "", err
}

return string(queryJSON), nil
}

func (q *QueryExprTranslator) TranslateToEsQuery(node *ast.Node) interface{} {
if *node == nil {
return nil
}
switch n := (*node).(type) {
case *ast.BinaryNode:

Check failure on line 183 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 183 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 183 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 183 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)

Check failure on line 183 in pkg/translator/query_expr_translator.go

View workflow job for this annotation

GitHub Actions / golangci

previous case (typecheck)
return q.translateBinaryNodeToEsQuery(n)
case *ast.NilNode:
return nil
case *ast.IdentifierNode:
return n.Value
case *ast.IntegerNode:
return n.Value
case *ast.FloatNode:
return n.Value
case *ast.BoolNode:
return n.Value
case *ast.StringNode:
return n.Value
case *ast.UnaryNode:
return q.translateUnaryNodeToEsQuery(n)
case *ast.ArrayNode:
return q.translateArrayNodeToEsQuery(n)
case *ast.ConstantNode:
return n.Value
case *ast.BuiltinNode:
result, err := q.getQueryExprResult(n.String())
if err != nil {
return nil
}
return result
case *ast.ConditionalNode:
result, err := q.getQueryExprResult(n.String())
if err != nil {
return nil
}
if nodeV, ok := result.(ast.Node); ok {
return q.TranslateToEsQuery(&nodeV)
}
case *ast.ChainNode:
case *ast.MemberNode:
case *ast.SliceNode:
case *ast.CallNode:
case *ast.ClosureNode:
case *ast.PointerNode:
case *ast.VariableDeclaratorNode:
case *ast.MapNode:
case *ast.PairNode:
default:
panic(fmt.Sprintf("undefined node type (%T)", node))
}

return nil
}

func (q *QueryExprTranslator) translateBinaryNodeToEsQuery(n *ast.BinaryNode) map[string]interface{} {
left := q.TranslateToEsQuery(&n.Left)
right := q.TranslateToEsQuery(&n.Right)

switch n.Operator {
case "&&":
return q.boolQuery("must", left, right)
case "||":
return q.boolQuery("should", left, right)
case "==":
return q.termQuery(left.(string), right)
case "!=":
return q.mustNotQuery(left.(string), right)
case "<", "<=", ">", ">=":
return q.rangeQuery(left.(string), q.operatorToEsQuery(n.Operator), right)
case "in":
return q.termsQuery(left.(string), right)
default:
return nil
}
}

func (q *QueryExprTranslator) translateUnaryNodeToEsQuery(n *ast.UnaryNode) interface{} {
switch n.Operator {
case "not":
if binaryNode, ok := n.Node.(*ast.BinaryNode); ok && binaryNode.Operator == "in" {
left := q.TranslateToEsQuery(&binaryNode.Left)
right := q.TranslateToEsQuery(&binaryNode.Right)
return q.mustNotTermsQuery(left.(string), right)
}
return nil
case "!":
nodeValue := q.TranslateToEsQuery(&n.Node)
switch value := nodeValue.(type) {
case bool:
return !value
default:
return map[string]interface{}{
"bool": map[string]interface{}{
"must_not": []interface{}{nodeValue},
},
}
}
default:
return nil
}
}

func (q *QueryExprTranslator) translateArrayNodeToEsQuery(n *ast.ArrayNode) []interface{} {
values := make([]interface{}, len(n.Nodes))
for i, node := range n.Nodes {
values[i] = q.TranslateToEsQuery(&node)
}
return values
}

func (q *QueryExprTranslator) operatorToEsQuery(operator string) string {
switch operator {
case ">":
return "gt"
case ">=":
return "gte"
case "<":
return "lt"
case "<=":
return "lte"
}

return operator
}

func (q *QueryExprTranslator) boolQuery(condition string, left, right interface{}) map[string]interface{} {
return map[string]interface{}{
"bool": map[string]interface{}{
condition: []interface{}{left, right},
},
}
}

func (q *QueryExprTranslator) termQuery(field string, value interface{}) map[string]interface{} {
return map[string]interface{}{
"term": map[string]interface{}{
field: value,
},
}
}

func (q *QueryExprTranslator) mustNotQuery(field string, value interface{}) map[string]interface{} {
return map[string]interface{}{
"bool": map[string]interface{}{
"must_not": []interface{}{
map[string]interface{}{
"term": map[string]interface{}{
field: value,
},
},
},
},
}
}

func (q *QueryExprTranslator) rangeQuery(field, operator string, value interface{}) map[string]interface{} {
return map[string]interface{}{
"range": map[string]interface{}{
field: map[string]interface{}{
operator: value,
},
},
}
}

func (q *QueryExprTranslator) termsQuery(field string, values interface{}) map[string]interface{} {
return map[string]interface{}{
"terms": map[string]interface{}{
field: values,
},
}
}

func (q *QueryExprTranslator) mustNotTermsQuery(field string, values interface{}) map[string]interface{} {
return map[string]interface{}{
"bool": map[string]interface{}{
"must_not": []interface{}{
map[string]interface{}{
"terms": map[string]interface{}{
field: values,
},
},
},
},
}
}
Loading

0 comments on commit 1bc3990

Please sign in to comment.