Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
355b82d
Queries and parameters tests.
El-76 Jul 20, 2025
b222140
OUT parameter tests.
El-76 Jul 27, 2025
d2254b6
Bug fix.
El-76 Jul 27, 2025
f6d9eef
TVP test.
El-76 Jul 31, 2025
50e5c43
Bug fix.
El-76 Jul 21, 2025
88bd7b5
Another TVP test.
El-76 Aug 2, 2025
f180976
Another TVP test.
El-76 Aug 2, 2025
861f0f5
Another TVP test.
El-76 Aug 2, 2025
f981817
Bug fix.
El-76 Aug 2, 2025
00bc87f
Bug fix.
El-76 Aug 2, 2025
5d9662c
Another TVP test.
El-76 Aug 3, 2025
2ff7a0f
Bulk copy test.
El-76 Aug 3, 2025
cfd6a54
Bug fix.
El-76 Aug 3, 2025
5b899a8
Bug fix.
El-76 Aug 3, 2025
d0484de
Another bulk copy test.
El-76 Aug 3, 2025
6a6bd7b
Bug fix.
El-76 Aug 3, 2025
9e653eb
Some MONEY type tests.
El-76 Aug 9, 2025
e6140e3
Support MONEY type.
El-76 Aug 9, 2025
ed539e5
Support Decimal type.
El-76 Aug 9, 2025
486afc6
Support NULL for MONEY type.
El-76 Aug 9, 2025
d829e40
Merge branch 'shopspring-decimal-support' of github.com:El-76/go-mssq…
El-76 Aug 9, 2025
185d6a5
Bug fix and more tests.
El-76 Aug 9, 2025
f16aae0
MONEY encoding test.
El-76 Aug 16, 2025
8a648d4
Bug fix.
El-76 Aug 16, 2025
ccb0e19
Query MONEY encoding test.
El-76 Aug 16, 2025
6f3d11d
Bug fix.
El-76 Aug 16, 2025
8fa6a7e
Query MONEY encoding tests.
El-76 Aug 16, 2025
5907498
DECIMAL encoding tests.
El-76 Aug 16, 2025
31824d4
Bug fix.
El-76 Aug 16, 2025
12227f8
Bulk copy money tests.
El-76 Aug 16, 2025
0432834
Bug fix.
El-76 Aug 16, 2025
cbadbf4
Money bulkcopy support.
El-76 Aug 16, 2025
c5b2963
Bug fix.
El-76 Aug 16, 2025
cd6ea2b
SMALLMONEY bulkcopy support.
El-76 Aug 16, 2025
07b5e6a
Bug fix.
El-76 Aug 16, 2025
be4c83c
Bug fix.
El-76 Aug 16, 2025
acbd807
Refactoring - generalize Money type wrapper.
El-76 Aug 16, 2025
a1249b2
Bug fix.
El-76 Aug 18, 2025
fc14ce1
TVP MONEY test.
El-76 Aug 18, 2025
fb27c6f
TVP MONEY test.
El-76 Aug 18, 2025
b95ed9f
TVP MONEY test.
El-76 Aug 18, 2025
b4909d5
TVP MONEY test.
El-76 Aug 18, 2025
a81a8d5
Remove redundant file.
El-76 Aug 18, 2025
b832815
Simplify money encoding.
El-76 Aug 23, 2025
bcc0a71
Money test.
El-76 Aug 23, 2025
ab7caa3
More money tests.
El-76 Aug 24, 2025
ac07725
Merge branch 'main' into shopspring-decimal-support
El-76 Aug 24, 2025
7cbd01e
Remove redundant file.
El-76 Aug 24, 2025
7e61f6b
Update README.
El-76 Aug 24, 2025
afc1a1a
Money / bulk tests - error cases.
El-76 Aug 24, 2025
09a3511
Merge branch 'main' into shopspring-decimal-support
El-76 Aug 24, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ are supported:
* "github.com/golang-sql/civil".DateTime -> datetime2
* "github.com/golang-sql/civil".Time -> time
* mssql.TVP -> Table Value Parameter (TDS version dependent)
* "github.com/shopspring/decimal".Decimal -> decimal
* mssql.Money -> money

Using an `int` parameter will send a 4 byte value (int) from a 32bit app and an 8 byte value (bigint) from a 64bit app.
To make sure your integer parameter matches the size of the SQL parameter, use the appropriate sized type like `int32` or `int8`.
Expand Down
37 changes: 36 additions & 1 deletion bulkcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/microsoft/go-mssqldb/internal/decimal"
"github.com/microsoft/go-mssqldb/msdsn"
shopspring "github.com/shopspring/decimal"
)

type Bulk struct {
Expand Down Expand Up @@ -346,6 +347,10 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
loc := getTimezone(b.cn)

switch valuer := val.(type) {
case Money[shopspring.Decimal]:
return b.makeParam(valuer.Decimal, col)
case Money[shopspring.NullDecimal]:
return b.makeParam(valuer.Decimal, col)
case driver.Valuer:
var e error
val, e = driver.DefaultParameterConverter.ConvertValue(valuer)
Expand Down Expand Up @@ -561,7 +566,37 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error)
err = fmt.Errorf("mssql: invalid type for time column: %T %s", val, val)
return
}
// case typeMoney, typeMoney4, typeMoneyN:
case typeMoney, typeMoney4, typeMoneyN:
switch v := val.(type) {
case string:
money, err := decimal.StringToDecimalScale(v, 4)
if err != nil {
return res, err
}

buf := make([]byte, col.ti.Size)

integer0 := money.GetInteger(0)
if col.ti.Size == 4 {
if money.IsPositive() {
binary.LittleEndian.PutUint32(buf, integer0)
} else {
binary.LittleEndian.PutUint32(buf, ^integer0+1)
}
} else {
integer := (uint64(money.GetInteger(1)) << 32) | uint64(integer0)
if !money.IsPositive() {
integer = ^integer + 1
}

binary.LittleEndian.PutUint32(buf, uint32(integer>>32))
binary.LittleEndian.PutUint32(buf[4:], uint32(integer))
}

res.buffer = buf
default:
return res, fmt.Errorf("unknown value for money: %T %#v", v, v)
}
case typeDecimal, typeDecimalN, typeNumeric, typeNumericN:
prec := col.ti.Prec
scale := col.ti.Scale
Expand Down
29 changes: 27 additions & 2 deletions bulkcopy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"
"time"

"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
)

Expand All @@ -29,6 +30,8 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) {
"test_nullint16",
"test_nulltime",
"test_nulluniqueidentifier",
"test_nulldecimal",
"test_nullmoney",
}
values := []interface{}{
sql.NullFloat64{Valid: false},
Expand All @@ -40,6 +43,8 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) {
sql.NullInt16{Valid: false},
sql.NullTime{Valid: false},
NullUniqueIdentifier{Valid: false},
decimal.NullDecimal{Valid: false},
Money[decimal.NullDecimal]{decimal.NullDecimal{Valid: false}},
}

pool, logger := open(t)
Expand Down Expand Up @@ -176,9 +181,11 @@ func testBulkcopy(t *testing.T, guidConversion bool) {
{"test_nullint32", sql.NullInt32{2147483647, true}, 2147483647},
{"test_nullint16", sql.NullInt16{32767, true}, 32767},
{"test_nulltime", sql.NullTime{time.Date(2010, 11, 12, 13, 14, 15, 120000000, time.UTC), true}, time.Date(2010, 11, 12, 13, 14, 15, 120000000, time.UTC)},
{"test_nulldecimal", decimal.NewNullDecimal(decimal.New(1232355, -4)), decimal.New(1232355, -4)},
{"test_nullmoney", Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-21232311232355, -4))}, decimal.New(-21232311232355, -4)},
{"test_datetimen_midnight", time.Date(2025, 1, 1, 23, 59, 59, 998_350_000, time.UTC), time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)},
// {"test_smallmoney", 1234.56, nil},
// {"test_money", 1234.56, nil},
{"test_smallmoney", Money[decimal.Decimal]{decimal.New(-32856, -4)}, decimal.New(-32856, -4)},
{"test_money", Money[decimal.Decimal]{decimal.New(-21232311232355, -4)}, decimal.New(-21232311232355, -4)},
{"test_decimal_18_0", 1234.0001, "1234"},
{"test_decimal_9_2", -1234.560001, "-1234.56"},
{"test_decimal_20_0", 1234, "1234"},
Expand Down Expand Up @@ -334,6 +341,20 @@ func compareValue(a interface{}, expected interface{}) bool {
return expected.Equal(got) && ez == az
}
return false
case decimal.Decimal:
actual, err := decimal.NewFromString(a.(string))
if err != nil {
return false
}

return expected.Equal(actual)
case Money[decimal.Decimal]:
actual, err := decimal.NewFromString(a.(string))
if err != nil {
return false
}

return expected.Decimal.Equal(actual)
default:
return reflect.DeepEqual(expected, a)
}
Expand All @@ -351,6 +372,8 @@ func setupNullableTypeTable(ctx context.Context, t *testing.T, conn *sql.Conn, t
[test_nullint16] [smallint] NULL,
[test_nulltime] [datetime] NULL,
[test_nulluniqueidentifier] [uniqueidentifier] NULL,
[test_nulldecimal] [decimal](18, 4) NULL,
[test_nullmoney] [money] NULL,
CONSTRAINT [PK_` + tableName + `_id] PRIMARY KEY CLUSTERED
(
[id] ASC
Expand Down Expand Up @@ -438,6 +461,8 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str
[test_nullint32] [int] NULL,
[test_nullint16] [smallint] NULL,
[test_nulltime] [datetime] NULL,
[test_nulldecimal] [decimal](18, 4) NULL,
[test_nullmoney] [money] NULL,
[test_datetimen_midnight] [datetime] NULL,
CONSTRAINT [PK_` + tableName + `_id] PRIMARY KEY CLUSTERED
(
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/net v0.40.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhi
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
Copy link
Collaborator

@shueybubbles shueybubbles Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's nice that this module doesn't bring it a bunch of other stuff.
It'd be nice if the driver supported a pluggable type system like we support different network protocols for SQL connections so apps could include the module of their choice to get the implementation. #Resolved

Copy link
Author

@El-76 El-76 Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is already some work done in sql library itself, but its still considered experimental, see: golang/go#30870 .

Source code:

% fgrep -A 3 'type decimal interface {' src/database/sql/convert.go type decimal interface {	decimalDecompose	decimalCompose } 
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
5 changes: 5 additions & 0 deletions internal/decimal/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func init() {

const autoScale = 100

// GetInteger gets the ind'th element of the integer array
func (d *Decimal) GetInteger(ind uint8) uint32 {
return d.integer[ind]
}

// SetInteger sets the ind'th element in the integer array
func (d *Decimal) SetInteger(integer uint32, ind uint8) {
d.integer[ind] = integer
Expand Down
24 changes: 24 additions & 0 deletions money.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package mssql

import (
"database/sql"
"database/sql/driver"

"github.com/shopspring/decimal"
)

type Money[D decimal.Decimal|decimal.NullDecimal] struct {
Copy link
Collaborator

@shueybubbles shueybubbles Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ecimal.NullDecimal]

i think this is the first use of generics in the driver #Resolved

Decimal D
}

func (m Money[D]) Value() (driver.Value, error) {
valuer, _ := any(m.Decimal).(driver.Valuer)

return valuer.Value()
}

func (m *Money[D]) Scan(v any) error {
scanner, _ := any(&m.Decimal).(sql.Scanner)

return scanner.Scan(v);
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the unnecessary semicolon at the end of the return statement. Go doesn't require semicolons at the end of statements.

Copilot uses AI. Check for mistakes.
}
Loading