You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by li...@apache.org on 2026/07/21 10:58:26 UTC

(skywalking-banyandb) 01/01: Allow `*` in the BydbQL identifier

This is an automated email from the ASF dual-hosted git repository.

mrproliu pushed a commit to branch ql-star-support
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 05438286ab1e1709127e11cb345c3e57db970cca
Author: mrproliu <74...@qq.com>
AuthorDate: Tue Jul 21 18:58:10 2026 +0800

    Allow `*` in the BydbQL identifier
---
 CHANGES.md                 |  1 +
 docs/interacting/bydbql.md | 28 +++++++++++++++++----
 pkg/bydbql/bydbql_test.go  | 63 ++++++++++++++++++++++++++++++++++++++++++++++
 pkg/bydbql/parser.go       |  7 +++++-
 4 files changed, 93 insertions(+), 6 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 95dbdbf6d..147d9ba83 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -138,6 +138,7 @@ Release Notes.
 - Deleting one TopN aggregation no longer tears down sibling aggregations on the same source measure.
 - Purge a deleted group's resource, index-rule and binding cache entries to avoid dangling references.
 - Clear a trace subject's index when its last index rule or binding is removed.
+- Allow `*` as a non-initial character in BydbQL identifiers, so resources named with `*` become queryable, not just writable.
 
 ### Document
 
diff --git a/docs/interacting/bydbql.md b/docs/interacting/bydbql.md
index a2ab16166..02cc21436 100644
--- a/docs/interacting/bydbql.md
+++ b/docs/interacting/bydbql.md
@@ -98,6 +98,24 @@ FROM STREAM mystream in group1   -- refers to stream "mystream"
 
 **Best Practice**: Use uppercase for reserved words and consistent casing for identifiers to maintain readability.
 
+#### Identifier Character Set
+
+An identifier starts with a letter or underscore and may continue with letters, digits, underscores, hyphens (`-`), and asterisks (`*`):
+
+```
+identifier ::= [a-zA-Z_][a-zA-Z0-9_*-]*
+```
+
+Hyphens and asterisks are allowed only after the first character. This matters because the schema layer accepts such names at creation time — a TopN rule named `endpoint_avg-cluster-excludes-*`, for instance — and a resource that can be created must remain referenceable from a query:
+
+```sql
+SHOW TOP 10 FROM MEASURE endpoint_avg-cluster-excludes-* IN default;
+```
+
+Confining `*` to non-initial positions is what keeps the star projection unambiguous: a bare `*` cannot begin an identifier, so `SELECT *` is always a projection.
+
+**Caveat**: an identifier that begins with a reserved word followed by a non-word character is rejected by the lexer — `IN group-a` fails because `group` matches the keyword rule first. Avoid naming resources with a reserved-word prefix.
+
 ## 2.5. Timestamp Formats
 
 BydbQL supports flexible timestamp specifications in TIME clauses, accommodating both absolute and relative time formats:
@@ -392,7 +410,7 @@ timestamp       ::= string_literal | integer_literal
 	/* timestamp supports both absolute and relative time formats:
 	   - Absolute: RFC3339 format like "2006-01-02T15:04:05Z07:00"
 	   - Relative: duration strings like "-30m", "2h", "1d" (relative to current time) */
-identifier      ::= [a-zA-Z_][a-zA-Z0-9_]*
+identifier      ::= [a-zA-Z_][a-zA-Z0-9_*-]*
 string_literal  ::= "'" [^']* "'" | "\"" [^\"]* "\""
 integer_literal ::= [0-9]+
 ```
@@ -530,7 +548,7 @@ timestamp         ::= string_literal | integer_literal
 	/* timestamp supports both absolute and relative time formats:
 	   - Absolute: RFC3339 format like "2006-01-02T15:04:05Z07:00"
 	   - Relative: duration strings like "-30m", "2h", "1d" (relative to current time) */
-identifier        ::= [a-zA-Z_][a-zA-Z0-9_]*
+identifier        ::= [a-zA-Z_][a-zA-Z0-9_*-]*
 string_literal    ::= "'" [^']* "'" | "\"" [^\"]* "\""
 integer_literal   ::= [0-9]+
 ```
@@ -705,7 +723,7 @@ timestamp          ::= string_literal | integer_literal
 	/* timestamp supports both absolute and relative time formats:
 	   - Absolute: RFC3339 format like "2006-01-02T15:04:05Z07:00"
 	   - Relative: duration strings like "-30m", "2h", "1d" (relative to current time) */
-identifier         ::= [a-zA-Z_][a-zA-Z0-9_]*
+identifier         ::= [a-zA-Z_][a-zA-Z0-9_*-]*
 string_literal     ::= "'" [^']* "'" | "\"" [^\"]* "\""
 integer_literal    ::= [0-9]+
 ```
@@ -825,7 +843,7 @@ condition           ::= identifier binary_op (value | value_list) | "ID" binary_
 binary_op           ::= "=" | "!=" | ">" | "<" | ">=" | "<=" | "IN" | "NOT IN"
 value               ::= string_literal | integer_literal | "NULL"
 value_list          ::= "(" value ("," value)* ")"
-identifier          ::= [a-zA-Z_][a-zA-Z0-9_]*
+identifier          ::= [a-zA-Z_][a-zA-Z0-9_*-]*
 string_literal      ::= "'" [^']* "'" | "\"" [^\"]* "\""
 integer_literal     ::= [0-9]+
 ```
@@ -912,7 +930,7 @@ timestamp             ::= string_literal | integer_literal
 	/* timestamp supports both absolute and relative time formats:
 	   - Absolute: RFC3339 format like "2006-01-02T15:04:05Z07:00"
 	   - Relative: duration strings like "-30m", "2h", "1d" (relative to current time) */
-identifier            ::= [a-zA-Z_][a-zA-Z0-9_]*
+identifier            ::= [a-zA-Z_][a-zA-Z0-9_*-]*
 string_literal        ::= "'" [^']* "'" | "\"" [^\"]* "\""
 integer_literal       ::= [0-9]+
 ```
diff --git a/pkg/bydbql/bydbql_test.go b/pkg/bydbql/bydbql_test.go
index 9578e0cf5..e29b5559c 100644
--- a/pkg/bydbql/bydbql_test.go
+++ b/pkg/bydbql/bydbql_test.go
@@ -3794,6 +3794,69 @@ var _ = Describe("Parser", func() {
 			})
 		})
 	})
+
+	Describe("Identifier Character Set", func() {
+		// The schema layer accepts resource names containing `-` and `*` at creation time, so
+		// the query layer must be able to name them; otherwise a resource can be written but
+		// never read. `*` is confined to non-initial positions to keep `SELECT *` unambiguous.
+		It("parses a measure name containing a star", func() {
+			grammar, err := ParseQuery("SHOW TOP 5 FROM MEASURE endpoint_avg-cluster-excludes-* IN default")
+			Expect(err).To(BeNil())
+			Expect(grammar).NotTo(BeNil())
+
+			stmt := grammar.TopN
+			Expect(stmt.From.ResourceName).To(Equal("endpoint_avg-cluster-excludes-*"))
+		})
+
+		It("parses a stream name containing a star", func() {
+			grammar, err := ParseQuery("SELECT * FROM STREAM sw-excludes-* IN default")
+			Expect(err).To(BeNil())
+
+			stmt := grammar.Select
+			Expect(stmt.From.ResourceName).To(Equal("sw-excludes-*"))
+		})
+
+		// A group name that begins with a reserved word (e.g. `group-a`) is rejected by the
+		// keyword rule regardless of the star, so this uses a non-reserved prefix.
+		It("parses a group name containing a star", func() {
+			grammar, err := ParseQuery("SELECT * FROM MEASURE m IN mygroup-*")
+			Expect(err).To(BeNil())
+
+			stmt := grammar.Select
+			Expect(stmt.From.In.Groups).To(Equal([]string{"mygroup-*"}))
+		})
+
+		It("keeps the star projection working alongside a starred resource name", func() {
+			grammar, err := ParseQuery("SELECT * FROM MEASURE endpoint_avg-cluster-excludes-* IN default")
+			Expect(err).To(BeNil())
+
+			stmt := grammar.Select
+			Expect(stmt.Projection.All).To(BeTrue())
+			Expect(stmt.From.ResourceName).To(Equal("endpoint_avg-cluster-excludes-*"))
+		})
+
+		It("still parses the bare star projection as a projection, not an identifier", func() {
+			grammar, err := ParseQuery("SELECT * FROM STREAM sw IN default")
+			Expect(err).To(BeNil())
+
+			stmt := grammar.Select
+			Expect(stmt.Projection.All).To(BeTrue())
+			Expect(stmt.Projection.Columns).To(BeEmpty())
+		})
+
+		It("rejects an identifier starting with a star", func() {
+			_, err := ParseQuery("SELECT * FROM MEASURE *bad IN default")
+			Expect(err).NotTo(BeNil())
+		})
+
+		It("parses a starred column reference in a condition", func() {
+			grammar, err := ParseQuery("SELECT * FROM MEASURE m IN default WHERE col-* = 'v'")
+			Expect(err).To(BeNil())
+
+			stmt := grammar.Select
+			Expect(stmt.Where).NotTo(BeNil())
+		})
+	})
 })
 
 func BenchmarkParser(b *testing.B) {
diff --git a/pkg/bydbql/parser.go b/pkg/bydbql/parser.go
index 35cbc8e15..66892e088 100644
--- a/pkg/bydbql/parser.go
+++ b/pkg/bydbql/parser.go
@@ -48,7 +48,12 @@ func init() {
 			Name:    "Keyword",
 			Pattern: fmt.Sprintf(`(?i)(%s)\b`, keywordStr),
 		},
-		{Name: "Ident", Pattern: `[a-zA-Z_][a-zA-Z0-9_-]*`},
+		// `-` and `*` are accepted as continuation characters, never as the first one, so that
+		// resource names the schema layer already accepts at creation time — e.g. a TopN rule
+		// named `endpoint_avg-cluster-excludes-*` — stay referenceable from a query. Confining
+		// `*` to non-initial positions is what keeps the `SELECT *` projection unambiguous: a
+		// bare `*` cannot begin an identifier, so it still lexes as an operator.
+		{Name: "Ident", Pattern: `[a-zA-Z_][a-zA-Z0-9_*-]*`},
 		{Name: "Int", Pattern: `[-+]?\d+`},
 		{Name: "String", Pattern: `'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"`},
 		{Name: "QuotedIdent", Pattern: `"[a-zA-Z_][a-zA-Z0-9_.]*"|'[a-zA-Z_][a-zA-Z0-9_.]*'`},