Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Bug fixes:
- Do not attempt to get S3 bucket accelerate config for Snowflake-internal stages (matched by bucket name `sfc-*`) since s3:GetAccelerateConfiguration not granted anyways (snowflakedb/gosnowflake#1805).
- Fixed gosnowflake writing a `gosnowflake-cgo` directory under the system temp dir at package import time even when the driver was never used (e.g. when imported only as a transitive dependency). Minicore now loads lazily when the driver is first referenced (`NewConnector`/`OpenWithConfig`) instead of in `init()` (snowflakedb/gosnowflake#1807).
- Fixed `GET` from a LOCAL_FS stage downloading 0 files and returning `264011: not implemented`. Cloud downloads were unaffected (snowflakedb/gosnowflake#1810).
- Fixed `NUMBER` columns with a non-zero scale losing precision: values needing more significant digits than a binary float's mantissa were silently rounded, so `NUMBER(20,10)` holding `1234567890.1234567890` returned `1234567890.1234567889`. The `WithHigherPrecision` path is unchanged and still returns a 64-bit `*big.Float` (snowflakedb/gosnowflake#1835).

Internal changes:
- Migrated from deprecated `github.com/aws/aws-sdk-go-v2/feature/s3/manager` (v1.16.15) to `github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager` (v0.3.5).
Expand Down
7 changes: 3 additions & 4 deletions converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2406,11 +2406,10 @@ func arrowDecimal128ToValue(srcValue *array.Decimal128, rowIdx int, higherPrecis
}
return num.ToString(0)
}
f := decimalToBigFloat(num, int64(srcColumnMeta.Scale))
if higherPrecision {
return f
return decimalToBigFloat(num, int64(srcColumnMeta.Scale))
}
return fmt.Sprintf("%.*f", srcColumnMeta.Scale, f)
return num.ToString(int32(srcColumnMeta.Scale))
}
return nil
}
Expand Down Expand Up @@ -2461,7 +2460,7 @@ func arrowIntToValue(srcColumnMeta query.FieldMetadata, higherPrecision bool, va
f := intToBigFloat(val, int64(srcColumnMeta.Scale))
return f
}
return fmt.Sprintf("%.*f", srcColumnMeta.Scale, float64(val)/math.Pow10(srcColumnMeta.Scale))
return decimal128.FromI64(val).ToString(int32(srcColumnMeta.Scale))
}

func arrowRealToValue(srcValue *array.Float64, rowIdx int) snowflakeValue {
Expand Down
85 changes: 85 additions & 0 deletions converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,3 +1198,88 @@ func mustArray(v any, typ ...any) driver.Value {
}
return array
}

// exactDecimalString renders unscaled/10^scale exactly, independently of the conversion under test.
func exactDecimalString(unscaled *big.Int, scale int) string {
pow := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scale)), nil)
return new(big.Rat).SetFrac(unscaled, pow).FloatString(scale)
}

// Fixed-point values with a non-zero scale must round-trip exactly, however many significant digits
// they carry. The decimal128-backed path covers NUMBER with precision >= 19.
func TestArrowDecimal128ToValueExactForNonZeroScale(t *testing.T) {
testcases := []struct {
unscaled string
scale int
}{
// More significant digits than a 64-bit mantissa can represent.
{"12345678901234567890", 10},
{"-12345678901234567890", 10},
// 38 digits, the widest NUMBER Snowflake supports.
{"12345678901234567890123456789012345678", 10},
{"-12345678901234567890123456789012345678", 2},
{"99999999999999999999999999999999999999", 1},
// Values below one, where every digit is fractional.
{"5", 10},
{"-5", 10},
{"0", 10},
// Narrow values, as a regression guard.
{"12345", 2},
{"-12345", 2},
}

for _, tc := range testcases {
t.Run(fmt.Sprintf("%s_scale_%d", tc.unscaled, tc.scale), func(t *testing.T) {
unscaled, ok := new(big.Int).SetString(tc.unscaled, 10)
if !ok {
t.Fatalf("could not parse %v", tc.unscaled)
}
expected := exactDecimalString(unscaled, tc.scale)

builder := array.NewDecimal128Builder(memory.DefaultAllocator, &arrow.Decimal128Type{
Precision: 38, Scale: int32(tc.scale),
})
defer builder.Release()
builder.Append(decimal128.FromBigInt(unscaled))
arr := builder.NewDecimal128Array()
defer arr.Release()

meta := query.FieldMetadata{Type: "fixed", Precision: 38, Scale: tc.scale}
if actual := arrowDecimal128ToValue(arr, 0, false, meta); actual != expected {
t.Errorf("expected %v, got %v", expected, actual)
}
})
}
}

// The integer-backed path covers NUMBER with precision <= 18.
func TestArrowIntToValueExactForNonZeroScale(t *testing.T) {
testcases := []struct {
val int64
scale int
}{
// More significant digits than a float64 mantissa can represent.
{123456789012345678, 4},
{-123456789012345678, 4},
{999999999999999999, 2},
{math.MaxInt64, 2},
{math.MinInt64, 2},
{12345678901234567, 2},
// Narrow values, as a regression guard.
{1234567890, 2},
{-1234567890, 2},
{5, 10},
{0, 2},
}

for _, tc := range testcases {
t.Run(fmt.Sprintf("%d_scale_%d", tc.val, tc.scale), func(t *testing.T) {
expected := exactDecimalString(big.NewInt(tc.val), tc.scale)

meta := query.FieldMetadata{Type: "fixed", Precision: 18, Scale: tc.scale}
if actual := arrowIntToValue(meta, false, tc.val); actual != expected {
t.Errorf("expected %v, got %v", expected, actual)
}
})
}
}
Loading