From 6c4bb5c35a2a661fc7eb8128086943c208d15acd Mon Sep 17 00:00:00 2001 From: jennifersp Date: Wed, 7 Aug 2024 14:50:25 -0700 Subject: [PATCH 01/10] support INTERVAL type --- postgres/messages/row_description.go | 2 + server/ast/expr.go | 10 +- server/ast/resolvable_type_reference.go | 2 + server/expression/literal.go | 9 + server/functions/extract.go | 80 +++++-- server/types/globals.go | 1 + server/types/interface.go | 2 + server/types/interval.go | 264 ++++++++++++++++++++++++ server/types/interval_array.go | 20 ++ testing/go/functions_test.go | 104 +++++++--- testing/go/types_test.go | 10 +- 11 files changed, 452 insertions(+), 52 deletions(-) create mode 100644 server/types/interval.go create mode 100644 server/types/interval_array.go diff --git a/postgres/messages/row_description.go b/postgres/messages/row_description.go index 036b655101..0dd4c18a50 100644 --- a/postgres/messages/row_description.go +++ b/postgres/messages/row_description.go @@ -88,6 +88,8 @@ const ( OidTimestampArray = 1115 OidDateArray = 1182 OidTimeArray = 1183 + OidInterval = 1186 + OidIntervalArray = 1187 OidNumeric = 1700 OidRefcursor = 1790 OidRegprocedure = 2202 diff --git a/server/ast/expr.go b/server/ast/expr.go index 403f38da4b..376fedd6c3 100644 --- a/server/ast/expr.go +++ b/server/ast/expr.go @@ -442,7 +442,15 @@ func nodeExpr(node tree.Expr) (vitess.Expr, error) { case *tree.DInt: return nil, fmt.Errorf("the statement is not yet supported") case *tree.DInterval: - return nil, fmt.Errorf("the statement is not yet supported") + cast, err := pgexprs.NewExplicitCastInjectable(pgtypes.Interval) + if err != nil { + return nil, err + } + expr := pgexprs.NewIntervalLiteral(node.Duration) + return vitess.InjectedExpr{ + Expression: cast, + Children: vitess.Exprs{vitess.InjectedExpr{Expression: expr}}, + }, nil case *tree.DJSON: return nil, fmt.Errorf("the statement is not yet supported") case *tree.DOid: diff --git a/server/ast/resolvable_type_reference.go b/server/ast/resolvable_type_reference.go index 242f25b981..ce61ea02b1 100755 --- a/server/ast/resolvable_type_reference.go +++ b/server/ast/resolvable_type_reference.go @@ -93,6 +93,8 @@ func nodeResolvableTypeReference(typ tree.ResolvableTypeReference) (*vitess.Conv resolvedType = pgtypes.Int32 case oid.T_int8: resolvedType = pgtypes.Int64 + case oid.T_interval: + resolvedType = pgtypes.Interval case oid.T_json: resolvedType = pgtypes.Json case oid.T_jsonb: diff --git a/server/expression/literal.go b/server/expression/literal.go index 9647c7183c..6d6d7b9eba 100644 --- a/server/expression/literal.go +++ b/server/expression/literal.go @@ -23,6 +23,7 @@ import ( vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -88,6 +89,14 @@ func NewStringLiteral(stringValue string) *Literal { } } +// NewIntervalLiteral returns a new *Literal containing a INTERVAL value. +func NewIntervalLiteral(duration duration.Duration) *Literal { + return &Literal{ + value: duration, + typ: pgtypes.Interval, + } +} + // NewJSONLiteral returns a new *Literal containing a JSON value. This is different from JSONB. func NewJSONLiteral(jsonValue string) *Literal { return &Literal{ diff --git a/server/functions/extract.go b/server/functions/extract.go index a9cd26e4ca..d4a115c65a 100644 --- a/server/functions/extract.go +++ b/server/functions/extract.go @@ -24,6 +24,7 @@ import ( "github.com/shopspring/decimal" "gopkg.in/src-d/go-errors.v1" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -35,7 +36,7 @@ func initExtract() { framework.RegisterFunction(extract_text_timetz) framework.RegisterFunction(extract_text_timestamp) framework.RegisterFunction(extract_text_timestamptz) - //framework.RegisterFunction(extract_text_interval) + framework.RegisterFunction(extract_text_interval) } var ErrUnitNotSupported = errors.NewKind("unit \"%s\" not supported for type %s") @@ -119,22 +120,67 @@ var extract_text_timestamptz = framework.Function2{ }, } -//// extract_text_interval represents the PostgreSQL date/time function, taking {text, interval} -//var extract_text_interval = framework.Function2{ -// Name: "extract", -// Return: pgtypes.Numeric, -// Parameters: [2]pgtypes.DoltgresType{pgtypes.Text, pgtypes.Interval}, -// IsNonDeterministic: true, -// Strict: true, -// Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1, val2 any) (any, error) { -// field := val1.(string) -// if strings.HasPrefix(strings.ToLower(field), "timezone") { -// return nil, ErrUnitNotSupported.New(field, "time without time zone") -// } -// //intervalVal := -// return nil, nil -// }, -//} +const ( + NanosPerMicro = 1000 + NanosPerMilli = NanosPerMicro * duration.MicrosPerMilli + NanosPerSec = NanosPerMicro * duration.MicrosPerMilli * duration.MillisPerSec +) + +// extract_text_interval represents the PostgreSQL date/time function, taking {text, interval} +var extract_text_interval = framework.Function2{ + Name: "extract", + Return: pgtypes.Numeric, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Text, pgtypes.Interval}, + IsNonDeterministic: true, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1, val2 any) (any, error) { + field := val1.(string) + dur := val2.(duration.Duration) + switch strings.ToLower(field) { + case "century", "centuries": + return decimal.NewFromFloat(math.Floor(float64(dur.Months) / 12 / 100)), nil + case "day", "days": + return decimal.NewFromInt(dur.Days), nil + case "decade", "decades": + return decimal.NewFromFloat(math.Floor(float64(dur.Months) / 12 / 10)), nil + case "epoch": + epoch := float64(duration.SecsPerDay*duration.DaysPerMonth*dur.Months) + float64(duration.SecsPerDay*dur.Days) + + (float64(dur.Nanos()) / (NanosPerSec)) + return decimal.NewFromFloatWithExponent(epoch, -6), nil + case "hour", "hours": + hours := math.Floor(float64(dur.Nanos()) / (NanosPerSec * duration.SecsPerHour)) + return decimal.NewFromFloat(hours), nil + case "microsecond", "microseconds": + secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute) + microseconds := float64(secondsInNanos) / NanosPerMicro + return decimal.NewFromFloat(microseconds), nil + case "millennium", "millenniums": + return decimal.NewFromFloat(math.Floor(float64(dur.Months) / 12 / 1000)), nil + case "millisecond", "milliseconds": + secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute) + milliseconds := float64(secondsInNanos) / NanosPerMilli + return decimal.NewFromFloatWithExponent(milliseconds, -3), nil + case "minute", "minutes": + minutesInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerHour) + minutes := math.Floor(float64(minutesInNanos) / (NanosPerSec * duration.SecsPerMinute)) + return decimal.NewFromFloat(minutes), nil + case "month", "months": + return decimal.NewFromInt(dur.Months % 12), nil + case "quarter": + return decimal.NewFromInt((dur.Months%12-1)/3 + 1), nil + case "second", "seconds": + secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute) + seconds := float64(secondsInNanos) / NanosPerSec + return decimal.NewFromFloatWithExponent(seconds, -6), nil + case "year", "years": + return decimal.NewFromFloat(math.Floor(float64(dur.Months) / 12)), nil + case "dow", "doy", "isodow", "isoyear", "julian", "timezone", "timezone_hour", "timezone_minute", "week": + return nil, ErrUnitNotSupported.New(field, "interval") + default: + return nil, fmt.Errorf("unknown field given: %s", field) + } + }, +} // getFieldFromTimeVal returns the value for given field extracted from non-interval values. func getFieldFromTimeVal(field string, tVal time.Time) (any, error) { diff --git a/server/types/globals.go b/server/types/globals.go index 471ec88912..00d6dcc154 100644 --- a/server/types/globals.go +++ b/server/types/globals.go @@ -76,6 +76,7 @@ const ( DoltgresTypeBaseID_Int32 = DoltgresTypeBaseID(SerializationID_Int32) DoltgresTypeBaseID_Int64 = DoltgresTypeBaseID(SerializationID_Int64) DoltgresTypeBaseID_InternalChar = DoltgresTypeBaseID(SerializationID_InternalChar) + DoltgresTypeBaseID_Interval = DoltgresTypeBaseID(SerializationID_Interval) DoltgresTypeBaseID_Json = DoltgresTypeBaseID(SerializationID_Json) DoltgresTypeBaseID_JsonB = DoltgresTypeBaseID(SerializationID_JsonB) DoltgresTypeBaseID_Name = DoltgresTypeBaseID(SerializationID_Name) diff --git a/server/types/interface.go b/server/types/interface.go index 9864152673..696605b124 100644 --- a/server/types/interface.go +++ b/server/types/interface.go @@ -112,6 +112,8 @@ var typesFromBaseID = map[DoltgresTypeBaseID]DoltgresType{ Int64Serial.BaseID(): Int64Serial, InternalChar.BaseID(): InternalChar, InternalCharArray.BaseID(): InternalCharArray, + Interval.BaseID(): Interval, + IntervalArray.BaseID(): IntervalArray, Json.BaseID(): Json, JsonArray.BaseID(): JsonArray, JsonB.BaseID(): JsonB, diff --git a/server/types/interval.go b/server/types/interval.go new file mode 100644 index 0000000000..92a3bfa1c9 --- /dev/null +++ b/server/types/interval.go @@ -0,0 +1,264 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "bytes" + "fmt" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + "github.com/dolthub/doltgresql/utils" + "reflect" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + "github.com/dolthub/vitess/go/sqltypes" + "github.com/dolthub/vitess/go/vt/proto/query" + "github.com/lib/pq/oid" +) + +// Interval is the interval type. +var Interval = IntervalType{} + +// IntervalType is the extended type implementation of the PostgreSQL interval. +type IntervalType struct{} + +var _ DoltgresType = IntervalType{} + +// Alignment implements the DoltgresType interface. +func (b IntervalType) Alignment() TypeAlignment { + return TypeAlignment_Double +} + +// BaseID implements the DoltgresType interface. +func (b IntervalType) BaseID() DoltgresTypeBaseID { + return DoltgresTypeBaseID_Interval +} + +// BaseName implements the DoltgresType interface. +func (b IntervalType) BaseName() string { + return "interval" +} + +// Category implements the DoltgresType interface. +func (b IntervalType) Category() TypeCategory { + return TypeCategory_TimespanTypes +} + +// CollationCoercibility implements the DoltgresType interface. +func (b IntervalType) CollationCoercibility(ctx *sql.Context) (collation sql.CollationID, coercibility byte) { + return sql.Collation_binary, 5 +} + +// Compare implements the DoltgresType interface. +func (b IntervalType) Compare(v1 any, v2 any) (int, error) { + if v1 == nil && v2 == nil { + return 0, nil + } else if v1 != nil && v2 == nil { + return 1, nil + } else if v1 == nil && v2 != nil { + return -1, nil + } + + ac, _, err := b.Convert(v1) + if err != nil { + return 0, err + } + bc, _, err := b.Convert(v2) + if err != nil { + return 0, err + } + + ab := ac.(duration.Duration) + bb := bc.(duration.Duration) + return ab.Compare(bb), nil +} + +// Convert implements the DoltgresType interface. +func (b IntervalType) Convert(val any) (any, sql.ConvertInRange, error) { + switch val := val.(type) { + case duration.Duration: + return val, sql.InRange, nil + case nil: + return nil, sql.InRange, nil + default: + return nil, sql.OutOfRange, fmt.Errorf("%s: unhandled type: %T", b.String(), val) + } +} + +// Equals implements the DoltgresType interface. +func (b IntervalType) Equals(otherType sql.Type) bool { + if otherExtendedType, ok := otherType.(types.ExtendedType); ok { + return bytes.Equal(MustSerializeType(b), MustSerializeType(otherExtendedType)) + } + return false +} + +// FormatValue implements the DoltgresType interface. +func (b IntervalType) FormatValue(val any) (string, error) { + if val == nil { + return "", nil + } + return b.IoOutput(sql.NewEmptyContext(), val) +} + +// GetSerializationID implements the DoltgresType interface. +func (b IntervalType) GetSerializationID() SerializationID { + return SerializationID_Interval +} + +// IoInput implements the DoltgresType interface. +func (b IntervalType) IoInput(ctx *sql.Context, input string) (any, error) { + dInterval, err := tree.ParseDInterval(input) + if err != nil { + return nil, err + } + return dInterval.Duration, nil +} + +// IoOutput implements the DoltgresType interface. +func (b IntervalType) IoOutput(ctx *sql.Context, output any) (string, error) { + converted, _, err := b.Convert(output) + if err != nil { + return "", err + } + // TODO: depends on `intervalStyle` configuration variable. Defaults to `postgres`. + d := converted.(duration.Duration) + return d.String(), nil +} + +// IsPreferredType implements the DoltgresType interface. +func (b IntervalType) IsPreferredType() bool { + return true +} + +// IsUnbounded implements the DoltgresType interface. +func (b IntervalType) IsUnbounded() bool { + return false +} + +// MaxSerializedWidth implements the DoltgresType interface. +func (b IntervalType) MaxSerializedWidth() types.ExtendedTypeSerializedWidth { + return types.ExtendedTypeSerializedWidth_64K +} + +// MaxTextResponseByteLength implements the DoltgresType interface. +func (b IntervalType) MaxTextResponseByteLength(ctx *sql.Context) uint32 { + return 64 +} + +// OID implements the DoltgresType interface. +func (b IntervalType) OID() uint32 { + return uint32(oid.T_interval) +} + +// Promote implements the DoltgresType interface. +func (b IntervalType) Promote() sql.Type { + return Interval +} + +// SerializedCompare implements the DoltgresType interface. +func (b IntervalType) SerializedCompare(v1 []byte, v2 []byte) (int, error) { + if len(v1) == 0 && len(v2) == 0 { + return 0, nil + } else if len(v1) > 0 && len(v2) == 0 { + return 1, nil + } else if len(v1) == 0 && len(v2) > 0 { + return -1, nil + } + + return bytes.Compare(v1, v2), nil +} + +// SQL implements the DoltgresType interface. +func (b IntervalType) SQL(ctx *sql.Context, dest []byte, v any) (sqltypes.Value, error) { + if v == nil { + return sqltypes.NULL, nil + } + value, err := b.IoOutput(ctx, v) + if err != nil { + return sqltypes.Value{}, err + } + return sqltypes.MakeTrusted(sqltypes.Text, types.AppendAndSliceBytes(dest, []byte(value))), nil +} + +// String implements the DoltgresType interface. +func (b IntervalType) String() string { + return "interval" +} + +// ToArrayType implements the DoltgresType interface. +func (b IntervalType) ToArrayType() DoltgresArrayType { + return IntervalArray +} + +// Type implements the DoltgresType interface. +func (b IntervalType) Type() query.Type { + return sqltypes.Text +} + +// ValueType implements the DoltgresType interface. +func (b IntervalType) ValueType() reflect.Type { + return reflect.TypeOf(duration.MakeDuration(0, 0, 0)) +} + +// Zero implements the DoltgresType interface. +func (b IntervalType) Zero() any { + return duration.MakeDuration(0, 0, 0) +} + +// SerializeType implements the DoltgresType interface. +func (b IntervalType) SerializeType() ([]byte, error) { + return SerializationID_Interval.ToByteSlice(0), nil +} + +// deserializeType implements the DoltgresType interface. +func (b IntervalType) deserializeType(version uint16, metadata []byte) (DoltgresType, error) { + switch version { + case 0: + return Interval, nil + default: + return nil, fmt.Errorf("version %d is not yet supported for %s", version, b.String()) + } +} + +// SerializeValue implements the DoltgresType interface. +func (b IntervalType) SerializeValue(val any) ([]byte, error) { + if val == nil { + return nil, nil + } + converted, _, err := b.Convert(val) + if err != nil { + return nil, err + } + str := converted.(duration.Duration).String() + writer := utils.NewWriter(uint64(len(str) + 1)) + writer.String(str) + return writer.Data(), nil +} + +// DeserializeValue implements the DoltgresType interface. +func (b IntervalType) DeserializeValue(val []byte) (any, error) { + if len(val) == 0 { + return nil, nil + } + reader := utils.NewReader(val) + str := reader.String() + dInterval, err := tree.ParseDInterval(str) + if err != nil { + return nil, err + } + return dInterval.Duration, nil +} diff --git a/server/types/interval_array.go b/server/types/interval_array.go new file mode 100644 index 0000000000..f3ab21f32f --- /dev/null +++ b/server/types/interval_array.go @@ -0,0 +1,20 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "github.com/lib/pq/oid" + +// IntervalArray is the array variant of Interval. +var IntervalArray = createArrayType(Text, SerializationID_IntervalArray, oid.T__interval) diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index 626f099eb0..009291b32c 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -1143,20 +1143,10 @@ func TestDateAndTimeFunction(t *testing.T) { Query: `SELECT EXTRACT(CENTURY FROM DATE '0001-12-31 BC');`, Expected: []sql.Row{{float64(-1)}}, }, - { - Skip: true, // TODO: support INTERVAL type - Query: `SELECT EXTRACT(CENTURY FROM INTERVAL '2001 years');`, - Expected: []sql.Row{{float64(20)}}, - }, { Query: `SELECT EXTRACT(DAY FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(16)}}, }, - { - Skip: true, // TODO: support INTERVAL type - Query: `SELECT EXTRACT(DAY FROM INTERVAL '40 days 1 minute');`, - Expected: []sql.Row{{float64(40)}}, - }, { Query: `SELECT EXTRACT(DECADE FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(200)}}, @@ -1177,11 +1167,6 @@ func TestDateAndTimeFunction(t *testing.T) { Query: `SELECT EXTRACT(EPOCH FROM TIMESTAMP '2001-02-16 20:38:40.12');`, Expected: []sql.Row{{float64(982355920.120000)}}, }, - { - Skip: true, // TODO: support INTERVAL type - Query: `SELECT EXTRACT(EPOCH FROM INTERVAL '5 days 3 hours');`, - Expected: []sql.Row{{float64(442800.000000)}}, - }, { Query: `SELECT EXTRACT(HOUR FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(20)}}, @@ -1217,11 +1202,6 @@ func TestDateAndTimeFunction(t *testing.T) { Query: `SELECT EXTRACT(MILLENNIUM FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(3)}}, }, - { - Skip: true, // TODO: not supported yet - Query: `SELECT EXTRACT(MILLENNIUM FROM INTERVAL '2001 years');`, - Expected: []sql.Row{{float64(2)}}, - }, { Query: `SELECT EXTRACT(MILLISECONDS FROM TIME '17:12:28.5');`, Expected: []sql.Row{{float64(28500.000)}}, @@ -1234,16 +1214,6 @@ func TestDateAndTimeFunction(t *testing.T) { Query: `SELECT EXTRACT(MONTH FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(2)}}, }, - { - Skip: true, // TODO: not supported yet - Query: `SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months');`, - Expected: []sql.Row{{float64(3)}}, - }, - { - Skip: true, // TODO: not supported yet - Query: `SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months');`, - Expected: []sql.Row{{float64(1)}}, - }, { Query: `SELECT EXTRACT(QUARTER FROM TIMESTAMP '2001-02-16 20:38:40');`, Expected: []sql.Row{{float64(1)}}, @@ -1266,5 +1236,79 @@ func TestDateAndTimeFunction(t *testing.T) { }, }, }, + { + Name: "extract interval", + SetUpScript: []string{}, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT EXTRACT(CENTURY FROM INTERVAL '2001 years');`, + Expected: []sql.Row{{float64(20)}}, + }, + { + Query: `SELECT EXTRACT(DAY FROM INTERVAL '40 days 1 minute');`, + Expected: []sql.Row{{float64(40)}}, + }, + { + Query: `select extract(decades from interval '1000 months');`, + Expected: []sql.Row{{float64(8)}}, + }, + { + Query: `SELECT EXTRACT(EPOCH FROM INTERVAL '5 days 3 hours');`, + Expected: []sql.Row{{float64(442800.000000)}}, + }, + { + Query: `select extract(epoch from interval '10 months 10 seconds');`, + Expected: []sql.Row{{float64(25920010.000000)}}, + }, + { + Query: `select extract(hours from interval '10 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(1)}}, + }, + { + Query: `select extract(microsecond from interval '10 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(10000000)}}, + }, + { + Query: `SELECT EXTRACT(MILLENNIUM FROM INTERVAL '2001 years');`, + Expected: []sql.Row{{float64(2)}}, + }, + { + Query: `select extract(millenniums from interval '3000 years 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(3)}}, + }, + { + Query: `select extract(millisecond from interval '10 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(10000.000)}}, + }, + { + Query: `select extract(minutes from interval '10 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(5)}}, + }, + { + Query: `SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months');`, + Expected: []sql.Row{{float64(3)}}, + }, + { + Query: `SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months');`, + Expected: []sql.Row{{float64(1)}}, + }, + { + Query: `select extract(months from interval '20 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(8)}}, + }, + { + Query: `select extract(quarter from interval '20 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(3)}}, + }, + { + Query: `select extract(seconds from interval '65 minutes 10 seconds 5 millisecond');`, + Expected: []sql.Row{{float64(10.005000)}}, + }, + { + Query: `select extract(years from interval '20 months 65 minutes 10 seconds');`, + Expected: []sql.Row{{float64(1)}}, + }, + }, + }, }) } diff --git a/testing/go/types_test.go b/testing/go/types_test.go index 5d3350241c..0cc4260c82 100644 --- a/testing/go/types_test.go +++ b/testing/go/types_test.go @@ -579,20 +579,22 @@ var typesTests = []ScriptTest{ }, { Name: "Interval type", - Skip: true, SetUpScript: []string{ "CREATE TABLE t_interval (id INTEGER primary key, v1 INTERVAL);", "INSERT INTO t_interval VALUES (1, '1 day 3 hours'), (2, '2 hours 30 minutes');", }, Assertions: []ScriptTestAssertion{ { - // TODO: might need a GMS type here, not a string (psql output is different than below) Query: "SELECT * FROM t_interval ORDER BY id;", Expected: []sql.Row{ - {1, "1 day 3 hours"}, - {2, "2 hours 30 minutes"}, + {1, "1 day 03:00:00"}, + {2, "02:30:00"}, }, }, + { + Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval;`, + Expected: []sql.Row{{"3 years 3 mons 700 days 133:17:36.789"}}, + }, }, }, { From 6a2d58d1b479fdf63bf2ea342d75ec51fc70fe9d Mon Sep 17 00:00:00 2001 From: jennifersp Date: Wed, 7 Aug 2024 16:19:27 -0700 Subject: [PATCH 02/10] add AGE function --- server/functions/age.go | 118 +++++++++++++++++++++++++++++++++++ server/functions/init.go | 1 + server/types/interval.go | 3 +- testing/go/functions_test.go | 39 ++++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 server/functions/age.go diff --git a/server/functions/age.go b/server/functions/age.go new file mode 100644 index 0000000000..309bb047b6 --- /dev/null +++ b/server/functions/age.go @@ -0,0 +1,118 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package functions + +import ( + "time" + + "github.com/dolthub/go-mysql-server/sql" + + duration "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initAge registers the functions to the catalog. +func initAge() { + framework.RegisterFunction(age_timestamp_timestamp) + framework.RegisterFunction(age_timestamp) +} + +// age_timestamp_timestamp represents the PostgreSQL date/time function. +var age_timestamp_timestamp = framework.Function2{ + Name: "age", + Return: pgtypes.Interval, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Timestamp, pgtypes.Timestamp}, + IsNonDeterministic: true, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1, val2 any) (any, error) { + t1 := val1.(time.Time) + t2 := val2.(time.Time) + return diffTimes(t1, t2), nil + }, +} + +// age_timestamp_timestamp represents the PostgreSQL date/time function. +var age_timestamp = framework.Function1{ + Name: "age", + Return: pgtypes.Interval, + Parameters: [1]pgtypes.DoltgresType{pgtypes.Timestamp}, + IsNonDeterministic: true, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]pgtypes.DoltgresType, val any) (any, error) { + t := val.(time.Time) + // current_date (at midnight) + cur, err := time.Parse("2006-01-02", time.Now().Format("2006-01-02")) + if err != nil { + return nil, err + } + return diffTimes(cur, t), nil + }, +} + +// diffTimes returns the duration t1-t2. It subtracts each time component separately, +// unlike time.Sub() function. +func diffTimes(t1, t2 time.Time) duration.Duration { + // if t1 is before t2, then negate the result. + negate := t1.Before(t2) + if negate { + t1, t2 = t2, t1 + } + + // Calculate difference in each unit + years := int64(t1.Year() - t2.Year()) + months := int64(t1.Month() - t2.Month()) + days := int64(t1.Day() - t2.Day()) + hours := int64(t1.Hour() - t2.Hour()) + minutes := int64(t1.Minute() - t2.Minute()) + seconds := int64(t1.Second() - t2.Second()) + nanoseconds := int64(t1.Nanosecond() - t2.Nanosecond()) + + // Adjust for any negative values + if nanoseconds < 0 { + nanoseconds += 1e9 + seconds-- + } + if seconds < 0 { + seconds += 60 + minutes-- + } + if minutes < 0 { + minutes += 60 + hours-- + } + if hours < 0 { + hours += 24 + days-- + } + if days < 0 { + days += 30 + months-- + } + if months < 0 { + months += 12 + years-- + } + + durNanos := nanoseconds + seconds*NanosPerSec + minutes*NanosPerSec*duration.SecsPerMinute + hours*NanosPerSec*duration.SecsPerHour + durDays := days + durMonths := months + years*duration.MonthsPerYear + + if negate { + return duration.MakeDuration(-durNanos, -durDays, -durMonths) + } else { + return duration.MakeDuration(durNanos, durDays, durMonths) + } +} diff --git a/server/functions/init.go b/server/functions/init.go index 99088084f8..d03d4affb0 100644 --- a/server/functions/init.go +++ b/server/functions/init.go @@ -20,6 +20,7 @@ func Init() { initAcos() initAcosd() initAcosh() + initAge() initArrayAppend() initArrayToString() initAscii() diff --git a/server/types/interval.go b/server/types/interval.go index 92a3bfa1c9..aa6eb00724 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -17,10 +17,11 @@ package types import ( "bytes" "fmt" + "reflect" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/sem/tree" "github.com/dolthub/doltgresql/utils" - "reflect" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index 009291b32c..23ba78fc6c 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -1310,5 +1310,44 @@ func TestDateAndTimeFunction(t *testing.T) { }, }, }, + { + Name: "age", + SetUpScript: []string{}, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT age(timestamp '2001-04-10', timestamp '1957-06-13');`, + Expected: []sql.Row{{"43 years 9 mons 27 days"}}, + }, + { + Query: `SELECT age(timestamp '1957-06-13', timestamp '2001-04-10');`, + Expected: []sql.Row{{"-43 years -9 mons -27 days"}}, + }, + { + Query: `SELECT age(timestamp '2001-06-13', timestamp '2001-04-10');`, + Expected: []sql.Row{{"2 mons 3 days"}}, + }, + { + Query: `SELECT age(timestamp '2001-04-10', timestamp '2001-06-13');`, + Expected: []sql.Row{{"-2 mons -3 days"}}, + }, + { + Query: `SELECT age(timestamp '2001-04-10 12:23:33', timestamp '1957-06-13 13:23:34.4');`, + Expected: []sql.Row{{"43 years 9 mons 26 days 22:59:58.6"}}, + }, + { + Query: `SELECT age(timestamp '1957-06-13 13:23:34.4', timestamp '2001-04-10 12:23:33');`, + Expected: []sql.Row{{"-43 years -9 mons -26 days -22:59:58.6"}}, + }, + { + Skip: true, // TODO: current_date should return timestamp, not text + Query: `SELECT age(current_date);`, + Expected: []sql.Row{{"00:00:00"}}, + }, + { + Query: `SELECT age(current_date::timestamp);`, + Expected: []sql.Row{{"00:00:00"}}, + }, + }, + }, }) } From 2064b839f135e94910bd8a1fadacb84a22327912 Mon Sep 17 00:00:00 2001 From: jennifersp Date: Wed, 7 Aug 2024 16:24:21 -0700 Subject: [PATCH 03/10] rm --- server/functions/age.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/functions/age.go b/server/functions/age.go index 309bb047b6..236ca2a4d7 100644 --- a/server/functions/age.go +++ b/server/functions/age.go @@ -19,7 +19,7 @@ import ( "github.com/dolthub/go-mysql-server/sql" - duration "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) From 6e9dd0af253c759949a7590d3095eabff997c66f Mon Sep 17 00:00:00 2001 From: jennifersp Date: Thu, 8 Aug 2024 16:29:47 -0700 Subject: [PATCH 04/10] add cast and convert for interval --- server/cast/init.go | 2 + server/cast/internal_char.go | 1 - server/cast/interval.go | 77 +++++++++++++++++++++ server/cast/time.go | 42 ++++++++++++ server/functions/age.go | 16 +++-- server/functions/binary/divide.go | 16 +++++ server/functions/binary/equal.go | 14 ++++ server/functions/binary/greater.go | 14 ++++ server/functions/binary/greater_equal.go | 14 ++++ server/functions/binary/less.go | 14 ++++ server/functions/binary/less_equal.go | 14 ++++ server/functions/binary/minus.go | 15 +++++ server/functions/binary/multiply.go | 13 ++++ server/functions/binary/not_equal.go | 14 ++++ server/functions/binary/plus.go | 86 ++++++++++++++++++++++++ server/functions/unary/minus.go | 14 ++++ server/types/date.go | 2 +- server/types/interval.go | 16 ++++- server/types/interval_array.go | 2 +- testing/go/functions_test.go | 73 ++++++++++++++++++++ testing/go/types_test.go | 83 +++++++++++++++++++++-- 21 files changed, 526 insertions(+), 16 deletions(-) create mode 100644 server/cast/interval.go create mode 100644 server/cast/time.go diff --git a/server/cast/init.go b/server/cast/init.go index ce5d42ba42..0f1277be9f 100644 --- a/server/cast/init.go +++ b/server/cast/init.go @@ -24,6 +24,7 @@ func Init() { initInt32() initInt64() initInternalChar() + initInterval() initJson() initJsonB() initName() @@ -33,5 +34,6 @@ func Init() { initRegproc() initRegtype() initText() + initTime() initVarChar() } diff --git a/server/cast/internal_char.go b/server/cast/internal_char.go index 397bd739ea..b1d598808a 100644 --- a/server/cast/internal_char.go +++ b/server/cast/internal_char.go @@ -80,5 +80,4 @@ func internalCharImplicit() { return val, nil }, }) - } diff --git a/server/cast/interval.go b/server/cast/interval.go new file mode 100644 index 0000000000..582cb6cb72 --- /dev/null +++ b/server/cast/interval.go @@ -0,0 +1,77 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initInterval handles all casts that are built-in. This comprises only the "From" types. +func initInterval() { + intervalAssignment() + intervalExplicit() + intervalImplicit() +} + +// intervalAssignment registers all assignment casts. This comprises only the "From" types. +func intervalAssignment() { + framework.MustAddAssignmentTypeCast(framework.TypeCast{ + FromType: pgtypes.Interval, + ToType: pgtypes.BpChar, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + return handleStringCast(val.(duration.Duration).String(), targetType) + }, + }) + framework.MustAddAssignmentTypeCast(framework.TypeCast{ + FromType: pgtypes.Interval, + ToType: pgtypes.VarChar, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + return handleStringCast(val.(duration.Duration).String(), targetType) + }, + }) +} + +// intervalExplicit registers all implicit casts. This comprises only the "From" types. +func intervalExplicit() { + framework.MustAddExplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Interval, + ToType: pgtypes.InternalChar, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + return handleStringCast(val.(duration.Duration).String(), targetType) + }, + }) + framework.MustAddExplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Interval, + ToType: pgtypes.Name, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + return handleStringCast(val.(duration.Duration).String(), targetType) + }, + }) +} + +// intervalImplicit registers all implicit casts. This comprises only the "From" types. +func intervalImplicit() { + framework.MustAddImplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Interval, + ToType: pgtypes.Text, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + return val.(duration.Duration).String(), nil + }, + }) +} diff --git a/server/cast/time.go b/server/cast/time.go new file mode 100644 index 0000000000..e590ec10b1 --- /dev/null +++ b/server/cast/time.go @@ -0,0 +1,42 @@ +// Copyright 2024 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cast + +import ( + "github.com/dolthub/doltgresql/server/functions" + "github.com/dolthub/go-mysql-server/sql" + "time" + + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initTime handles all casts that are built-in. This comprises only the "From" types. +func initTime() { + timeExplicit() +} + +// timeExplicit registers all implicit casts. This comprises only the "From" types. +func timeExplicit() { + framework.MustAddExplicitTypeCast(framework.TypeCast{ + FromType: pgtypes.Time, + ToType: pgtypes.Interval, + Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { + t := val.(time.Time) + dur := functions.GetIntervalDurationFromTimeComponents(0, 0, 0, int64(t.Hour()), int64(t.Minute()), int64(t.Second()), int64(t.Nanosecond())) + return dur, nil + }, + }) +} diff --git a/server/functions/age.go b/server/functions/age.go index 236ca2a4d7..21facec090 100644 --- a/server/functions/age.go +++ b/server/functions/age.go @@ -106,13 +106,17 @@ func diffTimes(t1, t2 time.Time) duration.Duration { years-- } - durNanos := nanoseconds + seconds*NanosPerSec + minutes*NanosPerSec*duration.SecsPerMinute + hours*NanosPerSec*duration.SecsPerHour + dur := GetIntervalDurationFromTimeComponents(years, months, days, hours, minutes, seconds, nanoseconds) + if negate { + return dur.Mul(-1) + } + return dur +} + +func GetIntervalDurationFromTimeComponents(years, months, days, hours, minutes, seconds, nanos int64) duration.Duration { + durNanos := nanos + seconds*NanosPerSec + minutes*NanosPerSec*duration.SecsPerMinute + hours*NanosPerSec*duration.SecsPerHour durDays := days durMonths := months + years*duration.MonthsPerYear - if negate { - return duration.MakeDuration(-durNanos, -durDays, -durMonths) - } else { - return duration.MakeDuration(durNanos, durDays, durMonths) - } + return duration.MakeDuration(durNanos, durDays, durMonths) } diff --git a/server/functions/binary/divide.go b/server/functions/binary/divide.go index ecd3b05ca5..b1991e42c0 100644 --- a/server/functions/binary/divide.go +++ b/server/functions/binary/divide.go @@ -16,6 +16,7 @@ package binary import ( "fmt" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" @@ -42,6 +43,7 @@ func initBinaryDivide() { framework.RegisterBinaryFunction(framework.Operator_BinaryDivide, int8div) framework.RegisterBinaryFunction(framework.Operator_BinaryDivide, int82div) framework.RegisterBinaryFunction(framework.Operator_BinaryDivide, int84div) + framework.RegisterBinaryFunction(framework.Operator_BinaryDivide, interval_div) framework.RegisterBinaryFunction(framework.Operator_BinaryDivide, numeric_div) } @@ -227,6 +229,20 @@ var int84div = framework.Function2{ }, } +// interval_div represents the PostgreSQL function of the same name, taking the same parameters. +var interval_div = framework.Function2{ + Name: "interval_div", + Return: pgtypes.Interval, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Float64}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + if val2.(float64) == 0 { + return nil, fmt.Errorf("division by zero") + } + return val1.(duration.Duration).DivFloat(val2.(float64)), nil + }, +} + // numeric_div represents the PostgreSQL function of the same name, taking the same parameters. var numeric_div = framework.Function2{ Name: "numeric_div", diff --git a/server/functions/binary/equal.go b/server/functions/binary/equal.go index 80998665e9..0b2218d9ef 100644 --- a/server/functions/binary/equal.go +++ b/server/functions/binary/equal.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryEqual() { framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, int82eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, int84eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, int8eq) + framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, interval_eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, jsonb_eq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, nameeq) framework.RegisterBinaryFunction(framework.Operator_BinaryEqual, nameeqtext) @@ -310,6 +312,18 @@ var int8eq = framework.Function2{ }, } +// interval_eq represents the PostgreSQL function of the same name, taking the same parameters. +var interval_eq = framework.Function2{ + Name: "interval_eq", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res == 0, err + }, +} + // jsonb_eq represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_eq = framework.Function2{ Name: "jsonb_eq", diff --git a/server/functions/binary/greater.go b/server/functions/binary/greater.go index eeee4a471f..2135a5b85f 100644 --- a/server/functions/binary/greater.go +++ b/server/functions/binary/greater.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryGreaterThan() { framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, int82gt) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, int84gt) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, int8gt) + framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, interval_gt) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, jsonb_gt) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, namegt) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterThan, namegttext) @@ -308,6 +310,18 @@ var int8gt = framework.Function2{ }, } +// interval_gt represents the PostgreSQL function of the same name, taking the same parameters. +var interval_gt = framework.Function2{ + Name: "interval_gt", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res == 1, err + }, +} + // jsonb_gt represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_gt = framework.Function2{ Name: "jsonb_gt", diff --git a/server/functions/binary/greater_equal.go b/server/functions/binary/greater_equal.go index 3fee353a2f..2802b6b285 100644 --- a/server/functions/binary/greater_equal.go +++ b/server/functions/binary/greater_equal.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryGreaterOrEqual() { framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, int82ge) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, int84ge) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, int8ge) + framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, interval_ge) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, jsonb_ge) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, namege) framework.RegisterBinaryFunction(framework.Operator_BinaryGreaterOrEqual, namegetext) @@ -308,6 +310,18 @@ var int8ge = framework.Function2{ }, } +// interval_ge represents the PostgreSQL function of the same name, taking the same parameters. +var interval_ge = framework.Function2{ + Name: "interval_ge", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res >= 0, err + }, +} + // jsonb_ge represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_ge = framework.Function2{ Name: "jsonb_ge", diff --git a/server/functions/binary/less.go b/server/functions/binary/less.go index 17c81fae67..419e4abacd 100644 --- a/server/functions/binary/less.go +++ b/server/functions/binary/less.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryLessThan() { framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, int82lt) framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, int84lt) framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, int8lt) + framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, interval_lt) framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, jsonb_lt) framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, namelt) framework.RegisterBinaryFunction(framework.Operator_BinaryLessThan, namelttext) @@ -308,6 +310,18 @@ var int8lt = framework.Function2{ }, } +// interval_lt represents the PostgreSQL function of the same name, taking the same parameters. +var interval_lt = framework.Function2{ + Name: "interval_lt", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res == -1, err + }, +} + // jsonb_lt represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_lt = framework.Function2{ Name: "jsonb_lt", diff --git a/server/functions/binary/less_equal.go b/server/functions/binary/less_equal.go index d381cd4645..b4a1c2b458 100644 --- a/server/functions/binary/less_equal.go +++ b/server/functions/binary/less_equal.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryLessOrEqual() { framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, int82le) framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, int84le) framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, int8le) + framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, interval_le) framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, jsonb_le) framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, namele) framework.RegisterBinaryFunction(framework.Operator_BinaryLessOrEqual, nameletext) @@ -308,6 +310,18 @@ var int8le = framework.Function2{ }, } +// interval_le represents the PostgreSQL function of the same name, taking the same parameters. +var interval_le = framework.Function2{ + Name: "interval_le", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res <= 0, err + }, +} + // jsonb_le represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_le = framework.Function2{ Name: "jsonb_le", diff --git a/server/functions/binary/minus.go b/server/functions/binary/minus.go index d0ab689318..9e16f206c3 100644 --- a/server/functions/binary/minus.go +++ b/server/functions/binary/minus.go @@ -16,6 +16,7 @@ package binary import ( "fmt" + "github.com/dolthub/doltgresql/postgres/parser/duration" "math" "github.com/dolthub/go-mysql-server/sql" @@ -43,6 +44,7 @@ func initBinaryMinus() { framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, int8mi) framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, int82mi) framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, int84mi) + framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, interval_mi) framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, numeric_sub) } @@ -205,6 +207,19 @@ var int84mi = framework.Function2{ }, } +// interval_mi represents the PostgreSQL function of the same name, taking the same parameters. +var interval_mi = framework.Function2{ + Name: "interval_mi", + Return: pgtypes.Interval, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + dur1 := val1.(duration.Duration) + dur2 := val2.(duration.Duration) + return dur1.Sub(dur2), nil + }, +} + // numeric_sub represents the PostgreSQL function of the same name, taking the same parameters. var numeric_sub = framework.Function2{ Name: "numeric_sub", diff --git a/server/functions/binary/multiply.go b/server/functions/binary/multiply.go index 66360725c0..57d0dbfdb2 100644 --- a/server/functions/binary/multiply.go +++ b/server/functions/binary/multiply.go @@ -16,6 +16,7 @@ package binary import ( "fmt" + "github.com/dolthub/doltgresql/postgres/parser/duration" "math" "github.com/dolthub/go-mysql-server/sql" @@ -43,6 +44,7 @@ func initBinaryMultiply() { framework.RegisterBinaryFunction(framework.Operator_BinaryMultiply, int8mul) framework.RegisterBinaryFunction(framework.Operator_BinaryMultiply, int82mul) framework.RegisterBinaryFunction(framework.Operator_BinaryMultiply, int84mul) + framework.RegisterBinaryFunction(framework.Operator_BinaryMultiply, interval_mul) framework.RegisterBinaryFunction(framework.Operator_BinaryMultiply, numeric_mul) } @@ -205,6 +207,17 @@ var int84mul = framework.Function2{ }, } +// interval_mul represents the PostgreSQL function of the same name, taking the same parameters. +var interval_mul = framework.Function2{ + Name: "interval_mul", + Return: pgtypes.Interval, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Float64}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return val1.(duration.Duration).MulFloat(val2.(float64)), nil + }, +} + // numeric_mul represents the PostgreSQL function of the same name, taking the same parameters. var numeric_mul = framework.Function2{ Name: "numeric_mul", diff --git a/server/functions/binary/not_equal.go b/server/functions/binary/not_equal.go index 8bb3847542..941991b652 100644 --- a/server/functions/binary/not_equal.go +++ b/server/functions/binary/not_equal.go @@ -15,6 +15,7 @@ package binary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" @@ -50,6 +51,7 @@ func initBinaryNotEqual() { framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, int82ne) framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, int84ne) framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, int8ne) + framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, interval_ne) framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, jsonb_ne) framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, namene) framework.RegisterBinaryFunction(framework.Operator_BinaryNotEqual, namenetext) @@ -310,6 +312,18 @@ var int8ne = framework.Function2{ }, } +// interval_ne represents the PostgreSQL function of the same name, taking the same parameters. +var interval_ne = framework.Function2{ + Name: "interval_ne", + Return: pgtypes.Bool, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + res, err := pgtypes.Interval.Compare(val1.(duration.Duration), val2.(duration.Duration)) + return res != 0, err + }, +} + // jsonb_ne represents the PostgreSQL function of the same name, taking the same parameters. var jsonb_ne = framework.Function2{ Name: "jsonb_ne", diff --git a/server/functions/binary/plus.go b/server/functions/binary/plus.go index bfe497111b..9d6f392836 100644 --- a/server/functions/binary/plus.go +++ b/server/functions/binary/plus.go @@ -16,7 +16,10 @@ package binary import ( "fmt" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/server/functions" "math" + "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" @@ -43,6 +46,12 @@ func initBinaryPlus() { framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, int8pl) framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, int82pl) framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, int84pl) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl_time) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl_date) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl_timetz) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl_timestamp) + framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, interval_pl_timestamptz) framework.RegisterBinaryFunction(framework.Operator_BinaryPlus, numeric_add) } @@ -205,6 +214,83 @@ var int84pl = framework.Function2{ }, } +// interval_pl represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl = framework.Function2{ + Name: "interval_pl", + Return: pgtypes.Interval, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + dur1 := val1.(duration.Duration) + dur2 := val2.(duration.Duration) + return dur1.Add(dur2), nil + }, +} + +// interval_pl_time represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl_time = framework.Function2{ + Name: "interval_pl_time", + Return: pgtypes.Time, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Time}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return intervalPlusNonInterval(val1.(duration.Duration), val2.(time.Time)) + }, +} + +// interval_pl_date represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl_date = framework.Function2{ + Name: "interval_pl_date", + Return: pgtypes.Timestamp, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Date}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return intervalPlusNonInterval(val1.(duration.Duration), val2.(time.Time)) + }, +} + +// interval_pl_timetz represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl_timetz = framework.Function2{ + Name: "interval_pl_timetz", + Return: pgtypes.TimeTZ, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.TimeTZ}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return intervalPlusNonInterval(val1.(duration.Duration), val2.(time.Time)) + }, +} + +// interval_pl_timestamp represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl_timestamp = framework.Function2{ + Name: "interval_pl_timestamp", + Return: pgtypes.Timestamp, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.Timestamp}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return intervalPlusNonInterval(val1.(duration.Duration), val2.(time.Time)) + }, +} + +// interval_pl_timestamptz represents the PostgreSQL function of the same name, taking the same parameters. +var interval_pl_timestamptz = framework.Function2{ + Name: "interval_pl_timestamptz", + Return: pgtypes.TimestampTZ, + Parameters: [2]pgtypes.DoltgresType{pgtypes.Interval, pgtypes.TimestampTZ}, + Strict: true, + Callable: func(ctx *sql.Context, _ [3]pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + return intervalPlusNonInterval(val1.(duration.Duration), val2.(time.Time)) + }, +} + +func intervalPlusNonInterval(d duration.Duration, t time.Time) (time.Time, error) { + seconds, ok := d.AsInt64() + if !ok { + return time.Time{}, fmt.Errorf("interval overflow") + } + nanos := seconds * functions.NanosPerSec // TODO: might overflow + return t.Add(time.Duration(nanos)), nil +} + // numeric_add represents the PostgreSQL function of the same name, taking the same parameters. var numeric_add = framework.Function2{ Name: "numeric_add", diff --git a/server/functions/unary/minus.go b/server/functions/unary/minus.go index 47e84f5b90..acb11f2f0a 100644 --- a/server/functions/unary/minus.go +++ b/server/functions/unary/minus.go @@ -15,6 +15,7 @@ package unary import ( + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" @@ -32,6 +33,7 @@ func initUnaryMinus() { framework.RegisterUnaryFunction(framework.Operator_UnaryMinus, int2um) framework.RegisterUnaryFunction(framework.Operator_UnaryMinus, int4um) framework.RegisterUnaryFunction(framework.Operator_UnaryMinus, int8um) + framework.RegisterUnaryFunction(framework.Operator_UnaryMinus, interval_um) framework.RegisterUnaryFunction(framework.Operator_UnaryMinus, numeric_uminus) } @@ -90,6 +92,18 @@ var int8um = framework.Function1{ }, } +// interval_um represents the PostgreSQL function of the same name, taking the same parameters. +var interval_um = framework.Function1{ + Name: "interval_um", + Return: pgtypes.Interval, + Parameters: [1]pgtypes.DoltgresType{pgtypes.Interval}, + Strict: true, + Callable: func(ctx *sql.Context, _ [2]pgtypes.DoltgresType, val1 any) (any, error) { + dur := val1.(duration.Duration) + return dur.Mul(-1), nil + }, +} + // numeric_uminus represents the PostgreSQL function of the same name, taking the same parameters. var numeric_uminus = framework.Function1{ Name: "numeric_uminus", diff --git a/server/types/date.go b/server/types/date.go index 3aba2793a9..6d3409bee5 100644 --- a/server/types/date.go +++ b/server/types/date.go @@ -120,7 +120,7 @@ func (b DateType) GetSerializationID() SerializationID { // IoInput implements the DoltgresType interface. func (b DateType) IoInput(ctx *sql.Context, input string) (any, error) { // TODO: need support for calendar era, AD and BC - if t, err := time.Parse("2006-01-02", input); err == nil { + if t, err := time.Parse("2006-1-2", input); err == nil { return t.UTC(), nil } else if t, err = time.Parse("January 02, 2006", input); err == nil { return t.UTC(), nil diff --git a/server/types/interval.go b/server/types/interval.go index aa6eb00724..ec9b113cf0 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -180,7 +180,15 @@ func (b IntervalType) SerializedCompare(v1 []byte, v2 []byte) (int, error) { return -1, nil } - return bytes.Compare(v1, v2), nil + d1, err := deserializeDuration(v1) + if err != nil { + return 0, err + } + d2, err := deserializeDuration(v2) + if err != nil { + return 0, err + } + return d1.Compare(d2), nil } // SQL implements the DoltgresType interface. @@ -255,11 +263,15 @@ func (b IntervalType) DeserializeValue(val []byte) (any, error) { if len(val) == 0 { return nil, nil } + return deserializeDuration(val) +} + +func deserializeDuration(val []byte) (duration.Duration, error) { reader := utils.NewReader(val) str := reader.String() dInterval, err := tree.ParseDInterval(str) if err != nil { - return nil, err + return duration.Duration{}, err } return dInterval.Duration, nil } diff --git a/server/types/interval_array.go b/server/types/interval_array.go index f3ab21f32f..77e26ba9f6 100644 --- a/server/types/interval_array.go +++ b/server/types/interval_array.go @@ -17,4 +17,4 @@ package types import "github.com/lib/pq/oid" // IntervalArray is the array variant of Interval. -var IntervalArray = createArrayType(Text, SerializationID_IntervalArray, oid.T__interval) +var IntervalArray = createArrayType(Interval, SerializationID_IntervalArray, oid.T__interval) diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index cf074a9f8c..615fab6bc3 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -1352,5 +1352,78 @@ func TestDateAndTimeFunction(t *testing.T) { }, }, }, + { + Name: "Interval type functions", + SetUpScript: []string{ + "CREATE TABLE t_interval (id INTEGER primary key, v1 INTERVAL);", + "INSERT INTO t_interval VALUES (1, '1 day 3 hours'), (2, '23 hours 30 minutes');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: `select - interval '20 days -11:00:00';`, + Expected: []sql.Row{{"-20 days +11:00:00"}}, + }, + { + Query: `select '27:00:24'::interval = '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '27:00:24'::interval <> '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"f"}}, + }, + { + Query: `select '23:22:24'::interval > '1 day 03:00:00'::interval;`, + Expected: []sql.Row{{"f"}}, + }, + { + Query: `select '27:00:24'::interval >= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"f"}}, + }, + { + Query: `select '27:00:24'::interval < '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '27:00:24.5'::interval <= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select interval '2 days' + interval '1.5 days';`, + Expected: []sql.Row{{"3 days 12:00:00"}}, + }, + { + Query: `select interval '2 days' + time '12:23:34';`, + Expected: []sql.Row{{"12:23:34"}}, + }, + { + Query: `select interval '2 days' + date '2022-2-5';`, + Expected: []sql.Row{{"2022-02-07 00:00:00"}}, + }, + { + Query: `select interval '2 days' + time with time zone '12:23:45-0700';`, + Expected: []sql.Row{{"12:23:45-07"}}, + }, + { + Query: `select interval '2 days' + timestamp '2021-04-08 12:23:45';`, + Expected: []sql.Row{{"2021-04-10 12:23:45"}}, + }, + { + Query: `select interval '2 days' + timestamp with time zone '2021-04-08 12:23:45-0700';`, + Expected: []sql.Row{{"2021-04-10 12:23:45-07"}}, + }, + { + Query: `select interval '2 days' - interval '1.5 days';`, + Expected: []sql.Row{{"1 day -12:00:00"}}, + }, + { + Query: `select interval '20 days' / 2.3`, + Expected: []sql.Row{{"8 days 16:41:44.347826"}}, + }, + { + Query: `select interval '20 days' * 2.3`, + Expected: []sql.Row{{"46 days"}}, + }, + }, + }, }) } diff --git a/testing/go/types_test.go b/testing/go/types_test.go index 0cc4260c82..cd2b352312 100644 --- a/testing/go/types_test.go +++ b/testing/go/types_test.go @@ -581,20 +581,86 @@ var typesTests = []ScriptTest{ Name: "Interval type", SetUpScript: []string{ "CREATE TABLE t_interval (id INTEGER primary key, v1 INTERVAL);", - "INSERT INTO t_interval VALUES (1, '1 day 3 hours'), (2, '2 hours 30 minutes');", + "INSERT INTO t_interval VALUES (1, '1 day 3 hours'), (2, '23 hours 30 minutes');", }, Assertions: []ScriptTestAssertion{ { Query: "SELECT * FROM t_interval ORDER BY id;", Expected: []sql.Row{ {1, "1 day 03:00:00"}, - {2, "02:30:00"}, + {2, "23:30:00"}, + }, + }, + { + Query: "SELECT * FROM t_interval ORDER BY v1;", + Expected: []sql.Row{ + {2, "23:30:00"}, + {1, "1 day 03:00:00"}, + }, + }, + { + Query: `SELECT id, v1::char, v1::name FROM t_interval;`, + Expected: []sql.Row{ + {1, "1", "1 day 03:00:00"}, + {2, "2", "23:30:00"}, }, }, { Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval;`, Expected: []sql.Row{{"3 years 3 mons 700 days 133:17:36.789"}}, }, + { + Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval::char;`, + Expected: []sql.Row{{"3"}}, + }, + { + Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::char::interval;`, + Expected: []sql.Row{{"00:00:02"}}, + }, + { + Query: `SELECT '13 months'::name::interval;`, + Expected: []sql.Row{{"1 year 1 mon"}}, + }, + { + Query: `SELECT '13 months'::bpchar::interval;`, + Expected: []sql.Row{{"1 year 1 mon"}}, + }, + { + Query: `SELECT '13 months'::varchar::interval;`, + Expected: []sql.Row{{"1 year 1 mon"}}, + }, + { + Query: `SELECT '13 months'::text::interval;`, + Expected: []sql.Row{{"1 year 1 mon"}}, + }, + { + Query: `SELECT '13 months'::char::interval;`, + Expected: []sql.Row{{"00:00:01"}}, + }, + { + Query: "INSERT INTO t_interval VALUES (3, 7);", + ExpectedErr: `ASSIGNMENT_CAST: target is of type interval but expression is of type integer: 7`, + }, + { + Query: "INSERT INTO t_interval VALUES (3, true);", + ExpectedErr: `ASSIGNMENT_CAST: target is of type interval but expression is of type boolean: true`, + }, + }, + }, + { + Name: "Interval array type", + SetUpScript: []string{ + "CREATE TABLE t_interval_array (id INTEGER primary key, v1 INTERVAL[]);", + "INSERT INTO t_interval_array VALUES (1, ARRAY['1 day 3 hours'::interval,'5 days 2 hours'::interval]), (2, ARRAY['3 years 3 mons 700 days 133:17:36.789'::interval,'200 hours'::interval]);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM t_interval_array ORDER BY id;", + Expected: []sql.Row{ + {1, "{\"1 day 03:00:00\",\"5 days 02:00:00\"}"}, + {2, "{\"3 years 3 mons 700 days 133:17:36.789\",200:00:00}"}, + }, + }, }, }, { @@ -1896,6 +1962,13 @@ var typesTests = []ScriptTest{ {2, "23:45:01"}, }, }, + { + Query: "SELECT v1::interval FROM t_time_without_zone ORDER BY id;", + Expected: []sql.Row{ + {"12:34:56"}, + {"23:45:01"}, + }, + }, { Query: `SELECT '00:00:00'::time;`, Expected: []sql.Row{ @@ -1909,14 +1982,14 @@ var typesTests = []ScriptTest{ Skip: true, SetUpScript: []string{ "CREATE TABLE t_time_with_zone (id INTEGER primary key, v1 TIME WITH TIME ZONE);", - "INSERT INTO t_time_with_zone VALUES (1, '12:34:56 UTC'), (2, '23:45:01 America/New_York');", + "INSERT INTO t_time_with_zone VALUES (1, '12:34:56 UTC'), (2, '23:45:01-0200');", }, Assertions: []ScriptTestAssertion{ { Query: "SELECT * FROM t_time_with_zone ORDER BY id;", Expected: []sql.Row{ - {1, "12:34:56 UTC"}, - {2, "23:45:01 America/New_York"}, + {1, "12:34:56+00"}, + {2, "23:45:01-02"}, }, }, { From f8d3412d1ce286ea32c3e78dcc195ae67afad944 Mon Sep 17 00:00:00 2001 From: jennifersp Date: Thu, 8 Aug 2024 16:41:31 -0700 Subject: [PATCH 05/10] format and add godoc --- server/cast/interval.go | 3 ++- server/cast/time.go | 5 +++-- server/functions/binary/divide.go | 2 +- server/functions/binary/equal.go | 2 +- server/functions/binary/greater.go | 2 +- server/functions/binary/greater_equal.go | 2 +- server/functions/binary/less.go | 2 +- server/functions/binary/less_equal.go | 2 +- server/functions/binary/minus.go | 2 +- server/functions/binary/multiply.go | 2 +- server/functions/binary/not_equal.go | 2 +- server/functions/binary/plus.go | 13 ++++++++++--- server/functions/unary/minus.go | 3 ++- 13 files changed, 26 insertions(+), 16 deletions(-) diff --git a/server/cast/interval.go b/server/cast/interval.go index 582cb6cb72..bce4b0fb95 100644 --- a/server/cast/interval.go +++ b/server/cast/interval.go @@ -15,9 +15,10 @@ package cast import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) diff --git a/server/cast/time.go b/server/cast/time.go index e590ec10b1..4a7f1b9d54 100644 --- a/server/cast/time.go +++ b/server/cast/time.go @@ -15,10 +15,11 @@ package cast import ( - "github.com/dolthub/doltgresql/server/functions" - "github.com/dolthub/go-mysql-server/sql" "time" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/server/functions" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) diff --git a/server/functions/binary/divide.go b/server/functions/binary/divide.go index b1991e42c0..224dafa8d4 100644 --- a/server/functions/binary/divide.go +++ b/server/functions/binary/divide.go @@ -16,11 +16,11 @@ package binary import ( "fmt" - "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) diff --git a/server/functions/binary/equal.go b/server/functions/binary/equal.go index 0b2218d9ef..b9e403fbab 100644 --- a/server/functions/binary/equal.go +++ b/server/functions/binary/equal.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/greater.go b/server/functions/binary/greater.go index 2135a5b85f..1c87220787 100644 --- a/server/functions/binary/greater.go +++ b/server/functions/binary/greater.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/greater_equal.go b/server/functions/binary/greater_equal.go index 2802b6b285..1179209a7f 100644 --- a/server/functions/binary/greater_equal.go +++ b/server/functions/binary/greater_equal.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/less.go b/server/functions/binary/less.go index 419e4abacd..8cd212eaf8 100644 --- a/server/functions/binary/less.go +++ b/server/functions/binary/less.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/less_equal.go b/server/functions/binary/less_equal.go index b4a1c2b458..4ec1caaee7 100644 --- a/server/functions/binary/less_equal.go +++ b/server/functions/binary/less_equal.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/minus.go b/server/functions/binary/minus.go index 9e16f206c3..7fa9f3b217 100644 --- a/server/functions/binary/minus.go +++ b/server/functions/binary/minus.go @@ -16,12 +16,12 @@ package binary import ( "fmt" - "github.com/dolthub/doltgresql/postgres/parser/duration" "math" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) diff --git a/server/functions/binary/multiply.go b/server/functions/binary/multiply.go index 57d0dbfdb2..8f5381eca9 100644 --- a/server/functions/binary/multiply.go +++ b/server/functions/binary/multiply.go @@ -16,12 +16,12 @@ package binary import ( "fmt" - "github.com/dolthub/doltgresql/postgres/parser/duration" "math" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) diff --git a/server/functions/binary/not_equal.go b/server/functions/binary/not_equal.go index 941991b652..9f54b82b40 100644 --- a/server/functions/binary/not_equal.go +++ b/server/functions/binary/not_equal.go @@ -15,12 +15,12 @@ package binary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/postgres/parser/uuid" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" diff --git a/server/functions/binary/plus.go b/server/functions/binary/plus.go index 9d6f392836..e56e213cfa 100644 --- a/server/functions/binary/plus.go +++ b/server/functions/binary/plus.go @@ -16,14 +16,14 @@ package binary import ( "fmt" - "github.com/dolthub/doltgresql/postgres/parser/duration" - "github.com/dolthub/doltgresql/server/functions" "math" "time" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/server/functions" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -282,12 +282,19 @@ var interval_pl_timestamptz = framework.Function2{ }, } +// intervalPlusNonInterval adds given interval duration to the given time.Time value. +// During converting interval duration to time.Duration type, it can overflow. func intervalPlusNonInterval(d duration.Duration, t time.Time) (time.Time, error) { seconds, ok := d.AsInt64() if !ok { + if !ok { + return time.Time{}, fmt.Errorf("interval overflow") + } + } + nanos := float64(seconds) * functions.NanosPerSec + if nanos > float64(math.MaxInt64) || nanos < float64(math.MinInt64) { return time.Time{}, fmt.Errorf("interval overflow") } - nanos := seconds * functions.NanosPerSec // TODO: might overflow return t.Add(time.Duration(nanos)), nil } diff --git a/server/functions/unary/minus.go b/server/functions/unary/minus.go index acb11f2f0a..e614eb2415 100644 --- a/server/functions/unary/minus.go +++ b/server/functions/unary/minus.go @@ -15,10 +15,11 @@ package unary import ( - "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/go-mysql-server/sql" "github.com/shopspring/decimal" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) From 502ed7b2b00ef462fe9075ecb45706be605a8be1 Mon Sep 17 00:00:00 2001 From: jennifersp Date: Thu, 8 Aug 2024 16:53:19 -0700 Subject: [PATCH 06/10] use encoding/gob library for serialization --- server/types/interval.go | 43 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/server/types/interval.go b/server/types/interval.go index ec9b113cf0..fe10ad5128 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -16,18 +16,18 @@ package types import ( "bytes" + "encoding/gob" "fmt" "reflect" - "github.com/dolthub/doltgresql/postgres/parser/duration" - "github.com/dolthub/doltgresql/postgres/parser/sem/tree" - "github.com/dolthub/doltgresql/utils" - "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" "github.com/dolthub/vitess/go/sqltypes" "github.com/dolthub/vitess/go/vt/proto/query" "github.com/lib/pq/oid" + + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" ) // Interval is the interval type. @@ -180,14 +180,19 @@ func (b IntervalType) SerializedCompare(v1 []byte, v2 []byte) (int, error) { return -1, nil } - d1, err := deserializeDuration(v1) + var d1, d2 duration.Duration + dec := gob.NewDecoder(bytes.NewReader(v1)) + err := dec.Decode(&d1) if err != nil { return 0, err } - d2, err := deserializeDuration(v2) + + dec = gob.NewDecoder(bytes.NewReader(v2)) + err = dec.Decode(&d2) if err != nil { return 0, err } + return d1.Compare(d2), nil } @@ -252,10 +257,13 @@ func (b IntervalType) SerializeValue(val any) ([]byte, error) { if err != nil { return nil, err } - str := converted.(duration.Duration).String() - writer := utils.NewWriter(uint64(len(str) + 1)) - writer.String(str) - return writer.Data(), nil + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(converted.(duration.Duration)) + if err != nil { + return nil, err + } + return buf.Bytes(), nil } // DeserializeValue implements the DoltgresType interface. @@ -263,15 +271,8 @@ func (b IntervalType) DeserializeValue(val []byte) (any, error) { if len(val) == 0 { return nil, nil } - return deserializeDuration(val) -} - -func deserializeDuration(val []byte) (duration.Duration, error) { - reader := utils.NewReader(val) - str := reader.String() - dInterval, err := tree.ParseDInterval(str) - if err != nil { - return duration.Duration{}, err - } - return dInterval.Duration, nil + var deserialized duration.Duration + dec := gob.NewDecoder(bytes.NewReader(val)) + err := dec.Decode(&deserialized) + return deserialized, err } From d9ed65795dea60a7caaa26c0c4d1dfca77f2ce53 Mon Sep 17 00:00:00 2001 From: jennifersp Date: Thu, 8 Aug 2024 16:58:21 -0700 Subject: [PATCH 07/10] Revert "use encoding/gob library for serialization" This reverts commit 502ed7b2b00ef462fe9075ecb45706be605a8be1. --- server/types/interval.go | 43 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/server/types/interval.go b/server/types/interval.go index fe10ad5128..ec9b113cf0 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -16,18 +16,18 @@ package types import ( "bytes" - "encoding/gob" "fmt" "reflect" + "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + "github.com/dolthub/doltgresql/utils" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" "github.com/dolthub/vitess/go/sqltypes" "github.com/dolthub/vitess/go/vt/proto/query" "github.com/lib/pq/oid" - - "github.com/dolthub/doltgresql/postgres/parser/duration" - "github.com/dolthub/doltgresql/postgres/parser/sem/tree" ) // Interval is the interval type. @@ -180,19 +180,14 @@ func (b IntervalType) SerializedCompare(v1 []byte, v2 []byte) (int, error) { return -1, nil } - var d1, d2 duration.Duration - dec := gob.NewDecoder(bytes.NewReader(v1)) - err := dec.Decode(&d1) + d1, err := deserializeDuration(v1) if err != nil { return 0, err } - - dec = gob.NewDecoder(bytes.NewReader(v2)) - err = dec.Decode(&d2) + d2, err := deserializeDuration(v2) if err != nil { return 0, err } - return d1.Compare(d2), nil } @@ -257,13 +252,10 @@ func (b IntervalType) SerializeValue(val any) ([]byte, error) { if err != nil { return nil, err } - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - err = enc.Encode(converted.(duration.Duration)) - if err != nil { - return nil, err - } - return buf.Bytes(), nil + str := converted.(duration.Duration).String() + writer := utils.NewWriter(uint64(len(str) + 1)) + writer.String(str) + return writer.Data(), nil } // DeserializeValue implements the DoltgresType interface. @@ -271,8 +263,15 @@ func (b IntervalType) DeserializeValue(val []byte) (any, error) { if len(val) == 0 { return nil, nil } - var deserialized duration.Duration - dec := gob.NewDecoder(bytes.NewReader(val)) - err := dec.Decode(&deserialized) - return deserialized, err + return deserializeDuration(val) +} + +func deserializeDuration(val []byte) (duration.Duration, error) { + reader := utils.NewReader(val) + str := reader.String() + dInterval, err := tree.ParseDInterval(str) + if err != nil { + return duration.Duration{}, err + } + return dInterval.Duration, nil } From ecec683c26ecdf7803469778d8977c1f402ceb83 Mon Sep 17 00:00:00 2001 From: jennifersp Date: Fri, 9 Aug 2024 11:57:41 -0700 Subject: [PATCH 08/10] fixes --- server/cast/interval.go | 39 ++++---------- server/cast/time.go | 8 +-- server/functions/binary/plus.go | 32 +++++------ server/types/interval.go | 35 +++++------- server/types/time.go | 2 +- testing/go/functions_test.go | 73 ------------------------- testing/go/operators_test.go | 96 +++++++++++++++++++++++++++++++++ testing/go/types_test.go | 20 ++++++- 8 files changed, 157 insertions(+), 148 deletions(-) diff --git a/server/cast/interval.go b/server/cast/interval.go index bce4b0fb95..64887eaf1c 100644 --- a/server/cast/interval.go +++ b/server/cast/interval.go @@ -16,6 +16,7 @@ package cast import ( "github.com/dolthub/go-mysql-server/sql" + "time" "github.com/dolthub/doltgresql/postgres/parser/duration" @@ -26,7 +27,6 @@ import ( // initInterval handles all casts that are built-in. This comprises only the "From" types. func initInterval() { intervalAssignment() - intervalExplicit() intervalImplicit() } @@ -34,34 +34,13 @@ func initInterval() { func intervalAssignment() { framework.MustAddAssignmentTypeCast(framework.TypeCast{ FromType: pgtypes.Interval, - ToType: pgtypes.BpChar, + ToType: pgtypes.Time, Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { - return handleStringCast(val.(duration.Duration).String(), targetType) - }, - }) - framework.MustAddAssignmentTypeCast(framework.TypeCast{ - FromType: pgtypes.Interval, - ToType: pgtypes.VarChar, - Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { - return handleStringCast(val.(duration.Duration).String(), targetType) - }, - }) -} - -// intervalExplicit registers all implicit casts. This comprises only the "From" types. -func intervalExplicit() { - framework.MustAddExplicitTypeCast(framework.TypeCast{ - FromType: pgtypes.Interval, - ToType: pgtypes.InternalChar, - Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { - return handleStringCast(val.(duration.Duration).String(), targetType) - }, - }) - framework.MustAddExplicitTypeCast(framework.TypeCast{ - FromType: pgtypes.Interval, - ToType: pgtypes.Name, - Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { - return handleStringCast(val.(duration.Duration).String(), targetType) + dur := val.(duration.Duration) + // truncate the month and day of the duration. + dur.Months = 0 + dur.Days = 0 + return time.Parse("15:04:05.999", dur.String()) }, }) } @@ -70,9 +49,9 @@ func intervalExplicit() { func intervalImplicit() { framework.MustAddImplicitTypeCast(framework.TypeCast{ FromType: pgtypes.Interval, - ToType: pgtypes.Text, + ToType: pgtypes.Interval, Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { - return val.(duration.Duration).String(), nil + return val.(duration.Duration), nil }, }) } diff --git a/server/cast/time.go b/server/cast/time.go index 4a7f1b9d54..ef37beef6e 100644 --- a/server/cast/time.go +++ b/server/cast/time.go @@ -26,12 +26,12 @@ import ( // initTime handles all casts that are built-in. This comprises only the "From" types. func initTime() { - timeExplicit() + timeImplicit() } -// timeExplicit registers all implicit casts. This comprises only the "From" types. -func timeExplicit() { - framework.MustAddExplicitTypeCast(framework.TypeCast{ +// timeImplicit registers all implicit casts. This comprises only the "From" types. +func timeImplicit() { + framework.MustAddImplicitTypeCast(framework.TypeCast{ FromType: pgtypes.Time, ToType: pgtypes.Interval, Function: func(ctx *sql.Context, val any, targetType pgtypes.DoltgresType) (any, error) { diff --git a/server/functions/binary/plus.go b/server/functions/binary/plus.go index e56e213cfa..65855b5b72 100644 --- a/server/functions/binary/plus.go +++ b/server/functions/binary/plus.go @@ -282,22 +282,6 @@ var interval_pl_timestamptz = framework.Function2{ }, } -// intervalPlusNonInterval adds given interval duration to the given time.Time value. -// During converting interval duration to time.Duration type, it can overflow. -func intervalPlusNonInterval(d duration.Duration, t time.Time) (time.Time, error) { - seconds, ok := d.AsInt64() - if !ok { - if !ok { - return time.Time{}, fmt.Errorf("interval overflow") - } - } - nanos := float64(seconds) * functions.NanosPerSec - if nanos > float64(math.MaxInt64) || nanos < float64(math.MinInt64) { - return time.Time{}, fmt.Errorf("interval overflow") - } - return t.Add(time.Duration(nanos)), nil -} - // numeric_add represents the PostgreSQL function of the same name, taking the same parameters. var numeric_add = framework.Function2{ Name: "numeric_add", @@ -322,3 +306,19 @@ func plusOverflow(val1 int64, val2 int64) (any, error) { } return val1 + val2, nil } + +// intervalPlusNonInterval adds given interval duration to the given time.Time value. +// During converting interval duration to time.Duration type, it can overflow. +func intervalPlusNonInterval(d duration.Duration, t time.Time) (time.Time, error) { + seconds, ok := d.AsInt64() + if !ok { + if !ok { + return time.Time{}, fmt.Errorf("interval overflow") + } + } + nanos := float64(seconds) * functions.NanosPerSec + if nanos > float64(math.MaxInt64) || nanos < float64(math.MinInt64) { + return time.Time{}, fmt.Errorf("interval overflow") + } + return t.Add(time.Duration(nanos)), nil +} diff --git a/server/types/interval.go b/server/types/interval.go index ec9b113cf0..908c2a0a56 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -180,15 +180,7 @@ func (b IntervalType) SerializedCompare(v1 []byte, v2 []byte) (int, error) { return -1, nil } - d1, err := deserializeDuration(v1) - if err != nil { - return 0, err - } - d2, err := deserializeDuration(v2) - if err != nil { - return 0, err - } - return d1.Compare(d2), nil + return bytes.Compare(v1, v2), nil } // SQL implements the DoltgresType interface. @@ -252,9 +244,14 @@ func (b IntervalType) SerializeValue(val any) ([]byte, error) { if err != nil { return nil, err } - str := converted.(duration.Duration).String() - writer := utils.NewWriter(uint64(len(str) + 1)) - writer.String(str) + sortNanos, months, days, err := converted.(duration.Duration).Encode() + if err != nil { + return nil, err + } + writer := utils.NewWriter(0) + writer.Int64(sortNanos) + writer.Int64(months) + writer.Int64(days) return writer.Data(), nil } @@ -263,15 +260,9 @@ func (b IntervalType) DeserializeValue(val []byte) (any, error) { if len(val) == 0 { return nil, nil } - return deserializeDuration(val) -} - -func deserializeDuration(val []byte) (duration.Duration, error) { reader := utils.NewReader(val) - str := reader.String() - dInterval, err := tree.ParseDInterval(str) - if err != nil { - return duration.Duration{}, err - } - return dInterval.Duration, nil + sortNanos := reader.Int64() + months := reader.Int64() + days := reader.Int64() + return duration.Decode(sortNanos, months, days) } diff --git a/server/types/time.go b/server/types/time.go index 310d7c12cd..2ba16d9a06 100644 --- a/server/types/time.go +++ b/server/types/time.go @@ -124,7 +124,7 @@ func (b TimeType) GetSerializationID() SerializationID { func (b TimeType) IoInput(ctx *sql.Context, input string) (any, error) { if t, err := time.Parse("15:04:05", input); err == nil { return t.UTC(), nil - } else if t, err = time.Parse("15:04:05.000", input); err == nil { + } else if t, err = time.Parse("15:04:05.999", input); err == nil { return t.UTC(), nil } return nil, fmt.Errorf("invalid format for time") diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index 615fab6bc3..cf074a9f8c 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -1352,78 +1352,5 @@ func TestDateAndTimeFunction(t *testing.T) { }, }, }, - { - Name: "Interval type functions", - SetUpScript: []string{ - "CREATE TABLE t_interval (id INTEGER primary key, v1 INTERVAL);", - "INSERT INTO t_interval VALUES (1, '1 day 3 hours'), (2, '23 hours 30 minutes');", - }, - Assertions: []ScriptTestAssertion{ - { - Query: `select - interval '20 days -11:00:00';`, - Expected: []sql.Row{{"-20 days +11:00:00"}}, - }, - { - Query: `select '27:00:24'::interval = '1 day 03:00:24'::interval;`, - Expected: []sql.Row{{"t"}}, - }, - { - Query: `select '27:00:24'::interval <> '1 day 03:00:24'::interval;`, - Expected: []sql.Row{{"f"}}, - }, - { - Query: `select '23:22:24'::interval > '1 day 03:00:00'::interval;`, - Expected: []sql.Row{{"f"}}, - }, - { - Query: `select '27:00:24'::interval >= '1 day 03:00:24.5'::interval;`, - Expected: []sql.Row{{"f"}}, - }, - { - Query: `select '27:00:24'::interval < '1 day 03:00:24.5'::interval;`, - Expected: []sql.Row{{"t"}}, - }, - { - Query: `select '27:00:24.5'::interval <= '1 day 03:00:24.5'::interval;`, - Expected: []sql.Row{{"t"}}, - }, - { - Query: `select interval '2 days' + interval '1.5 days';`, - Expected: []sql.Row{{"3 days 12:00:00"}}, - }, - { - Query: `select interval '2 days' + time '12:23:34';`, - Expected: []sql.Row{{"12:23:34"}}, - }, - { - Query: `select interval '2 days' + date '2022-2-5';`, - Expected: []sql.Row{{"2022-02-07 00:00:00"}}, - }, - { - Query: `select interval '2 days' + time with time zone '12:23:45-0700';`, - Expected: []sql.Row{{"12:23:45-07"}}, - }, - { - Query: `select interval '2 days' + timestamp '2021-04-08 12:23:45';`, - Expected: []sql.Row{{"2021-04-10 12:23:45"}}, - }, - { - Query: `select interval '2 days' + timestamp with time zone '2021-04-08 12:23:45-0700';`, - Expected: []sql.Row{{"2021-04-10 12:23:45-07"}}, - }, - { - Query: `select interval '2 days' - interval '1.5 days';`, - Expected: []sql.Row{{"1 day -12:00:00"}}, - }, - { - Query: `select interval '20 days' / 2.3`, - Expected: []sql.Row{{"8 days 16:41:44.347826"}}, - }, - { - Query: `select interval '20 days' * 2.3`, - Expected: []sql.Row{{"46 days"}}, - }, - }, - }, }) } diff --git a/testing/go/operators_test.go b/testing/go/operators_test.go index 76844f608a..2a8ebfe563 100644 --- a/testing/go/operators_test.go +++ b/testing/go/operators_test.go @@ -169,6 +169,30 @@ func TestOperators(t *testing.T) { Query: `SELECT 1::numeric + 2::numeric;`, Expected: []sql.Row{{Numeric("3")}}, }, + { + Query: `select interval '2 days' + interval '1.5 days';`, + Expected: []sql.Row{{"3 days 12:00:00"}}, + }, + { + Query: `select interval '2 days' + time '12:23:34';`, + Expected: []sql.Row{{"12:23:34"}}, + }, + { + Query: `select interval '2 days' + date '2022-2-5';`, + Expected: []sql.Row{{"2022-02-07 00:00:00"}}, + }, + { + Query: `select interval '2 days' + time with time zone '12:23:45-0700';`, + Expected: []sql.Row{{"12:23:45-07"}}, + }, + { + Query: `select interval '2 days' + timestamp '2021-04-08 12:23:45';`, + Expected: []sql.Row{{"2021-04-10 12:23:45"}}, + }, + { + Query: `select interval '2 days' + timestamp with time zone '2021-04-08 12:23:45-0700';`, + Expected: []sql.Row{{"2021-04-10 12:23:45-07"}}, + }, }, }, { @@ -318,6 +342,10 @@ func TestOperators(t *testing.T) { Query: `SELECT 1::numeric - 2::numeric;`, Expected: []sql.Row{{Numeric("-1")}}, }, + { + Query: `select interval '2 days' - interval '1.5 days';`, + Expected: []sql.Row{{"1 day -12:00:00"}}, + }, }, }, { @@ -467,6 +495,10 @@ func TestOperators(t *testing.T) { Query: `SELECT 1::numeric * 2::numeric;`, Expected: []sql.Row{{Numeric("2")}}, }, + { + Query: `select interval '20 days' * 2.3`, + Expected: []sql.Row{{"46 days"}}, + }, }, }, { @@ -616,6 +648,10 @@ func TestOperators(t *testing.T) { Query: `SELECT 8::numeric / 2::numeric;`, Expected: []sql.Row{{Numeric("4")}}, }, + { + Query: `select interval '20 days' / 2.3`, + Expected: []sql.Row{{"8 days 16:41:44.347826"}}, + }, }, }, { @@ -1060,6 +1096,14 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid < '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"f"}}, }, + { + Query: `select '27:00:24'::interval < '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '27:01:24'::interval < '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -1357,6 +1401,14 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid > '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"t"}}, }, + { + Query: `select '28:22:24'::interval > '1 day 03:00:00'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '23:22:24'::interval > '1 day 03:00:00'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -1794,6 +1846,18 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid <= '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"f"}}, }, + { + Query: `select '27:00:24.5'::interval <= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '25:00:24.5'::interval <= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '2 days 27:00:24.5'::interval <= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -2231,6 +2295,18 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid >= '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"t"}}, }, + { + Query: `select '1 month 00:00:24'::interval >= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '2 days 00:00:24'::interval >= '48:00:24'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '27:00:24'::interval >= '1 day 03:00:24.5'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -2528,6 +2604,14 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid = '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"f"}}, }, + { + Query: `select '27:00:24'::interval = '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '1 day'::interval = '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -2821,6 +2905,14 @@ func TestOperators(t *testing.T) { Query: `SELECT '64b67ba1-e368-4cfd-ae6f-0c3e77716fb6'::uuid <> '64b67ba1-e368-4cfd-ae6f-0c3e77716fb5'::uuid;`, Expected: []sql.Row{{"t"}}, }, + { + Query: `select '3 hours 24 seconds'::interval <> '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `select '27:00:24'::interval <> '1 day 03:00:24'::interval;`, + Expected: []sql.Row{{"f"}}, + }, }, }, { @@ -3050,6 +3142,10 @@ func TestOperators(t *testing.T) { Query: `SELECT -(7::numeric);`, Expected: []sql.Row{{Numeric("-7")}}, }, + { + Query: `select - interval '20 days -11:00:00';`, + Expected: []sql.Row{{"-20 days +11:00:00"}}, + }, }, }, { diff --git a/testing/go/types_test.go b/testing/go/types_test.go index cd2b352312..fa867416b4 100644 --- a/testing/go/types_test.go +++ b/testing/go/types_test.go @@ -494,6 +494,18 @@ var typesTests = []ScriptTest{ {2, "2023-02-02"}, }, }, + { + Query: "SELECT date '2022-2-2'", + Expected: []sql.Row{ + {"2022-02-02"}, + }, + }, + { + Query: "SELECT date '2022-02-02'", + Expected: []sql.Row{ + {"2022-02-02"}, + }, + }, }, }, { @@ -613,6 +625,10 @@ var typesTests = []ScriptTest{ Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval::char;`, Expected: []sql.Row{{"3"}}, }, + { + Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::interval::text;`, + Expected: []sql.Row{{"3 years 3 mons 700 days 133:17:36.789"}}, + }, { Query: `SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::char::interval;`, Expected: []sql.Row{{"00:00:02"}}, @@ -657,8 +673,8 @@ var typesTests = []ScriptTest{ { Query: "SELECT * FROM t_interval_array ORDER BY id;", Expected: []sql.Row{ - {1, "{\"1 day 03:00:00\",\"5 days 02:00:00\"}"}, - {2, "{\"3 years 3 mons 700 days 133:17:36.789\",200:00:00}"}, + {1, `{"1 day 03:00:00","5 days 02:00:00"}`}, + {2, `{"3 years 3 mons 700 days 133:17:36.789",200:00:00}`}, }, }, }, From 92a1682834d1f7ff76e3bebfbc5546582eb2425d Mon Sep 17 00:00:00 2001 From: jennifersp Date: Fri, 9 Aug 2024 12:01:38 -0700 Subject: [PATCH 09/10] format --- server/cast/interval.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/cast/interval.go b/server/cast/interval.go index 64887eaf1c..b6451231c9 100644 --- a/server/cast/interval.go +++ b/server/cast/interval.go @@ -15,11 +15,11 @@ package cast import ( - "github.com/dolthub/go-mysql-server/sql" "time" - "github.com/dolthub/doltgresql/postgres/parser/duration" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) From 817b81d442e7c82ad081b19701d47ad800dca5aa Mon Sep 17 00:00:00 2001 From: jennifersp Date: Fri, 9 Aug 2024 17:54:16 -0700 Subject: [PATCH 10/10] use int32 --- server/types/interval.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/types/interval.go b/server/types/interval.go index 908c2a0a56..379e17b3f5 100644 --- a/server/types/interval.go +++ b/server/types/interval.go @@ -250,8 +250,8 @@ func (b IntervalType) SerializeValue(val any) ([]byte, error) { } writer := utils.NewWriter(0) writer.Int64(sortNanos) - writer.Int64(months) - writer.Int64(days) + writer.Int32(int32(months)) + writer.Int32(int32(days)) return writer.Data(), nil } @@ -262,7 +262,7 @@ func (b IntervalType) DeserializeValue(val []byte) (any, error) { } reader := utils.NewReader(val) sortNanos := reader.Int64() - months := reader.Int64() - days := reader.Int64() - return duration.Decode(sortNanos, months, days) + months := reader.Int32() + days := reader.Int32() + return duration.Decode(sortNanos, int64(months), int64(days)) }