Skip to content

Commit 5d17654

Browse files
Copilotshueybubbles
andcommitted
Update documentation for nullable civil types
Co-authored-by: shueybubbles <[email protected]>
1 parent 4c3896a commit 5d17654

File tree

7 files changed

+66
-45
lines changed

7 files changed

+66
-45
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,9 @@ are supported:
437437
* "github.com/golang-sql/civil".Date -> date
438438
* "github.com/golang-sql/civil".DateTime -> datetime2
439439
* "github.com/golang-sql/civil".Time -> time
440+
* mssql.NullDate -> date (nullable)
441+
* mssql.NullDateTime -> datetime2 (nullable)
442+
* mssql.NullTime -> time (nullable)
440443
* mssql.TVP -> Table Value Parameter (TDS version dependent)
441444
442445
Using an `int` parameter will send a 4 byte value (int) from a 32bit app and an 8 byte value (bigint) from a 64bit app.

civil_null.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,4 @@ func (n NullTime) MarshalJSON() ([]byte, error) {
211211
return []byte("null"), nil
212212
}
213213
return json.Marshal(n.Time)
214-
}
214+
}

civil_null_integration_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
// This test requires a SQL Server connection
1414
func TestNullCivilTypesIntegration(t *testing.T) {
1515
checkConnStr(t)
16-
16+
1717
tl := testLogger{t: t}
1818
defer tl.StopLogging()
1919

@@ -29,7 +29,7 @@ func TestNullCivilTypesIntegration(t *testing.T) {
2929
// Test NullDate OUT parameter
3030
t.Run("NullDate", func(t *testing.T) {
3131
var nullDate NullDate
32-
32+
3333
// Test NULL value
3434
_, err := conn.ExecContext(ctx, "SELECT @p1 = NULL", sql.Out{Dest: &nullDate})
3535
if err != nil {
@@ -56,7 +56,7 @@ func TestNullCivilTypesIntegration(t *testing.T) {
5656
// Test NullDateTime OUT parameter
5757
t.Run("NullDateTime", func(t *testing.T) {
5858
var nullDateTime NullDateTime
59-
59+
6060
// Test NULL value
6161
_, err := conn.ExecContext(ctx, "SELECT @p1 = NULL", sql.Out{Dest: &nullDateTime})
6262
if err != nil {
@@ -75,20 +75,20 @@ func TestNullCivilTypesIntegration(t *testing.T) {
7575
t.Error("Expected NullDateTime to be valid")
7676
}
7777
// Check that the date and time components are correct
78-
if nullDateTime.DateTime.Date.Year != 2023 ||
79-
nullDateTime.DateTime.Date.Month != time.December ||
80-
nullDateTime.DateTime.Date.Day != 25 ||
81-
nullDateTime.DateTime.Time.Hour != 14 ||
82-
nullDateTime.DateTime.Time.Minute != 30 ||
83-
nullDateTime.DateTime.Time.Second != 45 {
78+
if nullDateTime.DateTime.Date.Year != 2023 ||
79+
nullDateTime.DateTime.Date.Month != time.December ||
80+
nullDateTime.DateTime.Date.Day != 25 ||
81+
nullDateTime.DateTime.Time.Hour != 14 ||
82+
nullDateTime.DateTime.Time.Minute != 30 ||
83+
nullDateTime.DateTime.Time.Second != 45 {
8484
t.Errorf("Unexpected datetime value: %v", nullDateTime.DateTime)
8585
}
8686
})
8787

8888
// Test NullTime OUT parameter
8989
t.Run("NullTime", func(t *testing.T) {
9090
var nullTime NullTime
91-
91+
9292
// Test NULL value
9393
_, err := conn.ExecContext(ctx, "SELECT @p1 = NULL", sql.Out{Dest: &nullTime})
9494
if err != nil {
@@ -107,7 +107,7 @@ func TestNullCivilTypesIntegration(t *testing.T) {
107107
t.Error("Expected NullTime to be valid")
108108
}
109109
if nullTime.Time.Hour != 14 || nullTime.Time.Minute != 30 || nullTime.Time.Second != 45 {
110-
t.Errorf("Expected time 14:30:45, got %02d:%02d:%02d",
110+
t.Errorf("Expected time 14:30:45, got %02d:%02d:%02d",
111111
nullTime.Time.Hour, nullTime.Time.Minute, nullTime.Time.Second)
112112
}
113113
})
@@ -194,4 +194,4 @@ func TestNullCivilTypesIntegration(t *testing.T) {
194194
}
195195
})
196196
})
197-
}
197+
}

civil_null_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,4 @@ func TestNullCivilTypesImplementInterfaces(t *testing.T) {
264264
_ driver.Valuer = NullTime{}
265265
)
266266
// Note: Scanner interface is verified by successful compilation of Scan methods
267-
}
267+
}

doc/how-to-handle-date-and-time-types.md

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ SQL Server has six date and time datatypes: date, time, smalldatetime, datetime,
55
## Inserting Date and Time Data
66

77
The following is a list of datatypes that can be used to insert data into a SQL Server date and/or time type column:
8-
- string
9-
- time.Time
10-
- mssql.DateTime1
11-
- mssql.DateTimeOffset
12-
- "github.com/golang-sql/civil".Date
13-
- "github.com/golang-sql/civil".Time
14-
- "github.com/golang-sql/civil".DateTime
8+
- string
9+
- time.Time
10+
- mssql.DateTime1
11+
- mssql.DateTimeOffset
12+
- "github.com/golang-sql/civil".Date
13+
- "github.com/golang-sql/civil".Time
14+
- "github.com/golang-sql/civil".DateTime
15+
- mssql.NullDate (nullable civil.Date)
16+
- mssql.NullTime (nullable civil.Time)
17+
- mssql.NullDateTime (nullable civil.DateTime)
1518

1619
`time.Time` and `mssql.DateTimeOffset` contain the most information (time zone and over 7 digits precision). Designed to match the SQL Server `datetime` type, `mssql.DateTime1` does not have time zone information, only has up to 3 digits precision and they are rouded to increments of .000, .003 or .007 seconds when the data is passed to SQL Server. If you use `mssql.DateTime1` to hold time zone information or very precised time data (more than 3 decimal digits), you will see data lost when inserting into columns with types that can hold more information. For example:
1720

@@ -30,16 +33,31 @@ _, err = stmt.Exec(param, param, param)
3033
// precisions are lost in all columns. Also, time zone information is lost in datetimeoffsetCol
3134
```
3235

33-
`"github.com/golang-sql/civil".DateTime` does not have time zone information. `"github.com/golang-sql/civil".Date` only has the date information, and `"github.com/golang-sql/civil".Time` only has the time information. `string` can also be used to insert data into date and time types columns, but you have to make sure the format is accepted by SQL Server.
36+
`"github.com/golang-sql/civil".DateTime` does not have time zone information. `"github.com/golang-sql/civil".Date` only has the date information, and `"github.com/golang-sql/civil".Time` only has the time information. `string` can also be used to insert data into date and time types columns, but you have to make sure the format is accepted by SQL Server.
37+
38+
The nullable civil types (`mssql.NullDate`, `mssql.NullDateTime`, `mssql.NullTime`) can be used when you need to handle NULL values, particularly useful for OUT parameters:
39+
40+
```go
41+
var nullDate mssql.NullDate
42+
_, err := conn.ExecContext(ctx, "SELECT @p1 = NULL", sql.Out{Dest: &nullDate})
43+
// nullDate.Valid will be false
44+
45+
var nullDateTime mssql.NullDateTime
46+
_, err = conn.ExecContext(ctx, "SELECT @p1 = '2023-12-25 14:30:45'", sql.Out{Dest: &nullDateTime})
47+
// nullDateTime.Valid will be true, nullDateTime.DateTime contains the value
48+
```
3449

3550
## Retrieving Date and Time Data
3651

37-
The following is a list of datatypes that can be used to retrieved data from a SQL Server date and/or time type column:
38-
- string
39-
- sql.RawBytes
40-
- time.Time
41-
- mssql.DateTime1
42-
- mssql.DateTiimeOffset
52+
The following is a list of datatypes that can be used to retrieved data from a SQL Server date and/or time type column:
53+
- string
54+
- sql.RawBytes
55+
- time.Time
56+
- mssql.DateTime1
57+
- mssql.DateTiimeOffset
58+
- mssql.NullDate (for nullable date columns)
59+
- mssql.NullTime (for nullable time columns)
60+
- mssql.NullDateTime (for nullable datetime2 columns)
4361

4462
When using these data types to retrieve information from a date and/or time type column, you may end up with some extra unexpected information. For example, if you use Go type `time.Time` to retrieve information from a SQL Server `date` column:
4563

msdsn/conn_str.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -711,11 +711,11 @@ func splitAdoConnectionStringParts(dsn string) []string {
711711
var parts []string
712712
var current strings.Builder
713713
inQuotes := false
714-
714+
715715
runes := []rune(dsn)
716716
for i := 0; i < len(runes); i++ {
717717
char := runes[i]
718-
718+
719719
if char == '"' {
720720
if inQuotes && i+1 < len(runes) && runes[i+1] == '"' {
721721
// Double quote escape sequence - add both quotes to current part
@@ -735,12 +735,12 @@ func splitAdoConnectionStringParts(dsn string) []string {
735735
current.WriteRune(char)
736736
}
737737
}
738-
738+
739739
// Add the last part if it's not empty
740740
if current.Len() > 0 {
741741
parts = append(parts, current.String())
742742
}
743-
743+
744744
return parts
745745
}
746746

msdsn/conn_str_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ func TestValidConnectionString(t *testing.T) {
111111
{"MultiSubnetFailover=false", func(p Config) bool { return !p.MultiSubnetFailover }},
112112
{"timezone=Asia/Shanghai", func(p Config) bool { return p.Encoding.Timezone.String() == "Asia/Shanghai" }},
113113
{"Pwd=placeholder", func(p Config) bool { return p.Password == "placeholder" }},
114-
114+
115115
// ADO connection string tests with double-quoted values containing semicolons
116116
{"server=test;password=\"pass;word\"", func(p Config) bool { return p.Host == "test" && p.Password == "pass;word" }},
117117
{"password=\"[2+R2B6O:fF/[;]cJsr\"", func(p Config) bool { return p.Password == "[2+R2B6O:fF/[;]cJsr" }},
118-
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
119-
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
118+
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
119+
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
120120
}},
121121
{"password=\"value with \"\"quotes\"\" inside\"", func(p Config) bool { return p.Password == "value with \"quotes\" inside" }},
122122
{"server=test;password=\"simple\"", func(p Config) bool { return p.Host == "test" && p.Password == "simple" }},
@@ -125,19 +125,19 @@ func TestValidConnectionString(t *testing.T) {
125125
return p.Host == "sql.database.windows.net" && p.Database == "MyDatabase" && p.User == "[email protected]" && p.Password == "[2+R2B6O:fF/[;]cJsr"
126126
}},
127127
// Additional edge cases for double-quoted values
128-
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
129-
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
130-
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
128+
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
129+
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
130+
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
131131
{"server=\"host;name\";password=\"pass;word\"", func(p Config) bool { return p.Host == "host;name" && p.Password == "pass;word" }}, // Multiple quoted values
132-
132+
133133
// Test cases with multibyte UTF-8 characters
134-
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
134+
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
135135
{"server=\"服务器;name\";password=\"密码;word\"", func(p Config) bool { return p.Host == "服务器;name" && p.Password == "密码;word" }}, // Chinese characters
136-
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
137-
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
138-
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
139-
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters
140-
136+
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
137+
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
138+
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
139+
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters
140+
141141
// those are supported currently, but maybe should not be
142142
{"someparam", func(p Config) bool { return true }},
143143
{";;=;", func(p Config) bool { return true }},

0 commit comments

Comments
 (0)