From 6be998d05989ec18ff1fafeb311858aaa4e8d423 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Thu, 11 Jun 2020 13:57:53 -0700 Subject: [PATCH 1/6] Fix incorrect varbinary being returned Varbinary was being returned with a number greater than 8000 (the maximum) when the length of []byte was too long. Switched so that it will automatically switch to varbinary (MAX) when needed. --- executesql.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/executesql.go b/executesql.go index 7c313be..45dac13 100644 --- a/executesql.go +++ b/executesql.go @@ -141,6 +141,11 @@ func go2SqlDataType(value interface{}) (string, string, error) { case []byte: { b, _ := value.([]byte) + if(max(1, len(b)) > 8000) { + return "varbinary (MAX)", + fmt.Sprintf("0x%x", b), nil + } + return fmt.Sprintf("varbinary (%d)", max(1, len(b))), fmt.Sprintf("0x%x", b), nil } From 321c9e46abec50da9e4368b8d9a7cef72a2a14e2 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Fri, 12 Jun 2020 14:20:56 -0700 Subject: [PATCH 2/6] Refactor removed redundant max() statement Removed redundant max(1, len(b)) function call since that is not needed to check if the length is over 8000. Slightly altered style to match the rest of the code base as well. --- executesql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executesql.go b/executesql.go index 45dac13..0d8e459 100644 --- a/executesql.go +++ b/executesql.go @@ -141,7 +141,7 @@ func go2SqlDataType(value interface{}) (string, string, error) { case []byte: { b, _ := value.([]byte) - if(max(1, len(b)) > 8000) { + if len(b) > 8000 { return "varbinary (MAX)", fmt.Sprintf("0x%x", b), nil } From e407ec4498c3b2ae11c5f7db9431582a2de0db18 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Fri, 12 Jun 2020 14:23:30 -0700 Subject: [PATCH 3/6] Added test for varbinary (MAX) Added a test to ensure that all byte slices with a length over 8000 will auto-switch to varbinary (MAX). --- executesql_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/executesql_test.go b/executesql_test.go index 428749f..f268ee2 100644 --- a/executesql_test.go +++ b/executesql_test.go @@ -1,6 +1,7 @@ package freetds import ( + "strings" "testing" "time" @@ -80,6 +81,7 @@ func TestGoTo2SqlDataType(t *testing.T) { checker(tm.In(paris), "datetimeoffset", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") checker([]byte{1, 2, 3, 4, 5, 6, 7, 8}, "varbinary (8)", "0x0102030405060708") + checker(make([]byte, 8001), "varbinary (MAX)", "0x" + strings.Repeat("00", 8001)) checker("", "nvarchar (1)", "''") checker(true, "bit", "1") From e67cb2247f853853a910e42c668b08f402fb16ac Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Thu, 2 Jul 2020 10:33:57 -0700 Subject: [PATCH 4/6] Adjusted for MSSQL2005 compatibility Added a boolean flag for whether the attached database is MSSQL2005, and it's being used to determine whether to use datetimeoffset or datetime for variables. Added a test to make sure the correct date type is used. --- conn.go | 37 +++++++++++++++++++++++++++++ executesql.go | 14 +++++++---- executesql_test.go | 58 ++++++++++++++++++++++++---------------------- 3 files changed, 76 insertions(+), 33 deletions(-) diff --git a/conn.go b/conn.go index 1fc4822..84531a8 100644 --- a/conn.go +++ b/conn.go @@ -103,6 +103,7 @@ type Conn struct { credentials freetdsVersionGte095 bool + mssqlVersion2005 bool } func (conn *Conn) addMessage(msg string, msgno int) { @@ -235,6 +236,11 @@ func (conn *Conn) getDbProc() (*C.DBPROCESS, error) { return nil, dbProcError("dbopen error") } conn.readFreeTdsVersion() + var sql_runtime_err = conn.readMSSQLVersion() + if sql_runtime_err != nil { + return nil, sql_runtime_err + } + return dbproc, nil } @@ -244,6 +250,29 @@ func (conn *Conn) readFreeTdsVersion() { conn.setFreetdsVersionGte095(freeTdsVersion) } +func (conn *Conn) readMSSQLVersion() error { + var results, err = conn.ExecuteSql("SELECT @@VERSION") + if err != nil { + return err + } + + var version_string string + + if len(results) > 1 { + var result = results[0] + if result.Next() { + var err = result.Scan(&version_string) + + if err != nil { + return err + } + } + } + + conn.setMSSQLVersion(version_string) + return nil +} + func dbProcError(msg string) error { return fmt.Errorf("%s\n%s\n%s", msg, lastError, lastMessage) } @@ -467,6 +496,14 @@ func (conn *Conn) setFreetdsVersionGte095(freeTdsVersion []int) { } } +func (conn *Conn) setMSSQLVersion(version string) { + conn.mssqlVersion2005 = false + version = strings.ToLower(version) + if version[0:25] == "microsoft sql server 2005" { + conn.mssqlVersion2005 = true + } +} + func parseFreeTdsVersion(dbVersion string) []int { rxFreeTdsVersion := regexp.MustCompile(`v(\d+).(\d+).(\d+)`) //log.Println("FreeTDS Version: ", dbVersion) diff --git a/executesql.go b/executesql.go index 0d8e459..94b1adc 100644 --- a/executesql.go +++ b/executesql.go @@ -28,7 +28,7 @@ func (conn *Conn) ExecuteSql(query string, params ...driver.Value) ([]*Result, e if numParams != len(params) { return nil, fmt.Errorf("Incorrect number of params, expecting %d got %d", numParams, len(params)) } - paramDef, paramVal, err := parseParams(params...) + paramDef, paramVal, err := parseParams(conn.mssqlVersion2005, params...) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (conn *Conn) executeSqlSybase125(query string, params ...driver.Value) ([]* matches := re.FindAllSubmatchIndex([]byte(sql), -1) for i, _ := range matches { - _, escapedValue, _ := go2SqlDataType(params[i]) + _, escapedValue, _ := go2SqlDataType(conn.mssqlVersion2005, params[i]) sql = fmt.Sprintf("%s", strings.Replace(sql, "$bindkey", escapedValue, 1)) } @@ -81,7 +81,7 @@ func query2Statement(query string) (string, int) { return quote(statement), numParams } -func parseParams(params ...driver.Value) (string, string, error) { +func parseParams(sql_2005 bool, params ...driver.Value) (string, string, error) { paramDef := "" paramVal := "" for i, param := range params { @@ -89,7 +89,7 @@ func parseParams(params ...driver.Value) (string, string, error) { paramVal += ", " paramDef += ", " } - sqlType, sqlValue, err := go2SqlDataType(param) + sqlType, sqlValue, err := go2SqlDataType(sql_2005, param) if err != nil { return "", "", err } @@ -104,7 +104,7 @@ func quote(in string) string { return strings.Replace(in, "'", "''", -1) } -func go2SqlDataType(value interface{}) (string, string, error) { +func go2SqlDataType(sql_2005 bool, value interface{}) (string, string, error) { max := func(a int, b int) int { if a > b { return a @@ -136,6 +136,10 @@ func go2SqlDataType(value interface{}) (string, string, error) { case time.Time: { strValue = t.Format(time.RFC3339Nano) + if sql_2005 { + return "datetime", fmt.Sprintf("'%s'", quote(strValue)), nil + } + return "datetimeoffset", fmt.Sprintf("'%s'", quote(strValue)), nil } case []byte: diff --git a/executesql_test.go b/executesql_test.go index f268ee2..9d87e21 100644 --- a/executesql_test.go +++ b/executesql_test.go @@ -13,29 +13,30 @@ const ( ) func TestGoTo2SqlDataType2(t *testing.T) { - var checker = func(value interface{}, sqlType string, sqlFormatedValue string) { - actualSqlType, actualSqlFormatedValue, err := go2SqlDataType(value) + var checker = func(sql_2005 bool, value interface{}, sqlType string, sqlFormatedValue string) { + actualSqlType, actualSqlFormatedValue, err := go2SqlDataType(sql_2005, value) assert.Nil(t, err) assert.Equal(t, actualSqlType, sqlType) assert.Equal(t, actualSqlFormatedValue, sqlFormatedValue) } - checker(123, "int", "123") - checker(int64(123), "bigint", "123") - checker(int16(123), "smallint", "123") - checker(int8(123), "tinyint", "123") - checker(123.23, "real", "123.23") - checker(float64(123.23), "real", "123.23") + checker(false, 123, "int", "123") + checker(false, int64(123), "bigint", "123") + checker(false, int16(123), "smallint", "123") + checker(false, int8(123), "tinyint", "123") + checker(false, 123.23, "real", "123.23") + checker(false, float64(123.23), "real", "123.23") - checker("iso medo", "nvarchar (8)", "'iso medo'") - checker("iso medo isn't", "nvarchar (14)", "'iso medo isn''t'") + checker(false, "iso medo", "nvarchar (8)", "'iso medo'") + checker(false, "iso medo isn't", "nvarchar (14)", "'iso medo isn''t'") tm := time.Unix(1136239445, 0) paris, _ := time.LoadLocation("Europe/Paris") - checker(tm.In(paris), "datetimeoffset", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") + checker(false, tm.In(paris), "datetimeoffset", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") + checker(true, tm.In(paris), "datetime", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") - checker([]byte{1, 2, 3, 4, 5, 6, 7, 8}, "varbinary (8)", "0x0102030405060708") + checker(false, []byte{1, 2, 3, 4, 5, 6, 7, 8}, "varbinary (8)", "0x0102030405060708") //go2SqlDataType(t) } @@ -59,33 +60,34 @@ func TestQuery2Statement(t *testing.T) { } func TestGoTo2SqlDataType(t *testing.T) { - var checker = func(value interface{}, sqlType string, sqlFormatedValue string) { - actualSqlType, actualSqlFormatedValue, err := go2SqlDataType(value) + var checker = func(sql_2005 bool, value interface{}, sqlType string, sqlFormatedValue string) { + actualSqlType, actualSqlFormatedValue, err := go2SqlDataType(sql_2005, value) assert.Nil(t, err) assert.Equal(t, actualSqlType, sqlType) assert.Equal(t, actualSqlFormatedValue, sqlFormatedValue) } - checker(123, "int", "123") - checker(int64(123), "bigint", "123") - checker(int8(123), "tinyint", "123") - checker(123.23, "real", "123.23") - checker(float64(123.23), "real", "123.23") + checker(false, 123, "int", "123") + checker(false, int64(123), "bigint", "123") + checker(false, int8(123), "tinyint", "123") + checker(false, 123.23, "real", "123.23") + checker(false, float64(123.23), "real", "123.23") - checker("iso medo", "nvarchar (8)", "'iso medo'") - checker("iso medo isn't", "nvarchar (14)", "'iso medo isn''t'") + checker(false, "iso medo", "nvarchar (8)", "'iso medo'") + checker(false, "iso medo isn't", "nvarchar (14)", "'iso medo isn''t'") tm := time.Unix(1136239445, 0) paris, _ := time.LoadLocation("Europe/Paris") - checker(tm.In(paris), "datetimeoffset", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") + checker(false, tm.In(paris), "datetimeoffset", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") + checker(true, tm.In(paris), "datetime", "'"+tm.In(paris).Format(sqlDateTimeOffSet)+"'") - checker([]byte{1, 2, 3, 4, 5, 6, 7, 8}, "varbinary (8)", "0x0102030405060708") - checker(make([]byte, 8001), "varbinary (MAX)", "0x" + strings.Repeat("00", 8001)) + checker(false, []byte{1, 2, 3, 4, 5, 6, 7, 8}, "varbinary (8)", "0x0102030405060708") + checker(false, make([]byte, 8001), "varbinary (MAX)", "0x" + strings.Repeat("00", 8001)) - checker("", "nvarchar (1)", "''") - checker(true, "bit", "1") - checker(false, "bit", "0") + checker(false, "", "nvarchar (1)", "''") + checker(false, true, "bit", "1") + checker(false, false, "bit", "0") } func TestExecuteSqlNumberOfParams(t *testing.T) { @@ -96,7 +98,7 @@ func TestExecuteSqlNumberOfParams(t *testing.T) { } func TestParseParams(t *testing.T) { - def, val, err := parseParams(1, 2, "pero") + def, val, err := parseParams(false, 1, 2, "pero") assert.Nil(t, err) assert.Equal(t, def, "@p1 int, @p2 int, @p3 nvarchar (4)") assert.Equal(t, val, "@p1=1, @p2=2, @p3='pero'") From 45a4417208462af3985b0afff62379db3dda2529 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Thu, 2 Jul 2020 13:39:57 -0700 Subject: [PATCH 5/6] Fixed locking when creating new connections Attempting to run a SQL query after the DbProcMutex was locked was causing a permament lockup. Switched to running the SQL query asynchronously. --- conn.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/conn.go b/conn.go index 84531a8..b860005 100644 --- a/conn.go +++ b/conn.go @@ -236,10 +236,14 @@ func (conn *Conn) getDbProc() (*C.DBPROCESS, error) { return nil, dbProcError("dbopen error") } conn.readFreeTdsVersion() - var sql_runtime_err = conn.readMSSQLVersion() - if sql_runtime_err != nil { - return nil, sql_runtime_err - } + + //Calling asynchronously to work around the DbProcMutex lock + go func() { + var sql_runtime_err = conn.readMSSQLVersion() + if sql_runtime_err != nil { + fmt.Println("Error checking database version:", sql_runtime_err) + } + }() return dbproc, nil } From 80a4aa19e0191e319732050188a93a6487089c12 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Thu, 2 Jul 2020 15:40:39 -0700 Subject: [PATCH 6/6] Bugfix mssqlVersion2005's value was incorrect The value of conn.mssqlVersion2005 was incorrect on query runtime. This was either because it never got set, or didn't get set in time. Removed asynchronous function call and replaced it with a synchronous one in a spot of code that will not cause a lock to occur. --- conn.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/conn.go b/conn.go index b860005..3e99256 100644 --- a/conn.go +++ b/conn.go @@ -166,6 +166,12 @@ func (conn *Conn) connect() (*Conn, error) { conn.close() return nil, err } + + var sql_runtime_err = conn.readMSSQLVersion() + if sql_runtime_err != nil { + return nil, err + } + //log.Printf("freetds connected to %s@%s.%s", conn.user, conn.host, conn.database) return conn, nil } @@ -237,14 +243,6 @@ func (conn *Conn) getDbProc() (*C.DBPROCESS, error) { } conn.readFreeTdsVersion() - //Calling asynchronously to work around the DbProcMutex lock - go func() { - var sql_runtime_err = conn.readMSSQLVersion() - if sql_runtime_err != nil { - fmt.Println("Error checking database version:", sql_runtime_err) - } - }() - return dbproc, nil }