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 <[email protected]> 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_.]*'`},
