You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2025/10/01 12:53:20 UTC
(skywalking-banyandb) 01/04: ql
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 2a3954e5c2a11b2215edaf17c5e576e803719527
Author: kezhenxu94 <ke...@apache.org>
AuthorDate: Wed Sep 17 17:18:18 2025 +0800
ql
---
bydbctl/internal/cmd/bydbql.go | 378 +++++++++++++++++
bydbctl/internal/cmd/bydbql_test.go | 345 ++++++++++++++++
bydbctl/internal/cmd/root.go | 2 +-
pkg/bydbql/ast.go | 327 +++++++++++++++
pkg/bydbql/bydbql_suite_test.go | 30 ++
pkg/bydbql/bydbql_test.go | 465 +++++++++++++++++++++
pkg/bydbql/lexer.go | 608 +++++++++++++++++++++++++++
pkg/bydbql/parser.go | 796 ++++++++++++++++++++++++++++++++++++
pkg/bydbql/translator.go | 591 ++++++++++++++++++++++++++
9 files changed, 3541 insertions(+), 1 deletion(-)
diff --git a/bydbctl/internal/cmd/bydbql.go b/bydbctl/internal/cmd/bydbql.go
new file mode 100644
index 00000000..67545eae
--- /dev/null
+++ b/bydbctl/internal/cmd/bydbql.go
@@ -0,0 +1,378 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package cmd
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "sigs.k8s.io/yaml"
+
+ "github.com/apache/skywalking-banyandb/pkg/bydbql"
+ "github.com/apache/skywalking-banyandb/pkg/version"
+)
+
+// BydbQL endpoint paths
+const (
+ streamQueryPath = "/api/v1/stream/query"
+ measureQueryPath = "/api/v1/measure/query"
+ traceQueryPath = "/api/v1/trace/query"
+ propertyQueryPath = "/api/v1/property/query"
+ topnQueryPath = "/api/v1/measure/topn"
+)
+
+// newBydbQLCmd creates the BydbQL command
+func newBydbQLCmd() *cobra.Command {
+ bydbqlCmd := &cobra.Command{
+ Use: "bydbql",
+ Version: version.Build(),
+ Short: "BanyanDB Query Language (BydbQL) interface",
+ Long: `BanyanDB Query Language (BydbQL) provides a SQL-like interface for querying BanyanDB.
+It supports querying streams, measures, traces, properties, and top-N operations with familiar SQL syntax.
+
+Examples:
+ # Interactive mode
+ bydbctl bydbql
+
+ # Execute query from file
+ bydbctl bydbql query -f query.sql
+
+ # Execute query from stdin
+ echo "SELECT * FROM STREAM sw WHERE service_id = 'webapp'" | bydbctl bydbql query -f -`,
+ }
+
+ // Interactive command
+ interactiveCmd := &cobra.Command{
+ Use: "interactive",
+ Aliases: []string{"i"},
+ Version: version.Build(),
+ Short: "Start interactive BydbQL shell",
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ return runInteractiveMode(cmd)
+ },
+ }
+
+ // Query command
+ queryCmd := &cobra.Command{
+ Use: "query [-f file]",
+ Version: version.Build(),
+ Short: "Execute BydbQL query from file or stdin",
+ Long: `Execute BydbQL query from a file or stdin. The query will be parsed and translated to YAML format,
+then sent to the appropriate BanyanDB endpoint.
+
+Supported query types:
+ - SELECT queries for streams, measures, traces, and properties
+ - SHOW TOP N queries for top-N operations
+
+Examples:
+ bydbctl bydbql query -f query.sql
+ echo "SELECT * FROM STREAM sw" | bydbctl bydbql query -f -`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ return runQueryMode(cmd)
+ },
+ }
+
+ bindFileFlag(queryCmd)
+ bindTLSRelatedFlag(interactiveCmd, queryCmd)
+
+ bydbqlCmd.AddCommand(interactiveCmd, queryCmd)
+
+ // Default to interactive mode if no subcommand specified
+ bydbqlCmd.RunE = func(cmd *cobra.Command, _ []string) error {
+ return runInteractiveMode(cmd)
+ }
+
+ return bydbqlCmd
+}
+
+// runInteractiveMode starts the interactive BydbQL shell
+func runInteractiveMode(cmd *cobra.Command) error {
+ fmt.Println("BanyanDB Query Language (BydbQL) Interactive Shell")
+ fmt.Println("Type 'help' for help, 'exit' or 'quit' to quit.")
+ fmt.Println()
+
+ scanner := bufio.NewScanner(os.Stdin)
+
+ for {
+ fmt.Print("bydbql> ")
+
+ if !scanner.Scan() {
+ break
+ }
+
+ line := strings.TrimSpace(scanner.Text())
+
+ if line == "" {
+ continue
+ }
+
+ // Handle special commands
+ switch strings.ToLower(line) {
+ case "help":
+ printHelp()
+ continue
+ case "exit", "quit":
+ fmt.Println("Goodbye!")
+ return nil
+ case "\\q":
+ fmt.Println("Goodbye!")
+ return nil
+ }
+
+ // Check for multi-line queries (ending with semicolon)
+ query := line
+ for !strings.HasSuffix(strings.TrimSpace(query), ";") {
+ fmt.Print(" -> ")
+ if !scanner.Scan() {
+ break
+ }
+ query += " " + scanner.Text()
+ }
+
+ // Remove trailing semicolon
+ query = strings.TrimSuffix(strings.TrimSpace(query), ";")
+
+ // Execute the query
+ if err := executeBydbQLQuery(query); err != nil {
+ fmt.Printf("Error: %v\n", err)
+ }
+ fmt.Println()
+ }
+
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("error reading input: %w", err)
+ }
+
+ return nil
+}
+
+// runQueryMode executes BydbQL query from file or stdin
+func runQueryMode(cmd *cobra.Command) error {
+ var reader io.Reader
+
+ if filePath == "-" || filePath == "" {
+ reader = cmd.InOrStdin()
+ } else {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return fmt.Errorf("error opening file %s: %w", filePath, err)
+ }
+ defer file.Close()
+ reader = file
+ }
+
+ // Read the entire query
+ content, err := io.ReadAll(reader)
+ if err != nil {
+ return fmt.Errorf("error reading query: %w", err)
+ }
+
+ query := strings.TrimSpace(string(content))
+ query = strings.TrimSuffix(query, ";")
+
+ if query == "" {
+ return fmt.Errorf("empty query")
+ }
+
+ return executeBydbQLQuery(query)
+}
+
+// executeBydbQLQuery parses and executes a BydbQL query
+func executeBydbQLQuery(query string) error {
+ // Create query context
+ context := &bydbql.QueryContext{
+ DefaultGroup: viper.GetString("group"),
+ DefaultResourceType: bydbql.ResourceTypeAuto,
+ }
+
+ // Parse the query
+ parsed, parseErrors := bydbql.ParseQuery(query)
+ if len(parseErrors) > 0 {
+ return fmt.Errorf("parsing errors:\n %s", strings.Join(parseErrors, "\n "))
+ }
+
+ if parsed == nil {
+ return fmt.Errorf("failed to parse query")
+ }
+
+ // Merge context
+ if context.DefaultGroup != "" && len(parsed.Groups) == 0 {
+ parsed.Groups = []string{context.DefaultGroup}
+ }
+
+ // Translate to YAML
+ translator := bydbql.NewTranslator(context)
+ yamlData, err := translator.TranslateToMap(parsed)
+ if err != nil {
+ return fmt.Errorf("translation error: %w", err)
+ }
+ fmt.Printf("Translated YAML:\n%s\n", yamlData)
+
+ // Convert to JSON for REST API
+ jsonData, err := json.Marshal(yamlData)
+ if err != nil {
+ return fmt.Errorf("JSON conversion error: %w", err)
+ }
+
+ // Determine endpoint based on query type
+ endpoint, err := determineEndpoint(parsed)
+ if err != nil {
+ return err
+ }
+
+ // Execute the query via REST API
+ return executeRESTQuery(endpoint, jsonData)
+}
+
+// determineEndpoint determines the REST endpoint based on query type
+func determineEndpoint(parsed *bydbql.ParsedQuery) (string, error) {
+ switch parsed.Statement.(type) {
+ case *bydbql.SelectStatement:
+ // Determine resource type
+ resourceType := parsed.ResourceType
+ if resourceType == bydbql.ResourceTypeAuto {
+ // Default to stream if not specified
+ resourceType = bydbql.ResourceTypeStream
+ }
+
+ switch resourceType {
+ case bydbql.ResourceTypeStream:
+ return streamQueryPath, nil
+ case bydbql.ResourceTypeMeasure:
+ return measureQueryPath, nil
+ case bydbql.ResourceTypeTrace:
+ return traceQueryPath, nil
+ case bydbql.ResourceTypeProperty:
+ return propertyQueryPath, nil
+ default:
+ return "", fmt.Errorf("unsupported resource type: %s", resourceType.String())
+ }
+
+ case *bydbql.TopNStatement:
+ return topnQueryPath, nil
+
+ default:
+ return "", fmt.Errorf("unsupported statement type")
+ }
+}
+
+// executeRESTQuery executes the query via REST API
+func executeRESTQuery(endpoint string, jsonData []byte) error {
+ // Create REST request using existing infrastructure
+ reqBodyData := reqBody{
+ data: jsonData,
+ }
+
+ // Use existing rest function with appropriate parameters
+ return rest(
+ func() ([]reqBody, error) {
+ return []reqBody{reqBodyData}, nil
+ },
+ func(req request) (*resty.Response, error) {
+ url := viper.GetString("addr") + endpoint
+ resp, err := req.req.
+ SetHeader("Content-Type", "application/json").
+ SetBody(req.data).
+ Post(url)
+ return resp, err
+ },
+ yamlPrinter,
+ enableTLS,
+ insecure,
+ cert,
+ )
+}
+
+// printHelp prints help information for the interactive mode
+func printHelp() {
+ fmt.Println("BanyanDB Query Language (BydbQL) Help")
+ fmt.Println("=====================================")
+ fmt.Println()
+ fmt.Println("BydbQL supports SQL-like syntax for querying BanyanDB resources:")
+ fmt.Println()
+ fmt.Println("STREAM QUERIES:")
+ fmt.Println(" SELECT * FROM STREAM sw WHERE service_id = 'webapp';")
+ fmt.Println(" SELECT trace_id, service_id FROM STREAM sw TIME > '-30m';")
+ fmt.Println(" SELECT * FROM STREAM sw TIME BETWEEN '2023-01-01T00:00:00Z' AND '2023-01-02T00:00:00Z';")
+ fmt.Println()
+ fmt.Println("MEASURE QUERIES:")
+ fmt.Println(" SELECT region, SUM(latency) FROM MEASURE service_cpm GROUP BY region;")
+ fmt.Println(" SELECT TOP 10 instance, cpu_usage FROM MEASURE instance_metrics ORDER BY cpu_usage DESC;")
+ fmt.Println(" SELECT AVG(response_time) FROM MEASURE http_metrics WHERE region = 'us-west';")
+ fmt.Println()
+ fmt.Println("TRACE QUERIES:")
+ fmt.Println(" SELECT trace_id, service_id FROM TRACE sw_trace WHERE status = 'error';")
+ fmt.Println(" SELECT () FROM TRACE sw_trace WHERE service_id = 'webapp' LIMIT 100;")
+ fmt.Println()
+ fmt.Println("PROPERTY QUERIES:")
+ fmt.Println(" SELECT ip, owner FROM PROPERTY server_metadata WHERE datacenter = 'dc-101';")
+ fmt.Println(" SELECT * FROM PROPERTY server_metadata WHERE ID = 'server-123';")
+ fmt.Println()
+ fmt.Println("TOP-N QUERIES:")
+ fmt.Println(" SHOW TOP 10 FROM MEASURE service_latency ORDER BY value DESC;")
+ fmt.Println(" SHOW TOP 5 FROM MEASURE service_errors WHERE status_code = '500';")
+ fmt.Println()
+ fmt.Println("TIME FORMATS:")
+ fmt.Println(" Absolute: '2023-01-01T00:00:00Z' (RFC3339)")
+ fmt.Println(" Relative: '-30m', '2h', '-1d', '-1w', 'now'")
+ fmt.Println()
+ fmt.Println("OPERATORS:")
+ fmt.Println(" =, !=, >, <, >=, <=, IN, NOT IN, HAVING, NOT HAVING, MATCH")
+ fmt.Println(" AND, OR for combining conditions")
+ fmt.Println()
+ fmt.Println("SPECIAL COMMANDS:")
+ fmt.Println(" help - Show this help")
+ fmt.Println(" exit - Exit the shell")
+ fmt.Println(" quit - Exit the shell")
+ fmt.Println(" \\q - Exit the shell")
+ fmt.Println()
+ fmt.Println("QUERY TRACING:")
+ fmt.Println(" Add 'WITH QUERY_TRACE' to any query for distributed tracing")
+ fmt.Println()
+ fmt.Println("NOTE: Queries can span multiple lines and should end with semicolon (;)")
+}
+
+// printQueryResult prints the query result in YAML format
+func printQueryResult(data []byte) error {
+ // The data is already in JSON format from the REST response
+ // Convert it to YAML for better readability
+ var jsonData interface{}
+ if err := json.Unmarshal(data, &jsonData); err != nil {
+ // If JSON parsing fails, print raw data
+ fmt.Println(string(data))
+ return nil
+ }
+
+ yamlData, err := yaml.Marshal(jsonData)
+ if err != nil {
+ // If YAML conversion fails, print JSON data
+ fmt.Println(string(data))
+ return nil
+ }
+
+ fmt.Print(string(yamlData))
+ return nil
+}
+
diff --git a/bydbctl/internal/cmd/bydbql_test.go b/bydbctl/internal/cmd/bydbql_test.go
new file mode 100644
index 00000000..1b13e792
--- /dev/null
+++ b/bydbctl/internal/cmd/bydbql_test.go
@@ -0,0 +1,345 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package cmd
+
+import (
+ "bytes"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+ "github.com/zenizh/go-capturer"
+ "sigs.k8s.io/yaml"
+
+ "github.com/apache/skywalking-banyandb/pkg/bydbql"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+ "github.com/apache/skywalking-banyandb/pkg/test/setup"
+)
+
+const httpSchema = "http://"
+
+var _ = Describe("BydbQL Command", func() {
+ var addr string
+ var deferFunc func()
+ var rootCmd *cobra.Command
+
+ BeforeEach(func() {
+ _, addr, deferFunc = setup.EmptyStandalone()
+ addr = httpSchema + addr
+
+ // Reset viper for clean test state
+ viper.Reset()
+ viper.Set("addr", addr)
+ viper.Set("group", "default")
+
+ // Create root command with all subcommands
+ rootCmd = &cobra.Command{Use: "root"}
+ RootCmdFlags(rootCmd)
+ })
+
+ AfterEach(func() {
+ deferFunc()
+ })
+
+ It("creates command with correct structure", func() {
+ // Find bydbql command in root commands
+ var bydbqlCmd *cobra.Command
+ for _, cmd := range rootCmd.Commands() {
+ if cmd.Use == "bydbql" {
+ bydbqlCmd = cmd
+ break
+ }
+ }
+
+ Expect(bydbqlCmd).NotTo(BeNil())
+ Expect(bydbqlCmd.Use).To(Equal("bydbql"))
+
+ // Test subcommands
+ subCommands := bydbqlCmd.Commands()
+ Expect(subCommands).To(HaveLen(2))
+
+ // Check that the expected subcommands exist (order may vary)
+ subCommandNames := make([]string, len(subCommands))
+ for i, cmd := range subCommands {
+ subCommandNames[i] = cmd.Use
+ }
+ Expect(subCommandNames).To(ContainElements("interactive", "query"))
+ })
+})
+
+var _ = Describe("Endpoint Determination", func() {
+ DescribeTable("determines correct endpoint for different query types",
+ func(query, expectedEndpoint string) {
+ parsed, errors := bydbql.ParseQuery(query)
+ Expect(errors).To(BeEmpty())
+
+ endpoint, err := determineEndpoint(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(endpoint).To(Equal(expectedEndpoint))
+ },
+ Entry("stream query", "SELECT * FROM STREAM sw", streamQueryPath),
+ Entry("measure query", "SELECT * FROM MEASURE metrics", measureQueryPath),
+ Entry("trace query", "SELECT * FROM TRACE traces", traceQueryPath),
+ Entry("property query", "SELECT * FROM PROPERTY props", propertyQueryPath),
+ Entry("top-n query", "SHOW TOP 10 FROM MEASURE metrics", topnQueryPath),
+ )
+})
+
+var _ = Describe("Query Execution Parse Errors", func() {
+ DescribeTable("handles parsing errors correctly",
+ func(query string) {
+ err := executeBydbQLQuery(query)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("parsing errors"))
+ },
+ Entry("incomplete SELECT", "SELECT"),
+ Entry("missing resource", "SELECT * FROM"),
+ Entry("missing N in TOP", "SHOW TOP FROM MEASURE metrics"),
+ )
+})
+
+var _ = Describe("Valid Query Parsing", func() {
+ DescribeTable("parses valid queries correctly",
+ func(query string) {
+ // Parse and validate query structure
+ parsed, errors := bydbql.ParseQuery(query)
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ // Validate endpoint determination
+ _, err := determineEndpoint(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ },
+ Entry("stream query", "SELECT * FROM STREAM sw"),
+ Entry("measure query with aggregation", "SELECT region, SUM(latency) FROM MEASURE metrics GROUP BY region"),
+ Entry("top-n query", "SHOW TOP 10 FROM MEASURE service_latency ORDER BY value DESC"),
+ Entry("trace query with condition", "SELECT * FROM TRACE traces WHERE status = 'error'"),
+ Entry("property query with condition", "SELECT ip, owner FROM PROPERTY metadata WHERE datacenter = 'dc-1'"),
+ )
+})
+
+var _ = Describe("Help Command", func() {
+ var addr string
+ var deferFunc func()
+ var rootCmd *cobra.Command
+
+ BeforeEach(func() {
+ _, addr, deferFunc = setup.EmptyStandalone()
+ addr = httpSchema + addr
+
+ // Reset viper for clean test state
+ viper.Reset()
+ viper.Set("addr", addr)
+ viper.Set("group", "default")
+
+ // Create root command with all subcommands
+ rootCmd = &cobra.Command{Use: "root"}
+ RootCmdFlags(rootCmd)
+ })
+
+ AfterEach(func() {
+ deferFunc()
+ })
+
+ It("displays help information correctly", func() {
+ // Capture help output
+ var output bytes.Buffer
+ rootCmd.SetOut(&output)
+
+ // Execute help for bydbql command
+ rootCmd.SetArgs([]string{"bydbql", "--help"})
+ err := rootCmd.Execute()
+ Expect(err).NotTo(HaveOccurred())
+
+ helpOutput := output.String()
+
+ // Verify help content includes key information
+ expectedContent := []string{
+ "BanyanDB Query Language",
+ "BydbQL",
+ "interactive",
+ "query",
+ "SQL-like",
+ }
+
+ for _, expected := range expectedContent {
+ Expect(helpOutput).To(ContainSubstring(expected))
+ }
+ })
+})
+
+var _ = Describe("BydbQL Integration", func() {
+ DescribeTable("full pipeline processing",
+ func(query string, resourceType bydbql.ResourceType) {
+ // Step 1: Parse query
+ parsed, errors := bydbql.ParseQuery(query)
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ // Step 2: Validate resource type detection
+ if parsed.ResourceType != resourceType {
+ // For auto-detected resource types, this might be expected
+ GinkgoWriter.Printf("Resource type: expected %s, got %s (may be auto-detected)",
+ resourceType.String(), parsed.ResourceType.String())
+ }
+
+ // Step 3: Translate to YAML
+ context := &bydbql.QueryContext{
+ DefaultGroup: "default",
+ }
+ translator := bydbql.NewTranslator(context)
+ yamlData, err := translator.TranslateToYAML(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(yamlData).NotTo(BeEmpty())
+
+ // Step 4: Determine correct endpoint
+ endpoint, err := determineEndpoint(parsed)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Step 5: Validate endpoint matches expected resource type
+ expectedEndpoints := map[bydbql.ResourceType]string{
+ bydbql.ResourceTypeStream: streamQueryPath,
+ bydbql.ResourceTypeMeasure: measureQueryPath,
+ bydbql.ResourceTypeTrace: traceQueryPath,
+ bydbql.ResourceTypeProperty: propertyQueryPath,
+ }
+
+ // For Top-N queries, always use measure endpoint
+ if _, isTopN := parsed.Statement.(*bydbql.TopNStatement); isTopN {
+ Expect(endpoint).To(Equal(topnQueryPath))
+ } else if expectedEndpoint, ok := expectedEndpoints[resourceType]; ok {
+ Expect(endpoint).To(Equal(expectedEndpoint))
+ }
+
+ // Step 6: Validate YAML structure
+ var yamlMap map[string]interface{}
+ err = yaml.Unmarshal(yamlData, &yamlMap)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Basic validation: should have name or resource identifier
+ Expect(yamlMap["Name"] != nil || yamlMap["topN"] != nil).To(BeTrue())
+
+ GinkgoWriter.Printf("Successfully processed query: %s", query)
+ GinkgoWriter.Printf("Generated YAML length: %d bytes", len(yamlData))
+ GinkgoWriter.Printf("Target endpoint: %s", endpoint)
+ },
+ Entry("stream query full pipeline",
+ "SELECT trace_id, service_id FROM STREAM sw WHERE service_id = 'webapp' TIME > '-30m' LIMIT 100",
+ bydbql.ResourceTypeStream),
+ Entry("measure query full pipeline",
+ "SELECT region, AVG(latency) FROM MEASURE service_metrics WHERE service = 'auth' GROUP BY region",
+ bydbql.ResourceTypeMeasure),
+ Entry("topn query full pipeline",
+ "SHOW TOP 5 FROM MEASURE error_count WHERE status_code = '500'",
+ bydbql.ResourceTypeMeasure),
+ Entry("trace query full pipeline",
+ "SELECT * FROM TRACE app_traces WHERE operation_name = 'GET /api/users' TIME BETWEEN '-1h' AND 'now'",
+ bydbql.ResourceTypeTrace),
+ Entry("property query full pipeline",
+ "SELECT ip, region FROM PROPERTY server_info WHERE ID = 'server-1'",
+ bydbql.ResourceTypeProperty),
+ )
+})
+
+var _ = Describe("Command Line Flags", func() {
+ var addr string
+ var deferFunc func()
+ var rootCmd *cobra.Command
+
+ BeforeEach(func() {
+ _, addr, deferFunc = setup.EmptyStandalone()
+ addr = httpSchema + addr
+
+ // Reset viper for clean test state
+ viper.Reset()
+ viper.Set("addr", addr)
+ viper.Set("group", "default")
+
+ // Create root command with all subcommands
+ rootCmd = &cobra.Command{Use: "root"}
+ RootCmdFlags(rootCmd)
+ })
+
+ AfterEach(func() {
+ deferFunc()
+ })
+
+ DescribeTable("handles command line flags correctly",
+ func(args []string, expectError bool) {
+ // Prepend "bydbql" to the args since we're testing the bydbql subcommand
+ fullArgs := append([]string{"bydbql"}, args...)
+ rootCmd.SetArgs(fullArgs)
+
+ // We're testing command structure, not execution
+ // So we expect parsing to succeed even if execution might fail
+ err := rootCmd.ParseFlags(fullArgs)
+
+ if expectError {
+ Expect(err).To(HaveOccurred())
+ } else {
+ Expect(err).NotTo(HaveOccurred())
+ }
+ },
+ Entry("valid query command with file", []string{"query", "-f", "test.sql"}, false),
+ Entry("valid interactive command", []string{"interactive"}, false),
+ Entry("alias i for interactive", []string{"i"}, false),
+ )
+})
+
+var _ = Describe("BydbQL Query Execution", func() {
+ var addr string
+ var deferFunc func()
+ var rootCmd *cobra.Command
+
+ BeforeEach(func() {
+ _, addr, deferFunc = setup.EmptyStandalone()
+ addr = httpSchema + addr
+
+ // Reset viper for clean test state
+ viper.Reset()
+ viper.Set("addr", addr)
+ viper.Set("group", "default")
+
+ // Create root command with all subcommands
+ rootCmd = &cobra.Command{Use: "root"}
+ RootCmdFlags(rootCmd)
+ })
+
+ AfterEach(func() {
+ deferFunc()
+ })
+
+ It("executes simple query successfully", func() {
+ // Test a simple query execution
+ rootCmd.SetArgs([]string{"bydbql", "query", "-f", "-"})
+ rootCmd.SetIn(strings.NewReader("SELECT * FROM STREAM sw LIMIT 1"))
+
+ executeQuery := func() string {
+ return capturer.CaptureStdout(func() {
+ err := rootCmd.Execute()
+ if err != nil {
+ GinkgoWriter.Printf("execution fails: %v", err)
+ }
+ })
+ }
+
+ Eventually(executeQuery, flags.EventuallyTimeout).ShouldNot(ContainSubstring("error"))
+ })
+})
diff --git a/bydbctl/internal/cmd/root.go b/bydbctl/internal/cmd/root.go
index c83f2752..095a995d 100644
--- a/bydbctl/internal/cmd/root.go
+++ b/bydbctl/internal/cmd/root.go
@@ -80,7 +80,7 @@ func RootCmdFlags(command *cobra.Command) {
_ = viper.BindPFlag("password", command.PersistentFlags().Lookup("password"))
command.AddCommand(newGroupCmd(), newUseCmd(), newStreamCmd(), newMeasureCmd(), newTopnCmd(),
- newIndexRuleCmd(), newIndexRuleBindingCmd(), newPropertyCmd(), newTraceCmd(), newHealthCheckCmd(), newAnalyzeCmd())
+ newIndexRuleCmd(), newIndexRuleBindingCmd(), newPropertyCmd(), newTraceCmd(), newHealthCheckCmd(), newAnalyzeCmd(), newBydbQLCmd())
}
func init() {
diff --git a/pkg/bydbql/ast.go b/pkg/bydbql/ast.go
new file mode 100644
index 00000000..0348d1e1
--- /dev/null
+++ b/pkg/bydbql/ast.go
@@ -0,0 +1,327 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package bydbql provides BanyanDB Query Language (BydbQL) parsing and translation capabilities.
+package bydbql
+
+import "time"
+
+// Node represents a node in the Abstract Syntax Tree
+type Node interface {
+ String() string
+}
+
+// Statement represents a BydbQL statement
+type Statement interface {
+ Node
+ statementNode()
+}
+
+// Expression represents a BydbQL expression
+type Expression interface {
+ Node
+ expressionNode()
+}
+
+// SelectStatement represents a SELECT statement
+type SelectStatement struct {
+ Projection *Projection
+ From *FromClause
+ Time *TimeCondition
+ Where *WhereClause
+ GroupBy *GroupByClause
+ OrderBy *OrderByClause
+ Limit *int
+ Offset *int
+ QueryTrace bool
+}
+
+func (s *SelectStatement) statementNode() {}
+func (s *SelectStatement) String() string { return "SELECT" }
+
+// TopNStatement represents a SHOW TOP N statement
+type TopNStatement struct {
+ TopN int
+ From *FromClause
+ Time *TimeCondition
+ Where *WhereClause
+ AggregateBy *AggregateFunction
+ OrderBy *OrderByClause
+ QueryTrace bool
+}
+
+func (s *TopNStatement) statementNode() {}
+func (s *TopNStatement) String() string { return "SHOW TOP" }
+
+// Projection represents the SELECT projection clause
+type Projection struct {
+ All bool
+ Empty bool // SELECT () for traces
+ Columns []*Column
+ TopN *TopNProjection
+}
+
+func (p *Projection) expressionNode() {}
+func (p *Projection) String() string { return "projection" }
+
+// TopNProjection represents TOP N projection
+type TopNProjection struct {
+ N int
+ Projection *Projection
+}
+
+func (p *TopNProjection) expressionNode() {}
+func (p *TopNProjection) String() string { return "TOP N" }
+
+// Column represents a column in projection
+type Column struct {
+ Name string
+ Type ColumnType // ::tag or ::field disambiguator
+ Function *AggregateFunction
+}
+
+func (c *Column) expressionNode() {}
+func (c *Column) String() string { return c.Name }
+
+// ColumnType represents the type disambiguator for columns
+type ColumnType int
+
+const (
+ ColumnTypeAuto ColumnType = iota
+ ColumnTypeTag
+ ColumnTypeField
+)
+
+// AggregateFunction represents aggregate functions (SUM, MEAN, COUNT, MAX, MIN)
+type AggregateFunction struct {
+ Function string // SUM, MEAN, COUNT, MAX, MIN
+ Column string
+}
+
+func (f *AggregateFunction) expressionNode() {}
+func (f *AggregateFunction) String() string { return f.Function }
+
+// FromClause represents the FROM clause
+type FromClause struct {
+ ResourceType ResourceType
+ ResourceName string
+ Groups []string
+}
+
+func (f *FromClause) expressionNode() {}
+func (f *FromClause) String() string { return "FROM" }
+
+// ResourceType represents the type of resource being queried
+type ResourceType int
+
+const (
+ ResourceTypeAuto ResourceType = iota
+ ResourceTypeStream
+ ResourceTypeMeasure
+ ResourceTypeTrace
+ ResourceTypeProperty
+)
+
+func (r ResourceType) String() string {
+ switch r {
+ case ResourceTypeStream:
+ return "STREAM"
+ case ResourceTypeMeasure:
+ return "MEASURE"
+ case ResourceTypeTrace:
+ return "TRACE"
+ case ResourceTypeProperty:
+ return "PROPERTY"
+ default:
+ return "AUTO"
+ }
+}
+
+// TimeCondition represents TIME conditions
+type TimeCondition struct {
+ Operator TimeOperator
+ Timestamp string
+ Begin string // for BETWEEN
+ End string // for BETWEEN
+}
+
+func (t *TimeCondition) expressionNode() {}
+func (t *TimeCondition) String() string { return "TIME" }
+
+// TimeOperator represents time comparison operators
+type TimeOperator int
+
+const (
+ TimeOpEqual TimeOperator = iota
+ TimeOpGreater
+ TimeOpLess
+ TimeOpGreaterEqual
+ TimeOpLessEqual
+ TimeOpBetween
+)
+
+// WhereClause represents the WHERE clause
+type WhereClause struct {
+ Conditions []*Condition
+ Logic LogicOperator
+}
+
+func (w *WhereClause) expressionNode() {}
+func (w *WhereClause) String() string { return "WHERE" }
+
+// Condition represents a single condition in WHERE clause
+type Condition struct {
+ Left string
+ Operator BinaryOperator
+ Right *Value
+ Values []*Value // for IN, NOT IN
+ Logic LogicOperator // for combining conditions
+}
+
+func (c *Condition) expressionNode() {}
+func (c *Condition) String() string { return c.Left }
+
+// BinaryOperator represents binary operators
+type BinaryOperator int
+
+const (
+ OpEqual BinaryOperator = iota
+ OpNotEqual
+ OpGreater
+ OpLess
+ OpGreaterEqual
+ OpLessEqual
+ OpIn
+ OpNotIn
+ OpHaving
+ OpNotHaving
+ OpMatch
+)
+
+func (op BinaryOperator) String() string {
+ switch op {
+ case OpEqual:
+ return "="
+ case OpNotEqual:
+ return "!="
+ case OpGreater:
+ return ">"
+ case OpLess:
+ return "<"
+ case OpGreaterEqual:
+ return ">="
+ case OpLessEqual:
+ return "<="
+ case OpIn:
+ return "IN"
+ case OpNotIn:
+ return "NOT IN"
+ case OpHaving:
+ return "HAVING"
+ case OpNotHaving:
+ return "NOT HAVING"
+ case OpMatch:
+ return "MATCH"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// LogicOperator represents logical operators (AND, OR)
+type LogicOperator int
+
+const (
+ LogicAnd LogicOperator = iota
+ LogicOr
+)
+
+func (op LogicOperator) String() string {
+ switch op {
+ case LogicAnd:
+ return "AND"
+ case LogicOr:
+ return "OR"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// Value represents a value in conditions
+type Value struct {
+ Type ValueType
+ StringVal string
+ Integer int64
+ IsNull bool
+}
+
+func (v *Value) expressionNode() {}
+func (v *Value) String() string {
+ if v.IsNull {
+ return "NULL"
+ }
+ switch v.Type {
+ case ValueTypeString:
+ return v.StringVal
+ case ValueTypeInteger:
+ return string(rune(v.Integer))
+ default:
+ return ""
+ }
+}
+
+// ValueType represents the type of a value
+type ValueType int
+
+const (
+ ValueTypeString ValueType = iota
+ ValueTypeInteger
+ ValueTypeNull
+)
+
+// GroupByClause represents GROUP BY clause
+type GroupByClause struct {
+ Columns []string
+}
+
+func (g *GroupByClause) expressionNode() {}
+func (g *GroupByClause) String() string { return "GROUP BY" }
+
+// OrderByClause represents ORDER BY clause
+type OrderByClause struct {
+ Column string
+ Desc bool
+}
+
+func (o *OrderByClause) expressionNode() {}
+func (o *OrderByClause) String() string { return "ORDER BY" }
+
+// ParsedQuery represents a parsed BydbQL query with metadata
+type ParsedQuery struct {
+ Statement Statement
+ ResourceType ResourceType
+ ResourceName string
+ Groups []string
+ Context *QueryContext
+}
+
+// QueryContext provides execution context for queries
+type QueryContext struct {
+ DefaultGroup string
+ DefaultResourceName string
+ DefaultResourceType ResourceType
+ CurrentTime time.Time
+}
\ No newline at end of file
diff --git a/pkg/bydbql/bydbql_suite_test.go b/pkg/bydbql/bydbql_suite_test.go
new file mode 100644
index 00000000..623ecab8
--- /dev/null
+++ b/pkg/bydbql/bydbql_suite_test.go
@@ -0,0 +1,30 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package bydbql_test
+
+import (
+ "testing"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+func TestBydbql(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Bydbql Suite")
+}
\ No newline at end of file
diff --git a/pkg/bydbql/bydbql_test.go b/pkg/bydbql/bydbql_test.go
new file mode 100644
index 00000000..57e8d8f8
--- /dev/null
+++ b/pkg/bydbql/bydbql_test.go
@@ -0,0 +1,465 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package bydbql_test
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "sigs.k8s.io/yaml"
+
+ . "github.com/apache/skywalking-banyandb/pkg/bydbql"
+)
+
+var _ = Describe("Lexer", func() {
+ DescribeTable("tokenizes queries correctly",
+ func(input string, expected []TokenType) {
+ lexer := NewLexer(input)
+ tokens := lexer.GetAllTokens()
+
+ Expect(tokens).To(HaveLen(len(expected)))
+
+ for i, expectedToken := range expected {
+ Expect(tokens[i].Type).To(Equal(expectedToken),
+ "token %d: expected %s, got %s", i, expectedToken.String(), tokens[i].Type.String())
+ }
+ },
+ Entry("basic SELECT query",
+ "SELECT * FROM STREAM sw",
+ []TokenType{TokenSelect, TokenStar, TokenFrom, TokenStream, TokenIdentifier, TokenEOF}),
+ Entry("SELECT with WHERE clause",
+ "SELECT trace_id FROM STREAM sw WHERE service_id = 'webapp'",
+ []TokenType{
+ TokenSelect, TokenIdentifier, TokenFrom, TokenStream, TokenIdentifier,
+ TokenWhere, TokenIdentifier, TokenEqual, TokenString, TokenEOF,
+ }),
+ Entry("TOP N query",
+ "SHOW TOP 10 FROM MEASURE service_latency",
+ []TokenType{TokenShow, TokenTop, TokenInteger, TokenFrom, TokenMeasure, TokenIdentifier, TokenEOF}),
+ Entry("aggregate function",
+ "SELECT SUM(latency) FROM MEASURE metrics",
+ []TokenType{
+ TokenSelect, TokenSum, TokenLeftParen, TokenIdentifier, TokenRightParen,
+ TokenFrom, TokenMeasure, TokenIdentifier, TokenEOF,
+ }),
+ Entry("time range",
+ "TIME BETWEEN '2023-01-01T00:00:00Z' AND '2023-01-02T00:00:00Z'",
+ []TokenType{TokenTime, TokenBetween, TokenString, TokenAnd, TokenString, TokenEOF}),
+ Entry("column type disambiguator",
+ "SELECT status::tag, status::field FROM MEASURE metrics",
+ []TokenType{
+ TokenSelect, TokenIdentifier, TokenDoubleColon, TokenIdentifier,
+ TokenComma, TokenIdentifier, TokenDoubleColon, TokenIdentifier,
+ TokenFrom, TokenMeasure, TokenIdentifier, TokenEOF,
+ }),
+ )
+})
+
+var _ = Describe("Parser", func() {
+ Describe("valid queries", func() {
+ It("parses simple SELECT *", func() {
+ parsed, errors := ParseQuery("SELECT *")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.Projection).NotTo(BeNil())
+ Expect(stmt.Projection.All).To(BeTrue())
+ })
+
+ It("parses SELECT with FROM clause", func() {
+ parsed, errors := ParseQuery("SELECT * FROM STREAM sw")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.From).NotTo(BeNil())
+ Expect(stmt.From.ResourceType).To(Equal(ResourceTypeStream))
+ Expect(stmt.From.ResourceName).To(Equal("sw"))
+ })
+
+ It("parses SELECT with WHERE clause", func() {
+ parsed, errors := ParseQuery("SELECT trace_id FROM STREAM sw WHERE service_id = 'webapp'")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.Where).NotTo(BeNil())
+ Expect(stmt.Where.Conditions).To(HaveLen(1))
+ Expect(stmt.Where.Conditions[0].Left).To(Equal("service_id"))
+ })
+
+ It("parses SELECT with TIME condition", func() {
+ parsed, errors := ParseQuery("SELECT * FROM STREAM sw TIME > '-30m'")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.Time).NotTo(BeNil())
+ Expect(stmt.Time.Operator).To(Equal(TimeOpGreater))
+ })
+
+ It("parses SELECT with GROUP BY", func() {
+ parsed, errors := ParseQuery("SELECT region, SUM(latency) FROM MEASURE metrics GROUP BY region")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.GroupBy).NotTo(BeNil())
+ Expect(stmt.GroupBy.Columns).To(HaveLen(1))
+ Expect(stmt.GroupBy.Columns[0]).To(Equal("region"))
+ })
+
+ It("parses TOP N statement", func() {
+ parsed, errors := ParseQuery("SHOW TOP 10 FROM MEASURE service_latency ORDER BY value DESC")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*TopNStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.TopN).To(Equal(10))
+ Expect(stmt.OrderBy).NotTo(BeNil())
+ Expect(stmt.OrderBy.Desc).To(BeTrue())
+ })
+
+ It("parses empty projection for traces", func() {
+ parsed, errors := ParseQuery("SELECT () FROM TRACE sw_trace")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.Projection).NotTo(BeNil())
+ Expect(stmt.Projection.Empty).To(BeTrue())
+ })
+
+ It("parses WITH QUERY_TRACE", func() {
+ parsed, errors := ParseQuery("SELECT * FROM STREAM sw WITH QUERY_TRACE")
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ stmt, ok := parsed.Statement.(*SelectStatement)
+ Expect(ok).To(BeTrue())
+ Expect(stmt.QueryTrace).To(BeTrue())
+ })
+ })
+
+ Describe("invalid queries", func() {
+ It("handles invalid syntax", func() {
+ parsed, errors := ParseQuery("SELECT FROM")
+ Expect(errors).NotTo(BeEmpty())
+ Expect(parsed).To(BeNil())
+ })
+ })
+})
+
+var _ = Describe("Translator", 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 queries correctly",
+ func(input string, validateFunc func(map[string]any) bool) {
+ parsed, errors := ParseQuery(input)
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ translator := NewTranslator(context)
+ data, err := translator.TranslateToMap(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(validateFunc(data)).To(BeTrue(), "validation failed for data: %+v", data)
+ },
+ Entry("simple stream query",
+ "SELECT * FROM STREAM sw",
+ func(data map[string]any) bool {
+ return data["Name"] == "sw"
+ }),
+ Entry("measure query with aggregation",
+ "SELECT region, SUM(latency) FROM MEASURE metrics GROUP BY region",
+ func(data map[string]any) bool {
+ agg, ok := data["Agg"].(map[string]any)
+ return ok && agg["Function"] == "SUM" && agg["FieldName"] == "latency"
+ }),
+ Entry("time range query",
+ "SELECT * FROM STREAM sw TIME BETWEEN '2023-01-01T00:00:00Z' AND '2023-01-02T00:00:00Z'",
+ func(data map[string]any) bool {
+ timeRange, ok := data["TimeRange"].(map[string]any)
+ return ok && timeRange["Begin"] == "2023-01-01T00:00:00Z" &&
+ timeRange["End"] == "2023-01-02T00:00:00Z"
+ }),
+ Entry("relative time query",
+ "SELECT * FROM STREAM sw TIME > '-30m'",
+ func(data map[string]any) bool {
+ timeRange, ok := data["TimeRange"].(map[string]any)
+ return ok && timeRange["Begin"] != nil && timeRange["End"] != nil
+ }),
+ 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 {
+ return false
+ }
+ first, ok := criteria[0].(map[string]any)
+ return ok && first["tagName"] == "service_id" && first["op"] == "BINARY_OP_EQ"
+ }),
+ Entry("TOP N query",
+ "SHOW TOP 10 FROM MEASURE service_latency ORDER BY value DESC",
+ func(data map[string]any) bool {
+ topN := data["TopN"]
+ fieldValueSort := data["FieldValueSort"]
+ return topN == float64(10) && fieldValueSort == "DESC"
+ }),
+ 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
+ }),
+ Entry("query trace enabled",
+ "SELECT * FROM STREAM sw WITH QUERY_TRACE",
+ func(data map[string]any) bool {
+ return data["Trace"] == true
+ }),
+ )
+})
+
+var _ = Describe("Complex Queries", func() {
+ var context *QueryContext
+
+ BeforeEach(func() {
+ context = &QueryContext{
+ DefaultGroup: "default",
+ CurrentTime: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
+ }
+ })
+
+ complexQueries := []string{
+ // Stream queries
+ `SELECT trace_id, service_id, start_time
+ FROM STREAM sw IN (default, updated)
+ WHERE service_id = 'webapp' AND state = 1
+ ORDER BY start_time DESC
+ LIMIT 100`,
+
+ // Measure queries with complex projections
+ `SELECT region, SUM(latency)
+ FROM MEASURE service_cpm IN (us-west, us-east)
+ TIME BETWEEN '-2h' AND 'now'
+ WHERE service = 'auth-service'
+ GROUP BY region`,
+
+ // Trace queries with empty projection
+ `SELECT ()
+ FROM TRACE sw_trace
+ TIME > '-1h'
+ WHERE status = 'error'
+ WITH QUERY_TRACE
+ LIMIT 50`,
+
+ // Property queries
+ `SELECT ip, region, owner
+ FROM PROPERTY server_metadata IN (datacenter-1, datacenter-2)
+ WHERE datacenter = 'dc-101'
+ LIMIT 50`,
+
+ // Top-N queries
+ `SHOW TOP 5
+ FROM MEASURE service_errors IN (production, staging)
+ TIME BETWEEN '-24h' AND 'now'
+ WHERE status_code = '500'
+ ORDER BY value DESC`,
+ }
+
+ for i, query := range complexQueries {
+ It(fmt.Sprintf("parses and translates complex query %d", i+1), func() {
+ // Parse the query
+ parsed, errors := ParseQuery(query)
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ // Translate to YAML
+ translator := NewTranslator(context)
+ yamlData, err := translator.TranslateToYAML(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(yamlData).NotTo(BeEmpty())
+
+ // Validate YAML structure by unmarshaling
+ var result map[string]any
+ err = yaml.Unmarshal(yamlData, &result)
+ Expect(err).NotTo(HaveOccurred())
+ })
+ }
+})
+
+var _ = Describe("Error Handling", func() {
+ invalidQueries := []string{
+ "SELECT", // incomplete query
+ "SELECT * FROM", // missing resource
+ "SELECT * FROM INVALID sw", // invalid resource type
+ "SHOW TOP FROM MEASURE metrics", // missing N
+ "SELECT * WHERE service_id", // incomplete condition
+ "TIME > '2023-01-01'", // missing SELECT
+ "SELECT * FROM STREAM sw GROUP BY", // incomplete GROUP BY
+ }
+
+ for i, query := range invalidQueries {
+ It(fmt.Sprintf("handles invalid query %d", i+1), func() {
+ parsed, errors := ParseQuery(query)
+
+ // Should have parsing errors
+ Expect(errors).NotTo(BeEmpty(), "expected parsing errors for query: %s", query)
+
+ // Parsed query should be nil for invalid queries
+ Expect(parsed).To(BeNil(), "expected nil parsed query for invalid query: %s", query)
+ })
+ }
+})
+
+var _ = Describe("Time Format Parsing", func() {
+ var context *QueryContext
+
+ BeforeEach(func() {
+ context = &QueryContext{
+ CurrentTime: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC),
+ }
+ })
+
+ DescribeTable("parses time formats correctly in queries",
+ func(timeCondition string, validateFunc func(map[string]any) bool) {
+ query := fmt.Sprintf("SELECT * FROM STREAM sw TIME %s", timeCondition)
+ parsed, errors := ParseQuery(query)
+ Expect(errors).To(BeEmpty())
+ Expect(parsed).NotTo(BeNil())
+
+ translator := NewTranslator(context)
+ data, err := translator.TranslateToMap(parsed)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(validateFunc(data)).To(BeTrue(), "time validation failed for data: %+v", data)
+ },
+ Entry("absolute time range",
+ "BETWEEN '2023-01-01T10:00:00Z' AND '2023-01-01T11:00:00Z'",
+ func(data map[string]any) bool {
+ timeRange, ok := data["TimeRange"].(map[string]any)
+ return ok && timeRange["Begin"] == "2023-01-01T10:00:00Z" &&
+ timeRange["End"] == "2023-01-01T11:00:00Z"
+ }),
+ Entry("relative time condition",
+ "> '-30m'",
+ func(data map[string]any) bool {
+ timeRange, ok := data["TimeRange"].(map[string]any)
+ return ok && timeRange["Begin"] != nil && timeRange["End"] != nil
+ }),
+ )
+})
+
+var _ = Describe("Case Insensitivity", func() {
+ // Test that keywords are case-insensitive
+ queries := []string{
+ "SELECT * FROM STREAM sw",
+ "select * from stream sw",
+ "Select * From Stream sw",
+ "sElEcT * fRoM sTrEaM sw",
+ }
+
+ for i, query := range queries {
+ It(fmt.Sprintf("parses case-insensitive query %d", i+1), func() {
+ parsed, errors := ParseQuery(query)
+ Expect(errors).To(BeEmpty(), "parsing errors for query '%s': %v", query, errors)
+ Expect(parsed).NotTo(BeNil(), "failed to parse case-insensitive query: %s", query)
+ })
+ }
+})
+
+// Benchmark tests
+func BenchmarkLexer(b *testing.B) {
+ query := `SELECT trace_id, service_id, start_time
+ FROM STREAM sw IN (default, updated)
+ WHERE service_id = 'webapp' AND state = 1
+ ORDER BY start_time DESC
+ LIMIT 100`
+
+ for i := 0; i < b.N; i++ {
+ lexer := NewLexer(query)
+ _ = lexer.GetAllTokens()
+ }
+}
+
+func BenchmarkParser(b *testing.B) {
+ query := `SELECT trace_id, service_id, start_time
+ FROM STREAM sw IN (default, updated)
+ WHERE service_id = 'webapp' AND state = 1
+ ORDER BY start_time DESC
+ LIMIT 100`
+
+ for i := 0; i < b.N; i++ {
+ _, _ = ParseQuery(query)
+ }
+}
+
+func BenchmarkTranslator(b *testing.B) {
+ query := `SELECT trace_id, service_id, start_time
+ FROM STREAM sw IN (default, updated)
+ WHERE service_id = 'webapp' AND state = 1
+ ORDER BY start_time DESC
+ LIMIT 100`
+
+ context := &QueryContext{
+ CurrentTime: time.Now(),
+ }
+
+ // Parse once
+ parsed, _ := ParseQuery(query)
+ translator := NewTranslator(context)
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, _ = translator.TranslateToYAML(parsed)
+ }
+}
+
+func BenchmarkEndToEnd(b *testing.B) {
+ query := `SELECT trace_id, service_id, start_time
+ FROM STREAM sw IN (default, updated)
+ WHERE service_id = 'webapp' AND state = 1
+ ORDER BY start_time DESC
+ LIMIT 100`
+
+ context := &QueryContext{
+ CurrentTime: time.Now(),
+ }
+
+ for i := 0; i < b.N; i++ {
+ _, _, _ = TranslateQuery(query, context)
+ }
+}
diff --git a/pkg/bydbql/lexer.go b/pkg/bydbql/lexer.go
new file mode 100644
index 00000000..a3acee3f
--- /dev/null
+++ b/pkg/bydbql/lexer.go
@@ -0,0 +1,608 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package bydbql
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+// TokenType represents the type of a token
+type TokenType int
+
+const (
+ // Special tokens
+ TokenEOF TokenType = iota
+ TokenIllegal
+
+ // Literals
+ TokenIdentifier
+ TokenString
+ TokenInteger
+
+ // Keywords
+ TokenSelect
+ TokenFrom
+ TokenWhere
+ TokenOrderBy
+ TokenGroupBy
+ TokenLimit
+ TokenOffset
+ TokenTime
+ TokenBetween
+ TokenAnd
+ TokenOr
+ TokenIn
+ TokenNotIn
+ TokenHaving
+ TokenNotHaving
+ TokenMatch
+ TokenShow
+ TokenTop
+ TokenStream
+ TokenMeasure
+ TokenTrace
+ TokenProperty
+ TokenAsc
+ TokenDesc
+ TokenWith
+ TokenQueryTrace
+ TokenNull
+ TokenAggregateBy
+
+ // Aggregate functions
+ TokenSum
+ TokenMean
+ TokenAvg
+ TokenCount
+ TokenMax
+ TokenMin
+
+ // Operators
+ TokenEqual
+ TokenNotEqual
+ TokenGreater
+ TokenLess
+ TokenGreaterEqual
+ TokenLessEqual
+
+ // Punctuation
+ TokenComma
+ TokenSemicolon
+ TokenLeftParen
+ TokenRightParen
+ TokenDot
+ TokenDoubleColon
+
+ // Special literals
+ TokenStar
+ TokenBy
+ TokenValue
+)
+
+// Token represents a lexical token
+type Token struct {
+ Type TokenType
+ Literal string
+ Line int
+ Column int
+ Position int
+}
+
+func (t Token) String() string {
+ return fmt.Sprintf("Token{Type: %s, Literal: %s, Line: %d, Column: %d}",
+ t.Type.String(), t.Literal, t.Line, t.Column)
+}
+
+func (tt TokenType) String() string {
+ switch tt {
+ case TokenEOF:
+ return "EOF"
+ case TokenIllegal:
+ return "ILLEGAL"
+ case TokenIdentifier:
+ return "IDENTIFIER"
+ case TokenString:
+ return "STRING"
+ case TokenInteger:
+ return "INTEGER"
+ case TokenSelect:
+ return "SELECT"
+ case TokenFrom:
+ return "FROM"
+ case TokenWhere:
+ return "WHERE"
+ case TokenOrderBy:
+ return "ORDER BY"
+ case TokenGroupBy:
+ return "GROUP BY"
+ case TokenLimit:
+ return "LIMIT"
+ case TokenOffset:
+ return "OFFSET"
+ case TokenTime:
+ return "TIME"
+ case TokenBetween:
+ return "BETWEEN"
+ case TokenAnd:
+ return "AND"
+ case TokenOr:
+ return "OR"
+ case TokenIn:
+ return "IN"
+ case TokenNotIn:
+ return "NOT IN"
+ case TokenHaving:
+ return "HAVING"
+ case TokenNotHaving:
+ return "NOT HAVING"
+ case TokenMatch:
+ return "MATCH"
+ case TokenShow:
+ return "SHOW"
+ case TokenTop:
+ return "TOP"
+ case TokenStream:
+ return "STREAM"
+ case TokenMeasure:
+ return "MEASURE"
+ case TokenTrace:
+ return "TRACE"
+ case TokenProperty:
+ return "PROPERTY"
+ case TokenAsc:
+ return "ASC"
+ case TokenDesc:
+ return "DESC"
+ case TokenWith:
+ return "WITH"
+ case TokenQueryTrace:
+ return "QUERY_TRACE"
+ case TokenNull:
+ return "NULL"
+ case TokenAggregateBy:
+ return "AGGREGATE BY"
+ case TokenSum:
+ return "SUM"
+ case TokenMean:
+ return "MEAN"
+ case TokenAvg:
+ return "AVG"
+ case TokenCount:
+ return "COUNT"
+ case TokenMax:
+ return "MAX"
+ case TokenMin:
+ return "MIN"
+ case TokenEqual:
+ return "="
+ case TokenNotEqual:
+ return "!="
+ case TokenGreater:
+ return ">"
+ case TokenLess:
+ return "<"
+ case TokenGreaterEqual:
+ return ">="
+ case TokenLessEqual:
+ return "<="
+ case TokenComma:
+ return ","
+ case TokenSemicolon:
+ return ";"
+ case TokenLeftParen:
+ return "("
+ case TokenRightParen:
+ return ")"
+ case TokenDot:
+ return "."
+ case TokenDoubleColon:
+ return "::"
+ case TokenStar:
+ return "*"
+ case TokenBy:
+ return "BY"
+ case TokenValue:
+ return "VALUE"
+ default:
+ return "UNKNOWN"
+ }
+}
+
+// keywords maps keyword strings to their token types
+var keywords = map[string]TokenType{
+ "select": TokenSelect,
+ "from": TokenFrom,
+ "where": TokenWhere,
+ "order": TokenOrderBy, // handled specially as "order by"
+ "group": TokenGroupBy, // handled specially as "group by"
+ "limit": TokenLimit,
+ "offset": TokenOffset,
+ "time": TokenTime,
+ "between": TokenBetween,
+ "and": TokenAnd,
+ "or": TokenOr,
+ "in": TokenIn,
+ "not": TokenNotIn, // handled specially
+ "having": TokenHaving,
+ "match": TokenMatch,
+ "show": TokenShow,
+ "top": TokenTop,
+ "stream": TokenStream,
+ "measure": TokenMeasure,
+ "trace": TokenTrace,
+ "property": TokenProperty,
+ "asc": TokenAsc,
+ "desc": TokenDesc,
+ "with": TokenWith,
+ "query_trace": TokenQueryTrace,
+ "null": TokenNull,
+ "aggregate": TokenAggregateBy, // handled specially as "aggregate by"
+ "sum": TokenSum,
+ "mean": TokenMean,
+ "avg": TokenAvg,
+ "count": TokenCount,
+ "max": TokenMax,
+ "min": TokenMin,
+ "by": TokenBy,
+ "value": TokenValue,
+}
+
+// Lexer represents a lexical analyzer
+type Lexer struct {
+ input string
+ position int
+ readPosition int
+ ch byte
+ line int
+ column int
+}
+
+// NewLexer creates a new lexer instance
+func NewLexer(input string) *Lexer {
+ l := &Lexer{
+ input: input,
+ line: 1,
+ column: 0,
+ }
+ l.readChar()
+ return l
+}
+
+// readChar reads the next character and advances position
+func (l *Lexer) readChar() {
+ if l.readPosition >= len(l.input) {
+ l.ch = 0 // EOF
+ } else {
+ l.ch = l.input[l.readPosition]
+ }
+ l.position = l.readPosition
+ l.readPosition++
+
+ if l.ch == '\n' {
+ l.line++
+ l.column = 0
+ } else {
+ l.column++
+ }
+}
+
+// peekChar returns the next character without advancing position
+func (l *Lexer) peekChar() byte {
+ if l.readPosition >= len(l.input) {
+ return 0
+ }
+ return l.input[l.readPosition]
+}
+
+// skipWhitespace skips whitespace characters
+func (l *Lexer) skipWhitespace() {
+ for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
+ l.readChar()
+ }
+}
+
+// readIdentifier reads an identifier or keyword
+func (l *Lexer) readIdentifier() string {
+ position := l.position
+ for isLetter(l.ch) || isDigit(l.ch) || l.ch == '_' || l.ch == '-' {
+ l.readChar()
+ }
+ return l.input[position:l.position]
+}
+
+// readNumber reads a numeric literal
+func (l *Lexer) readNumber() string {
+ position := l.position
+ for isDigit(l.ch) {
+ l.readChar()
+ }
+ return l.input[position:l.position]
+}
+
+// readString reads a string literal
+func (l *Lexer) readString(delimiter byte) (string, error) {
+ position := l.position + 1 // skip opening quote
+ for {
+ l.readChar()
+ if l.ch == delimiter || l.ch == 0 {
+ break
+ }
+ if l.ch == '\\' && l.peekChar() == delimiter {
+ l.readChar() // skip escaped quote
+ }
+ }
+
+ if l.ch == 0 {
+ return "", fmt.Errorf("unterminated string literal at line %d, column %d", l.line, l.column)
+ }
+
+ str := l.input[position:l.position]
+ l.readChar() // skip closing quote
+ return str, nil
+}
+
+// lookupIdent checks if an identifier is a keyword
+func (l *Lexer) lookupIdent(ident string) TokenType {
+ lower := strings.ToLower(ident)
+ if tok, ok := keywords[lower]; ok {
+ // Handle compound keywords
+ switch lower {
+ case "order":
+ if l.peekAhead("by") {
+ l.consumeKeyword("by")
+ return TokenOrderBy
+ }
+ case "group":
+ if l.peekAhead("by") {
+ l.consumeKeyword("by")
+ return TokenGroupBy
+ }
+ case "aggregate":
+ if l.peekAhead("by") {
+ l.consumeKeyword("by")
+ return TokenAggregateBy
+ }
+ case "not":
+ if l.peekAhead("in") {
+ l.consumeKeyword("in")
+ return TokenNotIn
+ } else if l.peekAhead("having") {
+ l.consumeKeyword("having")
+ return TokenNotHaving
+ }
+ }
+ return tok
+ }
+ return TokenIdentifier
+}
+
+// peekAhead checks if the next identifier matches the expected keyword
+func (l *Lexer) peekAhead(expected string) bool {
+ savedPos := l.position
+ savedReadPos := l.readPosition
+ savedCh := l.ch
+ savedLine := l.line
+ savedColumn := l.column
+
+ l.skipWhitespace()
+
+ if !isLetter(l.ch) {
+ // Restore lexer state
+ l.position = savedPos
+ l.readPosition = savedReadPos
+ l.ch = savedCh
+ l.line = savedLine
+ l.column = savedColumn
+ return false
+ }
+
+ ident := l.readIdentifier()
+ match := strings.EqualFold(ident, expected)
+
+ if !match {
+ // Restore lexer state
+ l.position = savedPos
+ l.readPosition = savedReadPos
+ l.ch = savedCh
+ l.line = savedLine
+ l.column = savedColumn
+ } else {
+ // When there's a match, we need to restore the lexer state
+ // because consumeKeyword will consume the keyword again
+ l.position = savedPos
+ l.readPosition = savedReadPos
+ l.ch = savedCh
+ l.line = savedLine
+ l.column = savedColumn
+ }
+
+ return match
+}
+
+// consumeKeyword consumes the expected keyword
+func (l *Lexer) consumeKeyword(expected string) {
+ l.skipWhitespace()
+ l.readIdentifier() // consume the keyword
+}
+
+// NextToken returns the next token
+func (l *Lexer) NextToken() Token {
+ var tok Token
+ tok.Line = l.line
+ tok.Column = l.column
+ tok.Position = l.position
+
+ l.skipWhitespace()
+
+ switch l.ch {
+ case '=':
+ tok.Type = TokenEqual
+ tok.Literal = "="
+ case '!':
+ if l.peekChar() == '=' {
+ ch := l.ch
+ l.readChar()
+ tok.Type = TokenNotEqual
+ tok.Literal = string(ch) + string(l.ch)
+ } else {
+ tok.Type = TokenIllegal
+ tok.Literal = string(l.ch)
+ }
+ case '>':
+ if l.peekChar() == '=' {
+ ch := l.ch
+ l.readChar()
+ tok.Type = TokenGreaterEqual
+ tok.Literal = string(ch) + string(l.ch)
+ } else {
+ tok.Type = TokenGreater
+ tok.Literal = ">"
+ }
+ case '<':
+ if l.peekChar() == '=' {
+ ch := l.ch
+ l.readChar()
+ tok.Type = TokenLessEqual
+ tok.Literal = string(ch) + string(l.ch)
+ } else {
+ tok.Type = TokenLess
+ tok.Literal = "<"
+ }
+ case ',':
+ tok.Type = TokenComma
+ tok.Literal = ","
+ case ';':
+ tok.Type = TokenSemicolon
+ tok.Literal = ";"
+ case '(':
+ tok.Type = TokenLeftParen
+ tok.Literal = "("
+ case ')':
+ tok.Type = TokenRightParen
+ tok.Literal = ")"
+ case '.':
+ tok.Type = TokenDot
+ tok.Literal = "."
+ case ':':
+ if l.peekChar() == ':' {
+ l.readChar()
+ tok.Type = TokenDoubleColon
+ tok.Literal = "::"
+ } else {
+ tok.Type = TokenIllegal
+ tok.Literal = ":"
+ }
+ case '*':
+ tok.Type = TokenStar
+ tok.Literal = "*"
+ case '\'', '"':
+ str, err := l.readString(l.ch)
+ if err != nil {
+ tok.Type = TokenIllegal
+ tok.Literal = err.Error()
+ } else {
+ tok.Type = TokenString
+ tok.Literal = str
+ }
+ case 0:
+ tok.Type = TokenEOF
+ tok.Literal = ""
+ default:
+ if isLetter(l.ch) {
+ tok.Literal = l.readIdentifier()
+ tok.Type = l.lookupIdent(tok.Literal)
+ return tok // return early to avoid readChar()
+ } else if isDigit(l.ch) {
+ tok.Type = TokenInteger
+ tok.Literal = l.readNumber()
+ return tok // return early to avoid readChar()
+ } else {
+ tok.Type = TokenIllegal
+ tok.Literal = string(l.ch)
+ }
+ }
+
+ l.readChar()
+ return tok
+}
+
+// GetAllTokens returns all tokens from the input
+func (l *Lexer) GetAllTokens() []Token {
+ var tokens []Token
+ for {
+ tok := l.NextToken()
+ tokens = append(tokens, tok)
+ if tok.Type == TokenEOF {
+ break
+ }
+ }
+ return tokens
+}
+
+// isLetter checks if the character is a letter
+func isLetter(ch byte) bool {
+ return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
+}
+
+// isDigit checks if the character is a digit
+func isDigit(ch byte) bool {
+ return '0' <= ch && ch <= '9'
+}
+
+// isAlphaNumeric checks if the character is alphanumeric
+func isAlphaNumeric(ch byte) bool {
+ return isLetter(ch) || isDigit(ch)
+}
+
+// ParseInteger parses a token literal as integer
+func ParseInteger(literal string) (int64, error) {
+ return strconv.ParseInt(literal, 10, 64)
+}
+
+// IsUnicodeIdentifier checks if a rune can be part of a Unicode identifier
+func IsUnicodeIdentifier(r rune) bool {
+ return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
+}
+
+// IsUnicodeIdentifierStart checks if a rune can start a Unicode identifier
+func IsUnicodeIdentifierStart(r rune) bool {
+ return unicode.IsLetter(r) || r == '_'
+}
+
+// ReadUnicodeIdentifier reads a Unicode identifier
+func (l *Lexer) ReadUnicodeIdentifier() string {
+ start := l.position
+ for l.position < len(l.input) {
+ r, size := utf8.DecodeRuneInString(l.input[l.position:])
+ if r == utf8.RuneError || !IsUnicodeIdentifier(r) {
+ break
+ }
+ l.position += size
+ l.readPosition = l.position + 1
+ if l.position < len(l.input) {
+ l.ch = l.input[l.position]
+ } else {
+ l.ch = 0
+ }
+ }
+ return l.input[start:l.position]
+}
\ No newline at end of file
diff --git a/pkg/bydbql/parser.go b/pkg/bydbql/parser.go
new file mode 100644
index 00000000..07b3610a
--- /dev/null
+++ b/pkg/bydbql/parser.go
@@ -0,0 +1,796 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package bydbql
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Parser represents a BydbQL parser
+type Parser struct {
+ lexer *Lexer
+ errors []string
+
+ currentToken Token
+ peekToken Token
+}
+
+// NewParser creates a new parser instance
+func NewParser(lexer *Lexer) *Parser {
+ p := &Parser{
+ lexer: lexer,
+ errors: []string{},
+ }
+
+ // Read two tokens, so currentToken and peekToken are both set
+ p.nextToken()
+ p.nextToken()
+
+ return p
+}
+
+// nextToken advances to the next token
+func (p *Parser) nextToken() {
+ p.currentToken = p.peekToken
+ p.peekToken = p.lexer.NextToken()
+}
+
+// addError adds a parsing error
+func (p *Parser) addError(msg string, args ...interface{}) {
+ p.errors = append(p.errors, fmt.Sprintf(msg, args...))
+}
+
+// Errors returns parsing errors
+func (p *Parser) Errors() []string {
+ return p.errors
+}
+
+// expectToken checks if the current token matches expected type and advances
+func (p *Parser) expectToken(tokenType TokenType) bool {
+ if p.currentTokenIs(tokenType) {
+ p.nextToken()
+ return true
+ }
+
+ p.addError("expected token %s, got %s at line %d, column %d",
+ tokenType.String(), p.currentToken.Type.String(),
+ p.currentToken.Line, p.currentToken.Column)
+ return false
+}
+
+// currentTokenIs checks if current token matches the given type
+func (p *Parser) currentTokenIs(tokenType TokenType) bool {
+ return p.currentToken.Type == tokenType
+}
+
+// peekTokenIs checks if peek token matches the given type
+func (p *Parser) peekTokenIs(tokenType TokenType) bool {
+ return p.peekToken.Type == tokenType
+}
+
+// Parse parses the BydbQL query and returns a parsed query
+func (p *Parser) Parse() *ParsedQuery {
+ var stmt Statement
+
+ switch p.currentToken.Type {
+ case TokenSelect:
+ stmt = p.parseSelectStatement()
+ case TokenShow:
+ stmt = p.parseTopNStatement()
+ default:
+ p.addError("unexpected token %s at line %d, column %d. Expected SELECT or SHOW",
+ p.currentToken.Type.String(), p.currentToken.Line, p.currentToken.Column)
+ return nil
+ }
+
+ if len(p.errors) > 0 {
+ return nil
+ }
+
+ // Extract resource information from the statement
+ query := &ParsedQuery{
+ Statement: stmt,
+ Context: &QueryContext{CurrentTime: time.Now()},
+ }
+
+ switch s := stmt.(type) {
+ case *SelectStatement:
+ if s.From != nil {
+ query.ResourceType = s.From.ResourceType
+ query.ResourceName = s.From.ResourceName
+ query.Groups = s.From.Groups
+ }
+ case *TopNStatement:
+ if s.From != nil {
+ query.ResourceType = s.From.ResourceType
+ query.ResourceName = s.From.ResourceName
+ query.Groups = s.From.Groups
+ }
+ }
+
+ return query
+}
+
+// parseSelectStatement parses a SELECT statement
+func (p *Parser) parseSelectStatement() *SelectStatement {
+ stmt := &SelectStatement{}
+
+ if !p.expectToken(TokenSelect) {
+ return nil
+ }
+
+ // Parse projection
+ stmt.Projection = p.parseProjection()
+ if stmt.Projection == nil {
+ return nil
+ }
+
+ // Parse optional FROM clause
+ if p.currentTokenIs(TokenFrom) {
+ stmt.From = p.parseFromClause()
+ }
+
+ // Parse optional TIME clause
+ if p.currentTokenIs(TokenTime) {
+ stmt.Time = p.parseTimeCondition()
+ }
+
+ // Parse optional WHERE clause
+ if p.currentTokenIs(TokenWhere) {
+ stmt.Where = p.parseWhereClause()
+ }
+
+ // Parse optional GROUP BY clause
+ if p.currentTokenIs(TokenGroupBy) {
+ stmt.GroupBy = p.parseGroupByClause()
+ }
+
+ // Parse optional ORDER BY clause
+ if p.currentTokenIs(TokenOrderBy) {
+ stmt.OrderBy = p.parseOrderByClause()
+ }
+
+ // Parse optional LIMIT clause
+ if p.currentTokenIs(TokenLimit) {
+ p.nextToken()
+ if p.currentTokenIs(TokenInteger) {
+ if limit, err := strconv.Atoi(p.currentToken.Literal); err == nil {
+ stmt.Limit = &limit
+ } else {
+ p.addError("invalid LIMIT value: %s", p.currentToken.Literal)
+ }
+ p.nextToken()
+ } else {
+ p.addError("expected integer after LIMIT")
+ }
+ }
+
+ // Parse optional OFFSET clause
+ if p.currentTokenIs(TokenOffset) {
+ p.nextToken()
+ if p.currentTokenIs(TokenInteger) {
+ if offset, err := strconv.Atoi(p.currentToken.Literal); err == nil {
+ stmt.Offset = &offset
+ } else {
+ p.addError("invalid OFFSET value: %s", p.currentToken.Literal)
+ }
+ p.nextToken()
+ } else {
+ p.addError("expected integer after OFFSET")
+ }
+ }
+
+ // Parse optional WITH QUERY_TRACE clause
+ if p.currentTokenIs(TokenWith) {
+ p.nextToken()
+ if p.currentTokenIs(TokenQueryTrace) {
+ stmt.QueryTrace = true
+ p.nextToken()
+ } else {
+ p.addError("expected QUERY_TRACE after WITH")
+ }
+ }
+
+ return stmt
+}
+
+// parseTopNStatement parses a SHOW TOP N statement
+func (p *Parser) parseTopNStatement() *TopNStatement {
+ stmt := &TopNStatement{}
+
+ if !p.expectToken(TokenShow) {
+ return nil
+ }
+
+ if !p.expectToken(TokenTop) {
+ return nil
+ }
+
+ // Parse N
+ if p.currentTokenIs(TokenInteger) {
+ if n, err := strconv.Atoi(p.currentToken.Literal); err == nil {
+ stmt.TopN = n
+ } else {
+ p.addError("invalid TOP N value: %s", p.currentToken.Literal)
+ return nil
+ }
+ p.nextToken()
+ } else {
+ p.addError("expected integer after TOP")
+ return nil
+ }
+
+ // Parse FROM clause (mandatory for Top-N)
+ if p.currentTokenIs(TokenFrom) {
+ stmt.From = p.parseFromClause()
+ if stmt.From == nil {
+ return nil
+ }
+ } else {
+ p.addError("FROM clause is mandatory for TOP N queries")
+ return nil
+ }
+
+ // Parse optional TIME clause
+ if p.currentTokenIs(TokenTime) {
+ stmt.Time = p.parseTimeCondition()
+ }
+
+ // Parse optional WHERE clause
+ if p.currentTokenIs(TokenWhere) {
+ stmt.Where = p.parseWhereClause()
+ }
+
+ // Parse optional AGGREGATE BY clause
+ if p.currentTokenIs(TokenAggregateBy) {
+ p.nextToken()
+ stmt.AggregateBy = p.parseAggregateFunction()
+ }
+
+ // Parse optional ORDER BY clause
+ if p.currentTokenIs(TokenOrderBy) {
+ stmt.OrderBy = p.parseOrderByClause()
+ }
+
+ // Parse optional WITH QUERY_TRACE clause
+ if p.currentTokenIs(TokenWith) {
+ p.nextToken()
+ if p.currentTokenIs(TokenQueryTrace) {
+ stmt.QueryTrace = true
+ p.nextToken()
+ } else {
+ p.addError("expected QUERY_TRACE after WITH")
+ }
+ }
+
+ return stmt
+}
+
+// parseProjection parses the SELECT projection
+func (p *Parser) parseProjection() *Projection {
+ projection := &Projection{}
+
+ if p.currentTokenIs(TokenStar) {
+ projection.All = true
+ p.nextToken()
+ return projection
+ }
+
+ // Check for empty projection SELECT ()
+ if p.currentTokenIs(TokenLeftParen) {
+ p.nextToken()
+ if p.currentTokenIs(TokenRightParen) {
+ projection.Empty = true
+ p.nextToken()
+ return projection
+ } else {
+ p.addError("expected ) after (")
+ return nil
+ }
+ }
+
+ // Check for TOP N projection
+ if p.currentTokenIs(TokenTop) {
+ p.nextToken()
+ if p.currentTokenIs(TokenInteger) {
+ if n, err := strconv.Atoi(p.currentToken.Literal); err == nil {
+ projection.TopN = &TopNProjection{N: n}
+ p.nextToken()
+
+ // Parse the rest of the projection after TOP N
+ if p.currentTokenIs(TokenStar) {
+ projection.TopN.Projection = &Projection{All: true}
+ p.nextToken()
+ } else {
+ // Parse column list
+ projection.TopN.Projection = &Projection{}
+ projection.TopN.Projection.Columns = p.parseColumnList()
+ }
+ return projection
+ } else {
+ p.addError("invalid TOP N value: %s", p.currentToken.Literal)
+ return nil
+ }
+ } else {
+ p.addError("expected integer after TOP")
+ return nil
+ }
+ }
+
+ // Parse column list
+ projection.Columns = p.parseColumnList()
+ if projection.Columns == nil {
+ return nil
+ }
+
+ return projection
+}
+
+// parseColumnList parses a comma-separated list of columns
+func (p *Parser) parseColumnList() []*Column {
+ var columns []*Column
+
+ for {
+ col := p.parseColumn()
+ if col == nil {
+ return nil
+ }
+ columns = append(columns, col)
+
+ if !p.currentTokenIs(TokenComma) {
+ break
+ }
+ p.nextToken() // consume comma
+ }
+
+ return columns
+}
+
+// parseColumn parses a single column
+func (p *Parser) parseColumn() *Column {
+ col := &Column{}
+
+ // Check for aggregate functions
+ if p.isAggregateFunction(p.currentToken.Type) {
+ col.Function = p.parseAggregateFunction()
+ return col
+ }
+
+ // Parse column name
+ if p.currentTokenIs(TokenIdentifier) {
+ col.Name = p.currentToken.Literal
+ p.nextToken()
+
+ // Check for type disambiguator ::tag or ::field
+ if p.currentTokenIs(TokenDoubleColon) {
+ p.nextToken()
+ if p.currentTokenIs(TokenIdentifier) {
+ switch strings.ToLower(p.currentToken.Literal) {
+ case "tag":
+ col.Type = ColumnTypeTag
+ case "field":
+ col.Type = ColumnTypeField
+ default:
+ p.addError("invalid column type: %s. Expected 'tag' or 'field'", p.currentToken.Literal)
+ return nil
+ }
+ p.nextToken()
+ } else {
+ p.addError("expected 'tag' or 'field' after ::")
+ return nil
+ }
+ }
+ } else {
+ p.addError("expected column name")
+ return nil
+ }
+
+ return col
+}
+
+// parseAggregateFunction parses an aggregate function
+func (p *Parser) parseAggregateFunction() *AggregateFunction {
+ fn := &AggregateFunction{}
+
+ if p.isAggregateFunction(p.currentToken.Type) {
+ fn.Function = strings.ToUpper(p.currentToken.Literal)
+ p.nextToken()
+
+ if !p.expectToken(TokenLeftParen) {
+ return nil
+ }
+
+ if p.currentTokenIs(TokenIdentifier) {
+ fn.Column = p.currentToken.Literal
+ p.nextToken()
+ } else {
+ p.addError("expected column name in aggregate function")
+ return nil
+ }
+
+ if !p.expectToken(TokenRightParen) {
+ return nil
+ }
+ }
+
+ return fn
+}
+
+// isAggregateFunction checks if token type is an aggregate function
+func (p *Parser) isAggregateFunction(tokenType TokenType) bool {
+ return tokenType == TokenSum || tokenType == TokenMean || tokenType == TokenAvg ||
+ tokenType == TokenCount || tokenType == TokenMax || tokenType == TokenMin
+}
+
+// parseFromClause parses the FROM clause
+func (p *Parser) parseFromClause() *FromClause {
+ if !p.expectToken(TokenFrom) {
+ return nil
+ }
+
+ from := &FromClause{}
+
+ // Parse resource type
+ switch p.currentToken.Type {
+ case TokenStream:
+ from.ResourceType = ResourceTypeStream
+ p.nextToken()
+ case TokenMeasure:
+ from.ResourceType = ResourceTypeMeasure
+ p.nextToken()
+ case TokenTrace:
+ from.ResourceType = ResourceTypeTrace
+ p.nextToken()
+ case TokenProperty:
+ from.ResourceType = ResourceTypeProperty
+ p.nextToken()
+ case TokenIdentifier:
+ // Only allow specific resource types, reject others
+ switch strings.ToUpper(p.currentToken.Literal) {
+ case "STREAM":
+ from.ResourceType = ResourceTypeStream
+ case "MEASURE":
+ from.ResourceType = ResourceTypeMeasure
+ case "TRACE":
+ from.ResourceType = ResourceTypeTrace
+ case "PROPERTY":
+ from.ResourceType = ResourceTypeProperty
+ default:
+ p.addError("expected resource type (STREAM, MEASURE, TRACE, PROPERTY), got: %s", p.currentToken.Literal)
+ return nil
+ }
+ p.nextToken()
+ default:
+ p.addError("expected resource type (STREAM, MEASURE, TRACE, PROPERTY) or resource name")
+ return nil
+ }
+
+ // Parse resource name
+ if p.currentTokenIs(TokenIdentifier) {
+ from.ResourceName = p.currentToken.Literal
+ p.nextToken()
+ } else {
+ p.addError("expected resource name")
+ return nil
+ }
+
+ // Parse optional IN (groups) clause
+ if p.currentTokenIs(TokenIn) {
+ p.nextToken()
+ if !p.expectToken(TokenLeftParen) {
+ return nil
+ }
+
+ // Parse group list
+ for {
+ if p.currentTokenIs(TokenIdentifier) {
+ from.Groups = append(from.Groups, p.currentToken.Literal)
+ p.nextToken()
+ } else {
+ p.addError("expected group name")
+ return nil
+ }
+
+ if !p.currentTokenIs(TokenComma) {
+ break
+ }
+ p.nextToken() // consume comma
+ }
+
+ if !p.expectToken(TokenRightParen) {
+ return nil
+ }
+ }
+
+ return from
+}
+
+// parseTimeCondition parses TIME conditions
+func (p *Parser) parseTimeCondition() *TimeCondition {
+ if !p.expectToken(TokenTime) {
+ return nil
+ }
+
+ condition := &TimeCondition{}
+
+ // Parse operator
+ switch p.currentToken.Type {
+ case TokenEqual:
+ condition.Operator = TimeOpEqual
+ p.nextToken()
+ condition.Timestamp = p.parseTimestamp()
+ case TokenGreater:
+ condition.Operator = TimeOpGreater
+ p.nextToken()
+ condition.Timestamp = p.parseTimestamp()
+ case TokenLess:
+ condition.Operator = TimeOpLess
+ p.nextToken()
+ condition.Timestamp = p.parseTimestamp()
+ case TokenGreaterEqual:
+ condition.Operator = TimeOpGreaterEqual
+ p.nextToken()
+ condition.Timestamp = p.parseTimestamp()
+ case TokenLessEqual:
+ condition.Operator = TimeOpLessEqual
+ p.nextToken()
+ condition.Timestamp = p.parseTimestamp()
+ case TokenBetween:
+ condition.Operator = TimeOpBetween
+ p.nextToken()
+ condition.Begin = p.parseTimestamp()
+ if !p.expectToken(TokenAnd) {
+ return nil
+ }
+ condition.End = p.parseTimestamp()
+ default:
+ p.addError("expected time operator (=, >, <, >=, <=, BETWEEN)")
+ return nil
+ }
+
+ return condition
+}
+
+// parseTimestamp parses a timestamp (string or integer)
+func (p *Parser) parseTimestamp() string {
+ if p.currentTokenIs(TokenString) || p.currentTokenIs(TokenInteger) {
+ timestamp := p.currentToken.Literal
+ p.nextToken()
+ return timestamp
+ }
+
+ p.addError("expected timestamp")
+ return ""
+}
+
+// parseWhereClause parses WHERE clause
+func (p *Parser) parseWhereClause() *WhereClause {
+ if !p.expectToken(TokenWhere) {
+ return nil
+ }
+
+ where := &WhereClause{}
+ where.Conditions = p.parseConditions()
+
+ return where
+}
+
+// parseConditions parses conditions with AND/OR logic
+func (p *Parser) parseConditions() []*Condition {
+ var conditions []*Condition
+
+ condition := p.parseCondition()
+ if condition == nil {
+ return nil
+ }
+ conditions = append(conditions, condition)
+
+ for p.currentTokenIs(TokenAnd) || p.currentTokenIs(TokenOr) {
+ if p.currentTokenIs(TokenAnd) {
+ condition.Logic = LogicAnd
+ } else {
+ condition.Logic = LogicOr
+ }
+ p.nextToken()
+
+ condition = p.parseCondition()
+ if condition == nil {
+ return nil
+ }
+ conditions = append(conditions, condition)
+ }
+
+ return conditions
+}
+
+// parseCondition parses a single condition
+func (p *Parser) parseCondition() *Condition {
+ condition := &Condition{}
+
+ // Parse left side (column name)
+ if p.currentTokenIs(TokenIdentifier) {
+ condition.Left = p.currentToken.Literal
+ p.nextToken()
+ } else {
+ p.addError("expected column name in condition")
+ return nil
+ }
+
+ // Parse operator
+ switch p.currentToken.Type {
+ case TokenEqual:
+ condition.Operator = OpEqual
+ case TokenNotEqual:
+ condition.Operator = OpNotEqual
+ case TokenGreater:
+ condition.Operator = OpGreater
+ case TokenLess:
+ condition.Operator = OpLess
+ case TokenGreaterEqual:
+ condition.Operator = OpGreaterEqual
+ case TokenLessEqual:
+ condition.Operator = OpLessEqual
+ case TokenIn:
+ condition.Operator = OpIn
+ case TokenNotIn:
+ condition.Operator = OpNotIn
+ case TokenHaving:
+ condition.Operator = OpHaving
+ case TokenNotHaving:
+ condition.Operator = OpNotHaving
+ case TokenMatch:
+ condition.Operator = OpMatch
+ default:
+ p.addError("expected comparison operator")
+ return nil
+ }
+ p.nextToken()
+
+ // Parse right side
+ if condition.Operator == OpIn || condition.Operator == OpNotIn {
+ // Parse value list
+ if !p.expectToken(TokenLeftParen) {
+ return nil
+ }
+
+ for {
+ value := p.parseValue()
+ if value == nil {
+ return nil
+ }
+ condition.Values = append(condition.Values, value)
+
+ if !p.currentTokenIs(TokenComma) {
+ break
+ }
+ p.nextToken() // consume comma
+ }
+
+ if !p.expectToken(TokenRightParen) {
+ return nil
+ }
+ } else {
+ // Parse single value
+ condition.Right = p.parseValue()
+ if condition.Right == nil {
+ return nil
+ }
+ }
+
+ return condition
+}
+
+// parseValue parses a value (string, integer, or null)
+func (p *Parser) parseValue() *Value {
+ value := &Value{}
+
+ switch p.currentToken.Type {
+ case TokenString:
+ value.Type = ValueTypeString
+ value.StringVal = p.currentToken.Literal
+ p.nextToken()
+ case TokenInteger:
+ value.Type = ValueTypeInteger
+ if i, err := strconv.ParseInt(p.currentToken.Literal, 10, 64); err == nil {
+ value.Integer = i
+ } else {
+ p.addError("invalid integer: %s", p.currentToken.Literal)
+ return nil
+ }
+ p.nextToken()
+ case TokenNull:
+ value.Type = ValueTypeNull
+ value.IsNull = true
+ p.nextToken()
+ default:
+ p.addError("expected value (string, integer, or NULL)")
+ return nil
+ }
+
+ return value
+}
+
+// parseGroupByClause parses GROUP BY clause
+func (p *Parser) parseGroupByClause() *GroupByClause {
+ if !p.expectToken(TokenGroupBy) {
+ return nil
+ }
+
+ groupBy := &GroupByClause{}
+
+ // Parse column list
+ for {
+ if p.currentTokenIs(TokenIdentifier) {
+ groupBy.Columns = append(groupBy.Columns, p.currentToken.Literal)
+ p.nextToken()
+ } else {
+ p.addError("expected column name in GROUP BY")
+ return nil
+ }
+
+ if !p.currentTokenIs(TokenComma) {
+ break
+ }
+ p.nextToken() // consume comma
+ }
+
+ return groupBy
+}
+
+// parseOrderByClause parses ORDER BY clause
+func (p *Parser) parseOrderByClause() *OrderByClause {
+ if !p.expectToken(TokenOrderBy) {
+ return nil
+ }
+
+ orderBy := &OrderByClause{}
+
+ // Parse column name (or "value" for Top-N queries)
+ if p.currentTokenIs(TokenIdentifier) {
+ orderBy.Column = p.currentToken.Literal
+ p.nextToken()
+ } else if p.currentTokenIs(TokenValue) {
+ orderBy.Column = "value"
+ p.nextToken()
+ } else {
+ p.addError("expected column name in ORDER BY")
+ return nil
+ }
+
+ // Parse optional ASC/DESC
+ if p.currentTokenIs(TokenAsc) {
+ orderBy.Desc = false
+ p.nextToken()
+ } else if p.currentTokenIs(TokenDesc) {
+ orderBy.Desc = true
+ p.nextToken()
+ }
+
+ return orderBy
+}
+
+// ParseQuery is a convenience function to parse a BydbQL query string
+func ParseQuery(query string) (*ParsedQuery, []string) {
+ lexer := NewLexer(query)
+ parser := NewParser(lexer)
+ parsed := parser.Parse()
+ return parsed, parser.Errors()
+}
\ No newline at end of file
diff --git a/pkg/bydbql/translator.go b/pkg/bydbql/translator.go
new file mode 100644
index 00000000..ca41b8fd
--- /dev/null
+++ b/pkg/bydbql/translator.go
@@ -0,0 +1,591 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package bydbql
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ str2duration "github.com/xhit/go-str2duration/v2"
+ "sigs.k8s.io/yaml"
+)
+
+// QueryYAML represents the YAML structure for BanyanDB queries
+type QueryYAML struct {
+ // Common fields
+ Name string `yaml:"name,omitempty"`
+ Groups []string `yaml:"groups,omitempty"`
+ TimeRange *TimeRangeYAML `yaml:"timeRange,omitempty"`
+ Criteria []map[string]interface{} `yaml:"criteria,omitempty"`
+ Limit *int `yaml:"limit,omitempty"`
+ Offset *int `yaml:"offset,omitempty"`
+ Trace bool `yaml:"trace,omitempty"`
+
+ // Stream/Trace specific
+ Projection []string `yaml:"projection,omitempty"`
+ OrderBy *OrderByYAML `yaml:"orderBy,omitempty"`
+
+ // Measure specific
+ TagProjection []string `yaml:"tagProjection,omitempty"`
+ FieldProjection []string `yaml:"fieldProjection,omitempty"`
+ GroupBy *GroupByYAML `yaml:"groupBy,omitempty"`
+ Agg *AggregationYAML `yaml:"agg,omitempty"`
+ Top *TopYAML `yaml:"top,omitempty"`
+
+ // Property specific
+ IDs []string `yaml:"ids,omitempty"`
+
+ // Top-N specific (for separate endpoint)
+ TopN int `yaml:"topN,omitempty"`
+ FieldValueSort string `yaml:"fieldValueSort,omitempty"`
+ Conditions []map[string]interface{} `yaml:"conditions,omitempty"`
+}
+
+// TimeRangeYAML represents time range in YAML
+type TimeRangeYAML struct {
+ Begin string `yaml:"begin"`
+ End string `yaml:"end"`
+}
+
+// OrderByYAML represents ORDER BY clause in YAML
+type OrderByYAML struct {
+ IndexRuleName string `yaml:"indexRuleName"`
+ Sort string `yaml:"sort"` // ASC or DESC
+}
+
+// GroupByYAML represents GROUP BY clause in YAML
+type GroupByYAML struct {
+ TagProjection []string `yaml:"tagProjection"`
+}
+
+// AggregationYAML represents aggregation function in YAML
+type AggregationYAML struct {
+ Function string `yaml:"function"`
+ FieldName string `yaml:"fieldName"`
+}
+
+// TopYAML represents TOP N clause in YAML
+type TopYAML struct {
+ Number int `yaml:"number"`
+ FieldName string `yaml:"fieldName"`
+ FieldValueSort string `yaml:"fieldValueSort"`
+}
+
+// Translator converts parsed BydbQL to YAML format
+type Translator struct {
+ context *QueryContext
+}
+
+// NewTranslator creates a new translator
+func NewTranslator(context *QueryContext) *Translator {
+ if context == nil {
+ context = &QueryContext{CurrentTime: time.Now()}
+ }
+ return &Translator{context: context}
+}
+
+// TranslateToYAML translates a parsed query to YAML format
+func (t *Translator) TranslateToYAML(query *ParsedQuery) ([]byte, error) {
+ yamlQuery, err := t.translateQuery(query)
+ if err != nil {
+ return nil, err
+ }
+
+ return yaml.Marshal(yamlQuery)
+}
+
+// TranslateToMap translates a parsed query to a map suitable for JSON/YAML conversion
+func (t *Translator) TranslateToMap(query *ParsedQuery) (map[string]interface{}, error) {
+ yamlQuery, err := t.translateQuery(query)
+ if err != nil {
+ return nil, err
+ }
+
+ yamlBytes, err := yaml.Marshal(yamlQuery)
+ if err != nil {
+ return nil, err
+ }
+
+ var result map[string]interface{}
+ err = yaml.Unmarshal(yamlBytes, &result)
+ return result, err
+}
+
+// translateQuery translates the main query structure
+func (t *Translator) translateQuery(query *ParsedQuery) (*QueryYAML, error) {
+ yamlQuery := &QueryYAML{}
+
+ // Set common fields from context or FROM clause
+ if query.ResourceName != "" {
+ yamlQuery.Name = query.ResourceName
+ } else if t.context.DefaultResourceName != "" {
+ yamlQuery.Name = t.context.DefaultResourceName
+ }
+
+ if len(query.Groups) > 0 {
+ yamlQuery.Groups = query.Groups
+ } else if t.context.DefaultGroup != "" {
+ yamlQuery.Groups = []string{t.context.DefaultGroup}
+ }
+
+ switch stmt := query.Statement.(type) {
+ case *SelectStatement:
+ return t.translateSelectStatement(stmt, yamlQuery, query)
+ case *TopNStatement:
+ return t.translateTopNStatement(stmt, yamlQuery, query)
+ default:
+ return nil, fmt.Errorf("unsupported statement type")
+ }
+}
+
+// translateSelectStatement translates SELECT statements
+func (t *Translator) translateSelectStatement(stmt *SelectStatement, yamlQuery *QueryYAML, query *ParsedQuery) (*QueryYAML, error) {
+ // Set query trace flag
+ yamlQuery.Trace = stmt.QueryTrace
+
+ // Translate TIME clause
+ if stmt.Time != nil {
+ timeRange, err := t.translateTimeCondition(stmt.Time)
+ if err != nil {
+ return nil, err
+ }
+ yamlQuery.TimeRange = timeRange
+ }
+
+ // Translate WHERE clause
+ if stmt.Where != nil {
+ criteria, err := t.translateWhereClause(stmt.Where)
+ if err != nil {
+ return nil, err
+ }
+ yamlQuery.Criteria = criteria
+ }
+
+ // Set LIMIT and OFFSET
+ yamlQuery.Limit = stmt.Limit
+ yamlQuery.Offset = stmt.Offset
+
+ // Translate based on resource type
+ resourceType := query.ResourceType
+ if resourceType == ResourceTypeAuto {
+ resourceType = t.context.DefaultResourceType
+ }
+
+ switch resourceType {
+ case ResourceTypeStream, ResourceTypeTrace:
+ return t.translateStreamOrTraceQuery(stmt, yamlQuery)
+ case ResourceTypeMeasure:
+ return t.translateMeasureQuery(stmt, yamlQuery)
+ case ResourceTypeProperty:
+ return t.translatePropertyQuery(stmt, yamlQuery)
+ default:
+ // Auto-detect or use default
+ return t.translateStreamOrTraceQuery(stmt, yamlQuery)
+ }
+}
+
+// translateStreamOrTraceQuery translates stream/trace specific fields
+func (t *Translator) translateStreamOrTraceQuery(stmt *SelectStatement, yamlQuery *QueryYAML) (*QueryYAML, error) {
+ // Translate projection
+ if stmt.Projection != nil {
+ if stmt.Projection.All {
+ // SELECT * - no specific projection needed
+ } else if stmt.Projection.Empty {
+ // SELECT () - empty projection for traces
+ yamlQuery.Projection = []string{}
+ } else if len(stmt.Projection.Columns) > 0 {
+ for _, col := range stmt.Projection.Columns {
+ yamlQuery.Projection = append(yamlQuery.Projection, col.Name)
+ }
+ }
+ }
+
+ // Translate ORDER BY
+ if stmt.OrderBy != nil {
+ yamlQuery.OrderBy = &OrderByYAML{
+ IndexRuleName: stmt.OrderBy.Column,
+ }
+ if stmt.OrderBy.Desc {
+ yamlQuery.OrderBy.Sort = "DESC"
+ } else {
+ yamlQuery.OrderBy.Sort = "ASC"
+ }
+ }
+
+ return yamlQuery, nil
+}
+
+// translateMeasureQuery translates measure specific fields
+func (t *Translator) translateMeasureQuery(stmt *SelectStatement, yamlQuery *QueryYAML) (*QueryYAML, error) {
+ // Translate projection
+ if stmt.Projection != nil {
+ if stmt.Projection.All {
+ // SELECT * - no specific projection needed
+ } else if stmt.Projection.TopN != nil {
+ // Handle TOP N projection
+ yamlQuery.Top = &TopYAML{
+ Number: stmt.Projection.TopN.N,
+ }
+ if stmt.OrderBy != nil {
+ if stmt.OrderBy.Desc {
+ yamlQuery.Top.FieldValueSort = "DESC"
+ } else {
+ yamlQuery.Top.FieldValueSort = "ASC"
+ }
+ }
+ } else if len(stmt.Projection.Columns) > 0 {
+ // Separate tags and fields based on column type or function
+ for _, col := range stmt.Projection.Columns {
+ if col.Function != nil {
+ // Aggregate function
+ yamlQuery.Agg = &AggregationYAML{
+ Function: col.Function.Function,
+ FieldName: col.Function.Column,
+ }
+ } else {
+ // Regular column - determine if tag or field
+ switch col.Type {
+ case ColumnTypeTag:
+ yamlQuery.TagProjection = append(yamlQuery.TagProjection, col.Name)
+ case ColumnTypeField:
+ yamlQuery.FieldProjection = append(yamlQuery.FieldProjection, col.Name)
+ default:
+ // Auto-detect: assume tag for now (schema lookup would be needed)
+ yamlQuery.TagProjection = append(yamlQuery.TagProjection, col.Name)
+ }
+ }
+ }
+ }
+ }
+
+ // Translate GROUP BY
+ if stmt.GroupBy != nil {
+ yamlQuery.GroupBy = &GroupByYAML{
+ TagProjection: stmt.GroupBy.Columns,
+ }
+ }
+
+ // Translate ORDER BY
+ if stmt.OrderBy != nil {
+ yamlQuery.OrderBy = &OrderByYAML{
+ IndexRuleName: stmt.OrderBy.Column,
+ }
+ if stmt.OrderBy.Desc {
+ yamlQuery.OrderBy.Sort = "DESC"
+ } else {
+ yamlQuery.OrderBy.Sort = "ASC"
+ }
+ }
+
+ return yamlQuery, nil
+}
+
+// translatePropertyQuery translates property specific fields
+func (t *Translator) translatePropertyQuery(stmt *SelectStatement, yamlQuery *QueryYAML) (*QueryYAML, error) {
+ // Translate projection
+ if stmt.Projection != nil && !stmt.Projection.All && len(stmt.Projection.Columns) > 0 {
+ for _, col := range stmt.Projection.Columns {
+ yamlQuery.Projection = append(yamlQuery.Projection, col.Name)
+ }
+ }
+
+ // Extract IDs from WHERE clause if present
+ if stmt.Where != nil {
+ for _, condition := range stmt.Where.Conditions {
+ if strings.ToUpper(condition.Left) == "ID" {
+ if condition.Operator == OpEqual && condition.Right != nil {
+ yamlQuery.IDs = []string{condition.Right.StringVal}
+ } else if condition.Operator == OpIn && len(condition.Values) > 0 {
+ for _, val := range condition.Values {
+ yamlQuery.IDs = append(yamlQuery.IDs, val.StringVal)
+ }
+ }
+ }
+ }
+ }
+
+ return yamlQuery, nil
+}
+
+// translateTopNStatement translates SHOW TOP N statements
+func (t *Translator) translateTopNStatement(stmt *TopNStatement, yamlQuery *QueryYAML, query *ParsedQuery) (*QueryYAML, error) {
+ // Set Top-N specific fields
+ yamlQuery.TopN = stmt.TopN
+ yamlQuery.Trace = stmt.QueryTrace
+
+ // Translate TIME clause
+ if stmt.Time != nil {
+ timeRange, err := t.translateTimeCondition(stmt.Time)
+ if err != nil {
+ return nil, err
+ }
+ yamlQuery.TimeRange = timeRange
+ }
+
+ // Translate WHERE clause to conditions (Top-N uses different field name)
+ if stmt.Where != nil {
+ conditions, err := t.translateWhereClause(stmt.Where)
+ if err != nil {
+ return nil, err
+ }
+ yamlQuery.Conditions = conditions
+ }
+
+ // Translate AGGREGATE BY
+ if stmt.AggregateBy != nil {
+ yamlQuery.Agg = &AggregationYAML{
+ Function: stmt.AggregateBy.Function,
+ FieldName: stmt.AggregateBy.Column,
+ }
+ }
+
+ // Translate ORDER BY
+ if stmt.OrderBy != nil {
+ if stmt.OrderBy.Desc {
+ yamlQuery.FieldValueSort = "DESC"
+ } else {
+ yamlQuery.FieldValueSort = "ASC"
+ }
+ }
+
+ return yamlQuery, nil
+}
+
+// translateTimeCondition translates TIME conditions to time range
+func (t *Translator) translateTimeCondition(timeCondition *TimeCondition) (*TimeRangeYAML, error) {
+ timeRange := &TimeRangeYAML{}
+
+ switch timeCondition.Operator {
+ case TimeOpEqual:
+ timestamp, err := t.parseTimestamp(timeCondition.Timestamp)
+ if err != nil {
+ return nil, err
+ }
+ timeRange.Begin = timestamp
+ timeRange.End = timestamp
+ case TimeOpGreater:
+ timestamp, err := t.parseTimestamp(timeCondition.Timestamp)
+ if err != nil {
+ return nil, err
+ }
+ timeRange.Begin = timestamp
+ // End time not specified, use current time
+ timeRange.End = t.context.CurrentTime.Format(time.RFC3339)
+ case TimeOpLess:
+ timestamp, err := t.parseTimestamp(timeCondition.Timestamp)
+ if err != nil {
+ return nil, err
+ }
+ // Begin time not specified, use a time far in the past
+ timeRange.Begin = "1970-01-01T00:00:00Z"
+ timeRange.End = timestamp
+ case TimeOpGreaterEqual:
+ timestamp, err := t.parseTimestamp(timeCondition.Timestamp)
+ if err != nil {
+ return nil, err
+ }
+ timeRange.Begin = timestamp
+ timeRange.End = t.context.CurrentTime.Format(time.RFC3339)
+ case TimeOpLessEqual:
+ timestamp, err := t.parseTimestamp(timeCondition.Timestamp)
+ if err != nil {
+ return nil, err
+ }
+ timeRange.Begin = "1970-01-01T00:00:00Z"
+ timeRange.End = timestamp
+ case TimeOpBetween:
+ begin, err := t.parseTimestamp(timeCondition.Begin)
+ if err != nil {
+ return nil, err
+ }
+ end, err := t.parseTimestamp(timeCondition.End)
+ if err != nil {
+ return nil, err
+ }
+ timeRange.Begin = begin
+ timeRange.End = end
+ }
+
+ return timeRange, nil
+}
+
+// parseTimestamp parses timestamp string (absolute or relative)
+func (t *Translator) parseTimestamp(timestamp string) (string, error) {
+ // Try parsing as absolute time first (RFC3339)
+ if parsedTime, err := time.Parse(time.RFC3339, timestamp); err == nil {
+ return parsedTime.Format(time.RFC3339), nil
+ }
+
+ // Try parsing as relative time (duration string)
+ if strings.ToLower(timestamp) == "now" {
+ return t.context.CurrentTime.Format(time.RFC3339), nil
+ }
+
+ duration, err := str2duration.ParseDuration(timestamp)
+ if err != nil {
+ return "", fmt.Errorf("invalid timestamp format: %s", timestamp)
+ }
+
+ resultTime := t.context.CurrentTime.Add(duration)
+ return resultTime.Format(time.RFC3339), nil
+}
+
+// translateWhereClause translates WHERE conditions to criteria format
+func (t *Translator) translateWhereClause(where *WhereClause) ([]map[string]interface{}, error) {
+ var criteria []map[string]interface{}
+
+ for _, condition := range where.Conditions {
+ criterion := make(map[string]interface{})
+
+ // Set tag name
+ criterion["tagName"] = 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)
+ }
+ }
+
+ criteria = append(criteria, criterion)
+ }
+
+ return criteria, nil
+}
+
+// translateValue translates a value to the appropriate YAML format
+func (t *Translator) translateValue(value *Value) interface{} {
+ if value.IsNull {
+ return nil
+ }
+
+ switch value.Type {
+ case ValueTypeString:
+ return map[string]interface{}{
+ "str": map[string]interface{}{
+ "value": value.StringVal,
+ },
+ }
+ case ValueTypeInteger:
+ return map[string]interface{}{
+ "int": map[string]interface{}{
+ "value": strconv.FormatInt(value.Integer, 10),
+ },
+ }
+ default:
+ return nil
+ }
+}
+
+// TranslateQuery is a convenience function to translate a BydbQL query string to YAML
+func TranslateQuery(query string, context *QueryContext) ([]byte, []string, error) {
+ parsed, errors := ParseQuery(query)
+ if len(errors) > 0 {
+ return nil, errors, fmt.Errorf("parsing errors occurred")
+ }
+
+ if parsed == nil {
+ return nil, []string{"failed to parse query"}, fmt.Errorf("parsing failed")
+ }
+
+ translator := NewTranslator(context)
+ yamlBytes, err := translator.TranslateToYAML(parsed)
+ if err != nil {
+ return nil, []string{err.Error()}, err
+ }
+
+ return yamlBytes, nil, nil
+}
\ No newline at end of file