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): Unify Literal and ColumnRef across logical and physical plan #16927

Merged
merged 2 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/engine/internal/types/column.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,15 @@ func (ct ColumnType) String() string {
return fmt.Sprintf("ColumnType(%d)", ct)
}
}

// A ColumnRef referenes a column within a table relation.
type ColumnRef struct {
Column string // Name of the column being referenced.
Type ColumnType // Type of the column being referenced.
}

// Name returns the identifier of the ColumnRef, which combines the column type
// and column name being referenced.
func (c *ColumnRef) String() string {
return fmt.Sprintf("%s.%s", c.Type, c.Column)
}
112 changes: 82 additions & 30 deletions pkg/engine/internal/types/literal.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,90 @@
package types

import "fmt"

// LiteralKind denotes the kind of [Literal] value.
type LiteralKind int
import (
"fmt"
"strconv"
)

// Recognized values of [LiteralKind].
const (
// LiteralKindInvalid indicates an invalid literal value.
LiteralKindInvalid LiteralKind = iota
// Literal is holds a value of [ValueType].
type Literal struct {
Value any
}

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 literal value.
func (e *Literal) String() string {
switch v := e.Value.(type) {
case nil:
return "NULL"
case bool:
return strconv.FormatBool(v)
case string:
return fmt.Sprintf(`"%s"`, v)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case int64:
return strconv.FormatInt(v, 10)
case uint64:
return strconv.FormatUint(v, 10)
case []byte:
return fmt.Sprintf("%v", v)
default:
return "invalid"
}
}

// 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"
// ValueType returns the kind of value represented by the literal.
func (e *Literal) ValueType() ValueType {
switch e.Value.(type) {
case nil:
return ValueTypeNull
case bool:
return ValueTypeBool
case string:
return ValueTypeStr
case float64:
return ValueTypeFloat
case int64:
return ValueTypeInt
case uint64:
return ValueTypeTimestamp
case []byte:
return ValueTypeByteArray
default:
return fmt.Sprintf("LiteralKind(%d)", k)
return ValueTypeInvalid
}
}

// Convenience function for creating a NULL literal.
func NullLiteral() Literal {
return Literal{Value: nil}
}

// Convenience function for creating a bool literal.
func BoolLiteral(v bool) Literal {
return Literal{Value: v}
}

// Convenience function for creating a string literal.
func StringLiteral(v string) Literal {
return Literal{Value: v}
}

// Convenience function for creating a float literal.
func FloatLiteral(v float64) Literal {
return Literal{Value: v}
}

// Convenience function for creating a timestamp literal.
func TimestampLiteral(v uint64) Literal {
return Literal{Value: v}
}

// Convenience function for creating an integer literal.
func IntLiteral(v int64) Literal {
return Literal{Value: v}
}

// Convenience function for creating a byte array literal.
func ByteArrayLiteral(v []byte) Literal {
return Literal{Value: v}
}
83 changes: 83 additions & 0 deletions pkg/engine/internal/types/literal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package types

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestLiteralRepresentation(t *testing.T) {
testCases := []struct {
name string
literal Literal
expected string
}{
{
name: "null literal",
literal: NullLiteral(),
expected: "NULL",
},
{
name: "bool literal - true",
literal: BoolLiteral(true),
expected: "true",
},
{
name: "bool literal - false",
literal: BoolLiteral(false),
expected: "false",
},
{
name: "string literal",
literal: StringLiteral("test"),
expected: `"test"`,
},
{
name: "string literal with quotes",
literal: StringLiteral(`test "quoted" string`),
expected: `"test "quoted" string"`,
},
{
name: "float literal - integer value",
literal: FloatLiteral(42.0),
expected: "42",
},
{
name: "float literal - decimal value",
literal: FloatLiteral(3.14159),
expected: "3.14159",
},
{
name: "int literal - positive",
literal: IntLiteral(123),
expected: "123",
},
{
name: "int literal - negative",
literal: IntLiteral(-456),
expected: "-456",
},
{
name: "timestamp literal",
literal: TimestampLiteral(1625097600000),
expected: "1625097600000",
},
{
name: "byte array literal",
literal: ByteArrayLiteral([]byte("hello")),
expected: "[104 101 108 108 111]",
},
{
name: "invalid literal",
literal: Literal{Value: make(chan int)}, // channels are not supported types
expected: "invalid",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.literal.String()
require.Equal(t, tc.expected, result)
})
}
}
30 changes: 29 additions & 1 deletion pkg/engine/internal/types/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,36 @@ const (

ValueTypeNull // NULL value.
ValueTypeBool // Boolean value
ValueTypeFloat // 64bit floating point value
ValueTypeInt // Signed 64bit integer value
ValueTypeTimestamp // Unsigned 64bit integer value (nanosecond timestamp)
ValueTypeStr // String value
ValueTypeBytes // Byte-slice value
ValueTypeByteArray // Byte-slice value
// ValueTypeBytes
// ValueTypeDate
// ValueTypeDuration
)

// String returns the string representation of the LiteralKind.
func (t ValueType) String() string {
switch t {
case ValueTypeInvalid:
return typeInvalid
case ValueTypeNull:
return "null"
case ValueTypeBool:
return "bool"
case ValueTypeFloat:
return "float"
case ValueTypeInt:
return "int"
case ValueTypeTimestamp:
return "timestamp"
case ValueTypeStr:
return "string"
case ValueTypeByteArray:
return "[]byte"
default:
return typeInvalid
}
}
25 changes: 19 additions & 6 deletions pkg/engine/planner/logical/column_ref.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package logical

import (
"fmt"

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

// 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 types.ColumnType // Type of the column being referenced.
ref types.ColumnRef
}

var (
Expand All @@ -21,11 +18,18 @@ var (
// Name returns the identifier of the ColumnRef, which combines the column type
// and column name being referenced.
func (c *ColumnRef) Name() string {
return fmt.Sprintf("%s.%s", c.Type, c.Column)
return c.ref.String()
}

// String returns [ColumnRef.Name].
func (c *ColumnRef) String() string { return c.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
}

// Schema returns the schema of the column being referenced.
func (c *ColumnRef) Schema() *schema.Schema {
Expand All @@ -35,3 +39,12 @@ func (c *ColumnRef) Schema() *schema.Schema {
}

func (c *ColumnRef) isValue() {}

func NewColumnRef(name string, ty types.ColumnType) *ColumnRef {
return &ColumnRef{
ref: types.ColumnRef{
Column: name,
Type: ty,
},
}
}
22 changes: 11 additions & 11 deletions pkg/engine/planner/logical/format_tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ func TestFormatSimpleQuery(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Left: NewColumnRef("app", types.ColumnTypeLabel),
Right: NewLiteral("users"),
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Left: NewColumnRef("age", types.ColumnTypeMetadata),
Right: NewLiteral[int64](21),
Op: types.BinaryOpGt,
},
)
Expand All @@ -47,7 +47,7 @@ func TestFormatSimpleQuery(t *testing.T) {
Select
│ └── BinOp op=GT
│ ├── ColumnRef #metadata.age
│ └── Literal value=21 kind=int64
│ └── Literal value=21 kind=int
└── MakeTable
└── BinOp op=EQ
├── ColumnRef #label.app
Expand All @@ -64,18 +64,18 @@ func TestFormatSortQuery(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Left: NewColumnRef("app", types.ColumnTypeLabel),
Right: NewLiteral("users"),
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Left: NewColumnRef("age", types.ColumnTypeMetadata),
Right: NewLiteral[int64](21),
Op: types.BinaryOpGt,
},
).Sort(ColumnRef{Column: "age", Type: types.ColumnTypeMetadata}, true, false)
).Sort(*NewColumnRef("age", types.ColumnTypeMetadata), true, false)

var sb strings.Builder
PrintTree(&sb, b.Value())
Expand All @@ -89,7 +89,7 @@ Sort direction=asc nulls=last
└── Select
│ └── BinOp op=GT
│ ├── ColumnRef #metadata.age
│ └── Literal value=21 kind=int64
│ └── Literal value=21 kind=int
└── MakeTable
└── BinOp op=EQ
├── ColumnRef #label.app
Expand Down
10 changes: 5 additions & 5 deletions pkg/engine/planner/logical/logical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ func TestPlan_String(t *testing.T) {
b := NewBuilder(
&MakeTable{
Selector: &BinOp{
Left: &ColumnRef{Column: "app", Type: types.ColumnTypeLabel},
Right: LiteralString("users"),
Left: NewColumnRef("app", types.ColumnTypeLabel),
Right: NewLiteral("users"),
Op: types.BinaryOpEq,
},
},
).Select(
&BinOp{
Left: &ColumnRef{Column: "age", Type: types.ColumnTypeMetadata},
Right: LiteralInt64(21),
Left: NewColumnRef("age", types.ColumnTypeMetadata),
Right: NewLiteral[int64](21),
Op: types.BinaryOpGt,
},
).Sort(ColumnRef{Column: "age", Type: types.ColumnTypeMetadata}, true, false)
).Sort(*NewColumnRef("age", types.ColumnTypeMetadata), true, false)

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