Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(engine): Add first stage of physical query planning #16891

Merged
merged 10 commits into from
Mar 26, 2025
32 changes: 32 additions & 0 deletions pkg/engine/internal/types/column.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Go package naming usually discourages naming a package types like this:

Don’t use a single package for all your APIs. Many well-intentioned programmers put all the interfaces exposed by their program into a single package named api, types, or interfaces, thinking it makes it easier to find the entry points to their code base. This is a mistake. Such packages suffer from the same problems as those named util or common, growing without bound, providing no guidance to users, accumulating dependencies, and colliding with other imports. Break them up, perhaps using directories to separate public packages from implementation.

I think it's fine for now since it's internal, but we should keep an eye out for opportunities to organize by domain. Some things like ColumnType might be better for the schema package, but I'm not sure about the other types yet.


import "fmt"

// ColumnType denotes the column type for a [ColumnRef].
type ColumnType int

// Recognized values of [ColumnType].
const (
// ColumnTypeInvalid indicates an invalid column type.
ColumnTypeInvalid ColumnType = iota

ColumnTypeBuiltin // ColumnTypeBuiltin represents a builtin column (such as timestamp).
ColumnTypeLabel // ColumnTypeLabel represents a column from a stream label.
ColumnTypeMetadata // ColumnTypeMetadata represents a column from a log metadata.
)

// String returns a human-readable representation of the column type.
func (ct ColumnType) String() string {
switch ct {
case ColumnTypeInvalid:
return typeInvalid
case ColumnTypeBuiltin:
return "builtin"
case ColumnTypeLabel:
return "label"
case ColumnTypeMetadata:
return "metadata"
default:
return fmt.Sprintf("ColumnType(%d)", ct)
}
}
38 changes: 38 additions & 0 deletions pkg/engine/internal/types/literal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package types

import "fmt"

// LiteralKind denotes the kind of [Literal] value.
type LiteralKind int

// Recognized values of [LiteralKind].
const (
// LiteralKindInvalid indicates an invalid literal value.
LiteralKindInvalid LiteralKind = iota

LiteralKindNull // NULL literal value.
LiteralKindString // String literal value.
LiteralKindInt64 // 64-bit integer literal value.
LiteralKindUint64 // 64-bit unsigned integer literal value.
LiteralKindByteArray // Byte array literal value.
)

// String returns the string representation of the LiteralKind.
func (k LiteralKind) String() string {
switch k {
case LiteralKindInvalid:
return typeInvalid
case LiteralKindNull:
return "null"
case LiteralKindString:
return "string"
case LiteralKindInt64:
return "int64"
case LiteralKindUint64:
return "uint64"
case LiteralKindByteArray:
return "[]byte"
default:
return fmt.Sprintf("LiteralKind(%d)", k)
}
}
108 changes: 108 additions & 0 deletions pkg/engine/internal/types/operators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package types

import "fmt"

// UnaryOp denotes the kind of [UnaryOp] operation to perform.
type UnaryOp uint32

// Recognized values of [UnaryOp].
const (
// UnaryOpKindInvalid indicates an invalid unary operation.
UnaryOpInvalid UnaryOp = iota

UnaryOpNot // Logical NOT operation (!).
UnaryOpAbs // Mathematical absolute operation (abs).
)

// String returns the string representation of the UnaryOp.
func (t UnaryOp) String() string {
switch t {
case UnaryOpInvalid:
return typeInvalid
case UnaryOpNot:
return "NOT"
case UnaryOpAbs:
return "ABS"
default:
panic(fmt.Sprintf("unknown unary operator %d", t))
}
}

// BinaryOp denotes the kind of [BinaryOp] operation to perform.
type BinaryOp uint32

// Recognized values of [BinaryOp].
const (
// BinaryOpInvalid indicates an invalid binary operation.
BinaryOpInvalid BinaryOp = iota

BinaryOpEq // Equality comparison (==).
BinaryOpNeq // Inequality comparison (!=).
BinaryOpGt // Greater than comparison (>).
BinaryOpGte // Greater than or equal comparison (>=).
BinaryOpLt // Less than comparison (<).
BinaryOpLte // Less than or equal comparison (<=).
BinaryOpAnd // Logical AND operation (&&).
BinaryOpOr // Logical OR operation (||).
BinaryOpXor // Logical XOR operation (^).
BinaryOpNot // Logical NOT operation (!).

BinaryOpAdd // Addition operation (+).
BinaryOpSub // Subtraction operation (-).
BinaryOpMul // Multiplication operation (*).
BinaryOpDiv // Division operation (/).
BinaryOpMod // Modulo operation (%).

BinaryOpMatchStr // String matching operation (|=).
BinaryOpNotMatchStr // String non-matching operation (!=).
BinaryOpMatchRe // Regular expression matching operation (|~).
BinaryOpNotMatchRe // Regular expression non-matching operation (!~).
)

// String returns a human-readable representation of the binary operation kind.
func (t BinaryOp) String() string {
switch t {
case BinaryOpInvalid:
return typeInvalid
case BinaryOpEq:
return "EQ"
case BinaryOpNeq:
return "NEQ" // convenience for NOT(EQ(expr))
case BinaryOpGt:
return "GT"
case BinaryOpGte:
return "GTE"
case BinaryOpLt:
return "LT" // convenience for NOT(GTE(expr))
case BinaryOpLte:
return "LTE" // convenience for NOT(GT(expr))
case BinaryOpAnd:
return "AND"
case BinaryOpOr:
return "OR"
case BinaryOpXor:
return "XOR"
case BinaryOpNot:
return "NOT"
case BinaryOpAdd:
return "ADD"
case BinaryOpSub:
return "SUB"
case BinaryOpMul:
return "MUL"
case BinaryOpDiv:
return "DIV"
case BinaryOpMod:
return "MOD"
case BinaryOpMatchStr:
return "MATCH_STR"
case BinaryOpNotMatchStr:
return "NOT_MATCH_STR" // convenience for NOT(MATCH_STR(...))
case BinaryOpMatchRe:
return "MATCH_RE"
case BinaryOpNotMatchRe:
return "NOT_MATCH_RE" // convenience for NOT(MATCH_RE(...))
default:
panic(fmt.Sprintf("unknown binary operator %d", t))
}
}
19 changes: 19 additions & 0 deletions pkg/engine/internal/types/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package types

const (
typeInvalid = "invalid"
)

// ValueType represents the type of a value, which can either be a literal value, or a column value.
type ValueType uint32

const (
ValueTypeInvalid ValueType = iota // zero-value is an invalid type

ValueTypeNull // NULL value.
ValueTypeBool // Boolean value
ValueTypeInt // Signed 64bit integer value
ValueTypeTimestamp // Unsigned 64bit integer value (nanosecond timestamp)
ValueTypeStr // String value
ValueTypeBytes // Byte-slice value
)
34 changes: 3 additions & 31 deletions pkg/engine/planner/logical/column_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,15 @@ package logical
import (
"fmt"

"github.com/grafana/loki/v3/pkg/engine/internal/types"
"github.com/grafana/loki/v3/pkg/engine/planner/schema"
)

// ColumnType denotes the column type for a [ColumnRef].
type ColumnType int

// Recognized values of [ColumnType].
const (
// ColumnTypeInvalid indicates an invalid column type.
ColumnTypeInvalid ColumnType = iota

ColumnTypeBuiltin // ColumnTypeBuiltin represents a builtin column (such as timestamp).
ColumnTypeLabel // ColumnTypeLabel represents a column from a stream label.
ColumnTypeMetadata // ColumnTypeMetadata represents a column from a log metadata.
)

// String returns a human-readable representation of the column type.
func (ct ColumnType) String() string {
switch ct {
case ColumnTypeInvalid:
return "invalid"
case ColumnTypeBuiltin:
return "builtin"
case ColumnTypeLabel:
return "label"
case ColumnTypeMetadata:
return "metadata"
default:
return fmt.Sprintf("ColumnType(%d)", ct)
}
}

// A ColumnRef referenes a column within a table relation. ColumnRef only
// implements [Value].
type ColumnRef struct {
Column string // Name of the column being referenced.
Type ColumnType // Type of the column being referenced.
Column string // Name of the column being referenced.
Type types.ColumnType // Type of the column being referenced.
}

var (
Expand Down
19 changes: 10 additions & 9 deletions pkg/engine/planner/logical/format_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/grafana/loki/v3/pkg/engine/internal/types"
"github.com/grafana/loki/v3/pkg/engine/planner/schema"
)

Expand All @@ -23,16 +24,16 @@ func TestFormatSimpleQuery(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: ColumnTypeLabel},
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Op: BinOpKindEq,
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: ColumnTypeMetadata},
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Op: BinOpKindGt,
Op: types.BinaryOpGt,
},
)

Expand Down Expand Up @@ -63,18 +64,18 @@ func TestFormatSortQuery(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: ColumnTypeLabel},
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Op: BinOpKindEq,
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: ColumnTypeMetadata},
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Op: BinOpKindGt,
Op: types.BinaryOpGt,
},
).Sort(ColumnRef{Column: "age", Type: ColumnTypeMetadata}, true, false)
).Sort(ColumnRef{Column: "age", Type: types.ColumnTypeMetadata}, true, false)

var sb strings.Builder
PrintTree(&sb, b.Value())
Expand Down
12 changes: 7 additions & 5 deletions pkg/engine/planner/logical/logical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"testing"

"github.com/stretchr/testify/require"

"github.com/grafana/loki/v3/pkg/engine/internal/types"
)

func TestPlan_String(t *testing.T) {
Expand All @@ -15,18 +17,18 @@ func TestPlan_String(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: ColumnTypeLabel},
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Op: BinOpKindEq,
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: ColumnTypeMetadata},
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Op: BinOpKindGt,
Op: types.BinaryOpGt,
},
).Sort(ColumnRef{Column: "age", Type: ColumnTypeMetadata}, true, false)
).Sort(ColumnRef{Column: "age", Type: types.ColumnTypeMetadata}, true, false)

// Convert to SSA
ssaForm, err := b.ToPlan()
Expand Down
Loading