This is an automated email from the ASF dual-hosted git repository. kezhenxu94 pushed a commit to branch ql in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit c9129efce96a7e2d759af2a4a0ad67c63072774b Author: kezhenxu94 <[email protected]> AuthorDate: Sun Sep 21 11:19:16 2025 +0800 sync --- bydbctl/internal/cmd/bydbql.go | 2 +- bydbctl/internal/cmd/bydbql_test.go | 8 +- pkg/bydbql/bydbql_test.go | 410 +++++++++++++++++++++++++++++++++++- pkg/bydbql/parser.go | 2 +- pkg/bydbql/translator.go | 231 ++++++++++++-------- 5 files changed, 555 insertions(+), 98 deletions(-) diff --git a/bydbctl/internal/cmd/bydbql.go b/bydbctl/internal/cmd/bydbql.go index 387898d7..ba1faaae 100644 --- a/bydbctl/internal/cmd/bydbql.go +++ b/bydbctl/internal/cmd/bydbql.go @@ -293,7 +293,7 @@ func executeRESTQuery(endpoint string, jsonData map[string]any) error { return []reqBody{reqBodyData}, nil }, func(req request) (*resty.Response, error) { - fmt.Printf("Executing query at endpoint %s\n", string(req.data)) + // fmt.Printf("Executing query at endpoint %s\n", string(req.data)) url := viper.GetString("addr") + endpoint resp, err := req.req. SetHeader("Content-Type", "application/json"). diff --git a/bydbctl/internal/cmd/bydbql_test.go b/bydbctl/internal/cmd/bydbql_test.go index df34fadf..130de133 100644 --- a/bydbctl/internal/cmd/bydbql_test.go +++ b/bydbctl/internal/cmd/bydbql_test.go @@ -90,7 +90,7 @@ SELECT trace_id FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s'`, nowStr, }, flags.EventuallyTimeout).Should(Equal(5)) }) - It("executes stream query with WHERE condition", func() { + It("executes stream query with WHERE condition and limit", func() { conn, err := grpclib.NewClient( grpcAddr, grpclib.WithTransportCredentials(insecure.NewCredentials()), @@ -104,7 +104,7 @@ SELECT trace_id FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s'`, nowStr, rootCmd.SetArgs([]string{"bydbql", "query", "-a", addr, "-f", "-"}) issue := func() string { rootCmd.SetIn(strings.NewReader(fmt.Sprintf(` -SELECT trace_id FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s' WHERE trace_id = 'trace-1' LIMIT 10`, nowStr, endStr))) + SELECT trace_id, webapp_service FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s' WHERE service_id = 'webapp_id' LIMIT 3`, nowStr, endStr))) return capturer.CaptureStdout(func() { err := rootCmd.Execute() if err != nil { @@ -119,7 +119,7 @@ SELECT trace_id FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s' WHERE tra helpers.UnmarshalYAML([]byte(out), resp) GinkgoWriter.Println(resp) return len(resp.Elements) - }, time.Microsecond).Should(Equal(1)) + }, flags.EventuallyTimeout).Should(Equal(3)) // We have > 3 elements match the condition, but limit 3 }) It("executes stream query with relative time range", func() { @@ -136,7 +136,7 @@ SELECT trace_id FROM STREAM sw in (default) TIME BETWEEN '%s' AND '%s' WHERE tra rootCmd.SetArgs([]string{"bydbql", "query", "-a", addr, "-f", "-"}) issue := func() string { rootCmd.SetIn(strings.NewReader(` -SELECT trace_id FROM STREAM sw TIME > '-30m' LIMIT 10`)) +SELECT trace_id FROM STREAM sw in (default) TIME > '-30m' LIMIT 10`)) return capturer.CaptureStdout(func() { err := rootCmd.Execute() if err != nil { diff --git a/pkg/bydbql/bydbql_test.go b/pkg/bydbql/bydbql_test.go index 8534dd73..22a14f49 100644 --- a/pkg/bydbql/bydbql_test.go +++ b/pkg/bydbql/bydbql_test.go @@ -249,12 +249,12 @@ var _ = Describe("Translator", func() { Entry("WHERE clause with criteria", "SELECT * FROM STREAM sw WHERE service_id = 'webapp'", func(data map[string]any) bool { - criteria, ok := data["criteria"].([]any) - if !ok || len(criteria) == 0 { + criteria, ok := data["criteria"].(map[string]any) + if !ok { return false } - first, ok := criteria[0].(map[string]any) - return ok && first["tagName"] == "service_id" && first["op"] == "BINARY_OP_EQ" + condition, ok := criteria["condition"].(map[string]any) + return ok && condition["name"] == "service_id" && condition["op"] == "BINARY_OP_EQ" }), Entry("TOP N query", "SHOW TOP 10 FROM MEASURE service_latency ORDER BY value DESC", @@ -266,8 +266,8 @@ var _ = Describe("Translator", func() { Entry("property query with IDs", "SELECT * FROM PROPERTY metadata WHERE ID = 'id1' OR ID = 'id2'", func(data map[string]any) bool { - criteria, ok := data["criteria"].([]any) - return ok && len(criteria) == 2 + criteria, ok := data["criteria"].(map[string]any) + return ok && criteria != nil }), Entry("query trace enabled", "SELECT * FROM STREAM sw WITH QUERY_TRACE", @@ -515,3 +515,401 @@ func BenchmarkEndToEnd(b *testing.B) { _, _, _ = TranslateQuery(query, context) } } + +var _ = Describe("Criteria BinaryOp Tests", func() { + var context *QueryContext + + BeforeEach(func() { + context = &QueryContext{ + DefaultGroup: "default", + DefaultResourceName: "test_resource", + DefaultResourceType: ResourceTypeStream, + CurrentTime: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + } + }) + + DescribeTable("translates all BinaryOp operators correctly", + func(whereClause string, expectedOp string, validateValue func(map[string]any) bool) { + query := fmt.Sprintf("SELECT * FROM STREAM sw WHERE %s", whereClause) + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Extract condition from criteria + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue(), "criteria should be an object") + Expect(criteria).NotTo(BeEmpty()) + + condition, ok := criteria["condition"].(map[string]any) + Expect(ok).To(BeTrue(), "should have condition field") + + Expect(condition["op"]).To(Equal(expectedOp)) + if validateValue != nil { + Expect(validateValue(condition)).To(BeTrue(), "value validation failed for condition: %+v", condition) + } + }, + // BINARY_OP_EQ + Entry("BINARY_OP_EQ with string", + "service_id = 'mysql'", + "BINARY_OP_EQ", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + str, ok := value["str"].(map[string]any) + if !ok { + return false + } + return str["value"] == "mysql" && condition["name"] == "service_id" + }), + Entry("BINARY_OP_EQ with integer", + "status = 200", + "BINARY_OP_EQ", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + intVal, ok := value["int"].(map[string]any) + if !ok { + return false + } + return intVal["value"] == "200" && condition["name"] == "status" + }), + // BINARY_OP_NE + Entry("BINARY_OP_NE with string", + "service_name != 'test-service'", + "BINARY_OP_NE", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + str, ok := value["str"].(map[string]any) + if !ok { + return false + } + return str["value"] == "test-service" && condition["name"] == "service_name" + }), + // BINARY_OP_GT + Entry("BINARY_OP_GT with integer", + "latency > 1000", + "BINARY_OP_GT", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + intVal, ok := value["int"].(map[string]any) + if !ok { + return false + } + return intVal["value"] == "1000" && condition["name"] == "latency" + }), + // BINARY_OP_LT + Entry("BINARY_OP_LT with integer", + "response_time < 500", + "BINARY_OP_LT", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + intVal, ok := value["int"].(map[string]any) + if !ok { + return false + } + return intVal["value"] == "500" && condition["name"] == "response_time" + }), + // BINARY_OP_GE + Entry("BINARY_OP_GE with integer", + "status >= 10", + "BINARY_OP_GE", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + intVal, ok := value["int"].(map[string]any) + if !ok { + return false + } + return intVal["value"] == "10" && condition["name"] == "status" + }), + // BINARY_OP_LE + Entry("BINARY_OP_LE with integer", + "duration <= 3000", + "BINARY_OP_LE", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + intVal, ok := value["int"].(map[string]any) + if !ok { + return false + } + return intVal["value"] == "3000" && condition["name"] == "duration" + }), + // BINARY_OP_MATCH + Entry("BINARY_OP_MATCH with string", + "service_id MATCH 'mysql'", + "BINARY_OP_MATCH", + func(condition map[string]any) bool { + value, ok := condition["value"].(map[string]any) + if !ok { + return false + } + str, ok := value["str"].(map[string]any) + if !ok { + return false + } + return str["value"] == "mysql" && condition["name"] == "service_id" + }), + ) +}) + +var _ = Describe("Criteria LogicalExpression Tests", func() { + var context *QueryContext + + BeforeEach(func() { + context = &QueryContext{ + DefaultGroup: "default", + DefaultResourceName: "test_resource", + DefaultResourceType: ResourceTypeStream, + CurrentTime: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + } + }) + + It("translates AND logical expression correctly", func() { + query := "SELECT * FROM STREAM sw WHERE service_id = 'webapp' AND status = 200" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Extract criteria + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + // Should have a logical expression + le, ok := criteria["le"].(map[string]any) + Expect(ok).To(BeTrue(), "should have logical expression") + Expect(le["op"]).To(Equal("LOGICAL_OP_AND")) + + // Check left condition + left, ok := le["left"].(map[string]any) + Expect(ok).To(BeTrue()) + leftCond, ok := left["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(leftCond["name"]).To(Equal("service_id")) + Expect(leftCond["op"]).To(Equal("BINARY_OP_EQ")) + + // Check right condition + right, ok := le["right"].(map[string]any) + Expect(ok).To(BeTrue()) + rightCond, ok := right["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(rightCond["name"]).To(Equal("status")) + Expect(rightCond["op"]).To(Equal("BINARY_OP_EQ")) + }) + + It("translates multiple AND conditions correctly", func() { + query := "SELECT * FROM STREAM sw WHERE service_id = 'webapp' AND status = 200 AND region = 'us-west'" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Extract criteria + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + // Should have nested logical expressions + le, ok := criteria["le"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(le["op"]).To(Equal("LOGICAL_OP_AND")) + + // The structure should be: ((service_id = 'webapp' AND status = 200) AND region = 'us-west') + // Check that left is another logical expression + left, ok := le["left"].(map[string]any) + Expect(ok).To(BeTrue()) + innerLe, ok := left["le"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(innerLe["op"]).To(Equal("LOGICAL_OP_AND")) + + // Check the rightmost condition + right, ok := le["right"].(map[string]any) + Expect(ok).To(BeTrue()) + rightCond, ok := right["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(rightCond["name"]).To(Equal("region")) + Expect(rightCond["op"]).To(Equal("BINARY_OP_EQ")) + }) + + It("translates complex nested criteria with different operators", func() { + query := "SELECT * FROM STREAM sw WHERE service_id = 'webapp' AND latency > 1000 AND status != 500" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Verify structure exists + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + // Verify it has nested logical expressions + _, hasLe := criteria["le"] + Expect(hasLe).To(BeTrue()) + }) + + It("translates MATCH operator correctly", func() { + query := "SELECT * FROM STREAM sw WHERE description MATCH 'error occurred'" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + condition, ok := criteria["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(condition["name"]).To(Equal("description")) + Expect(condition["op"]).To(Equal("BINARY_OP_MATCH")) + + value, ok := condition["value"].(map[string]any) + Expect(ok).To(BeTrue()) + str, ok := value["str"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(str["value"]).To(Equal("error occurred")) + }) + + It("translates mixed string and integer conditions", func() { + query := "SELECT * FROM STREAM sw WHERE service_id = 'webapp' AND status >= 400 AND latency < 5000" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + yamlData, err := translator.TranslateToYAML(parsed) + Expect(err).NotTo(HaveOccurred()) + Expect(yamlData).NotTo(BeEmpty()) + + // Verify YAML structure by unmarshaling + var result map[string]any + err = yaml.Unmarshal(yamlData, &result) + Expect(err).NotTo(HaveOccurred()) + Expect(result["criteria"]).NotTo(BeNil()) + }) +}) + +var _ = Describe("Criteria Proto Format Verification", func() { + var context *QueryContext + + BeforeEach(func() { + context = &QueryContext{ + DefaultGroup: "default", + DefaultResourceName: "test_resource", + DefaultResourceType: ResourceTypeStream, + CurrentTime: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + } + }) + + It("generates criteria in correct proto format for MATCH operator", func() { + query := "SELECT * FROM STREAM sw WHERE database_instance MATCH 'mysql'" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + yamlData, err := translator.TranslateToYAML(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Print the YAML output to verify the format + fmt.Printf("YAML Output for MATCH query:\n%s\n", string(yamlData)) + + // Verify the structure matches the expected proto format + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + condition, ok := criteria["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(condition["name"]).To(Equal("database_instance")) + Expect(condition["op"]).To(Equal("BINARY_OP_MATCH")) + + // Verify value structure + value, ok := condition["value"].(map[string]any) + Expect(ok).To(BeTrue()) + str, ok := value["str"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(str["value"]).To(Equal("mysql")) + }) + + It("generates criteria with LogicalExpression for AND conditions", func() { + query := "SELECT * FROM STREAM sw WHERE service_id = 'webapp' AND status = 200" + parsed, errors := ParseQuery(query) + Expect(errors).To(BeEmpty()) + Expect(parsed).NotTo(BeNil()) + + translator := NewTranslator(context) + yamlData, err := translator.TranslateToYAML(parsed) + Expect(err).NotTo(HaveOccurred()) + + // Print the YAML output to verify the format + fmt.Printf("YAML Output for AND conditions:\n%s\n", string(yamlData)) + + // Verify the structure has proper LogicalExpression + data, err := translator.TranslateToMap(parsed) + Expect(err).NotTo(HaveOccurred()) + + criteria, ok := data["criteria"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(criteria).NotTo(BeEmpty()) + + // Should have a logical expression + le, ok := criteria["le"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(le["op"]).To(Equal("LOGICAL_OP_AND")) + + // Verify left and right conditions + left, ok := le["left"].(map[string]any) + Expect(ok).To(BeTrue()) + leftCond, ok := left["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(leftCond["name"]).To(Equal("service_id")) + Expect(leftCond["op"]).To(Equal("BINARY_OP_EQ")) + + right, ok := le["right"].(map[string]any) + Expect(ok).To(BeTrue()) + rightCond, ok := right["condition"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(rightCond["name"]).To(Equal("status")) + Expect(rightCond["op"]).To(Equal("BINARY_OP_EQ")) + }) +}) diff --git a/pkg/bydbql/parser.go b/pkg/bydbql/parser.go index 07b3610a..0009873a 100644 --- a/pkg/bydbql/parser.go +++ b/pkg/bydbql/parser.go @@ -665,7 +665,7 @@ func (p *Parser) parseCondition() *Condition { p.nextToken() // Parse right side - if condition.Operator == OpIn || condition.Operator == OpNotIn { + if condition.Operator == OpIn || condition.Operator == OpNotIn || condition.Operator == OpHaving || condition.Operator == OpNotHaving { // Parse value list if !p.expectToken(TokenLeftParen) { return nil diff --git a/pkg/bydbql/translator.go b/pkg/bydbql/translator.go index df25f9eb..5f94ddd1 100644 --- a/pkg/bydbql/translator.go +++ b/pkg/bydbql/translator.go @@ -33,7 +33,7 @@ type QueryYAML struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` Groups []string `yaml:"groups,omitempty" json:"groups,omitempty"` TimeRange *TimeRangeYAML `yaml:"timeRange,omitempty" json:"timeRange,omitempty"` - Criteria []map[string]interface{} `yaml:"criteria,omitempty" json:"criteria,omitempty"` + Criteria map[string]interface{} `yaml:"criteria,omitempty" json:"criteria,omitempty"` Limit *int `yaml:"limit,omitempty" json:"limit,omitempty"` Offset *int `yaml:"offset,omitempty" json:"offset,omitempty"` Trace bool `yaml:"trace,omitempty" json:"trace,omitempty"` @@ -55,7 +55,7 @@ type QueryYAML struct { // Top-N specific (for separate endpoint) TopN int `yaml:"top_n,omitempty" json:"top_n,omitempty"` FieldValueSort string `yaml:"field_value_sort,omitempty" json:"field_value_sort,omitempty"` - Conditions []map[string]interface{} `yaml:"conditions,omitempty" json:"conditions,omitempty"` + Conditions map[string]interface{} `yaml:"conditions,omitempty" json:"conditions,omitempty"` } // TimeRangeYAML represents time range in YAML @@ -471,104 +471,163 @@ func (t *Translator) parseTimestamp(timestamp string) (string, error) { } // translateWhereClause translates WHERE conditions to criteria format -func (t *Translator) translateWhereClause(where *WhereClause) ([]map[string]interface{}, error) { - var criteria []map[string]interface{} +func (t *Translator) translateWhereClause(where *WhereClause) (map[string]interface{}, error) { + if len(where.Conditions) == 0 { + return nil, nil + } - for _, condition := range where.Conditions { - criterion := make(map[string]interface{}) + if len(where.Conditions) == 1 { + // Single condition - wrap in Criteria with condition field + condition, err := t.translateCondition(where.Conditions[0]) + if err != nil { + return nil, err + } + return map[string]interface{}{ + "condition": condition, + }, nil + } - // Set tag name - criterion["tagName"] = condition.Left + // Multiple conditions - create LogicalExpression with AND operator + var criteria map[string]interface{} + leftCondition, err := t.translateCondition(where.Conditions[0]) + if err != nil { + return nil, err + } - // Set operator and values - switch condition.Operator { - case OpEqual: - criterion["op"] = "BINARY_OP_EQ" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpNotEqual: - criterion["op"] = "BINARY_OP_NE" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpGreater: - criterion["op"] = "BINARY_OP_GT" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpLess: - criterion["op"] = "BINARY_OP_LT" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpGreaterEqual: - criterion["op"] = "BINARY_OP_GE" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpLessEqual: - criterion["op"] = "BINARY_OP_LE" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } - case OpIn: - criterion["op"] = "BINARY_OP_IN" - var values []interface{} - for _, val := range condition.Values { - values = append(values, t.translateValue(val)) - } - criterion["value"] = map[string]interface{}{ - "strArray": map[string]interface{}{ - "value": values, - }, - } - case OpNotIn: - criterion["op"] = "BINARY_OP_NOT_IN" - var values []interface{} - for _, val := range condition.Values { - values = append(values, t.translateValue(val)) - } - criterion["value"] = map[string]interface{}{ - "strArray": map[string]interface{}{ - "value": values, + if len(where.Conditions) == 2 { + rightCondition, err := t.translateCondition(where.Conditions[1]) + if err != nil { + return nil, err + } + criteria = map[string]interface{}{ + "le": map[string]interface{}{ + "op": "LOGICAL_OP_AND", + "left": map[string]interface{}{ + "condition": leftCondition, }, - } - case OpHaving: - criterion["op"] = "BINARY_OP_HAVING" - var values []interface{} - for _, val := range condition.Values { - values = append(values, t.translateValue(val)) - } - criterion["value"] = map[string]interface{}{ - "strArray": map[string]interface{}{ - "value": values, + "right": map[string]interface{}{ + "condition": rightCondition, }, + }, + } + } else { + // More than 2 conditions - chain them with AND operators + criteria = map[string]interface{}{ + "condition": leftCondition, + } + for i := 1; i < len(where.Conditions); i++ { + condition, err := t.translateCondition(where.Conditions[i]) + if err != nil { + return nil, err } - case OpNotHaving: - criterion["op"] = "BINARY_OP_NOT_HAVING" - var values []interface{} - for _, val := range condition.Values { - values = append(values, t.translateValue(val)) - } - criterion["value"] = map[string]interface{}{ - "strArray": map[string]interface{}{ - "value": values, + criteria = map[string]interface{}{ + "le": map[string]interface{}{ + "op": "LOGICAL_OP_AND", + "left": criteria, + "right": map[string]interface{}{ + "condition": condition, + }, }, } - case OpMatch: - criterion["op"] = "BINARY_OP_MATCH" - if condition.Right != nil { - criterion["value"] = t.translateValue(condition.Right) - } } - - criteria = append(criteria, criterion) } return criteria, nil } +// translateCondition translates a single condition to the proper format +func (t *Translator) translateCondition(condition *Condition) (map[string]interface{}, error) { + criterion := make(map[string]interface{}) + + // Set field name + criterion["name"] = condition.Left + + // Set operator and values + switch condition.Operator { + case OpEqual: + criterion["op"] = "BINARY_OP_EQ" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpNotEqual: + criterion["op"] = "BINARY_OP_NE" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpGreater: + criterion["op"] = "BINARY_OP_GT" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpLess: + criterion["op"] = "BINARY_OP_LT" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpGreaterEqual: + criterion["op"] = "BINARY_OP_GE" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpLessEqual: + criterion["op"] = "BINARY_OP_LE" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + case OpIn: + criterion["op"] = "BINARY_OP_IN" + var values []interface{} + for _, val := range condition.Values { + values = append(values, t.translateValue(val)) + } + criterion["value"] = map[string]interface{}{ + "strArray": map[string]interface{}{ + "value": values, + }, + } + case OpNotIn: + criterion["op"] = "BINARY_OP_NOT_IN" + var values []interface{} + for _, val := range condition.Values { + values = append(values, t.translateValue(val)) + } + criterion["value"] = map[string]interface{}{ + "strArray": map[string]interface{}{ + "value": values, + }, + } + case OpHaving: + criterion["op"] = "BINARY_OP_HAVING" + var values []interface{} + for _, val := range condition.Values { + values = append(values, t.translateValue(val)) + } + criterion["value"] = map[string]interface{}{ + "strArray": map[string]interface{}{ + "value": values, + }, + } + case OpNotHaving: + criterion["op"] = "BINARY_OP_NOT_HAVING" + var values []interface{} + for _, val := range condition.Values { + values = append(values, t.translateValue(val)) + } + criterion["value"] = map[string]interface{}{ + "strArray": map[string]interface{}{ + "value": values, + }, + } + case OpMatch: + criterion["op"] = "BINARY_OP_MATCH" + if condition.Right != nil { + criterion["value"] = t.translateValue(condition.Right) + } + } + + return criterion, nil +} + // translateValue translates a value to the appropriate YAML format func (t *Translator) translateValue(value *Value) interface{} { if value.IsNull {
