Skip to content

Commit 7e860f9

Browse files
feat: add native JSON type support for SQL Server 2025
1 parent c16a19e commit 7e860f9

File tree

17 files changed

+1543
-71
lines changed

17 files changed

+1543
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
### Features
55

6+
* Add native JSON type support for SQL Server 2025 Preview. The driver now negotiates JSON support during TDS login and properly handles the new JSON data type (0xF4). JSON data is decoded using the NVARCHAR string path and returned as `string`; it can be scanned into `string`, `[]byte`, `mssql.NullJSON`, or types implementing `sql.Scanner`.
67
* Added new `serverCertificate` connection parameter for byte-for-byte certificate validation, matching Microsoft.Data.SqlClient behavior. This parameter skips hostname validation, chain validation, and expiry checks, only verifying that the server's certificate exactly matches the provided file. This is useful when the server's hostname doesn't match the certificate CN/SAN. (#304)
78
* The existing `certificate` parameter maintains backward compatibility with traditional X.509 chain validation including hostname checks, expiry validation, and chain-of-trust verification.
89
* `serverCertificate` cannot be used with `certificate` or `hostnameincertificate` parameters to prevent conflicting validation methods.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,8 @@ are supported:
492492

493493
* string -> nvarchar
494494
* mssql.VarChar -> varchar
495+
* mssql.JSON -> json (SQL Server 2025+) or nvarchar(max) (older versions)
496+
* mssql.NullJSON -> json (nullable, SQL Server 2025+) or nvarchar(max) (older versions)
495497
* time.Time -> datetimeoffset or datetime (TDS version dependent)
496498
* mssql.DateTime1 -> datetime
497499
* mssql.DateTimeOffset -> datetimeoffset
@@ -515,6 +517,41 @@ db.QueryContext(ctx, `select * from t2 where user_name = @p1;`, mssql.VarChar(na
515517
// Note: Mismatched data types on table and parameter may cause long running queries
516518
```
517519

520+
### JSON Type
521+
522+
The driver provides `mssql.JSON` and `mssql.NullJSON` types for working with JSON data. These types work seamlessly across all supported SQL Server versions with automatic fallback behavior.
523+
524+
**Server Compatibility:**
525+
- **SQL Server 2025+ / Azure SQL Database**: Uses the native JSON data type (type ID 0xF4) for optimal performance and type safety
526+
- **SQL Server 2016-2022**: Automatically falls back to `nvarchar(max)` parameter declaration, allowing JSON data to work with the built-in JSON functions (JSON_VALUE, JSON_QUERY, ISJSON, etc.)
527+
528+
The driver automatically detects server capabilities during connection and chooses the appropriate behavior. You can use the same `mssql.JSON` and `mssql.NullJSON` types regardless of the SQL Server version - no code changes required.
529+
530+
**Note:** Native JSON *columns* (using the JSON data type in CREATE TABLE) require SQL Server 2025+ or Azure SQL Database. On older versions, store JSON in `nvarchar(max)` columns.
531+
532+
```go
533+
// Insert JSON data
534+
jsonData := json.RawMessage(`{"name":"John","age":30,"active":true}`)
535+
_, err := db.ExecContext(ctx, "INSERT INTO users (profile) VALUES (@p1)", mssql.JSON(jsonData))
536+
537+
// Insert nullable JSON
538+
nullableJSON := mssql.NullJSON{JSON: json.RawMessage(`{"key":"value"}`), Valid: true}
539+
_, err = db.ExecContext(ctx, "INSERT INTO users (metadata) VALUES (@p1)", nullableJSON)
540+
541+
// Insert NULL JSON value
542+
nullJSON := mssql.NullJSON{Valid: false}
543+
_, err = db.ExecContext(ctx, "INSERT INTO users (metadata) VALUES (@p1)", nullJSON)
544+
545+
// Read JSON data
546+
var result mssql.NullJSON
547+
err = db.QueryRowContext(ctx, "SELECT metadata FROM users WHERE id = @p1", 1).Scan(&result)
548+
if result.Valid {
549+
fmt.Println("JSON data:", result.JSON)
550+
} else {
551+
fmt.Println("JSON is NULL")
552+
}
553+
```
554+
518555
## Using Always Encrypted
519556

520557
The protocol and cryptography details for AE are [detailed elsewhere](https://learn.microsoft.com/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver16).
@@ -590,6 +627,7 @@ Constrain the provider to an allowed list of key vaults by appending vault host
590627
* Can be used with Microsoft Azure SQL Database
591628
* Can be used on all go supported platforms (e.g. Linux, Mac OS X and Windows)
592629
* Supports new date/time types: date, time, datetime2, datetimeoffset
630+
* Supports JSON data with automatic fallback (SQL Server 2016+, native JSON type in 2025+)
593631
* Supports string parameters longer than 8000 characters
594632
* Supports encryption using SSL/TLS
595633
* Supports SQL Server and Windows Authentication
@@ -647,6 +685,8 @@ More information: <http://support.microsoft.com/kb/2653857>
647685
648686
* Bulk copy does not yet support encrypting column values using Always Encrypted. Tracked in [#127](https://github.com/microsoft/go-mssqldb/issues/127)
649687
688+
* The JSON data type is not supported with Always Encrypted. This is a SQL Server limitation - the JSON type is not included in the list of [supported data types for Always Encrypted](https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-cryptography).
689+
650690
# Contributing
651691
This project is a fork of [https://github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) and welcomes new and previous contributors. For more informaton on contributing to this project, please see [Contributing](./CONTRIBUTING.md).
652692

examples/channel_binding/tsql.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ import (
1414

1515
"github.com/google/uuid"
1616
mssqldb "github.com/microsoft/go-mssqldb"
17-
"github.com/microsoft/go-mssqldb/msdsn"
1817
_ "github.com/microsoft/go-mssqldb/integratedauth/krb5"
18+
"github.com/microsoft/go-mssqldb/msdsn"
1919
)
2020

2121
func main() {
2222
var (
23-
userid = flag.String("U", "", "login_id")
24-
password = flag.String("P", "", "password")
25-
server = flag.String("S", "localhost", "server_name[\\instance_name]")
26-
port = flag.Uint64("p", 1433, "server port")
27-
keyLog = flag.String("K", "tlslog.log", "path to sslkeylog file")
28-
database = flag.String("d", "", "db_name")
29-
spn = flag.String("spn", "", "SPN")
30-
auth = flag.String("a", "ntlm", "Authentication method: ntlm, krb5 or winsspi")
31-
epa = flag.Bool("epa", true, "EPA enabled: true, false")
32-
encrypt = flag.String("e", "required", "encrypt mode: required, disabled, strict, optional")
33-
query = flag.String("q", "", "query to execute")
23+
userid = flag.String("U", "", "login_id")
24+
password = flag.String("P", "", "password")
25+
server = flag.String("S", "localhost", "server_name[\\instance_name]")
26+
port = flag.Uint64("p", 1433, "server port")
27+
keyLog = flag.String("K", "tlslog.log", "path to sslkeylog file")
28+
database = flag.String("d", "", "db_name")
29+
spn = flag.String("spn", "", "SPN")
30+
auth = flag.String("a", "ntlm", "Authentication method: ntlm, krb5 or winsspi")
31+
epa = flag.Bool("epa", true, "EPA enabled: true, false")
32+
encrypt = flag.String("e", "required", "encrypt mode: required, disabled, strict, optional")
33+
query = flag.String("q", "", "query to execute")
3434
tlsMinVersion = flag.String("tlsmin", "1.1", "TLS minimum version: 1.0, 1.1, 1.2, 1.3")
3535
tlsMaxVersion = flag.String("tlsmax", "1.3", "TLS maximum version: 1.0, 1.1, 1.2, 1.3")
3636
)
@@ -61,7 +61,7 @@ func main() {
6161
Password: *password,
6262
ChangePassword: "",
6363
AppName: "go-mssqldb",
64-
ServerSPN: *spn,
64+
ServerSPN: *spn,
6565
TLSConfig: &tls.Config{
6666
InsecureSkipVerify: true, // adjust for your case
6767
ServerName: *server,
@@ -70,11 +70,11 @@ func main() {
7070
MinVersion: tlsMinVersionNum,
7171
MaxVersion: tlsMaxVersionNum,
7272
},
73-
Encryption: encryption,
73+
Encryption: encryption,
7474
Parameters: map[string]string{
75-
"authenticator": *auth,
75+
"authenticator": *auth,
7676
"krb5-credcachefile": os.Getenv("KRB5_CCNAME"),
77-
"krb5-configfile": os.Getenv("KRB5_CONFIG"),
77+
"krb5-configfile": os.Getenv("KRB5_CONFIG"),
7878
},
7979
ProtocolParameters: map[string]interface{}{},
8080
Protocols: []string{
@@ -87,7 +87,7 @@ func main() {
8787
DialTimeout: time.Second * 5,
8888
ConnTimeout: time.Second * 10,
8989
KeepAlive: time.Second * 30,
90-
EpaEnabled: *epa,
90+
EpaEnabled: *epa,
9191
}
9292

9393
activityid, uerr := uuid.NewRandom()
@@ -116,7 +116,7 @@ func main() {
116116
fmt.Println("Cannot connect: ", err.Error())
117117
return
118118
}
119-
119+
120120
if *query != "" {
121121
err = exec(db, *query)
122122
if err != nil {
@@ -222,4 +222,4 @@ func parseEncrypt(encrypt string) (msdsn.Encryption, error) {
222222
default:
223223
return msdsn.EncryptionOff, fmt.Errorf("invalid encrypt '%s'", encrypt)
224224
}
225-
}
225+
}

integratedauth/auth_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ type stubAuth struct {
1414
user string
1515
}
1616

17-
func (s *stubAuth) InitialBytes() ([]byte, error) { return nil, nil }
18-
func (s *stubAuth) NextBytes([]byte) ([]byte, error) { return nil, nil }
19-
func (s *stubAuth) Free() {}
20-
func (s *stubAuth) SetChannelBinding(*ChannelBindings) {}
17+
func (s *stubAuth) InitialBytes() ([]byte, error) { return nil, nil }
18+
func (s *stubAuth) NextBytes([]byte) ([]byte, error) { return nil, nil }
19+
func (s *stubAuth) Free() {}
20+
func (s *stubAuth) SetChannelBinding(*ChannelBindings) {}
2121

2222
func getAuth(config msdsn.Config) (IntegratedAuthenticator, error) {
2323
return &stubAuth{config.User}, nil

integratedauth/channel_binding.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ type AuthenticatorWithEPA interface {
1414
}
1515

1616
type ChannelBindingsType uint32
17+
1718
const (
18-
ChannelBindingsTypeTLSExporter = 0
19-
ChannelBindingsTypeTLSUnique = 1
19+
ChannelBindingsTypeTLSExporter = 0
20+
ChannelBindingsTypeTLSUnique = 1
2021
ChannelBindingsTypeTLSServerEndPoint = 2
21-
ChannelBindingsTypeEmpty = 3
22+
ChannelBindingsTypeEmpty = 3
2223
)
2324

2425
const (
@@ -34,7 +35,7 @@ const (
3435
// gss_channel_bindings_struct: https://docs.oracle.com/cd/E19683-01/816-1331/overview-52/index.html
3536
// gss_buffer_desc: https://docs.oracle.com/cd/E19683-01/816-1331/reference-21/index.html
3637
type ChannelBindings struct {
37-
Type ChannelBindingsType
38+
Type ChannelBindingsType
3839
InitiatorAddrType uint32
3940
InitiatorAddress []byte
4041
AcceptorAddrType uint32
@@ -56,7 +57,7 @@ type SEC_CHANNEL_BINDINGS struct {
5657
}
5758

5859
var EmptyChannelBindings = &ChannelBindings{
59-
Type: ChannelBindingsTypeEmpty,
60+
Type: ChannelBindingsTypeEmpty,
6061
InitiatorAddrType: 0,
6162
InitiatorAddress: nil,
6263
AcceptorAddrType: 0,
@@ -175,7 +176,7 @@ func GenerateCBTFromTLSUnique(tlsUnique []byte) (*ChannelBindings, error) {
175176
return nil, fmt.Errorf("tlsUnique is empty")
176177
}
177178
return &ChannelBindings{
178-
Type: ChannelBindingsTypeTLSUnique,
179+
Type: ChannelBindingsTypeTLSUnique,
179180
InitiatorAddrType: 0,
180181
InitiatorAddress: nil,
181182
AcceptorAddrType: 0,
@@ -212,7 +213,7 @@ func GenerateCBTFromTLSExporter(exporterKey []byte) (*ChannelBindings, error) {
212213
}
213214

214215
return &ChannelBindings{
215-
Type: ChannelBindingsTypeTLSExporter,
216+
Type: ChannelBindingsTypeTLSExporter,
216217
InitiatorAddrType: 0,
217218
InitiatorAddress: nil,
218219
AcceptorAddrType: 0,
@@ -247,7 +248,7 @@ func GenerateCBTFromServerCert(cert *x509.Certificate) *ChannelBindings {
247248
_, _ = h.Write(cert.Raw)
248249
certHash = h.Sum(nil)
249250
return &ChannelBindings{
250-
Type: ChannelBindingsTypeTLSServerEndPoint,
251+
Type: ChannelBindingsTypeTLSServerEndPoint,
251252
InitiatorAddrType: 0,
252253
InitiatorAddress: nil,
253254
AcceptorAddrType: 0,

integratedauth/channel_binding_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ func TestAsSSPI_SEC_CHANNEL_BINDINGS(t *testing.T) {
3535
t.Errorf("Expected %s, but got %s for input %s", expected, hex.EncodeToString(winsspiCB), input)
3636
}
3737
}
38-
}
38+
}

integratedauth/winsspi/provider.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build windows
12
// +build windows
23

34
package winsspi
@@ -12,4 +13,4 @@ func init() {
1213
if err != nil {
1314
panic(err)
1415
}
15-
}
16+
}

integratedauth/winsspi/winsspi.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build windows
12
// +build windows
23

34
package winsspi
@@ -141,10 +142,10 @@ func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error
141142
}
142143
domainUser := strings.SplitN(config.User, "\\", 2)
143144
return &Auth{
144-
Domain: domainUser[0],
145-
UserName: domainUser[1],
146-
Password: config.Password,
147-
Service: config.ServerSPN,
145+
Domain: domainUser[0],
146+
UserName: domainUser[1],
147+
Password: config.Password,
148+
Service: config.ServerSPN,
148149
channelBinding: nil,
149150
}, nil
150151
}
@@ -243,7 +244,7 @@ func (auth *Auth) NextBytes(bytes []byte) ([]byte, error) {
243244
// Second buffer: channel bindings (if present)
244245
if auth.channelBinding != nil {
245246
channelBindingBytes := auth.channelBinding.ToBytes()
246-
in_desc_buffers[bufferCount].BufferType = SECBUFFER_CHANNEL_BINDINGS
247+
in_desc_buffers[bufferCount].BufferType = SECBUFFER_CHANNEL_BINDINGS
247248
in_desc_buffers[bufferCount].pvBuffer = &channelBindingBytes[0]
248249
in_desc_buffers[bufferCount].cbBuffer = uint32(len(channelBindingBytes))
249250
bufferCount++

0 commit comments

Comments
 (0)