From 57897eadb95befb7025f2111c62e65380f696610 Mon Sep 17 00:00:00 2001 From: Muhammad Luthfi Fahlevi Date: Mon, 12 Aug 2024 18:51:49 +0700 Subject: [PATCH] test: create unit test for converter --- pkg/query_expr/es_expr_test.go | 98 ++++++++++++++++++++++ pkg/query_expr/query_expr_test.go | 134 ++++++++++++++++++++++++++++++ pkg/query_expr/sql_expr.go | 23 ++--- pkg/query_expr/sql_expr_test.go | 98 ++++++++++++++++++++++ 4 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 pkg/query_expr/es_expr_test.go create mode 100644 pkg/query_expr/query_expr_test.go create mode 100644 pkg/query_expr/sql_expr_test.go diff --git a/pkg/query_expr/es_expr_test.go b/pkg/query_expr/es_expr_test.go new file mode 100644 index 00000000..93a2a75f --- /dev/null +++ b/pkg/query_expr/es_expr_test.go @@ -0,0 +1,98 @@ +package queryexpr_test + +import ( + "testing" + + queryexpr "github.com/goto/compass/pkg/query_expr" +) + +func TestESExpr_String(t *testing.T) { + tests := []struct { + expr queryexpr.SQLExpr + want string + }{ + { + expr: queryexpr.SQLExpr("test"), + want: "test", + }, + { + expr: queryexpr.SQLExpr("bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)"), + want: "bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)", + }, + } + for i, tt := range tests { + t.Run("test-case-"+string(rune(i)), func(t *testing.T) { + if got := tt.expr.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestESExpr_ToQuery(t *testing.T) { + tests := []struct { + name string + expr queryexpr.ESExpr + want string + wantErr bool + }{ + { + name: "less than condition with single quote", + expr: queryexpr.ESExpr(`updated_at < '2024-04-05 23:59:59'`), + want: `{"query":{"range":{"updated_at":{"lt":"2024-04-05 23:59:59"}}}}`, + wantErr: false, + }, + { + name: "greater than condition with double quote", + expr: queryexpr.ESExpr(`updated_at > "2024-04-05 23:59:59"`), + want: `{"query":{"range":{"updated_at":{"gt":"2024-04-05 23:59:59"}}}}`, + wantErr: false, + }, + { + name: "in condition", + expr: queryexpr.ESExpr(`service in ["test1","test2","test3"]`), + want: `{"query":{"terms":{"service":["test1","test2","test3"]}}}`, + wantErr: false, + }, + { + name: "equals or not in condition", + expr: queryexpr.ESExpr(`name == "John" || service not in ["test1","test2","test3"]`), + want: `{"query":{"bool":{"should":[{"term":{"name":"John"}},{"bool":{"must_not":[{"terms":{"service":["test1","test2","test3"]}}]}}]}}}`, + wantErr: false, + }, + { + name: "complex query expression that can directly produce a value", + expr: queryexpr.ESExpr(`bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)`), + want: `{"query":{"term":{"bool_identifier":false}}}`, + wantErr: false, + }, + { + name: "complex query expression that can NOT directly produce a value", + expr: queryexpr.ESExpr(`service in filter(assets, .Service startsWith "T")`), + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.expr.ToQuery() + if (err != nil) != tt.wantErr { + t.Errorf("ToQuery() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ToQuery() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestESExpr_Validate(t *testing.T) { + t.Run("should return nil as default validation", func(t *testing.T) { + expr := queryexpr.ESExpr("query_es == 'test'") + if err := (&expr).Validate(); err != nil { + t.Errorf("Validate() error = %v, wantErr %v", err, nil) + } + return + }) +} diff --git a/pkg/query_expr/query_expr_test.go b/pkg/query_expr/query_expr_test.go new file mode 100644 index 00000000..748628fa --- /dev/null +++ b/pkg/query_expr/query_expr_test.go @@ -0,0 +1,134 @@ +package queryexpr + +import ( + "reflect" + "testing" +) + +func TestGetIdentifiers(t *testing.T) { + tests := []struct { + name string + expr string + want []string + wantErr bool + }{ + { + name: "got 0 identifier test", + expr: `findLast([1, 2, 3, 4], # > 2)`, + want: nil, + wantErr: false, + }, + { + name: "got 1 identifiers test", + expr: `updated_at < '2024-04-05 23:59:59'`, + want: []string{"updated_at"}, + wantErr: false, + }, + { + name: "got 3 identifiers test", + expr: `(identifier1 == !(findLast([1, 2, 3, 4], # > 2) == 4)) && identifier2 != 'John' || identifier3 == "hallo"`, + want: []string{"identifier1", "identifier2", "identifier3"}, + wantErr: false, + }, + { + name: "got error", + expr: `findLast([1, 2, 3, 4], # > 2`, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetIdentifiers(tt.expr) + if (err != nil) != tt.wantErr { + t.Errorf("GetIdentifiers() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetIdentifiers() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetQueryExprResult(t *testing.T) { + tests := []struct { + name string + expr string + want any + wantErr bool + }{ + { + name: "return a value from func", + expr: `findLast([1, 2, 3, 4], # > 2)`, + want: 4, + wantErr: false, + }, + { + name: "return a value func equation", + expr: `false == !true`, + want: true, + wantErr: false, + }, + { + name: "got error due to can NOT directly produce a value", + expr: `updated_at < '2024-04-05 23:59:59'`, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetQueryExprResult(tt.expr) + if (err != nil) != tt.wantErr { + t.Errorf("GetQueryExprResult() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetQueryExprResult() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetTreeNodeFromQueryExpr(t *testing.T) { + tests := []struct { + name string + expr string + want any + wantErr bool + }{ + { + name: "success using func from the library", + expr: `findLast([1], # >= 1)`, + want: "findLast([1], # >= 1)", + wantErr: false, + }, + { + name: "success using equation", + expr: `false == !true`, + want: "false == !true", + wantErr: false, + }, + { + name: "got error using wrong syntax", + expr: `findLast(`, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetTreeNodeFromQueryExpr(tt.expr) + if (err != nil) != tt.wantErr { + t.Errorf("GetTreeNodeFromQueryExpr() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if got.String() != tt.want { + t.Errorf("GetTreeNodeFromQueryExpr() got = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/pkg/query_expr/sql_expr.go b/pkg/query_expr/sql_expr.go index 9d081038..ec5e3c52 100644 --- a/pkg/query_expr/sql_expr.go +++ b/pkg/query_expr/sql_expr.go @@ -21,7 +21,7 @@ func (s *SQLExpr) ToQuery() (string, error) { return "", err } stringBuilder := &strings.Builder{} - if err := s.ConvertToSQL(queryExprParsed, stringBuilder); err != nil { + if err := s.convertToSQL(queryExprParsed, stringBuilder); err != nil { return "", err } return stringBuilder.String(), nil @@ -32,9 +32,9 @@ func (*SQLExpr) Validate() error { return nil } -// ConvertToSQL The idea came from ast.Walk. Currently, the development focus implement for the node type that most likely used in our needs. +// convertToSQL The idea came from ast.Walk. Currently, the development focus implement for the node type that most likely used in our needs. // TODO: implement translator for node type that still not covered right now. -func (s *SQLExpr) ConvertToSQL(node ast.Node, stringBuilder *strings.Builder) error { +func (s *SQLExpr) convertToSQL(node ast.Node, stringBuilder *strings.Builder) error { if node == nil { return fmt.Errorf("cannot convert nil to SQL query") } @@ -62,7 +62,7 @@ func (s *SQLExpr) ConvertToSQL(node ast.Node, stringBuilder *strings.Builder) er if err := s.patchUnaryNode(n); err != nil { return err } - if err := s.ConvertToSQL(n.Node, stringBuilder); err != nil { + if err := s.convertToSQL(n.Node, stringBuilder); err != nil { return err } case *ast.ArrayNode: @@ -93,14 +93,14 @@ func (s *SQLExpr) binaryNodeToSQLQuery(n *ast.BinaryNode, stringBuilder *strings fmt.Fprintf(stringBuilder, "%v", result) } else { stringBuilder.WriteString("(") - if err := s.ConvertToSQL(n.Left, stringBuilder); err != nil { + if err := s.convertToSQL(n.Left, stringBuilder); err != nil { return err } // write operator fmt.Fprintf(stringBuilder, " %s ", strings.ToUpper(operator)) - if err := s.ConvertToSQL(n.Right, stringBuilder); err != nil { + if err := s.convertToSQL(n.Right, stringBuilder); err != nil { return err } stringBuilder.WriteString(")") @@ -112,7 +112,7 @@ func (s *SQLExpr) binaryNodeToSQLQuery(n *ast.BinaryNode, stringBuilder *strings func (s *SQLExpr) arrayNodeToSQLQuery(n *ast.ArrayNode, stringBuilder *strings.Builder) error { stringBuilder.WriteString("(") for i := range n.Nodes { - if err := s.ConvertToSQL(n.Nodes[i], stringBuilder); err != nil { + if err := s.convertToSQL(n.Nodes[i], stringBuilder); err != nil { return err } if i != len(n.Nodes)-1 { @@ -127,7 +127,7 @@ func (s *SQLExpr) patchUnaryNode(n *ast.UnaryNode) error { switch n.Operator { case "not": binaryNode, ok := (n.Node).(*ast.BinaryNode) - if ok && binaryNode.Operator == "in" { + if ok && strings.ToUpper(binaryNode.Operator) == "IN" { ast.Patch(&n.Node, &ast.BinaryNode{ Operator: "not in", Left: binaryNode.Left, @@ -149,7 +149,7 @@ func (s *SQLExpr) patchUnaryNode(n *ast.UnaryNode) error { } if boolResult, ok := result.(bool); ok { ast.Patch(&n.Node, &ast.BoolNode{ - Value: !boolResult, + Value: boolResult, }) return nil } @@ -161,7 +161,7 @@ func (s *SQLExpr) patchUnaryNode(n *ast.UnaryNode) error { } func (*SQLExpr) operatorToSQL(bn *ast.BinaryNode) string { - switch bn.Operator { + switch strings.ToUpper(bn.Operator) { case "&&": return "AND" case "||": @@ -170,6 +170,7 @@ func (*SQLExpr) operatorToSQL(bn *ast.BinaryNode) string { if _, ok := bn.Right.(*ast.NilNode); ok { return "IS NOT" } + return bn.Operator case "==": if _, ok := bn.Right.(*ast.NilNode); ok { return "IS" @@ -177,6 +178,8 @@ func (*SQLExpr) operatorToSQL(bn *ast.BinaryNode) string { return "=" case "<", "<=", ">", ">=": return bn.Operator + case "IN", "NOT IN": + return bn.Operator } return "" // identify operation, like: +, -, *, etc diff --git a/pkg/query_expr/sql_expr_test.go b/pkg/query_expr/sql_expr_test.go new file mode 100644 index 00000000..fa4eb52e --- /dev/null +++ b/pkg/query_expr/sql_expr_test.go @@ -0,0 +1,98 @@ +package queryexpr_test + +import ( + "testing" + + queryexpr "github.com/goto/compass/pkg/query_expr" +) + +func TestSQLExpr_String(t *testing.T) { + tests := []struct { + expr queryexpr.SQLExpr + want string + }{ + { + expr: queryexpr.SQLExpr("test"), + want: "test", + }, + { + expr: queryexpr.SQLExpr("bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)"), + want: "bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)", + }, + } + for i, tt := range tests { + t.Run("test-case-"+string(rune(i)), func(t *testing.T) { + if got := tt.expr.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSQLExpr_ToQuery(t *testing.T) { + tests := []struct { + name string + expr queryexpr.SQLExpr + want string + wantErr bool + }{ + { + name: "less than condition with single quote", + expr: queryexpr.SQLExpr(`updated_at < '2024-04-05 23:59:59'`), + want: `(updated_at < '2024-04-05 23:59:59')`, + wantErr: false, + }, + { + name: "greater than condition with double quote", + expr: queryexpr.SQLExpr(`updated_at > "2024-04-05 23:59:59"`), + want: `(updated_at > '2024-04-05 23:59:59')`, + wantErr: false, + }, + { + name: "in condition", + expr: queryexpr.SQLExpr(`service in ["test1","test2","test3"]`), + want: `(service IN ('test1', 'test2', 'test3'))`, + wantErr: false, + }, + { + name: "equals or not in condition", + expr: queryexpr.SQLExpr(`name == "John" || service not in ["test1","test2","test3"]`), + want: `((name = 'John') OR (service NOT IN ('test1', 'test2', 'test3')))`, + wantErr: false, + }, + { + name: "complex query expression that can directly produce a value", + expr: queryexpr.SQLExpr(`(bool_identifier == !(findLast([1, 2, 3, 4], # > 2) == 4)) && name != 'John'`), + want: `((bool_identifier = false) AND (name != 'John'))`, + wantErr: false, + }, + { + name: "complex query expression that can NOT directly produce a value", + expr: queryexpr.SQLExpr(`service in filter(assets, .Service startsWith "T")`), + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.expr.ToQuery() + if (err != nil) != tt.wantErr { + t.Errorf("ToQuery() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ToQuery() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSQLExpr_Validate(t *testing.T) { + t.Run("should return nil as default validation", func(t *testing.T) { + expr := queryexpr.SQLExpr("query_sql == 'test'") + if err := (&expr).Validate(); err != nil { + t.Errorf("Validate() error = %v, wantErr %v", err, nil) + } + return + }) +}