This is an automated email from the ASF dual-hosted git repository. mrproliu pushed a commit to branch prepared-ql in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 3d8ffa7baa9780b90ae00647cc749f80f1aeee4b Author: mrproliu <[email protected]> AuthorDate: Thu Jul 9 11:36:34 2026 +0800 Introduce reusable `Prepared` QL binding --- CHANGES.md | 1 + pkg/bydbql/binder.go | 157 ++++++++++----- pkg/bydbql/binder_equivalence_test.go | 79 +++++++- pkg/bydbql/binder_test.go | 2 +- pkg/bydbql/grammar.go | 34 ++-- pkg/bydbql/prepared.go | 239 ++++++++++++++++++++++ pkg/bydbql/prepared_internal_test.go | 221 ++++++++++++++++++++ pkg/bydbql/transformer.go | 368 +++++++++++++++++++++------------- 8 files changed, 900 insertions(+), 201 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 41467ba54..479889b94 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -78,6 +78,7 @@ Release Notes. - Add bounded plugin telemetry (meter + logger) to the trace-pipeline sampler SDK. A sampler plugin opts in by implementing `sdk.HostAware`; the engine calls `UseHost(h)` once per group before the first `Decide`, delivering a scoped `sdk.Host` the plugin may cache and use in `Decide`. The host enforces all resource limits: metric names are forced under the `banyandb_trace_pipeline_plugin_` prefix; `{group, plugin_name}` labels are always present; per-plugin cardinality is capped at 100 s [...] - Add a Claude/Codex plugin packaging the BanyanDB MCP server with a parse-only `validate_bydbql` tool (backed by the `mcp/tools/bydbql-parse` Go validator) and a BydbQL skill for read-only natural-language-to-BydbQL generation over STREAM/MEASURE/TRACE/PROPERTY resources. - Introduce positional parameter binding (`?` placeholders) into BydbQL to eliminate QL injection. +- Add reusable BydbQL binding: `Prepare` a query once, then `Bind` it many times without re-parsing or mutating the template. ### Bug Fixes diff --git a/pkg/bydbql/binder.go b/pkg/bydbql/binder.go index 4b63c29fe..3d57620cd 100644 --- a/pkg/bydbql/binder.go +++ b/pkg/bydbql/binder.go @@ -34,8 +34,8 @@ import ( // // Binding mutates the grammar in place, so a grammar binds exactly once: rebinding is // rejected and a failed bind leaves the grammar partially bound and unusable — parse the -// query again in both cases. Parse-once/bind-many reuse requires an immutable binding -// design and is tracked as a follow-up. +// query again in both cases. For parse-once/bind-many reuse, use Prepare and +// (*PreparedStatement).Bind, which never mutate the parsed template. func BindParams(g *Grammar, params []*modelv1.TagValue) error { if g.paramsBound { return fmt.Errorf("grammar is already bound; parse the query again to bind new parameters") @@ -159,15 +159,11 @@ func (b *binder) collectIntSlot(value *int, param *bool, maxValue int64) { return } b.slots = append(b.slots, func(p *modelv1.TagValue) error { - intVal, ok := p.Value.(*modelv1.TagValue_Int) - if !ok { - return fmt.Errorf("this position only accepts int parameters, got %T", p.Value) - } - bound := intVal.Int.GetValue() - if err := validateCountValue("int parameter", bound, maxValue); err != nil { + bound, err := resolveCountParam(p, maxValue) + if err != nil { return err } - *value = int(bound) + *value = bound *param = false return nil }) @@ -287,31 +283,17 @@ func (b *binder) collectValueList(values []*GrammarValue, assign func([]*Grammar } func (b *binder) bindListValue(v *GrammarValue, p *modelv1.TagValue) error { - switch val := p.Value.(type) { + switch p.Value.(type) { case *modelv1.TagValue_Str, *modelv1.TagValue_Int, *modelv1.TagValue_Null: return bindScalarValue(v, p) - case *modelv1.TagValue_StrArray: - if len(val.StrArray.GetValue()) == 0 { - return fmt.Errorf("array parameter must not be empty") - } - expanded := make([]*GrammarValue, 0, len(val.StrArray.GetValue())) - for _, s := range val.StrArray.GetValue() { - expanded = append(expanded, &GrammarValue{String: &s}) - } - b.recordExpansion(v, expanded) - case *modelv1.TagValue_IntArray: - if len(val.IntArray.GetValue()) == 0 { - return fmt.Errorf("array parameter must not be empty") - } - expanded := make([]*GrammarValue, 0, len(val.IntArray.GetValue())) - for _, i := range val.IntArray.GetValue() { - expanded = append(expanded, &GrammarValue{Integer: &i}) + default: + expanded, err := resolveArrayElements(p) + if err != nil { + return err } b.recordExpansion(v, expanded) - default: - return fmt.Errorf("value list only accepts str, int, null, str_array, or int_array parameters, got %T", p.Value) + return nil } - return nil } func (b *binder) recordExpansion(v *GrammarValue, expanded []*GrammarValue) { @@ -336,40 +318,115 @@ func (b *binder) expandLists() { } func bindTimeValue(v *GrammarTimeValue, p *modelv1.TagValue) error { + strVal, err := resolveTimeParam(p) + if err != nil { + return err + } + v.String = &strVal + v.Param = false + return nil +} + +func bindScalarValue(v *GrammarValue, p *modelv1.TagValue) error { + resolved, err := resolveScalarParam(p) + if err != nil { + return err + } + v.String, v.Integer, v.Null = resolved.String, resolved.Integer, resolved.Null + v.Param = false + return nil +} + +// resolveScalarParam produces a fresh literal value node for a str/int/null +// parameter. It is the single source of scalar type acceptance, shared by the +// in-place bind path and the reusable Bind path. +func resolveScalarParam(p *modelv1.TagValue) (*GrammarValue, error) { switch val := p.Value.(type) { case *modelv1.TagValue_Str: strVal := val.Str.GetValue() - v.String = &strVal + return &GrammarValue{String: &strVal}, nil + case *modelv1.TagValue_Int: + intVal := val.Int.GetValue() + return &GrammarValue{Integer: &intVal}, nil + case *modelv1.TagValue_Null: + return &GrammarValue{Null: true}, nil + default: + return nil, fmt.Errorf("this position only accepts str, int, or null parameters, got %T", p.Value) + } +} + +// resolveTimeParam produces the time string for a str/timestamp parameter. int +// is rejected: the transformer cannot parse a bare integer as a timestamp, so +// accepting it would only defer a certain failure. +func resolveTimeParam(p *modelv1.TagValue) (string, error) { + switch val := p.Value.(type) { + case *modelv1.TagValue_Str: + return val.Str.GetValue(), nil case *modelv1.TagValue_Timestamp: // A nil inner timestamp would silently decode as the Unix epoch and an // out-of-range one would format as a nonsense year; reject both here. if err := val.Timestamp.CheckValid(); err != nil { - return fmt.Errorf("invalid timestamp parameter: %w", err) + return "", fmt.Errorf("invalid timestamp parameter: %w", err) } - strVal := val.Timestamp.AsTime().Format(time.RFC3339Nano) - v.String = &strVal + return val.Timestamp.AsTime().Format(time.RFC3339Nano), nil default: - // int is rejected as well: the transformer cannot parse a bare integer - // as a timestamp, so accepting it would only defer a certain failure. - return fmt.Errorf("time clause only accepts str or timestamp parameters, got %T", p.Value) + return "", fmt.Errorf("time clause only accepts str or timestamp parameters, got %T", p.Value) } - v.Param = false - return nil } -func bindScalarValue(v *GrammarValue, p *modelv1.TagValue) error { +// resolveArrayElements produces the literal nodes a str_array/int_array +// parameter expands to, rejecting empty arrays. +func resolveArrayElements(p *modelv1.TagValue) ([]*GrammarValue, error) { switch val := p.Value.(type) { - case *modelv1.TagValue_Str: - strVal := val.Str.GetValue() - v.String = &strVal - case *modelv1.TagValue_Int: - intVal := val.Int.GetValue() - v.Integer = &intVal - case *modelv1.TagValue_Null: - v.Null = true + case *modelv1.TagValue_StrArray: + if len(val.StrArray.GetValue()) == 0 { + return nil, fmt.Errorf("array parameter must not be empty") + } + expanded := make([]*GrammarValue, 0, len(val.StrArray.GetValue())) + for _, s := range val.StrArray.GetValue() { + expanded = append(expanded, &GrammarValue{String: &s}) + } + return expanded, nil + case *modelv1.TagValue_IntArray: + if len(val.IntArray.GetValue()) == 0 { + return nil, fmt.Errorf("array parameter must not be empty") + } + expanded := make([]*GrammarValue, 0, len(val.IntArray.GetValue())) + for _, i := range val.IntArray.GetValue() { + expanded = append(expanded, &GrammarValue{Integer: &i}) + } + return expanded, nil default: - return fmt.Errorf("this position only accepts str, int, or null parameters, got %T", p.Value) + return nil, fmt.Errorf("value list only accepts str, int, null, str_array, or int_array parameters, got %T", p.Value) } - v.Param = false - return nil +} + +// resolveListParam produces the value node(s) an IN/MATCH/HAVING placeholder +// resolves to: one for a scalar, N for an array expanded in place. It mirrors +// bindListValue's scalar-vs-array split so both paths accept the same types. +func resolveListParam(p *modelv1.TagValue) ([]*GrammarValue, error) { + switch p.Value.(type) { + case *modelv1.TagValue_Str, *modelv1.TagValue_Int, *modelv1.TagValue_Null: + v, err := resolveScalarParam(p) + if err != nil { + return nil, err + } + return []*GrammarValue{v}, nil + default: + return resolveArrayElements(p) + } +} + +// resolveCountParam produces the int for a LIMIT/OFFSET/TOP N parameter within +// that position's upper bound. +func resolveCountParam(p *modelv1.TagValue, maxValue int64) (int, error) { + intVal, ok := p.Value.(*modelv1.TagValue_Int) + if !ok { + return 0, fmt.Errorf("this position only accepts int parameters, got %T", p.Value) + } + bound := intVal.Int.GetValue() + if err := validateCountValue("int parameter", bound, maxValue); err != nil { + return 0, err + } + return int(bound), nil } diff --git a/pkg/bydbql/binder_equivalence_test.go b/pkg/bydbql/binder_equivalence_test.go index 3551ca0a4..0a9abf41e 100644 --- a/pkg/bydbql/binder_equivalence_test.go +++ b/pkg/bydbql/binder_equivalence_test.go @@ -19,6 +19,8 @@ package bydbql_test import ( "context" + "fmt" + "sync" "time" "github.com/alecthomas/participle/v2/lexer" @@ -329,6 +331,20 @@ func equivalenceCases() []equivalenceCase { params: []*modelv1.TagValue{intParam(5), strParam("-1h"), strParam("svc")}, ignoreTimeRange: true, }, + // PROPERTY query: the id-extraction path resolves placeholders separately + // from the criteria path, so it needs its own coverage. + { + name: "str param in PROPERTY id =", + parameterized: "SELECT env FROM PROPERTY sw_prop IN default WHERE id = ?", + literal: "SELECT env FROM PROPERTY sw_prop IN default WHERE id = 'k1'", + params: []*modelv1.TagValue{strParam("k1")}, + }, + { + name: "str_array param expands in PROPERTY id IN with a criteria param", + parameterized: "SELECT env FROM PROPERTY sw_prop IN default WHERE id IN (?) AND env = ?", + literal: "SELECT env FROM PROPERTY sw_prop IN default WHERE id IN ('k1', 'k2') AND env = 'prod'", + params: []*modelv1.TagValue{strArrayParam("k1", "k2"), strParam("prod")}, + }, } } @@ -353,10 +369,16 @@ var _ = Describe("BindParams equivalence with literal queries", func() { }}, Fields: []*databasev1.FieldSpec{{Name: "value", FieldType: databasev1.FieldType_FIELD_TYPE_INT}}, }, nil).AnyTimes() + propertyRegistry := schema.NewMockProperty(ctrl) + propertyRegistry.EXPECT().GetProperty(gomock.Any(), gomock.Any()).Return(&databasev1.Property{ + Metadata: &commonv1.Metadata{Name: "sw_prop", Group: "default"}, + Tags: []*databasev1.TagSpec{{Name: "env", Type: databasev1.TagType_TAG_TYPE_STRING}}, + }, nil).AnyTimes() mockRepo := metadata.NewMockRepo(ctrl) mockRepo.EXPECT().StreamRegistry().AnyTimes().Return(streamRegistry) mockRepo.EXPECT().TopNAggregationRegistry().AnyTimes().Return(topNRegistry) mockRepo.EXPECT().MeasureRegistry().AnyTimes().Return(measureRegistry) + mockRepo.EXPECT().PropertyRegistry().AnyTimes().Return(propertyRegistry) transformer = NewTransformer(mockRepo) }) @@ -406,19 +428,31 @@ var _ = Describe("BindParams equivalence with literal queries", func() { astDiff := cmp.Diff(literalGrammar, boundGrammar, cmpopts.IgnoreTypes(lexer.Position{}), cmpopts.IgnoreUnexported(Grammar{})) Expect(astDiff).To(BeEmpty(), "AST mismatch:\n%s", astDiff) - // Both must transform to the same native query request + // Both the in-place bind and the reusable Prepare/Bind path must + // transform to the same native request as the literal query. ctx := context.Background() literalResult, literalErr := transformer.Transform(ctx, literalGrammar) boundResult, boundErr := transformer.Transform(ctx, boundGrammar) + + prepared, prepErr := Prepare(testCase.parameterized) + Expect(prepErr).To(BeNil()) + bq, bindErr := prepared.Bind(testCase.params) + Expect(bindErr).To(BeNil()) + preparedResult, preparedErr := transformer.TransformBound(ctx, bq) + if literalErr != nil { Expect(testCase.expectError).To(BeTrue(), "unexpected transform error: %v", literalErr) Expect(boundErr).To(HaveOccurred()) Expect(boundErr.Error()).To(Equal(literalErr.Error())) + Expect(preparedErr).To(HaveOccurred()) + Expect(preparedErr.Error()).To(Equal(literalErr.Error())) return } Expect(testCase.expectError).To(BeFalse(), "expected a transform error but both sides succeeded") Expect(boundErr).To(BeNil()) + Expect(preparedErr).To(BeNil()) Expect(boundResult.Type).To(Equal(literalResult.Type)) + Expect(preparedResult.Type).To(Equal(literalResult.Type)) // SELECT * projections are built from a map, so their tag order is // nondeterministic per Transform call; sort them before comparing. opts := []cmp.Option{ @@ -430,8 +464,47 @@ var _ = Describe("BindParams equivalence with literal queries", func() { protocmp.IgnoreFields(&streamv1.QueryRequest{}, "time_range"), protocmp.IgnoreFields(&measurev1.TopNRequest{}, "time_range")) } - requestDiff := cmp.Diff(literalResult.QueryRequest, boundResult.QueryRequest, opts...) - Expect(requestDiff).To(BeEmpty(), "query request mismatch:\n%s", requestDiff) + Expect(cmp.Diff(literalResult.QueryRequest, boundResult.QueryRequest, opts...)). + To(BeEmpty(), "in-place bind request mismatch") + Expect(cmp.Diff(literalResult.QueryRequest, preparedResult.QueryRequest, opts...)). + To(BeEmpty(), "prepared bind request mismatch") }) } + + It("reuses one prepared statement concurrently without cross-talk", func() { + prepared, err := Prepare("SELECT * FROM STREAM sw IN default WHERE service_id = ?") + Expect(err).To(BeNil()) + const workers = 32 + var wg sync.WaitGroup + errs := make(chan error, workers) + for i := 0; i < workers; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + want := fmt.Sprintf("svc-%d", n) + bq, bindErr := prepared.Bind([]*modelv1.TagValue{strParam(want)}) + if bindErr != nil { + errs <- bindErr + return + } + result, transformErr := transformer.TransformBound(context.Background(), bq) + if transformErr != nil { + errs <- transformErr + return + } + got := result.QueryRequest.(*streamv1.QueryRequest).Criteria.GetCondition().GetValue().GetStr().GetValue() + if got != want { + errs <- fmt.Errorf("worker %d: got %q, want %q", n, got, want) + } + }(i) + } + wg.Wait() + close(errs) + for e := range errs { + Expect(e).NotTo(HaveOccurred()) + } + // Under -race, correct per-worker results also prove the shared template + // is never mutated during concurrent binds. TestBindTemplateStaysImmutable + // asserts the template fields directly. + }) }) diff --git a/pkg/bydbql/binder_test.go b/pkg/bydbql/binder_test.go index b3f4a7646..e6845d4ca 100644 --- a/pkg/bydbql/binder_test.go +++ b/pkg/bydbql/binder_test.go @@ -377,7 +377,7 @@ var _ = Describe("BindParams", func() { } walk(reflect.TypeOf(Grammar{})) Expect(found).To(Equal(expected), - "grammar @Param positions changed: wire any new position into binder.collect, the acceptance matrix, and this list") + "grammar @Param positions changed: wire any new position into binder.collect, preparer.walkGrammar, the acceptance matrix, and this list") }) }) diff --git a/pkg/bydbql/grammar.go b/pkg/bydbql/grammar.go index 7062461a5..e0022a139 100644 --- a/pkg/bydbql/grammar.go +++ b/pkg/bydbql/grammar.go @@ -58,10 +58,11 @@ type GrammarSelectStatement struct { // GrammarTopNStatement represents a SHOW TOP N statement. type GrammarTopNStatement struct { Pos lexer.Position - Show string `parser:"@'SHOW'"` - Top string `parser:"@'TOP'"` - N int `parser:"( @Int"` - NParam bool `parser:"| @Param )"` + Show string `parser:"@'SHOW'"` + Top string `parser:"@'TOP'"` + N int `parser:"( @Int"` + NParam bool `parser:"| @Param )"` + NParamIndex int From *GrammarFromClause `parser:"@@"` Time *GrammarTimeClause `parser:"@@?"` Where *GrammarTopNWhereClause `parser:"@@?"` @@ -80,8 +81,9 @@ type GrammarProjection struct { // GrammarTopNProjection represents TOP N projection. type GrammarTopNProjection struct { - N int `parser:"( @Int"` - NParam bool `parser:"| @Param )"` + N int `parser:"( @Int"` + NParam bool `parser:"| @Param )"` + NParamIndex int OrderField *GrammarIdentifierPath `parser:"@@"` Direction *string `parser:"@('ASC'|'DESC')?"` OtherColumns []*GrammarColumn `parser:" ( ',' @@ ( ',' @@ )* )?"` @@ -152,6 +154,9 @@ type GrammarTimeValue struct { String *string `parser:" @String"` Integer *int64 `parser:"| @Int"` Param bool `parser:"| @Param"` + // ParamIndex is the placeholder's positional index, assigned by Prepare and + // meaningful only when Param is true. It keys the per-request bound overlay. + ParamIndex int } // GrammarSelectWhereClause represents WHERE clause. @@ -260,6 +265,9 @@ type GrammarValue struct { Integer *int64 `parser:"| @Int"` Null bool `parser:"| @'NULL'"` Param bool `parser:"| @Param"` + // ParamIndex is the placeholder's positional index, assigned by Prepare and + // meaningful only when Param is true. It keys the per-request bound overlay. + ParamIndex int } // GrammarIdentifierPart Can be either an Ident or a Keyword (keywords are allowed in paths, but not as standalone identifiers). @@ -327,16 +335,18 @@ type GrammarOrderByWithIdent struct { // GrammarLimitClause represents LIMIT clause. type GrammarLimitClause struct { - Limit string `parser:"@'LIMIT'"` - Value int `parser:"( @Int"` - Param bool `parser:"| @Param )"` + Limit string `parser:"@'LIMIT'"` + Value int `parser:"( @Int"` + Param bool `parser:"| @Param )"` + ParamIndex int } // GrammarOffsetClause represents OFFSET clause. type GrammarOffsetClause struct { - Offset string `parser:"@'OFFSET'"` - Value int `parser:"( @Int"` - Param bool `parser:"| @Param )"` + Offset string `parser:"@'OFFSET'"` + Value int `parser:"( @Int"` + Param bool `parser:"| @Param )"` + ParamIndex int } // GrammarWithTraceClause represents WITH QUERY_TRACE clause. diff --git a/pkg/bydbql/prepared.go b/pkg/bydbql/prepared.go new file mode 100644 index 000000000..92e0aaa5f --- /dev/null +++ b/pkg/bydbql/prepared.go @@ -0,0 +1,239 @@ +// 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" + "math" + + modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1" +) + +// placeholderKind classifies a `?` placeholder by the position it occupies, +// which determines the parameter types Bind accepts and how Transform reads it. +type placeholderKind uint8 + +const ( + // phScalar is a WHERE/HAVING comparison value: str, int, or null. + phScalar placeholderKind = iota + // phList is an element of an IN/MATCH/HAVING value list: scalar or an array + // that expands in place. + phList + // phTime is a TIME value: str or timestamp. + phTime + // phCount is a LIMIT/OFFSET/TOP N count: an int within maxCount. + phCount +) + +// placeholderSpec describes one placeholder, recorded once by Prepare and read +// per request by Bind (to validate the parameter) and Transform (to read it). +type placeholderSpec struct { + maxCount int64 // upper bound for phCount positions + kind placeholderKind +} + +// PreparedStatement is an immutable, reusable parsed query. It is safe to cache +// and to Bind concurrently: Prepare numbers the placeholders once, and Bind +// never mutates the template — the bound values live in a per-request overlay. +type PreparedStatement struct { + template *Grammar + specs []placeholderSpec +} + +// Prepare parses a query and numbers its `?` placeholders so it can be bound +// repeatedly with different parameters without re-parsing. +func Prepare(query string) (*PreparedStatement, error) { + g, err := ParseQuery(query) + if err != nil { + return nil, err + } + p := &preparer{} + p.walkGrammar(g) + return &PreparedStatement{template: g, specs: p.specs}, nil +} + +// resolvedParam holds one placeholder's bound value in the per-request overlay. +// Which field is populated follows the placeholder's kind: a scalar/list +// position fills values (one node, or several for an expanded array), a TIME +// position fills timeStr, and a count position fills count. +type resolvedParam struct { + timeStr string + values []*GrammarValue + count int +} + +// BoundQuery pairs an immutable prepared statement with the values bound for one +// request. Its values are the per-request overlay indexed by placeholder +// position, allocated by Bind and never shared or mutated after. Pass it to +// Transformer.TransformBound. +type BoundQuery struct { + stmt *PreparedStatement + values []resolvedParam +} + +// Bind resolves params against the statement's placeholders and returns a +// per-request BoundQuery, leaving the template untouched, so a statement may be +// bound repeatedly and concurrently. Parameter positions in errors are 1-based, +// matching BindParams. +func (ps *PreparedStatement) Bind(params []*modelv1.TagValue) (*BoundQuery, error) { + if len(params) != len(ps.specs) { + return nil, fmt.Errorf("parameter count mismatch: query contains %d placeholder(s) but %d parameter(s) provided", len(ps.specs), len(params)) + } + values := make([]resolvedParam, len(ps.specs)) + for i, spec := range ps.specs { + p := params[i] + if p.GetValue() == nil { + return nil, fmt.Errorf("parameter #%d has no value", i+1) + } + var err error + switch spec.kind { + case phScalar: + var v *GrammarValue + v, err = resolveScalarParam(p) + if err == nil { + values[i].values = []*GrammarValue{v} + } + case phList: + values[i].values, err = resolveListParam(p) + case phTime: + values[i].timeStr, err = resolveTimeParam(p) + case phCount: + values[i].count, err = resolveCountParam(p, spec.maxCount) + } + if err != nil { + return nil, fmt.Errorf("failed to bind parameter #%d: %w", i+1, err) + } + } + return &BoundQuery{stmt: ps, values: values}, nil +} + +// preparer walks a parsed grammar in textual order, assigning each placeholder +// its positional index and recording its spec. The traversal mirrors +// binder.collect; the reflection tripwire test asserts both cover every +// placeholder-bearing grammar field. +type preparer struct { + specs []placeholderSpec +} + +func (p *preparer) add(kind placeholderKind, maxCount int64) int { + idx := len(p.specs) + p.specs = append(p.specs, placeholderSpec{kind: kind, maxCount: maxCount}) + return idx +} + +func (p *preparer) walkGrammar(g *Grammar) { + if g.Select != nil { + if g.Select.Projection != nil && g.Select.Projection.TopN != nil { + if topN := g.Select.Projection.TopN; topN.NParam { + topN.NParamIndex = p.add(phCount, math.MaxInt32) + } + } + p.walkTime(g.Select.Time) + if g.Select.Where != nil { + p.walkOrExpr(g.Select.Where.Expr) + } + if g.Select.Limit != nil && g.Select.Limit.Param { + g.Select.Limit.ParamIndex = p.add(phCount, math.MaxUint32) + } + if g.Select.Offset != nil && g.Select.Offset.Param { + g.Select.Offset.ParamIndex = p.add(phCount, math.MaxUint32) + } + } + if g.TopN != nil { + if g.TopN.NParam { + g.TopN.NParamIndex = p.add(phCount, math.MaxInt32) + } + p.walkTime(g.TopN.Time) + if g.TopN.Where != nil { + p.walkAndExpr(g.TopN.Where.Expr) + } + } +} + +func (p *preparer) walkTime(clause *GrammarTimeClause) { + if clause == nil { + return + } + p.walkTimeValue(clause.Value) + if clause.Between != nil { + p.walkTimeValue(clause.Between.Begin) + p.walkTimeValue(clause.Between.End) + } +} + +func (p *preparer) walkTimeValue(v *GrammarTimeValue) { + if v != nil && v.Param { + v.ParamIndex = p.add(phTime, 0) + } +} + +func (p *preparer) walkOrExpr(expr *GrammarOrExpr) { + if expr == nil { + return + } + p.walkAndExpr(expr.Left) + for _, right := range expr.Right { + p.walkAndExpr(right.Right) + } +} + +func (p *preparer) walkAndExpr(expr *GrammarAndExpr) { + if expr == nil { + return + } + p.walkPredicate(expr.Left) + for _, right := range expr.Right { + p.walkPredicate(right.Right) + } +} + +func (p *preparer) walkPredicate(pred *GrammarPredicate) { + if pred == nil { + return + } + switch { + case pred.Paren != nil: + p.walkOrExpr(pred.Paren) + case pred.Binary != nil && pred.Binary.Tail != nil: + if compare := pred.Binary.Tail.Compare; compare != nil && compare.Value != nil && compare.Value.Param { + compare.Value.ParamIndex = p.add(phScalar, 0) + } + if match := pred.Binary.Tail.Match; match != nil && match.Values != nil { + p.walkValueList(match.Values.Single, match.Values.Array) + } + case pred.In != nil: + p.walkValueList(nil, pred.In.Values) + case pred.Having != nil && pred.Having.Values != nil: + p.walkValueList(pred.Having.Values.Single, pred.Having.Values.Array) + } +} + +// walkValueList numbers the placeholders in a MATCH/HAVING single-or-array or an +// IN list. A container is parsed as either a single value or an array; both are +// walked the same way here. +func (p *preparer) walkValueList(single *GrammarValue, array []*GrammarValue) { + values := array + if single != nil { + values = []*GrammarValue{single} + } + for _, v := range values { + if v != nil && v.Param { + v.ParamIndex = p.add(phList, 0) + } + } +} diff --git a/pkg/bydbql/prepared_internal_test.go b/pkg/bydbql/prepared_internal_test.go new file mode 100644 index 000000000..00339ae67 --- /dev/null +++ b/pkg/bydbql/prepared_internal_test.go @@ -0,0 +1,221 @@ +// 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 ( + "math" + "strings" + "testing" + + modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1" +) + +func tvStr(v string) *modelv1.TagValue { + return &modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: v}}} +} + +func tvInt(v int64) *modelv1.TagValue { + return &modelv1.TagValue{Value: &modelv1.TagValue_Int{Int: &modelv1.Int{Value: v}}} +} + +func tvStrArray(vs ...string) *modelv1.TagValue { + return &modelv1.TagValue{Value: &modelv1.TagValue_StrArray{StrArray: &modelv1.StrArray{Value: vs}}} +} + +func TestPrepareNumbersPlaceholdersInTextOrder(t *testing.T) { + ps, err := Prepare("SELECT * FROM STREAM sw IN default TIME > ? WHERE service_id = ? AND codes IN (?) LIMIT ? OFFSET ?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + if got := len(ps.specs); got != 5 { + t.Fatalf("placeholder count = %d, want 5", got) + } + wantSpecs := []placeholderSpec{ + {kind: phTime}, + {kind: phScalar}, + {kind: phList}, + {kind: phCount, maxCount: math.MaxUint32}, + {kind: phCount, maxCount: math.MaxUint32}, + } + for i, want := range wantSpecs { + if ps.specs[i] != want { + t.Errorf("specs[%d] = %+v, want %+v", i, ps.specs[i], want) + } + } + sel := ps.template.Select + if idx := sel.Time.Value.ParamIndex; idx != 0 { + t.Errorf("TIME ParamIndex = %d, want 0", idx) + } + if idx := sel.Where.Expr.Left.Left.Binary.Tail.Compare.Value.ParamIndex; idx != 1 { + t.Errorf("scalar ParamIndex = %d, want 1", idx) + } + if idx := sel.Where.Expr.Left.Right[0].Right.In.Values[0].ParamIndex; idx != 2 { + t.Errorf("IN element ParamIndex = %d, want 2", idx) + } + if idx := sel.Limit.ParamIndex; idx != 3 { + t.Errorf("LIMIT ParamIndex = %d, want 3", idx) + } + if idx := sel.Offset.ParamIndex; idx != 4 { + t.Errorf("OFFSET ParamIndex = %d, want 4", idx) + } +} + +func TestPrepareNumbersShowTopCount(t *testing.T) { + ps, err := Prepare("SHOW TOP ? FROM MEASURE m IN default TIME > ? WHERE service = ?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + wantSpecs := []placeholderSpec{ + {kind: phCount, maxCount: math.MaxInt32}, + {kind: phTime}, + {kind: phScalar}, + } + if len(ps.specs) != len(wantSpecs) { + t.Fatalf("specs len = %d, want %d", len(ps.specs), len(wantSpecs)) + } + for i, want := range wantSpecs { + if ps.specs[i] != want { + t.Errorf("specs[%d] = %+v, want %+v", i, ps.specs[i], want) + } + } + if idx := ps.template.TopN.NParamIndex; idx != 0 { + t.Errorf("SHOW TOP NParamIndex = %d, want 0", idx) + } +} + +func TestPrepareLiteralQueryHasNoPlaceholders(t *testing.T) { + ps, err := Prepare("SELECT * FROM STREAM sw IN default WHERE service_id = 'webapp'") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + if got := len(ps.specs); got != 0 { + t.Fatalf("placeholder count = %d, want 0", got) + } +} + +func TestPrepareCoversSamePlaceholdersAsBinder(t *testing.T) { + // Prepare and binder.collect are parallel traversals; a query exercising + // every placeholder position must yield the same count from both, or one + // walk has drifted from the other. + query := "SELECT TOP ? f FROM MEASURE m IN default TIME BETWEEN ? AND ? " + + "WHERE a = ? AND b IN (?) AND c MATCH(?) AND d HAVING (?) LIMIT ? OFFSET ?" + ps, err := Prepare(query) + if err != nil { + t.Fatalf("Prepare: %v", err) + } + g, errs := ParseQuery(query) + if errs != nil { + t.Fatalf("ParseQuery: %v", errs) + } + if got, want := len(ps.specs), countUnboundParams(g); got != want { + t.Fatalf("Prepare counted %d placeholders, binder counted %d", got, want) + } +} + +func TestBindResolvesEachKind(t *testing.T) { + ps, err := Prepare("SELECT * FROM STREAM sw IN default TIME > ? WHERE service_id = ? AND codes IN (?) LIMIT ?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + bound, err := ps.Bind([]*modelv1.TagValue{tvStr("-30m"), tvStr("webapp"), tvStrArray("a", "b"), tvInt(10)}) + if err != nil { + t.Fatalf("Bind: %v", err) + } + p := bound.values + if p[0].timeStr != "-30m" { + t.Errorf("time = %q, want -30m", p[0].timeStr) + } + if len(p[1].values) != 1 || *p[1].values[0].String != "webapp" { + t.Errorf("scalar = %+v, want [webapp]", p[1].values) + } + if len(p[2].values) != 2 || *p[2].values[0].String != "a" || *p[2].values[1].String != "b" { + t.Errorf("list = %+v, want [a b]", p[2].values) + } + if p[3].count != 10 { + t.Errorf("count = %d, want 10", p[3].count) + } +} + +func TestBindTemplateStaysImmutable(t *testing.T) { + ps, err := Prepare("SELECT * FROM STREAM sw IN default WHERE service_id = ?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + if _, err = ps.Bind([]*modelv1.TagValue{tvStr("first")}); err != nil { + t.Fatalf("Bind: %v", err) + } + node := ps.template.Select.Where.Expr.Left.Left.Binary.Tail.Compare.Value + if !node.Param || node.String != nil { + t.Fatalf("template mutated by Bind: Param=%v String=%v", node.Param, node.String) + } + // A second bind with a different value must succeed on the same template. + bound, err := ps.Bind([]*modelv1.TagValue{tvStr("second")}) + if err != nil { + t.Fatalf("second Bind: %v", err) + } + if *bound.values[0].values[0].String != "second" { + t.Errorf("second bind value = %q, want second", *bound.values[0].values[0].String) + } +} + +func TestBindErrors(t *testing.T) { + ps, err := Prepare("SELECT * FROM STREAM sw IN default WHERE service_id = ?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + if _, err = ps.Bind(nil); err == nil || !strings.Contains(err.Error(), "count mismatch") { + t.Errorf("count mismatch: got %v", err) + } + if _, err = ps.Bind([]*modelv1.TagValue{tvStrArray("a", "b")}); err == nil || !strings.Contains(err.Error(), "parameter #1") { + t.Errorf("array in scalar: got %v", err) + } + if _, err = ps.Bind([]*modelv1.TagValue{{}}); err == nil || !strings.Contains(err.Error(), "has no value") { + t.Errorf("nil value: got %v", err) + } +} + +// BenchmarkBinding contrasts re-parsing per request with parse-once/bind-many. +func BenchmarkBinding(b *testing.B) { + const query = "SELECT * FROM STREAM sw IN default TIME > ? WHERE service_id = ? AND codes IN (?) LIMIT ?" + params := func() []*modelv1.TagValue { + return []*modelv1.TagValue{tvStr("-30m"), tvStr("webapp"), tvStrArray("a", "b"), tvInt(10)} + } + b.Run("ParseAndBindEachRequest", func(b *testing.B) { + for i := 0; i < b.N; i++ { + g, errs := ParseQuery(query) + if errs != nil { + b.Fatal(errs) + } + if err := BindParams(g, params()); err != nil { + b.Fatal(err) + } + } + }) + b.Run("PrepareOnceBindMany", func(b *testing.B) { + ps, err := Prepare(query) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := ps.Bind(params()); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/pkg/bydbql/transformer.go b/pkg/bydbql/transformer.go index d1460b85e..d4a169081 100644 --- a/pkg/bydbql/transformer.go +++ b/pkg/bydbql/transformer.go @@ -99,18 +99,98 @@ func NewTransformer(registry metadata.Repo) *Transformer { } } -// Transform transforms a Grammar into a native query request. +// transformRun holds the per-request state of a single transformation: the +// stateless Transformer plus the bound parameter overlay (nil for a literal or +// in-place-bound grammar). The conversion methods hang off transformRun so the +// shared Transformer stays safe for concurrent requests. +type transformRun struct { + *Transformer + bound []resolvedParam +} + +// resolveValue returns the effective literal value node for a scalar position: +// the overlay's bound node for a placeholder, or the node itself for a literal +// (also the in-place-bound path, where bound is nil and the node was mutated). +func (r *transformRun) resolveValue(v *GrammarValue) *GrammarValue { + if v != nil && v.Param && r.bound != nil { + return r.bound[v.ParamIndex].values[0] + } + return v +} + +// resolveValues expands a list of value nodes, splicing each placeholder's bound +// values (one, or several for an array) in place of the placeholder node. A list +// with no placeholder (a literal list, or a re-resolve of an already-expanded +// one) is returned unchanged, so the bound path allocates only when it must. +func (r *transformRun) resolveValues(values []*GrammarValue) []*GrammarValue { + if r.bound == nil { + return values + } + hasParam := false + for _, v := range values { + if v != nil && v.Param { + hasParam = true + break + } + } + if !hasParam { + return values + } + out := make([]*GrammarValue, 0, len(values)) + for _, v := range values { + if v != nil && v.Param { + out = append(out, r.bound[v.ParamIndex].values...) + continue + } + out = append(out, v) + } + return out +} + +// resolveTimeString returns a TIME value's effective string: the overlay's bound +// string for a placeholder, or the node's own literal. +func (r *transformRun) resolveTimeString(v *GrammarTimeValue) string { + if v != nil && v.Param && r.bound != nil { + return r.bound[v.ParamIndex].timeStr + } + return v.ToString() +} + +// resolveCount returns a count position's effective value: the overlay's bound +// int for a placeholder, or the node's own literal. +func (r *transformRun) resolveCount(value int, param bool, paramIndex int) int { + if param && r.bound != nil { + return r.bound[paramIndex].count + } + return value +} + +// Transform transforms a Grammar into a native query request. The grammar must +// be free of unbound placeholders: either a literal query or one bound in place +// by BindParams. Use TransformBound for a reusable prepared statement. func (t *Transformer) Transform(ctx context.Context, grammar *Grammar) (*TransformResult, error) { + return (&transformRun{Transformer: t}).transform(ctx, grammar) +} + +// TransformBound transforms a bound prepared statement, reading parameter values +// from the per-request overlay instead of the immutable template. +func (t *Transformer) TransformBound(ctx context.Context, bq *BoundQuery) (*TransformResult, error) { + return (&transformRun{Transformer: t, bound: bq.values}).transform(ctx, bq.stmt.template) +} + +func (r *transformRun) transform(ctx context.Context, grammar *Grammar) (*TransformResult, error) { // Defense in depth: an unbound placeholder would otherwise transform into an - // empty string or a zero count silently instead of failing loudly. A bound - // grammar is guaranteed placeholder-free, so it skips the walk. - if !grammar.paramsBound { + // empty string or a zero count silently instead of failing loudly. The + // overlay path (r.bound != nil) carries the values; the in-place path sets + // paramsBound; a literal grammar has no placeholders to begin with. + if !grammar.paramsBound && r.bound == nil { if unbound := countUnboundParams(grammar); unbound > 0 { - return nil, fmt.Errorf("query contains %d unbound placeholder(s); BindParams must be called before Transform", unbound) + return nil, fmt.Errorf("query contains %d unbound placeholder(s); bind parameters before transforming", unbound) } } // Literal counts get the same wrap guard as bound parameters, so LIMIT -5 - // fails loudly instead of wrapping when narrowed to uint32. + // fails loudly instead of wrapping when narrowed to uint32. Overlay counts + // were already range-checked at Bind time. if err := validateGrammarCounts(grammar); err != nil { return nil, err } @@ -119,13 +199,13 @@ func (t *Transformer) Transform(ctx context.Context, grammar *Grammar) (*Transfo resourceType := grammar.Select.From.ResourceType switch strings.ToUpper(resourceType) { case "STREAM": - return t.transformStreamQuery(ctx, grammar) + return r.transformStreamQuery(ctx, grammar) case "MEASURE": - return t.transformMeasureQuery(ctx, grammar) + return r.transformMeasureQuery(ctx, grammar) case "TRACE": - return t.transformTraceQuery(ctx, grammar) + return r.transformTraceQuery(ctx, grammar) case "PROPERTY": - return t.transformPropertyQuery(ctx, grammar) + return r.transformPropertyQuery(ctx, grammar) default: return nil, fmt.Errorf("unsupported resource type in select statement: %s", resourceType) } @@ -133,14 +213,14 @@ func (t *Transformer) Transform(ctx context.Context, grammar *Grammar) (*Transfo if grammar.TopN != nil { resourceType := grammar.TopN.From.ResourceType if strings.EqualFold(resourceType, "MEASURE") { - return t.transformTopNMeasureQuery(ctx, grammar) + return r.transformTopNMeasureQuery(ctx, grammar) } return nil, fmt.Errorf("unsupported resource type in topn statement: %s", resourceType) } return nil, errors.New("grammar must contain either Select or TopN statement") } -func (t *Transformer) transformStreamQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { +func (r *transformRun) transformStreamQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { statement := grammar.Select if statement == nil { return nil, errors.New("stream query must be a select statement") @@ -151,15 +231,15 @@ func (t *Transformer) transformStreamQuery(ctx context.Context, grammar *Grammar resourceName := statement.From.ResourceName // validate query - if err := t.validateGroupOrResourceName(groups, resourceName); err != nil { + if err := r.validateGroupOrResourceName(groups, resourceName); err != nil { return nil, err } // query schema for getting tags - projection, _, allTags, _, err := t.convertTagAndField( + projection, _, allTags, _, err := r.convertTagAndField( groups, resourceName, func(group, name string) ([]*databasev1.TagFamilySpec, []*databasev1.FieldSpec, error) { - stream, getErr := t.schemaRegistry.StreamRegistry().GetStream(ctx, &commonv1.Metadata{ + stream, getErr := r.schemaRegistry.StreamRegistry().GetStream(ctx, &commonv1.Metadata{ Name: name, Group: group, }) @@ -173,26 +253,26 @@ func (t *Transformer) transformStreamQuery(ctx context.Context, grammar *Grammar } // convert time range - timeRange, err := t.convertTimeRange(time.Now(), statement.Time) + timeRange, err := r.convertTimeRange(time.Now(), statement.Time) if err != nil { return nil, fmt.Errorf("failed to convert time range: %w", err) } // convert order by - orderBy := t.convertSelectOrderBy(statement.OrderBy) + orderBy := r.convertSelectOrderBy(statement.OrderBy) // convert criteria - criteria, err := t.convertSelectCriteria(statement.Where, allTags) + criteria, err := r.convertSelectCriteria(statement.Where, allTags) if err != nil { return nil, fmt.Errorf("failed to convert criteria: %w", err) } var offset, limit uint32 if statement.Offset != nil { - offset = uint32(statement.Offset.Value) + offset = uint32(r.resolveCount(statement.Offset.Value, statement.Offset.Param, statement.Offset.ParamIndex)) } if statement.Limit != nil { - limit = uint32(statement.Limit.Value) + limit = uint32(r.resolveCount(statement.Limit.Value, statement.Limit.Param, statement.Limit.ParamIndex)) } // extract stages @@ -219,7 +299,7 @@ func (t *Transformer) transformStreamQuery(ctx context.Context, grammar *Grammar }, nil } -func (t *Transformer) transformMeasureQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { +func (r *transformRun) transformMeasureQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { statement := grammar.Select if statement == nil { return nil, errors.New("measure query must be a select statement") @@ -230,15 +310,15 @@ func (t *Transformer) transformMeasureQuery(ctx context.Context, grammar *Gramma resourceName := statement.From.ResourceName // validate query - if err := t.validateGroupOrResourceName(groups, resourceName); err != nil { + if err := r.validateGroupOrResourceName(groups, resourceName); err != nil { return nil, err } // query schema for getting tags and fields - projection, fields, allTags, allFields, err := t.convertTagAndField( + projection, fields, allTags, allFields, err := r.convertTagAndField( groups, resourceName, func(group, name string) ([]*databasev1.TagFamilySpec, []*databasev1.FieldSpec, error) { - measure, getErr := t.schemaRegistry.MeasureRegistry().GetMeasure(ctx, &commonv1.Metadata{ + measure, getErr := r.schemaRegistry.MeasureRegistry().GetMeasure(ctx, &commonv1.Metadata{ Name: name, Group: group, }) @@ -259,28 +339,28 @@ func (t *Transformer) transformMeasureQuery(ctx context.Context, grammar *Gramma } // convert time range - timeRange, err := t.convertTimeRange(time.Now(), statement.Time) + timeRange, err := r.convertTimeRange(time.Now(), statement.Time) if err != nil { return nil, fmt.Errorf("failed to convert time range: %w", err) } // convert order by - orderBy := t.convertSelectOrderBy(statement.OrderBy) + orderBy := r.convertSelectOrderBy(statement.OrderBy) // convert criteria - criteria, err := t.convertSelectCriteria(statement.Where, allTags) + criteria, err := r.convertSelectCriteria(statement.Where, allTags) if err != nil { return nil, fmt.Errorf("failed to convert criteria: %w", err) } // convert aggregation - agg, err := t.convertAggregation(statement.Projection, allFields) + agg, err := r.convertAggregation(statement.Projection, allFields) if err != nil { return nil, fmt.Errorf("failed to convert aggregation: %w", err) } // convert group by - groupBy, err := t.convertGroupBy(statement.GroupBy, projection, fields) + groupBy, err := r.convertGroupBy(statement.GroupBy, projection, fields) if err != nil { return nil, fmt.Errorf("failed to convert group by: %w", err) } @@ -288,17 +368,17 @@ func (t *Transformer) transformMeasureQuery(ctx context.Context, grammar *Gramma return nil, errors.New("when aggregation and group by are both present, group by must include a field") } - top, err := t.convertTOP(statement.Projection, allFields) + top, err := r.convertTOP(statement.Projection, allFields) if err != nil { return nil, fmt.Errorf("failed to convert top: %w", err) } var offset, limit uint32 if statement.Offset != nil { - offset = uint32(statement.Offset.Value) + offset = uint32(r.resolveCount(statement.Offset.Value, statement.Offset.Param, statement.Offset.ParamIndex)) } if statement.Limit != nil { - limit = uint32(statement.Limit.Value) + limit = uint32(r.resolveCount(statement.Limit.Value, statement.Limit.Param, statement.Limit.ParamIndex)) } // extract stages @@ -329,7 +409,7 @@ func (t *Transformer) transformMeasureQuery(ctx context.Context, grammar *Gramma }, nil } -func (t *Transformer) transformTraceQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { +func (r *transformRun) transformTraceQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { statement := grammar.Select if statement == nil { return nil, errors.New("trace query must be a select statement") @@ -340,11 +420,11 @@ func (t *Transformer) transformTraceQuery(ctx context.Context, grammar *Grammar) resourceName := statement.From.ResourceName // validate query - if err := t.validateGroupOrResourceName(groups, resourceName); err != nil { + if err := r.validateGroupOrResourceName(groups, resourceName); err != nil { return nil, err } - timeRange, err := t.convertTimeRange(time.Now(), statement.Time) + timeRange, err := r.convertTimeRange(time.Now(), statement.Time) if err != nil { return nil, fmt.Errorf("failed to convert time range: %w", err) } @@ -352,7 +432,7 @@ func (t *Transformer) transformTraceQuery(ctx context.Context, grammar *Grammar) // query the trace schema for getting tags allTags := make(map[string]*tagSpecWithFamily) for _, g := range groups { - trace, getErr := t.schemaRegistry.TraceRegistry().GetTrace(ctx, &commonv1.Metadata{ + trace, getErr := r.schemaRegistry.TraceRegistry().GetTrace(ctx, &commonv1.Metadata{ Name: resourceName, Group: g, }) @@ -391,20 +471,20 @@ func (t *Transformer) transformTraceQuery(ctx context.Context, grammar *Grammar) } // convert criteria - criteria, err := t.convertSelectCriteria(statement.Where, allTags) + criteria, err := r.convertSelectCriteria(statement.Where, allTags) if err != nil { return nil, fmt.Errorf("failed to convert criteria: %w", err) } // convert order by - orderBy := t.convertSelectOrderBy(statement.OrderBy) + orderBy := r.convertSelectOrderBy(statement.OrderBy) var offset, limit uint32 if statement.Offset != nil { - offset = uint32(statement.Offset.Value) + offset = uint32(r.resolveCount(statement.Offset.Value, statement.Offset.Param, statement.Offset.ParamIndex)) } if statement.Limit != nil { - limit = uint32(statement.Limit.Value) + limit = uint32(r.resolveCount(statement.Limit.Value, statement.Limit.Param, statement.Limit.ParamIndex)) } // extract stages @@ -431,7 +511,7 @@ func (t *Transformer) transformTraceQuery(ctx context.Context, grammar *Grammar) }, nil } -func (t *Transformer) transformPropertyQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { +func (r *transformRun) transformPropertyQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { statement := grammar.Select if statement == nil { return nil, errors.New("property query must be a select statement") @@ -442,14 +522,14 @@ func (t *Transformer) transformPropertyQuery(ctx context.Context, grammar *Gramm resourceName := statement.From.ResourceName // validate query - if err := t.validateGroupOrResourceName(groups, resourceName); err != nil { + if err := r.validateGroupOrResourceName(groups, resourceName); err != nil { return nil, err } // get property schema to extract all tags allTags := make(map[string]*tagSpecWithFamily) for _, g := range groups { - property, getErr := t.schemaRegistry.PropertyRegistry().GetProperty(ctx, &commonv1.Metadata{ + property, getErr := r.schemaRegistry.PropertyRegistry().GetProperty(ctx, &commonv1.Metadata{ Name: resourceName, Group: g, }) @@ -499,7 +579,7 @@ func (t *Transformer) transformPropertyQuery(ctx context.Context, grammar *Gramm var criteria *modelv1.Criteria if statement.Where != nil && statement.Where.Expr != nil { var extractErr error - ids, criteria, extractErr = t.extractIDsAndCriteria(statement.Where.Expr, allTags) + ids, criteria, extractErr = r.extractIDsAndCriteria(statement.Where.Expr, allTags) if extractErr != nil { return nil, fmt.Errorf("failed to convert criteria: %w", extractErr) } @@ -508,13 +588,13 @@ func (t *Transformer) transformPropertyQuery(ctx context.Context, grammar *Gramm // handle limit var limit uint32 if statement.Limit != nil { - limit = uint32(statement.Limit.Value) + limit = uint32(r.resolveCount(statement.Limit.Value, statement.Limit.Param, statement.Limit.ParamIndex)) } // handle ORDER BY var orderBy *propertyv1.QueryOrder if statement.OrderBy != nil { - modelQueryOrder := t.convertSelectOrderBy(statement.OrderBy) + modelQueryOrder := r.convertSelectOrderBy(statement.OrderBy) if modelQueryOrder != nil { orderBy = &propertyv1.QueryOrder{ TagName: modelQueryOrder.IndexRuleName, @@ -539,7 +619,7 @@ func (t *Transformer) transformPropertyQuery(ctx context.Context, grammar *Gramm }, nil } -func (t *Transformer) transformTopNMeasureQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { +func (r *transformRun) transformTopNMeasureQuery(ctx context.Context, grammar *Grammar) (*TransformResult, error) { statement := grammar.TopN if statement == nil { return nil, errors.New("topn measure query must be a topn statement") @@ -550,12 +630,12 @@ func (t *Transformer) transformTopNMeasureQuery(ctx context.Context, grammar *Gr resourceName := statement.From.ResourceName // validate query - if err := t.validateGroupOrResourceName(groups, resourceName); err != nil { + if err := r.validateGroupOrResourceName(groups, resourceName); err != nil { return nil, err } // convert time range - timeRange, err := t.convertTimeRange(time.Now(), statement.Time) + timeRange, err := r.convertTimeRange(time.Now(), statement.Time) if err != nil { return nil, fmt.Errorf("failed to convert time range: %w", err) } @@ -563,20 +643,20 @@ func (t *Transformer) transformTopNMeasureQuery(ctx context.Context, grammar *Gr // convert agg var aggFunc modelv1.AggregationFunction if statement.AggregateBy != nil { - aggFunc, err = t.convertAggregationFunc(statement.AggregateBy.Function.Function) + aggFunc, err = r.convertAggregationFunc(statement.AggregateBy.Function.Function) if err != nil { return nil, err } } // convert conditions - conditions, err := t.convertTopNAndConditions(ctx, statement.Where, groups, resourceName) + conditions, err := r.convertTopNAndConditions(ctx, statement.Where, groups, resourceName) if err != nil { return nil, fmt.Errorf("failed to convert criteria: %w", err) } // convert order by - orderBy := t.convertTopNOrderBy(statement.OrderBy) + orderBy := r.convertTopNOrderBy(statement.OrderBy) sort := modelv1.Sort_SORT_UNSPECIFIED if orderBy != nil { sort = orderBy.Sort @@ -595,7 +675,7 @@ func (t *Transformer) transformTopNMeasureQuery(ctx context.Context, grammar *Gr Groups: groups, Name: resourceName, TimeRange: timeRange, - TopN: int32(statement.N), + TopN: int32(r.resolveCount(statement.N, statement.NParam, statement.NParamIndex)), Agg: aggFunc, Conditions: conditions, FieldValueSort: sort, @@ -605,22 +685,22 @@ func (t *Transformer) transformTopNMeasureQuery(ctx context.Context, grammar *Gr }, nil } -func (t *Transformer) convertSelectCriteria(where *GrammarSelectWhereClause, allTags map[string]*tagSpecWithFamily) (*modelv1.Criteria, error) { +func (r *transformRun) convertSelectCriteria(where *GrammarSelectWhereClause, allTags map[string]*tagSpecWithFamily) (*modelv1.Criteria, error) { if where == nil || where.Expr == nil { return nil, nil } - return t.convertOrExpr(where.Expr, allTags, nil) + return r.convertOrExpr(where.Expr, allTags, nil) } -func (t *Transformer) convertTopNAndConditions(ctx context.Context, where *GrammarTopNWhereClause, groups []string, resourceName string) ([]*modelv1.Condition, error) { +func (r *transformRun) convertTopNAndConditions(ctx context.Context, where *GrammarTopNWhereClause, groups []string, resourceName string) ([]*modelv1.Condition, error) { if where == nil || where.Expr == nil { return nil, nil } allTags := make(map[string]*tagSpecWithFamily) for _, g := range groups { - aggregation, getErr := t.schemaRegistry.TopNAggregationRegistry().GetTopNAggregation(ctx, &commonv1.Metadata{ + aggregation, getErr := r.schemaRegistry.TopNAggregationRegistry().GetTopNAggregation(ctx, &commonv1.Metadata{ Name: resourceName, Group: g, }) @@ -628,7 +708,7 @@ func (t *Transformer) convertTopNAndConditions(ctx context.Context, where *Gramm return nil, fmt.Errorf("failed to get topn aggregation %s/%s: %w", g, resourceName, getErr) } sourceMeasure := aggregation.SourceMeasure - measure, getErr := t.schemaRegistry.MeasureRegistry().GetMeasure(ctx, sourceMeasure) + measure, getErr := r.schemaRegistry.MeasureRegistry().GetMeasure(ctx, sourceMeasure) if getErr != nil { return nil, fmt.Errorf("failed to get measure %s/%s: %w", sourceMeasure.Group, sourceMeasure.Name, getErr) } @@ -644,7 +724,7 @@ func (t *Transformer) convertTopNAndConditions(ctx context.Context, where *Gramm } var conditions []*modelv1.Condition - _, err := t.convertAndExpr(where.Expr, allTags, func(c *modelv1.Condition) { + _, err := r.convertAndExpr(where.Expr, allTags, func(c *modelv1.Condition) { conditions = append(conditions, c) }) if err != nil { @@ -654,7 +734,7 @@ func (t *Transformer) convertTopNAndConditions(ctx context.Context, where *Gramm return conditions, nil } -func (t *Transformer) convertGroupBy(g *GrammarGroupByClause, queryTags *modelv1.TagProjection, queryFields []string) (*measurev1.QueryRequest_GroupBy, error) { +func (r *transformRun) convertGroupBy(g *GrammarGroupByClause, queryTags *modelv1.TagProjection, queryFields []string) (*measurev1.QueryRequest_GroupBy, error) { if g == nil { return nil, nil } @@ -754,7 +834,7 @@ func (t *Transformer) convertGroupBy(g *GrammarGroupByClause, queryTags *modelv1 return groupBy, nil } -func (t *Transformer) convertAggregation(projection *GrammarProjection, allFields map[string]*databasev1.FieldSpec) (*measurev1.QueryRequest_Aggregation, error) { +func (r *transformRun) convertAggregation(projection *GrammarProjection, allFields map[string]*databasev1.FieldSpec) (*measurev1.QueryRequest_Aggregation, error) { var columns []*GrammarColumn if projection != nil && len(projection.Columns) > 0 { columns = append(columns, projection.Columns...) @@ -789,7 +869,7 @@ func (t *Transformer) convertAggregation(projection *GrammarProjection, allField return nil, fmt.Errorf("field %s not found in schema", aggColName) } - aggFunc, err := t.convertAggregationFunc(aggCol.Aggregate.Function) + aggFunc, err := r.convertAggregationFunc(aggCol.Aggregate.Function) if err != nil { return nil, err } @@ -800,7 +880,7 @@ func (t *Transformer) convertAggregation(projection *GrammarProjection, allField }, nil } -func (t *Transformer) convertAggregationFunc(f string) (modelv1.AggregationFunction, error) { +func (r *transformRun) convertAggregationFunc(f string) (modelv1.AggregationFunction, error) { switch strings.ToUpper(f) { case "MEAN", "AVG": return modelv1.AggregationFunction_AGGREGATION_FUNCTION_MEAN, nil @@ -817,12 +897,12 @@ func (t *Transformer) convertAggregationFunc(f string) (modelv1.AggregationFunct } } -func (t *Transformer) convertOrExpr(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { +func (r *transformRun) convertOrExpr(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { if expr == nil { return nil, nil } - leftCriteria, err := t.convertAndExpr(expr.Left, allTags, accept) + leftCriteria, err := r.convertAndExpr(expr.Left, allTags, accept) if err != nil { return nil, err } @@ -833,7 +913,7 @@ func (t *Transformer) convertOrExpr(expr *GrammarOrExpr, allTags map[string]*tag // process all OR operations for _, orRight := range expr.Right { - rightCriteria, err := t.convertAndExpr(orRight.Right, allTags, accept) + rightCriteria, err := r.convertAndExpr(orRight.Right, allTags, accept) if err != nil { return nil, err } @@ -852,12 +932,12 @@ func (t *Transformer) convertOrExpr(expr *GrammarOrExpr, allTags map[string]*tag return leftCriteria, nil } -func (t *Transformer) convertAndExpr(expr *GrammarAndExpr, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { +func (r *transformRun) convertAndExpr(expr *GrammarAndExpr, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { if expr == nil { return nil, nil } - leftCriteria, err := t.convertPredicate(expr.Left, allTags, accept) + leftCriteria, err := r.convertPredicate(expr.Left, allTags, accept) if err != nil { return nil, err } @@ -868,7 +948,7 @@ func (t *Transformer) convertAndExpr(expr *GrammarAndExpr, allTags map[string]*t // process all AND operations for _, andRight := range expr.Right { - rightCriteria, err := t.convertPredicate(andRight.Right, allTags, accept) + rightCriteria, err := r.convertPredicate(andRight.Right, allTags, accept) if err != nil { return nil, err } @@ -887,31 +967,31 @@ func (t *Transformer) convertAndExpr(expr *GrammarAndExpr, allTags map[string]*t return leftCriteria, nil } -func (t *Transformer) convertPredicate(pred *GrammarPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { +func (r *transformRun) convertPredicate(pred *GrammarPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { if pred == nil { return nil, nil } if pred.Paren != nil { - return t.convertOrExpr(pred.Paren, allTags, accept) + return r.convertOrExpr(pred.Paren, allTags, accept) } if pred.Binary != nil { - return t.convertBinaryPredicate(pred.Binary, allTags, accept) + return r.convertBinaryPredicate(pred.Binary, allTags, accept) } if pred.In != nil { - return t.convertInPredicate(pred.In, allTags, accept) + return r.convertInPredicate(pred.In, allTags, accept) } if pred.Having != nil { - return t.convertHavingPredicate(pred.Having, allTags, accept) + return r.convertHavingPredicate(pred.Having, allTags, accept) } return nil, errors.New("empty predicate") } -func (t *Transformer) convertBinaryPredicate( +func (r *transformRun) convertBinaryPredicate( pred *GrammarBinaryPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition), @@ -927,17 +1007,17 @@ func (t *Transformer) convertBinaryPredicate( } if pred.Tail.Match != nil { - return t.convertMatchPredicate(identifierName, pred.Tail.Match, accept) + return r.convertMatchPredicate(identifierName, pred.Tail.Match, accept) } if pred.Tail.Compare != nil { - return t.convertComparePredicate(identifierName, pred.Tail.Compare, tagSpec, accept) + return r.convertComparePredicate(identifierName, pred.Tail.Compare, tagSpec, accept) } return nil, errors.New("empty binary predicate tail") } -func (t *Transformer) convertMatchPredicate(identifierName string, match *GrammarMatchTail, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { +func (r *transformRun) convertMatchPredicate(identifierName string, match *GrammarMatchTail, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { if match.Values == nil { return nil, fmt.Errorf("MATCH operator requires values") } @@ -948,6 +1028,9 @@ func (t *Transformer) convertMatchPredicate(identifierName string, match *Gramma } else if match.Values.Array != nil { values = match.Values.Array } + // Expand any placeholder into its bound value(s); a single placeholder + // bound to an array becomes multiple values, matching the literal form. + values = r.resolveValues(values) if len(values) == 0 { return nil, fmt.Errorf("MATCH requires at least one value") @@ -970,14 +1053,14 @@ func (t *Transformer) convertMatchPredicate(identifierName string, match *Gramma // single value: set as Str cond.Value = &modelv1.TagValue{ Value: &modelv1.TagValue_Str{ - Str: &modelv1.Str{Value: t.grammarValueToString(values[0])}, + Str: &modelv1.Str{Value: r.grammarValueToString(values[0])}, }, } } else { // multiple values: set as string array strArr := make([]string, len(values)) for i, val := range values { - strArr[i] = t.grammarValueToString(val) + strArr[i] = r.grammarValueToString(val) } cond.Value = &modelv1.TagValue{ Value: &modelv1.TagValue_StrArray{ @@ -1016,7 +1099,7 @@ func (t *Transformer) convertMatchPredicate(identifierName string, match *Gramma }, nil } -func (t *Transformer) convertComparePredicate( +func (r *transformRun) convertComparePredicate( identifierName string, compare *GrammarCompareTail, tagSpec *tagSpecWithFamily, @@ -1024,10 +1107,10 @@ func (t *Transformer) convertComparePredicate( ) (*modelv1.Criteria, error) { cond := &modelv1.Condition{ Name: identifierName, - Op: t.convertCompareOp(compare.Operator), + Op: r.convertCompareOp(compare.Operator), } - if err := t.setGrammarConditionValue(cond, compare.Value, tagSpec); err != nil { + if err := r.setGrammarConditionValue(cond, compare.Value, tagSpec); err != nil { return nil, fmt.Errorf("failed to set condition for tag %s: %w", identifierName, err) } @@ -1042,7 +1125,7 @@ func (t *Transformer) convertComparePredicate( }, nil } -func (t *Transformer) convertInPredicate(pred *GrammarInPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { +func (r *transformRun) convertInPredicate(pred *GrammarInPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition)) (*modelv1.Criteria, error) { identifierName, nameErr := pred.Identifier.ToString(false) if nameErr != nil { return nil, fmt.Errorf("failed to parse identifier: %w", nameErr) @@ -1063,7 +1146,7 @@ func (t *Transformer) convertInPredicate(pred *GrammarInPredicate, allTags map[s Op: op, } - if err := t.setGrammarMultiValueCondition(cond, pred.Values, tagSpec); err != nil { + if err := r.setGrammarMultiValueCondition(cond, pred.Values, tagSpec); err != nil { return nil, fmt.Errorf("failed to set condition for tag %s: %w", identifierName, err) } @@ -1078,7 +1161,7 @@ func (t *Transformer) convertInPredicate(pred *GrammarInPredicate, allTags map[s }, nil } -func (t *Transformer) convertHavingPredicate( +func (r *transformRun) convertHavingPredicate( pred *GrammarHavingPredicate, allTags map[string]*tagSpecWithFamily, accept func(c *modelv1.Condition), @@ -1103,11 +1186,21 @@ func (t *Transformer) convertHavingPredicate( Op: op, } + // Preserve the literal single-vs-list form: a single-value HAVING stays + // single unless a bound array expands it to several values, while a + // parenthesized list stays a list regardless of count. + wasSingle := pred.Values.Single != nil + values := pred.Values.Array + if wasSingle { + values = []*GrammarValue{pred.Values.Single} + } + values = r.resolveValues(values) + var err error - if pred.Values.Single != nil { - err = t.setGrammarConditionValue(cond, pred.Values.Single, tagSpec) - } else if pred.Values.Array != nil { - err = t.setGrammarMultiValueCondition(cond, pred.Values.Array, tagSpec) + if wasSingle && len(values) == 1 { + err = r.setGrammarConditionValue(cond, values[0], tagSpec) + } else { + err = r.setGrammarMultiValueCondition(cond, values, tagSpec) } if err != nil { @@ -1125,7 +1218,8 @@ func (t *Transformer) convertHavingPredicate( }, nil } -func (t *Transformer) setGrammarConditionValue(cond *modelv1.Condition, val *GrammarValue, tagSpec *tagSpecWithFamily) error { +func (r *transformRun) setGrammarConditionValue(cond *modelv1.Condition, val *GrammarValue, tagSpec *tagSpecWithFamily) error { + val = r.resolveValue(val) if val.Null { cond.Value = &modelv1.TagValue{ Value: &modelv1.TagValue_Null{ @@ -1140,12 +1234,12 @@ func (t *Transformer) setGrammarConditionValue(cond *modelv1.Condition, val *Gra // Convert to Str cond.Value = &modelv1.TagValue{ Value: &modelv1.TagValue_Str{ - Str: &modelv1.Str{Value: t.grammarValueToString(val)}, + Str: &modelv1.Str{Value: r.grammarValueToString(val)}, }, } case databasev1.TagType_TAG_TYPE_INT, databasev1.TagType_TAG_TYPE_INT_ARRAY: - intVal, err := t.grammarValueToInt64(val) + intVal, err := r.grammarValueToInt64(val) if err != nil { return err } @@ -1165,7 +1259,8 @@ func (t *Transformer) setGrammarConditionValue(cond *modelv1.Condition, val *Gra return nil } -func (t *Transformer) setGrammarMultiValueCondition(cond *modelv1.Condition, values []*GrammarValue, tagSpec *tagSpecWithFamily) error { +func (r *transformRun) setGrammarMultiValueCondition(cond *modelv1.Condition, values []*GrammarValue, tagSpec *tagSpecWithFamily) error { + values = r.resolveValues(values) switch tagSpec.tag.Type { case databasev1.TagType_TAG_TYPE_STRING, databasev1.TagType_TAG_TYPE_STRING_ARRAY: strArr := make([]string, len(values)) @@ -1173,7 +1268,7 @@ func (t *Transformer) setGrammarMultiValueCondition(cond *modelv1.Condition, val if val.Null { return fmt.Errorf("NULL is not allowed in array values") } - strArr[i] = t.grammarValueToString(val) + strArr[i] = r.grammarValueToString(val) } cond.Value = &modelv1.TagValue{ Value: &modelv1.TagValue_StrArray{ @@ -1187,7 +1282,7 @@ func (t *Transformer) setGrammarMultiValueCondition(cond *modelv1.Condition, val if val.Null { return fmt.Errorf("NULL is not allowed in array values") } - intVal, err := t.grammarValueToInt64(val) + intVal, err := r.grammarValueToInt64(val) if err != nil { return err } @@ -1206,25 +1301,25 @@ func (t *Transformer) setGrammarMultiValueCondition(cond *modelv1.Condition, val return nil } -func (t *Transformer) convertTimeRange(now time.Time, timeClause *GrammarTimeClause) (*modelv1.TimeRange, error) { +func (r *transformRun) convertTimeRange(now time.Time, timeClause *GrammarTimeClause) (*modelv1.TimeRange, error) { if timeClause == nil { return nil, nil } var begin, end time.Time if timeClause.Between != nil { - beginTS, err := t.parseTimestamp(now, timeClause.Between.Begin.ToString()) + beginTS, err := r.parseTimestamp(now, r.resolveTimeString(timeClause.Between.Begin)) if err != nil { return nil, err } - endTS, err := t.parseTimestamp(now, timeClause.Between.End.ToString()) + endTS, err := r.parseTimestamp(now, r.resolveTimeString(timeClause.Between.End)) if err != nil { return nil, err } begin = *beginTS end = *endTS } else if timeClause.Comparator != nil && timeClause.Value != nil { - timestamp, err := t.parseTimestamp(now, timeClause.Value.ToString()) + timestamp, err := r.parseTimestamp(now, r.resolveTimeString(timeClause.Value)) if err != nil { return nil, err } @@ -1255,7 +1350,7 @@ func (t *Transformer) convertTimeRange(now time.Time, timeClause *GrammarTimeCla }, nil } -func (t *Transformer) parseTimestamp(now time.Time, timestamp string) (*time.Time, error) { +func (r *transformRun) parseTimestamp(now time.Time, timestamp string) (*time.Time, error) { // Try parsing as absolute time first (RFC3339) if parsedTime, err := time.Parse(time.RFC3339, timestamp); err == nil { return &parsedTime, nil @@ -1275,7 +1370,7 @@ func (t *Transformer) parseTimestamp(now time.Time, timestamp string) (*time.Tim return &resultTime, nil } -func (t *Transformer) convertTagAndField( +func (r *transformRun) convertTagAndField( groups []string, name string, query func(group, name string) ([]*databasev1.TagFamilySpec, []*databasev1.FieldSpec, error), @@ -1323,12 +1418,12 @@ func (t *Transformer) convertTagAndField( if includeAll { for _, spec := range allTags { - if addErr := t.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, spec.tag.Name, columnTypeTag, nil); addErr != nil { + if addErr := r.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, spec.tag.Name, columnTypeTag, nil); addErr != nil { return nil, nil, nil, nil, addErr } } for _, f := range allFields { - if addErr := t.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, f.Name, columnTypeField, nil); addErr != nil { + if addErr := r.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, f.Name, columnTypeField, nil); addErr != nil { return nil, nil, nil, nil, addErr } } @@ -1346,7 +1441,7 @@ func (t *Transformer) convertTagAndField( if col.TypeSpec != nil { colType = strings.ToUpper(*col.TypeSpec) } - if addErr := t.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, colName, colType, col.Aggregate); addErr != nil { + if addErr := r.checkOrAddGrammarTagOrField(allTags, allFields, &targetTagFamilies, &targetFields, columnExists, colName, colType, col.Aggregate); addErr != nil { return nil, nil, nil, nil, addErr } } @@ -1358,7 +1453,7 @@ func (t *Transformer) convertTagAndField( return tagProjection, targetFields, allTags, allFields, nil } -func (t *Transformer) findGrammarTagOrField( +func (r *transformRun) findGrammarTagOrField( sourceTags map[string]*tagSpecWithFamily, sourceFields map[string]*databasev1.FieldSpec, targetName string, @@ -1378,7 +1473,7 @@ type tagSpecWithFamily struct { family string } -func (t *Transformer) checkOrAddGrammarTagOrField( +func (r *transformRun) checkOrAddGrammarTagOrField( sourceTags map[string]*tagSpecWithFamily, sourceFields map[string]*databasev1.FieldSpec, targetTags *[]*modelv1.TagProjection_TagFamily, @@ -1412,7 +1507,7 @@ func (t *Transformer) checkOrAddGrammarTagOrField( return nil } - tag, field := t.findGrammarTagOrField(sourceTags, sourceFields, colName, colType) + tag, field := r.findGrammarTagOrField(sourceTags, sourceFields, colName, colType) if tag == nil && field == nil { return fmt.Errorf("column %s not found in schema", colName) } @@ -1472,7 +1567,7 @@ func (t *Transformer) checkOrAddGrammarTagOrField( return nil } -func (t *Transformer) validateGroupOrResourceName(groups []string, resourceName string) error { +func (r *transformRun) validateGroupOrResourceName(groups []string, resourceName string) error { if len(groups) == 0 { return errors.New("at least one group must be specified") } @@ -1483,7 +1578,7 @@ func (t *Transformer) validateGroupOrResourceName(groups []string, resourceName return nil } -func (t *Transformer) convertSelectOrderBy(orderBy *GrammarSelectOrderByClause) *modelv1.QueryOrder { +func (r *transformRun) convertSelectOrderBy(orderBy *GrammarSelectOrderByClause) *modelv1.QueryOrder { if orderBy == nil { return nil } @@ -1513,7 +1608,7 @@ func (t *Transformer) convertSelectOrderBy(orderBy *GrammarSelectOrderByClause) } } -func (t *Transformer) convertTopNOrderBy(orderBy *GrammarTopNOrderByClause) *modelv1.QueryOrder { +func (r *transformRun) convertTopNOrderBy(orderBy *GrammarTopNOrderByClause) *modelv1.QueryOrder { if orderBy == nil { return nil } @@ -1528,7 +1623,7 @@ func (t *Transformer) convertTopNOrderBy(orderBy *GrammarTopNOrderByClause) *mod } } -func (t *Transformer) convertCompareOp(op string) modelv1.Condition_BinaryOp { +func (r *transformRun) convertCompareOp(op string) modelv1.Condition_BinaryOp { switch op { case "=": return modelv1.Condition_BINARY_OP_EQ @@ -1547,7 +1642,7 @@ func (t *Transformer) convertCompareOp(op string) modelv1.Condition_BinaryOp { } } -func (t *Transformer) convertTOP(projection *GrammarProjection, fields map[string]*databasev1.FieldSpec) (*measurev1.QueryRequest_Top, error) { +func (r *transformRun) convertTOP(projection *GrammarProjection, fields map[string]*databasev1.FieldSpec) (*measurev1.QueryRequest_Top, error) { if projection == nil || projection.TopN == nil { return nil, nil } @@ -1570,13 +1665,14 @@ func (t *Transformer) convertTOP(projection *GrammarProjection, fields map[strin } return &measurev1.QueryRequest_Top{ - Number: int32(topn.N), + Number: int32(r.resolveCount(topn.N, topn.NParam, topn.NParamIndex)), FieldName: orderFieldName, FieldValueSort: sort, }, nil } -func (t *Transformer) grammarValueToString(val *GrammarValue) string { +func (r *transformRun) grammarValueToString(val *GrammarValue) string { + val = r.resolveValue(val) if val.String != nil { return *val.String } @@ -1586,7 +1682,8 @@ func (t *Transformer) grammarValueToString(val *GrammarValue) string { return "" } -func (t *Transformer) grammarValueToInt64(val *GrammarValue) (int64, error) { +func (r *transformRun) grammarValueToInt64(val *GrammarValue) (int64, error) { + val = r.resolveValue(val) if val.Integer != nil { return *val.Integer, nil } @@ -1602,13 +1699,13 @@ func (t *Transformer) grammarValueToInt64(val *GrammarValue) (int64, error) { // extractIDsAndCriteria separates ID conditions from other conditions in property queries. // ID conditions are extracted into a string array, while other conditions are converted to criteria. -func (t *Transformer) extractIDsAndCriteria(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { +func (r *transformRun) extractIDsAndCriteria(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { if expr == nil { return nil, nil, nil } // Process the expression tree and collect IDs and criteria - ids, criteria, err := t.extractIDsFromOrExpr(expr, allTags) + ids, criteria, err := r.extractIDsFromOrExpr(expr, allTags) if err != nil { return nil, nil, err } @@ -1617,8 +1714,8 @@ func (t *Transformer) extractIDsAndCriteria(expr *GrammarOrExpr, allTags map[str } // extractIDsFromOrExpr processes OR expressions for ID extraction. -func (t *Transformer) extractIDsFromOrExpr(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { - leftIDs, leftCriteria, err := t.extractIDsFromAndExpr(expr.Left, allTags) +func (r *transformRun) extractIDsFromOrExpr(expr *GrammarOrExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { + leftIDs, leftCriteria, err := r.extractIDsFromAndExpr(expr.Left, allTags) if err != nil { return nil, nil, err } @@ -1633,7 +1730,7 @@ func (t *Transformer) extractIDsFromOrExpr(expr *GrammarOrExpr, allTags map[stri currentCriteria := leftCriteria for _, orRight := range expr.Right { - rightIDs, rightCriteria, err := t.extractIDsFromAndExpr(orRight.Right, allTags) + rightIDs, rightCriteria, err := r.extractIDsFromAndExpr(orRight.Right, allTags) if err != nil { return nil, nil, err } @@ -1658,8 +1755,8 @@ func (t *Transformer) extractIDsFromOrExpr(expr *GrammarOrExpr, allTags map[stri } // extractIDsFromAndExpr processes AND expressions for ID extraction. -func (t *Transformer) extractIDsFromAndExpr(expr *GrammarAndExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { - leftIDs, leftCriteria, err := t.extractIDsFromPredicate(expr.Left, allTags) +func (r *transformRun) extractIDsFromAndExpr(expr *GrammarAndExpr, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { + leftIDs, leftCriteria, err := r.extractIDsFromPredicate(expr.Left, allTags) if err != nil { return nil, nil, err } @@ -1674,7 +1771,7 @@ func (t *Transformer) extractIDsFromAndExpr(expr *GrammarAndExpr, allTags map[st currentCriteria := leftCriteria for _, andRight := range expr.Right { - rightIDs, rightCriteria, err := t.extractIDsFromPredicate(andRight.Right, allTags) + rightIDs, rightCriteria, err := r.extractIDsFromPredicate(andRight.Right, allTags) if err != nil { return nil, nil, err } @@ -1699,9 +1796,9 @@ func (t *Transformer) extractIDsFromAndExpr(expr *GrammarAndExpr, allTags map[st } // extractIDsFromPredicate processes predicates for ID extraction. -func (t *Transformer) extractIDsFromPredicate(pred *GrammarPredicate, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { +func (r *transformRun) extractIDsFromPredicate(pred *GrammarPredicate, allTags map[string]*tagSpecWithFamily) ([]string, *modelv1.Criteria, error) { if pred.Paren != nil { - return t.extractIDsFromOrExpr(pred.Paren, allTags) + return r.extractIDsFromOrExpr(pred.Paren, allTags) } if pred.Binary != nil { @@ -1714,17 +1811,17 @@ func (t *Transformer) extractIDsFromPredicate(pred *GrammarPredicate, allTags ma if strings.EqualFold(identifierName, "ID") { if pred.Binary.Tail.Compare != nil && pred.Binary.Tail.Compare.Operator == "=" { // ID = 'value' - if pred.Binary.Tail.Compare.Value.Null { + if r.resolveValue(pred.Binary.Tail.Compare.Value).Null { return nil, nil, fmt.Errorf("ID cannot be NULL") } - idValue := t.grammarValueToString(pred.Binary.Tail.Compare.Value) + idValue := r.grammarValueToString(pred.Binary.Tail.Compare.Value) return []string{idValue}, nil, nil } return nil, nil, fmt.Errorf("unsupported operator for ID condition (only = is supported)") } // Not an ID condition, convert to criteria - criteria, err := t.convertBinaryPredicate(pred.Binary, allTags, nil) + criteria, err := r.convertBinaryPredicate(pred.Binary, allTags, nil) return nil, criteria, err } @@ -1740,27 +1837,28 @@ func (t *Transformer) extractIDsFromPredicate(pred *GrammarPredicate, allTags ma return nil, nil, fmt.Errorf("NOT IN is not supported for ID condition") } // ID IN ('value1', 'value2', ...) - if len(pred.In.Values) == 0 { + values := r.resolveValues(pred.In.Values) + if len(values) == 0 { return nil, nil, fmt.Errorf("ID IN requires at least one value") } - ids := make([]string, 0, len(pred.In.Values)) - for _, val := range pred.In.Values { + ids := make([]string, 0, len(values)) + for _, val := range values { if val.Null { return nil, nil, fmt.Errorf("ID cannot be NULL in IN clause") } - ids = append(ids, t.grammarValueToString(val)) + ids = append(ids, r.grammarValueToString(val)) } return ids, nil, nil } // not an ID condition, convert to criteria - criteria, err := t.convertInPredicate(pred.In, allTags, nil) + criteria, err := r.convertInPredicate(pred.In, allTags, nil) return nil, criteria, err } if pred.Having != nil { // HAVING is not an ID condition - criteria, err := t.convertHavingPredicate(pred.Having, allTags, nil) + criteria, err := r.convertHavingPredicate(pred.Having, allTags, nil) return nil, criteria, err }
