Repository: denisenkom/go-mssqldb
Pull request #667 (2021-06-29 16:45:14)

Add synonyms for ADO-style connection string parameters
Per request in #645 this change expands the set of keys usable in the ADO-style connection string to include `application name` as well as synonyms for `database` and `user id` and `server`.

The scope of the PR is a bit bigger than that because it includes:
1. A simple Azure Devops pipeline yml I used in my fork for testing, 
2. A small bug fix in a test to match case on a variable. The test was failing when run against a case sensitive collation
3. Some formatting fixes to md files to enable `go` syntax highlighting and satisfy vs code's go linter.

Feel free to cherry pick those out or I can figure out how to do it. 

codecov[bot]: # [Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#667](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d277541) into [master](https://codecov.io/gh/denisenkom/go-mssqldb/commit/35162394aa7974ac37c4b42ddb1c200f6b01ebac?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3516239) will **decrease** coverage by `0.34%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667/graphs/tree.svg?width=650&height=150&src=pr&token=MoMB2u5avp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master     #667      +/-   ##
==========================================
- Coverage   72.28%   71.93%   -0.35%     
==========================================
  Files          24       24              
  Lines        5469     5480      +11     
==========================================
- Hits         3953     3942      -11     
- Misses       1291     1313      +22     
  Partials      225      225              
```


| [Impacted Files](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [conn\_str.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29ubl9zdHIuZ28=) | `100.00% <100.00%> (ø)` | |
| [net.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bmV0Lmdv) | `42.52% <0.00%> (-25.29%)` | :arrow_down: |
| [tds.go](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-dGRzLmdv) | `66.83% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [3516239...d277541](https://codecov.io/gh/denisenkom/go-mssqldb/pull/667?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/.gitignore b/.gitignore
index 1dda703..1f8b088 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,8 @@
 *.log
 *.swp
 *~
+coverage.json
+coverage.txt
+coverage.xml
+testresults.xml
+
diff --git a/.pipelines/README.md b/.pipelines/README.md
new file mode 100644
index 0000000..086aaa7
--- /dev/null
+++ b/.pipelines/README.md
@@ -0,0 +1,7 @@
+# Azure pipelines for go-mssqldb
+
+## Purpose
+
+Created by @shueybubbles, a member of the SQL Server team at Microsoft. I built these pipelines to run tests against specific configurations of SQL Server and Azure SQL Database in our internal Azure Devops subscriptions.
+
+Each YML will be sufficiently parameterized to be runnable in other environments.
diff --git a/README.md b/README.md
index db245c5..92e74b6 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ The recommended connection string uses a URL format:
 `sqlserver://username:password@host/instance?param1=value&param2=value`
 Other supported formats are listed below.
 
-### Common parameters:
+### Common parameters
 
 * `user id` - enter the SQL Server Authentication user id or the Windows Authentication user id in the DOMAIN\User format. On Windows, if user id is empty or missing Single-Sign-On is used. The user domain sensitive to the case which is defined in the connection string.
 * `password`
@@ -29,24 +29,24 @@ Other supported formats are listed below.
   * `true` - Data sent between client and server is encrypted.
 * `app name` - The application name (default is go-mssqldb)
 
-### Connection parameters for ODBC and ADO style connection strings:
+### Connection parameters for ODBC and ADO style connection strings
 
 * `server` - host or host\instance (default localhost)
 * `port` - used only when there is no instance in server (default 1433)
 
-### Less common parameters:
+### Less common parameters
 
 * `keepAlive` - in seconds; 0 to disable (default is 30)
-* `failoverpartner` - host or host\instance (default is no partner). 
+* `failoverpartner` - host or host\instance (default is no partner).
 * `failoverport` - used only when there is no instance in failoverpartner (default 1433)
 * `packet size` - in bytes; 512 to 32767 (default is 4096)
   * Encrypted connections have a maximum packet size of 16383 bytes
-  * Further information on usage: https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option
+  * Further information on usage: <https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/configure-the-network-packet-size-server-configuration-option>
 * `log` - logging flags (default 0/no logging, 63 for full logging)
-  *  1 log errors
-  *  2 log messages
-  *  4 log rows affected
-  *  8 trace sql statements
+  * 1 log errors
+  * 2 log messages
+  * 4 log rows affected
+  * 8 trace sql statements
   * 16 log statement parameters
   * 32 log transaction begin/end
 * `TrustServerCertificate`
@@ -56,72 +56,82 @@ Other supported formats are listed below.
 * `hostNameInCertificate` - Specifies the Common Name (CN) in the server certificate. Default value is the server host.
 * `ServerSPN` - The kerberos SPN (Service Principal Name) for the server. Default is MSSQLSvc/host:port.
 * `Workstation ID` - The workstation name (default is the host name)
-* `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`. 
-
-### The connection string can be specified in one of three formats:
+* `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`.
 
+### The connection string can be specified in one of three formats
 
 1. URL: with `sqlserver` scheme. username and password appears before the host. Any instance appears as
     the first segment in the path. All other options are query parameters. Examples:
 
-  * `sqlserver://username:password@host/instance?param1=value&param2=value`
-  * `sqlserver://username:password@host:port?param1=value&param2=value`
-  * `sqlserver://sa@localhost/SQLExpress?database=master&connection+timeout=30` // `SQLExpress instance.
-  * `sqlserver://sa:mypass@localhost?database=master&connection+timeout=30`     // username=sa, password=mypass.
-  * `sqlserver://sa:mypass@localhost:1234?database=master&connection+timeout=30` // port 1234 on localhost.
-  * `sqlserver://sa:my%7Bpass@somehost?connection+timeout=30` // password is "my{pass"
-
-  A string of this format can be constructed using the `URL` type in the `net/url` package.
-
-```go
-  query := url.Values{}
-  query.Add("app name", "MyAppName")
-
-  u := &url.URL{
-      Scheme:   "sqlserver",
-      User:     url.UserPassword(username, password),
-      Host:     fmt.Sprintf("%s:%d", hostname, port),
-      // Path:  instance, // if connecting to an instance instead of a port
-      RawQuery: query.Encode(),
-  }
-  db, err := sql.Open("sqlserver", u.String())
-```
+    * `sqlserver://username:password@host/instance?param1=value&param2=value`
+    * `sqlserver://username:password@host:port?param1=value&param2=value`
+    * `sqlserver://sa@localhost/SQLExpress?database=master&connection+timeout=30` // `SQLExpress instance.
+    * `sqlserver://sa:mypass@localhost?database=master&connection+timeout=30`     // username=sa, password=mypass.
+    * `sqlserver://sa:mypass@localhost:1234?database=master&connection+timeout=30` // port 1234 on localhost.
+    * `sqlserver://sa:my%7Bpass@somehost?connection+timeout=30` // password is "my{pass"
+      A string of this format can be constructed using the `URL` type in the `net/url` package.
+
+    ```go
+
+    query := url.Values{}
+    query.Add("app name", "MyAppName")
+    
+    u := &url.URL{
+    	Scheme:   "sqlserver",
+    	User:     url.UserPassword(username, password),
+    	Host:     fmt.Sprintf("%s:%d", hostname, port),
+    	// Path:  instance, // if connecting to an instance instead of a port
+    	RawQuery: query.Encode(),
+    }
+    db, err := sql.Open("sqlserver", u.String())
+
+    ```
 
 2. ADO: `key=value` pairs separated by `;`. Values may not contain `;`, leading and trailing whitespace is ignored.
      Examples:
-	
-  * `server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName`
-  * `server=localhost;user id=sa;database=master;app name=MyAppName`
+
+    * `server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName`
+    * `server=localhost;user id=sa;database=master;app name=MyAppName`
+
+    ADO strings support synonyms for database, app name, user id, and server
+    * server <= addr, address, network address, data source
+    * user id <= user, uid
+    * database <= initial catalog
+    * app name <= application name
 
 3. ODBC: Prefix with `odbc`, `key=value` pairs separated by `;`. Allow `;` by wrapping
     values in `{}`. Examples:
-	
-  * `odbc:server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName`
-  * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName`
-  * `odbc:server=localhost;user id=sa;password={foo;bar}` // Value marked with `{}`, password is "foo;bar"
-  * `odbc:server=localhost;user id=sa;password={foo{bar}` // Value marked with `{}`, password is "foo{bar"
-  * `odbc:server=localhost;user id=sa;password={foobar }` // Value marked with `{}`, password is "foobar "
-  * `odbc:server=localhost;user id=sa;password=foo{bar`   // Literal `{`, password is "foo{bar"
-  * `odbc:server=localhost;user id=sa;password=foo}bar`   // Literal `}`, password is "foo}bar"
-  * `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar"
-  * `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with `}}`, password is "foo}bar"
+
+    * `odbc:server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName`
+    * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName`
+    * `odbc:server=localhost;user id=sa;password={foo;bar}` // Value marked with `{}`, password is "foo;bar"
+    * `odbc:server=localhost;user id=sa;password={foo{bar}` // Value marked with `{}`, password is "foo{bar"
+    * `odbc:server=localhost;user id=sa;password={foobar }` // Value marked with `{}`, password is "foobar "
+    * `odbc:server=localhost;user id=sa;password=foo{bar`   // Literal `{`, password is "foo{bar"
+    * `odbc:server=localhost;user id=sa;password=foo}bar`   // Literal `}`, password is "foo}bar"
+    * `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar"
+    * `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with`}}`, password is "foo}bar"
 
 ### Azure Active Directory authentication - preview
 
 The configuration of functionality might change in the future.
 
-Azure Active Directory (AAD) access tokens are relatively short lived and need to be 
+Azure Active Directory (AAD) access tokens are relatively short lived and need to be
 valid when a new connection is made. Authentication is supported using a callback func that
 provides a fresh and valid token using a connector:
-``` golang
+
+``` go
+
 conn, err := mssql.NewAccessTokenConnector(
-  "Server=test.database.windows.net;Database=testdb",
-  tokenProvider)
+	"Server=test.database.windows.net;Database=testdb",
+	tokenProvider)
 if err != nil {
 	// handle errors in DSN
 }
 db := sql.OpenDB(conn)
+
 ```
+
 Where `tokenProvider` is a function that returns a fresh access token or an error. None of these statements
 actually trigger the retrieval of a token, this happens when the first statment is issued and a connection
 is created.
@@ -129,18 +139,23 @@ is created.
 ## Executing Stored Procedures
 
 To run a stored procedure, set the query text to the procedure name:
+
 ```go
+
 var account = "abc"
 _, err := db.ExecContext(ctx, "sp_RunMe",
 	sql.Named("ID", 123),
 	sql.Named("Account", sql.Out{Dest: &account}),
 )
+
 ```
 
 ## Reading Output Parameters from a Stored Procedure with Resultset
 
 To read output parameters from a stored procedure with resultset, make sure you read all the rows before reading the output parameters:
+
 ```go
+
 sqltextcreate := `
 CREATE PROCEDURE spwithoutputandrows
 	@bitparam BIT OUTPUT
@@ -156,6 +171,7 @@ for rows.Next() {
 	err = rows.Scan(&strrow)
 }
 fmt.Printf("bitparam is %d", bitout)
+
 ```
 
 ## Caveat for local temporary tables
@@ -201,21 +217,25 @@ _, err := conn.ExecContext(ctx, "insert into #mytemp (x) values (@p1)", 1)
 
 To get the procedure return status, pass into the parameters a
 `*mssql.ReturnStatus`. For example:
-```
+
+```go
+
 var rs mssql.ReturnStatus
 _, err := db.ExecContext(ctx, "theproc", &rs)
 log.Printf("status=%d", rs)
+
 ```
 
 or
 
-```
+```go
 var rs mssql.ReturnStatus
 _, err := db.QueryContext(ctx, "theproc", &rs)
 for rows.Next() {
 	err = rows.Scan(&val)
 }
 log.Printf("status=%d", rs)
+
 ```
 
 Limitation: ReturnStatus cannot be retrieved using `QueryRow`.
@@ -226,7 +246,9 @@ The `sqlserver` driver uses normal MS SQL Server syntax and expects parameters i
 the sql query to be in the form of either `@Name` or `@p1` to `@pN` (ordinal position).
 
 ```go
+
 db.QueryContext(ctx, `select * from t where ID = @ID and Name = @p2;`, sql.Named("ID", 6), "Bob")
+
 ```
 
 ### Parameter Types
@@ -235,30 +257,30 @@ To pass specific types to the query parameters, say `varchar` or `date` types,
 you must convert the types to the type before passing in. The following types
 are supported:
 
- * string -> nvarchar
- * mssql.VarChar -> varchar
- * time.Time -> datetimeoffset or datetime (TDS version dependent)
- * mssql.DateTime1 -> datetime
- * mssql.DateTimeOffset -> datetimeoffset
- * "github.com/golang-sql/civil".Date -> date
- * "github.com/golang-sql/civil".DateTime -> datetime2
- * "github.com/golang-sql/civil".Time -> time
- * mssql.TVP -> Table Value Parameter (TDS version dependent)
+* string -> nvarchar
+* mssql.VarChar -> varchar
+* time.Time -> datetimeoffset or datetime (TDS version dependent)
+* mssql.DateTime1 -> datetime
+* mssql.DateTimeOffset -> datetimeoffset
+* "github.com/golang-sql/civil".Date -> date
+* "github.com/golang-sql/civil".DateTime -> datetime2
+* "github.com/golang-sql/civil".Time -> time
+* mssql.TVP -> Table Value Parameter (TDS version dependent)
 
 ## Important Notes
 
- * [LastInsertId](https://golang.org/pkg/database/sql/#Result.LastInsertId) should
+* [LastInsertId](https://golang.org/pkg/database/sql/#Result.LastInsertId) should
     not be used with this driver (or SQL Server) due to how the TDS protocol
-	works. Please use the [OUTPUT Clause](https://docs.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
-	or add a `select ID = convert(bigint, SCOPE_IDENTITY());` to the end of your
-	query (ref [SCOPE_IDENTITY](https://docs.microsoft.com/en-us/sql/t-sql/functions/scope-identity-transact-sql)).
-	This will ensure you are getting the correct ID and will prevent a network round trip.
- * [NewConnector](https://godoc.org/github.com/denisenkom/go-mssqldb#NewConnector)
+ works. Please use the [OUTPUT Clause](https://docs.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
+ or add a `select ID = convert(bigint, SCOPE_IDENTITY());` to the end of your
+ query (ref [SCOPE_IDENTITY](https://docs.microsoft.com/en-us/sql/t-sql/functions/scope-identity-transact-sql)).
+ This will ensure you are getting the correct ID and will prevent a network round trip.
+* [NewConnector](https://godoc.org/github.com/denisenkom/go-mssqldb#NewConnector)
     may be used with [OpenDB](https://golang.org/pkg/database/sql/#OpenDB).
- * [Connector.SessionInitSQL](https://godoc.org/github.com/denisenkom/go-mssqldb#Connector.SessionInitSQL)
-	may be set to set any driver specific session settings after the session
-	has been reset. If empty the session will still be reset but use the database
-	defaults in Go1.10+.
+* [Connector.SessionInitSQL](https://godoc.org/github.com/denisenkom/go-mssqldb#Connector.SessionInitSQL)
+ may be set to set any driver specific session settings after the session
+ has been reset. If empty the session will still be reset but use the database
+ defaults in Go1.10+.
 
 ## Features
 
@@ -280,7 +302,9 @@ Environment variables are used to pass login information.
 
 Example:
 
+```bash
     env SQLSERVER_DSN=sqlserver://user:pass@hostname/instance?database=test1 go test
+```
 
 ## Deprecated
 
@@ -296,7 +320,7 @@ will be loosly parsed and an attempt to extract identifiers using one of
 * :nnn
 * $nnn
 
-will be used. This is not recommended with SQL Server. 
+will be used. This is not recommended with SQL Server.
 There is at least one existing `won't fix` issue with the query parsing.
 
 Use the native "@Name" parameters instead with the "sqlserver" driver name.
@@ -306,4 +330,4 @@ Use the native "@Name" parameters instead with the "sqlserver" driver name.
 * SQL Server 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled.
 To fix SQL Server 2008 R2 issue, install SQL Server 2008 R2 Service Pack 2.
 To fix SQL Server 2008 issue, install Microsoft SQL Server 2008 Service Pack 3 and Cumulative update package 3 for SQL Server 2008 SP3.
-More information: http://support.microsoft.com/kb/2653857
+More information: <http://support.microsoft.com/kb/2653857>
diff --git a/conn_str.go b/conn_str.go
index d7d9e06..fb1aacc 100644
--- a/conn_str.go
+++ b/conn_str.go
@@ -262,6 +262,17 @@ func (p connectParams) toUrl() *url.URL {
 	return &res
 }
 
+var adoSynonyms = map[string]string{
+	"application name": "app name",
+	"data source":      "server",
+	"address":          "server",
+	"network address":  "server",
+	"addr":             "server",
+	"user":             "user id",
+	"uid":              "user id",
+	"initial catalog":  "database",
+}
+
 func splitConnectionString(dsn string) (res map[string]string) {
 	res = map[string]string{}
 	parts := strings.Split(dsn, ";")
@@ -278,6 +289,20 @@ func splitConnectionString(dsn string) (res map[string]string) {
 		if len(lst) > 1 {
 			value = strings.TrimSpace(lst[1])
 		}
+		synonym, hasSynonym := adoSynonyms[name]
+		if hasSynonym {
+			name = synonym
+		}
+		// "server" in ADO can include a protocol and a port.
+		// We only support tcp protocol
+		if name == "server" {
+			value = strings.TrimPrefix(value, "tcp:")
+			serverParts := strings.Split(value, ",")
+			if len(serverParts) == 2 && len(serverParts[1]) > 0 {
+				value = serverParts[0]
+				res["port"] = serverParts[1]
+			}
+		}
 		res[name] = value
 	}
 	return res
diff --git a/doc/how-to-use-table-valued-parameters.md b/doc/how-to-use-table-valued-parameters.md
index 1bbc701..b3746c4 100644
--- a/doc/how-to-use-table-valued-parameters.md
+++ b/doc/how-to-use-table-valued-parameters.md
@@ -4,7 +4,8 @@ Table-valued parameters are declared by using user-defined table types. You can
 
 To make use of the TVP functionality, first you need to create a table type, and a procedure or function to receive data from the table-valued parameter.
 
-```
+```go
+
 createTVP = "CREATE TYPE LocationTableType AS TABLE (LocationName VARCHAR(50), CostRate INT)"
 _, err = db.Exec(createTable)
 
@@ -22,11 +23,13 @@ INSERT INTO Location
 SELECT *, 0,GETDATE()
 FROM @TVP`
 _, err = db.Exec(createProc)
+
 ```
 
 In your go application, create a struct that corresponds to the table type you have created. Create a slice of these structs which contain the data you want to pass to the stored procedure.
 
-```
+```go
+
 type LocationTableTvp struct {
 	LocationName string
 	CostRate     int64
@@ -42,15 +45,18 @@ locationTableTypeData := []LocationTableTvp{
 		CostRate:     1,
 	},
 }
+
 ```
 
 Create a `mssql.TVP` object, and pass the slice of structs into the `Value` member. Set `TypeName` to the table type name.
 
-```
+```go
+
 tvpType := mssql.TVP{
 	TypeName: "LocationTableType",
 	Value:    locationTableTypeData,
 }
+
 ```
 
 Finally, execute the stored procedure and pass the `mssql.TVPType` object you have created as a parameter.
@@ -63,18 +69,21 @@ Sometimes users may find it useful to include fields in the struct that do not h
 
 For example, the user wants to define a struct with two more fields: `LocationCountry` and `Currency`. However, the `LocationTableType` table type do not have these corresponding columns. The user can omit the two new fields from being read by using the `json` or `tvp` tag.
 
-```
+```go
+
 type LocationTableTvpDetailed struct {
 	LocationName	string
 	LocationCountry string	`tvp:"-"`
 	CostRate		int64
 	Currency		string	`json:"-"`
 }
+
 ```
 
 The `tvp` tag is the highest priority. Therefore if there is a field with tag `json:"-" tvp:"any"`, the field is not omitted. The following struct demonstrates different scenarios of using the `json` and `tvp` tags.
 
-```
+```go
+
 type T struct {
 	F1 string `json:"f1" tvp:"f1"`	// not omitted
 	F2 string `json:"-" tvp:"f2"`	// tvp tag takes precedence; not omitted
@@ -85,7 +94,9 @@ type T struct {
 	F7 string `tvp:"f7"`			// not omitted
 	F8 string `tvp:"-"`				// omitted
 }
+
 ```
 
 ## Example
+
 [TVPType example](../tvp_example_test.go)
diff --git a/mssql.go b/mssql.go
index 6e2f4af..c0fd44d 100644
--- a/mssql.go
+++ b/mssql.go
@@ -158,6 +158,11 @@ type Conn struct {
 	outs map[string]interface{}
 }
 
+// IsValid satisfies the driver.Validator interface.
+func (c *Conn) IsValid() bool {
+	return c.connectionGood
+}
+
 func (c *Conn) checkBadConn(err error) error {
 	// this is a hack to address Issue #275
 	// we set connectionGood flag to false if


Test patch:
diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml
new file mode 100644
index 0000000..5422daa
--- /dev/null
+++ b/.pipelines/TestSql2017.yml
@@ -0,0 +1,80 @@
+pool:
+   vmImage: 'ubuntu-latest'
+
+trigger: none
+
+steps: 
+- task: GoTool@0
+  inputs:
+    version: '1.16.5'
+- task: Go@0
+  displayName: 'Go: get sources'
+  inputs:
+    command: 'get'
+    arguments: '-d'
+    workingDirectory: '$(Build.SourcesDirectory)'
+
+
+- task: Go@0
+  displayName: 'Go: install gotest.tools/gotestsum'
+  inputs:
+    command: 'custom'
+    customCommand: 'install'
+    arguments: 'gotest.tools/gotestsum@latest'
+    workingDirectory: '$(System.DefaultWorkingDirectory)'
+
+- task: Go@0
+  displayName: 'Go: install github.com/axw/gocov/gocov'
+  inputs:
+    command: 'custom'
+    customCommand: 'install'
+    arguments: 'github.com/axw/gocov/gocov@latest'
+    workingDirectory: '$(System.DefaultWorkingDirectory)'
+
+- task: Go@0
+  displayName: 'Go: install github.com/AlekSi/gocov-xml'
+  inputs:
+    command: 'custom'
+    customCommand: 'install'
+    arguments: 'github.com/AlekSi/gocov-xml@latest'
+    workingDirectory: '$(System.DefaultWorkingDirectory)'
+
+#Your build pipeline references an undefined variables named SQLPASSWORD and HOST. 
+#Create or edit the build pipeline for this YAML file, define the variable on the Variables tab. See https://go.microsoft.com/fwlink/?linkid=865972
+
+- task: Docker@2
+  displayName: 'Run SQL 2017 docker image'
+  inputs:
+    command: run
+    arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(SQLPASSWORD) mcr.microsoft.com/mssql/server:2017-latest'
+
+- script: |
+    ~/go/bin/gotestsum --junitfile testresults.xml -- -coverprofile=coverage.txt -covermode count
+    ~/go/bin/gocov convert coverage.txt > coverage.json
+    ~/go/bin/gocov-xml < coverage.json > coverage.xml
+    mkdir coverage
+  workingDirectory: '$(Build.SourcesDirectory)'
+  displayName: 'run tests'
+  env:
+    SQLPASSWORD: $(SQLPASSWORD)
+  continueOnError: true
+- task: PublishTestResults@2
+  displayName: "Publish junit-style results"
+  inputs:
+    testResultsFiles: 'testresults.xml'
+    testResultsFormat: JUnit
+    searchFolder: '$(Build.SourcesDirectory)'
+    testRunTitle: 'SQL 2017 - $(Build.SourceBranchName)'
+  condition: always()
+  continueOnError: true
+
+- task: PublishCodeCoverageResults@1
+  inputs:
+    codeCoverageTool: Cobertura 
+    pathToSources: '$(Build.SourcesDirectory)'
+    summaryFileLocation: $(Build.SourcesDirectory)/**/coverage.xml
+    reportDirectory: $(Build.SourcesDirectory)/**/coverage
+    failIfCoverageEmpty: true
+  condition: always()
+  continueOnError: true
+  
diff --git a/conn_str_test.go b/conn_str_test.go
index bb6e268..4c8f3fa 100644
--- a/conn_str_test.go
+++ b/conn_str_test.go
@@ -21,6 +21,7 @@ func TestInvalidConnectionString(t *testing.T) {
 		"trustservercertificate=invalid",
 		"failoverport=invalid",
 		"applicationintent=ReadOnly",
+		"server=someserver,notanumber",
 
 		// ODBC mode
 		"odbc:password={",
@@ -82,6 +83,20 @@ func TestValidConnectionString(t *testing.T) {
 		{"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"
@@ -186,10 +201,10 @@ func testConnParams(t testing.TB) connectParams {
 	}
 	if len(os.Getenv("HOST")) > 0 && len(os.Getenv("DATABASE")) > 0 {
 		return connectParams{
-			host: os.Getenv("HOST"),
+			host:     os.Getenv("HOST"),
 			instance: os.Getenv("INSTANCE"),
 			database: os.Getenv("DATABASE"),
-			user: os.Getenv("SQLUSER"),
+			user:     os.Getenv("SQLUSER"),
 			password: os.Getenv("SQLPASSWORD"),
 			logFlags: logFlags,
 		}
diff --git a/queries_go19_test.go b/queries_go19_test.go
index 1f7a7c8..ae552db 100644
--- a/queries_go19_test.go
+++ b/queries_go19_test.go
@@ -317,7 +317,7 @@ BEGIN
    SELECT
 		@bid = @aid + @bid,
 		@cstr = 'OK',
-		@Vout = 'DREAM'
+		@vout = 'DREAM'
 	;
 END;
 `
diff --git a/tds_test.go b/tds_test.go
index a76d88e..4465160 100644
--- a/tds_test.go
+++ b/tds_test.go
@@ -240,11 +240,15 @@ func TestConnectViaIp(t *testing.T) {
 		t.Skip("Unable to test connection to IP for servers that expect encryption")
 	}
 
-	ips, err := net.LookupIP(params.host)
-	if err != nil {
-		t.Fatal("Unable to lookup IP", err)
+	if params.host == "." {
+		params.host = "127.0.0.1"
+	} else {
+		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())
 }