Files
2026-07-13 12:32:25 +08:00

287 lines
9.6 KiB
Go

// Copyright 2025 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 (
"strings"
"time"
"github.com/cockroachdb/apd/v3"
cerrors "github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/postgres/parser/duration"
"github.com/dolthub/doltgresql/postgres/parser/timeofday"
"github.com/dolthub/doltgresql/postgres/parser/timetz"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// initDatePart registers the functions to the catalog.
func initDatePart() {
framework.RegisterFunction(date_part_text_date)
framework.RegisterFunction(date_part_text_time)
framework.RegisterFunction(date_part_text_timetz)
framework.RegisterFunction(date_part_text_timestamp)
framework.RegisterFunction(date_part_text_timestamptz)
framework.RegisterFunction(date_part_text_interval)
}
// date_part_text_date represents the PostgreSQL date_part function for date type.
var date_part_text_date = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.Date},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
dateVal := val2.(time.Time)
switch strings.ToLower(field) {
case "hour", "hours", "microsecond", "microseconds", "millisecond", "milliseconds",
"minute", "minutes", "second", "seconds", "timezone", "timezone_hour", "timezone_minute":
return nil, ErrUnitNotSupported.New(field, "date")
case "epoch":
return float64(dateVal.UnixMicro()) / 1000000, nil
default:
result, err := getFieldFromTimeVal(field, dateVal)
if err != nil {
return nil, err
}
f, _ := result.Float64()
return f, nil
}
},
}
// date_part_text_time represents the PostgreSQL date_part function for time type.
var date_part_text_time = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.Time},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
timeVal := val2.(timeofday.TimeOfDay).ToTime()
switch strings.ToLower(field) {
case "century", "centuries", "day", "days", "decade", "decades", "dow", "doy",
"isodow", "isoyear", "julian", "millennium", "millenniums", "month", "months",
"quarter", "timezone", "timezone_hour", "timezone_minute", "week", "year", "years":
return nil, ErrUnitNotSupported.New(field, "time without time zone")
default:
result, err := getFieldFromTimeVal(field, timeVal)
if err != nil {
return nil, err
}
f, _ := result.Float64()
return f, nil
}
},
}
// date_part_text_timetz represents the PostgreSQL date_part function for time with time zone type.
var date_part_text_timetz = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.TimeTZ},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
timetzVal := val2.(timetz.TimeTZ).ToTime()
_, currentOffset := timetzVal.Zone()
switch strings.ToLower(field) {
case "century", "centuries", "day", "days", "decade", "decades", "dow", "doy",
"isodow", "isoyear", "julian", "millennium", "millenniums", "month", "months",
"quarter", "week", "year", "years":
return nil, ErrUnitNotSupported.New(field, "time with time zone")
case "timezone":
return float64(-currentOffset), nil
case "timezone_hour":
return float64(-currentOffset / 3600), nil
case "timezone_minute":
return float64((-currentOffset % 3600) / 60), nil
default:
result, err := getFieldFromTimeVal(field, timetzVal)
if err != nil {
return nil, err
}
f, _ := result.Float64()
return f, nil
}
},
}
// date_part_text_timestamp represents the PostgreSQL date_part function for timestamp type.
var date_part_text_timestamp = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.Timestamp},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
tsVal := val2.(time.Time)
switch strings.ToLower(field) {
case "timezone", "timezone_hour", "timezone_minute":
return nil, ErrUnitNotSupported.New(field, "timestamp without time zone")
default:
result, err := getFieldFromTimeVal(field, tsVal)
if err != nil {
return nil, err
}
f, _ := result.Float64()
return f, nil
}
},
}
// date_part_text_timestamptz represents the PostgreSQL date_part function for timestamp with time zone type.
var date_part_text_timestamptz = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.TimestampTZ},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
loc, err := GetServerLocation(ctx)
if err != nil {
return nil, err
}
tstzVal := val2.(time.Time).In(loc)
switch strings.ToLower(field) {
case "timezone":
// TODO: postgres seem to use server timezone regardless of input value
return float64(-28800), nil
case "timezone_hour":
// TODO: postgres seem to use server timezone regardless of input value
return float64(-8), nil
case "timezone_minute":
// TODO: postgres seem to use server timezone regardless of input value
return float64(0), nil
default:
result, err := getFieldFromTimeVal(field, tstzVal)
if err != nil {
return nil, err
}
f, _ := result.Float64()
return f, nil
}
},
}
// date_part_text_interval represents the PostgreSQL date_part function for interval type.
var date_part_text_interval = framework.Function2{
Name: "date_part",
Return: pgtypes.Float64,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Text, pgtypes.Interval},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
field := val1.(string)
dur := val2.(duration.Duration)
// This mirrors the exact logic from extract_text_interval
switch strings.ToLower(field) {
case "century", "centuries":
dec, err := numericFloor(float64(dur.Months) / 12 / 100)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "day", "days":
return float64(dur.Days), nil
case "decade", "decades":
dec, err := numericFloor(float64(dur.Months) / 12 / 10)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "epoch":
epoch := float64(duration.SecsPerDay*duration.DaysPerMonth*dur.Months) + float64(duration.SecsPerDay*dur.Days) +
(float64(dur.Nanos()) / float64(NanosPerSec))
return epoch, nil
case "hour", "hours":
hours := float64(dur.Nanos()) / float64(NanosPerSec*duration.SecsPerHour)
dec, err := numericFloor(hours)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "microsecond", "microseconds":
secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute)
microseconds := float64(secondsInNanos) / float64(NanosPerMicro)
return microseconds, nil
case "millennium", "millenniums":
dec, err := numericFloor(float64(dur.Months) / 12 / 1000)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "millisecond", "milliseconds":
secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute)
milliseconds := float64(secondsInNanos) / float64(NanosPerMilli)
return milliseconds, nil
case "minute", "minutes":
minutesInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerHour)
minutes := float64(minutesInNanos) / float64(NanosPerSec*duration.SecsPerMinute)
dec, err := numericFloor(minutes)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "month", "months":
return float64(dur.Months % 12), nil
case "quarter":
return float64((dur.Months%12-1)/3 + 1), nil
case "second", "seconds":
secondsInNanos := dur.Nanos() % (NanosPerSec * duration.SecsPerMinute)
seconds := float64(secondsInNanos) / float64(NanosPerSec)
return seconds, nil
case "year", "years":
dec, err := numericFloor(float64(dur.Months) / 12)
if err != nil {
return nil, err
}
f, _ := dec.Float64()
return f, nil
case "dow", "doy", "isodow", "isoyear", "julian", "timezone", "timezone_hour", "timezone_minute", "week":
return nil, ErrUnitNotSupported.New(field, "interval")
default:
return nil, ErrUnitNotSupported.New(field, "interval")
}
},
}
func numericFloor(val any) (*apd.Decimal, error) {
switch val.(type) {
case int64, float64:
// expects these types to Scan from
default:
return nil, cerrors.Errorf("invalid type for numeric convert: %T", val)
}
dec := new(apd.Decimal)
err := dec.Scan(val)
if err != nil {
return nil, err
}
_, err = sql.DecimalCtx.Floor(dec, dec)
if err != nil {
return nil, err
}
return dec, nil
}