chore(engine): Integrate metastore into physical query planning (#17016)

This PR integrates the data object lookup using the metastore into the query planning.

To achieve that, I extended the `Metastore` interface with an additional function that not only returns the data object paths, but also the stream IDs of streams that match the given label matchers.

```go
type Metastore interface {
	...

	// StreamsIDs returns object store paths and stream IDs for all matching objects for the given matchers between [start,end]
	StreamIDs(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]string, [][]int64, error)

	...
}
```

Signed-off-by: Christian Haudum <christian.haudum@gmail.com>
pull/16566/merge
Christian Haudum 2 months ago committed by GitHub
parent 58aa00a5f4
commit ba61aa88de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      pkg/dataobj/metastore/metastore.go
  2. 67
      pkg/dataobj/metastore/object.go
  3. 15
      pkg/dataobj/metastore/object_test.go
  4. 53
      pkg/engine/internal/types/literal.go
  5. 13
      pkg/engine/planner/logical/column_ref.go
  6. 4
      pkg/engine/planner/logical/format_tree.go
  7. 43
      pkg/engine/planner/logical/node_literal.go
  8. 128
      pkg/engine/planner/physical/context.go
  9. 162
      pkg/engine/planner/physical/context_test.go
  10. 11
      pkg/engine/planner/physical/expressions.go
  11. 4
      pkg/engine/planner/physical/expressions_test.go
  12. 2
      pkg/engine/planner/physical/optimizer.go
  13. 9
      pkg/engine/planner/physical/planner.go
  14. 4
      pkg/engine/planner/physical/planner_test.go

@ -12,8 +12,12 @@ type Metastore interface {
Streams(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]*labels.Labels, error)
// DataObjects returns paths to all matching the given matchers between [start,end]
// TODO(chaudum); The comment is not correct, because the implementation does not filter by matchers, only by [start, end].
DataObjects(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]string, error)
// StreamsIDs returns object store paths and stream IDs for all matching objects for the given matchers between [start,end]
StreamIDs(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]string, [][]int64, error)
// Labels returns all possible labels from matching streams between [start,end]
Labels(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]string, error) // Used to get possible labels for a given stream

@ -26,7 +26,8 @@ const (
)
type ObjectMetastore struct {
bucket objstore.Bucket
bucket objstore.Bucket
parallelism int
}
func metastorePath(tenantID string, window time.Time) string {
@ -48,7 +49,8 @@ func iterStorePaths(tenantID string, start, end time.Time) iter.Seq[string] {
func NewObjectMetastore(bucket objstore.Bucket) *ObjectMetastore {
return &ObjectMetastore{
bucket: bucket,
bucket: bucket,
parallelism: 64,
}
}
@ -74,6 +76,40 @@ func (m *ObjectMetastore) Streams(ctx context.Context, start, end time.Time, mat
return m.listStreamsFromObjects(ctx, paths, predicate)
}
func (m *ObjectMetastore) StreamIDs(ctx context.Context, start, end time.Time, matchers ...*labels.Matcher) ([]string, [][]int64, error) {
tenantID, err := tenant.TenantID(ctx)
if err != nil {
return nil, nil, err
}
// Get all metastore paths for the time range
var storePaths []string
for path := range iterStorePaths(tenantID, start, end) {
storePaths = append(storePaths, path)
}
// List objects from all stores concurrently
paths, err := m.listObjectsFromStores(ctx, storePaths, start, end)
if err != nil {
return nil, nil, err
}
// Search the stream sections of the matching objects to find matching streams
predicate := predicateFromMatchers(start, end, matchers...)
streamIDs, err := m.listStreamIDsFromObjects(ctx, paths, predicate)
// Remove objects that do not contain any matching streams
for i := 0; i < len(paths); i++ {
if len(streamIDs[i]) == 0 {
paths = slices.Delete(paths, i, i+1)
streamIDs = slices.Delete(streamIDs, i, i+1)
i--
}
}
return paths, streamIDs, err
}
func (m *ObjectMetastore) DataObjects(ctx context.Context, start, end time.Time, _ ...*labels.Matcher) ([]string, error) {
tenantID, err := tenant.TenantID(ctx)
if err != nil {
@ -226,7 +262,7 @@ func (m *ObjectMetastore) listStreamsFromObjects(ctx context.Context, paths []st
streams := make(map[uint64][]*labels.Labels, 1024)
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(64)
g.SetLimit(m.parallelism)
for _, path := range paths {
g.Go(func() error {
@ -250,6 +286,31 @@ func (m *ObjectMetastore) listStreamsFromObjects(ctx context.Context, paths []st
return streamsSlice, nil
}
func (m *ObjectMetastore) listStreamIDsFromObjects(ctx context.Context, paths []string, predicate dataobj.StreamsPredicate) ([][]int64, error) {
streamIDs := make([][]int64, len(paths))
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(m.parallelism)
for i, path := range paths {
func(idx int) {
g.Go(func() error {
object := dataobj.FromBucket(m.bucket, path)
return forEachStream(ctx, object, predicate, func(stream dataobj.Stream) {
streamIDs[idx] = append(streamIDs[idx], stream.ID)
})
})
}(i)
}
if err := g.Wait(); err != nil {
return nil, err
}
return streamIDs, nil
}
func addLabels(mtx *sync.Mutex, streams map[uint64][]*labels.Labels, newLabels *labels.Labels) {
mtx.Lock()
defer mtx.Unlock()

@ -87,6 +87,21 @@ func (b *testDataBuilder) addStreamAndFlush(stream logproto.Stream) {
b.builder.Reset()
}
func TestStreamIDs(t *testing.T) {
matchers := []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"),
labels.MustNewMatcher(labels.MatchEqual, "env", "prod"),
}
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) {
paths, streamIDs, err := mstore.StreamIDs(ctx, start, end, matchers...)
require.NoError(t, err)
require.Len(t, paths, 1)
require.Len(t, streamIDs, 1)
require.Equal(t, []int64{1}, streamIDs[0])
})
}
func TestLabels(t *testing.T) {
matchers := []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"),

@ -11,8 +11,8 @@ type Literal struct {
}
// String returns the string representation of the literal value.
func (e *Literal) String() string {
switch v := e.Value.(type) {
func (l *Literal) String() string {
switch v := l.Value.(type) {
case nil:
return "NULL"
case bool:
@ -33,8 +33,8 @@ func (e *Literal) String() string {
}
// ValueType returns the kind of value represented by the literal.
func (e *Literal) ValueType() ValueType {
switch e.Value.(type) {
func (l *Literal) ValueType() ValueType {
switch l.Value.(type) {
case nil:
return ValueTypeNull
case bool:
@ -54,6 +54,51 @@ func (e *Literal) ValueType() ValueType {
}
}
// IsNull returns true if lit is a [ValueTypeNull] value.
func (l Literal) IsNull() bool {
return l.ValueType() == ValueTypeNull
}
// Str returns the value as a string. It panics if lit is not a [ValueTypeString].
func (l Literal) Str() string {
if expect, actual := ValueTypeStr, l.ValueType(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.Value.(string)
}
// Int64 returns the value as an int64. It panics if lit is not a [ValueTypeFloat].
func (l Literal) Float() float64 {
if expect, actual := ValueTypeFloat, l.ValueType(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.Value.(float64)
}
// Int returns the value as an int64. It panics if lit is not a [ValueTypeInt].
func (l Literal) Int() int64 {
if expect, actual := ValueTypeInt, l.ValueType(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.Value.(int64)
}
// Timestamp returns the value as a uint64. It panics if lit is not a [ValueTypeTimestamp].
func (l Literal) Timestamp() uint64 {
if expect, actual := ValueTypeTimestamp, l.ValueType(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.Value.(uint64)
}
// ByteArray returns the value as a byte slice. It panics if lit is not a [ValueTypeByteArray].
func (l Literal) ByteArray() []byte {
if expect, actual := ValueTypeByteArray, l.ValueType(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.Value.([]byte)
}
// Convenience function for creating a NULL literal.
func NullLiteral() Literal {
return Literal{Value: nil}

@ -8,7 +8,7 @@ import (
// A ColumnRef referenes a column within a table relation. ColumnRef only
// implements [Value].
type ColumnRef struct {
ref types.ColumnRef
Ref types.ColumnRef
}
var (
@ -18,17 +18,12 @@ var (
// Name returns the identifier of the ColumnRef, which combines the column type
// and column name being referenced.
func (c *ColumnRef) Name() string {
return c.ref.String()
return c.Ref.String()
}
// String returns [ColumnRef.Name].
func (c *ColumnRef) String() string {
return c.ref.String()
}
// Ref returns the wrapped [types.ColumnRef].
func (c *ColumnRef) Ref() types.ColumnRef {
return c.ref
return c.Ref.String()
}
// Schema returns the schema of the column being referenced.
@ -42,7 +37,7 @@ func (c *ColumnRef) isValue() {}
func NewColumnRef(name string, ty types.ColumnType) *ColumnRef {
return &ColumnRef{
ref: types.ColumnRef{
Ref: types.ColumnRef{
Column: name,
Type: ty,
},

@ -115,8 +115,8 @@ func (t *treeFormatter) convertBinOp(expr *BinOp) *tree.Node {
func (t *treeFormatter) convertColumnRef(expr *ColumnRef) *tree.Node {
return tree.NewNode("ColumnRef", "",
tree.NewProperty("column", false, expr.Ref().Column),
tree.NewProperty("type", false, expr.Ref().Type),
tree.NewProperty("column", false, expr.Ref.Column),
tree.NewProperty("type", false, expr.Ref.Type),
)
}

@ -1,8 +1,6 @@
package logical
import (
"fmt"
"github.com/grafana/loki/v3/pkg/engine/internal/types"
"github.com/grafana/loki/v3/pkg/engine/planner/schema"
)
@ -44,47 +42,6 @@ func (l Literal) Value() any {
return l.val.Value
}
// IsNull returns true if lit is a [ValueTypeNull] value.
func (l Literal) IsNull() bool {
return l.Kind() == types.ValueTypeNull
}
// Int64 returns lit's value as an int64. It panics if lit is not a
// [ValueTypeFloat].
func (l Literal) Float() float64 {
if expect, actual := types.ValueTypeFloat, l.Kind(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.val.Value.(float64)
}
// Int returns lit's value as an int64. It panics if lit is not a
// [ValueTypeInt].
func (l Literal) Int() int64 {
if expect, actual := types.ValueTypeInt, l.Kind(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.val.Value.(int64)
}
// Timestamp returns lit's value as a uint64. It panics if lit is not a
// [ValueTypeTimestamp].
func (l Literal) Timestamp() uint64 {
if expect, actual := types.ValueTypeTimestamp, l.Kind(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.val.Value.(uint64)
}
// ByteArray returns lit's value as a byte slice. It panics if lit is not a
// [ValueTypeByteArray].
func (l Literal) ByteArray() []byte {
if expect, actual := types.ValueTypeByteArray, l.Kind(); expect != actual {
panic(fmt.Sprintf("literal type is %s, not %s", actual, expect))
}
return l.val.Value.([]byte)
}
func (l *Literal) Schema() *schema.Schema {
// TODO(rfratto): schema.Schema needs to be updated to be a more general
// "type" instead.

@ -1,25 +1,143 @@
package physical
import "github.com/grafana/loki/v3/pkg/dataobj/metastore"
import (
"context"
"fmt"
"time"
"github.com/prometheus/prometheus/model/labels"
"github.com/grafana/loki/v3/pkg/dataobj/metastore"
"github.com/grafana/loki/v3/pkg/engine/internal/types"
)
var (
binOpToMatchTypeMapping = map[types.BinaryOp]labels.MatchType{
types.BinaryOpMatchStr: labels.MatchEqual,
types.BinaryOpNotMatchStr: labels.MatchNotEqual,
types.BinaryOpMatchRe: labels.MatchRegexp,
types.BinaryOpNotMatchRe: labels.MatchNotRegexp,
}
)
// Catalog is an interface that provides methods for interacting with
// storage metadata. In traditional database systems there are system tables
// providing this information (e.g. pg_catalog, ...) whereas in Loki there
// is the Metastore.
type Catalog interface {
ResolveDataObj(Expression) ([]DataObjLocation, [][]int64)
ResolveDataObj(Expression) ([]DataObjLocation, [][]int64, error)
}
// Context is the default implementation of [Catalog].
type Context struct {
metastore metastore.Metastore
ctx context.Context
metastore metastore.Metastore
from, through time.Time
}
// NewContext creates a new instance of [Context] for query planning.
func NewContext(ctx context.Context, ms metastore.Metastore, from, through time.Time) *Context {
return &Context{
ctx: ctx,
metastore: ms,
from: from,
through: through,
}
}
// ResolveDataObj resolves DataObj locations and streams IDs based on a given
// [Expression]. The expression is required to be a (tree of) [BinaryExpression]
// with a [ColumnExpression] on the left and a [LiteralExpression] on the right.
func (c *Context) ResolveDataObj(_ Expression) ([]DataObjLocation, [][]int64) {
panic("not implemented")
func (c *Context) ResolveDataObj(selector Expression) ([]DataObjLocation, [][]int64, error) {
matchers, err := expressionToMatchers(selector)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert selector expression into matchers: %w", err)
}
paths, streamIDs, err := c.metastore.StreamIDs(c.ctx, c.from, c.through, matchers...)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve data object locations: %w", err)
}
locations := make([]DataObjLocation, 0, len(paths))
for _, loc := range paths {
locations = append(locations, DataObjLocation(loc))
}
return locations, streamIDs, err
}
func expressionToMatchers(selector Expression) ([]*labels.Matcher, error) {
if selector == nil {
return nil, nil
}
switch expr := selector.(type) {
case *BinaryExpr:
switch expr.Op {
case types.BinaryOpAnd:
lhs, err := expressionToMatchers(expr.Left)
if err != nil {
return nil, err
}
rhs, err := expressionToMatchers(expr.Right)
if err != nil {
return nil, err
}
return append(lhs, rhs...), nil
case types.BinaryOpMatchStr, types.BinaryOpNotMatchStr, types.BinaryOpMatchRe, types.BinaryOpNotMatchRe:
op, err := convertBinaryOp(expr.Op)
if err != nil {
return nil, err
}
name, err := convertColumnRef(expr.Left)
if err != nil {
return nil, err
}
value, err := convertLiteral(expr.Right)
if err != nil {
return nil, err
}
lhs, err := labels.NewMatcher(op, name, value)
if err != nil {
return nil, err
}
return []*labels.Matcher{lhs}, nil
default:
return nil, fmt.Errorf("invalid binary expression in stream selector expression: %v", expr.Op.String())
}
default:
return nil, fmt.Errorf("invalid expression type in stream selector expression: %T", expr)
}
}
func convertLiteral(expr Expression) (string, error) {
l, ok := expr.(*LiteralExpr)
if !ok {
return "", fmt.Errorf("expected literal expression, got %T", expr)
}
if l.ValueType() != types.ValueTypeStr {
return "", fmt.Errorf("literal type is not a string, got %v", l.ValueType())
}
return l.Value.Str(), nil
}
func convertColumnRef(expr Expression) (string, error) {
ref, ok := expr.(*ColumnExpr)
if !ok {
return "", fmt.Errorf("expected column expression, got %T", expr)
}
if ref.Ref.Type != types.ColumnTypeLabel {
return "", fmt.Errorf("column type is not a label, got %v", ref.Ref.Type)
}
return ref.Ref.Column, nil
}
func convertBinaryOp(t types.BinaryOp) (labels.MatchType, error) {
ty, ok := binOpToMatchTypeMapping[t]
if !ok {
return -1, fmt.Errorf("invalid binary operator for matcher: %v", t)
}
return ty, nil
}
var _ Catalog = (*Context)(nil)

@ -0,0 +1,162 @@
package physical
import (
"testing"
"github.com/prometheus/prometheus/model/labels"
"github.com/stretchr/testify/require"
"github.com/grafana/loki/v3/pkg/engine/internal/types"
)
func TestContext_ConvertLiteral(t *testing.T) {
tests := []struct {
expr Expression
want string
wantErr bool
}{
{
expr: NewLiteral("foo"),
want: "foo",
},
{
expr: NewLiteral(false),
wantErr: true,
},
{
expr: NewLiteral(int64(123)),
wantErr: true,
},
{
expr: NewLiteral(uint64(123456789)),
wantErr: true,
},
{
expr: newColumnExpr("foo", types.ColumnTypeLabel),
wantErr: true,
},
{
expr: &BinaryExpr{
Left: newColumnExpr("foo", types.ColumnTypeLabel),
Right: NewLiteral("foo"),
Op: types.BinaryOpEq,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.expr.String(), func(t *testing.T) {
got, err := convertLiteral(tt.expr)
if tt.wantErr {
require.Error(t, err)
t.Log(err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
}
func TestContext_ConvertColumnRef(t *testing.T) {
tests := []struct {
expr Expression
want string
wantErr bool
}{
{
expr: newColumnExpr("foo", types.ColumnTypeLabel),
want: "foo",
},
{
expr: newColumnExpr("foo", types.ColumnTypeAmbiguous),
wantErr: true,
},
{
expr: newColumnExpr("foo", types.ColumnTypeBuiltin),
wantErr: true,
},
{
expr: NewLiteral(false),
wantErr: true,
},
{
expr: &BinaryExpr{
Left: newColumnExpr("foo", types.ColumnTypeLabel),
Right: NewLiteral("foo"),
Op: types.BinaryOpEq,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.expr.String(), func(t *testing.T) {
got, err := convertColumnRef(tt.expr)
if tt.wantErr {
require.Error(t, err)
t.Log(err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
}
func TestContext_ExpressionToMatchers(t *testing.T) {
tests := []struct {
expr Expression
want []*labels.Matcher
wantErr bool
}{
{
expr: newColumnExpr("foo", types.ColumnTypeLabel),
wantErr: true,
},
{
expr: NewLiteral("foo"),
wantErr: true,
},
{
expr: &BinaryExpr{
Left: newColumnExpr("foo", types.ColumnTypeLabel),
Right: NewLiteral("bar"),
Op: types.BinaryOpMatchStr,
},
want: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"),
},
},
{
expr: &BinaryExpr{
Left: &BinaryExpr{
Left: newColumnExpr("foo", types.ColumnTypeLabel),
Right: NewLiteral("bar"),
Op: types.BinaryOpMatchStr,
},
Right: &BinaryExpr{
Left: newColumnExpr("bar", types.ColumnTypeLabel),
Right: NewLiteral("baz"),
Op: types.BinaryOpNotMatchStr,
},
Op: types.BinaryOpAnd,
},
want: []*labels.Matcher{
labels.MustNewMatcher(labels.MatchEqual, "foo", "bar"),
labels.MustNewMatcher(labels.MatchNotEqual, "bar", "baz"),
},
},
}
for _, tt := range tests {
t.Run(tt.expr.String(), func(t *testing.T) {
got, err := expressionToMatchers(tt.expr)
if tt.wantErr {
require.Error(t, err)
t.Log(err)
} else {
require.NoError(t, err)
require.ElementsMatch(t, tt.want, got)
}
})
}
}

@ -139,12 +139,12 @@ func NewLiteral(value any) *LiteralExpr {
// ColumnExpr is an expression that implements the [ColumnExpr] interface.
type ColumnExpr struct {
ref types.ColumnRef
Ref types.ColumnRef
}
func newColumnExpr(column string, ty types.ColumnType) *ColumnExpr {
return &ColumnExpr{
ref: types.ColumnRef{
Ref: types.ColumnRef{
Column: column,
Type: ty,
},
@ -157,15 +157,10 @@ func (e *ColumnExpr) isColumnExpr() {}
// String returns the string representation of the column expression.
// It contains of the name of the column and its type, joined by a dot (`.`).
func (e *ColumnExpr) String() string {
return e.ref.String()
return e.Ref.String()
}
// ID returns the type of the [ColumnExpr].
func (e *ColumnExpr) Type() ExpressionType {
return ExprTypeColumn
}
// Ref returns the wrapped [types.ColumnRef].
func (e *ColumnExpr) Ref() types.ColumnRef {
return e.ref
}

@ -26,7 +26,7 @@ func TestExpressionTypes(t *testing.T) {
name: "BinaryExpression",
expr: &BinaryExpr{
Op: types.BinaryOpEq,
Left: &ColumnExpr{ref: types.ColumnRef{Column: "col", Type: types.ColumnTypeBuiltin}},
Left: &ColumnExpr{Ref: types.ColumnRef{Column: "col", Type: types.ColumnTypeBuiltin}},
Right: &LiteralExpr{Value: types.StringLiteral("foo")},
},
expected: ExprTypeBinary,
@ -38,7 +38,7 @@ func TestExpressionTypes(t *testing.T) {
},
{
name: "ColumnExpression",
expr: &ColumnExpr{ref: types.ColumnRef{Column: "col", Type: types.ColumnTypeBuiltin}},
expr: &ColumnExpr{Ref: types.ColumnRef{Column: "col", Type: types.ColumnTypeBuiltin}},
expected: ExprTypeColumn,
},
}

@ -77,7 +77,7 @@ func canApplyPredicate(predicate Expression) bool {
case *BinaryExpr:
return canApplyPredicate(pred.Left) && canApplyPredicate(pred.Right)
case *ColumnExpr:
return pred.ref.Type == types.ColumnTypeBuiltin || pred.ref.Type == types.ColumnTypeMetadata
return pred.Ref.Type == types.ColumnTypeBuiltin || pred.Ref.Type == types.ColumnTypeMetadata
case *LiteralExpr:
return true
default:

@ -60,7 +60,7 @@ func (p *Planner) convertPredicate(inst logical.Value) Expression {
Op: inst.Op,
}
case *logical.ColumnRef:
return &ColumnExpr{ref: inst.Ref()}
return &ColumnExpr{Ref: inst.Ref}
case *logical.Literal:
return NewLiteral(inst.Value())
default:
@ -85,7 +85,10 @@ func (p *Planner) process(inst logical.Value) ([]Node, error) {
// Convert [logical.MakeTable] into one or more [DataObjScan] nodes.
func (p *Planner) processMakeTable(lp *logical.MakeTable) ([]Node, error) {
objects, streams := p.catalog.ResolveDataObj(p.convertPredicate(lp.Selector))
objects, streams, err := p.catalog.ResolveDataObj(p.convertPredicate(lp.Selector))
if err != nil {
return nil, err
}
nodes := make([]Node, 0, len(objects))
for i := range objects {
node := &DataObjScan{
@ -123,7 +126,7 @@ func (p *Planner) processSort(lp *logical.Sort) ([]Node, error) {
order = DESC
}
node := &SortMerge{
Column: &ColumnExpr{ref: lp.Column.Ref()},
Column: &ColumnExpr{Ref: lp.Column.Ref},
Order: order,
}
p.plan.addNode(node)

@ -14,14 +14,14 @@ type catalog struct {
}
// ResolveDataObj implements Catalog.
func (t *catalog) ResolveDataObj(Expression) ([]DataObjLocation, [][]int64) {
func (t *catalog) ResolveDataObj(Expression) ([]DataObjLocation, [][]int64, error) {
objects := make([]DataObjLocation, 0, len(t.streamsByObject))
streams := make([][]int64, 0, len(t.streamsByObject))
for o, s := range t.streamsByObject {
objects = append(objects, DataObjLocation(o))
streams = append(streams, s)
}
return objects, streams
return objects, streams, nil
}
var _ Catalog = (*catalog)(nil)

Loading…
Cancel
Save