Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 37 additions & 24 deletions server/doltgres_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func (h *DoltgresHandler) convertBindParameters(ctx *sql.Context, types []uint32
return nil, err
}
if values[i] != nil {
if formatCode == 0 {
if formatCode == pgtype.TextFormatCode {
v, err := dgType.IoInput(ctx, string(values[i]))
if err != nil {
return nil, err
Expand Down Expand Up @@ -650,7 +650,11 @@ func resultForMax1RowIter(ctx *sql.Context, schema sql.Schema, iter sql.RowIter,
return nil, err
}

outputRow, err := rowToBytes(ctx, schema, row, formatCodes)
encoder, err := newWireRowEncoder(ctx, schema, formatCodes)
if err != nil {
return nil, err
}
outputRow, err := encoder.encode(ctx, row)
if err != nil {
return nil, err
}
Expand All @@ -664,6 +668,10 @@ func resultForMax1RowIter(ctx *sql.Context, schema sql.Schema, iter sql.RowIter,
// and writes results into the callback function.
func (h *DoltgresHandler) resultForDefaultIter(ctx *sql.Context, schema sql.Schema, iter sql.RowIter, callback func(*sql.Context, *Result) error, resultFields []pgproto3.FieldDescription, formatCodes []int16) (*Result, bool, error) {
defer trace.StartRegion(ctx, "DoltgresHandler.resultForDefaultIter").End()
encoder, err := newWireRowEncoder(ctx, schema, formatCodes)
if err != nil {
return nil, false, err
}

// TODO: use errguard.Go instead?
pan2err := func(err *error) {
Expand Down Expand Up @@ -764,7 +772,7 @@ func (h *DoltgresHandler) resultForDefaultIter(ctx *sql.Context, schema sql.Sche
}

outputRow := res.nextRowValues(len(schema))
rErr := rowToBytesInto(ctx, schema, row, formatCodes, outputRow)
rErr := encoder.encodeInto(ctx, row, outputRow)
if rErr != nil {
return rErr
}
Expand Down Expand Up @@ -816,7 +824,7 @@ func (h *DoltgresHandler) resultForDefaultIter(ctx *sql.Context, schema sql.Sche
return iter.Close(ctx)
})

err := eg.Wait()
err = eg.Wait()
if err != nil {
if printErrorStackTraces {
fmt.Printf("error running query: %+v\n", err)
Expand Down Expand Up @@ -865,31 +873,36 @@ func rowToBytesInto(ctx *sql.Context, s sql.Schema, row sql.Row, formatCodes []i
for i, v := range row {
if v == nil {
o[i] = nil
} else if formatCodes[i] == 1 {
switch d := s[i].Type.(type) {
case *pgtypes.DoltgresType:
o[i], err = d.CallSend(ctx, v)
if err != nil {
return err
}
default:
cast := pgexprs.NewGMSCast(expression.NewLiteral(v, d))
v, err = cast.Eval(ctx, nil)
if err != nil {
return err
}
o[i], err = cast.DoltgresType(ctx).CallSend(ctx, v)
if err != nil {
return err
}
}
} else {
val, err := s[i].Type.SQL(ctx, []byte{}, v) // We use []byte{} as there's a distinction between nil and empty
o[i], err = valueToBytes(ctx, s[i].Type, formatCodes[i], v)
if err != nil {
return err
}
o[i] = val.ToBytes()
}
}
return nil
}

// valueToBytes applies the generic text or binary conversion for one result value.
func valueToBytes(ctx *sql.Context, typ sql.Type, formatCode int16, v any) ([]byte, error) {
var err error
if formatCode == pgtype.BinaryFormatCode {
switch d := typ.(type) {
case *pgtypes.DoltgresType:
return d.CallSend(ctx, v)
default:
cast := pgexprs.NewGMSCast(expression.NewLiteral(v, d))
v, err = cast.Eval(ctx, nil)
if err != nil {
return nil, err
}
return cast.DoltgresType(ctx).CallSend(ctx, v)
}
} else {
val, err := typ.SQL(ctx, []byte{}, v) // We use []byte{} as there's a distinction between nil and empty
if err != nil {
return nil, err
}
return val.ToBytes(), nil
}
}
11 changes: 4 additions & 7 deletions server/functions/float4.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package functions

import (
"math"
"strconv"
"strings"

Expand Down Expand Up @@ -62,13 +61,11 @@ var float4out = framework.Function1{
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Float32},
Strict: true,
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
fVal := float64(val.(float32))
if math.IsInf(fVal, 1) {
return "Infinity", nil
} else if math.IsInf(fVal, -1) {
return "-Infinity", nil
extraFloatDigits, err := pgtypes.ExtraFloatDigits(ctx)
if err != nil {
return nil, err
}
return strconv.FormatFloat(fVal, 'f', -1, 32), nil
return string(pgtypes.AppendFloat32Text(nil, val.(float32), extraFloatDigits)), nil
},
}

Expand Down
11 changes: 4 additions & 7 deletions server/functions/float8.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package functions

import (
"math"
"strconv"
"strings"

Expand Down Expand Up @@ -62,13 +61,11 @@ var float8out = framework.Function1{
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Float64},
Strict: true,
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
fVal := val.(float64)
if math.IsInf(fVal, 1) {
return "Infinity", nil
} else if math.IsInf(fVal, -1) {
return "-Infinity", nil
extraFloatDigits, err := pgtypes.ExtraFloatDigits(ctx)
if err != nil {
return nil, err
}
return strconv.FormatFloat(fVal, 'f', -1, 64), nil
return string(pgtypes.AppendFloat64Text(nil, val.(float64), extraFloatDigits)), nil
},
}

Expand Down
86 changes: 86 additions & 0 deletions server/types/float_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2026 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 (
"fmt"
"math"
"strconv"

"github.com/dolthub/go-mysql-server/sql"
)

// ExtraFloatDigits returns the PostgreSQL float-output precision setting.
func ExtraFloatDigits(ctx *sql.Context) (int, error) {
if ctx == nil {
return 1, nil
}
value, err := ctx.GetSessionVariable(ctx, "extra_float_digits")
if err != nil {
return 0, err
}
switch value := value.(type) {
case int:
return value, nil
case int64:
return int(value), nil
default:
return 0, fmt.Errorf("extra_float_digits has unexpected type %T", value)
}
}

// AppendFloat32Text appends PostgreSQL-compatible float4 text to dst.
func AppendFloat32Text(dst []byte, value float32, extraFloatDigits int) []byte {
return appendFloatText(dst, float64(value), 32, extraFloatDigits)
}

// AppendFloat64Text appends PostgreSQL-compatible float8 text to dst.
func AppendFloat64Text(dst []byte, value float64, extraFloatDigits int) []byte {
return appendFloatText(dst, value, 64, extraFloatDigits)
}

// appendFloatText implements PostgreSQL's special values, shortest mode, and legacy precision mode.
func appendFloatText(dst []byte, value float64, bitSize int, extraFloatDigits int) []byte {
if math.IsInf(value, 1) {
return append(dst, "Infinity"...)
}
if math.IsInf(value, -1) {
return append(dst, "-Infinity"...)
}
if math.IsNaN(value) {
return append(dst, "NaN"...)
}
if extraFloatDigits <= 0 {
precision := 15 + extraFloatDigits
if bitSize == 32 {
precision = 6 + extraFloatDigits
}
if precision < 1 {
precision = 1
}
return strconv.AppendFloat(dst, value, 'g', precision, bitSize)
}

abs := math.Abs(value)
format := byte('f')
lowerFixed := 1e-4
if bitSize == 32 {
lowerFixed = float64(float32(1e-4))
}
if abs != 0 && (abs < lowerFixed || bitSize == 32 && abs >= 1e6 || bitSize == 64 && abs >= 1e15) {
format = 'e'
}
return strconv.AppendFloat(dst, value, format, -1, bitSize)
}
65 changes: 65 additions & 0 deletions server/types/float_output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 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 (
"math"
"testing"
)

// TestExtraFloatDigitsWithoutContextUsesDefault verifies context-free formatting uses PostgreSQL's default precision.
func TestExtraFloatDigitsWithoutContextUsesDefault(t *testing.T) {
got, err := ExtraFloatDigits(nil)
if err != nil {
t.Fatal(err)
}
if got != 1 {
t.Fatalf("got %d, want 1", got)
}
}

// TestAppendFloatTextMatchesPostgres verifies default and legacy output modes against PostgreSQL 15.
func TestAppendFloatTextMatchesPostgres(t *testing.T) {
tests := []struct {
name string
got []byte
extraFloatDigits int
want string
}{
{"float4 smallest subnormal", AppendFloat32Text(nil, math.SmallestNonzeroFloat32, 1), 1, "1e-45"},
{"float8 smallest subnormal", AppendFloat64Text(nil, math.SmallestNonzeroFloat64, 1), 1, "5e-324"},
{"float4 negative zero", AppendFloat32Text(nil, float32(math.Copysign(0, -1)), 1), 1, "-0"},
{"float8 negative zero", AppendFloat64Text(nil, math.Copysign(0, -1), 1), 1, "-0"},
{"float4 lower fixed", AppendFloat32Text(nil, 1e-4, 1), 1, "0.0001"},
{"float4 lower exponent", AppendFloat32Text(nil, 1e-5, 1), 1, "1e-05"},
{"float4 upper fixed", AppendFloat32Text(nil, 1e5, 1), 1, "100000"},
{"float4 upper exponent", AppendFloat32Text(nil, 1e6, 1), 1, "1e+06"},
{"float8 lower fixed", AppendFloat64Text(nil, 1e-4, 1), 1, "0.0001"},
{"float8 lower exponent", AppendFloat64Text(nil, 1e-5, 1), 1, "1e-05"},
{"float8 upper fixed", AppendFloat64Text(nil, 1e14, 1), 1, "100000000000000"},
{"float8 upper exponent", AppendFloat64Text(nil, 1e15, 1), 1, "1e+15"},
{"float4 legacy precision", AppendFloat32Text(nil, float32(1.17549435e-38), 0), 0, "1.17549e-38"},
{"float8 legacy precision", AppendFloat64Text(nil, 1.234567890123456, 0), 0, "1.23456789012346"},
{"float4 minimum precision", AppendFloat32Text(nil, float32(1.17549435e-38), -15), -15, "1e-38"},
{"float8 minimum precision", AppendFloat64Text(nil, 1.234567890123456, -15), -15, "1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if string(test.got) != test.want {
t.Fatalf("extra_float_digits=%d: got %q want %q", test.extraFloatDigits, test.got, test.want)
}
})
}
}
Loading
Loading