Skip to content

Commit

Permalink
feat(type): Add support for interval compound literal (#44)
Browse files Browse the repository at this point in the history
* feat(type): Add support for interval compound literal

* Fix pre-commit errors and rename test case

* Address review comments

* Address review comments

* Address review comments

* Address review comments
  • Loading branch information
anshuldata authored Aug 26, 2024
1 parent 597afdb commit 58e4ba0
Show file tree
Hide file tree
Showing 10 changed files with 800 additions and 9 deletions.
143 changes: 143 additions & 0 deletions expr/interval_compound_literal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package expr

import (
"errors"
"fmt"

"github.com/substrait-io/substrait-go/proto"
"github.com/substrait-io/substrait-go/types"
)

// IntervalCompoundLiteral creates an interval compound literal
type IntervalCompoundLiteral struct {
Years int32
Months int32
Days int32
Seconds int32
SubSeconds int64
SubSecondPrecision types.TimePrecision
Nullability types.Nullability
}

func (m IntervalCompoundLiteral) getType() types.Type {
return types.NewIntervalCompoundType().WithPrecision(m.SubSecondPrecision).WithNullability(m.Nullability)
}

func (m IntervalCompoundLiteral) ToProtoLiteral() *proto.Expression_Literal {
t := m.getType()
intrCompPB := &proto.Expression_Literal_IntervalCompound{}

if m.Years != 0 || m.Months != 0 {
yearToMonthProto := &proto.Expression_Literal_IntervalYearToMonth{
Years: m.Years,
Months: m.Months,
}
intrCompPB.IntervalYearToMonth = yearToMonthProto
}

if m.Days != 0 || m.Seconds != 0 || m.SubSeconds != 0 {
dayToSecondProto := &proto.Expression_Literal_IntervalDayToSecond{
Days: m.Days,
Seconds: m.Seconds,
PrecisionMode: &proto.Expression_Literal_IntervalDayToSecond_Precision{Precision: m.SubSecondPrecision.ToProtoVal()},
Subseconds: m.SubSeconds,
}
intrCompPB.IntervalDayToSecond = dayToSecondProto
}

return &proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: intrCompPB},
Nullable: t.GetNullability() == types.NullabilityNullable,
TypeVariationReference: t.GetTypeVariationReference(),
}
}

func (m IntervalCompoundLiteral) ToProto() *proto.Expression {
return &proto.Expression{RexType: &proto.Expression_Literal_{
Literal: m.ToProtoLiteral(),
}}
}

func intervalCompoundLiteralFromProto(l *proto.Expression_Literal) Literal {
icLiteral := IntervalCompoundLiteral{Nullability: getNullability(l.Nullable)}
yearToMonth := l.GetIntervalCompound().GetIntervalYearToMonth()
if yearToMonth != nil {
icLiteral.Years = yearToMonth.Years
icLiteral.Months = yearToMonth.Months
}
dayToSecond := l.GetIntervalCompound().GetIntervalDayToSecond()
if dayToSecond == nil {
// no day to second part
return icLiteral
}
err := validateIntervalDayToSecondProto(dayToSecond)
if err != nil {
return nil
}

// get subSecond/precision value from proto. To get value it takes care of deprecated microseconds
precision, subSeconds, err := intervalCompoundPrecisionSubSecondsFromProto(dayToSecond)
if err != nil {
return nil
}
icLiteral.Days = dayToSecond.Days
icLiteral.Seconds = dayToSecond.Seconds
icLiteral.SubSeconds = subSeconds
icLiteral.SubSecondPrecision = precision
return icLiteral
}

func (IntervalCompoundLiteral) isRootRef() {}
func (m IntervalCompoundLiteral) GetType() types.Type { return m.getType() }
func (m IntervalCompoundLiteral) String() string {
return fmt.Sprintf("%s(years:%d,months:%d, days:%d, seconds:%d subseconds:%d)",
m.getType(), m.Years, m.Months, m.Days, m.Seconds, m.SubSeconds)
}
func (m IntervalCompoundLiteral) Equals(rhs Expression) bool {
if other, ok := rhs.(IntervalCompoundLiteral); ok {
return m.getType().Equals(other.GetType()) && (m == other)
}
return false
}

func (m IntervalCompoundLiteral) ToProtoFuncArg() *proto.FunctionArgument {
return &proto.FunctionArgument{
ArgType: &proto.FunctionArgument_Value{Value: m.ToProto()},
}
}

func (m IntervalCompoundLiteral) Visit(VisitFunc) Expression { return m }
func (IntervalCompoundLiteral) IsScalar() bool { return true }

func validateIntervalDayToSecondProto(idts *proto.Expression_Literal_IntervalDayToSecond) error {
if idts.PrecisionMode == nil {
// error, precision mode must be set for intervalCompound
return errors.New("missing precision mode for interval compound")
}
if _, ok := idts.PrecisionMode.(*proto.Expression_Literal_IntervalDayToSecond_Microseconds); ok {
// if microsecond precision then subseconds must be set to zero
if idts.Subseconds > 0 {
return errors.New("both deprecated microseconds and subseconds can't be non zero")
}
}
return nil
}

func intervalCompoundPrecisionSubSecondsFromProto(protoVal *proto.Expression_Literal_IntervalDayToSecond) (types.TimePrecision, int64, error) {
var precisionVal int32
var subSecondVal int64
switch pmt := protoVal.PrecisionMode.(type) {
case *proto.Expression_Literal_IntervalDayToSecond_Precision:
precisionVal = pmt.Precision
subSecondVal = protoVal.Subseconds
case *proto.Expression_Literal_IntervalDayToSecond_Microseconds:
// deprecated field microsecond is set, treat its value subsecond
precisionVal = types.PrecisionMicroSeconds.ToProtoVal()
subSecondVal = int64(pmt.Microseconds)
}
precision, err := types.ProtoToTimePrecision(precisionVal)
if err != nil {
return types.PrecisionUnknown, 0, err
}
return precision, subSecondVal, nil
}
240 changes: 240 additions & 0 deletions expr/interval_compound_literal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package expr

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/substrait-io/substrait-go/proto"
"github.com/substrait-io/substrait-go/types"
"google.golang.org/protobuf/testing/protocmp"
)

const (
yearVal int32 = 1
monthVal int32 = 10
dayVal int32 = 100
secondsVal int32 = 1000
subSecondsVal int64 = 10000
negativeInt32Val int32 = -5
negativeInt64Val int64 = -100000
)

func TestIntervalCompoundToProto(t *testing.T) {
// precision and nullability belong to type. In type unit tests they are already tested
// for different values so no need to test for multiple values
precisionVal := types.PrecisionNanoSeconds
nanoSecPrecision := &proto.Expression_Literal_IntervalDayToSecond_Precision{Precision: precisionVal.ToProtoVal()}
nullable := true
nullability := types.NullabilityNullable

for _, tc := range []struct {
name string
inputLiteral IntervalCompoundLiteral
expectedExpressionLiteral *proto.Expression_Literal_IntervalCompound_
}{
{"WithOnlyYearAndMonth",
IntervalCompoundLiteral{Nullability: nullability, Years: yearVal, Months: monthVal},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: yearVal, Months: monthVal},
}},
},
{"WithOnlyYearAndMonthNegativeVal",
IntervalCompoundLiteral{Nullability: nullability, Years: negativeInt32Val, Months: negativeInt32Val},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: negativeInt32Val, Months: negativeInt32Val},
}},
},
{"WithOnlyDayToSecond",
IntervalCompoundLiteral{Nullability: nullability, Days: dayVal, Seconds: secondsVal, SubSeconds: subSecondsVal, SubSecondPrecision: precisionVal},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{
Days: dayVal, Seconds: secondsVal, PrecisionMode: nanoSecPrecision, Subseconds: subSecondsVal,
},
}},
},
{"WithOnlyDayToSecondNegativeVal",
IntervalCompoundLiteral{Nullability: nullability, Days: negativeInt32Val, Seconds: negativeInt32Val, SubSeconds: negativeInt64Val, SubSecondPrecision: precisionVal},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{
Days: negativeInt32Val, Seconds: negativeInt32Val, PrecisionMode: nanoSecPrecision, Subseconds: negativeInt64Val,
},
}},
},
{"WithBothYearToMonthAndDayToSecond",
IntervalCompoundLiteral{Nullability: nullability, Years: yearVal, Months: monthVal, Days: dayVal, Seconds: secondsVal, SubSeconds: subSecondsVal, SubSecondPrecision: precisionVal},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: yearVal, Months: monthVal},
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{
Days: dayVal, Seconds: secondsVal, PrecisionMode: nanoSecPrecision, Subseconds: subSecondsVal,
},
}},
},
{"WithBothYearToMonthAndDayToSecondAllNegativeVal",
IntervalCompoundLiteral{Nullability: nullability, Years: negativeInt32Val, Months: negativeInt32Val, Days: negativeInt32Val, Seconds: negativeInt32Val, SubSeconds: negativeInt64Val, SubSecondPrecision: precisionVal},
&proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: negativeInt32Val, Months: negativeInt32Val},
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{
Days: negativeInt32Val, Seconds: negativeInt32Val, PrecisionMode: nanoSecPrecision, Subseconds: negativeInt64Val,
},
}},
},
} {
t.Run(tc.name, func(t *testing.T) {
expectedProtoExpression := &proto.Expression{RexType: &proto.Expression_Literal_{Literal: &proto.Expression_Literal{LiteralType: tc.expectedExpressionLiteral, Nullable: nullable}}}
gotExpressionProto := tc.inputLiteral.ToProto()
assert.NotNil(t, gotExpressionProto)
if diff := cmp.Diff(gotExpressionProto, expectedProtoExpression, protocmp.Transform()); diff != "" {
t.Errorf("proto didn't match, diff:\n%v", diff)
}
// verify ToProtoFuncArg
funcArgProto := &proto.FunctionArgument{
ArgType: &proto.FunctionArgument_Value{Value: gotExpressionProto},
}
if diff := cmp.Diff(tc.inputLiteral.ToProtoFuncArg(), funcArgProto, protocmp.Transform()); diff != "" {
t.Errorf("expression proto didn't match, diff:\n%v", diff)
}
})

}
}

func TestIntervalCompoundFromProto(t *testing.T) {
precisionNanoVal := types.PrecisionNanoSeconds
nanoSecPrecision := &proto.Expression_Literal_IntervalDayToSecond_Precision{Precision: precisionNanoVal.ToProtoVal()}

var microSecondVal int32 = 70
deprecatedMicroSecPrecision := &proto.Expression_Literal_IntervalDayToSecond_Microseconds{
Microseconds: microSecondVal}
nullable := true
nullability := types.NullabilityNullable
for _, tc := range []struct {
name string
constructedProto *proto.Expression_Literal
expectedLiteral IntervalCompoundLiteral
}{
{"NoPartsValue",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability},
},
{"OnlyYearAndMonth",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: yearVal, Months: monthVal},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Years: yearVal, Months: monthVal},
},
{"OnlyYearAndMonthNegativeVal",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: negativeInt32Val, Months: negativeInt32Val},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Years: negativeInt32Val, Months: negativeInt32Val},
},
{"OnlyDayToSecond",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Days: dayVal, Seconds: secondsVal,
PrecisionMode: nanoSecPrecision, Subseconds: subSecondsVal},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Days: dayVal, Seconds: secondsVal, SubSeconds: subSecondsVal, SubSecondPrecision: precisionNanoVal},
},
{"OnlyDayToSecondNegativeVal",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Days: negativeInt32Val, Seconds: negativeInt32Val,
PrecisionMode: nanoSecPrecision, Subseconds: negativeInt64Val},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Days: negativeInt32Val, Seconds: negativeInt32Val, SubSeconds: negativeInt64Val, SubSecondPrecision: precisionNanoVal},
},
{"BothYearToMonthAndDayToSecond",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: yearVal, Months: monthVal},
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Days: dayVal, Seconds: secondsVal,
PrecisionMode: nanoSecPrecision, Subseconds: subSecondsVal},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Years: yearVal, Months: monthVal, Days: dayVal, Seconds: secondsVal, SubSeconds: subSecondsVal, SubSecondPrecision: precisionNanoVal},
},
{"BothYearToMonthAndDayToSecondAllNegVal",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: negativeInt32Val, Months: negativeInt32Val},
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Days: negativeInt32Val, Seconds: negativeInt32Val,
PrecisionMode: nanoSecPrecision, Subseconds: negativeInt64Val},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Years: negativeInt32Val, Months: negativeInt32Val, Days: negativeInt32Val, Seconds: negativeInt32Val, SubSeconds: negativeInt64Val, SubSecondPrecision: precisionNanoVal},
},
{"WithDeprecatedMicroSecondPrecision",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalYearToMonth: &proto.Expression_Literal_IntervalYearToMonth{Years: yearVal, Months: monthVal},
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Days: dayVal, Seconds: secondsVal,
PrecisionMode: deprecatedMicroSecPrecision},
}},
Nullable: nullable},
IntervalCompoundLiteral{Nullability: nullability, Years: yearVal, Months: monthVal, Days: dayVal, Seconds: secondsVal, SubSeconds: int64(microSecondVal), SubSecondPrecision: types.PrecisionMicroSeconds},
},
} {
t.Run(tc.name, func(t *testing.T) {
gotLiteral := LiteralFromProto(tc.constructedProto)
assert.NotNil(t, gotLiteral)
assert.Equal(t, tc.expectedLiteral, gotLiteral)
// verify equal method too returns true
assert.True(t, tc.expectedLiteral.Equals(gotLiteral))
assert.True(t, gotLiteral.IsScalar())
// got literal after serialization is different from empty literal
assert.False(t, IntervalCompoundLiteral{}.Equals(gotLiteral))
})

}
}

func TestIntervalCompoundFromProtoError(t *testing.T) {
// valid precision val is [0, 9]
invalidPrecision := &proto.Expression_Literal_IntervalDayToSecond_Precision{Precision: 10}
deprecatedMicroSecPrecision := &proto.Expression_Literal_IntervalDayToSecond_Microseconds{Microseconds: 70}
for _, tc := range []struct {
name string
constructedProto *proto.Expression_Literal
}{
{"NoPrecisionMode",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{},
}}},
},
{"NoPrecisionModeButSubsecondsPresent",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{Subseconds: 123},
}}},
},
{"InvalidPrecisionMode",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{PrecisionMode: invalidPrecision},
}}},
},
{"DeprecatedMicrosecondPrecisionWithSubsecondsSet",
&proto.Expression_Literal{
LiteralType: &proto.Expression_Literal_IntervalCompound_{IntervalCompound: &proto.Expression_Literal_IntervalCompound{
IntervalDayToSecond: &proto.Expression_Literal_IntervalDayToSecond{PrecisionMode: deprecatedMicroSecPrecision, Subseconds: 70},
}}},
},
} {
t.Run(tc.name, func(t *testing.T) {
gotLiteral := LiteralFromProto(tc.constructedProto)
assert.Nil(t, gotLiteral)
})

}
}
Loading

0 comments on commit 58e4ba0

Please sign in to comment.