You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2026/05/15 12:22:31 UTC

(skywalking-banyandb) 01/08: feat(query/vectorized/measure/plan): resolve order_by in vec dispatch

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

hanahmily pushed a commit to branch vectorized-query
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 8f21383d9b10d0a9fe2dc93d9dab3b4b3694d5d3
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Fri May 15 01:49:20 2026 +0000

    feat(query/vectorized/measure/plan): resolve order_by in vec dispatch
    
    Lift the order_by bail-out and thread req.OrderBy through
    logical.ParseOrderBy into MeasureQueryOptions.Order, mirroring the
    row path's PushDownOrder optimizer rule. Empty index rule +
    UNSPECIFIED sort yields no Order (matching the row path's no-order
    default); unknown index rules surface as a dispatch error with
    handled=true so the caller does not silently retry the row path.
    Single-node order_by queries now clear the G8e parity gate
    (518/518 specs pass with proto.Equal) instead of falling through.
    
    Replace TestDispatch_OrderBy_FallsThrough — which passed only because
    bareReq's nil schemas caused an earlier bail-out — with two tests
    that exercise the new code: TestDispatch_OrderBy_ReachesEcQuery
    covers the time-only ordering path;
    TestDispatch_OrderBy_UnknownIndexRule_BubblesUpError covers the
    parse-error branch.
---
 CHANGES.md                                         |  2 +-
 pkg/query/vectorized/measure/plan/dispatch.go      | 39 ++++++++++---
 pkg/query/vectorized/measure/plan/dispatch_test.go | 67 ++++++++++++++++++----
 3 files changed, 89 insertions(+), 19 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 6d9c3f311..49b87c729 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -5,7 +5,7 @@ Release Notes.
 ## 0.11.0
 
 ### Features
-- Vectorized measure query path is now enabled by default. The columnar pipeline replaces per-row protobuf serialization in `NewMIterator`, cutting allocations and ns/op for scan-heavy measure queries; gRPC wire format (`*measurev1.InternalDataPoint`) is byte-identical. Coverage extends to single-node GroupBy+Agg via `BatchAggregation` (SUM/COUNT/MIN/MAX/MEAN with row-path-equivalent type semantics and first-seen carry-forward of non-key projected tags). Distributed Map-mode GroupBy+Agg, [...]
+- Vectorized measure query path is now enabled by default. The columnar pipeline replaces per-row protobuf serialization in `NewMIterator`, cutting allocations and ns/op for scan-heavy measure queries; gRPC wire format (`*measurev1.InternalDataPoint`) is byte-identical. Coverage extends to single-node GroupBy+Agg via `BatchAggregation` (SUM/COUNT/MIN/MAX/MEAN with row-path-equivalent type semantics and first-seen carry-forward of non-key projected tags). Distributed Map-mode GroupBy+Agg, [...]
 - Add validation to ensure Measure's ShardingKey contains all Entity tags to guarantee entity locality.
 - Organize access logs under a dedicated "accesslog" subdirectory to improve log organization and separation from other application data.
 - Collect BanyanDB data on e2e test failure for CI debugging.
diff --git a/pkg/query/vectorized/measure/plan/dispatch.go b/pkg/query/vectorized/measure/plan/dispatch.go
index d1d07fc2f..3ada53ef1 100644
--- a/pkg/query/vectorized/measure/plan/dispatch.go
+++ b/pkg/query/vectorized/measure/plan/dispatch.go
@@ -146,14 +146,6 @@ func Dispatch(
 			return nil, "", false, nil
 		}
 	}
-	if req.GetOrderBy() != nil {
-		// The row path resolves order_by via the PushDownOrder optimizer
-		// rule (logical.NewPushDownOrder applied after Analyze). The vec
-		// dispatch does not invoke those rules, so it would silently
-		// drop OrderBy and return unsorted rows. Fall through until
-		// dispatch threads order_by into model.MeasureQueryOptions.Order.
-		return nil, "", false, nil
-	}
 	if req.GetTimeRange() == nil {
 		return nil, "", false, nil
 	}
@@ -197,6 +189,11 @@ func Dispatch(
 		return nil, "", false, nil
 	}
 
+	indexOrder, orderErr := resolveOrderBy(req.GetOrderBy(), logicalSchema)
+	if orderErr != nil {
+		return nil, "", true, orderErr
+	}
+
 	// Resolve the index.Query + entities the same way the row path does
 	// in unresolvedIndexScan.Analyze.
 	var query index.Query
@@ -242,6 +239,7 @@ func Dispatch(
 		TimeRange:       scan.Params.TimeRange,
 		Entities:        entities,
 		Query:           query,
+		Order:           indexOrder,
 		GroupBy:         scan.Params.GroupBy,
 		Agg:             scan.Params.Agg,
 		TagProjection:   scan.Params.TagProjection,
@@ -277,6 +275,31 @@ func Dispatch(
 	return iter, p.String(), true, nil
 }
 
+// resolveOrderBy mirrors the row path's PushDownOrder optimizer rule.
+// Empty index rule + UNSPECIFIED sort yields (nil, nil) so dispatch
+// leaves opts.Order unset, matching the row path's no-order default.
+// ParseOrderBy errors on an unknown index rule or one with NoSort=true
+// — surface that error so dispatch reports handled=true rather than
+// silently retrying the row path, which would produce the same error
+// downstream.
+func resolveOrderBy(reqOrder *modelv1.QueryOrder, schema logical.Schema) (*index.OrderBy, error) {
+	if reqOrder == nil {
+		return nil, nil
+	}
+	parsed, err := logical.ParseOrderBy(schema, reqOrder.GetIndexRuleName(), reqOrder.GetSort())
+	if err != nil {
+		return nil, fmt.Errorf("vec dispatch: parse order_by: %w", err)
+	}
+	if parsed == nil {
+		return nil, nil
+	}
+	out := &index.OrderBy{Sort: parsed.Sort, Index: parsed.Index, Type: index.OrderByTypeIndex}
+	if parsed.Index == nil {
+		out.Type = index.OrderByTypeTime
+	}
+	return out, nil
+}
+
 // locateScan walks a vec plan tree to find the leaf Scan node. Today there
 // is exactly one Scan per plan (multi-measure merge is a G8 follow-up).
 func locateScan(p VecPlan) *Scan {
diff --git a/pkg/query/vectorized/measure/plan/dispatch_test.go b/pkg/query/vectorized/measure/plan/dispatch_test.go
index d0c923e4b..d83a29191 100644
--- a/pkg/query/vectorized/measure/plan/dispatch_test.go
+++ b/pkg/query/vectorized/measure/plan/dispatch_test.go
@@ -180,22 +180,69 @@ func TestDispatch_Top_FallsThrough(t *testing.T) {
 	}
 }
 
-// TestDispatch_OrderBy_FallsThrough covers the order_by gap. The row
-// path resolves OrderBy via the PushDownOrder optimizer rule which the
-// vec dispatch does not invoke. Until dispatch threads OrderBy into
-// MeasureQueryOptions.Order, requests with OrderBy must fall through.
-func TestDispatch_OrderBy_FallsThrough(t *testing.T) {
+// TestDispatch_OrderBy_ReachesEcQuery confirms dispatch resolves
+// req.OrderBy via logical.ParseOrderBy and threads it into
+// MeasureQueryOptions.Order, instead of falling through to the row
+// path. fakeEC returns nil so dispatch falls through after ec.Query
+// — what matters is that ec.Query was reached at all, proving the
+// OrderBy gate no longer rejects the request.
+func TestDispatch_OrderBy_ReachesEcQuery(t *testing.T) {
+	measureSchema := testMeasureSchema()
+	// nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it.
+	logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil)
+	if schemaErr != nil {
+		t.Fatalf("BuildSchema: %v", schemaErr)
+	}
+	metadata := &commonv1.Metadata{Name: "demo", Group: "default"}
+	ec := &fakeEC{wantResult: nil, wantErr: nil}
+
 	req := bareReq()
 	req.OrderBy = &modelv1.QueryOrder{
 		Sort: modelv1.Sort_SORT_DESC,
 	}
-	_, _, handled, err := Dispatch(context.Background(),
-		req, nil, nil, nil, nil, dispatchCfg(true))
+
+	iter, planStr, handled, err := Dispatch(context.Background(),
+		req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true))
 	if err != nil {
-		t.Fatalf("OrderBy fallthrough must not error: %v", err)
+		t.Fatalf("OrderBy must not error before ec.Query: %v", err)
 	}
-	if handled {
-		t.Fatal("OrderBy must fall through (row path applies it via PushDownOrder)")
+	if !ec.called {
+		t.Fatalf("OrderBy must reach ec.Query (no longer falls through); "+
+			"got iter=%v planStr=%q handled=%v", iter, planStr, handled)
+	}
+}
+
+// TestDispatch_OrderBy_UnknownIndexRule_BubblesUpError covers the
+// error branch dispatch added when threading OrderBy through
+// logical.ParseOrderBy: an unknown index rule name must surface as a
+// dispatch error with handled=true so the caller does not silently
+// retry the row path (which would produce the same canonical error).
+func TestDispatch_OrderBy_UnknownIndexRule_BubblesUpError(t *testing.T) {
+	measureSchema := testMeasureSchema()
+	// nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it.
+	logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil)
+	if schemaErr != nil {
+		t.Fatalf("BuildSchema: %v", schemaErr)
+	}
+	metadata := &commonv1.Metadata{Name: "demo", Group: "default"}
+	ec := &fakeEC{wantResult: nil, wantErr: nil}
+
+	req := bareReq()
+	req.OrderBy = &modelv1.QueryOrder{
+		IndexRuleName: "no_such_index_rule",
+		Sort:          modelv1.Sort_SORT_ASC,
+	}
+
+	_, _, handled, err := Dispatch(context.Background(),
+		req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true))
+	if err == nil {
+		t.Fatal("unknown OrderBy index rule must surface as a dispatch error")
+	}
+	if !handled {
+		t.Fatal("unknown OrderBy index rule must report handled=true so caller does not re-try row path")
+	}
+	if ec.called {
+		t.Fatal("unknown OrderBy index rule must error before ec.Query is invoked")
 	}
 }