Repository: denisenkom/go-mssqldb
Pull request #627 (2020-12-18 17:50:00)

mssql/msdsn: break out DSN parser from the rest of the package
This allows creating a Connector without parsing the DSN.


kardianos: @wrosenuance @denisenkom @coquagli

Here's what I'm thinking to expose the DSN parsing.

@wrosenuance by returning the raw parameters from parse as well, you can add extra parameters when using federated auth.

I also pulled out the certificate as an entity, so the driver doesn't need to hit the disk, but it could be in a configuration as well.

kardianos: I'll look at the `fedAuthLbrary` zero value issue. Goal here is to continue to not expose any new API (other then existing and new msdsn), but get it situated for the next commit with fed auth. So I'm okay with a few params in the connector that are sub-optimally placed for now.

If we do expose something on the Config, I would want it to be a package controlled "enum" const, like what I did with Encryption, that would then get translated over to the correct values. I'm okay with adding that if it is package, not protocol, defined.

wrosenuance: > Goal here is to continue to not expose any new API (other then existing and new msdsn), but get it situated for the next commit with fed auth. So I'm okay with a few params in the connector that are sub-optimally placed for now.

After a little more reflection I think it makes more sense to have them in the Connector anyway, since...

> I'm okay with adding that if it is package, not protocol, defined.

... and these are protocol defined constants that don't really make sense as arbitrarily specified connection parameters.

> I'll look at the `fedAuthLbrary` zero value issue.

Perhaps the most straightforward thing is to have a bool flag in the Connector struct that has to be set to true for the fed auth to activate? That way it would default to off unless one of the specific Connector creation functions was called. Then those places where the flag is set to true can also make sure to set fedAuthLibrary correctly.


wrosenuance: I was curious as to why the tests didn't fail with the fedAuthLibrary defaulting to zero, so I checked out your branch to test. I think it's because that's the code for the Live ID token scheme and it's not supported, so attempting to use it falls through to other mechanisms.

I added a commit on my branch where I use an explicit flag to indicate that the fedAuthLibrary value has been initialized - https://github.com/denisenkom/go-mssqldb/compare/msdsn...wrosenuance:msdsn
wrosenuance: Hi @kardianos, is there something I can do to help with this part of the PR? I'd like to finish the rest of the Azure authentication PR, but I think this needs to be finalized first.
kardianos: @wrosenuance Let me try to finish this and address your comments. I'm sorry for the monumental delay.

Patch:
diff --git a/accesstokenconnector.go b/accesstokenconnector.go
index 8365e4d..80213d1 100644
--- a/accesstokenconnector.go
+++ b/accesstokenconnector.go
@@ -21,7 +21,8 @@ func NewAccessTokenConnector(dsn string, tokenProvider func() (string, error)) (
 		return nil, err
 	}
 
-	conn.params.fedAuthLibrary = fedAuthLibrarySecurityToken
+	conn.fedAuthRequired = true
+	conn.fedAuthLibrary = fedAuthLibrarySecurityToken
 	conn.securityTokenProvider = func(ctx context.Context) (string, error) {
 		return tokenProvider()
 	}
diff --git a/buf.go b/buf.go
index bad2b00..051d7ba 100644
--- a/buf.go
+++ b/buf.go
@@ -144,11 +144,11 @@ func (r *tdsBuffer) readNextPacket() error {
 	}
 	h := header{
 		PacketType: packetType(buf[0]),
-		Status: buf[1],
-		Size: binary.BigEndian.Uint16(buf[2:4]),
-		Spid: binary.BigEndian.Uint16(buf[4:6]),
-		PacketNo: buf[6],
-		Pad: buf[7],
+		Status:     buf[1],
+		Size:       binary.BigEndian.Uint16(buf[2:4]),
+		Spid:       binary.BigEndian.Uint16(buf[4:6]),
+		PacketNo:   buf[6],
+		Pad:        buf[7],
 	}
 	if int(h.Size) > r.packetSize {
 		return errors.New("invalid packet size, it is longer than buffer size")
diff --git a/examples/routine/routine.go b/examples/routine/routine.go
index 9532941..3c9cf74 100644
--- a/examples/routine/routine.go
+++ b/examples/routine/routine.go
@@ -1,100 +1,100 @@
-package main
-
-import (
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-	"strconv"
-	"sync"
-
-	_ "github.com/denisenkom/go-mssqldb"
-)
-
-var (
-	debug         = flag.Bool("debug", false, "enable debugging")
-	password      = flag.String("password", "", "the database password")
-	port     *int = flag.Int("port", 1433, "the database port")
-	server        = flag.String("server", "", "the database server")
-	user          = flag.String("user", "", "the database user")
-)
-
-func main() {
-	flag.Parse()
-
-	if *debug {
-		fmt.Printf(" password:%s\n", *password)
-		fmt.Printf(" port:%d\n", *port)
-		fmt.Printf(" server:%s\n", *server)
-		fmt.Printf(" user:%s\n", *user)
-	}
-
-	connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d", *server, *user, *password, *port)
-	if *debug {
-		fmt.Printf(" connString:%s\n", connString)
-	}
-
-	db, err := sql.Open("sqlserver", connString)
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer db.Close()
-
-	cExec := 100
-
-	dropSql := "drop table test"
-	db.Exec(dropSql)
-	createSql := "create table test (id INT, idstr varchar(10))"
-	_, err = db.Exec(createSql)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	insertSql := "insert into test (id, idstr) values (@p1, @p2)"
-	done := make(chan bool)
-	stmt, err := db.Prepare(insertSql)
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer stmt.Close()
-
-	// Stmt is safe to be used by multiple goroutines
-	var wg sync.WaitGroup
-	wg.Add(cExec)
-	for j := 0; j < cExec; j++ {
-		go func(val int) {
-			defer wg.Done()
-			_, err := stmt.Exec(val, strconv.Itoa(val))
-			if err != nil {
-				log.Fatal(err)
-			}
-		}(j)
-	}
-	wg.Wait()
-
-	selectSql := "select idstr from test where id = "
-	// DB is safe to be used by multiple goroutines
-	for i := 0; i < cExec; i++ {
-		go func(key int) {
-			rows, err := db.Query(selectSql + strconv.Itoa(key))
-			if err != nil {
-				log.Fatal(err)
-			}
-			defer rows.Close()
-			for rows.Next() {
-				var id int64
-				err := rows.Scan(&id)
-				if err != nil {
-					log.Fatal(err)
-				} else {
-					log.Printf("Found %d\n", key)
-				}
-			}
-			done <- true
-		}(i)
-	}
-
-	for i := 0; i < cExec; i++ {
-		<-done
-	}
-}
+package main
+
+import (
+	"database/sql"
+	"flag"
+	"fmt"
+	"log"
+	"strconv"
+	"sync"
+
+	_ "github.com/denisenkom/go-mssqldb"
+)
+
+var (
+	debug         = flag.Bool("debug", false, "enable debugging")
+	password      = flag.String("password", "", "the database password")
+	port     *int = flag.Int("port", 1433, "the database port")
+	server        = flag.String("server", "", "the database server")
+	user          = flag.String("user", "", "the database user")
+)
+
+func main() {
+	flag.Parse()
+
+	if *debug {
+		fmt.Printf(" password:%s\n", *password)
+		fmt.Printf(" port:%d\n", *port)
+		fmt.Printf(" server:%s\n", *server)
+		fmt.Printf(" user:%s\n", *user)
+	}
+
+	connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d", *server, *user, *password, *port)
+	if *debug {
+		fmt.Printf(" connString:%s\n", connString)
+	}
+
+	db, err := sql.Open("sqlserver", connString)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer db.Close()
+
+	cExec := 100
+
+	dropSql := "drop table test"
+	db.Exec(dropSql)
+	createSql := "create table test (id INT, idstr varchar(10))"
+	_, err = db.Exec(createSql)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	insertSql := "insert into test (id, idstr) values (@p1, @p2)"
+	done := make(chan bool)
+	stmt, err := db.Prepare(insertSql)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer stmt.Close()
+
+	// Stmt is safe to be used by multiple goroutines
+	var wg sync.WaitGroup
+	wg.Add(cExec)
+	for j := 0; j < cExec; j++ {
+		go func(val int) {
+			defer wg.Done()
+			_, err := stmt.Exec(val, strconv.Itoa(val))
+			if err != nil {
+				log.Fatal(err)
+			}
+		}(j)
+	}
+	wg.Wait()
+
+	selectSql := "select idstr from test where id = "
+	// DB is safe to be used by multiple goroutines
+	for i := 0; i < cExec; i++ {
+		go func(key int) {
+			rows, err := db.Query(selectSql + strconv.Itoa(key))
+			if err != nil {
+				log.Fatal(err)
+			}
+			defer rows.Close()
+			for rows.Next() {
+				var id int64
+				err := rows.Scan(&id)
+				if err != nil {
+					log.Fatal(err)
+				} else {
+					log.Printf("Found %d\n", key)
+				}
+			}
+			done <- true
+		}(i)
+	}
+
+	for i := 0; i < cExec; i++ {
+		<-done
+	}
+}
diff --git a/fedauth.go b/fedauth.go
index 86fed25..459c664 100644
--- a/fedauth.go
+++ b/fedauth.go
@@ -1,82 +1,78 @@
-package mssql
-
-import (
-	"context"
-	"errors"
-)
-
-// Federated authentication library affects the login data structure and message sequence.
-const (
-	// fedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme
-	fedAuthLibraryLiveIDCompactToken = 0x00
-
-	// fedAuthLibrarySecurityToken specifies a token-based authentication where the token is available
-	// without additional information provided during the login sequence.
-	fedAuthLibrarySecurityToken = 0x01
-
-	// fedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the
-	// login sequence using the server SPN and STS URL provided by the server during login.
-	fedAuthLibraryADAL = 0x02
-
-	// fedAuthLibraryReserved is used to indicate that no federated authentication scheme applies.
-	fedAuthLibraryReserved = 0x7F
-)
-
-// Federated authentication ADAL workflow affects the mechanism used to authenticate.
-const (
-	// fedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory
-	fedAuthADALWorkflowPassword = 0x01
-
-	// fedAuthADALWorkflowPassword uses the Windows identity to obtain a token from Active Directory
-	fedAuthADALWorkflowIntegrated = 0x02
-
-	// fedAuthADALWorkflowMSI uses the managed identity service to obtain a token
-	fedAuthADALWorkflowMSI = 0x03
-)
-
-// newSecurityTokenConnector creates a new connector from a DSN and a token provider.
-// When invoked, token provider implementations should contact the security token
-// service specified and obtain the appropriate token, or return an error
-// to indicate why a token is not available.
-// The returned connector may be used with sql.OpenDB.
-func newSecurityTokenConnector(dsn string, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) {
-	if tokenProvider == nil {
-		return nil, errors.New("mssql: tokenProvider cannot be nil")
-	}
-
-	conn, err := NewConnector(dsn)
-	if err != nil {
-		return nil, err
-	}
-
-	conn.params.fedAuthLibrary = fedAuthLibrarySecurityToken
-	conn.securityTokenProvider = tokenProvider
-
-	return conn, nil
-}
-
-// newADALTokenConnector creates a new connector from a DSN and a Active Directory token provider.
-// Token provider implementations are called during federated
-// authentication login sequences where the server provides a service
-// principal name and security token service endpoint that should be used
-// to obtain the token. Implementations should contact the security token
-// service specified and obtain the appropriate token, or return an error
-// to indicate why a token is not available.
-//
-// The returned connector may be used with sql.OpenDB.
-func newActiveDirectoryTokenConnector(dsn string, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) {
-	if tokenProvider == nil {
-		return nil, errors.New("mssql: tokenProvider cannot be nil")
-	}
-
-	conn, err := NewConnector(dsn)
-	if err != nil {
-		return nil, err
-	}
-
-	conn.params.fedAuthLibrary = fedAuthLibraryADAL
-	conn.params.fedAuthADALWorkflow = adalWorkflow
-	conn.adalTokenProvider = tokenProvider
-
-	return conn, nil
-}
+package mssql
+
+import (
+	"context"
+	"errors"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
+)
+
+// Federated authentication library affects the login data structure and message sequence.
+const (
+	// fedAuthLibraryLiveIDCompactToken specifies the Microsoft Live ID Compact Token authentication scheme
+	fedAuthLibraryLiveIDCompactToken = 0x00
+
+	// fedAuthLibrarySecurityToken specifies a token-based authentication where the token is available
+	// without additional information provided during the login sequence.
+	fedAuthLibrarySecurityToken = 0x01
+
+	// fedAuthLibraryADAL specifies a token-based authentication where a token is obtained during the
+	// login sequence using the server SPN and STS URL provided by the server during login.
+	fedAuthLibraryADAL = 0x02
+
+	// fedAuthLibraryReserved is used to indicate that no federated authentication scheme applies.
+	fedAuthLibraryReserved = 0x7F
+)
+
+// Federated authentication ADAL workflow affects the mechanism used to authenticate.
+const (
+	// fedAuthADALWorkflowPassword uses a username/password to obtain a token from Active Directory
+	fedAuthADALWorkflowPassword = 0x01
+
+	// fedAuthADALWorkflowPassword uses the Windows identity to obtain a token from Active Directory
+	fedAuthADALWorkflowIntegrated = 0x02
+
+	// fedAuthADALWorkflowMSI uses the managed identity service to obtain a token
+	fedAuthADALWorkflowMSI = 0x03
+)
+
+// newSecurityTokenConnector creates a new connector from a Config and a token provider.
+// When invoked, token provider implementations should contact the security token
+// service specified and obtain the appropriate token, or return an error
+// to indicate why a token is not available.
+// The returned connector may be used with sql.OpenDB.
+func newSecurityTokenConnector(config msdsn.Config, tokenProvider func(ctx context.Context) (string, error)) (*Connector, error) {
+	if tokenProvider == nil {
+		return nil, errors.New("mssql: tokenProvider cannot be nil")
+	}
+
+	conn := NewConnectorConfig(config)
+	conn.fedAuthRequired = true
+	conn.fedAuthLibrary = fedAuthLibrarySecurityToken
+	conn.securityTokenProvider = tokenProvider
+
+	return conn, nil
+}
+
+// newADALTokenConnector creates a new connector from a Config and a Active Directory token provider.
+// Token provider implementations are called during federated
+// authentication login sequences where the server provides a service
+// principal name and security token service endpoint that should be used
+// to obtain the token. Implementations should contact the security token
+// service specified and obtain the appropriate token, or return an error
+// to indicate why a token is not available.
+//
+// The returned connector may be used with sql.OpenDB.
+func newActiveDirectoryTokenConnector(config msdsn.Config, adalWorkflow byte, tokenProvider func(ctx context.Context, serverSPN, stsURL string) (string, error)) (*Connector, error) {
+	if tokenProvider == nil {
+		return nil, errors.New("mssql: tokenProvider cannot be nil")
+	}
+
+	conn := NewConnectorConfig(config)
+	conn.fedAuthRequired = true
+	conn.fedAuthLibrary = fedAuthLibraryADAL
+	conn.fedAuthADALWorkflow = adalWorkflow
+	conn.adalTokenProvider = tokenProvider
+
+	return conn, nil
+}
diff --git a/conn_str.go b/msdsn/conn_str.go
similarity index 65%
rename from conn_str.go
rename to msdsn/conn_str.go
index fb1aacc..9a141e3 100644
--- a/conn_str.go
+++ b/msdsn/conn_str.go
@@ -1,7 +1,10 @@
-package mssql
+package msdsn
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"fmt"
+	"io/ioutil"
 	"net"
 	"net/url"
 	"os"
@@ -11,55 +14,100 @@ import (
 	"unicode"
 )
 
-const defaultServerPort = 1433
-
-type connectParams struct {
-	logFlags                  uint64
-	port                      uint64
-	host                      string
-	instance                  string
-	database                  string
-	user                      string
-	password                  string
-	dial_timeout              time.Duration
-	conn_timeout              time.Duration
-	keepAlive                 time.Duration
-	encrypt                   bool
-	disableEncryption         bool
-	trustServerCertificate    bool
-	certificate               string
-	hostInCertificate         string
-	hostInCertificateProvided bool
-	serverSPN                 string
-	workstation               string
-	appname                   string
-	typeFlags                 uint8
-	failOverPartner           string
-	failOverPort              uint64
-	packetSize                uint16
-	fedAuthLibrary            int
-	fedAuthADALWorkflow       byte
-}
+type (
+	Encryption int
+	Log        uint64
+)
+
+const (
+	EncryptionOff      = 0
+	EncryptionRequired = 1
+	EncryptionDisabled = 3
+)
+
+const (
+	LogErrors      Log = 1
+	LogMessages    Log = 2
+	LogRows        Log = 4
+	LogSQL         Log = 8
+	LogParams      Log = 16
+	LogTransaction Log = 32
+	LogDebug       Log = 64
+)
+
+type Config struct {
+	Port       uint64
+	Host       string
+	Instance   string
+	Database   string
+	User       string
+	Password   string
+	Encryption Encryption
+	TLSConfig  *tls.Config
+
+	FailOverPartner string
+	FailOverPort    uint64
+
+	// If true the TLSConfig servername should use the routed server.
+	HostInCertificateProvided bool
 
-// default packet size for TDS buffer
-const defaultPacketSize = 4096
+	// Read Only intent for application database.
+	// NOTE: This does not make queries to most databases read-only.
+	ReadOnlyIntent bool
 
-func parseConnectParams(dsn string) (connectParams, error) {
-	p := connectParams{
-		fedAuthLibrary: fedAuthLibraryReserved,
+	LogFlags Log
+
+	ServerSPN   string
+	Workstation string
+	AppName     string
+
+	// Do not use the following.
+
+	DialTimeout time.Duration // DialTimeout defaults to 15s. Set negative to disable.
+	ConnTimeout time.Duration // Use context for timeouts.
+	KeepAlive   time.Duration // Leave at default.
+	PacketSize  uint16
+}
+
+func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string) (*tls.Config, error) {
+	var config tls.Config
+	if certificate != "" {
+		pem, err := ioutil.ReadFile(certificate)
+		if err != nil {
+			return nil, fmt.Errorf("cannot read certificate %q: %v", certificate, err)
+		}
+		certs := x509.NewCertPool()
+		certs.AppendCertsFromPEM(pem)
+		config.RootCAs = certs
 	}
+	if insecureSkipVerify {
+		config.InsecureSkipVerify = true
+	}
+	config.ServerName = hostInCertificate
+
+	// fix for https://github.com/denisenkom/go-mssqldb/issues/166
+	// Go implementation of TLS payload size heuristic algorithm splits single TDS package to multiple TCP segments,
+	// while SQL Server seems to expect one TCP segment per encrypted TDS package.
+	// Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package
+	config.DynamicRecordSizingDisabled = true
+
+	return &config, nil
+}
+
+func Parse(dsn string) (Config, map[string]string, error) {
+	p := Config{}
 
 	var params map[string]string
 	if strings.HasPrefix(dsn, "odbc:") {
 		parameters, err := splitConnectionStringOdbc(dsn[len("odbc:"):])
 		if err != nil {
-			return p, err
+			return p, params, err
 		}
 		params = parameters
 	} else if strings.HasPrefix(dsn, "sqlserver://") {
 		parameters, err := splitConnectionStringURL(dsn)
 		if err != nil {
-			return p, err
+			return p, params, err
 		}
 		params = parameters
 	} else {
@@ -68,56 +116,55 @@ func parseConnectParams(dsn string) (connectParams, error) {
 
 	strlog, ok := params["log"]
 	if ok {
-		var err error
-		p.logFlags, err = strconv.ParseUint(strlog, 10, 64)
+		flags, err := strconv.ParseUint(strlog, 10, 64)
 		if err != nil {
-			return p, fmt.Errorf("invalid log parameter '%s': %s", strlog, err.Error())
+			return p, params, fmt.Errorf("invalid log parameter '%s': %s", strlog, err.Error())
 		}
+		p.LogFlags = Log(flags)
 	}
 	server := params["server"]
 	parts := strings.SplitN(server, `\`, 2)
-	p.host = parts[0]
-	if p.host == "." || strings.ToUpper(p.host) == "(LOCAL)" || p.host == "" {
-		p.host = "localhost"
+	p.Host = parts[0]
+	if p.Host == "." || strings.ToUpper(p.Host) == "(LOCAL)" || p.Host == "" {
+		p.Host = "localhost"
 	}
 	if len(parts) > 1 {
-		p.instance = parts[1]
+		p.Instance = parts[1]
 	}
-	p.database = params["database"]
-	p.user = params["user id"]
-	p.password = params["password"]
+	p.Database = params["database"]
+	p.User = params["user id"]
+	p.Password = params["password"]
 
-	p.port = 0
+	p.Port = 0
 	strport, ok := params["port"]
 	if ok {
 		var err error
-		p.port, err = strconv.ParseUint(strport, 10, 16)
+		p.Port, err = strconv.ParseUint(strport, 10, 16)
 		if err != nil {
 			f := "invalid tcp port '%v': %v"
-			return p, fmt.Errorf(f, strport, err.Error())
+			return p, params, fmt.Errorf(f, strport, err.Error())
 		}
 	}
 
-	// https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option
-	p.packetSize = defaultPacketSize
+	// https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option\
 	strpsize, ok := params["packet size"]
 	if ok {
 		var err error
 		psize, err := strconv.ParseUint(strpsize, 0, 16)
 		if err != nil {
 			f := "invalid packet size '%v': %v"
-			return p, fmt.Errorf(f, strpsize, err.Error())
+			return p, params, fmt.Errorf(f, strpsize, err.Error())
 		}
 
 		// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
 		// NOTE: Encrypted connections have a maximum size of 16383 bytes.  If you request
 		// a higher packet size, the server will respond with an ENVCHANGE request to
 		// alter the packet size to 16383 bytes.
-		p.packetSize = uint16(psize)
-		if p.packetSize < 512 {
-			p.packetSize = 512
-		} else if p.packetSize > 32767 {
-			p.packetSize = 32767
+		p.PacketSize = uint16(psize)
+		if p.PacketSize < 512 {
+			p.PacketSize = 512
+		} else if p.PacketSize > 32767 {
+			p.PacketSize = 32767
 		}
 	}
 
@@ -129,78 +176,94 @@ func parseConnectParams(dsn string) (connectParams, error) {
 		timeout, err := strconv.ParseUint(strconntimeout, 10, 64)
 		if err != nil {
 			f := "invalid connection timeout '%v': %v"
-			return p, fmt.Errorf(f, strconntimeout, err.Error())
+			return p, params, fmt.Errorf(f, strconntimeout, err.Error())
 		}
-		p.conn_timeout = time.Duration(timeout) * time.Second
+		p.ConnTimeout = time.Duration(timeout) * time.Second
 	}
-	p.dial_timeout = 15 * time.Second
+	p.DialTimeout = 15 * time.Second
 	if strdialtimeout, ok := params["dial timeout"]; ok {
 		timeout, err := strconv.ParseUint(strdialtimeout, 10, 64)
 		if err != nil {
 			f := "invalid dial timeout '%v': %v"
-			return p, fmt.Errorf(f, strdialtimeout, err.Error())
+			return p, params, fmt.Errorf(f, strdialtimeout, err.Error())
 		}
-		p.dial_timeout = time.Duration(timeout) * time.Second
+		p.DialTimeout = time.Duration(timeout) * time.Second
 	}
 
 	// default keep alive should be 30 seconds according to spec:
 	// https://msdn.microsoft.com/en-us/library/dd341108.aspx
-	p.keepAlive = 30 * time.Second
+	p.KeepAlive = 30 * time.Second
 	if keepAlive, ok := params["keepalive"]; ok {
 		timeout, err := strconv.ParseUint(keepAlive, 10, 64)
 		if err != nil {
 			f := "invalid keepAlive value '%s': %s"
-			return p, fmt.Errorf(f, keepAlive, err.Error())
+			return p, params, fmt.Errorf(f, keepAlive, err.Error())
 		}
-		p.keepAlive = time.Duration(timeout) * time.Second
+		p.KeepAlive = time.Duration(timeout) * time.Second
 	}
+
+	var (
+		trustServerCert   = false
+		certificate       = ""
+		hostInCertificate = ""
+	)
 	encrypt, ok := params["encrypt"]
 	if ok {
 		if strings.EqualFold(encrypt, "DISABLE") {
-			p.disableEncryption = true
+			p.Encryption = EncryptionDisabled
 		} else {
-			var err error
-			p.encrypt, err = strconv.ParseBool(encrypt)
+			e, err := strconv.ParseBool(encrypt)
 			if err != nil {
 				f := "invalid encrypt '%s': %s"
-				return p, fmt.Errorf(f, encrypt, err.Error())
+				return p, params, fmt.Errorf(f, encrypt, err.Error())
+			}
+			if e {
+				p.Encryption = EncryptionRequired
 			}
 		}
 	} else {
-		p.trustServerCertificate = true
+		trustServerCert = true
 	}
 	trust, ok := params["trustservercertificate"]
 	if ok {
 		var err error
-		p.trustServerCertificate, err = strconv.ParseBool(trust)
+		trustServerCert, err = strconv.ParseBool(trust)
 		if err != nil {
 			f := "invalid trust server certificate '%s': %s"
-			return p, fmt.Errorf(f, trust, err.Error())
+			return p, params, fmt.Errorf(f, trust, err.Error())
 		}
 	}
-	p.certificate = params["certificate"]
-	p.hostInCertificate, ok = params["hostnameincertificate"]
+	certificate = params["certificate"]
+	hostInCertificate, ok = params["hostnameincertificate"]
 	if ok {
-		p.hostInCertificateProvided = true
+		p.HostInCertificateProvided = true
 	} else {
-		p.hostInCertificate = p.host
-		p.hostInCertificateProvided = false
+		hostInCertificate = p.Host
+		p.HostInCertificateProvided = false
+	}
+
+	if p.Encryption != EncryptionDisabled {
+		var err error
+		p.TLSConfig, err = SetupTLS(certificate, trustServerCert, hostInCertificate)
+		if err != nil {
+			return p, params, fmt.Errorf("failed to setup TLS: %w", err)
+		}
 	}
 
 	serverSPN, ok := params["serverspn"]
 	if ok {
-		p.serverSPN = serverSPN
+		p.ServerSPN = serverSPN
 	} else {
-		p.serverSPN = generateSpn(p.host, resolveServerPort(p.port))
+		p.ServerSPN = generateSpn(p.Host, p.Port)
 	}
 
 	workstation, ok := params["workstation id"]
 	if ok {
-		p.workstation = workstation
+		p.Workstation = workstation
 	} else {
 		workstation, err := os.Hostname()
 		if err == nil {
-			p.workstation = workstation
+			p.Workstation = workstation
 		}
 	}
 
@@ -208,53 +271,57 @@ func parseConnectParams(dsn string) (connectParams, error) {
 	if !ok {
 		appname = "go-mssqldb"
 	}
-	p.appname = appname
+	p.AppName = appname
 
 	appintent, ok := params["applicationintent"]
 	if ok {
 		if appintent == "ReadOnly" {
-			if p.database == "" {
-				return p, fmt.Errorf("database must be specified when ApplicationIntent is ReadOnly")
+			if p.Database == "" {
+				return p, params, fmt.Errorf("database must be specified when ApplicationIntent is ReadOnly")
 			}
-			p.typeFlags |= fReadOnlyIntent
+			p.ReadOnlyIntent = true
 		}
 	}
 
 	failOverPartner, ok := params["failoverpartner"]
 	if ok {
-		p.failOverPartner = failOverPartner
+		p.FailOverPartner = failOverPartner
 	}
 
 	failOverPort, ok := params["failoverport"]
 	if ok {
 		var err error
-		p.failOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
+		p.FailOverPort, err = strconv.ParseUint(failOverPort, 0, 16)
 		if err != nil {
 			f := "invalid tcp port '%v': %v"
-			return p, fmt.Errorf(f, failOverPort, err.Error())
+			return p, params, fmt.Errorf(f, failOverPort, err.Error())
 		}
 	}
 
-	return p, nil
+	return p, params, nil
 }
 
 // convert connectionParams to url style connection string
 // used mostly for testing
-func (p connectParams) toUrl() *url.URL {
+func (p Config) URL() *url.URL {
 	q := url.Values{}
-	if p.database != "" {
-		q.Add("database", p.database)
+	if p.Database != "" {
+		q.Add("database", p.Database)
+	}
+	if p.LogFlags != 0 {
+		q.Add("log", strconv.FormatUint(uint64(p.LogFlags), 10))
 	}
-	if p.logFlags != 0 {
-		q.Add("log", strconv.FormatUint(p.logFlags, 10))
+	host := p.Host
+	if p.Port > 0 {
+		host = fmt.Sprintf("%s:%d", p.Host, p.Port)
 	}
 	res := url.URL{
 		Scheme: "sqlserver",
-		Host:   p.host,
-		User:   url.UserPassword(p.user, p.password),
+		Host:   host,
+		User:   url.UserPassword(p.User, p.Password),
 	}
-	if p.instance != "" {
-		res.Path = p.instance
+	if p.Instance != "" {
+		res.Path = p.Instance
 	}
 	if len(q) > 0 {
 		res.RawQuery = q.Encode()
@@ -512,14 +579,6 @@ func normalizeOdbcKey(s string) string {
 	return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace))
 }
 
-func resolveServerPort(port uint64) uint64 {
-	if port == 0 {
-		return defaultServerPort
-	}
-
-	return port
-}
-
 func generateSpn(host string, port uint64) string {
 	return fmt.Sprintf("MSSQLSvc/%s:%d", host, port)
 }
diff --git a/mssql.go b/mssql.go
index 579d00f..237b1b8 100644
--- a/mssql.go
+++ b/mssql.go
@@ -16,6 +16,7 @@ import (
 	"unicode"
 
 	"github.com/denisenkom/go-mssqldb/internal/querytext"
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 // ReturnStatus may be used to return the return value from a proc.
@@ -31,12 +32,16 @@ var driverInstanceNoProcess = &Driver{processQueryText: false}
 func init() {
 	sql.Register("mssql", driverInstance)
 	sql.Register("sqlserver", driverInstanceNoProcess)
-	createDialer = func(p *connectParams) Dialer {
-		return netDialer{&net.Dialer{KeepAlive: p.keepAlive}}
+	createDialer = func(p *msdsn.Config) Dialer {
+		ka := p.KeepAlive
+		if ka == 0 {
+			ka = 30 * time.Second
+		}
+		return netDialer{&net.Dialer{KeepAlive: ka}}
 	}
 }
 
-var createDialer func(p *connectParams) Dialer
+var createDialer func(p *msdsn.Config) Dialer
 
 type netDialer struct {
 	nd *net.Dialer
@@ -54,7 +59,7 @@ type Driver struct {
 
 // OpenConnector opens a new connector. Useful to dial with a context.
 func (d *Driver) OpenConnector(dsn string) (*Connector, error) {
-	params, err := parseConnectParams(dsn)
+	params, _, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -81,7 +86,7 @@ func (d *Driver) SetLogger(logger Logger) {
 // NewConnector creates a new connector from a DSN.
 // The returned connector may be used with sql.OpenDB.
 func NewConnector(dsn string) (*Connector, error) {
-	params, err := parseConnectParams(dsn)
+	params, _, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
@@ -92,15 +97,28 @@ func NewConnector(dsn string) (*Connector, error) {
 	return c, nil
 }
 
+// NewConnectorConfig creates a new Connector for a DSN Config struct.
+// The returned connector may be used with sql.OpenDB.
+func NewConnectorConfig(config msdsn.Config) *Connector {
+	return &Connector{
+		params: config,
+		driver: driverInstanceNoProcess,
+	}
+}
+
 // Connector holds the parsed DSN and is ready to make a new connection
 // at any time.
 //
 // In the future, settings that cannot be passed through a string DSN
 // may be set directly on the connector.
 type Connector struct {
-	params connectParams
+	params msdsn.Config
 	driver *Driver
 
+	fedAuthRequired     bool
+	fedAuthLibrary      int
+	fedAuthADALWorkflow byte
+
 	// callback that can provide a security token during login
 	securityTokenProvider func(ctx context.Context) (string, error)
 
@@ -139,7 +157,7 @@ type Dialer interface {
 	DialContext(ctx context.Context, network string, addr string) (net.Conn, error)
 }
 
-func (c *Connector) getDialer(p *connectParams) Dialer {
+func (c *Connector) getDialer(p *msdsn.Config) Dialer {
 	if c != nil && c.Dialer != nil {
 		return c.Dialer
 	}
@@ -317,25 +335,26 @@ func (c *Conn) processBeginResponse(ctx context.Context) (driver.Tx, error) {
 }
 
 func (d *Driver) open(ctx context.Context, dsn string) (*Conn, error) {
-	params, err := parseConnectParams(dsn)
+	params, _, err := msdsn.Parse(dsn)
 	if err != nil {
 		return nil, err
 	}
-	return d.connect(ctx, nil, params)
+	c := &Connector{params: params}
+	return d.connect(ctx, c, params)
 }
 
 // connect to the server, using the provided context for dialing only.
-func (d *Driver) connect(ctx context.Context, c *Connector, params connectParams) (*Conn, error) {
+func (d *Driver) connect(ctx context.Context, c *Connector, params msdsn.Config) (*Conn, error) {
 	sess, err := connect(ctx, c, d.log, params)
 	if err != nil {
 		// main server failed, try fail-over partner
-		if params.failOverPartner == "" {
+		if params.FailOverPartner == "" {
 			return nil, err
 		}
 
-		params.host = params.failOverPartner
-		if params.failOverPort != 0 {
-			params.port = params.failOverPort
+		params.Host = params.FailOverPartner
+		if params.FailOverPort != 0 {
+			params.Port = params.FailOverPort
 		}
 
 		sess, err = connect(ctx, c, d.log, params)
diff --git a/tds.go b/tds.go
index 041b881..563e2e6 100644
--- a/tds.go
+++ b/tds.go
@@ -3,7 +3,6 @@ package mssql
 import (
 	"context"
 	"crypto/tls"
-	"crypto/x509"
 	"encoding/binary"
 	"errors"
 	"fmt"
@@ -13,8 +12,11 @@ import (
 	"sort"
 	"strconv"
 	"strings"
+	"time"
 	"unicode/utf16"
 	"unicode/utf8"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 func parseInstances(msg []byte) map[string]map[string]string {
@@ -145,13 +147,21 @@ type tdsSession struct {
 }
 
 const (
-	logErrors      = 1
-	logMessages    = 2
-	logRows        = 4
-	logSQL         = 8
-	logParams      = 16
-	logTransaction = 32
-	logDebug       = 64
+	// Default packet size for a TDS buffer.
+	defaultPacketSize = 4096
+
+	// Default port if no port given.
+	defaultServerPort = 1433
+)
+
+const (
+	logErrors      = uint64(msdsn.LogErrors)
+	logMessages    = uint64(msdsn.LogMessages)
+	logRows        = uint64(msdsn.LogRows)
+	logSQL         = uint64(msdsn.LogSQL)
+	logParams      = uint64(msdsn.LogParams)
+	logTransaction = uint64(msdsn.LogTransaction)
+	logDebug       = uint64(msdsn.LogDebug)
 )
 
 type columnStruct struct {
@@ -844,11 +854,11 @@ type auth interface {
 // SQL Server AlwaysOn Availability Group Listeners are bound by DNS to a
 // list of IP addresses.  So if there is more than one, try them all and
 // use the first one that allows a connection.
-func dialConnection(ctx context.Context, c *Connector, p connectParams) (conn net.Conn, err error) {
+func dialConnection(ctx context.Context, c *Connector, p msdsn.Config) (conn net.Conn, err error) {
 	var ips []net.IP
-	ip := net.ParseIP(p.host)
+	ip := net.ParseIP(p.Host)
 	if ip == nil {
-		ips, err = net.LookupIP(p.host)
+		ips, err = net.LookupIP(p.Host)
 		if err != nil {
 			return
 		}
@@ -857,14 +867,14 @@ func dialConnection(ctx context.Context, c *Connector, p connectParams) (conn ne
 	}
 	if len(ips) == 1 {
 		d := c.getDialer(&p)
-		addr := net.JoinHostPort(ips[0].String(), strconv.Itoa(int(resolveServerPort(p.port))))
+		addr := net.JoinHostPort(ips[0].String(), strconv.Itoa(int(resolveServerPort(p.Port))))
 		conn, err = d.DialContext(ctx, "tcp", addr)
 
 	} else {
 		//Try Dials in parallel to avoid waiting for timeouts.
 		connChan := make(chan net.Conn, len(ips))
 		errChan := make(chan error, len(ips))
-		portStr := strconv.Itoa(int(resolveServerPort(p.port)))
+		portStr := strconv.Itoa(int(resolveServerPort(p.Port)))
 		for _, ip := range ips {
 			go func(ip net.IP) {
 				d := c.getDialer(&p)
@@ -902,21 +912,24 @@ func dialConnection(ctx context.Context, c *Connector, p connectParams) (conn ne
 	// Can't do the usual err != nil check, as it is possible to have gotten an error before a successful connection
 	if conn == nil {
 		f := "unable to open tcp connection with host '%v:%v': %v"
-		return nil, fmt.Errorf(f, p.host, resolveServerPort(p.port), err.Error())
+		return nil, fmt.Errorf(f, p.Host, resolveServerPort(p.Port), err.Error())
 	}
 	return conn, err
 }
 
-func preparePreloginFields(p connectParams, fe *featureExtFedAuth) map[uint8][]byte {
-	instance_buf := []byte(p.instance)
+func preparePreloginFields(p msdsn.Config, fe *featureExtFedAuth) map[uint8][]byte {
+	instance_buf := []byte(p.Instance)
 	instance_buf = append(instance_buf, 0) // zero terminate instance name
 
 	var encrypt byte
-	if p.disableEncryption {
+	switch p.Encryption {
+	default:
+		panic(fmt.Errorf("Unsupported Encryption Config %v", p.Encryption))
+	case msdsn.EncryptionDisabled:
 		encrypt = encryptNotSup
-	} else if p.encrypt {
+	case msdsn.EncryptionRequired:
 		encrypt = encryptOn
-	} else {
+	case msdsn.EncryptionOff:
 		encrypt = encryptOff
 	}
 
@@ -935,7 +948,7 @@ func preparePreloginFields(p connectParams, fe *featureExtFedAuth) map[uint8][]b
 	return fields
 }
 
-func interpretPreloginResponse(p connectParams, fe *featureExtFedAuth, fields map[uint8][]byte) (encrypt byte, err error) {
+func interpretPreloginResponse(p msdsn.Config, fe *featureExtFedAuth, fields map[uint8][]byte) (encrypt byte, err error) {
 	// If the server returns the preloginFEDAUTHREQUIRED field, then federated authentication
 	// is supported. The actual value may be 0 or 1, where 0 means either SSPI or federated
 	// authentication is allowed, while 1 means only federated authentication is allowed.
@@ -955,33 +968,37 @@ func interpretPreloginResponse(p connectParams, fe *featureExtFedAuth, fields ma
 		return 0, fmt.Errorf("encrypt negotiation failed")
 	}
 	encrypt = encryptBytes[0]
-	if p.encrypt && (encrypt == encryptNotSup || encrypt == encryptOff) {
+	if p.Encryption == msdsn.EncryptionRequired && (encrypt == encryptNotSup || encrypt == encryptOff) {
 		return 0, fmt.Errorf("server does not support encryption")
 	}
 
 	return
 }
 
-func prepareLogin(ctx context.Context, c *Connector, p connectParams, log optionalLogger, auth auth, fe *featureExtFedAuth, packetSize uint32) (l *login, err error) {
+func prepareLogin(ctx context.Context, c *Connector, p msdsn.Config, log optionalLogger, auth auth, fe *featureExtFedAuth, packetSize uint32) (l *login, err error) {
+	var typeFlags uint8
+	if p.ReadOnlyIntent {
+		typeFlags |= fReadOnlyIntent
+	}
 	l = &login{
 		TDSVersion:   verTDS74,
 		PacketSize:   packetSize,
-		Database:     p.database,
+		Database:     p.Database,
 		OptionFlags2: fODBC, // to get unlimited TEXTSIZE
-		HostName:     p.workstation,
-		ServerName:   p.host,
-		AppName:      p.appname,
-		TypeFlags:    p.typeFlags,
+		HostName:     p.Workstation,
+		ServerName:   p.Host,
+		AppName:      p.AppName,
+		TypeFlags:    typeFlags,
 	}
 	switch {
 	case fe.FedAuthLibrary == fedAuthLibrarySecurityToken:
-		if p.logFlags&logDebug != 0 {
+		if uint64(p.LogFlags)&logDebug != 0 {
 			log.Println("Starting federated authentication using security token")
 		}
 
 		fe.FedAuthToken, err = c.securityTokenProvider(ctx)
 		if err != nil {
-			if p.logFlags&logDebug != 0 {
+			if uint64(p.LogFlags)&logDebug != 0 {
 				log.Printf("Failed to retrieve service principal token for federated authentication security token library: %v", err)
 			}
 			return nil, err
@@ -990,14 +1007,14 @@ func prepareLogin(ctx context.Context, c *Connector, p connectParams, log option
 		l.FeatureExt.Add(fe)
 
 	case fe.FedAuthLibrary == fedAuthLibraryADAL:
-		if p.logFlags&logDebug != 0 {
+		if uint64(p.LogFlags)&logDebug != 0 {
 			log.Println("Starting federated authentication using ADAL")
 		}
 
 		l.FeatureExt.Add(fe)
 
 	case auth != nil:
-		if p.logFlags&logDebug != 0 {
+		if uint64(p.LogFlags)&logDebug != 0 {
 			log.Println("Starting SSPI login")
 		}
 
@@ -1011,46 +1028,67 @@ func prepareLogin(ctx context.Context, c *Connector, p connectParams, log option
 
 	default:
 		// Default to SQL server authentication with user and password
-		l.UserName = p.user
-		l.Password = p.password
+		l.UserName = p.User
+		l.Password = p.Password
 	}
 
 	return l, nil
 }
 
-func connect(ctx context.Context, c *Connector, log optionalLogger, p connectParams) (res *tdsSession, err error) {
+func connect(ctx context.Context, c *Connector, log optionalLogger, p msdsn.Config) (res *tdsSession, err error) {
 	dialCtx := ctx
-	if p.dial_timeout > 0 {
+	if p.DialTimeout >= 0 {
+		dt := p.DialTimeout
+		if dt == 0 {
+			dt = 15 * time.Second
+		}
 		var cancel func()
-		dialCtx, cancel = context.WithTimeout(ctx, p.dial_timeout)
+		dialCtx, cancel = context.WithTimeout(ctx, dt)
 		defer cancel()
 	}
 	// if instance is specified use instance resolution service
-	if p.instance != "" && p.port != 0 {
+	if len(p.Instance) > 0 && p.Port != 0 {
 		// both instance name and port specified
 		// when port is specified instance name is not used
 		// you should not provide instance name when you provide port
 		log.Println("WARN: You specified both instance name and port in the connection string, port will be used and instance name will be ignored")
 	}
-	if p.instance != "" && p.port == 0 {
-		p.instance = strings.ToUpper(p.instance)
+	if len(p.Instance) > 0 {
+		p.Instance = strings.ToUpper(p.Instance)
 		d := c.getDialer(&p)
-		instances, err := getInstances(dialCtx, d, p.host)
+		instances, err := getInstances(dialCtx, d, p.Host)
 		if err != nil {
 			f := "unable to get instances from Sql Server Browser on host %v: %v"
-			return nil, fmt.Errorf(f, p.host, err.Error())
+			return nil, fmt.Errorf(f, p.Host, err.Error())
 		}
-		strport, ok := instances[p.instance]["tcp"]
+		strport, ok := instances[p.Instance]["tcp"]
 		if !ok {
 			f := "no instance matching '%v' returned from host '%v'"
-			return nil, fmt.Errorf(f, p.instance, p.host)
+			return nil, fmt.Errorf(f, p.Instance, p.Host)
 		}
 		port, err := strconv.ParseUint(strport, 0, 16)
 		if err != nil {
 			f := "invalid tcp port returned from Sql Server Browser '%v': %v"
 			return nil, fmt.Errorf(f, strport, err.Error())
 		}
-		p.port = port
+		p.Port = port
+	}
+	if p.Port == 0 {
+		p.Port = defaultServerPort
+	}
+
+	packetSize := p.PacketSize
+	if packetSize == 0 {
+		packetSize = defaultPacketSize
+	}
+	// Ensure packet size falls within the TDS protocol range of 512 to 32767 bytes
+	// NOTE: Encrypted connections have a maximum size of 16383 bytes.  If you request
+	// a higher packet size, the server will respond with an ENVCHANGE request to
+	// alter the packet size to 16383 bytes.
+	if packetSize < 512 {
+		packetSize = 512
+	} else if packetSize > 32767 {
+		packetSize = 32767
 	}
 
 initiate_connection:
@@ -1059,18 +1097,21 @@ initiate_connection:
 		return nil, err
 	}
 
-	toconn := newTimeoutConn(conn, p.conn_timeout)
+	toconn := newTimeoutConn(conn, p.ConnTimeout)
 
-	outbuf := newTdsBuffer(p.packetSize, toconn)
+	outbuf := newTdsBuffer(packetSize, toconn)
 	sess := tdsSession{
 		buf:      outbuf,
 		log:      log,
-		logFlags: p.logFlags,
+		logFlags: uint64(p.LogFlags),
 	}
 
 	fedAuth := &featureExtFedAuth{
-		FedAuthLibrary: p.fedAuthLibrary,
-		ADALWorkflow:   p.fedAuthADALWorkflow,
+		FedAuthLibrary: fedAuthLibraryReserved,
+	}
+	if c.fedAuthRequired {
+		fedAuth.FedAuthLibrary = c.fedAuthLibrary
+		fedAuth.ADALWorkflow = c.fedAuthADALWorkflow
 	}
 
 	fields := preparePreloginFields(p, fedAuth)
@@ -1091,29 +1132,30 @@ initiate_connection:
 	}
 
 	if encrypt != encryptNotSup {
-		var config tls.Config
-		if p.certificate != "" {
-			pem, err := ioutil.ReadFile(p.certificate)
-			if err != nil {
-				return nil, fmt.Errorf("cannot read certificate %q: %v", p.certificate, err)
+		var config *tls.Config
+		if pc := p.TLSConfig; pc != nil {
+			config = pc
+			if config.DynamicRecordSizingDisabled == false {
+				config = config.Clone()
+
+				// fix for https://github.com/denisenkom/go-mssqldb/issues/166
+				// Go implementation of TLS payload size heuristic algorithm splits single TDS package to multiple TCP segments,
+				// while SQL Server seems to expect one TCP segment per encrypted TDS package.
+				// Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package
+				config.DynamicRecordSizingDisabled = true
 			}
-			certs := x509.NewCertPool()
-			certs.AppendCertsFromPEM(pem)
-			config.RootCAs = certs
 		}
-		if p.trustServerCertificate {
-			config.InsecureSkipVerify = true
+		if config == nil {
+			config, err = msdsn.SetupTLS("", false, p.Host)
+			if err != nil {
+				return nil, err
+			}
 		}
-		config.ServerName = p.hostInCertificate
-		// fix for https://github.com/denisenkom/go-mssqldb/issues/166
-		// Go implementation of TLS payload size heuristic algorithm splits single TDS package to multiple TCP segments,
-		// while SQL Server seems to expect one TCP segment per encrypted TDS package.
-		// Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package
-		config.DynamicRecordSizingDisabled = true
+
 		// setting up connection handler which will allow wrapping of TLS handshake packets inside TDS stream
 		handshakeConn := tlsHandshakeConn{buf: outbuf}
 		passthrough := passthroughConn{c: &handshakeConn}
-		tlsConn := tls.Client(&passthrough, &config)
+		tlsConn := tls.Client(&passthrough, config)
 		err = tlsConn.Handshake()
 		passthrough.c = toconn
 		outbuf.transport = tlsConn
@@ -1127,7 +1169,7 @@ initiate_connection:
 		}
 	}
 
-	auth, authOk := getAuth(p.user, p.password, p.serverSPN, p.workstation)
+	auth, authOk := getAuth(p.User, p.Password, p.ServerSPN, p.Workstation)
 	if authOk {
 		defer auth.Free()
 	} else {
@@ -1217,10 +1259,10 @@ initiate_connection:
 
 	if sess.routedServer != "" {
 		toconn.Close()
-		p.host = sess.routedServer
-		p.port = uint64(sess.routedPort)
-		if !p.hostInCertificateProvided {
-			p.hostInCertificate = sess.routedServer
+		p.Host = sess.routedServer
+		p.Port = uint64(sess.routedPort)
+		if !p.HostInCertificateProvided && p.TLSConfig != nil {
+			p.TLSConfig.ServerName = sess.routedServer
 		}
 		goto initiate_connection
 	}
@@ -1232,3 +1274,11 @@ func (sess *tdsSession) setReturnStatus(status ReturnStatus) {
 		*sess.returnStatus = status
 	}
 }
+
+func resolveServerPort(port uint64) uint64 {
+	if port == 0 {
+		return defaultServerPort
+	}
+
+	return port
+}


Test patch:
diff --git a/accesstokenconnector_test.go b/accesstokenconnector_test.go
index 9bfc0ac..52940fc 100644
--- a/accesstokenconnector_test.go
+++ b/accesstokenconnector_test.go
@@ -35,11 +35,11 @@ func TestNewAccessTokenConnector(t *testing.T) {
 					return fmt.Errorf("Expected driver to be of type *Connector, but got %T", c)
 				}
 				p := tc.params
-				if p.database != "db" {
-					return fmt.Errorf("expected params.database=db, but got %v", p.database)
+				if p.Database != "db" {
+					return fmt.Errorf("expected params.database=db, but got %v", p.Database)
 				}
-				if p.host != "server.database.windows.net" {
-					return fmt.Errorf("expected params.host=server.database.windows.net, but got %v", p.host)
+				if p.Host != "server.database.windows.net" {
+					return fmt.Errorf("expected params.host=server.database.windows.net, but got %v", p.Host)
 				}
 				if tc.securityTokenProvider == nil {
 					return fmt.Errorf("Expected federated authentication provider to not be nil")
diff --git a/bad_server_test.go b/bad_server_test.go
index ec65d20..5ecf411 100644
--- a/bad_server_test.go
+++ b/bad_server_test.go
@@ -56,8 +56,8 @@ func TestBadServerInvalidPreLoginPacketSize(t *testing.T) {
 		// but since there are no more data written, client would fail
 		err := binary.Write(conn, binary.BigEndian, header{
 			PacketType: packReply,
-			Size: uint16(headerSize),
-			Status: 0, // indicates non final packet
+			Size:       uint16(headerSize),
+			Status:     0, // indicates non final packet
 		})
 		if err != nil {
 			t.Fatal("Writing header failed", err)
@@ -69,8 +69,8 @@ func TestBadServerInvalidPreLoginPacketType(t *testing.T) {
 	testBadServer(t, func(conn net.Conn) {
 		err := binary.Write(conn, binary.BigEndian, header{
 			PacketType: packNormal, // invalid packet type, packReply
-			Size: uint16(headerSize),
-			Status: 1, // indicate final packet
+			Size:       uint16(headerSize),
+			Status:     1, // indicate final packet
 		})
 		if err != nil {
 			t.Fatal("Writing header failed", err)
@@ -82,8 +82,8 @@ func TestBadServerEmptyPreLoginPacket(t *testing.T) {
 	testBadServer(t, func(conn net.Conn) {
 		err := binary.Write(conn, binary.BigEndian, header{
 			PacketType: packReply,
-			Size: uint16(headerSize),
-			Status: 1, // indicate final packet
+			Size:       uint16(headerSize),
+			Status:     1, // indicate final packet
 		})
 		if err != nil {
 			t.Fatal("Writing header failed", err)
diff --git a/bulkimport_example_test.go b/bulkimport_example_test.go
index 3379108..54d3dc2 100644
--- a/bulkimport_example_test.go
+++ b/bulkimport_example_test.go
@@ -1,116 +1,116 @@
-// +build go1.10
-
-package mssql_test
-
-import (
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-	"strings"
-	"unicode/utf8"
-
-	"github.com/denisenkom/go-mssqldb"
-)
-
-const (
-	createTestTable = `CREATE TABLE test_table(
-		id int IDENTITY(1,1) NOT NULL,
-		test_nvarchar nvarchar(50) NULL,
-		test_varchar varchar(50) NULL,
-		test_float float NULL,
-		test_datetime2_3 datetime2(3) NULL,
-		test_bitn bit NULL,
-		test_bigint bigint NOT NULL,
-		test_geom geometry NULL,
-	CONSTRAINT PK_table_test_id PRIMARY KEY CLUSTERED
-	(
-		id ASC
-	) ON [PRIMARY]);`
-	dropTestTable = "IF OBJECT_ID('test_table', 'U') IS NOT NULL DROP TABLE test_table;"
-)
-
-// This example shows how to perform bulk imports
-func ExampleCopyIn() {
-	flag.Parse()
-
-	if *debug {
-		fmt.Printf(" password:%s\n", *password)
-		fmt.Printf(" port:%d\n", *port)
-		fmt.Printf(" server:%s\n", *server)
-		fmt.Printf(" user:%s\n", *user)
-	}
-
-	connString := makeConnURL().String()
-	if *debug {
-		fmt.Printf(" connString:%s\n", connString)
-	}
-
-	db, err := sql.Open("sqlserver", connString)
-	if err != nil {
-		log.Fatal("Open connection failed:", err.Error())
-	}
-	defer db.Close()
-
-	txn, err := db.Begin()
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// Create table
-	_, err = db.Exec(createTestTable)
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer db.Exec(dropTestTable)
-
-	// mssqldb.CopyIn creates string to be consumed by Prepare
-	stmt, err := txn.Prepare(mssql.CopyIn("test_table", mssql.BulkOptions{}, "test_varchar", "test_nvarchar", "test_float", "test_bigint"))
-	if err != nil {
-		log.Fatal(err.Error())
-	}
-
-	for i := 0; i < 10; i++ {
-		_, err = stmt.Exec(generateString(0, 30), generateStringUnicode(0, 30), i, i)
-		if err != nil {
-			log.Fatal(err.Error())
-		}
-	}
-
-	result, err := stmt.Exec()
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	err = stmt.Close()
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	err = txn.Commit()
-	if err != nil {
-		log.Fatal(err)
-	}
-	rowCount, _ := result.RowsAffected()
-	log.Printf("%d row copied\n", rowCount)
-	log.Printf("bye\n")
-}
-
-func generateString(x int, n int) string {
-	letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-	b := make([]byte, n)
-	for i := range b {
-		b[i] = letters[(x+i)%len(letters)]
-	}
-	return string(b)
-}
-func generateStringUnicode(x int, n int) string {
-	letters := []byte("ab©💾é?ghïjklmnopqЯ☀tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
-	b := &strings.Builder{}
-	for i := 0; i < n; i++ {
-		r, sz := utf8.DecodeRune(letters[x%len(letters):])
-		x += sz
-		b.WriteRune(r)
-	}
-	return b.String()
-}
+// +build go1.10
+
+package mssql_test
+
+import (
+	"database/sql"
+	"flag"
+	"fmt"
+	"log"
+	"strings"
+	"unicode/utf8"
+
+	mssql "github.com/denisenkom/go-mssqldb"
+)
+
+const (
+	createTestTable = `CREATE TABLE test_table(
+		id int IDENTITY(1,1) NOT NULL,
+		test_nvarchar nvarchar(50) NULL,
+		test_varchar varchar(50) NULL,
+		test_float float NULL,
+		test_datetime2_3 datetime2(3) NULL,
+		test_bitn bit NULL,
+		test_bigint bigint NOT NULL,
+		test_geom geometry NULL,
+	CONSTRAINT PK_table_test_id PRIMARY KEY CLUSTERED
+	(
+		id ASC
+	) ON [PRIMARY]);`
+	dropTestTable = "IF OBJECT_ID('test_table', 'U') IS NOT NULL DROP TABLE test_table;"
+)
+
+// This example shows how to perform bulk imports
+func ExampleCopyIn() {
+	flag.Parse()
+
+	if *debug {
+		fmt.Printf(" password:%s\n", *password)
+		fmt.Printf(" port:%d\n", *port)
+		fmt.Printf(" server:%s\n", *server)
+		fmt.Printf(" user:%s\n", *user)
+	}
+
+	connString := makeConnURL().String()
+	if *debug {
+		fmt.Printf(" connString:%s\n", connString)
+	}
+
+	db, err := sql.Open("sqlserver", connString)
+	if err != nil {
+		log.Fatal("Open connection failed:", err.Error())
+	}
+	defer db.Close()
+
+	txn, err := db.Begin()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Create table
+	_, err = db.Exec(createTestTable)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer db.Exec(dropTestTable)
+
+	// mssqldb.CopyIn creates string to be consumed by Prepare
+	stmt, err := txn.Prepare(mssql.CopyIn("test_table", mssql.BulkOptions{}, "test_varchar", "test_nvarchar", "test_float", "test_bigint"))
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+
+	for i := 0; i < 10; i++ {
+		_, err = stmt.Exec(generateString(0, 30), generateStringUnicode(0, 30), i, i)
+		if err != nil {
+			log.Fatal(err.Error())
+		}
+	}
+
+	result, err := stmt.Exec()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = stmt.Close()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = txn.Commit()
+	if err != nil {
+		log.Fatal(err)
+	}
+	rowCount, _ := result.RowsAffected()
+	log.Printf("%d row copied\n", rowCount)
+	log.Printf("bye\n")
+}
+
+func generateString(x int, n int) string {
+	letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	b := make([]byte, n)
+	for i := range b {
+		b[i] = letters[(x+i)%len(letters)]
+	}
+	return string(b)
+}
+func generateStringUnicode(x int, n int) string {
+	letters := []byte("ab©💾é?ghïjklmnopqЯ☀tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+	b := &strings.Builder{}
+	for i := 0; i < n; i++ {
+		r, sz := utf8.DecodeRune(letters[x%len(letters):])
+		x += sz
+		b.WriteRune(r)
+	}
+	return b.String()
+}
diff --git a/conn_str_test.go b/conn_str_test.go
deleted file mode 100644
index 4c8f3fa..0000000
--- a/conn_str_test.go
+++ /dev/null
@@ -1,244 +0,0 @@
-package mssql
-
-import (
-	"bufio"
-	"io"
-	"os"
-	"reflect"
-	"testing"
-	"time"
-)
-
-func TestInvalidConnectionString(t *testing.T) {
-	connStrings := []string{
-		"log=invalid",
-		"port=invalid",
-		"packet size=invalid",
-		"connection timeout=invalid",
-		"dial timeout=invalid",
-		"keepalive=invalid",
-		"encrypt=invalid",
-		"trustservercertificate=invalid",
-		"failoverport=invalid",
-		"applicationintent=ReadOnly",
-		"server=someserver,notanumber",
-
-		// ODBC mode
-		"odbc:password={",
-		"odbc:password={somepass",
-		"odbc:password={somepass}}",
-		"odbc:password={some}pass",
-		"odbc:=", // unexpected =
-		"odbc: =",
-		"odbc:password={some} a",
-
-		// URL mode
-		"sqlserver://\x00",
-		"sqlserver://host?key=value1&key=value2", // duplicate keys
-	}
-	for _, connStr := range connStrings {
-		_, err := parseConnectParams(connStr)
-		if err == nil {
-			t.Errorf("Connection expected to fail for connection string %s but it didn't", connStr)
-			continue
-		} else {
-			t.Logf("Connection failed for %s as expected with error %v", connStr, err)
-		}
-	}
-}
-
-func TestValidConnectionString(t *testing.T) {
-	type testStruct struct {
-		connStr string
-		check   func(connectParams) bool
-	}
-	connStrings := []testStruct{
-		{"server=server\\instance;database=testdb;user id=tester;password=pwd", func(p connectParams) bool {
-			return p.host == "server" && p.instance == "instance" && p.user == "tester" && p.password == "pwd"
-		}},
-		{"server=.", func(p connectParams) bool { return p.host == "localhost" }},
-		{"server=(local)", func(p connectParams) bool { return p.host == "localhost" }},
-		{"ServerSPN=serverspn;Workstation ID=workstid", func(p connectParams) bool { return p.serverSPN == "serverspn" && p.workstation == "workstid" }},
-		{"failoverpartner=fopartner;failoverport=2000", func(p connectParams) bool { return p.failOverPartner == "fopartner" && p.failOverPort == 2000 }},
-		{"app name=appname;applicationintent=ReadOnly;database=testdb", func(p connectParams) bool { return p.appname == "appname" && (p.typeFlags&fReadOnlyIntent != 0) }},
-		{"encrypt=disable", func(p connectParams) bool { return p.disableEncryption }},
-		{"encrypt=true", func(p connectParams) bool { return p.encrypt && !p.disableEncryption }},
-		{"encrypt=false", func(p connectParams) bool { return !p.encrypt && !p.disableEncryption }},
-		{"trustservercertificate=true", func(p connectParams) bool { return p.trustServerCertificate }},
-		{"trustservercertificate=false", func(p connectParams) bool { return !p.trustServerCertificate }},
-		{"certificate=abc", func(p connectParams) bool { return p.certificate == "abc" }},
-		{"hostnameincertificate=abc", func(p connectParams) bool { return p.hostInCertificate == "abc" }},
-		{"connection timeout=3;dial timeout=4;keepalive=5", func(p connectParams) bool {
-			return p.conn_timeout == 3*time.Second && p.dial_timeout == 4*time.Second && p.keepAlive == 5*time.Second
-		}},
-		{"log=63", func(p connectParams) bool { return p.logFlags == 63 && p.port == 0 }},
-		{"log=63;port=1000", func(p connectParams) bool { return p.logFlags == 63 && p.port == 1000 }},
-		{"log=64", func(p connectParams) bool { return p.logFlags == 64 && p.packetSize == 4096 }},
-		{"log=64;packet size=0", func(p connectParams) bool { return p.logFlags == 64 && p.packetSize == 512 }},
-		{"log=64;packet size=300", func(p connectParams) bool { return p.logFlags == 64 && p.packetSize == 512 }},
-		{"log=64;packet size=8192", func(p connectParams) bool { return p.logFlags == 64 && p.packetSize == 8192 }},
-		{"log=64;packet size=48000", func(p connectParams) bool { return p.logFlags == 64 && p.packetSize == 32767 }},
-
-		// those are supported currently, but maybe should not be
-		{"someparam", func(p connectParams) bool { return true }},
-		{";;=;", func(p connectParams) bool { return true }},
-
-		// https://github.com/denisenkom/go-mssqldb/issues/645 enable Application Name to mirror ADO
-		// Verify the various synonyms for server and database etc
-		{"application name=appname", func(p connectParams) bool { return p.appname == "appname" }},
-		{"data source=someserver;Initial Catalog=somedatabase;user=someuser", func(p connectParams) bool {
-			return p.host == "someserver" && p.database == "somedatabase" && p.user == "someuser"
-		}},
-		{"network address=someserver;uid=someuser", func(p connectParams) bool { return p.host == "someserver" && p.user == "someuser" }},
-		{"address=someserver", func(p connectParams) bool { return p.host == "someserver" }},
-		{"addr=someserver", func(p connectParams) bool { return p.host == "someserver" }},
-
-		// ADO server names can include a tcp: prefix and a port suffix
-
-		{"server=tcp:someserver,1300", func(p connectParams) bool { return p.host == "someserver" && p.port == 1300 }},
-		{"data source=someserver,1300", func(p connectParams) bool { return p.host == "someserver" && p.port == 1300 }},
-		// ODBC mode
-		{"odbc:server=somehost;user id=someuser;password=somepass", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "somepass"
-		}},
-		{"odbc:server=somehost;user id=someuser;password=some{pass", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some{pass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={somepass}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "somepass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={some=pass}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some=pass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={some;pass}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some;pass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={some{pass}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some{pass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={some}}pass}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some}pass"
-		}},
-		{"odbc:server={somehost};user id={someuser};password={some{}}p=a;ss}", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some{}p=a;ss"
-		}},
-		{"odbc: server = somehost; user id =  someuser ; password = {some pass } ;", func(p connectParams) bool {
-			return p.host == "somehost" && p.user == "someuser" && p.password == "some pass "
-		}},
-		{"odbc:password", func(p connectParams) bool {
-			return p.password == ""
-		}},
-		{"odbc:", func(p connectParams) bool {
-			return true
-		}},
-		{"odbc:password=", func(p connectParams) bool {
-			return p.password == ""
-		}},
-		{"odbc:password;", func(p connectParams) bool {
-			return p.password == ""
-		}},
-		{"odbc:password=;", func(p connectParams) bool {
-			return p.password == ""
-		}},
-		{"odbc:password={value}  ", func(p connectParams) bool {
-			return p.password == "value"
-		}},
-
-		// URL mode
-		{"sqlserver://somehost?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 0 && p.instance == "" && p.conn_timeout == 30*time.Second
-		}},
-		{"sqlserver://someuser@somehost?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 0 && p.instance == "" && p.user == "someuser" && p.password == "" && p.conn_timeout == 30*time.Second
-		}},
-		{"sqlserver://someuser:@somehost?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 0 && p.instance == "" && p.user == "someuser" && p.password == "" && p.conn_timeout == 30*time.Second
-		}},
-		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 0 && p.instance == "" && p.user == "someuser" && p.password == "foo:/\\!~@;bar" && p.conn_timeout == 30*time.Second
-		}},
-		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 1434 && p.instance == "" && p.user == "someuser" && p.password == "foo:/\\!~@;bar" && p.conn_timeout == 30*time.Second
-		}},
-		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434/someinstance?connection+timeout=30", func(p connectParams) bool {
-			return p.host == "somehost" && p.port == 1434 && p.instance == "someinstance" && p.user == "someuser" && p.password == "foo:/\\!~@;bar" && p.conn_timeout == 30*time.Second
-		}},
-	}
-	for _, ts := range connStrings {
-		p, err := parseConnectParams(ts.connStr)
-		if err == nil {
-			t.Logf("Connection string was parsed successfully %s", ts.connStr)
-		} else {
-			t.Errorf("Connection string %s failed to parse with error %s", ts.connStr, err)
-			continue
-		}
-
-		if !ts.check(p) {
-			t.Errorf("Check failed on conn str %s", ts.connStr)
-		}
-	}
-}
-
-func TestSplitConnectionStringURL(t *testing.T) {
-	_, err := splitConnectionStringURL("http://bad")
-	if err == nil {
-		t.Error("Connection string with invalid scheme should fail to parse but it didn't")
-	}
-}
-
-// returns parsed connection parameters derived from
-// environment variables
-func testConnParams(t testing.TB) connectParams {
-	dsn := os.Getenv("SQLSERVER_DSN")
-	const logFlags = 127
-	if len(dsn) > 0 {
-		params, err := parseConnectParams(dsn)
-		if err != nil {
-			t.Fatal("unable to parse SQLSERVER_DSN", err)
-		}
-		params.logFlags = logFlags
-		return params
-	}
-	if len(os.Getenv("HOST")) > 0 && len(os.Getenv("DATABASE")) > 0 {
-		return connectParams{
-			host:     os.Getenv("HOST"),
-			instance: os.Getenv("INSTANCE"),
-			database: os.Getenv("DATABASE"),
-			user:     os.Getenv("SQLUSER"),
-			password: os.Getenv("SQLPASSWORD"),
-			logFlags: logFlags,
-		}
-	}
-	// try loading connection string from file
-	f, err := os.Open(".connstr")
-	if err == nil {
-		rdr := bufio.NewReader(f)
-		dsn, err := rdr.ReadString('\n')
-		if err != io.EOF {
-			t.Fatal(err)
-		}
-		params, err := parseConnectParams(dsn)
-		if err != nil {
-			t.Fatal("unable to parse connection string loaded from file", err)
-		}
-		params.logFlags = logFlags
-		return params
-	}
-	t.Skip("no database connection string")
-	return connectParams{}
-}
-
-func TestConnParseRoundTripFixed(t *testing.T) {
-	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127"
-	params, err := parseConnectParams(connStr)
-	if err != nil {
-		t.Fatal("Test URL is not valid", err)
-	}
-	rtParams, err := parseConnectParams(params.toUrl().String())
-	if err != nil {
-		t.Fatal("Params after roundtrip are not valid", err)
-	}
-	if !reflect.DeepEqual(params, rtParams) {
-		t.Fatal("Parameters do not match after roundtrip", params, rtParams)
-	}
-}
diff --git a/datetimeoffset_example_test.go b/datetimeoffset_example_test.go
index 097a372..fa3dffb 100644
--- a/datetimeoffset_example_test.go
+++ b/datetimeoffset_example_test.go
@@ -1,123 +1,123 @@
-// +build go1.10
-
-package mssql_test
-
-import (
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-	"time"
-
-	mssql "github.com/denisenkom/go-mssqldb"
-	"github.com/golang-sql/civil"
-)
-
-// This example shows how to insert and retrieve date and time types data
-func ExampleDateTimeOffset() {
-	flag.Parse()
-
-	if *debug {
-		fmt.Printf(" password:%s\n", *password)
-		fmt.Printf(" port:%d\n", *port)
-		fmt.Printf(" server:%s\n", *server)
-		fmt.Printf(" user:%s\n", *user)
-	}
-
-	connString := makeConnURL().String()
-	if *debug {
-		fmt.Printf(" connString:%s\n", connString)
-	}
-
-	db, err := sql.Open("sqlserver", connString)
-	if err != nil {
-		log.Fatal("Open connection failed:", err.Error())
-	}
-	defer db.Close()
-
-	insertDateTime(db)
-	retrieveDateTime(db)
-	retrieveDateTimeOutParam(db)
-}
-
-func insertDateTime(db *sql.DB) {
-	_, err := db.Exec("CREATE TABLE datetimeTable (timeCol TIME, dateCol DATE, smalldatetimeCol SMALLDATETIME, datetimeCol DATETIME, datetime2Col DATETIME2, datetimeoffsetCol DATETIMEOFFSET)")
-	if err != nil {
-		log.Fatal(err)
-	}
-	stmt, err := db.Prepare("INSERT INTO datetimeTable VALUES(@p1, @p2, @p3, @p4, @p5, @p6)")
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer stmt.Close()
-
-	tin, err := time.Parse(time.RFC3339, "2006-01-02T22:04:05.787-07:00")
-	if err != nil {
-		log.Fatal(err)
-	}
-	var timeCol civil.Time = civil.TimeOf(tin)
-	var dateCol civil.Date = civil.DateOf(tin)
-	var smalldatetimeCol string = "2006-01-02 22:04:00"
-	var datetimeCol mssql.DateTime1 = mssql.DateTime1(tin)
-	var datetime2Col civil.DateTime = civil.DateTimeOf(tin)
-	var datetimeoffsetCol mssql.DateTimeOffset = mssql.DateTimeOffset(tin)
-	_, err = stmt.Exec(timeCol, dateCol, smalldatetimeCol, datetimeCol, datetime2Col, datetimeoffsetCol)
-	if err != nil {
-		log.Fatal(err)
-	}
-}
-
-func retrieveDateTime(db *sql.DB) {
-	rows, err := db.Query("SELECT timeCol, dateCol, smalldatetimeCol, datetimeCol, datetime2Col, datetimeoffsetCol FROM datetimeTable")
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer rows.Close()
-	var c1, c2, c3, c4, c5, c6 time.Time
-	for rows.Next() {
-		err = rows.Scan(&c1, &c2, &c3, &c4, &c5, &c6)
-		if err != nil {
-			log.Fatal(err)
-		}
-		fmt.Printf("c1: %+v; c2: %+v; c3: %+v; c4: %+v; c5: %+v; c6: %+v;\n", c1, c2, c3, c4, c5, c6)
-	}
-}
-
-func retrieveDateTimeOutParam(db *sql.DB) {
-	CreateProcSql := `
-	CREATE PROCEDURE OutDatetimeProc
-		@timeOutParam TIME OUTPUT,
-		@dateOutParam DATE OUTPUT,
-		@smalldatetimeOutParam SMALLDATETIME OUTPUT,
-		@datetimeOutParam DATETIME OUTPUT,
-		@datetime2OutParam DATETIME2 OUTPUT,
-		@datetimeoffsetOutParam DATETIMEOFFSET OUTPUT
-	AS
-		SET NOCOUNT ON
-		SET @timeOutParam = '22:04:05.7870015'
-		SET @dateOutParam = '2006-01-02'
-		SET @smalldatetimeOutParam = '2006-01-02 22:04:00'
-		SET @datetimeOutParam = '2006-01-02 22:04:05.787'
-		SET @datetime2OutParam = '2006-01-02 22:04:05.7870015'
-		SET @datetimeoffsetOutParam = '2006-01-02 22:04:05.7870015 -07:00'`
-	_, err := db.Exec(CreateProcSql)
-	if err != nil {
-		log.Fatal(err)
-	}
-	var (
-		timeOutParam, datetime2OutParam, datetimeoffsetOutParam mssql.DateTimeOffset
-		dateOutParam, datetimeOutParam                          mssql.DateTime1
-		smalldatetimeOutParam                                   string
-	)
-	_, err = db.Exec("OutDatetimeProc",
-		sql.Named("timeOutParam", sql.Out{Dest: &timeOutParam}),
-		sql.Named("dateOutParam", sql.Out{Dest: &dateOutParam}),
-		sql.Named("smalldatetimeOutParam", sql.Out{Dest: &smalldatetimeOutParam}),
-		sql.Named("datetimeOutParam", sql.Out{Dest: &datetimeOutParam}),
-		sql.Named("datetime2OutParam", sql.Out{Dest: &datetime2OutParam}),
-		sql.Named("datetimeoffsetOutParam", sql.Out{Dest: &datetimeoffsetOutParam}))
-	if err != nil {
-		log.Fatal(err)
-	}
-	fmt.Printf("timeOutParam: %+v; dateOutParam: %+v; smalldatetimeOutParam: %s; datetimeOutParam: %+v; datetime2OutParam: %+v; datetimeoffsetOutParam: %+v;\n", time.Time(timeOutParam), time.Time(dateOutParam), smalldatetimeOutParam, time.Time(datetimeOutParam), time.Time(datetime2OutParam), time.Time(datetimeoffsetOutParam))
-}
+// +build go1.10
+
+package mssql_test
+
+import (
+	"database/sql"
+	"flag"
+	"fmt"
+	"log"
+	"time"
+
+	mssql "github.com/denisenkom/go-mssqldb"
+	"github.com/golang-sql/civil"
+)
+
+// This example shows how to insert and retrieve date and time types data
+func ExampleDateTimeOffset() {
+	flag.Parse()
+
+	if *debug {
+		fmt.Printf(" password:%s\n", *password)
+		fmt.Printf(" port:%d\n", *port)
+		fmt.Printf(" server:%s\n", *server)
+		fmt.Printf(" user:%s\n", *user)
+	}
+
+	connString := makeConnURL().String()
+	if *debug {
+		fmt.Printf(" connString:%s\n", connString)
+	}
+
+	db, err := sql.Open("sqlserver", connString)
+	if err != nil {
+		log.Fatal("Open connection failed:", err.Error())
+	}
+	defer db.Close()
+
+	insertDateTime(db)
+	retrieveDateTime(db)
+	retrieveDateTimeOutParam(db)
+}
+
+func insertDateTime(db *sql.DB) {
+	_, err := db.Exec("CREATE TABLE datetimeTable (timeCol TIME, dateCol DATE, smalldatetimeCol SMALLDATETIME, datetimeCol DATETIME, datetime2Col DATETIME2, datetimeoffsetCol DATETIMEOFFSET)")
+	if err != nil {
+		log.Fatal(err)
+	}
+	stmt, err := db.Prepare("INSERT INTO datetimeTable VALUES(@p1, @p2, @p3, @p4, @p5, @p6)")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer stmt.Close()
+
+	tin, err := time.Parse(time.RFC3339, "2006-01-02T22:04:05.787-07:00")
+	if err != nil {
+		log.Fatal(err)
+	}
+	var timeCol civil.Time = civil.TimeOf(tin)
+	var dateCol civil.Date = civil.DateOf(tin)
+	var smalldatetimeCol string = "2006-01-02 22:04:00"
+	var datetimeCol mssql.DateTime1 = mssql.DateTime1(tin)
+	var datetime2Col civil.DateTime = civil.DateTimeOf(tin)
+	var datetimeoffsetCol mssql.DateTimeOffset = mssql.DateTimeOffset(tin)
+	_, err = stmt.Exec(timeCol, dateCol, smalldatetimeCol, datetimeCol, datetime2Col, datetimeoffsetCol)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func retrieveDateTime(db *sql.DB) {
+	rows, err := db.Query("SELECT timeCol, dateCol, smalldatetimeCol, datetimeCol, datetime2Col, datetimeoffsetCol FROM datetimeTable")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer rows.Close()
+	var c1, c2, c3, c4, c5, c6 time.Time
+	for rows.Next() {
+		err = rows.Scan(&c1, &c2, &c3, &c4, &c5, &c6)
+		if err != nil {
+			log.Fatal(err)
+		}
+		fmt.Printf("c1: %+v; c2: %+v; c3: %+v; c4: %+v; c5: %+v; c6: %+v;\n", c1, c2, c3, c4, c5, c6)
+	}
+}
+
+func retrieveDateTimeOutParam(db *sql.DB) {
+	CreateProcSql := `
+	CREATE PROCEDURE OutDatetimeProc
+		@timeOutParam TIME OUTPUT,
+		@dateOutParam DATE OUTPUT,
+		@smalldatetimeOutParam SMALLDATETIME OUTPUT,
+		@datetimeOutParam DATETIME OUTPUT,
+		@datetime2OutParam DATETIME2 OUTPUT,
+		@datetimeoffsetOutParam DATETIMEOFFSET OUTPUT
+	AS
+		SET NOCOUNT ON
+		SET @timeOutParam = '22:04:05.7870015'
+		SET @dateOutParam = '2006-01-02'
+		SET @smalldatetimeOutParam = '2006-01-02 22:04:00'
+		SET @datetimeOutParam = '2006-01-02 22:04:05.787'
+		SET @datetime2OutParam = '2006-01-02 22:04:05.7870015'
+		SET @datetimeoffsetOutParam = '2006-01-02 22:04:05.7870015 -07:00'`
+	_, err := db.Exec(CreateProcSql)
+	if err != nil {
+		log.Fatal(err)
+	}
+	var (
+		timeOutParam, datetime2OutParam, datetimeoffsetOutParam mssql.DateTimeOffset
+		dateOutParam, datetimeOutParam                          mssql.DateTime1
+		smalldatetimeOutParam                                   string
+	)
+	_, err = db.Exec("OutDatetimeProc",
+		sql.Named("timeOutParam", sql.Out{Dest: &timeOutParam}),
+		sql.Named("dateOutParam", sql.Out{Dest: &dateOutParam}),
+		sql.Named("smalldatetimeOutParam", sql.Out{Dest: &smalldatetimeOutParam}),
+		sql.Named("datetimeOutParam", sql.Out{Dest: &datetimeOutParam}),
+		sql.Named("datetime2OutParam", sql.Out{Dest: &datetime2OutParam}),
+		sql.Named("datetimeoffsetOutParam", sql.Out{Dest: &datetimeoffsetOutParam}))
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Printf("timeOutParam: %+v; dateOutParam: %+v; smalldatetimeOutParam: %s; datetimeOutParam: %+v; datetime2OutParam: %+v; datetimeoffsetOutParam: %+v;\n", time.Time(timeOutParam), time.Time(dateOutParam), smalldatetimeOutParam, time.Time(datetimeOutParam), time.Time(datetime2OutParam), time.Time(datetimeoffsetOutParam))
+}
diff --git a/internal/decimal/decimal_test.go b/internal/decimal/decimal_test.go
index 4086a96..d36daac 100644
--- a/internal/decimal/decimal_test.go
+++ b/internal/decimal/decimal_test.go
@@ -166,7 +166,7 @@ func TestFromStringBad(t *testing.T) {
 	bigScaleNumber := "0." + string(arr) + "1"
 
 	values := []struct {
-		in string
+		in    string
 		scale uint8
 	}{
 		{"0.0001", 2},
diff --git a/lastinsertid_example_test.go b/lastinsertid_example_test.go
index 760bc1c..9a82284 100644
--- a/lastinsertid_example_test.go
+++ b/lastinsertid_example_test.go
@@ -1,72 +1,72 @@
-// +build go1.10
-
-package mssql_test
-
-import (
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-)
-
-// This example shows the usage of Connector type
-func ExampleLastInsertId() {
-	flag.Parse()
-
-	if *debug {
-		fmt.Printf(" password:%s\n", *password)
-		fmt.Printf(" port:%d\n", *port)
-		fmt.Printf(" server:%s\n", *server)
-		fmt.Printf(" user:%s\n", *user)
-	}
-
-	connString := makeConnURL().String()
-	if *debug {
-		fmt.Printf(" connString:%s\n", connString)
-	}
-
-	db, err := sql.Open("sqlserver", connString)
-	if err != nil {
-		log.Fatal("Open connection failed:", err.Error())
-	}
-	defer db.Close()
-
-	// Create table
-	_, err = db.Exec("create table foo (bar int identity, baz int unique);")
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer db.Exec("if object_id('foo', 'U') is not null drop table foo;")
-
-	// Attempt to retrieve scope identity using LastInsertId
-	res, err := db.Exec("insert into foo (baz) values (1)")
-	if err != nil {
-		log.Fatal(err)
-	}
-	n, err := res.LastInsertId()
-	if err != nil {
-		log.Print(err)
-		// Gets error: LastInsertId is not supported. Please use the OUTPUT clause or add `select ID = convert(bigint, SCOPE_IDENTITY())` to the end of your query.
-	}
-	log.Printf("LastInsertId: %d\n", n)
-
-	// Retrieve scope identity by adding 'select ID = convert(bigint, SCOPE_IDENTITY())' to the end of the query
-	rows, err := db.Query("insert into foo (baz) values (10); select ID = convert(bigint, SCOPE_IDENTITY())")
-	if err != nil {
-		log.Fatal(err)
-	}
-	defer rows.Close()
-	var lastInsertId1 int64
-	for rows.Next() {
-		rows.Scan(&lastInsertId1)
-		log.Printf("LastInsertId from SCOPE_IDENTITY(): %d\n", lastInsertId1)
-	}
-
-	// Retrieve scope identity by 'output inserted``
-	var lastInsertId2 int64
-	err = db.QueryRow("insert into foo (baz) output inserted.bar values (100)").Scan(&lastInsertId2)
-	if err != nil {
-		log.Fatal(err)
-	}
-	log.Printf("LastInsertId from output inserted: %d\n", lastInsertId2)
-}
+// +build go1.10
+
+package mssql_test
+
+import (
+	"database/sql"
+	"flag"
+	"fmt"
+	"log"
+)
+
+// This example shows the usage of Connector type
+func ExampleLastInsertId() {
+	flag.Parse()
+
+	if *debug {
+		fmt.Printf(" password:%s\n", *password)
+		fmt.Printf(" port:%d\n", *port)
+		fmt.Printf(" server:%s\n", *server)
+		fmt.Printf(" user:%s\n", *user)
+	}
+
+	connString := makeConnURL().String()
+	if *debug {
+		fmt.Printf(" connString:%s\n", connString)
+	}
+
+	db, err := sql.Open("sqlserver", connString)
+	if err != nil {
+		log.Fatal("Open connection failed:", err.Error())
+	}
+	defer db.Close()
+
+	// Create table
+	_, err = db.Exec("create table foo (bar int identity, baz int unique);")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer db.Exec("if object_id('foo', 'U') is not null drop table foo;")
+
+	// Attempt to retrieve scope identity using LastInsertId
+	res, err := db.Exec("insert into foo (baz) values (1)")
+	if err != nil {
+		log.Fatal(err)
+	}
+	n, err := res.LastInsertId()
+	if err != nil {
+		log.Print(err)
+		// Gets error: LastInsertId is not supported. Please use the OUTPUT clause or add `select ID = convert(bigint, SCOPE_IDENTITY())` to the end of your query.
+	}
+	log.Printf("LastInsertId: %d\n", n)
+
+	// Retrieve scope identity by adding 'select ID = convert(bigint, SCOPE_IDENTITY())' to the end of the query
+	rows, err := db.Query("insert into foo (baz) values (10); select ID = convert(bigint, SCOPE_IDENTITY())")
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer rows.Close()
+	var lastInsertId1 int64
+	for rows.Next() {
+		rows.Scan(&lastInsertId1)
+		log.Printf("LastInsertId from SCOPE_IDENTITY(): %d\n", lastInsertId1)
+	}
+
+	// Retrieve scope identity by 'output inserted``
+	var lastInsertId2 int64
+	err = db.QueryRow("insert into foo (baz) output inserted.bar values (100)").Scan(&lastInsertId2)
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Printf("LastInsertId from output inserted: %d\n", lastInsertId2)
+}
diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go
new file mode 100644
index 0000000..0b22ae5
--- /dev/null
+++ b/msdsn/conn_str_test.go
@@ -0,0 +1,180 @@
+package msdsn
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestInvalidConnectionString(t *testing.T) {
+	connStrings := []string{
+		"log=invalid",
+		"port=invalid",
+		"packet size=invalid",
+		"connection timeout=invalid",
+		"dial timeout=invalid",
+		"keepalive=invalid",
+		"encrypt=invalid",
+		"trustservercertificate=invalid",
+		"failoverport=invalid",
+		"applicationintent=ReadOnly",
+
+		// ODBC mode
+		"odbc:password={",
+		"odbc:password={somepass",
+		"odbc:password={somepass}}",
+		"odbc:password={some}pass",
+		"odbc:=", // unexpected =
+		"odbc: =",
+		"odbc:password={some} a",
+
+		// URL mode
+		"sqlserver://\x00",
+		"sqlserver://host?key=value1&key=value2", // duplicate keys
+	}
+	for _, connStr := range connStrings {
+		_, _, err := Parse(connStr)
+		if err == nil {
+			t.Errorf("Connection expected to fail for connection string %s but it didn't", connStr)
+			continue
+		} else {
+			t.Logf("Connection failed for %s as expected with error %v", connStr, err)
+		}
+	}
+}
+
+func TestValidConnectionString(t *testing.T) {
+	type testStruct struct {
+		connStr string
+		check   func(Config) bool
+	}
+	connStrings := []testStruct{
+		{"server=server\\instance;database=testdb;user id=tester;password=pwd", func(p Config) bool {
+			return p.Host == "server" && p.Instance == "instance" && p.User == "tester" && p.Password == "pwd"
+		}},
+		{"server=.", func(p Config) bool { return p.Host == "localhost" }},
+		{"server=(local)", func(p Config) bool { return p.Host == "localhost" }},
+		{"ServerSPN=serverspn;Workstation ID=workstid", func(p Config) bool { return p.ServerSPN == "serverspn" && p.Workstation == "workstid" }},
+		{"failoverpartner=fopartner;failoverport=2000", func(p Config) bool { return p.FailOverPartner == "fopartner" && p.FailOverPort == 2000 }},
+		{"app name=appname;applicationintent=ReadOnly;database=testdb", func(p Config) bool { return p.AppName == "appname" && p.ReadOnlyIntent }},
+		{"encrypt=disable", func(p Config) bool { return p.Encryption == EncryptionDisabled }},
+		{"encrypt=true", func(p Config) bool { return p.Encryption == EncryptionRequired }},
+		{"encrypt=false", func(p Config) bool { return p.Encryption == EncryptionOff }},
+		{"connection timeout=3;dial timeout=4;keepalive=5", func(p Config) bool {
+			return p.ConnTimeout == 3*time.Second && p.DialTimeout == 4*time.Second && p.KeepAlive == 5*time.Second
+		}},
+		{"log=63", func(p Config) bool { return p.LogFlags == 63 && p.Port == 0 }},
+		{"log=63;port=1000", func(p Config) bool { return p.LogFlags == 63 && p.Port == 1000 }},
+		{"log=64", func(p Config) bool { return p.LogFlags == 64 }},
+		{"log=64;packet size=0", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 512 }},
+		{"log=64;packet size=300", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 512 }},
+		{"log=64;packet size=8192", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 8192 }},
+		{"log=64;packet size=48000", func(p Config) bool { return p.LogFlags == 64 && p.PacketSize == 32767 }},
+
+		// those are supported currently, but maybe should not be
+		{"someparam", func(p Config) bool { return true }},
+		{";;=;", func(p Config) bool { return true }},
+
+		// ODBC mode
+		{"odbc:server=somehost;user id=someuser;password=somepass", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "somepass"
+		}},
+		{"odbc:server=somehost;user id=someuser;password=some{pass", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some{pass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={somepass}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "somepass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={some=pass}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some=pass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={some;pass}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some;pass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={some{pass}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some{pass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={some}}pass}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some}pass"
+		}},
+		{"odbc:server={somehost};user id={someuser};password={some{}}p=a;ss}", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some{}p=a;ss"
+		}},
+		{"odbc: server = somehost; user id =  someuser ; password = {some pass } ;", func(p Config) bool {
+			return p.Host == "somehost" && p.User == "someuser" && p.Password == "some pass "
+		}},
+		{"odbc:password", func(p Config) bool {
+			return p.Password == ""
+		}},
+		{"odbc:", func(p Config) bool {
+			return true
+		}},
+		{"odbc:password=", func(p Config) bool {
+			return p.Password == ""
+		}},
+		{"odbc:password;", func(p Config) bool {
+			return p.Password == ""
+		}},
+		{"odbc:password=;", func(p Config) bool {
+			return p.Password == ""
+		}},
+		{"odbc:password={value}  ", func(p Config) bool {
+			return p.Password == "value"
+		}},
+
+		// URL mode
+		{"sqlserver://somehost?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.ConnTimeout == 30*time.Second
+		}},
+		{"sqlserver://someuser@somehost?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "" && p.ConnTimeout == 30*time.Second
+		}},
+		{"sqlserver://someuser:@somehost?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "" && p.ConnTimeout == 30*time.Second
+		}},
+		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "foo:/\\!~@;bar" && p.ConnTimeout == 30*time.Second
+		}},
+		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 1434 && p.Instance == "" && p.User == "someuser" && p.Password == "foo:/\\!~@;bar" && p.ConnTimeout == 30*time.Second
+		}},
+		{"sqlserver://someuser:foo%3A%2F%5C%21~%40;bar@somehost:1434/someinstance?connection+timeout=30", func(p Config) bool {
+			return p.Host == "somehost" && p.Port == 1434 && p.Instance == "someinstance" && p.User == "someuser" && p.Password == "foo:/\\!~@;bar" && p.ConnTimeout == 30*time.Second
+		}},
+	}
+	for _, ts := range connStrings {
+		p, _, err := Parse(ts.connStr)
+		if err == nil {
+			t.Logf("Connection string was parsed successfully %s", ts.connStr)
+		} else {
+			t.Errorf("Connection string %s failed to parse with error %s", ts.connStr, err)
+			continue
+		}
+
+		if !ts.check(p) {
+			t.Errorf("Check failed on conn str %s", ts.connStr)
+		}
+	}
+}
+
+func TestSplitConnectionStringURL(t *testing.T) {
+	_, err := splitConnectionStringURL("http://bad")
+	if err == nil {
+		t.Error("Connection string with invalid scheme should fail to parse but it didn't")
+	}
+}
+
+func TestConnParseRoundTripFixed(t *testing.T) {
+	connStr := "sqlserver://sa:sa@localhost/sqlexpress?database=master&log=127"
+	params, _, err := Parse(connStr)
+	if err != nil {
+		t.Fatal("Test URL is not valid", err)
+	}
+	rtParams, _, err := Parse(params.URL().String())
+	if err != nil {
+		t.Fatal("Params after roundtrip are not valid", err)
+	}
+	if !reflect.DeepEqual(params, rtParams) {
+		t.Fatal("Parameters do not match after roundtrip", params, rtParams)
+	}
+}
diff --git a/net_go116_test.go b/net_go116_test.go
index 79dc077..10c6d9f 100644
--- a/net_go116_test.go
+++ b/net_go116_test.go
@@ -71,7 +71,7 @@ func TestTLSHandshakeConn(t *testing.T) {
 		t.Error(err)
 	}
 
-	outbuf := newTdsBuffer(connector.params.packetSize, toconn)
+	outbuf := newTdsBuffer(connector.params.PacketSize, toconn)
 	handshakeConn := tlsHandshakeConn{buf: outbuf}
 
 	t.Run(`set deadline`, func(t *testing.T) {
@@ -128,7 +128,7 @@ func TestPassthroughConn(t *testing.T) {
 		t.Error(err)
 	}
 
-	outbuf := newTdsBuffer(connector.params.packetSize, toconn)
+	outbuf := newTdsBuffer(connector.params.PacketSize, toconn)
 
 	handshakeConn := tlsHandshakeConn{buf: outbuf}
 	passthrough := passthroughConn{c: &handshakeConn}
diff --git a/newconnector_example_test.go b/newconnector_example_test.go
index 81ca73c..613866b 100644
--- a/newconnector_example_test.go
+++ b/newconnector_example_test.go
@@ -1,143 +1,143 @@
-// +build go1.10
-
-package mssql_test
-
-import (
-	"context"
-	"database/sql"
-	"flag"
-	"fmt"
-	"log"
-	"net/url"
-	"strconv"
-
-	mssql "github.com/denisenkom/go-mssqldb"
-)
-
-var (
-	debug         = flag.Bool("debug", false, "enable debugging")
-	password      = flag.String("password", "", "the database password")
-	port     *int = flag.Int("port", 1433, "the database port")
-	server        = flag.String("server", "", "the database server")
-	user          = flag.String("user", "", "the database user")
-)
-
-const (
-	createTableSql      = "CREATE TABLE TestAnsiNull (bitcol bit, charcol char(1));"
-	dropTableSql        = "IF OBJECT_ID('TestAnsiNull', 'U') IS NOT NULL DROP TABLE TestAnsiNull;"
-	insertQuery1        = "INSERT INTO TestAnsiNull VALUES (0, NULL);"
-	insertQuery2        = "INSERT INTO TestAnsiNull VALUES (1, 'a');"
-	selectNullFilter    = "SELECT bitcol FROM TestAnsiNull WHERE charcol = NULL;"
-	selectNotNullFilter = "SELECT bitcol FROM TestAnsiNull WHERE charcol <> NULL;"
-)
-
-func makeConnURL() *url.URL {
-	return &url.URL{
-		Scheme: "sqlserver",
-		Host:   *server + ":" + strconv.Itoa(*port),
-		User:   url.UserPassword(*user, *password),
-	}
-}
-
-// This example shows the usage of Connector type
-func ExampleConnector() {
-	flag.Parse()
-
-	if *debug {
-		fmt.Printf(" password:%s\n", *password)
-		fmt.Printf(" port:%d\n", *port)
-		fmt.Printf(" server:%s\n", *server)
-		fmt.Printf(" user:%s\n", *user)
-	}
-
-	connString := makeConnURL().String()
-	if *debug {
-		fmt.Printf(" connString:%s\n", connString)
-	}
-
-	// Create a new connector object by calling NewConnector
-	connector, err := mssql.NewConnector(connString)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-
-	// Use SessionInitSql to set any options that cannot be set with the dsn string
-	// With ANSI_NULLS set to ON, compare NULL data with = NULL or <> NULL will return 0 rows
-	connector.SessionInitSQL = "SET ANSI_NULLS ON"
-
-	// Pass connector to sql.OpenDB to get a sql.DB object
-	db := sql.OpenDB(connector)
-	defer db.Close()
-
-	// Create and populate table
-	_, err = db.Exec(createTableSql)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	defer db.Exec(dropTableSql)
-	_, err = db.Exec(insertQuery1)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	_, err = db.Exec(insertQuery2)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-
-	var bitval bool
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-
-	// (*Row) Scan should return ErrNoRows since ANSI_NULLS is set to ON
-	err = db.QueryRowContext(ctx, selectNullFilter).Scan(&bitval)
-	if err != nil {
-		if err.Error() != "sql: no rows in result set" {
-			log.Println(err)
-			return
-		}
-	} else {
-		log.Println("Expects an ErrNoRows error. No error is returned")
-		return
-	}
-
-	// (*Row) Scan should return ErrNoRows since ANSI_NULLS is set to ON
-	err = db.QueryRowContext(ctx, selectNotNullFilter).Scan(&bitval)
-	if err != nil {
-		if err.Error() != "sql: no rows in result set" {
-			log.Println(err)
-			return
-		}
-	} else {
-		log.Println("Expects an ErrNoRows error. No error is returned")
-		return
-	}
-
-	// Set ANSI_NULLS to OFF
-	connector.SessionInitSQL = "SET ANSI_NULLS OFF"
-
-	// (*Row) Scan should copy data to bitval
-	err = db.QueryRowContext(ctx, selectNullFilter).Scan(&bitval)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	if bitval != false {
-		log.Println("Incorrect value retrieved.")
-		return
-	}
-
-	// (*Row) Scan should copy data to bitval
-	err = db.QueryRowContext(ctx, selectNotNullFilter).Scan(&bitval)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	if bitval != true {
-		log.Println("Incorrect value retrieved.")
-		return
-	}
-}
+// +build go1.10
+
+package mssql_test
+
+import (
+	"context"
+	"database/sql"
+	"flag"
+	"fmt"
+	"log"
+	"net/url"
+	"strconv"
+
+	mssql "github.com/denisenkom/go-mssqldb"
+)
+
+var (
+	debug         = flag.Bool("debug", false, "enable debugging")
+	password      = flag.String("password", "", "the database password")
+	port     *int = flag.Int("port", 1433, "the database port")
+	server        = flag.String("server", "", "the database server")
+	user          = flag.String("user", "", "the database user")
+)
+
+const (
+	createTableSql      = "CREATE TABLE TestAnsiNull (bitcol bit, charcol char(1));"
+	dropTableSql        = "IF OBJECT_ID('TestAnsiNull', 'U') IS NOT NULL DROP TABLE TestAnsiNull;"
+	insertQuery1        = "INSERT INTO TestAnsiNull VALUES (0, NULL);"
+	insertQuery2        = "INSERT INTO TestAnsiNull VALUES (1, 'a');"
+	selectNullFilter    = "SELECT bitcol FROM TestAnsiNull WHERE charcol = NULL;"
+	selectNotNullFilter = "SELECT bitcol FROM TestAnsiNull WHERE charcol <> NULL;"
+)
+
+func makeConnURL() *url.URL {
+	return &url.URL{
+		Scheme: "sqlserver",
+		Host:   *server + ":" + strconv.Itoa(*port),
+		User:   url.UserPassword(*user, *password),
+	}
+}
+
+// This example shows the usage of Connector type
+func ExampleConnector() {
+	flag.Parse()
+
+	if *debug {
+		fmt.Printf(" password:%s\n", *password)
+		fmt.Printf(" port:%d\n", *port)
+		fmt.Printf(" server:%s\n", *server)
+		fmt.Printf(" user:%s\n", *user)
+	}
+
+	connString := makeConnURL().String()
+	if *debug {
+		fmt.Printf(" connString:%s\n", connString)
+	}
+
+	// Create a new connector object by calling NewConnector
+	connector, err := mssql.NewConnector(connString)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	// Use SessionInitSql to set any options that cannot be set with the dsn string
+	// With ANSI_NULLS set to ON, compare NULL data with = NULL or <> NULL will return 0 rows
+	connector.SessionInitSQL = "SET ANSI_NULLS ON"
+
+	// Pass connector to sql.OpenDB to get a sql.DB object
+	db := sql.OpenDB(connector)
+	defer db.Close()
+
+	// Create and populate table
+	_, err = db.Exec(createTableSql)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+	defer db.Exec(dropTableSql)
+	_, err = db.Exec(insertQuery1)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+	_, err = db.Exec(insertQuery2)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	var bitval bool
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// (*Row) Scan should return ErrNoRows since ANSI_NULLS is set to ON
+	err = db.QueryRowContext(ctx, selectNullFilter).Scan(&bitval)
+	if err != nil {
+		if err.Error() != "sql: no rows in result set" {
+			log.Println(err)
+			return
+		}
+	} else {
+		log.Println("Expects an ErrNoRows error. No error is returned")
+		return
+	}
+
+	// (*Row) Scan should return ErrNoRows since ANSI_NULLS is set to ON
+	err = db.QueryRowContext(ctx, selectNotNullFilter).Scan(&bitval)
+	if err != nil {
+		if err.Error() != "sql: no rows in result set" {
+			log.Println(err)
+			return
+		}
+	} else {
+		log.Println("Expects an ErrNoRows error. No error is returned")
+		return
+	}
+
+	// Set ANSI_NULLS to OFF
+	connector.SessionInitSQL = "SET ANSI_NULLS OFF"
+
+	// (*Row) Scan should copy data to bitval
+	err = db.QueryRowContext(ctx, selectNullFilter).Scan(&bitval)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+	if bitval != false {
+		log.Println("Incorrect value retrieved.")
+		return
+	}
+
+	// (*Row) Scan should copy data to bitval
+	err = db.QueryRowContext(ctx, selectNotNullFilter).Scan(&bitval)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+	if bitval != true {
+		log.Println("Incorrect value retrieved.")
+		return
+	}
+}
diff --git a/queries_test.go b/queries_test.go
index 0589491..8e6c90d 100644
--- a/queries_test.go
+++ b/queries_test.go
@@ -15,6 +15,8 @@ import (
 	"sync"
 	"testing"
 	"time"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 func driverWithProcess(t *testing.T) *Driver {
@@ -2125,8 +2127,8 @@ func TestDisconnect1(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
 	defer cancel()
 
-	createDialer = func(p *connectParams) Dialer {
-		nd := netDialer{&net.Dialer{Timeout: p.dial_timeout, KeepAlive: p.keepAlive}}
+	createDialer = func(p *msdsn.Config) Dialer {
+		nd := netDialer{&net.Dialer{Timeout: p.DialTimeout, KeepAlive: p.KeepAlive}}
 		di := &dialerInterrupt{nd: nd}
 		go func() {
 			<-waitDisrupt
@@ -2188,8 +2190,8 @@ func TestDisconnect2(t *testing.T) {
 		ctx, cancel = context.WithTimeout(ctx, time.Second*2)
 		defer cancel()
 
-		createDialer = func(p *connectParams) Dialer {
-			nd := netDialer{&net.Dialer{Timeout: p.dial_timeout, KeepAlive: p.keepAlive}}
+		createDialer = func(p *msdsn.Config) Dialer {
+			nd := netDialer{&net.Dialer{Timeout: p.DialTimeout, KeepAlive: p.KeepAlive}}
 			di := &dialerInterrupt{nd: nd}
 			go func() {
 				<-waitDisrupt
diff --git a/tds_go110_test.go b/tds_go110_test.go
index 62a404f..0a0e6f6 100644
--- a/tds_go110_test.go
+++ b/tds_go110_test.go
@@ -13,7 +13,7 @@ func open(t *testing.T) *sql.DB {
 	connector, err := NewConnector(makeConnStr(t).String())
 	if err != nil {
 		t.Error("Open connection failed:", err.Error())
-	return nil
+		return nil
 	}
 	return sql.OpenDB(connector)
 }
diff --git a/tds_go110pre_test.go b/tds_go110pre_test.go
index 4c43699..d0a57c4 100644
--- a/tds_go110pre_test.go
+++ b/tds_go110pre_test.go
@@ -12,8 +12,8 @@ func open(t *testing.T) *sql.DB {
 	SetLogger(testLogger{t})
 	conn, err := sql.Open("sqlserver", makeConnStr(t).String())
 	if err != nil {
-	t.Error("Open connection failed:", err.Error())
+		t.Error("Open connection failed:", err.Error())
 		return nil
 	}
 	return conn
-}
\ No newline at end of file
+}
diff --git a/tds_login_test.go b/tds_login_test.go
index 6f5ec44..47173a4 100644
--- a/tds_login_test.go
+++ b/tds_login_test.go
@@ -10,6 +10,8 @@ import (
 	"regexp"
 	"sync/atomic"
 	"testing"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 type MockTransportDialer struct {
@@ -158,7 +160,11 @@ func TestLoginWithSQLServerAuth(t *testing.T) {
 }
 
 func TestLoginWithSecurityTokenAuth(t *testing.T) {
-	conn, err := newSecurityTokenConnector("sqlserver://localhost:1433?Workstation ID=localhost&log=128",
+	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	if err != nil {
+		t.Fatal(err)
+	}
+	conn, err := newSecurityTokenConnector(config,
 		func(ctx context.Context) (string, error) {
 			return "<token>", nil
 		},
@@ -213,8 +219,12 @@ func TestLoginWithSecurityTokenAuth(t *testing.T) {
 }
 
 func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
+	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	if err != nil {
+		t.Fatal(err)
+	}
 	conn, err := newActiveDirectoryTokenConnector(
-		"sqlserver://localhost:1433?Workstation ID=localhost&log=128",
+		config,
 		fedAuthADALWorkflowPassword,
 		func(ctx context.Context, serverSPN, stsURL string) (string, error) {
 			return "<token>", nil
@@ -281,8 +291,12 @@ func TestLoginWithADALUsernamePasswordAuth(t *testing.T) {
 }
 
 func TestLoginWithADALManagedIdentityAuth(t *testing.T) {
+	config, _, err := msdsn.Parse("sqlserver://localhost:1433?Workstation ID=localhost&log=128")
+	if err != nil {
+		t.Fatal(err)
+	}
 	conn, err := newActiveDirectoryTokenConnector(
-		"sqlserver://localhost:1433?Workstation ID=localhost&log=128",
+		config,
 		fedAuthADALWorkflowMSI,
 		func(ctx context.Context, serverSPN, stsURL string) (string, error) {
 			return "<token>", nil
diff --git a/tds_test.go b/tds_test.go
index 4465160..9a77ea5 100644
--- a/tds_test.go
+++ b/tds_test.go
@@ -1,14 +1,19 @@
 package mssql
 
 import (
+	"bufio"
 	"bytes"
 	"context"
 	"database/sql"
 	"encoding/hex"
+	"io"
 	"net"
 	"net/url"
+	"os"
 	"runtime"
 	"testing"
+
+	"github.com/denisenkom/go-mssqldb/msdsn"
 )
 
 type MockTransport struct {
@@ -145,13 +150,13 @@ func TestSendLoginWithFeatureExt(t *testing.T) {
 
 func TestSendSqlBatch(t *testing.T) {
 	checkConnStr(t)
-	p, err := parseConnectParams(makeConnStr(t).String())
+	p, _, err := msdsn.Parse(makeConnStr(t).String())
 	if err != nil {
 		t.Error("parseConnectParams failed:", err.Error())
 		return
 	}
 
-	conn, err := connect(context.Background(), nil, optionalLogger{testLogger{t}}, p)
+	conn, err := connect(context.Background(), &Connector{params: p}, optionalLogger{testLogger{t}}, p)
 	if err != nil {
 		t.Error("Open connection failed:", err.Error())
 		return
@@ -188,6 +193,48 @@ func TestSendSqlBatch(t *testing.T) {
 	}
 }
 
+// returns parsed connection parameters derived from
+// environment variables
+func testConnParams(t testing.TB) msdsn.Config {
+	dsn := os.Getenv("SQLSERVER_DSN")
+	const logFlags = 127
+	if len(dsn) > 0 {
+		params, _, err := msdsn.Parse(dsn)
+		if err != nil {
+			t.Fatal("unable to parse SQLSERVER_DSN", err)
+		}
+		params.LogFlags = logFlags
+		return params
+	}
+	if len(os.Getenv("HOST")) > 0 && len(os.Getenv("DATABASE")) > 0 {
+		return msdsn.Config{
+			Host:     os.Getenv("HOST"),
+			Instance: os.Getenv("INSTANCE"),
+			Database: os.Getenv("DATABASE"),
+			User:     os.Getenv("SQLUSER"),
+			Password: os.Getenv("SQLPASSWORD"),
+			LogFlags: logFlags,
+		}
+	}
+	// try loading connection string from file
+	f, err := os.Open(".connstr")
+	if err == nil {
+		rdr := bufio.NewReader(f)
+		dsn, err := rdr.ReadString('\n')
+		if err != io.EOF {
+			t.Fatal(err)
+		}
+		params, _, err := msdsn.Parse(dsn)
+		if err != nil {
+			t.Fatal("unable to parse connection string loaded from file", err)
+		}
+		params.LogFlags = logFlags
+		return params
+	}
+	t.Skip("no database connection string")
+	return msdsn.Config{}
+}
+
 func checkConnStr(t testing.TB) {
 	testConnParams(t)
 }
@@ -195,7 +242,7 @@ func checkConnStr(t testing.TB) {
 // makeConnStr returns a URL struct so it may be modified by various
 // tests before used as a DSN.
 func makeConnStr(t testing.TB) *url.URL {
-	return testConnParams(t).toUrl()
+	return testConnParams(t).URL()
 }
 
 type testLogger struct {
@@ -231,25 +278,25 @@ func testConnection(t *testing.T, connStr string) {
 func TestConnect(t *testing.T) {
 	params := testConnParams(t)
 	SetLogger(testLogger{t})
-	testConnection(t, params.toUrl().String())
+	testConnection(t, params.URL().String())
 }
 
 func TestConnectViaIp(t *testing.T) {
 	params := testConnParams(t)
-	if params.encrypt {
+	if params.Encryption == msdsn.EncryptionRequired {
 		t.Skip("Unable to test connection to IP for servers that expect encryption")
 	}
 
-	if params.host == "." {
-		params.host = "127.0.0.1"
+	if params.Host == "." {
+		params.Host = "127.0.0.1"
 	} else {
-		ips, err := net.LookupIP(params.host)
+		ips, err := net.LookupIP(params.Host)
 		if err != nil {
 			t.Fatal("Unable to lookup IP", err)
 		}
-		params.host = ips[0].String()
+		params.Host = ips[0].String()
 	}
-	testConnection(t, params.toUrl().String())
+	testConnection(t, params.URL().String())
 }
 
 func simpleQuery(conn *sql.DB, t *testing.T) (stmt *sql.Stmt) {
@@ -430,16 +477,16 @@ func TestSecureConnection(t *testing.T) {
 
 func TestBadCredentials(t *testing.T) {
 	params := testConnParams(t)
-	params.password = "padpwd"
-	params.user = "baduser"
-	testConnectionBad(t, params.toUrl().String())
+	params.Password = "padpwd"
+	params.User = "baduser"
+	testConnectionBad(t, params.URL().String())
 }
 
 func TestBadHost(t *testing.T) {
 	params := testConnParams(t)
-	params.host = "badhost"
-	params.instance = ""
-	testConnectionBad(t, params.toUrl().String())
+	params.Host = "badhost"
+	params.Instance = ""
+	testConnectionBad(t, params.URL().String())
 }
 
 func TestSSPIAuth(t *testing.T) {
@@ -551,7 +598,7 @@ func TestReadBVarByte(t *testing.T) {
 
 func BenchmarkPacketSize(b *testing.B) {
 	checkConnStr(b)
-	p, err := parseConnectParams(makeConnStr(b).String())
+	p, _, err := msdsn.Parse(makeConnStr(b).String())
 	if err != nil {
 		b.Error("parseConnectParams failed:", err.Error())
 		return
@@ -570,15 +617,15 @@ func BenchmarkPacketSize(b *testing.B) {
 	for _, bm := range benchmarks {
 		b.Run(bm.name, func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
-				p.packetSize = bm.packetSize
+				p.PacketSize = bm.packetSize
 				runBatch(b, p)
 			}
 		})
 	}
 }
 
-func runBatch(t testing.TB, p connectParams) {
-	conn, err := connect(context.Background(), nil, optionalLogger{testLogger{t}}, p)
+func runBatch(t testing.TB, p msdsn.Config) {
+	conn, err := connect(context.Background(), &Connector{params: p}, optionalLogger{testLogger{t}}, p)
 	if err != nil {
 		t.Error("Open connection failed:", err.Error())
 		return