From 907a1e612ae8cf62ed4ccbcc25bcc573049389b9 Mon Sep 17 00:00:00 2001 From: El-76 Date: Sat, 6 Dec 2025 20:10:44 +0300 Subject: [PATCH 01/32] Types plus simple test. --- civil_time_null.go | 20 ++++++++++++++++++++ money.go | 4 ++-- queries_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 civil_time_null.go diff --git a/civil_time_null.go b/civil_time_null.go new file mode 100644 index 00000000..f3f20f11 --- /dev/null +++ b/civil_time_null.go @@ -0,0 +1,20 @@ +package mssql + +import ( + "github.com/golang-sql/civil" +) + +type NullDateTime struct { + DateTime civil.DateTime + Valid bool +} + +type NullDate struct { + Date civil.Date + Valid bool +} + +type NullTime struct { + Time civil.Time + Valid bool +} diff --git a/money.go b/money.go index aa2bd176..2fb890c9 100644 --- a/money.go +++ b/money.go @@ -7,7 +7,7 @@ import ( "github.com/shopspring/decimal" ) -type Money[D decimal.Decimal|decimal.NullDecimal] struct { +type Money[D decimal.Decimal | decimal.NullDecimal] struct { Decimal D } @@ -20,5 +20,5 @@ func (m Money[D]) Value() (driver.Value, error) { func (m *Money[D]) Scan(v any) error { scanner, _ := any(&m.Decimal).(sql.Scanner) - return scanner.Scan(v); + return scanner.Scan(v) } diff --git a/queries_test.go b/queries_test.go index 2a26d57a..aa85ffaa 100644 --- a/queries_test.go +++ b/queries_test.go @@ -17,6 +17,7 @@ import ( "testing" "time" + "github.com/golang-sql/civil" "github.com/microsoft/go-mssqldb/msdsn" "github.com/shopspring/decimal" ) @@ -254,6 +255,47 @@ func testSelect(t *testing.T, guidConversion bool) { t.Errorf("got back a NullDecimal with value: %t, %s", out.Valid, out.Decimal.String()) } }) + t.Run("scan into civil.Date", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02' AS DATE)") + var out civil.Date + err := row.Scan(&out) + if err != nil { + t.Error("Scan to civil.Date failed", err.Error()) + return + } + + d := civil.Date{Year: 2006, Month: 1, Day: 2} + if out != d { + t.Errorf("got back a civil.Date with value: %s", out.String()) + } + }) + t.Run("scan into NullDate", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02' AS DATE))") + var out NullDate + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDate failed", err.Error()) + return + } + + nd := NullDate{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Valid: true} + if out.Date != nd.Date || !out.Valid { + t.Errorf("got back a NullDate with value: %t, %s", out.Valid, out.Date.String()) + } + }) + t.Run("scan into NullDate from NULL", func(t *testing.T) { + row := conn.QueryRow("SELECT NULL") + var out NullDate + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDate failed", err.Error()) + return + } + + if out.Valid { + t.Errorf("got back a NullDate with value: %t, %s", out.Valid, out.Date.String()) + } + }) } func TestSelectWithGuidConversion(t *testing.T) { From bf4204af82bd0e39ffc940e653749015959b8791 Mon Sep 17 00:00:00 2001 From: Anton Ostroumov Date: Sun, 1 Feb 2026 20:19:33 +0300 Subject: [PATCH 02/32] Add some time types. --- bulkcopy.go | 33 ++++- civil_time_null.go | 227 ++++++++++++++++++++++++++++++- datetime_midnight_test.go | 9 +- encode_datetime_overflow_test.go | 27 +++- mssql.go | 9 +- mssql_go19.go | 129 +++++++++++++++--- queries_test.go | 6 +- types.go | 32 +++-- 8 files changed, 420 insertions(+), 52 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index 6ddc0ddc..c9a1067a 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -482,14 +482,30 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) case typeDateTime2N: switch val := val.(type) { case time.Time: - res.buffer = encodeDateTime2(val, int(col.ti.Scale)) + res.buffer = encodeDateTime2( + val.Day(), + val.YearDay(), + val.Hour(), + val.Minute(), + val.Second(), + val.Nanosecond(), + int(col.ti.Scale), + ) res.ti.Size = len(res.buffer) case string: var t time.Time if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } - res.buffer = encodeDateTime2(t, int(col.ti.Scale)) + res.buffer = encodeDateTime2( + t.Day(), + t.YearDay(), + t.Hour(), + t.Minute(), + t.Second(), + t.Nanosecond(), + int(col.ti.Scale), + ) res.ti.Size = len(res.buffer) default: err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %s", val, val) @@ -514,14 +530,14 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) case typeDateN: switch val := val.(type) { case time.Time: - res.buffer = encodeDate(val) + res.buffer = encodeDate(val.Year(), val.YearDay()) res.ti.Size = len(res.buffer) case string: var t time.Time if t, err = time.ParseInLocation(sqlDateFormat, val, loc); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } - res.buffer = encodeDate(t) + res.buffer = encodeDate(t.Year(), t.YearDay()) res.ti.Size = len(res.buffer) default: err = fmt.Errorf("mssql: invalid type for date column: %T %s", val, val) @@ -545,7 +561,14 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.buffer = encodeDateTim4(t, loc) res.ti.Size = len(res.buffer) } else if col.ti.Size == 8 { - res.buffer = encodeDateTime(t) + res.buffer = encodeDateTime( + t.Day(), + t.YearDay(), + t.Hour(), + t.Minute(), + t.Second(), + t.Nanosecond(), + ) res.ti.Size = len(res.buffer) } else { err = fmt.Errorf("mssql: invalid size of column %d", col.ti.Size) diff --git a/civil_time_null.go b/civil_time_null.go index f3f20f11..c66fd79a 100644 --- a/civil_time_null.go +++ b/civil_time_null.go @@ -1,20 +1,237 @@ package mssql import ( + "database/sql" + "github.com/golang-sql/civil" ) -type NullDateTime struct { - DateTime civil.DateTime - Valid bool +type Date civil.Date + +// Scan implements the [Scanner] interface. +func (d *Date) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + d.Year = t.Time.Year() + d.Month = t.Time.Month() + d.Day = t.Time.Day() + + return nil +} + +type DateTime civil.DateTime + +// Scan implements the [Scanner] interface. +func (dt *DateTime) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + dt.Date.Year = t.Time.Year() + dt.Date.Month = t.Time.Month() + dt.Date.Day = t.Time.Day() + + dt.Time.Hour = t.Time.Hour() + dt.Time.Minute = t.Time.Minute() + dt.Time.Second = t.Time.Second() + dt.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + +type DateTime2 civil.DateTime + +// Scan implements the [Scanner] interface. +func (dt *DateTime2) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + dt.Date.Year = t.Time.Year() + dt.Date.Month = t.Time.Month() + dt.Date.Day = t.Time.Day() + + dt.Time.Hour = t.Time.Hour() + dt.Time.Minute = t.Time.Minute() + dt.Time.Second = t.Time.Second() + dt.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + +type Time civil.Time + +// Scan implements the [Scanner] interface. +func (tt *Time) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + tt.Hour = t.Time.Hour() + tt.Minute = t.Time.Minute() + tt.Second = t.Time.Second() + tt.Nanosecond = t.Time.Nanosecond() + + return nil } type NullDate struct { - Date civil.Date + Date Date Valid bool } +// Scan implements the [Scanner] interface. +func (n *NullDate) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + if !t.Valid { + n.Valid = false + + return nil + } + + n.Valid = true + + n.Date.Year = t.Time.Year() + n.Date.Month = t.Time.Month() + n.Date.Day = t.Time.Day() + + return nil +} + +type NullDateTime struct { + DateTime DateTime + Valid bool +} + +// Scan implements the [Scanner] interface. +func (n *NullDateTime) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + if !t.Valid { + n.Valid = false + + return nil + } + + n.Valid = true + + n.DateTime.Date.Year = t.Time.Year() + n.DateTime.Date.Month = t.Time.Month() + n.DateTime.Date.Day = t.Time.Day() + + n.DateTime.Time.Hour = t.Time.Hour() + n.DateTime.Time.Minute = t.Time.Minute() + n.DateTime.Time.Second = t.Time.Second() + n.DateTime.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + +type NullDateTime2 struct { + DateTime DateTime + Valid bool +} + +// Scan implements the [Scanner] interface. +func (n *NullDateTime2) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + if !t.Valid { + n.Valid = false + + return nil + } + + n.Valid = true + + n.DateTime.Date.Year = t.Time.Year() + n.DateTime.Date.Month = t.Time.Month() + n.DateTime.Date.Day = t.Time.Day() + + n.DateTime.Time.Hour = t.Time.Hour() + n.DateTime.Time.Minute = t.Time.Minute() + n.DateTime.Time.Second = t.Time.Second() + n.DateTime.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + type NullTime struct { - Time civil.Time + Time Time Valid bool } + +// Scan implements the [Scanner] interface. +func (n *NullTime) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + if !t.Valid { + n.Valid = false + + return nil + } + + n.Valid = true + + n.Time.Hour = t.Time.Hour() + n.Time.Minute = t.Time.Minute() + n.Time.Second = t.Time.Second() + n.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + +type NullDateTimeOffset struct { + DateTimeOffset DateTimeOffset + Valid bool +} + +// Scan implements the [Scanner] interface. +func (n *NullDateTimeOffset) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if (err != nil) { + return err + } + + n.Valid = t.Valid + n.DateTimeOffset = DateTimeOffset(t.Time) + + return nil +} diff --git a/datetime_midnight_test.go b/datetime_midnight_test.go index e3c7b331..000a6ad9 100644 --- a/datetime_midnight_test.go +++ b/datetime_midnight_test.go @@ -25,7 +25,14 @@ func TestDatetimeNearMidnightBoundaries(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Test encoding/decoding - encoded := encodeDateTime(tc.time) + encoded := encodeDateTime( + tc.time.Day(), + tc.time.YearDay(), + tc.time.Hour(), + tc.time.Minute(), + tc.time.Second(), + tc.time.Nanosecond(), + ) decoded := decodeDateTime(encoded, time.UTC) t.Logf("Original: %s", tc.time.Format(time.RFC3339Nano)) diff --git a/encode_datetime_overflow_test.go b/encode_datetime_overflow_test.go index abffaae2..125558e4 100644 --- a/encode_datetime_overflow_test.go +++ b/encode_datetime_overflow_test.go @@ -40,7 +40,14 @@ func TestEncodeDateTimeOverflow(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Encode the time - encoded := encodeDateTime(tc.input) + encoded := encodeDateTime( + tc.input.Day(), + tc.input.YearDay(), + tc.input.Hour(), + tc.input.Minute(), + tc.input.Second(), + tc.input.Nanosecond(), + ) // Verify round-trip decoding gives the expected result decoded := decodeDateTime(encoded, time.UTC) @@ -59,7 +66,14 @@ func TestEncodeDateTimeMaxDateOverflow(t *testing.T) { maxTime := time.Date(9999, 12, 31, 23, 59, 59, 998_350_000, time.UTC) // Encode the time - encoded := encodeDateTime(maxTime) + encoded := encodeDateTime( + maxTime.Day(), + maxTime.YearDay(), + maxTime.Hour(), + maxTime.Minute(), + maxTime.Second(), + maxTime.Nanosecond(), + ) // Decode it back decoded := decodeDateTime(encoded, time.UTC) @@ -78,7 +92,14 @@ func TestEncodeDateTimeNoOverflow(t *testing.T) { normalTime := time.Date(2025, 1, 1, 23, 59, 59, 997_000_000, time.UTC) // Encode the time - encoded := encodeDateTime(normalTime) + encoded := encodeDateTime( + normalTime.Day(), + normalTime.YearDay(), + normalTime.Hour(), + normalTime.Minute(), + normalTime.Second(), + normalTime.Nanosecond(), + ) // Decode the days and time portions days := int32(binary.LittleEndian.Uint32(encoded[0:4])) diff --git a/mssql.go b/mssql.go index a4b70bdc..ee417002 100644 --- a/mssql.go +++ b/mssql.go @@ -1151,7 +1151,14 @@ func (s *Stmt) makeParam(val driver.Value) (res param, err error) { res.ti.Size = len(res.buffer) } else { res.ti.TypeId = typeDateTimeN - res.buffer = encodeDateTime(val) + res.buffer = encodeDateTime( + val.Year(), + val.YearDay(), + val.Hour(), + val.Minute(), + val.Second(), + val.Nanosecond(), + ) res.ti.Size = len(res.buffer) } case sql.NullTime: // only null values reach here diff --git a/mssql_go19.go b/mssql_go19.go index 3fae4b7a..10d1ac76 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -159,9 +159,66 @@ func makeMoneyParam(val decimal.Decimal) (res param) { return } -func (s *Stmt) makeParamExtra(val driver.Value) (res param, err error) { - loc := getTimezone(s.c) +func makeDate(val civil.Date, res *param) { + res.ti.TypeId = typeDateN + res.buffer = encodeDate( + val.Year, + val.DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}), + ) + res.ti.Size = len(res.buffer) +} + +func makeDateTime(val civil.DateTime, res *param) { + res.ti.TypeId = typeDateTime2N + res.ti.Scale = 7 + res.buffer = encodeDateTime2( + val.Date.Year, + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}), + val.Time.Hour, + val.Time.Minute, + val.Time.Second, + val.Time.Nanosecond, + int(res.ti.Scale), + ) + res.ti.Size = len(res.buffer) +} + +func makeDateTime2(val civil.DateTime, res *param) { + res.ti.TypeId = typeDateTime2N + res.ti.Scale = 7 + res.buffer = encodeDateTime2( + val.Date.Year, + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}), + val.Time.Hour, + val.Time.Minute, + val.Time.Second, + val.Time.Nanosecond, + int(res.ti.Scale), + ) + res.ti.Size = len(res.buffer) +} + +func makeTime(val civil.Time, res *param) { + res.ti.TypeId = typeTimeN + res.ti.Scale = 7 + res.buffer = encodeTime( + val.Hour, + val.Minute, + val.Second, + val.Nanosecond, + int(res.ti.Scale), + ) + res.ti.Size = len(res.buffer) +} +func makeDateTimeOffset(val time.Time, res *param) { + res.ti.TypeId = typeDateTimeOffsetN + res.ti.Scale = 7 + res.buffer = encodeDateTimeOffset(val, int(res.ti.Scale)) + res.ti.Size = len(res.buffer) +} + +func (s *Stmt) makeParamExtra(val driver.Value) (res param, err error) { switch val := val.(type) { case VarChar: res.ti.TypeId = typeBigVarChar @@ -180,29 +237,57 @@ func (s *Stmt) makeParamExtra(val driver.Value) (res param, err error) { res.buffer = str2ucs2(string(val)) res.ti.Size = len(res.buffer) case DateTime1: - t := time.Time(val) - res.ti.TypeId = typeDateTimeN - res.buffer = encodeDateTime(t) - res.ti.Size = len(res.buffer) - case DateTimeOffset: - res.ti.TypeId = typeDateTimeOffsetN - res.ti.Scale = 7 - res.buffer = encodeDateTimeOffset(time.Time(val), int(res.ti.Scale)) - res.ti.Size = len(res.buffer) + makeDateTime(civil.DateTimeOf(time.Time(val)), &res) + case civil.Date: - res.ti.TypeId = typeDateN - res.buffer = encodeDate(val.In(loc)) - res.ti.Size = len(res.buffer) + makeDate(val, &res) case civil.DateTime: - res.ti.TypeId = typeDateTime2N - res.ti.Scale = 7 - res.buffer = encodeDateTime2(val.In(loc), int(res.ti.Scale)) - res.ti.Size = len(res.buffer) + makeDateTime2(val, &res) case civil.Time: - res.ti.TypeId = typeTimeN - res.ti.Scale = 7 - res.buffer = encodeTime(val.Hour, val.Minute, val.Second, val.Nanosecond, int(res.ti.Scale)) - res.ti.Size = len(res.buffer) + makeTime(val, &res) + + case Date: + makeDate(civil.Date(val), &res) + case DateTime: + makeDateTime(civil.DateTime(val), &res) + case DateTime2: + makeDateTime2(civil.DateTime(val), &res) + case Time: + makeTime(civil.Time(val), &res) + case DateTimeOffset: + makeDateTimeOffset(time.Time(val), &res) + + case NullDate: + makeDate(civil.Date(val.Date), &res) + + if !val.Valid { + res.buffer = []byte{} + } + case NullDateTime: + makeDateTime(civil.DateTime(val.DateTime), &res) + + if !val.Valid { + res.buffer = []byte{} + } + case NullDateTime2: + makeDateTime2(civil.DateTime(val.DateTime), &res) + + if !val.Valid { + res.buffer = []byte{} + } + case NullTime: + makeTime(civil.Time(val.Time), &res) + + if !val.Valid { + res.buffer = []byte{} + } + case NullDateTimeOffset: + makeDateTimeOffset(time.Time(val.DateTimeOffset), &res) + + if !val.Valid { + res.buffer = []byte{} + } + case sql.Out: switch dest := val.Dest.(type) { case Money[decimal.Decimal]: diff --git a/queries_test.go b/queries_test.go index aa85ffaa..f918500c 100644 --- a/queries_test.go +++ b/queries_test.go @@ -278,9 +278,9 @@ func testSelect(t *testing.T, guidConversion bool) { return } - nd := NullDate{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Valid: true} + nd := NullDate{Date: Date(civil.Date{Year: 2006, Month: 1, Day: 2}), Valid: true} if out.Date != nd.Date || !out.Valid { - t.Errorf("got back a NullDate with value: %t, %s", out.Valid, out.Date.String()) + t.Errorf("got back a NullDate with value: %t, %s", out.Valid, civil.Date(out.Date).String()) } }) t.Run("scan into NullDate from NULL", func(t *testing.T) { @@ -293,7 +293,7 @@ func testSelect(t *testing.T, guidConversion bool) { } if out.Valid { - t.Errorf("got back a NullDate with value: %t, %s", out.Valid, out.Date.String()) + t.Errorf("got back a NullDate with value: %t, %s", out.Valid, civil.Date(out.Date).String()) } }) } diff --git a/types.go b/types.go index 655eb7c8..a443fe09 100644 --- a/types.go +++ b/types.go @@ -287,12 +287,12 @@ func encodeDateTim4(val time.Time, loc *time.Location) (buf []byte) { // encodes datetime value // type identifier is typeDateTimeN -func encodeDateTime(t time.Time) (res []byte) { +func encodeDateTime(year, yearDay, hour, minute, second, nanosecond int) (res []byte) { // base date in days since Jan 1st 1900 basedays := gregorianDays(1900, 1) // days since Jan 1st 1900 (same TZ as t) - days := gregorianDays(t.Year(), t.YearDay()) - basedays - tm := 300*(t.Second()+t.Minute()*60+t.Hour()*60*60) + nanosToThreeHundredthsOfASecond(t.Nanosecond()) + days := gregorianDays(year, yearDay) - basedays + tm := 300*(second+minute*60+hour*60*60) + nanosToThreeHundredthsOfASecond(nanosecond) // Handle day overflow when time calculation exceeds one day // One day = 86400 seconds = 86400 * 300 three-hundredths = 25,920,000 @@ -932,8 +932,8 @@ func decodeDate(buf []byte, loc *time.Location) time.Time { return time.Date(1, 1, 1+decodeDateInt(buf), 0, 0, 0, 0, loc) } -func encodeDate(val time.Time) (buf []byte) { - days, _, _ := dateTime2(val) +func encodeDate(year, yearDay int) (buf []byte) { + days, _, _ := dateTime2(year, yearDay, 0, 0, 0, 0) buf = make([]byte, 3) buf[0] = byte(days) buf[1] = byte(days >> 8) @@ -998,8 +998,8 @@ func decodeDateTime2(scale uint8, buf []byte, loc *time.Location) time.Time { return time.Date(1, 1, 1+days, 0, 0, sec, ns, loc) } -func encodeDateTime2(val time.Time, scale int) (buf []byte) { - days, seconds, ns := dateTime2(val) +func encodeDateTime2(year, yearDay, hour, minute, second, nanosecond, scale int) (buf []byte) { + days, seconds, ns := dateTime2(year, yearDay, hour, minute, second, nanosecond) timesize := calcTimeSize(scale) buf = make([]byte, 3+timesize) encodeTimeInt(seconds, ns, scale, buf) @@ -1023,7 +1023,15 @@ func decodeDateTimeOffset(scale uint8, buf []byte) time.Time { func encodeDateTimeOffset(val time.Time, scale int) (buf []byte) { timesize := calcTimeSize(scale) buf = make([]byte, timesize+2+3) - days, seconds, ns := dateTime2(val.In(time.UTC)) + t := val.In(time.UTC) + days, seconds, ns := dateTime2( + t.Year(), + t.YearDay(), + t.Hour(), + t.Minute(), + t.Second(), + t.Nanosecond(), + ) encodeTimeInt(seconds, ns, scale, buf) buf[timesize] = byte(days) buf[timesize+1] = byte(days >> 8) @@ -1041,11 +1049,11 @@ func gregorianDays(year, yearday int) int { return year0*365 + year0/4 - year0/100 + year0/400 + yearday - 1 } -func dateTime2(t time.Time) (days int, seconds int, ns int) { +func dateTime2(year, yearDay, hour, minute, second, nanosecond int) (days int, seconds int, ns int) { // days since Jan 1 1 (in same TZ as t) - days = gregorianDays(t.Year(), t.YearDay()) - seconds = t.Second() + t.Minute()*60 + t.Hour()*60*60 - ns = t.Nanosecond() + days = gregorianDays(year, yearDay) + seconds = second + minute*60 + hour*60*60 + ns = nanosecond if days < 0 { days = 0 seconds = 0 From a49747f5c408818fcac86f4d0e334b2c329641df Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 11 Feb 2026 01:14:09 +0300 Subject: [PATCH 03/32] More types. --- civil_time_null.go | 79 +++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/civil_time_null.go b/civil_time_null.go index c66fd79a..f3451082 100644 --- a/civil_time_null.go +++ b/civil_time_null.go @@ -13,7 +13,7 @@ func (d *Date) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } @@ -31,14 +31,14 @@ func (dt *DateTime) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } dt.Date.Year = t.Time.Year() dt.Date.Month = t.Time.Month() dt.Date.Day = t.Time.Day() - + dt.Time.Hour = t.Time.Hour() dt.Time.Minute = t.Time.Minute() dt.Time.Second = t.Time.Second() @@ -50,22 +50,22 @@ func (dt *DateTime) Scan(value any) error { type DateTime2 civil.DateTime // Scan implements the [Scanner] interface. -func (dt *DateTime2) Scan(value any) error { +func (dt2 *DateTime2) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } - dt.Date.Year = t.Time.Year() - dt.Date.Month = t.Time.Month() - dt.Date.Day = t.Time.Day() - - dt.Time.Hour = t.Time.Hour() - dt.Time.Minute = t.Time.Minute() - dt.Time.Second = t.Time.Second() - dt.Time.Nanosecond = t.Time.Nanosecond() + dt2.Date.Year = t.Time.Year() + dt2.Date.Month = t.Time.Month() + dt2.Date.Day = t.Time.Day() + + dt2.Time.Hour = t.Time.Hour() + dt2.Time.Minute = t.Time.Minute() + dt2.Time.Second = t.Time.Second() + dt2.Time.Nanosecond = t.Time.Nanosecond() return nil } @@ -77,10 +77,10 @@ func (tt *Time) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } - + tt.Hour = t.Time.Hour() tt.Minute = t.Time.Minute() tt.Second = t.Time.Second() @@ -99,7 +99,7 @@ func (n *NullDate) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } @@ -128,7 +128,7 @@ func (n *NullDateTime) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } @@ -143,7 +143,7 @@ func (n *NullDateTime) Scan(value any) error { n.DateTime.Date.Year = t.Time.Year() n.DateTime.Date.Month = t.Time.Month() n.DateTime.Date.Day = t.Time.Day() - + n.DateTime.Time.Hour = t.Time.Hour() n.DateTime.Time.Minute = t.Time.Minute() n.DateTime.Time.Second = t.Time.Second() @@ -153,7 +153,7 @@ func (n *NullDateTime) Scan(value any) error { } type NullDateTime2 struct { - DateTime DateTime + DateTime DateTime2 Valid bool } @@ -162,7 +162,36 @@ func (n *NullDateTime2) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { + return err + } + + if !t.Valid { + n.Valid = false + + return nil + } + + n.Valid = true + + n.DateTime.Date.Year = t.Time.Year() + n.DateTime.Date.Month = t.Time.Month() + n.DateTime.Date.Day = t.Time.Day() + + n.DateTime.Time.Hour = t.Time.Hour() + n.DateTime.Time.Minute = t.Time.Minute() + n.DateTime.Time.Second = t.Time.Second() + n.DateTime.Time.Nanosecond = t.Time.Nanosecond() + + return nil +} + +// Scan implements the [Scanner] interface. +func (n *NullSmallDateTime) Scan(value any) error { + t := &sql.NullTime{} + + err := t.Scan(value) + if err != nil { return err } @@ -177,7 +206,7 @@ func (n *NullDateTime2) Scan(value any) error { n.DateTime.Date.Year = t.Time.Year() n.DateTime.Date.Month = t.Time.Month() n.DateTime.Date.Day = t.Time.Day() - + n.DateTime.Time.Hour = t.Time.Hour() n.DateTime.Time.Minute = t.Time.Minute() n.DateTime.Time.Second = t.Time.Second() @@ -196,7 +225,7 @@ func (n *NullTime) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } @@ -207,7 +236,7 @@ func (n *NullTime) Scan(value any) error { } n.Valid = true - + n.Time.Hour = t.Time.Hour() n.Time.Minute = t.Time.Minute() n.Time.Second = t.Time.Second() @@ -218,7 +247,7 @@ func (n *NullTime) Scan(value any) error { type NullDateTimeOffset struct { DateTimeOffset DateTimeOffset - Valid bool + Valid bool } // Scan implements the [Scanner] interface. @@ -226,7 +255,7 @@ func (n *NullDateTimeOffset) Scan(value any) error { t := &sql.NullTime{} err := t.Scan(value) - if (err != nil) { + if err != nil { return err } From 3ffbd0b384d337964ff9a58ec896a4dcd38a0044 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 11 Feb 2026 03:14:34 +0300 Subject: [PATCH 04/32] More types. --- civil_time_null.go | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/civil_time_null.go b/civil_time_null.go index f3451082..a940d6f8 100644 --- a/civil_time_null.go +++ b/civil_time_null.go @@ -186,35 +186,6 @@ func (n *NullDateTime2) Scan(value any) error { return nil } -// Scan implements the [Scanner] interface. -func (n *NullSmallDateTime) Scan(value any) error { - t := &sql.NullTime{} - - err := t.Scan(value) - if err != nil { - return err - } - - if !t.Valid { - n.Valid = false - - return nil - } - - n.Valid = true - - n.DateTime.Date.Year = t.Time.Year() - n.DateTime.Date.Month = t.Time.Month() - n.DateTime.Date.Day = t.Time.Day() - - n.DateTime.Time.Hour = t.Time.Hour() - n.DateTime.Time.Minute = t.Time.Minute() - n.DateTime.Time.Second = t.Time.Second() - n.DateTime.Time.Nanosecond = t.Time.Nanosecond() - - return nil -} - type NullTime struct { Time Time Valid bool From 3fefdccf08952c884824cf257d7ded57c7159980 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 18 Feb 2026 22:46:37 +0300 Subject: [PATCH 05/32] Add Date test. --- mssql_go19.go | 6 ++-- queries_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/mssql_go19.go b/mssql_go19.go index 10d1ac76..bf6d0995 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -163,7 +163,7 @@ func makeDate(val civil.Date, res *param) { res.ti.TypeId = typeDateN res.buffer = encodeDate( val.Year, - val.DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}), + val.DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}) + 1, ) res.ti.Size = len(res.buffer) } @@ -173,7 +173,7 @@ func makeDateTime(val civil.DateTime, res *param) { res.ti.Scale = 7 res.buffer = encodeDateTime2( val.Date.Year, - val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}), + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1, val.Time.Hour, val.Time.Minute, val.Time.Second, @@ -188,7 +188,7 @@ func makeDateTime2(val civil.DateTime, res *param) { res.ti.Scale = 7 res.buffer = encodeDateTime2( val.Date.Year, - val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}), + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1, val.Time.Hour, val.Time.Minute, val.Time.Second, diff --git a/queries_test.go b/queries_test.go index f918500c..115a5f27 100644 --- a/queries_test.go +++ b/queries_test.go @@ -255,22 +255,22 @@ func testSelect(t *testing.T, guidConversion bool) { t.Errorf("got back a NullDecimal with value: %t, %s", out.Valid, out.Decimal.String()) } }) - t.Run("scan into civil.Date", func(t *testing.T) { + t.Run("scan into Date", func(t *testing.T) { row := conn.QueryRow("SELECT cast('2006-01-02' AS DATE)") - var out civil.Date + var out Date err := row.Scan(&out) if err != nil { - t.Error("Scan to civil.Date failed", err.Error()) + t.Error("Scan to Date failed", err.Error()) return } - d := civil.Date{Year: 2006, Month: 1, Day: 2} + d := Date(civil.Date{Year: 2006, Month: 1, Day: 2}) if out != d { - t.Errorf("got back a civil.Date with value: %s", out.String()) + t.Errorf("got back a Date with value: %s", civil.Date(out).String()) } }) t.Run("scan into NullDate", func(t *testing.T) { - row := conn.QueryRow("SELECT cast('2006-01-02' AS DATE))") + row := conn.QueryRow("SELECT cast('2006-01-02' AS DATE)") var out NullDate err := row.Scan(&out) if err != nil { @@ -296,6 +296,89 @@ func testSelect(t *testing.T, guidConversion bool) { t.Errorf("got back a NullDate with value: %t, %s", out.Valid, civil.Date(out.Date).String()) } }) + t.Run("scan into DateTime", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02 23:12:44' AS DATETIME)") + var out DateTime + err := row.Scan(&out) + if err != nil { + t.Error("Scan to DateTime failed", err.Error()) + return + } + + d := DateTime(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}) + if out != d { + t.Errorf("got back a DateTime with value: %s", civil.DateTime(out).String()) + } + }) + t.Run("scan into NullDateTime", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02 23:12:44' AS DATETIME)") + var out NullDateTime + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTime failed", err.Error()) + return + } + + nd := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}), Valid: true} + if out.DateTime != nd.DateTime || !out.Valid { + t.Errorf("got back a NullDateTime with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + } + }) + t.Run("scan into NullDateTime from NULL", func(t *testing.T) { + row := conn.QueryRow("SELECT NULL") + var out NullDateTime + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTime failed", err.Error()) + return + } + + if out.Valid { + t.Errorf("got back a NullDateTime with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + } + }) + t.Run("scan into DateTime2", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02 23:12:44' AS DATETIME2)") + var out DateTime2 + err := row.Scan(&out) + if err != nil { + t.Error("Scan to DateTime2 failed", err.Error()) + return + } + + d := DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}) + if out != d { + t.Errorf("got back a DateTime2 with value: %s", civil.DateTime(out).String()) + } + }) + t.Run("scan into NullDateTime2", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02 23:12:44' AS DATETIME)") + var out NullDateTime2 + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTime2 failed", err.Error()) + return + } + + nd := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}), Valid: true} + if out.DateTime != nd.DateTime || !out.Valid { + t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + } + }) + t.Run("scan into NullDateTime2 from NULL", func(t *testing.T) { + row := conn.QueryRow("SELECT NULL") + var out NullDateTime2 + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTime2 failed", err.Error()) + return + } + + if out.Valid { + t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + } + }) + } func TestSelectWithGuidConversion(t *testing.T) { From 37d6706f78e374307b7ac1b13133abe5abb5704b Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 18 Feb 2026 23:02:56 +0300 Subject: [PATCH 06/32] Add Time tests. --- queries_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/queries_test.go b/queries_test.go index 115a5f27..0347f20e 100644 --- a/queries_test.go +++ b/queries_test.go @@ -379,6 +379,50 @@ func testSelect(t *testing.T, guidConversion bool) { } }) + t.Run("scan into Time", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('12:34:56' AS TIME)") + var out Time + err := row.Scan(&out) + if err != nil { + t.Error("Scan to Time failed", err.Error()) + return + } + + d := Time(civil.Time{Hour: 12, Minute: 34, Second: 56}) + if out != d { + t.Errorf("got back a Time with value: %s", civil.Time(out).String()) + } + }) + + t.Run("scan into NullTime", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('12:34:56' AS TIME)") + var out NullTime + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullTime failed", err.Error()) + return + } + + nd := NullTime{Time: Time(civil.Time{Hour: 12, Minute: 34, Second: 56}), Valid: true} + if out.Time != nd.Time || !out.Valid { + t.Errorf("got back a NullTime with value: %t, %s", out.Valid, civil.Time(out.Time).String()) + } + }) + + t.Run("scan into NullTime from NULL", func(t *testing.T) { + row := conn.QueryRow("SELECT NULL") + var out NullTime + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullTime failed", err.Error()) + return + } + + if out.Valid { + t.Errorf("got back a NullTime with value: %t, %s", out.Valid, civil.Time(out.Time).String()) + } + }) + } func TestSelectWithGuidConversion(t *testing.T) { From 115df72f5bd65977e358ef7a2fda1ff80d8522e9 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 18 Feb 2026 23:07:49 +0300 Subject: [PATCH 07/32] Rename variables. --- queries_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/queries_test.go b/queries_test.go index 0347f20e..77448bcc 100644 --- a/queries_test.go +++ b/queries_test.go @@ -388,8 +388,8 @@ func testSelect(t *testing.T, guidConversion bool) { return } - d := Time(civil.Time{Hour: 12, Minute: 34, Second: 56}) - if out != d { + tm := Time(civil.Time{Hour: 12, Minute: 34, Second: 56}) + if out != tm { t.Errorf("got back a Time with value: %s", civil.Time(out).String()) } }) @@ -403,8 +403,8 @@ func testSelect(t *testing.T, guidConversion bool) { return } - nd := NullTime{Time: Time(civil.Time{Hour: 12, Minute: 34, Second: 56}), Valid: true} - if out.Time != nd.Time || !out.Valid { + ntm := NullTime{Time: Time(civil.Time{Hour: 12, Minute: 34, Second: 56}), Valid: true} + if out.Time != ntm.Time || !out.Valid { t.Errorf("got back a NullTime with value: %t, %s", out.Valid, civil.Time(out.Time).String()) } }) From 64b978002d6dc807eb8cf03206081f86993ec306 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 18 Feb 2026 23:28:07 +0300 Subject: [PATCH 08/32] Add DateTimeOffset tests. --- queries_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/queries_test.go b/queries_test.go index 77448bcc..a014e0d7 100644 --- a/queries_test.go +++ b/queries_test.go @@ -423,6 +423,50 @@ func testSelect(t *testing.T, guidConversion bool) { } }) + t.Run("scan into DateTimeOffset", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02T15:04:05+02:00' AS DATETIMEOFFSET)") + var out DateTimeOffset + err := row.Scan(&out) + if err != nil { + t.Error("Scan to DateTimeOffset failed", err.Error()) + return + } + + exp := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) + if !time.Time(out).Equal(exp) { + t.Errorf("got back a DateTimeOffset with value: %s", time.Time(out).String()) + } + }) + + t.Run("scan into NullDateTimeOffset", func(t *testing.T) { + row := conn.QueryRow("SELECT cast('2006-01-02T15:04:05+02:00' AS DATETIMEOFFSET)") + var out NullDateTimeOffset + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTimeOffset failed", err.Error()) + return + } + + exp := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) + if !out.Valid || !time.Time(out.DateTimeOffset).Equal(exp) { + t.Errorf("got back a NullDateTimeOffset with value: %t, %s", out.Valid, time.Time(out.DateTimeOffset).String()) + } + }) + + t.Run("scan into NullDateTimeOffset from NULL", func(t *testing.T) { + row := conn.QueryRow("SELECT NULL") + var out NullDateTimeOffset + err := row.Scan(&out) + if err != nil { + t.Error("Scan to NullDateTimeOffset failed", err.Error()) + return + } + + if out.Valid { + t.Errorf("got back a NullDateTimeOffset with value: %t, %s", out.Valid, time.Time(out.DateTimeOffset).String()) + } + }) + } func TestSelectWithGuidConversion(t *testing.T) { From dddcea653604d0d9160112e1ba11ea56a9cd2cfa Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 18 Feb 2026 23:54:28 +0300 Subject: [PATCH 09/32] Rename variables. --- queries_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/queries_test.go b/queries_test.go index a014e0d7..509d2ea9 100644 --- a/queries_test.go +++ b/queries_test.go @@ -432,8 +432,8 @@ func testSelect(t *testing.T, guidConversion bool) { return } - exp := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) - if !time.Time(out).Equal(exp) { + dto := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) + if !time.Time(out).Equal(dto) { t.Errorf("got back a DateTimeOffset with value: %s", time.Time(out).String()) } }) @@ -447,8 +447,8 @@ func testSelect(t *testing.T, guidConversion bool) { return } - exp := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) - if !out.Valid || !time.Time(out.DateTimeOffset).Equal(exp) { + dto := time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60)) + if !out.Valid || !time.Time(out.DateTimeOffset).Equal(dto) { t.Errorf("got back a NullDateTimeOffset with value: %t, %s", out.Valid, time.Time(out.DateTimeOffset).String()) } }) From bacc269e3eaaf775faf4abf9a59d31d943584d0a Mon Sep 17 00:00:00 2001 From: El-76 Date: Sat, 21 Feb 2026 20:47:13 +0300 Subject: [PATCH 10/32] More tests. --- queries_go19_test.go | 107 ++++++++++++++++++++++++++++++++++ queries_test.go | 12 ++-- civil_time_null.go => time.go | 0 3 files changed, 113 insertions(+), 6 deletions(-) rename civil_time_null.go => time.go (100%) diff --git a/queries_go19_test.go b/queries_go19_test.go index 6313ae3f..093cc371 100644 --- a/queries_go19_test.go +++ b/queries_go19_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/golang-sql/civil" "github.com/golang-sql/sqlexp" "github.com/shopspring/decimal" ) @@ -740,6 +741,112 @@ END; }) } +func TestOutputINOUTDateParam(t *testing.T) { + sqltextcreate := ` +CREATE PROCEDURE vinout + @dinout DATE OUTPUT +AS +BEGIN + IF @dinout = '2006-01-02' + SET @dinout = NULL + ELSE IF @dinout IS NULL + SET @dinout = '2020-01-01' + ELSE + SET @dinout = '2030-05-15' +END; +` + sqltextdrop := `DROP PROCEDURE vinout;` + sqltextrun := `vinout` + + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + db, err := sql.Open("sqlserver", makeConnStr(t).String()) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + db.ExecContext(ctx, sqltextdrop) + _, err = db.ExecContext(ctx, sqltextcreate) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdrop) + + t.Run("original test", func(t *testing.T) { + dinout := Date(civil.Date{Year: 2000, Month: 6, Day: 15}) + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Date(civil.Date{Year: 2030, Month: 5, Day: 15}) + if dinout != expected { + t.Errorf("expected 2030-05-15, got %s", civil.Date(dinout).String()) + } + }) + + t.Run("nullable value", func(t *testing.T) { + dinout := NullDate{Date: Date(civil.Date{Year: 2000, Month: 6, Day: 15}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Date(civil.Date{Year: 2030, Month: 5, Day: 15}) + if !dinout.Valid || dinout.Date != expected { + if dinout.Valid { + t.Errorf("expected 2030-05-15, got %t, %s", dinout.Valid, civil.Date(dinout.Date).String()) + } else { + t.Errorf("expected 2030-05-15, got NULL") + } + } + }) + + t.Run("null value", func(t *testing.T) { + dinout := NullDate{} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Date(civil.Date{Year: 2020, Month: 1, Day: 1}) + if !dinout.Valid || dinout.Date != expected { + if dinout.Valid { + t.Errorf("expected 2020-01-01, got %t, %s", dinout.Valid, civil.Date(dinout.Date).String()) + } else { + t.Errorf("expected 2020-01-01, got NULL") + } + } + }) + + t.Run("null result", func(t *testing.T) { + dinout := NullDate{Date: Date(civil.Date{Year: 2006, Month: 1, Day: 2}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + if dinout.Valid { + t.Errorf("expected NULL, got %t, %s", dinout.Valid, civil.Date(dinout.Date).String()) + } + }) +} + func TestINOUTDecimalParamEncoding(t *testing.T) { sqltextcreate := ` CREATE PROCEDURE vinout diff --git a/queries_test.go b/queries_test.go index 509d2ea9..ba4e6d0a 100644 --- a/queries_test.go +++ b/queries_test.go @@ -379,7 +379,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into Time", func(t *testing.T) { + t.Run("scan into Time", func(t *testing.T) { row := conn.QueryRow("SELECT cast('12:34:56' AS TIME)") var out Time err := row.Scan(&out) @@ -394,7 +394,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into NullTime", func(t *testing.T) { + t.Run("scan into NullTime", func(t *testing.T) { row := conn.QueryRow("SELECT cast('12:34:56' AS TIME)") var out NullTime err := row.Scan(&out) @@ -409,7 +409,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into NullTime from NULL", func(t *testing.T) { + t.Run("scan into NullTime from NULL", func(t *testing.T) { row := conn.QueryRow("SELECT NULL") var out NullTime err := row.Scan(&out) @@ -423,7 +423,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into DateTimeOffset", func(t *testing.T) { + t.Run("scan into DateTimeOffset", func(t *testing.T) { row := conn.QueryRow("SELECT cast('2006-01-02T15:04:05+02:00' AS DATETIMEOFFSET)") var out DateTimeOffset err := row.Scan(&out) @@ -438,7 +438,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into NullDateTimeOffset", func(t *testing.T) { + t.Run("scan into NullDateTimeOffset", func(t *testing.T) { row := conn.QueryRow("SELECT cast('2006-01-02T15:04:05+02:00' AS DATETIMEOFFSET)") var out NullDateTimeOffset err := row.Scan(&out) @@ -453,7 +453,7 @@ func testSelect(t *testing.T, guidConversion bool) { } }) - t.Run("scan into NullDateTimeOffset from NULL", func(t *testing.T) { + t.Run("scan into NullDateTimeOffset from NULL", func(t *testing.T) { row := conn.QueryRow("SELECT NULL") var out NullDateTimeOffset err := row.Scan(&out) diff --git a/civil_time_null.go b/time.go similarity index 100% rename from civil_time_null.go rename to time.go From 64d03ab082479ff9c5a518fd4a72c77b87a8003f Mon Sep 17 00:00:00 2001 From: El-76 Date: Sat, 21 Feb 2026 21:45:35 +0300 Subject: [PATCH 11/32] Bug fix. --- mssql.go | 1 - mssql_go19.go | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mssql.go b/mssql.go index ee417002..a9c5a76c 100644 --- a/mssql.go +++ b/mssql.go @@ -983,7 +983,6 @@ func (s *Stmt) makeParam(val driver.Value) (res param, err error) { res.ti.Size = 0 return } - switch valuer := val.(type) { // sql.Nullxxx integer types return an int64. We want the original type, to match the SQL type size. case sql.NullByte: diff --git a/mssql_go19.go b/mssql_go19.go index bf6d0995..6a13256d 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -73,6 +73,10 @@ func convertInputParameter(val interface{}) (interface{}, error) { return val, nil case civil.Time: return val, nil + case Date: + return val, nil + case NullDate: + return val, nil // case *apd.Decimal: // return nil case float32: @@ -238,14 +242,12 @@ func (s *Stmt) makeParamExtra(val driver.Value) (res param, err error) { res.ti.Size = len(res.buffer) case DateTime1: makeDateTime(civil.DateTimeOf(time.Time(val)), &res) - case civil.Date: makeDate(val, &res) case civil.DateTime: makeDateTime2(val, &res) case civil.Time: makeTime(val, &res) - case Date: makeDate(civil.Date(val), &res) case DateTime: From 2729b2118b15a9fcecd1b65f0746f01a296cf9b1 Mon Sep 17 00:00:00 2001 From: El-76 Date: Sat, 21 Feb 2026 21:56:24 +0300 Subject: [PATCH 12/32] More tests. --- queries_go19_test.go | 424 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 424 insertions(+) diff --git a/queries_go19_test.go b/queries_go19_test.go index 093cc371..34e5cf16 100644 --- a/queries_go19_test.go +++ b/queries_go19_test.go @@ -847,6 +847,430 @@ END; }) } +func TestOutputINOUTDateTimeParam(t *testing.T) { + sqltextcreate := ` +CREATE PROCEDURE vinout + @dinout DATETIME OUTPUT +AS +BEGIN + IF @dinout = '2006-01-02 15:04:05' + SET @dinout = NULL + ELSE IF @dinout IS NULL + SET @dinout = '2020-01-02 10:11:12' + ELSE + SET @dinout = '2030-05-16 06:07:08' +END; +` + sqltextdrop := `DROP PROCEDURE vinout;` + sqltextrun := `vinout` + + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + db, err := sql.Open("sqlserver", makeConnStr(t).String()) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + db.ExecContext(ctx, sqltextdrop) + _, err = db.ExecContext(ctx, sqltextcreate) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdrop) + + t.Run("original test", func(t *testing.T) { + dinout := DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + if dinout != expected { + t.Errorf("expected 2030-05-16 06:07:08, got %s", civil.DateTime(dinout).String()) + } + }) + + t.Run("nullable value", func(t *testing.T) { + dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + if !dinout.Valid || dinout.DateTime != expected { + if dinout.Valid { + t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } else { + t.Errorf("expected 2030-05-16, got NULL") + } + } + }) + + t.Run("null value", func(t *testing.T) { + dinout := NullDateTime{} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12}}) + if !dinout.Valid || dinout.DateTime != expected { + if dinout.Valid { + t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } else { + t.Errorf("expected 2020-01-02, got NULL") + } + } + }) + + t.Run("null result", func(t *testing.T) { + dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + if dinout.Valid { + t.Errorf("expected NULL, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } + }) +} + +func TestOutputINOUTDateTime2Param(t *testing.T) { + sqltextcreate := ` +CREATE PROCEDURE vinout + @dinout DATETIME2 OUTPUT +AS +BEGIN + IF @dinout = '2006-01-02 15:04:05' + SET @dinout = NULL + ELSE IF @dinout IS NULL + SET @dinout = '2020-01-02 10:11:12' + ELSE + SET @dinout = '2030-05-16 06:07:08' +END; +` + sqltextdrop := `DROP PROCEDURE vinout;` + sqltextrun := `vinout` + + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + db, err := sql.Open("sqlserver", makeConnStr(t).String()) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + db.ExecContext(ctx, sqltextdrop) + _, err = db.ExecContext(ctx, sqltextcreate) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdrop) + + t.Run("original test", func(t *testing.T) { + dinout := DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + if dinout != expected { + t.Errorf("expected 2030-05-16 06:07:08, got %s", civil.DateTime(dinout).String()) + } + }) + + t.Run("nullable value", func(t *testing.T) { + dinout := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + if !dinout.Valid || dinout.DateTime != expected { + if dinout.Valid { + t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } else { + t.Errorf("expected 2030-05-16, got NULL") + } + } + }) + + t.Run("null value", func(t *testing.T) { + dinout := NullDateTime2{} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12}}) + if !dinout.Valid || dinout.DateTime != expected { + if dinout.Valid { + t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } else { + t.Errorf("expected 2020-01-02, got NULL") + } + } + }) + + t.Run("null result", func(t *testing.T) { + dinout := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + if dinout.Valid { + t.Errorf("expected NULL, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + } + }) +} + +func TestOutputINOUTTimeParam(t *testing.T) { + sqltextcreate := ` +CREATE PROCEDURE vinout + @tinout TIME OUTPUT +AS +BEGIN + IF @tinout = '15:04:05' + SET @tinout = NULL + ELSE IF @tinout IS NULL + SET @tinout = '02:03:04' + ELSE + SET @tinout = '06:07:08' +END; +` + sqltextdrop := `DROP PROCEDURE vinout;` + sqltextrun := `vinout` + + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + db, err := sql.Open("sqlserver", makeConnStr(t).String()) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + db.ExecContext(ctx, sqltextdrop) + _, err = db.ExecContext(ctx, sqltextcreate) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdrop) + + t.Run("original test", func(t *testing.T) { + tinout := Time(civil.Time{Hour: 6, Minute: 7, Second: 8}) + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("tinout", sql.Out{Dest: &tinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Time(civil.Time{Hour: 6, Minute: 7, Second: 8}) + if tinout != expected { + t.Errorf("expected 06:07:08, got %s", civil.Time(tinout).String()) + } + }) + + t.Run("nullable value", func(t *testing.T) { + tinout := NullTime{Time: Time(civil.Time{Hour: 6, Minute: 7, Second: 8}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("tinout", sql.Out{Dest: &tinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Time(civil.Time{Hour: 6, Minute: 7, Second: 8}) + if !tinout.Valid || tinout.Time != expected { + if tinout.Valid { + t.Errorf("expected 06:07:08, got %t, %s", tinout.Valid, civil.Time(tinout.Time).String()) + } else { + t.Errorf("expected 06:07:08, got NULL") + } + } + }) + + t.Run("null value", func(t *testing.T) { + tinout := NullTime{} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("tinout", sql.Out{Dest: &tinout}), + ) + if err != nil { + t.Error(err) + } + + expected := Time(civil.Time{Hour: 2, Minute: 3, Second: 4}) + if !tinout.Valid || tinout.Time != expected { + if tinout.Valid { + t.Errorf("expected 02:03:04, got %t, %s", tinout.Valid, civil.Time(tinout.Time).String()) + } else { + t.Errorf("expected 02:03:04, got NULL") + } + } + }) + + t.Run("null result", func(t *testing.T) { + tinout := NullTime{Time: Time(civil.Time{Hour: 15, Minute: 4, Second: 5}), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("tinout", sql.Out{Dest: &tinout}), + ) + if err != nil { + t.Error(err) + } + + if tinout.Valid { + t.Errorf("expected NULL, got %t, %s", tinout.Valid, civil.Time(tinout.Time).String()) + } + }) +} + +func TestOutputINOUTDateTimeOffsetParam(t *testing.T) { + sqltextcreate := ` +CREATE PROCEDURE vinout + @dinout DATETIMEOFFSET(3) OUTPUT +AS +BEGIN + IF @dinout = '2006-01-02T15:04:05+02:00' + SET @dinout = NULL + ELSE IF @dinout IS NULL + SET @dinout = '2020-01-02T10:11:12+03:00' + ELSE + SET @dinout = '2030-05-16T06:07:08+02:00' +END; +` + sqltextdrop := `DROP PROCEDURE vinout;` + sqltextrun := `vinout` + + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + db, err := sql.Open("sqlserver", makeConnStr(t).String()) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + db.ExecContext(ctx, sqltextdrop) + _, err = db.ExecContext(ctx, sqltextcreate) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdrop) + + t.Run("original test", func(t *testing.T) { + dinout := DateTimeOffset(time.Date(2000, 6, 15, 6, 7, 8, 0, time.FixedZone("", 2*60*60))) + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTimeOffset(time.Date(2030, 5, 16, 6, 7, 8, 0, time.FixedZone("", 2*60*60))) + if !time.Time(dinout).Equal(time.Time(expected)) { + t.Errorf("expected 2030-05-16T06:07:08+02:00, got %s", time.Time(dinout).String()) + } + }) + + t.Run("nullable value", func(t *testing.T) { + dinout := NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2000, 6, 15, 6, 7, 8, 0, time.FixedZone("", 2*60*60))), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTimeOffset(time.Date(2030, 5, 16, 6, 7, 8, 0, time.FixedZone("", 2*60*60))) + if !dinout.Valid || !time.Time(dinout.DateTimeOffset).Equal(time.Time(expected)) { + if dinout.Valid { + t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, time.Time(dinout.DateTimeOffset).String()) + } else { + t.Errorf("expected 2030-05-16, got NULL") + } + } + }) + + t.Run("null value", func(t *testing.T) { + dinout := NullDateTimeOffset{} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + expected := DateTimeOffset(time.Date(2020, 1, 2, 10, 11, 12, 0, time.FixedZone("", 3*60*60))) + if !dinout.Valid || !time.Time(dinout.DateTimeOffset).Equal(time.Time(expected)) { + if dinout.Valid { + t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, time.Time(dinout.DateTimeOffset).String()) + } else { + t.Errorf("expected 2020-01-02, got NULL") + } + } + }) + + t.Run("null result", func(t *testing.T) { + dinout := NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2006, 1, 2, 15, 4, 5, 0, time.FixedZone("", 2*60*60))), Valid: true} + _, err = db.ExecContext(ctx, sqltextrun, + sql.Named("dinout", sql.Out{Dest: &dinout}), + ) + if err != nil { + t.Error(err) + } + + if dinout.Valid { + t.Errorf("expected NULL, got %t, %s", dinout.Valid, time.Time(dinout.DateTimeOffset).String()) + } + }) +} + func TestINOUTDecimalParamEncoding(t *testing.T) { sqltextcreate := ` CREATE PROCEDURE vinout From ce87aa747c316296d8e6d42910b79f8e7bdf0167 Mon Sep 17 00:00:00 2001 From: El-76 Date: Sat, 21 Feb 2026 22:07:47 +0300 Subject: [PATCH 13/32] Bug fix. --- mssql_go19.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mssql_go19.go b/mssql_go19.go index 6a13256d..b8b064a0 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -75,8 +75,22 @@ func convertInputParameter(val interface{}) (interface{}, error) { return val, nil case Date: return val, nil + case DateTime: + return val, nil + case DateTime2: + return val, nil + case Time: + return val, nil case NullDate: return val, nil + case NullDateTime: + return val, nil + case NullDateTime2: + return val, nil + case NullTime: + return val, nil + case NullDateTimeOffset: + return val, nil // case *apd.Decimal: // return nil case float32: From b8c5248aaadf3d629e4d6237e05d1b9e3ccaa288 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 11:53:06 +0300 Subject: [PATCH 14/32] Date support for bulkcopy. --- bulkcopy.go | 11 +++++++++++ bulkcopy_test.go | 15 +++++++++++++++ time.go | 16 ++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/bulkcopy.go b/bulkcopy.go index d5cb077f..d29f22db 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/golang-sql/civil" "github.com/microsoft/go-mssqldb/internal/decimal" "github.com/microsoft/go-mssqldb/msdsn" shopspring "github.com/shopspring/decimal" @@ -532,6 +533,16 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) case time.Time: res.buffer = encodeDate(val.Year(), val.YearDay()) res.ti.Size = len(res.buffer) + case Date: + res.buffer = encodeDate(val.Year, civil.Date(val).DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}) + 1) + res.ti.Size = len(res.buffer) + case NullDate: + if val.Valid { + res.buffer = encodeDate(val.Date.Year, civil.Date(val.Date).DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1) + res.ti.Size = len(res.buffer) + } else { + res.ti.Size = 0 + } case string: var t time.Time if t, err = time.ParseInLocation(sqlDateFormat, val, loc); err != nil { diff --git a/bulkcopy_test.go b/bulkcopy_test.go index f7077374..eed9edc6 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -29,6 +29,11 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) { "test_nullint32", "test_nullint16", "test_nulltime", + "test_nullmssqldate", + "test_nullmssqldatetime", + "test_nullmssqldatetime2", + "test_nullmssqltime", + "test_nulldatetimeoffset", "test_nulluniqueidentifier", "test_nulldecimal", "test_nullmoney", @@ -42,6 +47,11 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) { sql.NullInt32{Valid: false}, sql.NullInt16{Valid: false}, sql.NullTime{Valid: false}, + NullDate{Valid: false}, + NullDateTime{Valid: false}, + NullDateTime2{Valid: false}, + NullTime{Valid: false}, + NullDateTimeOffset{Valid: false}, NullUniqueIdentifier{Valid: false}, decimal.NullDecimal{Valid: false}, Money[decimal.NullDecimal]{decimal.NullDecimal{Valid: false}}, @@ -371,6 +381,11 @@ func setupNullableTypeTable(ctx context.Context, t *testing.T, conn *sql.Conn, t [test_nullint32] [int] NULL, [test_nullint16] [smallint] NULL, [test_nulltime] [datetime] NULL, + [test_nullmssqldate] [date] NULL, + [test_nullmssqldatetime] [datetime] NULL, + [test_nullmssqldatetime2] [datetime2] NULL, + [test_nullmssqltime] [time] NULL, + [test_nulldatetimeoffset] [datetimeoffset] NULL, [test_nulluniqueidentifier] [uniqueidentifier] NULL, [test_nulldecimal] [decimal](18, 4) NULL, [test_nullmoney] [money] NULL, diff --git a/time.go b/time.go index a940d6f8..4e427034 100644 --- a/time.go +++ b/time.go @@ -2,6 +2,8 @@ package mssql import ( "database/sql" +// "database/sql/driver" +// "time" "github.com/golang-sql/civil" ) @@ -24,6 +26,11 @@ func (d *Date) Scan(value any) error { return nil } +// Value implements the [Valuer] interface +//func (d Date) Value() (driver.Value, error) { +// return civil.Date(d).In(time.UTC), nil +//} + type DateTime civil.DateTime // Scan implements the [Scanner] interface. @@ -118,6 +125,15 @@ func (n *NullDate) Scan(value any) error { return nil } +// Value implements the [Valuer] interface +//func (n NullDate) Value() (driver.Value, error) { +// if n.Valid { +// return civil.Date(n.Date).In(time.UTC), nil +// } else { +// return nil, nil +// } +//} + type NullDateTime struct { DateTime DateTime Valid bool From a70c13d0b5785559ad0a768e31583d18c9310d11 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 13:53:37 +0300 Subject: [PATCH 15/32] Time types bulkcopy support. --- bulkcopy.go | 121 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index d29f22db..19733b2b 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -481,25 +481,25 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.buffer[0] = 1 } case typeDateTime2N: - switch val := val.(type) { + switch v := val.(type) { case time.Time: res.buffer = encodeDateTime2( - val.Day(), - val.YearDay(), - val.Hour(), - val.Minute(), - val.Second(), - val.Nanosecond(), + v.Year(), + v.YearDay(), + v.Hour(), + v.Minute(), + v.Second(), + v.Nanosecond(), int(col.ti.Scale), ) res.ti.Size = len(res.buffer) case string: var t time.Time - if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } res.buffer = encodeDateTime2( - t.Day(), + t.Year(), t.YearDay(), t.Hour(), t.Minute(), @@ -508,24 +508,77 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) int(col.ti.Scale), ) res.ti.Size = len(res.buffer) + case DateTime: + d := v.(DateTime) + dt := civil.DateTime(d) + res.buffer = encodeDateTime2( + dt.Date.Year, + dt.Date.DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1})+1, + dt.Time.Hour, + dt.Time.Minute, + dt.Time.Second, + dt.Time.Nanosecond, + int(col.ti.Scale), + ) + res.ti.Size = len(res.buffer) + case DateTime2: + d := v.(DateTime2) + dt := civil.DateTime(d) + res.buffer = encodeDateTime2( + dt.Date.Year, + dt.Date.DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1})+1, + dt.Time.Hour, + dt.Time.Minute, + dt.Time.Second, + dt.Time.Nanosecond, + int(col.ti.Scale), + ) + res.ti.Size = len(res.buffer) + case NullDateTime2: + if v.Valid { + d := v.DateTime + dt := civil.DateTime(d) + res.buffer = encodeDateTime2( + dt.Date.Year, + dt.Date.DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1})+1, + dt.Time.Hour, + dt.Time.Minute, + dt.Time.Second, + dt.Time.Nanosecond, + int(col.ti.Scale), + ) + res.ti.Size = len(res.buffer) + } else { + res.ti.Size = 0 + } default: - err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %s", val, val) + err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %v", v, v) return } case typeDateTimeOffsetN: - switch val := val.(type) { + switch v := val.(type) { case time.Time: - res.buffer = encodeDateTimeOffset(val, int(col.ti.Scale)) + res.buffer = encodeDateTimeOffset(v, int(col.ti.Scale)) res.ti.Size = len(res.buffer) case string: var t time.Time - if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } res.buffer = encodeDateTimeOffset(t, int(col.ti.Scale)) res.ti.Size = len(res.buffer) + case DateTimeOffset: + res.buffer = encodeDateTimeOffset(time.Time(v.(DateTimeOffset)), int(col.ti.Scale)) + res.ti.Size = len(res.buffer) + case NullDateTimeOffset: + if v.Valid { + res.buffer = encodeDateTimeOffset(time.Time(v.DateTimeOffset), int(col.ti.Scale)) + res.ti.Size = len(res.buffer) + } else { + res.ti.Size = 0 + } default: - err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %T %s", val, val) + err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %T %v", v, v) return } case typeDateN: @@ -556,15 +609,27 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) } case typeDateTime, typeDateTimeN, typeDateTim4: var t time.Time - switch val := val.(type) { + switch v := val.(type) { case time.Time: - t = val + t = v case string: - if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } + case DateTime: + dt := v.(DateTime) + // Build time.Time from civil.DateTime + t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) + case NullDateTime: + if v.Valid { + dt := v.DateTime + t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) + } else { + res.ti.Size = 0 + return + } default: - err = fmt.Errorf("mssql: invalid type for datetime column: %T %s", val, val) + err = fmt.Errorf("mssql: invalid type for datetime column: %T %v", v, v) return } @@ -586,18 +651,30 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) } case typeTimeN: var t time.Time - switch val := val.(type) { + switch v := val.(type) { case time.Time: - res.buffer = encodeTime(val.Hour(), val.Minute(), val.Second(), val.Nanosecond(), int(col.ti.Scale)) + res.buffer = encodeTime(v.Hour(), v.Minute(), v.Second(), v.Nanosecond(), int(col.ti.Scale)) res.ti.Size = len(res.buffer) case string: - if t, err = time.Parse(sqlTimeFormat, val); err != nil { + if t, err = time.Parse(sqlTimeFormat, v); err != nil { return res, fmt.Errorf("bulk: unable to convert string to time: %v", err) } res.buffer = encodeTime(t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), int(col.ti.Scale)) res.ti.Size = len(res.buffer) + case Time: + tt := v.(Time) + res.buffer = encodeTime(tt.Hour, tt.Minute, tt.Second, tt.Nanosecond, int(col.ti.Scale)) + res.ti.Size = len(res.buffer) + case NullTime: + if v.Valid { + tm := v.Time + res.buffer = encodeTime(tm.Hour, tm.Minute, tm.Second, tm.Nanosecond, int(col.ti.Scale)) + res.ti.Size = len(res.buffer) + } else { + res.ti.Size = 0 + } default: - err = fmt.Errorf("mssql: invalid type for time column: %T %s", val, val) + err = fmt.Errorf("mssql: invalid type for time column: %T %v", v, v) return } case typeMoney, typeMoney4, typeMoneyN: From d831733a2756bcdbd1c1d50bee1c83811be73817 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 14:05:21 +0300 Subject: [PATCH 16/32] Bug fix. --- bulkcopy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index 19733b2b..f55ba7cd 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -587,11 +587,11 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.buffer = encodeDate(val.Year(), val.YearDay()) res.ti.Size = len(res.buffer) case Date: - res.buffer = encodeDate(val.Year, civil.Date(val).DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}) + 1) + res.buffer = encodeDate(val.Year, civil.Date(val).DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1})+1) res.ti.Size = len(res.buffer) case NullDate: if val.Valid { - res.buffer = encodeDate(val.Date.Year, civil.Date(val.Date).DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1) + res.buffer = encodeDate(val.Date.Year, civil.Date(val.Date).DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1})+1) res.ti.Size = len(res.buffer) } else { res.ti.Size = 0 From 6b191f309d65aaee319c456adb9b110e8b4d6ba9 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 14:06:14 +0300 Subject: [PATCH 17/32] Bug fix. --- bulkcopy.go | 80 ++++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index f55ba7cd..c3048af0 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -481,21 +481,21 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.buffer[0] = 1 } case typeDateTime2N: - switch v := val.(type) { + switch val := val.(type) { case time.Time: res.buffer = encodeDateTime2( - v.Year(), - v.YearDay(), - v.Hour(), - v.Minute(), - v.Second(), - v.Nanosecond(), + val.Year(), + val.YearDay(), + val.Hour(), + val.Minute(), + val.Second(), + val.Nanosecond(), int(col.ti.Scale), ) res.ti.Size = len(res.buffer) case string: var t time.Time - if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } res.buffer = encodeDateTime2( @@ -508,22 +508,8 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) int(col.ti.Scale), ) res.ti.Size = len(res.buffer) - case DateTime: - d := v.(DateTime) - dt := civil.DateTime(d) - res.buffer = encodeDateTime2( - dt.Date.Year, - dt.Date.DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1})+1, - dt.Time.Hour, - dt.Time.Minute, - dt.Time.Second, - dt.Time.Nanosecond, - int(col.ti.Scale), - ) - res.ti.Size = len(res.buffer) case DateTime2: - d := v.(DateTime2) - dt := civil.DateTime(d) + dt := civil.DateTime(val) res.buffer = encodeDateTime2( dt.Date.Year, dt.Date.DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1})+1, @@ -535,8 +521,8 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) ) res.ti.Size = len(res.buffer) case NullDateTime2: - if v.Valid { - d := v.DateTime + if val.Valid { + d := val.DateTime dt := civil.DateTime(d) res.buffer = encodeDateTime2( dt.Date.Year, @@ -552,33 +538,33 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.ti.Size = 0 } default: - err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %v", v, v) + err = fmt.Errorf("mssql: invalid type for datetime2 column: %T %v", val, val) return } case typeDateTimeOffsetN: - switch v := val.(type) { + switch val := val.(type) { case time.Time: - res.buffer = encodeDateTimeOffset(v, int(col.ti.Scale)) + res.buffer = encodeDateTimeOffset(val, int(col.ti.Scale)) res.ti.Size = len(res.buffer) case string: var t time.Time - if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } res.buffer = encodeDateTimeOffset(t, int(col.ti.Scale)) res.ti.Size = len(res.buffer) case DateTimeOffset: - res.buffer = encodeDateTimeOffset(time.Time(v.(DateTimeOffset)), int(col.ti.Scale)) + res.buffer = encodeDateTimeOffset(time.Time(val), int(col.ti.Scale)) res.ti.Size = len(res.buffer) case NullDateTimeOffset: - if v.Valid { - res.buffer = encodeDateTimeOffset(time.Time(v.DateTimeOffset), int(col.ti.Scale)) + if val.Valid { + res.buffer = encodeDateTimeOffset(time.Time(val.DateTimeOffset), int(col.ti.Scale)) res.ti.Size = len(res.buffer) } else { res.ti.Size = 0 } default: - err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %T %v", v, v) + err = fmt.Errorf("mssql: invalid type for datetimeoffset column: %T %v", val, val) return } case typeDateN: @@ -609,27 +595,27 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) } case typeDateTime, typeDateTimeN, typeDateTim4: var t time.Time - switch v := val.(type) { + switch val := val.(type) { case time.Time: - t = v + t = val case string: - if t, err = time.Parse(sqlDateTimeFormat, v); err != nil { + if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } case DateTime: - dt := v.(DateTime) + dt := val // Build time.Time from civil.DateTime t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) case NullDateTime: - if v.Valid { - dt := v.DateTime + if val.Valid { + dt := val.DateTime t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) } else { res.ti.Size = 0 return } default: - err = fmt.Errorf("mssql: invalid type for datetime column: %T %v", v, v) + err = fmt.Errorf("mssql: invalid type for datetime column: %T %v", val, val) return } @@ -651,30 +637,30 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) } case typeTimeN: var t time.Time - switch v := val.(type) { + switch val := val.(type) { case time.Time: - res.buffer = encodeTime(v.Hour(), v.Minute(), v.Second(), v.Nanosecond(), int(col.ti.Scale)) + res.buffer = encodeTime(val.Hour(), val.Minute(), val.Second(), val.Nanosecond(), int(col.ti.Scale)) res.ti.Size = len(res.buffer) case string: - if t, err = time.Parse(sqlTimeFormat, v); err != nil { + if t, err = time.Parse(sqlTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to time: %v", err) } res.buffer = encodeTime(t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), int(col.ti.Scale)) res.ti.Size = len(res.buffer) case Time: - tt := v.(Time) + tt := val res.buffer = encodeTime(tt.Hour, tt.Minute, tt.Second, tt.Nanosecond, int(col.ti.Scale)) res.ti.Size = len(res.buffer) case NullTime: - if v.Valid { - tm := v.Time + if val.Valid { + tm := val.Time res.buffer = encodeTime(tm.Hour, tm.Minute, tm.Second, tm.Nanosecond, int(col.ti.Scale)) res.ti.Size = len(res.buffer) } else { res.ti.Size = 0 } default: - err = fmt.Errorf("mssql: invalid type for time column: %T %v", v, v) + err = fmt.Errorf("mssql: invalid type for time column: %T %v", val, val) return } case typeMoney, typeMoney4, typeMoneyN: From 47d18b928d0afdf3b16afa20c24569f7eeefac52 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 14:29:04 +0300 Subject: [PATCH 18/32] Avoid conversion to time.Time. --- bulkcopy.go | 55 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index c3048af0..daecde8f 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -594,22 +594,58 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) return } case typeDateTime, typeDateTimeN, typeDateTim4: + var day, yearDay, hour, minute, second, nanosecond int var t time.Time switch val := val.(type) { case time.Time: t = val + + day = t.Day() + yearDay = t.YearDay() + hour = t.Hour() + minute = t.Minute() + second = t.Second() + nanosecond = t.Nanosecond() case string: if t, err = time.Parse(sqlDateTimeFormat, val); err != nil { return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } + + day = t.Day() + yearDay = t.YearDay() + hour = t.Hour() + minute = t.Minute() + second = t.Second() + nanosecond = t.Nanosecond() case DateTime: + if col.ti.Size == 4 { + err = fmt.Errorf("mssql: invalid type for datetime column: %T %v", val, val) + return + } + dt := val - // Build time.Time from civil.DateTime - t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) + + day = dt.Date.Day + yearDay = civil.Date(dt.Date).DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1}) + 1 + hour = dt.Time.Hour + minute = dt.Time.Minute + second = dt.Time.Second + nanosecond = dt.Time.Nanosecond case NullDateTime: + if col.ti.Size == 4 { + err = fmt.Errorf("mssql: invalid type for datetime column: %T %v", val, val) + return + } + if val.Valid { dt := val.DateTime - t = time.Date(dt.Date.Year, time.Month(dt.Date.Month), dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) + + day = dt.Date.Day + yearDay = civil.Date(dt.Date).DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1}) + 1 + hour = dt.Time.Hour + minute = dt.Time.Minute + second = dt.Time.Second + nanosecond = dt.Time.Nanosecond } else { res.ti.Size = 0 return @@ -624,13 +660,12 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.ti.Size = len(res.buffer) } else if col.ti.Size == 8 { res.buffer = encodeDateTime( - t.Day(), - t.YearDay(), - t.Hour(), - t.Minute(), - t.Second(), - t.Nanosecond(), - ) + day, + yearDay, + hour, + minute, + second, + nanosecond) res.ti.Size = len(res.buffer) } else { err = fmt.Errorf("mssql: invalid size of column %d", col.ti.Size) From 80b8eae503b3fca74f7a2cd1e641fce9857ca9a4 Mon Sep 17 00:00:00 2001 From: El-76 Date: Mon, 23 Feb 2026 15:34:45 +0300 Subject: [PATCH 19/32] Bug fix. --- bulkcopy.go | 12 ++++++------ datetime_midnight_test.go | 2 +- encode_datetime_overflow_test.go | 6 +++--- mssql_go19.go | 6 ++---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index daecde8f..70959502 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -594,13 +594,13 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) return } case typeDateTime, typeDateTimeN, typeDateTim4: - var day, yearDay, hour, minute, second, nanosecond int + var year, yearDay, hour, minute, second, nanosecond int var t time.Time switch val := val.(type) { case time.Time: t = val - day = t.Day() + year = t.Year() yearDay = t.YearDay() hour = t.Hour() minute = t.Minute() @@ -611,7 +611,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) return res, fmt.Errorf("bulk: unable to convert string to date: %v", err) } - day = t.Day() + year = t.Year() yearDay = t.YearDay() hour = t.Hour() minute = t.Minute() @@ -625,7 +625,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) dt := val - day = dt.Date.Day + year = dt.Date.Year yearDay = civil.Date(dt.Date).DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1}) + 1 hour = dt.Time.Hour minute = dt.Time.Minute @@ -640,7 +640,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) if val.Valid { dt := val.DateTime - day = dt.Date.Day + year = dt.Date.Year yearDay = civil.Date(dt.Date).DaysSince(civil.Date{Year: dt.Date.Year, Month: 1, Day: 1}) + 1 hour = dt.Time.Hour minute = dt.Time.Minute @@ -660,7 +660,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.ti.Size = len(res.buffer) } else if col.ti.Size == 8 { res.buffer = encodeDateTime( - day, + year, yearDay, hour, minute, diff --git a/datetime_midnight_test.go b/datetime_midnight_test.go index 000a6ad9..784bbefb 100644 --- a/datetime_midnight_test.go +++ b/datetime_midnight_test.go @@ -26,7 +26,7 @@ func TestDatetimeNearMidnightBoundaries(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Test encoding/decoding encoded := encodeDateTime( - tc.time.Day(), + tc.time.Year(), tc.time.YearDay(), tc.time.Hour(), tc.time.Minute(), diff --git a/encode_datetime_overflow_test.go b/encode_datetime_overflow_test.go index 125558e4..23b6ba01 100644 --- a/encode_datetime_overflow_test.go +++ b/encode_datetime_overflow_test.go @@ -41,7 +41,7 @@ func TestEncodeDateTimeOverflow(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Encode the time encoded := encodeDateTime( - tc.input.Day(), + tc.input.Year(), tc.input.YearDay(), tc.input.Hour(), tc.input.Minute(), @@ -67,7 +67,7 @@ func TestEncodeDateTimeMaxDateOverflow(t *testing.T) { // Encode the time encoded := encodeDateTime( - maxTime.Day(), + maxTime.Year(), maxTime.YearDay(), maxTime.Hour(), maxTime.Minute(), @@ -93,7 +93,7 @@ func TestEncodeDateTimeNoOverflow(t *testing.T) { // Encode the time encoded := encodeDateTime( - normalTime.Day(), + normalTime.Year(), normalTime.YearDay(), normalTime.Hour(), normalTime.Minute(), diff --git a/mssql_go19.go b/mssql_go19.go index b8b064a0..52c116d5 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -187,16 +187,14 @@ func makeDate(val civil.Date, res *param) { } func makeDateTime(val civil.DateTime, res *param) { - res.ti.TypeId = typeDateTime2N - res.ti.Scale = 7 - res.buffer = encodeDateTime2( + res.ti.TypeId = typeDateTimeN + res.buffer = encodeDateTime( val.Date.Year, val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1, val.Time.Hour, val.Time.Minute, val.Time.Second, val.Time.Nanosecond, - int(res.ti.Scale), ) res.ti.Size = len(res.buffer) } From 4e1d8a5dc0897427f19ed917701d50274999a40b Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 1 Mar 2026 01:08:15 +0300 Subject: [PATCH 20/32] Datetime bulk copy tests. --- bulkcopy_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index eed9edc6..2682a9a9 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/golang-sql/civil" + "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" ) @@ -168,6 +170,10 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_datetime2_3", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetime2_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetimeoffset_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, + {"test_mssqldatetime", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, + {"test_mssqldatetimen", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, + {"test_mssqldatetimen_1", Date(civil.Date{Year: 4010, Month: 11, Day: 12}), nil}, + {"test_mssqldatetimen_midnight", Date(civil.Date{Year: 2025, Month: 1, Day: 1}), Date(civil.Date{Year: 2025, Month: 1, Day: 2})}, {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, @@ -351,6 +357,13 @@ func compareValue(a interface{}, expected interface{}) bool { return expected.Equal(got) && ez == az } return false + case Date: + // compare Date (civil.Date) to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + exp := civil.Date(expected) + return exp.In(time.UTC).Equal(got) + } + return false case decimal.Decimal: actual, err := decimal.NewFromString(a.(string)) if err != nil { @@ -427,6 +440,10 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_datetime] [datetime] NOT NULL, [test_datetimen] [datetime] NULL, [test_datetimen_1] [datetime] NULL, + [test_mssqldatetime] [datetime] NULL, + [test_mssqldatetimen] [datetime] NULL, + [test_mssqldatetimen_1] [datetime] NULL, + [test_mssqldatetimen_midnight] [datetime] NULL, [test_datetime2_1] [datetime2](1) NULL, [test_datetime2_3] [datetime2](3) NULL, [test_datetime2_7] [datetime2](7) NULL, From b8432898059e79b5abb1faa298fc4c04df7cd720 Mon Sep 17 00:00:00 2001 From: El-76 Date: Tue, 3 Mar 2026 23:44:57 +0300 Subject: [PATCH 21/32] Bug fix. --- bulkcopy_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index 2682a9a9..b1dfc188 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -35,7 +35,7 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) { "test_nullmssqldatetime", "test_nullmssqldatetime2", "test_nullmssqltime", - "test_nulldatetimeoffset", + "test_nullmssqldatetimeoffset", "test_nulluniqueidentifier", "test_nulldecimal", "test_nullmoney", @@ -170,10 +170,10 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_datetime2_3", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetime2_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetimeoffset_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, - {"test_mssqldatetime", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, - {"test_mssqldatetimen", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, - {"test_mssqldatetimen_1", Date(civil.Date{Year: 4010, Month: 11, Day: 12}), nil}, - {"test_mssqldatetimen_midnight", Date(civil.Date{Year: 2025, Month: 1, Day: 1}), Date(civil.Date{Year: 2025, Month: 1, Day: 2})}, + {"test_mssqldatetime", DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), nil}, + {"test_mssqldatetimen", DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), nil}, + {"test_mssqldatetimen_1", DateTime(civil.DateTime{Date: civil.Date{Year: 4010, Month: 11, Day: 12}}), nil}, + {"test_mssqldatetimen_midnight", DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 1}, Time: civil.Time{Hour: 23, Minute: 59, Second: 59, Nanosecond: 998_350_000}}), DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 2}})}, {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, @@ -357,11 +357,10 @@ func compareValue(a interface{}, expected interface{}) bool { return expected.Equal(got) && ez == az } return false - case Date: - // compare Date (civil.Date) to time.Time returned by SQL + case DateTime: + // compare DateTime (civil.DateTime) to time.Time returned by SQL if got, ok := a.(time.Time); ok { - exp := civil.Date(expected) - return exp.In(time.UTC).Equal(got) + return civil.DateTimeOf(got) == civil.DateTime(expected) } return false case decimal.Decimal: @@ -398,7 +397,7 @@ func setupNullableTypeTable(ctx context.Context, t *testing.T, conn *sql.Conn, t [test_nullmssqldatetime] [datetime] NULL, [test_nullmssqldatetime2] [datetime2] NULL, [test_nullmssqltime] [time] NULL, - [test_nulldatetimeoffset] [datetimeoffset] NULL, + [test_nullmssqldatetimeoffset] [datetimeoffset] NULL, [test_nulluniqueidentifier] [uniqueidentifier] NULL, [test_nulldecimal] [decimal](18, 4) NULL, [test_nullmoney] [money] NULL, From b256dac134b4e661a0773a4111e5b7ca4ec05c81 Mon Sep 17 00:00:00 2001 From: El-76 Date: Tue, 3 Mar 2026 23:52:25 +0300 Subject: [PATCH 22/32] Time types bulkcopy support. --- bulkcopy_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index b1dfc188..f1617fc9 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -174,6 +174,9 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_mssqldatetimen", DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), nil}, {"test_mssqldatetimen_1", DateTime(civil.DateTime{Date: civil.Date{Year: 4010, Month: 11, Day: 12}}), nil}, {"test_mssqldatetimen_midnight", DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 1}, Time: civil.Time{Hour: 23, Minute: 59, Second: 59, Nanosecond: 998_350_000}}), DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 2}})}, + {"test_mssqldatetime2_1", "2010-11-12 13:14:15Z", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15}})}, + {"test_mssqldatetime2_3", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, + {"test_mssqldatetime2_7", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, @@ -360,7 +363,13 @@ func compareValue(a interface{}, expected interface{}) bool { case DateTime: // compare DateTime (civil.DateTime) to time.Time returned by SQL if got, ok := a.(time.Time); ok { - return civil.DateTimeOf(got) == civil.DateTime(expected) + return civil.DateTimeOf(got) == civil.DateTime(expected) + } + return false + case DateTime2: + // compare DateTime2 (civil.DateTime) to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return civil.DateTimeOf(got) == civil.DateTime(expected) } return false case decimal.Decimal: @@ -443,6 +452,9 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_mssqldatetimen] [datetime] NULL, [test_mssqldatetimen_1] [datetime] NULL, [test_mssqldatetimen_midnight] [datetime] NULL, + [test_mssqldatetime2_1] [datetime2](1) NULL, + [test_mssqldatetime2_3] [datetime2](3) NULL, + [test_mssqldatetime2_7] [datetime2](7) NULL, [test_datetime2_1] [datetime2](1) NULL, [test_datetime2_3] [datetime2](3) NULL, [test_datetime2_7] [datetime2](7) NULL, From 2c3da48074affc336819d77e687d4b200468e774 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 4 Mar 2026 00:13:00 +0300 Subject: [PATCH 23/32] Time types bulkcopy support. --- bulkcopy_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index f1617fc9..e7e4ee3e 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -177,6 +177,8 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_mssqldatetime2_1", "2010-11-12 13:14:15Z", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15}})}, {"test_mssqldatetime2_3", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, {"test_mssqldatetime2_7", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, + {"test_mssqldate", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, + {"test_mssqldate_2", "2015-06-07", Date(civil.Date{Year: 2015, Month: 6, Day: 7})}, {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, @@ -459,6 +461,8 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_datetime2_3] [datetime2](3) NULL, [test_datetime2_7] [datetime2](7) NULL, [test_datetimeoffset_7] [datetimeoffset](7) NULL, + [test_mssqldate] [date] NULL, + [test_mssqldate_2] [date] NULL, [test_date] [date] NULL, [test_date_2] [date] NULL, [test_time] [time](7) NULL, From b19acfcd3fa9cdc5951db36a610724d6ac4ec49f Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 4 Mar 2026 00:18:14 +0300 Subject: [PATCH 24/32] Bug fix. --- bulkcopy_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index e7e4ee3e..41f79855 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -362,6 +362,12 @@ func compareValue(a interface{}, expected interface{}) bool { return expected.Equal(got) && ez == az } return false + case Date: + // compare Date (civil.Date) to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return civil.DateOf(got) == civil.Date(expected) + } + return false case DateTime: // compare DateTime (civil.DateTime) to time.Time returned by SQL if got, ok := a.(time.Time); ok { From 8b4ace8b098e01d381583505dd71195d98d2c6eb Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 4 Mar 2026 00:24:46 +0300 Subject: [PATCH 25/32] Time types bulkcopy support. --- bulkcopy_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index 41f79855..5d0ff4e8 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -183,6 +183,9 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, {"test_time_2", "13:14:15.1230000", time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, + {"test_mssqltime", Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}, nil}, + {"test_mssqltime_2", "13:14:15.1230000", Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}, + {"test_tinyint", 255, nil}, {"test_smallint", 32767, nil}, {"test_smallintn", nil, nil}, @@ -472,8 +475,8 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_date] [date] NULL, [test_date_2] [date] NULL, [test_time] [time](7) NULL, - [test_time_2] [time](7) NULL, - [test_smallmoney] [smallmoney] NULL, + [test_time_2] [time](7) NULL, [test_mssqltime] [time](7) NULL, + [test_mssqltime_2] [time](7) NULL, [test_smallmoney] [smallmoney] NULL, [test_money] [money] NULL, [test_tinyint] [tinyint] NULL, [test_smallint] [smallint] NOT NULL, From 245a0cb7e6cfb00a53fd5298274750f8a348c3d1 Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 4 Mar 2026 00:26:24 +0300 Subject: [PATCH 26/32] Bug fix. --- bulkcopy_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index 5d0ff4e8..6adb382d 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -383,6 +383,12 @@ func compareValue(a interface{}, expected interface{}) bool { return civil.DateTimeOf(got) == civil.DateTime(expected) } return false + case Time: + // compare Time (civil.Time) to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return civil.TimeOf(got) == civil.Time(expected) + } + return false case decimal.Decimal: actual, err := decimal.NewFromString(a.(string)) if err != nil { From f843558a7a38eb419a1f4ed1182720b7dac10fcd Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 8 Mar 2026 14:30:48 +0300 Subject: [PATCH 27/32] More bulkcopy tests. --- bulkcopy.go | 2 +- bulkcopy_test.go | 107 ++++++++++++++++++++++++++++++++++++++++--- mssql_go19.go | 8 ++-- queries_go19_test.go | 14 +++--- queries_test.go | 8 ++-- time.go | 22 ++++----- 6 files changed, 127 insertions(+), 34 deletions(-) diff --git a/bulkcopy.go b/bulkcopy.go index 70959502..aa0edd0c 100644 --- a/bulkcopy.go +++ b/bulkcopy.go @@ -522,7 +522,7 @@ func (b *Bulk) makeParam(val DataValue, col columnStruct) (res param, err error) res.ti.Size = len(res.buffer) case NullDateTime2: if val.Valid { - d := val.DateTime + d := val.DateTime2 dt := civil.DateTime(d) res.buffer = encodeDateTime2( dt.Date.Year, diff --git a/bulkcopy_test.go b/bulkcopy_test.go index 6adb382d..28d7b78f 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -36,6 +36,18 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) { "test_nullmssqldatetime2", "test_nullmssqltime", "test_nullmssqldatetimeoffset", + "test_nullmssqlnullable_datetime", + "test_nullmssqlnullable_datetimen", + "test_nullmssqlnullable_datetimen_1", + "test_nullmssqlnullable_datetimen_midnight", + "test_nullmssqlnullable_datetime2_1", + "test_nullmssqlnullable_datetime2_3", + "test_nullmssqlnullable_datetime2_7", + "test_nullmssqlnullable_datetimeoffset_7", + "test_nullmssqlnullable_date", + "test_nullmssqlnullable_date_2", + "test_nullmssqlnullable_time", + "test_nullmssqlnullable_time_2", "test_nulluniqueidentifier", "test_nulldecimal", "test_nullmoney", @@ -54,6 +66,18 @@ func TestBulkcopyWithInvalidNullableType(t *testing.T) { NullDateTime2{Valid: false}, NullTime{Valid: false}, NullDateTimeOffset{Valid: false}, + NullDateTime{Valid: false}, + NullDateTime{Valid: false}, + NullDateTime{Valid: false}, + NullDateTime{Valid: false}, + NullDateTime2{Valid: false}, + NullDateTime2{Valid: false}, + NullDateTime2{Valid: false}, + NullDateTimeOffset{Valid: false}, + NullDate{Valid: false}, + NullDate{Valid: false}, + NullTime{Valid: false}, + NullTime{Valid: false}, NullUniqueIdentifier{Valid: false}, decimal.NullDecimal{Valid: false}, Money[decimal.NullDecimal]{decimal.NullDecimal{Valid: false}}, @@ -170,6 +194,10 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_datetime2_3", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetime2_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, {"test_datetimeoffset_7", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), nil}, + {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, + {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, + {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, + {"test_time_2", "13:14:15.1230000", time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, {"test_mssqldatetime", DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), nil}, {"test_mssqldatetimen", DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), nil}, {"test_mssqldatetimen_1", DateTime(civil.DateTime{Date: civil.Date{Year: 4010, Month: 11, Day: 12}}), nil}, @@ -177,15 +205,23 @@ func testBulkcopy(t *testing.T, guidConversion bool) { {"test_mssqldatetime2_1", "2010-11-12 13:14:15Z", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15}})}, {"test_mssqldatetime2_3", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, {"test_mssqldatetime2_7", DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), nil}, + {"test_mssqldatetimeoffset_7", DateTimeOffset(time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC)), nil}, {"test_mssqldate", Date(civil.Date{Year: 2010, Month: 11, Day: 12}), nil}, {"test_mssqldate_2", "2015-06-07", Date(civil.Date{Year: 2015, Month: 6, Day: 7})}, - {"test_date", time.Date(2010, 11, 12, 0, 0, 0, 0, time.UTC), nil}, - {"test_date_2", "2015-06-07", time.Date(2015, 6, 7, 0, 0, 0, 0, time.UTC)}, - {"test_time", time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC), time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, - {"test_time_2", "13:14:15.1230000", time.Date(1, 1, 1, 13, 14, 15, 123000000, time.UTC)}, {"test_mssqltime", Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}, nil}, {"test_mssqltime_2", "13:14:15.1230000", Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}, - + {"test_mssqlnullable_datetime", NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), Valid: true}, nil}, + {"test_mssqlnullable_datetimen", NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}}), Valid: true}, nil}, + {"test_mssqlnullable_datetimen_1", NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 4010, Month: 11, Day: 12}}), Valid: true}, nil}, + {"test_mssqlnullable_datetimen_midnight", NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 1}, Time: civil.Time{Hour: 23, Minute: 59, Second: 59, Nanosecond: 998_350_000}}), Valid: true}, NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2025, Month: 1, Day: 2}}), Valid: true}}, + {"test_mssqlnullable_datetime2_1", "2010-11-12 13:14:15Z", NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15}}), Valid: true}}, + {"test_mssqlnullable_datetime2_3", NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), Valid: true}, nil}, + {"test_mssqlnullable_datetime2_7", NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2010, Month: 11, Day: 12}, Time: civil.Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}}), Valid: true}, nil}, + {"test_mssqlnullable_datetimeoffset_7", NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2010, 11, 12, 13, 14, 15, 123000000, time.UTC)), Valid: true}, nil}, + {"test_mssqlnullable_date", NullDate{Date: Date(civil.Date{Year: 2010, Month: 11, Day: 12}), Valid: true}, nil}, + {"test_mssqlnullable_date_2", "2015-06-07", NullDate{Date: Date(civil.Date{Year: 2015, Month: 6, Day: 7}), Valid: true}}, + {"test_mssqlnullable_time", NullTime{Time: Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}, Valid: true}, nil}, + {"test_mssqlnullable_time_2", "13:14:15.1230000", NullTime{Time: Time{Hour: 13, Minute: 14, Second: 15, Nanosecond: 123000000}, Valid: true}}, {"test_tinyint", 255, nil}, {"test_smallint", 32767, nil}, {"test_smallintn", nil, nil}, @@ -389,6 +425,36 @@ func compareValue(a interface{}, expected interface{}) bool { return civil.TimeOf(got) == civil.Time(expected) } return false + case NullDate: + // compare NullDate to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return expected.Valid && civil.DateOf(got) == civil.Date(expected.Date) + } + return false + case NullDateTime: + // compare NullDateTime to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return expected.Valid && civil.DateTimeOf(got) == civil.DateTime(expected.DateTime) + } + return false + case NullDateTime2: + // compare NullDateTime2 to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return expected.Valid && civil.DateTimeOf(got) == civil.DateTime(expected.DateTime2) + } + return false + case NullTime: + // compare NullTime to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return expected.Valid && civil.TimeOf(got) == civil.Time(expected.Time) + } + return false + case NullDateTimeOffset: + // compare NullDateTimeOffset to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return expected.Valid && got.Equal(time.Time(expected.DateTimeOffset)) + } + return false case decimal.Decimal: actual, err := decimal.NewFromString(a.(string)) if err != nil { @@ -424,6 +490,18 @@ func setupNullableTypeTable(ctx context.Context, t *testing.T, conn *sql.Conn, t [test_nullmssqldatetime2] [datetime2] NULL, [test_nullmssqltime] [time] NULL, [test_nullmssqldatetimeoffset] [datetimeoffset] NULL, + [test_nullmssqlnullable_datetime] [datetime] NULL, + [test_nullmssqlnullable_datetimen] [datetime] NULL, + [test_nullmssqlnullable_datetimen_1] [datetime] NULL, + [test_nullmssqlnullable_datetimen_midnight] [datetime] NULL, + [test_nullmssqlnullable_datetime2_1] [datetime2](1) NULL, + [test_nullmssqlnullable_datetime2_3] [datetime2](3) NULL, + [test_nullmssqlnullable_datetime2_7] [datetime2](7) NULL, + [test_nullmssqlnullable_datetimeoffset_7] [datetimeoffset](7) NULL, + [test_nullmssqlnullable_date] [date] NULL, + [test_nullmssqlnullable_date_2] [date] NULL, + [test_nullmssqlnullable_time] [time](7) NULL, + [test_nullmssqlnullable_time_2] [time](7) NULL, [test_nulluniqueidentifier] [uniqueidentifier] NULL, [test_nulldecimal] [decimal](18, 4) NULL, [test_nullmoney] [money] NULL, @@ -472,6 +550,21 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_mssqldatetime2_1] [datetime2](1) NULL, [test_mssqldatetime2_3] [datetime2](3) NULL, [test_mssqldatetime2_7] [datetime2](7) NULL, + [test_mssqldatetimeoffset_7] [datetimeoffset](7) NULL, + [test_mssqltime] [time](7) NULL, + [test_mssqltime_2] [time](7) NULL, + [test_mssqlnullable_datetime] [datetime] NULL, + [test_mssqlnullable_datetimen] [datetime] NULL, + [test_mssqlnullable_datetimen_1] [datetime] NULL, + [test_mssqlnullable_datetimen_midnight] [datetime] NULL, + [test_mssqlnullable_datetime2_1] [datetime2](1) NULL, + [test_mssqlnullable_datetime2_3] [datetime2](3) NULL, + [test_mssqlnullable_datetime2_7] [datetime2](7) NULL, + [test_mssqlnullable_datetimeoffset_7] [datetimeoffset](7) NULL, + [test_mssqlnullable_date] [date] NULL, + [test_mssqlnullable_date_2] [date] NULL, + [test_mssqlnullable_time] [time](7) NULL, + [test_mssqlnullable_time_2] [time](7) NULL, [test_datetime2_1] [datetime2](1) NULL, [test_datetime2_3] [datetime2](3) NULL, [test_datetime2_7] [datetime2](7) NULL, @@ -481,8 +574,8 @@ func setupTable(ctx context.Context, t *testing.T, conn *sql.Conn, tableName str [test_date] [date] NULL, [test_date_2] [date] NULL, [test_time] [time](7) NULL, - [test_time_2] [time](7) NULL, [test_mssqltime] [time](7) NULL, - [test_mssqltime_2] [time](7) NULL, [test_smallmoney] [smallmoney] NULL, + [test_time_2] [time](7) NULL, + [test_smallmoney] [smallmoney] NULL, [test_money] [money] NULL, [test_tinyint] [tinyint] NULL, [test_smallint] [smallint] NOT NULL, diff --git a/mssql_go19.go b/mssql_go19.go index 52c116d5..a269f49f 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -181,7 +181,7 @@ func makeDate(val civil.Date, res *param) { res.ti.TypeId = typeDateN res.buffer = encodeDate( val.Year, - val.DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1}) + 1, + val.DaysSince(civil.Date{Year: val.Year, Month: 1, Day: 1})+1, ) res.ti.Size = len(res.buffer) } @@ -190,7 +190,7 @@ func makeDateTime(val civil.DateTime, res *param) { res.ti.TypeId = typeDateTimeN res.buffer = encodeDateTime( val.Date.Year, - val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1, + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1})+1, val.Time.Hour, val.Time.Minute, val.Time.Second, @@ -204,7 +204,7 @@ func makeDateTime2(val civil.DateTime, res *param) { res.ti.Scale = 7 res.buffer = encodeDateTime2( val.Date.Year, - val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1}) + 1, + val.Date.DaysSince(civil.Date{Year: val.Date.Year, Month: 1, Day: 1})+1, val.Time.Hour, val.Time.Minute, val.Time.Second, @@ -284,7 +284,7 @@ func (s *Stmt) makeParamExtra(val driver.Value) (res param, err error) { res.buffer = []byte{} } case NullDateTime2: - makeDateTime2(civil.DateTime(val.DateTime), &res) + makeDateTime2(civil.DateTime(val.DateTime2), &res) if !val.Valid { res.buffer = []byte{} diff --git a/queries_go19_test.go b/queries_go19_test.go index 34e5cf16..b4349c8c 100644 --- a/queries_go19_test.go +++ b/queries_go19_test.go @@ -1007,7 +1007,7 @@ END; }) t.Run("nullable value", func(t *testing.T) { - dinout := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} + dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -1016,9 +1016,9 @@ END; } expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) - if !dinout.Valid || dinout.DateTime != expected { + if !dinout.Valid || dinout.DateTime2 != expected { if dinout.Valid { - t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime2).String()) } else { t.Errorf("expected 2030-05-16, got NULL") } @@ -1035,9 +1035,9 @@ END; } expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12}}) - if !dinout.Valid || dinout.DateTime != expected { + if !dinout.Valid || dinout.DateTime2 != expected { if dinout.Valid { - t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime2).String()) } else { t.Errorf("expected 2020-01-02, got NULL") } @@ -1045,7 +1045,7 @@ END; }) t.Run("null result", func(t *testing.T) { - dinout := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} + dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -1054,7 +1054,7 @@ END; } if dinout.Valid { - t.Errorf("expected NULL, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + t.Errorf("expected NULL, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime2).String()) } }) } diff --git a/queries_test.go b/queries_test.go index ba4e6d0a..bae7d779 100644 --- a/queries_test.go +++ b/queries_test.go @@ -360,9 +360,9 @@ func testSelect(t *testing.T, guidConversion bool) { return } - nd := NullDateTime2{DateTime: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}), Valid: true} - if out.DateTime != nd.DateTime || !out.Valid { - t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + nd := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 23, Minute: 12, Second: 44}}), Valid: true} + if out.DateTime2 != nd.DateTime2 || !out.Valid { + t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime2).String()) } }) t.Run("scan into NullDateTime2 from NULL", func(t *testing.T) { @@ -375,7 +375,7 @@ func testSelect(t *testing.T, guidConversion bool) { } if out.Valid { - t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime).String()) + t.Errorf("got back a NullDateTime2 with value: %t, %s", out.Valid, civil.DateTime(out.DateTime2).String()) } }) diff --git a/time.go b/time.go index 4e427034..fddde7cd 100644 --- a/time.go +++ b/time.go @@ -2,8 +2,8 @@ package mssql import ( "database/sql" -// "database/sql/driver" -// "time" + // "database/sql/driver" + // "time" "github.com/golang-sql/civil" ) @@ -169,8 +169,8 @@ func (n *NullDateTime) Scan(value any) error { } type NullDateTime2 struct { - DateTime DateTime2 - Valid bool + DateTime2 DateTime2 + Valid bool } // Scan implements the [Scanner] interface. @@ -190,14 +190,14 @@ func (n *NullDateTime2) Scan(value any) error { n.Valid = true - n.DateTime.Date.Year = t.Time.Year() - n.DateTime.Date.Month = t.Time.Month() - n.DateTime.Date.Day = t.Time.Day() + n.DateTime2.Date.Year = t.Time.Year() + n.DateTime2.Date.Month = t.Time.Month() + n.DateTime2.Date.Day = t.Time.Day() - n.DateTime.Time.Hour = t.Time.Hour() - n.DateTime.Time.Minute = t.Time.Minute() - n.DateTime.Time.Second = t.Time.Second() - n.DateTime.Time.Nanosecond = t.Time.Nanosecond() + n.DateTime2.Time.Hour = t.Time.Hour() + n.DateTime2.Time.Minute = t.Time.Minute() + n.DateTime2.Time.Second = t.Time.Second() + n.DateTime2.Time.Nanosecond = t.Time.Nanosecond() return nil } From d29fc4f6ec9a0e96d55acb0410642ad6e5d419ba Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 8 Mar 2026 14:36:48 +0300 Subject: [PATCH 28/32] Bug fix. --- bulkcopy_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bulkcopy_test.go b/bulkcopy_test.go index 28d7b78f..6fdf75ff 100644 --- a/bulkcopy_test.go +++ b/bulkcopy_test.go @@ -425,6 +425,12 @@ func compareValue(a interface{}, expected interface{}) bool { return civil.TimeOf(got) == civil.Time(expected) } return false + case DateTimeOffset: + // compare DateTimeOffset to time.Time returned by SQL + if got, ok := a.(time.Time); ok { + return got.Equal(time.Time(expected)) + } + return false case NullDate: // compare NullDate to time.Time returned by SQL if got, ok := a.(time.Time); ok { From 978b10c701ba63ff0182beeb3254951297d9ad8a Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 22 Mar 2026 19:38:47 +0300 Subject: [PATCH 29/32] Time types tvp tests. --- tvp_go19_db_test.go | 128 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/tvp_go19_db_test.go b/tvp_go19_db_test.go index 8b3661b5..5c2d9651 100644 --- a/tvp_go19_db_test.go +++ b/tvp_go19_db_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/golang-sql/civil" "github.com/shopspring/decimal" ) @@ -55,7 +56,27 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { p_money MONEY, p_moneyNull MONEY, s_money MONEY, - s_moneyNull MONEY + s_moneyNull MONEY, + p_date DATE, + p_dateNull DATE, + s_date DATE, + s_dateNull DATE, + p_datetime DATETIME, + p_datetimeNull DATETIME, + s_datetime DATETIME, + s_datetimeNull DATETIME, + p_datetime2 DATETIME2, + p_datetime2Null DATETIME2, + s_datetime2 DATETIME2, + s_datetime2Null DATETIME2, + p_datetimeoffset DATETIMEOFFSET, + p_datetimeoffsetNull DATETIMEOFFSET, + s_datetimeoffset DATETIMEOFFSET, + s_datetimeoffsetNull DATETIMEOFFSET, + p_time TIME, + p_timeNull TIME, + s_time TIME, + s_timeNull TIME ); ` sqltextdroptable := `DROP TYPE tvpGoSQLTypesWithStandardType;` @@ -98,6 +119,26 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PMoneyNull Money[decimal.NullDecimal] SMoney Money[decimal.Decimal] SMoneyNull *Money[decimal.Decimal] + PDate Date + PDateNull NullDate + SDate Date + SDateNull *Date + PDateTime DateTime + PDateTimeNull NullDateTime + SDateTime DateTime + SDateTimeNull *DateTime + PDateTime2 DateTime2 + PDateTime2Null NullDateTime2 + SDateTime2 DateTime2 + SDateTime2Null *DateTime2 + PDateTimeOffset DateTimeOffset + PDateTimeOffsetNull NullDateTimeOffset + SDateTimeOffset DateTimeOffset + SDateTimeOffsetNull *DateTimeOffset + PTime Time + PTimeNull NullTime + STime Time + STimeNull *Time } sqltextdropsp := `DROP PROCEDURE spwithtvpGoSQLTypesWithStandardType;` @@ -120,6 +161,11 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { floatValue64 := 0.123 decimalValue := decimal.New(4821212, -4) moneyValue := Money[decimal.Decimal]{decimal.New(4821212, -4)} + dateValue := Date{Year: 2020, Month: 8, Day: 26} + dateTimeValue := DateTime{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}} + dateTime2Value := DateTime2{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100}} + dateTimeOffsetValue := DateTimeOffset(time.Date(2020, 8, 26, 23, 59, 39, 100, time.UTC)) + timeValue := Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100} param1 := []TvpGoSQLTypes{ { PBool: sql.NullBool{ @@ -146,6 +192,26 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PDecimalNull: decimal.NullDecimal{}, PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-2323, -3))}, PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PDate: dateValue, + PDateNull: NullDate{}, + SDate: dateValue, + SDateNull: &dateValue, + PDateTime: dateTimeValue, + PDateTimeNull: NullDateTime{}, + SDateTime: dateTimeValue, + SDateTimeNull: &dateTimeValue, + PDateTime2: dateTime2Value, + PDateTime2Null: NullDateTime2{}, + SDateTime2: dateTime2Value, + SDateTime2Null: &dateTime2Value, + PDateTimeOffset: dateTimeOffsetValue, + PDateTimeOffsetNull: NullDateTimeOffset{}, + SDateTimeOffset: dateTimeOffsetValue, + SDateTimeOffsetNull: &dateTimeOffsetValue, + PTime: timeValue, + PTimeNull: NullTime{}, + STime: timeValue, + STimeNull: &timeValue, }, { PBool: sql.NullBool{}, @@ -172,6 +238,26 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, SMoney: Money[decimal.Decimal]{decimal.New(20012, 2)}, SMoneyNull: nil, + PDate: Date{}, + PDateNull: NullDate{}, + SDate: Date{Year: 2001, Month: 11, Day: 16}, + SDateNull: nil, + PDateTime: DateTime{}, + PDateTimeNull: NullDateTime{}, + SDateTime: DateTime{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTimeNull: nil, + PDateTime2: DateTime2{}, + PDateTime2Null: NullDateTime2{}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTime2Null: nil, + PDateTimeOffset: DateTimeOffset{}, + PDateTimeOffsetNull: NullDateTimeOffset{}, + SDateTimeOffset: DateTimeOffset(time.Date(2001, 11, 16, 23, 59, 39, 0, time.UTC)), + SDateTimeOffsetNull: nil, + PTime: Time{}, + PTimeNull: NullTime{}, + STime: Time{Hour: 12, Minute: 30, Second: 45}, + STimeNull: nil, }, { PBool: sql.NullBool{ @@ -210,6 +296,26 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, SMoney: Money[decimal.Decimal]{decimal.New(1004, -1)}, SMoneyNull: &moneyValue, + PDate: Date{Year: 2010, Month: 5, Day: 15}, + PDateNull: NullDate{Date: Date{Year: 2010, Month: 5, Day: 15}, Valid: true}, + SDate: Date{Year: 2010, Month: 5, Day: 15}, + SDateNull: &dateValue, + PDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, + PDateTimeNull: NullDateTime{DateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, Valid: true}, + SDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, + SDateTimeNull: &dateTimeValue, + PDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, + PDateTime2Null: NullDateTime2{DateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, Valid: true}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, + SDateTime2Null: &dateTime2Value, + PDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), + PDateTimeOffsetNull: NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), Valid: true}, + SDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), + SDateTimeOffsetNull: &dateTimeOffsetValue, + PTime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, + PTimeNull: NullTime{Time: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, Valid: true}, + STime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, + STimeNull: &timeValue, }, } @@ -262,6 +368,26 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { &val.PMoneyNull, &val.SMoney, &val.SMoneyNull, + &val.PDate, + &val.PDateNull, + &val.SDate, + &val.SDateNull, + &val.PDateTime, + &val.PDateTimeNull, + &val.SDateTime, + &val.SDateTimeNull, + &val.PDateTime2, + &val.PDateTime2Null, + &val.SDateTime2, + &val.SDateTime2Null, + &val.PDateTimeOffset, + &val.PDateTimeOffsetNull, + &val.SDateTimeOffset, + &val.SDateTimeOffsetNull, + &val.PTime, + &val.PTimeNull, + &val.STime, + &val.STimeNull, ) if err != nil { t.Fatalf("scan failed with error: %s", err) From 1dfc4920d5727d4b14f3381d37f9e15e63868d7a Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 5 Apr 2026 01:23:13 +0300 Subject: [PATCH 30/32] TVP fixes. --- mssql_go19.go | 11 ++- queries_go19_test.go | 42 ++++++------ tvp_go19.go | 3 +- tvp_go19_db_test.go | 156 +++++++++++++++++++++++++++++-------------- types.go | 4 +- 5 files changed, 140 insertions(+), 76 deletions(-) diff --git a/mssql_go19.go b/mssql_go19.go index a269f49f..1de0dc66 100644 --- a/mssql_go19.go +++ b/mssql_go19.go @@ -50,7 +50,8 @@ type DateTime1 time.Time type DateTimeOffset time.Time func convertInputParameter(val interface{}) (interface{}, error) { - switch v := val.(type) { + rv := reflect.ValueOf(val) + switch val.(type) { case int, int16, int32, int64, int8: return val, nil case byte: @@ -97,9 +98,13 @@ func convertInputParameter(val interface{}) (interface{}, error) { return val, nil case driver.Valuer: return val, nil - default: - return driver.DefaultParameterConverter.ConvertValue(v) } + + if rv.Kind() == reflect.Pointer && !rv.IsNil() { + return convertInputParameter(rv.Elem().Interface()) + } + + return driver.DefaultParameterConverter.ConvertValue(val) } func (c *Conn) CheckNamedValue(nv *driver.NamedValue) error { diff --git a/queries_go19_test.go b/queries_go19_test.go index b4349c8c..cac62d87 100644 --- a/queries_go19_test.go +++ b/queries_go19_test.go @@ -853,12 +853,12 @@ CREATE PROCEDURE vinout @dinout DATETIME OUTPUT AS BEGIN - IF @dinout = '2006-01-02 15:04:05' + IF @dinout = '2006-01-02 15:04:05.05' SET @dinout = NULL ELSE IF @dinout IS NULL - SET @dinout = '2020-01-02 10:11:12' + SET @dinout = '2020-01-02 10:11:12.06' ELSE - SET @dinout = '2030-05-16 06:07:08' + SET @dinout = '2030-05-16 06:07:08.07' END; ` sqltextdrop := `DROP PROCEDURE vinout;` @@ -886,7 +886,7 @@ END; defer db.ExecContext(ctx, sqltextdrop) t.Run("original test", func(t *testing.T) { - dinout := DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + dinout := DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 123456}}) _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -894,14 +894,14 @@ END; t.Error(err) } - expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 70000000}}) if dinout != expected { - t.Errorf("expected 2030-05-16 06:07:08, got %s", civil.DateTime(dinout).String()) + t.Errorf("expected 2030-05-16 06:07:08.07, got %s", civil.DateTime(dinout).String()) } }) t.Run("nullable value", func(t *testing.T) { - dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} + dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 222222}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -909,7 +909,7 @@ END; t.Error(err) } - expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 70000000}}) if !dinout.Valid || dinout.DateTime != expected { if dinout.Valid { t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) @@ -928,18 +928,18 @@ END; t.Error(err) } - expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12}}) + expected := DateTime(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12, Nanosecond: 60000000}}) if !dinout.Valid || dinout.DateTime != expected { if dinout.Valid { - t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime).String()) + t.Errorf("expected %#v, got %t, %s", expected, dinout.Valid, civil.DateTime(dinout.DateTime).String()) } else { - t.Errorf("expected 2020-01-02, got NULL") + t.Errorf("expected %#v, got NULL", expected) } } }) t.Run("null result", func(t *testing.T) { - dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} + dinout := NullDateTime{DateTime: DateTime(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5, Nanosecond: 50000000}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -959,12 +959,12 @@ CREATE PROCEDURE vinout @dinout DATETIME2 OUTPUT AS BEGIN - IF @dinout = '2006-01-02 15:04:05' + IF @dinout = '2006-01-02 15:04:05.0000005' SET @dinout = NULL ELSE IF @dinout IS NULL - SET @dinout = '2020-01-02 10:11:12' + SET @dinout = '2020-01-02 10:11:12.0000006' ELSE - SET @dinout = '2030-05-16 06:07:08' + SET @dinout = '2030-05-16 06:07:08.0000007' END; ` sqltextdrop := `DROP PROCEDURE vinout;` @@ -992,7 +992,7 @@ END; defer db.ExecContext(ctx, sqltextdrop) t.Run("original test", func(t *testing.T) { - dinout := DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + dinout := DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 123456}}) _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -1000,14 +1000,14 @@ END; t.Error(err) } - expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 700}}) if dinout != expected { t.Errorf("expected 2030-05-16 06:07:08, got %s", civil.DateTime(dinout).String()) } }) t.Run("nullable value", func(t *testing.T) { - dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}), Valid: true} + dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2000, Month: 6, Day: 15}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 222222}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) @@ -1015,7 +1015,7 @@ END; t.Error(err) } - expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8}}) + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2030, Month: 5, Day: 16}, Time: civil.Time{Hour: 6, Minute: 7, Second: 8, Nanosecond: 700}}) if !dinout.Valid || dinout.DateTime2 != expected { if dinout.Valid { t.Errorf("expected 2030-05-16, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime2).String()) @@ -1034,7 +1034,7 @@ END; t.Error(err) } - expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12}}) + expected := DateTime2(civil.DateTime{Date: civil.Date{Year: 2020, Month: 1, Day: 2}, Time: civil.Time{Hour: 10, Minute: 11, Second: 12, Nanosecond: 600}}) if !dinout.Valid || dinout.DateTime2 != expected { if dinout.Valid { t.Errorf("expected 2020-01-02, got %t, %s", dinout.Valid, civil.DateTime(dinout.DateTime2).String()) @@ -1045,7 +1045,7 @@ END; }) t.Run("null result", func(t *testing.T) { - dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5}}), Valid: true} + dinout := NullDateTime2{DateTime2: DateTime2(civil.DateTime{Date: civil.Date{Year: 2006, Month: 1, Day: 2}, Time: civil.Time{Hour: 15, Minute: 4, Second: 5, Nanosecond: 500}}), Valid: true} _, err = db.ExecContext(ctx, sqltextrun, sql.Named("dinout", sql.Out{Dest: &dinout}), ) diff --git a/tvp_go19.go b/tvp_go19.go index 9a71b7de..428e0815 100644 --- a/tvp_go19.go +++ b/tvp_go19.go @@ -113,7 +113,8 @@ func (tvp TVP) encode(schema, name string, columnStr []columnStruct, tvpFieldInd if elemKind == reflect.Ptr && valOf.IsNil() { switch tvpVal.(type) { case *bool, *time.Time, *int8, *int16, *int32, *int64, *float32, *float64, *int, - *uint8, *uint16, *uint32, *uint64, *uint: + *uint8, *uint16, *uint32, *uint64, *uint, + *Date, *DateTime, *DateTime2, *DateTimeOffset, *Time: binary.Write(buf, binary.LittleEndian, uint8(0)) continue default: diff --git a/tvp_go19_db_test.go b/tvp_go19_db_test.go index 5c2d9651..87729d0e 100644 --- a/tvp_go19_db_test.go +++ b/tvp_go19_db_test.go @@ -162,7 +162,7 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { decimalValue := decimal.New(4821212, -4) moneyValue := Money[decimal.Decimal]{decimal.New(4821212, -4)} dateValue := Date{Year: 2020, Month: 8, Day: 26} - dateTimeValue := DateTime{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}} + dateTimeValue := DateTime{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 30000000}} dateTime2Value := DateTime2{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100}} dateTimeOffsetValue := DateTimeOffset(time.Date(2020, 8, 26, 23, 59, 39, 100, time.UTC)) timeValue := Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100} @@ -238,15 +238,15 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, SMoney: Money[decimal.Decimal]{decimal.New(20012, 2)}, SMoneyNull: nil, - PDate: Date{}, + PDate: Date{Year: 1, Month: 1, Day: 1}, // date can't be earlier the Jan 1, 1 PDateNull: NullDate{}, SDate: Date{Year: 2001, Month: 11, Day: 16}, SDateNull: nil, - PDateTime: DateTime{}, + PDateTime: DateTime{Date: civil.Date{Year:1753, Month:1, Day:1}}, // datetime can't be earlier the Jan 1, 1753 PDateTimeNull: NullDateTime{}, SDateTime: DateTime{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, SDateTimeNull: nil, - PDateTime2: DateTime2{}, + PDateTime2: DateTime2{Date:civil.Date{Year:1, Month:1, Day:1}}, // datetime2 can't be earlier the Jan 1, 1 PDateTime2Null: NullDateTime2{}, SDateTime2: DateTime2{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, SDateTime2Null: nil, @@ -300,21 +300,21 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { PDateNull: NullDate{Date: Date{Year: 2010, Month: 5, Day: 15}, Valid: true}, SDate: Date{Year: 2010, Month: 5, Day: 15}, SDateNull: &dateValue, - PDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, - PDateTimeNull: NullDateTime{DateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, Valid: true}, - SDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45}}, + PDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 40000000}}, + PDateTimeNull: NullDateTime{DateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 50000000}}, Valid: true}, + SDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 70000000}}, SDateTimeNull: &dateTimeValue, - PDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, - PDateTime2Null: NullDateTime2{DateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, Valid: true}, - SDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}}, + PDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, + PDateTime2Null: NullDateTime2{DateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, Valid: true}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, SDateTime2Null: &dateTime2Value, - PDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), - PDateTimeOffsetNull: NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), Valid: true}, - SDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123456, time.UTC)), + PDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), + PDateTimeOffsetNull: NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), Valid: true}, + SDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), SDateTimeOffsetNull: &dateTimeOffsetValue, - PTime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, - PTimeNull: NullTime{Time: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, Valid: true}, - STime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123456}, + PTime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, + PTimeNull: NullTime{Time: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, Valid: true}, + STime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, STimeNull: &timeValue, }, } @@ -395,40 +395,8 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { result1 = append(result1, val) } - - for i := 0; i < min(len(param1), len(result1)); i++ { - param1[i].PDecimal.Decimal, result1[i].PDecimal.Decimal = decimal.RescalePair( - param1[i].PDecimal.Decimal, result1[i].PDecimal.Decimal) - param1[i].PDecimalNull.Decimal, result1[i].PDecimalNull.Decimal = decimal.RescalePair( - param1[i].PDecimalNull.Decimal, result1[i].PDecimalNull.Decimal) - param1[i].SDecimal, result1[i].SDecimal = decimal.RescalePair( - param1[i].SDecimal, result1[i].SDecimal) - - if param1[i].SDecimalNull != nil && result1[i].SDecimalNull != nil { - p1, r1 := decimal.RescalePair( - *param1[i].SDecimalNull, *result1[i].SDecimalNull) - param1[i].SDecimalNull = &p1 - result1[i].SDecimalNull = &r1 - } - } - - for i := 0; i < min(len(param1), len(result1)); i++ { - param1[i].PMoney.Decimal.Decimal, result1[i].PMoney.Decimal.Decimal = decimal.RescalePair( - param1[i].PMoney.Decimal.Decimal, result1[i].PMoney.Decimal.Decimal) - param1[i].PMoneyNull.Decimal.Decimal, result1[i].PMoneyNull.Decimal.Decimal = decimal.RescalePair( - param1[i].PMoneyNull.Decimal.Decimal, result1[i].PMoneyNull.Decimal.Decimal) - param1[i].SMoney.Decimal, result1[i].SMoney.Decimal = decimal.RescalePair( - param1[i].SMoney.Decimal, result1[i].SMoney.Decimal) - - if param1[i].SMoneyNull != nil && result1[i].SMoneyNull != nil { - p1, r1 := decimal.RescalePair( - param1[i].SMoneyNull.Decimal, result1[i].SMoneyNull.Decimal) - param1[i].SMoneyNull.Decimal = p1 - result1[i].SMoneyNull.Decimal = r1 - } - } - - if !reflect.DeepEqual(param1, result1) { + + if !compare(param1, result1) { t.Logf("expected: %+v", param1) t.Logf("actual: %+v", result1) t.Errorf("first resultset did not match param1") @@ -459,6 +427,94 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { } } +func compare[T any](param, result []T) bool { + l := len(param) + if l != len(result) { + return false + } + + for j := 0; j < l; j++ { + for i := 0; i < reflect.ValueOf(param[j]).NumField(); i++ { + paramValue := reflect.ValueOf(param[j]).Field(i) + resultValue := reflect.ValueOf(result[j]).Field(i) + + if paramValue.Type() != resultValue.Type() { + return false + } + + var paramInterface any + var resultInterface any + + switch paramValue.Kind() { + case reflect.Ptr: + if paramValue.IsNil() != resultValue.IsNil() { + return false + } + + if paramValue.IsNil() { + continue + } + + paramInterface = paramValue.Elem().Interface() + resultInterface = resultValue.Elem().Interface() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.Bool, reflect.String: + + if paramValue.Equal(resultValue) { + continue + } + + return false + default: + paramInterface = paramValue.Interface() + resultInterface = resultValue.Interface() + } + + switch v := paramInterface.(type) { + case decimal.Decimal: + if !v.Equal(resultInterface.(decimal.Decimal)) { + return false + } + case decimal.NullDecimal: + if v.Valid != resultInterface.(decimal.NullDecimal).Valid { + return false + } + + if v.Valid && !v.Decimal.Equal(resultInterface.(decimal.NullDecimal).Decimal) { + return false + } + case Money[decimal.Decimal]: + if !v.Decimal.Equal(resultInterface.(Money[decimal.Decimal]).Decimal) { + return false + } + case Money[decimal.NullDecimal]: + if v.Decimal.Valid != resultInterface.(Money[decimal.NullDecimal]).Decimal.Valid { + return false + } + + if v.Decimal.Valid && !v.Decimal.Decimal.Equal(resultInterface.(Money[decimal.NullDecimal]).Decimal.Decimal) { + return false + } + case DateTimeOffset: + if !time.Time(v).Equal(time.Time(resultInterface.(DateTimeOffset))) { + return false + } + case NullDateTimeOffset: + if v.Valid != resultInterface.(NullDateTimeOffset).Valid || !time.Time(v.DateTimeOffset).Equal(time.Time(resultInterface.(NullDateTimeOffset).DateTimeOffset)) { + return false + } + default: + if !reflect.DeepEqual(paramInterface, resultInterface) { + return false + } + } + } + } + + return true +} + func TestTVPGoSQLTypes(t *testing.T) { checkConnStr(t) tl := testLogger{t: t} diff --git a/types.go b/types.go index a443fe09..a3b213dc 100644 --- a/types.go +++ b/types.go @@ -392,7 +392,8 @@ func readByteLenTypeWithEncoding(ti *typeInfo, r *tdsBuffer, c *cryptoMetadata, case typeTimeN: return decodeTime(ti.Scale, buf, loc) case typeDateTime2N: - return decodeDateTime2(ti.Scale, buf, loc) + v := decodeDateTime2(ti.Scale, buf, loc) + return v case typeDateTimeOffsetN: return decodeDateTimeOffset(ti.Scale, buf) case typeGuid: @@ -1006,6 +1007,7 @@ func encodeDateTime2(year, yearDay, hour, minute, second, nanosecond, scale int) buf[timesize] = byte(days) buf[timesize+1] = byte(days >> 8) buf[timesize+2] = byte(days >> 16) + return } From da9cdf1262583604744c55114beb90ab50151b95 Mon Sep 17 00:00:00 2001 From: El-76 Date: Sun, 5 Apr 2026 01:44:24 +0300 Subject: [PATCH 31/32] Time types tvp tests. --- tvp_go19_db_test.go | 361 +++++++++++++++++++++++++------------------- 1 file changed, 204 insertions(+), 157 deletions(-) diff --git a/tvp_go19_db_test.go b/tvp_go19_db_test.go index 87729d0e..0f01b76c 100644 --- a/tvp_go19_db_test.go +++ b/tvp_go19_db_test.go @@ -95,50 +95,50 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { END;` type TvpGoSQLTypes struct { - PBool sql.NullBool - PBoolNull sql.NullBool - SBool bool - SBoolNull *bool - PFloat64 sql.NullFloat64 - PFloat64Null sql.NullFloat64 - SFloat64 float64 - SFloat64Null *float64 - PInt64 sql.NullInt64 - PInt64Null sql.NullInt64 - SInt64 int64 - SInt64Null *int64 - PString sql.NullString - PStringNull sql.NullString - SString string - SStringNull *string - PDecimal decimal.NullDecimal - PDecimalNull decimal.NullDecimal - SDecimal decimal.Decimal - SDecimalNull *decimal.Decimal - PMoney Money[decimal.NullDecimal] - PMoneyNull Money[decimal.NullDecimal] - SMoney Money[decimal.Decimal] - SMoneyNull *Money[decimal.Decimal] - PDate Date - PDateNull NullDate - SDate Date - SDateNull *Date - PDateTime DateTime - PDateTimeNull NullDateTime - SDateTime DateTime - SDateTimeNull *DateTime - PDateTime2 DateTime2 - PDateTime2Null NullDateTime2 - SDateTime2 DateTime2 - SDateTime2Null *DateTime2 - PDateTimeOffset DateTimeOffset + PBool sql.NullBool + PBoolNull sql.NullBool + SBool bool + SBoolNull *bool + PFloat64 sql.NullFloat64 + PFloat64Null sql.NullFloat64 + SFloat64 float64 + SFloat64Null *float64 + PInt64 sql.NullInt64 + PInt64Null sql.NullInt64 + SInt64 int64 + SInt64Null *int64 + PString sql.NullString + PStringNull sql.NullString + SString string + SStringNull *string + PDecimal decimal.NullDecimal + PDecimalNull decimal.NullDecimal + SDecimal decimal.Decimal + SDecimalNull *decimal.Decimal + PMoney Money[decimal.NullDecimal] + PMoneyNull Money[decimal.NullDecimal] + SMoney Money[decimal.Decimal] + SMoneyNull *Money[decimal.Decimal] + PDate Date + PDateNull NullDate + SDate Date + SDateNull *Date + PDateTime DateTime + PDateTimeNull NullDateTime + SDateTime DateTime + SDateTimeNull *DateTime + PDateTime2 DateTime2 + PDateTime2Null NullDateTime2 + SDateTime2 DateTime2 + SDateTime2Null *DateTime2 + PDateTimeOffset DateTimeOffset PDateTimeOffsetNull NullDateTimeOffset - SDateTimeOffset DateTimeOffset + SDateTimeOffset DateTimeOffset SDateTimeOffsetNull *DateTimeOffset - PTime Time - PTimeNull NullTime - STime Time - STimeNull *Time + PTime Time + PTimeNull NullTime + STime Time + STimeNull *Time } sqltextdropsp := `DROP PROCEDURE spwithtvpGoSQLTypesWithStandardType;` @@ -187,77 +187,77 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { String: "test=tvp", Valid: true, }, - PStringNull: sql.NullString{}, - PDecimal: decimal.NewNullDecimal(decimal.New(-2323, -3)), - PDecimalNull: decimal.NullDecimal{}, - PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-2323, -3))}, - PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, - PDate: dateValue, - PDateNull: NullDate{}, - SDate: dateValue, - SDateNull: &dateValue, - PDateTime: dateTimeValue, - PDateTimeNull: NullDateTime{}, - SDateTime: dateTimeValue, - SDateTimeNull: &dateTimeValue, - PDateTime2: dateTime2Value, - PDateTime2Null: NullDateTime2{}, - SDateTime2: dateTime2Value, - SDateTime2Null: &dateTime2Value, - PDateTimeOffset: dateTimeOffsetValue, + PStringNull: sql.NullString{}, + PDecimal: decimal.NewNullDecimal(decimal.New(-2323, -3)), + PDecimalNull: decimal.NullDecimal{}, + PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-2323, -3))}, + PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PDate: dateValue, + PDateNull: NullDate{}, + SDate: dateValue, + SDateNull: &dateValue, + PDateTime: dateTimeValue, + PDateTimeNull: NullDateTime{}, + SDateTime: dateTimeValue, + SDateTimeNull: &dateTimeValue, + PDateTime2: dateTime2Value, + PDateTime2Null: NullDateTime2{}, + SDateTime2: dateTime2Value, + SDateTime2Null: &dateTime2Value, + PDateTimeOffset: dateTimeOffsetValue, PDateTimeOffsetNull: NullDateTimeOffset{}, - SDateTimeOffset: dateTimeOffsetValue, + SDateTimeOffset: dateTimeOffsetValue, SDateTimeOffsetNull: &dateTimeOffsetValue, - PTime: timeValue, - PTimeNull: NullTime{}, - STime: timeValue, - STimeNull: &timeValue, + PTime: timeValue, + PTimeNull: NullTime{}, + STime: timeValue, + STimeNull: &timeValue, }, { - PBool: sql.NullBool{}, - PBoolNull: sql.NullBool{}, - SBool: true, - SBoolNull: nil, - PFloat64: sql.NullFloat64{}, - PFloat64Null: sql.NullFloat64{}, - SFloat64: 1.1, - SFloat64Null: nil, - PInt64: sql.NullInt64{}, - PInt64Null: sql.NullInt64{}, - SInt64: 1, - SInt64Null: nil, - PString: sql.NullString{}, - PStringNull: sql.NullString{}, - SString: "any", - SStringNull: nil, - PDecimal: decimal.NullDecimal{}, - PDecimalNull: decimal.NullDecimal{}, - SDecimal: decimal.New(20012, 2), - SDecimalNull: nil, - PMoney: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, - PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, - SMoney: Money[decimal.Decimal]{decimal.New(20012, 2)}, - SMoneyNull: nil, - PDate: Date{Year: 1, Month: 1, Day: 1}, // date can't be earlier the Jan 1, 1 - PDateNull: NullDate{}, - SDate: Date{Year: 2001, Month: 11, Day: 16}, - SDateNull: nil, - PDateTime: DateTime{Date: civil.Date{Year:1753, Month:1, Day:1}}, // datetime can't be earlier the Jan 1, 1753 - PDateTimeNull: NullDateTime{}, - SDateTime: DateTime{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, - SDateTimeNull: nil, - PDateTime2: DateTime2{Date:civil.Date{Year:1, Month:1, Day:1}}, // datetime2 can't be earlier the Jan 1, 1 - PDateTime2Null: NullDateTime2{}, - SDateTime2: DateTime2{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, - SDateTime2Null: nil, - PDateTimeOffset: DateTimeOffset{}, + PBool: sql.NullBool{}, + PBoolNull: sql.NullBool{}, + SBool: true, + SBoolNull: nil, + PFloat64: sql.NullFloat64{}, + PFloat64Null: sql.NullFloat64{}, + SFloat64: 1.1, + SFloat64Null: nil, + PInt64: sql.NullInt64{}, + PInt64Null: sql.NullInt64{}, + SInt64: 1, + SInt64Null: nil, + PString: sql.NullString{}, + PStringNull: sql.NullString{}, + SString: "any", + SStringNull: nil, + PDecimal: decimal.NullDecimal{}, + PDecimalNull: decimal.NullDecimal{}, + SDecimal: decimal.New(20012, 2), + SDecimalNull: nil, + PMoney: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + SMoney: Money[decimal.Decimal]{decimal.New(20012, 2)}, + SMoneyNull: nil, + PDate: Date{Year: 1, Month: 1, Day: 1}, // date can't be earlier the Jan 1, 1 + PDateNull: NullDate{}, + SDate: Date{Year: 2001, Month: 11, Day: 16}, + SDateNull: nil, + PDateTime: DateTime{Date: civil.Date{Year: 1753, Month: 1, Day: 1}}, // datetime can't be earlier the Jan 1, 1753 + PDateTimeNull: NullDateTime{}, + SDateTime: DateTime{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTimeNull: nil, + PDateTime2: DateTime2{Date: civil.Date{Year: 1, Month: 1, Day: 1}}, // datetime2 can't be earlier the Jan 1, 1 + PDateTime2Null: NullDateTime2{}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTime2Null: nil, + PDateTimeOffset: DateTimeOffset{}, PDateTimeOffsetNull: NullDateTimeOffset{}, - SDateTimeOffset: DateTimeOffset(time.Date(2001, 11, 16, 23, 59, 39, 0, time.UTC)), + SDateTimeOffset: DateTimeOffset(time.Date(2001, 11, 16, 23, 59, 39, 0, time.UTC)), SDateTimeOffsetNull: nil, - PTime: Time{}, - PTimeNull: NullTime{}, - STime: Time{Hour: 12, Minute: 30, Second: 45}, - STimeNull: nil, + PTime: Time{}, + PTimeNull: NullTime{}, + STime: Time{Hour: 12, Minute: 30, Second: 45}, + STimeNull: nil, }, { PBool: sql.NullBool{ @@ -285,37 +285,37 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { String: "jifdijrgio", Valid: true, }, - PStringNull: sql.NullString{}, - SString: "geniefkl123", - SStringNull: &nvarchar, - PDecimal: decimal.NewNullDecimal(decimal.New(892332, -3)), - PDecimalNull: decimal.NullDecimal{}, - SDecimal: decimal.New(1004, -1), - SDecimalNull: &decimalValue, - PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(892332, -3))}, - PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, - SMoney: Money[decimal.Decimal]{decimal.New(1004, -1)}, - SMoneyNull: &moneyValue, - PDate: Date{Year: 2010, Month: 5, Day: 15}, - PDateNull: NullDate{Date: Date{Year: 2010, Month: 5, Day: 15}, Valid: true}, - SDate: Date{Year: 2010, Month: 5, Day: 15}, - SDateNull: &dateValue, - PDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 40000000}}, - PDateTimeNull: NullDateTime{DateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 50000000}}, Valid: true}, - SDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 70000000}}, - SDateTimeNull: &dateTimeValue, - PDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, - PDateTime2Null: NullDateTime2{DateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, Valid: true}, - SDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, - SDateTime2Null: &dateTime2Value, - PDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), + PStringNull: sql.NullString{}, + SString: "geniefkl123", + SStringNull: &nvarchar, + PDecimal: decimal.NewNullDecimal(decimal.New(892332, -3)), + PDecimalNull: decimal.NullDecimal{}, + SDecimal: decimal.New(1004, -1), + SDecimalNull: &decimalValue, + PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(892332, -3))}, + PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + SMoney: Money[decimal.Decimal]{decimal.New(1004, -1)}, + SMoneyNull: &moneyValue, + PDate: Date{Year: 2010, Month: 5, Day: 15}, + PDateNull: NullDate{Date: Date{Year: 2010, Month: 5, Day: 15}, Valid: true}, + SDate: Date{Year: 2010, Month: 5, Day: 15}, + SDateNull: &dateValue, + PDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 40000000}}, + PDateTimeNull: NullDateTime{DateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 50000000}}, Valid: true}, + SDateTime: DateTime{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 70000000}}, + SDateTimeNull: &dateTimeValue, + PDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, + PDateTime2Null: NullDateTime2{DateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, Valid: true}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2010, Month: 5, Day: 15}, Time: civil.Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}}, + SDateTime2Null: &dateTime2Value, + PDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), PDateTimeOffsetNull: NullDateTimeOffset{DateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), Valid: true}, - SDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), + SDateTimeOffset: DateTimeOffset(time.Date(2010, 5, 15, 14, 30, 45, 123400, time.FixedZone("", 2*60*60))), SDateTimeOffsetNull: &dateTimeOffsetValue, - PTime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, - PTimeNull: NullTime{Time: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, Valid: true}, - STime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, - STimeNull: &timeValue, + PTime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, + PTimeNull: NullTime{Time: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, Valid: true}, + STime: Time{Hour: 14, Minute: 30, Second: 45, Nanosecond: 123400}, + STimeNull: &timeValue, }, } @@ -395,7 +395,7 @@ func TestTVPGoSQLTypesWithStandardType(t *testing.T) { result1 = append(result1, val) } - + if !compare(param1, result1) { t.Logf("expected: %+v", param1) t.Logf("actual: %+v", result1) @@ -456,7 +456,7 @@ func compare[T any](param, result []T) bool { } paramInterface = paramValue.Elem().Interface() - resultInterface = resultValue.Elem().Interface() + resultInterface = resultValue.Elem().Interface() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Bool, reflect.String: @@ -544,7 +544,17 @@ func TestTVPGoSQLTypes(t *testing.T) { p_decimal DECIMAL(18, 4), p_decimalNull DECIMAL(18, 4), p_money MONEY, - p_moneyNull MONEY + p_moneyNull MONEY, + p_date DATE, + p_dateNull DATE, + p_datetime DATETIME, + p_datetimeNull DATETIME, + p_datetime2 DATETIME2, + p_datetime2Null DATETIME2, + p_datetimeoffset DATETIMEOFFSET, + p_datetimeoffsetNull DATETIMEOFFSET, + p_time TIME, + p_timeNull TIME ); ` sqltextdroptable := `DROP TYPE tvpGoSQLTypes;` @@ -563,18 +573,28 @@ func TestTVPGoSQLTypes(t *testing.T) { END;` type TvpGoSQLTypes struct { - PBool sql.NullBool - PBoolNull sql.NullBool - PFloat64 sql.NullFloat64 - PFloat64Null sql.NullFloat64 - PInt64 sql.NullInt64 - PInt64Null sql.NullInt64 - PString sql.NullString - PStringNull sql.NullString - PDecimal decimal.NullDecimal - PDecimalNull decimal.NullDecimal - PMoney Money[decimal.NullDecimal] - PMoneyNull Money[decimal.NullDecimal] + PBool sql.NullBool + PBoolNull sql.NullBool + PFloat64 sql.NullFloat64 + PFloat64Null sql.NullFloat64 + PInt64 sql.NullInt64 + PInt64Null sql.NullInt64 + PString sql.NullString + PStringNull sql.NullString + PDecimal decimal.NullDecimal + PDecimalNull decimal.NullDecimal + PMoney Money[decimal.NullDecimal] + PMoneyNull Money[decimal.NullDecimal] + PDate Date + PDateNull NullDate + PDateTime DateTime + PDateTimeNull NullDateTime + PDateTime2 DateTime2 + PDateTime2Null NullDateTime2 + PDateTimeOffset DateTimeOffset + PDateTimeOffsetNull NullDateTimeOffset + PTime Time + PTimeNull NullTime } sqltextdropsp := `DROP PROCEDURE spwithtvpGoSQLTypes;` @@ -590,6 +610,13 @@ func TestTVPGoSQLTypes(t *testing.T) { t.Fatal(err) } defer db.ExecContext(ctx, sqltextdropsp) + + dateValue := Date{Year: 2020, Month: 8, Day: 26} + dateTimeValue := DateTime{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 30000000}} + dateTime2Value := DateTime2{Date: civil.Date{Year: 2020, Month: 8, Day: 26}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100}} + dateTimeOffsetValue := DateTimeOffset(time.Date(2020, 8, 26, 23, 59, 39, 100, time.UTC)) + timeValue := Time{Hour: 23, Minute: 59, Second: 39, Nanosecond: 100} + param1 := []TvpGoSQLTypes{ { PBool: sql.NullBool{ @@ -611,11 +638,21 @@ func TestTVPGoSQLTypes(t *testing.T) { String: "test=tvp", Valid: true, }, - PStringNull: sql.NullString{}, - PDecimal: decimal.NewNullDecimal(decimal.New(-7644, -2)), - PDecimalNull: decimal.NullDecimal{}, - PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-7644, -2))}, - PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PStringNull: sql.NullString{}, + PDecimal: decimal.NewNullDecimal(decimal.New(-7644, -2)), + PDecimalNull: decimal.NullDecimal{}, + PMoney: Money[decimal.NullDecimal]{decimal.NewNullDecimal(decimal.New(-7644, -2))}, + PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PDate: dateValue, + PDateNull: NullDate{}, + PDateTime: dateTimeValue, + PDateTimeNull: NullDateTime{}, + PDateTime2: dateTime2Value, + PDateTime2Null: NullDateTime2{}, + PDateTimeOffset: dateTimeOffsetValue, + PDateTimeOffsetNull: NullDateTimeOffset{}, + PTime: timeValue, + PTimeNull: NullTime{}, }, } @@ -655,6 +692,16 @@ func TestTVPGoSQLTypes(t *testing.T) { &val.PDecimalNull, &val.PMoney, &val.PMoneyNull, + &val.PDate, + &val.PDateNull, + &val.PDateTime, + &val.PDateTimeNull, + &val.PDateTime2, + &val.PDateTime2Null, + &val.PDateTimeOffset, + &val.PDateTimeOffsetNull, + &val.PTime, + &val.PTimeNull, ) if err != nil { t.Fatalf("scan failed with error: %s", err) @@ -677,7 +724,7 @@ func TestTVPGoSQLTypes(t *testing.T) { param1[i].PMoneyNull.Decimal.Decimal, result1[i].PMoneyNull.Decimal.Decimal) } - if !reflect.DeepEqual(param1, result1) { + if !compare(param1, result1) { t.Logf("expected: %+v", param1) t.Logf("actual: %+v", result1) t.Errorf("first resultset did not match param1") From 3be5f603ea57a1d9ba34ccc38ea11cd3ad68a66f Mon Sep 17 00:00:00 2001 From: El-76 Date: Wed, 29 Apr 2026 23:33:20 +0300 Subject: [PATCH 32/32] More TVP tests. --- tvp_go19_db_test.go | 298 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) diff --git a/tvp_go19_db_test.go b/tvp_go19_db_test.go index 0f01b76c..2b0e0e55 100644 --- a/tvp_go19_db_test.go +++ b/tvp_go19_db_test.go @@ -15,6 +15,304 @@ import ( "github.com/shopspring/decimal" ) +func TestTVPGoSQLTypesWithStandardTypeNullsOnly(t *testing.T) { + checkConnStr(t) + tl := testLogger{t: t} + defer tl.StopLogging() + SetLogger(&tl) + + c := makeConnStr(t).String() + db, err := sql.Open("sqlserver", c) + if err != nil { + t.Fatalf("failed to open driver sqlserver") + } + defer db.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sqltextcreatetable := ` + CREATE TYPE tvpGoSQLTypesWithStandardType AS TABLE ( + p_bit BIT, + p_bitNull BIT, + s_bit BIT, + s_bitNull BIT, + p_float64 FLOAT, + p_floatNull64 FLOAT, + s_float64 FLOAT, + s_floatNull64 FLOAT, + p_bigint BIGINT, + p_bigintNull BIGINT, + s_bigint BIGINT, + s_bigintNull BIGINT, + p_nvarchar NVARCHAR(100), + p_nvarcharNull NVARCHAR(100), + s_nvarchar NVARCHAR(100), + s_nvarcharNull NVARCHAR(100), + p_decimal DECIMAL(18, 4), + p_decimalNull DECIMAL(18, 4), + s_decimal DECIMAL(18, 4), + s_decimalNull DECIMAL(18, 4), + p_money MONEY, + p_moneyNull MONEY, + s_money MONEY, + s_moneyNull MONEY, + p_date DATE, + p_dateNull DATE, + s_date DATE, + s_dateNull DATE, + p_datetime DATETIME, + p_datetimeNull DATETIME, + s_datetime DATETIME, + s_datetimeNull DATETIME, + p_datetime2 DATETIME2, + p_datetime2Null DATETIME2, + s_datetime2 DATETIME2, + s_datetime2Null DATETIME2, + p_datetimeoffset DATETIMEOFFSET, + p_datetimeoffsetNull DATETIMEOFFSET, + s_datetimeoffset DATETIMEOFFSET, + s_datetimeoffsetNull DATETIMEOFFSET, + p_time TIME, + p_timeNull TIME, + s_time TIME, + s_timeNull TIME + ); ` + + sqltextdroptable := `DROP TYPE tvpGoSQLTypesWithStandardType;` + + sqltextcreatesp := ` + CREATE PROCEDURE spwithtvpGoSQLTypesWithStandardType + @param1 tvpGoSQLTypesWithStandardType READONLY, + @param2 tvpGoSQLTypesWithStandardType READONLY, + @param3 NVARCHAR(10) + AS + BEGIN + SET NOCOUNT ON; + SELECT * FROM @param1; + SELECT * FROM @param2; + SELECT @param3; + END;` + + type TvpGoSQLTypes struct { + PBool sql.NullBool + PBoolNull sql.NullBool + SBool bool + SBoolNull *bool + PFloat64 sql.NullFloat64 + PFloat64Null sql.NullFloat64 + SFloat64 float64 + SFloat64Null *float64 + PInt64 sql.NullInt64 + PInt64Null sql.NullInt64 + SInt64 int64 + SInt64Null *int64 + PString sql.NullString + PStringNull sql.NullString + SString string + SStringNull *string + PDecimal decimal.NullDecimal + PDecimalNull decimal.NullDecimal + SDecimal decimal.Decimal + SDecimalNull *decimal.Decimal + PMoney Money[decimal.NullDecimal] + PMoneyNull Money[decimal.NullDecimal] + SMoney Money[decimal.Decimal] + SMoneyNull *Money[decimal.Decimal] + PDate Date + PDateNull NullDate + SDate Date + SDateNull *Date + PDateTime DateTime + PDateTimeNull NullDateTime + SDateTime DateTime + SDateTimeNull *DateTime + PDateTime2 DateTime2 + PDateTime2Null NullDateTime2 + SDateTime2 DateTime2 + SDateTime2Null *DateTime2 + PDateTimeOffset DateTimeOffset + PDateTimeOffsetNull NullDateTimeOffset + SDateTimeOffset DateTimeOffset + SDateTimeOffsetNull *DateTimeOffset + PTime Time + PTimeNull NullTime + STime Time + STimeNull *Time + } + + sqltextdropsp := `DROP PROCEDURE spwithtvpGoSQLTypesWithStandardType;` + + _, err = db.ExecContext(ctx, sqltextcreatetable) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdroptable) + + _, err = db.ExecContext(ctx, sqltextcreatesp) + if err != nil { + t.Fatal(err) + } + defer db.ExecContext(ctx, sqltextdropsp) + + param1 := []TvpGoSQLTypes{ + { + PBool: sql.NullBool{}, + PBoolNull: sql.NullBool{}, + SBool: true, + SBoolNull: nil, + PFloat64: sql.NullFloat64{}, + PFloat64Null: sql.NullFloat64{}, + SFloat64: 1.1, + SFloat64Null: nil, + PInt64: sql.NullInt64{}, + PInt64Null: sql.NullInt64{}, + SInt64: 1, + SInt64Null: nil, + PString: sql.NullString{}, + PStringNull: sql.NullString{}, + SString: "any", + SStringNull: nil, + PDecimal: decimal.NullDecimal{}, + PDecimalNull: decimal.NullDecimal{}, + SDecimal: decimal.New(20012, 2), + SDecimalNull: nil, + PMoney: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + PMoneyNull: Money[decimal.NullDecimal]{decimal.NullDecimal{}}, + SMoney: Money[decimal.Decimal]{decimal.New(20012, 2)}, + SMoneyNull: nil, + PDate: Date{Year: 1, Month: 1, Day: 1}, // date can't be earlier the Jan 1, 1 + PDateNull: NullDate{}, + SDate: Date{Year: 2001, Month: 11, Day: 16}, + SDateNull: nil, + PDateTime: DateTime{Date: civil.Date{Year: 1753, Month: 1, Day: 1}}, // datetime can't be earlier the Jan 1, 1753 + PDateTimeNull: NullDateTime{}, + SDateTime: DateTime{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTimeNull: nil, + PDateTime2: DateTime2{Date: civil.Date{Year: 1, Month: 1, Day: 1}}, // datetime2 can't be earlier the Jan 1, 1 + PDateTime2Null: NullDateTime2{}, + SDateTime2: DateTime2{Date: civil.Date{Year: 2001, Month: 11, Day: 16}, Time: civil.Time{Hour: 23, Minute: 59, Second: 39}}, + SDateTime2Null: nil, + PDateTimeOffset: DateTimeOffset{}, + PDateTimeOffsetNull: NullDateTimeOffset{}, + SDateTimeOffset: DateTimeOffset(time.Date(2001, 11, 16, 23, 59, 39, 0, time.UTC)), + SDateTimeOffsetNull: nil, + PTime: Time{}, + PTimeNull: NullTime{}, + STime: Time{Hour: 12, Minute: 30, Second: 45}, + STimeNull: nil, + }, + } + + tvpType := TVP{ + TypeName: "tvpGoSQLTypesWithStandardType", + Value: param1, + } + tvpTypeEmpty := TVP{ + TypeName: "tvpGoSQLTypesWithStandardType", + Value: []TvpGoSQLTypes{}, + } + + rows, err := db.QueryContext(ctx, + "exec spwithtvpGoSQLTypesWithStandardType @param1, @param2, @param3", + sql.Named("param1", tvpType), + sql.Named("param2", tvpTypeEmpty), + sql.Named("param3", "test"), + ) + + if err != nil { + t.Fatal(err) + } + + var result1 []TvpGoSQLTypes + for rows.Next() { + var val TvpGoSQLTypes + err := rows.Scan( + &val.PBool, + &val.PBoolNull, + &val.SBool, + &val.SBoolNull, + + &val.PFloat64, + &val.PFloat64Null, + &val.SFloat64, + &val.SFloat64Null, + &val.PInt64, + &val.PInt64Null, + &val.SInt64, + &val.SInt64Null, + &val.PString, + &val.PStringNull, + &val.SString, + &val.SStringNull, + &val.PDecimal, + &val.PDecimalNull, + &val.SDecimal, + &val.SDecimalNull, + &val.PMoney, + &val.PMoneyNull, + &val.SMoney, + &val.SMoneyNull, + &val.PDate, + &val.PDateNull, + &val.SDate, + &val.SDateNull, + &val.PDateTime, + &val.PDateTimeNull, + &val.SDateTime, + &val.SDateTimeNull, + &val.PDateTime2, + &val.PDateTime2Null, + &val.SDateTime2, + &val.SDateTime2Null, + &val.PDateTimeOffset, + &val.PDateTimeOffsetNull, + &val.SDateTimeOffset, + &val.SDateTimeOffsetNull, + &val.PTime, + &val.PTimeNull, + &val.STime, + &val.STimeNull, + ) + if err != nil { + t.Fatalf("scan failed with error: %s", err) + } + + result1 = append(result1, val) + } + + if !compare(param1, result1) { + t.Logf("expected: %+v", param1) + t.Logf("actual: %+v", result1) + t.Errorf("first resultset did not match param1") + } + + if !rows.NextResultSet() { + t.Errorf("second resultset did not exist") + } + + if rows.Next() { + t.Errorf("second resultset was not empty") + } + + if !rows.NextResultSet() { + t.Errorf("third resultset did not exist") + } + + if !rows.Next() { + t.Errorf("third resultset was empty") + } + + var result3 string + if err := rows.Scan(&result3); err != nil { + t.Errorf("error scanning third result set: %s", err) + } + if result3 != "test" { + t.Errorf("third result set had wrong value expected: %s actual: %s", "test", result3) + } +} + + func TestTVPGoSQLTypesWithStandardType(t *testing.T) { checkConnStr(t) tl := testLogger{t: t}