Files
dolthub--doltgresql/server/functions/numeric.go
T
2026-07-13 12:32:25 +08:00

328 lines
9.8 KiB
Go

// 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 (
"encoding/binary"
"fmt"
"strconv"
"strings"
"github.com/cockroachdb/apd/v3"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/utils"
)
// initNumeric registers the functions to the catalog.
func initNumeric() {
framework.RegisterFunction(numeric_in)
framework.RegisterFunction(numeric_out)
framework.RegisterFunction(numeric_recv)
framework.RegisterFunction(numeric_send)
framework.RegisterFunction(numerictypmodin)
framework.RegisterFunction(numerictypmodout)
framework.RegisterFunction(numeric_cmp)
}
// numeric_in represents the PostgreSQL function of numeric type IO input.
var numeric_in = framework.Function3{
Name: "numeric_in",
Return: pgtypes.Numeric,
Parameters: [3]*pgtypes.DoltgresType{pgtypes.Cstring, pgtypes.Oid, pgtypes.Int32},
Strict: true,
Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) {
input := val1.(string)
typmod := val3.(int32)
dec, _, err := apd.NewFromString(strings.TrimSpace(input))
if err != nil {
return nil, pgtypes.ErrInvalidSyntaxForType.New("numeric", input)
}
return pgtypes.GetNumericValueWithTypmod(dec, typmod)
},
}
// numeric_out represents the PostgreSQL function of numeric type IO output.
var numeric_out = framework.Function1{
Name: "numeric_out",
Return: pgtypes.Cstring,
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Numeric},
Strict: true,
Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) {
typ := t[0]
dec := val.(*apd.Decimal)
tm := typ.GetAttTypMod()
res, err := pgtypes.GetNumericValueWithTypmod(dec, tm)
if err != nil {
return nil, err
}
return res.Text('f'), nil
},
}
// numeric_recv represents the PostgreSQL function of numeric type IO receive.
var numeric_recv = framework.Function3{
Name: "numeric_recv",
Return: pgtypes.Numeric,
Parameters: [3]*pgtypes.DoltgresType{pgtypes.Internal, pgtypes.Oid, pgtypes.Int32},
Strict: true,
Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) {
data := val1.([]byte)
if data == nil {
return nil, nil
}
//typmod := val3.(int32)
if len(data) == 0 {
return nil, nil
}
reader := utils.NewWireReader(data)
var d *apd.Decimal
// 1. Read Header
ndigits := reader.ReadInt16()
weight := reader.ReadInt16()
sign := reader.ReadInt16()
dscale := reader.ReadInt16()
// 2. Handle Special Values (NaN, Inf)
// These usually manifest as specific bit patterns in the header
switch uint16(sign) {
case 0xC000: // pgNumericNaN
d.Form = apd.NaN
return d, nil
case 0xD000: // pgNumericPosInf
d.Form = apd.Infinite
return d, nil
case 0xF000: // pgNumericNegInf
d.Form = apd.Infinite
d.Negative = true
return d, nil
}
// 3. Handle Finite Values
if ndigits == 0 {
d.SetInt64(0)
return d, nil
}
// Read base-10000 digits
digits := make([]int16, ndigits)
for i := 0; i < int(ndigits); i++ {
digits[i] = reader.ReadInt16()
}
// 4. Convert base-10000 to string for *apd.Decimal
// Each digit is exactly 4 characters wide (except potentially the first)
var sb strings.Builder
if sign == 16384 {
sb.WriteByte('-')
}
for i, digit := range digits {
// Calculate how many 10000-base digits are before the decimal
// 'weight' is the index of the first digit, where 0 is 10^0 in base 10000
if i == int(weight)+1 {
sb.WriteByte('.')
}
sDigit := strconv.Itoa(int(digit))
// Pad with leading zeros if not the very first digit
if l := len(sDigit); l < 4 {
padding := 4 - l
for p := 0; p < padding; p++ {
sb.WriteByte('0')
}
}
sb.WriteString(sDigit)
}
// If weight is larger than digits, we need trailing zeros
if int(weight) >= len(digits) {
for i := 0; i < int(weight)-len(digits)+1; i++ {
sb.WriteString("0000")
}
}
dec, _, err := sql.DecimalHighPrecisionCtx.NewFromString(sb.String())
if err != nil {
return nil, err
}
return sql.DecimalRound(dec, int32(dscale))
},
}
// numeric_send represents the PostgreSQL function of numeric type IO send.
var numeric_send = framework.Function1{
Name: "numeric_send",
Return: pgtypes.Bytea,
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Numeric},
Strict: true,
Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) {
num := val.(*apd.Decimal)
typmod := t[0].GetAttTypMod()
writer := utils.NewWireWriter()
if num.Form == apd.Finite {
// Short-circuit if this is the zero value
if num.IsZero() && num.Exponent == 0 {
writer.WriteBytes([]byte{0, 0, 0, 0, 0, 0, 0, 0})
return writer.BufferData(), nil
}
// There's a way to do this more efficiently, but we can do that work once this becomes a performance issue.
// This is based on the terminology used in Postgres' `numeric.c` file
decStr := num.Text('f')
isNegative := false
if strings.HasPrefix(decStr, "-") {
isNegative = true
decStr = decStr[1:]
}
// Split the integer and fractional parts
var intPart string
var fractPart string
if idx := strings.Index(decStr, "."); idx != -1 {
intPart = decStr[:idx]
fractPart = decStr[idx+1:]
} else {
intPart = decStr
}
// Find the "dscale", which is the number of digits in the fractional part
var dscale int16
if typmod != -1 {
_, dscale32 := pgtypes.GetPrecisionAndScaleFromTypmod(typmod)
dscale = int16(dscale32)
} else {
dscale = int16(len(fractPart))
}
// Pad the integer and fractional parts so that we can take groups of 4 numbers
if intPart == "0" {
intPart = ""
} else if len(intPart)%4 != 0 {
intPart = strings.Repeat("0", 4-(len(intPart)%4)) + intPart
}
if len(fractPart)%4 != 0 {
// remove trailing zeroes on right side before filling it.
fractPart = strings.TrimRightFunc(fractPart, func(r rune) bool {
return r == '0'
})
fractPart = fractPart + strings.Repeat("0", 4-(len(fractPart)%4))
}
// Write the "ndigits" first, or the number of base-10000 digits
writer.WriteInt16(int16((len(intPart) / 4) + (len(fractPart) / 4)))
// Write the "weight", which is the number of base-10000 digits in the integer part subtracted by 1
writer.WriteInt16(int16((len(intPart) / 4) - 1))
// Write the "sign"
if isNegative {
writer.WriteInt16(16384)
} else {
writer.WriteInt16(0)
}
// Write the "dscale"
writer.WriteInt16(dscale)
// Write all of the digits
fullPart := intPart + fractPart
for i := 0; i < len(fullPart); i += 4 {
part, err := strconv.Atoi(fullPart[i : i+4])
if err != nil {
return nil, err
}
writer.WriteInt16(int16(part))
}
} else {
var buf []byte
wp := len(buf)
buf = append(buf, 0, 0, 0, 0, 0, 0, 0, 0)
if num.Form == apd.NaN {
binary.BigEndian.PutUint64(buf[wp:], pgNumericNaN)
} else if num.Form == apd.Infinite {
if num.Negative {
binary.BigEndian.PutUint64(buf[wp:], pgNumericNegInf)
} else {
binary.BigEndian.PutUint64(buf[wp:], pgNumericPosInf)
}
}
if typmod == -1 {
binary.BigEndian.PutUint16(buf[6:], uint16(32))
}
writer.WriteBytes(buf)
}
return writer.BufferData(), nil
},
}
// numerictypmodin represents the PostgreSQL function of numeric type IO typmod input.
var numerictypmodin = framework.Function1{
Name: "numerictypmodin",
Return: pgtypes.Int32,
Parameters: [1]*pgtypes.DoltgresType{pgtypes.CstringArray},
Strict: true,
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
arr := val.([]any)
if len(arr) == 0 {
return nil, pgtypes.ErrTypmodArrayMustBe1D.New()
} else if len(arr) > 2 {
return nil, pgtypes.ErrInvalidTypMod.New("NUMERIC")
}
p, err := strconv.ParseInt(arr[0].(string), 10, 32)
if err != nil {
return nil, err
}
precision := int32(p)
scale := int32(0)
if len(arr) == 2 {
s, err := strconv.ParseInt(arr[1].(string), 10, 32)
if err != nil {
return nil, err
}
scale = int32(s)
}
return pgtypes.GetTypmodFromNumericPrecisionAndScale(precision, scale)
},
}
// numerictypmodout represents the PostgreSQL function of numeric type IO typmod output.
var numerictypmodout = framework.Function1{
Name: "numerictypmodout",
Return: pgtypes.Cstring,
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32},
Strict: true,
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
typmod := val.(int32)
precision, scale := pgtypes.GetPrecisionAndScaleFromTypmod(typmod)
return fmt.Sprintf("(%v,%v)", precision, scale), nil
},
}
// numeric_cmp represents the PostgreSQL function of numeric type compare.
var numeric_cmp = framework.Function2{
Name: "numeric_cmp",
Return: pgtypes.Int32,
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Numeric, pgtypes.Numeric},
Strict: true,
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
ab := val1.(*apd.Decimal)
bb := val2.(*apd.Decimal)
return int32(pgtypes.NumericCompare(ab, bb)), nil
},
}
const (
pgNumericNaN = 0x00000000c0000000
pgNumericPosInf = 0x00000000d0000000
pgNumericNegInf = 0x00000000f0000000
)