From d57e236b29b9adaf07b0ad84359156b51492e8b5 Mon Sep 17 00:00:00 2001 From: huiyu Date: Tue, 26 Apr 2022 10:07:39 +0800 Subject: [PATCH 1/6] size should be column size, to fix #178 --- param.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/param.go b/param.go index e5dd8ba..0978648 100644 --- a/param.go +++ b/param.go @@ -70,6 +70,9 @@ func (p *Parameter) BindValue(h api.SQLHSTMT, idx int, v driver.Value, conn *Con sqltype = api.SQL_WLONGVARCHAR case p.isDescribed: sqltype = p.SQLType + if p.Size != 0 { + size = p.Size + } case size <= 1: sqltype = api.SQL_WVARCHAR default: From 4afec60bd176702bb2ac6a4fa32d43c938ff2eb5 Mon Sep 17 00:00:00 2001 From: Saxon Chuang Date: Tue, 7 Jun 2022 13:03:27 +0800 Subject: [PATCH 2/6] [test] add unit test of #179 --- mssql_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/mssql_test.go b/mssql_test.go index 45d59b1..e2bc11d 100644 --- a/mssql_test.go +++ b/mssql_test.go @@ -1978,3 +1978,85 @@ func TestMSSQLQueryContextCancel(t *testing.T) { t.Fatalf("Unexpected error value: should=%s, is=%s", context.Canceled, err) } } + +// https://github.com/alexbrainman/odbc/issues/178 +// [WARN] this test will drop database named test +func TestMSSQLIssue178(t *testing.T) { + db, _, err := mssqlConnect() + if err != nil { + t.Fatal(err) + } + defer func() { + // not checking resources usage here, because these are + // unpredictable due to use of goroutines. + err := db.Close() + if err != nil { + t.Fatalf("error closing DB: %v", err) + } + }() + + func() { + type testStruct struct { + collate string + poem string + } + + testCases := []testStruct{ + { + collate: "Chinese_Taiwan_Bopomofo_CI_AS", + poem: "花間一壺酒,獨酌無相親。", + }, + { + collate: "Chinese_PRC_90_CI_AS", + poem: "花间一壶酒,独酌无相亲。", + }, + { + collate: "Japanese_CI_AS", + poem: "月日は百代の過客にして、行かふ年も又旅人也", + }, + { + collate: "Korean_Wansung_CI_AS", + poem: "꽃 사이 놓인 한 동이 술을 친한 이 없이 혼자 마시네.", + }, + } + + db.Exec("use master") + db.Exec("drop database test") + for _, testCase := range testCases { + func() { + db.Exec("create database test collate " + testCase.collate) + db.Exec("use test") + db.Exec("create table dbo.temp (poem varchar(100))") + + defer func() { + db.Exec("use master") + db.Exec("drop database test") + }() + + // insert poem of test case + s, err := db.Prepare("insert into dbo.temp (poem) values (?)") + if err != nil { + t.Fatal(err) + } + defer s.Close() + + _, err = s.Exec(testCase.poem) + if err != nil { + t.Fatal(err) + } + + // query inserted poem and compare with testCase.poem + var rowPoem string + if err = db.QueryRow( + "select cast(poem as nvarchar(max)) from dbo.temp", + ).Scan(&rowPoem); err != nil { + t.Fatalf("Scan: %v", err) + } + + if same := testCase.poem == rowPoem; !same { + t.Fatalf("poem missmatch, want=%v, got=%v", testCase.poem, rowPoem) + } + }() + } + }() +} From 1bd3b0f36d83fca360790fe852e7b7da674731b3 Mon Sep 17 00:00:00 2001 From: Saxon Chuang Date: Wed, 4 Mar 2026 14:45:52 +0800 Subject: [PATCH 3/6] test: refactor TestMSSQLIssue178 to use subtests and optimize db operations - adopted t.Run for better test isolation and readability. - avoided creating/dropping databases per test case; applied collation at the table level instead. - simplified db teardown using closeDB helper. --- mssql_test.go | 122 ++++++++++++++++++++++---------------------------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/mssql_test.go b/mssql_test.go index e2bc11d..da99280 100644 --- a/mssql_test.go +++ b/mssql_test.go @@ -1980,83 +1980,67 @@ func TestMSSQLQueryContextCancel(t *testing.T) { } // https://github.com/alexbrainman/odbc/issues/178 -// [WARN] this test will drop database named test func TestMSSQLIssue178(t *testing.T) { - db, _, err := mssqlConnect() + db, sc, err := mssqlConnect() if err != nil { t.Fatal(err) } - defer func() { - // not checking resources usage here, because these are - // unpredictable due to use of goroutines. - err := db.Close() - if err != nil { - t.Fatalf("error closing DB: %v", err) - } - }() + defer closeDB(t, db, sc, sc) - func() { - type testStruct struct { - collate string - poem string - } - - testCases := []testStruct{ - { - collate: "Chinese_Taiwan_Bopomofo_CI_AS", - poem: "花間一壺酒,獨酌無相親。", - }, - { - collate: "Chinese_PRC_90_CI_AS", - poem: "花间一壶酒,独酌无相亲。", - }, - { - collate: "Japanese_CI_AS", - poem: "月日は百代の過客にして、行かふ年も又旅人也", - }, - { - collate: "Korean_Wansung_CI_AS", - poem: "꽃 사이 놓인 한 동이 술을 친한 이 없이 혼자 마시네.", - }, - } - - db.Exec("use master") - db.Exec("drop database test") - for _, testCase := range testCases { - func() { - db.Exec("create database test collate " + testCase.collate) - db.Exec("use test") - db.Exec("create table dbo.temp (poem varchar(100))") + type testStruct struct { + name string + collate string + poem string + } - defer func() { - db.Exec("use master") - db.Exec("drop database test") - }() + testCases := []testStruct{ + { + name: "zh_tw_big5", + collate: "Chinese_Taiwan_Bopomofo_CI_AS", + poem: "花間一壺酒,獨酌無相親。", + }, + { + name: "zh_cn_gbk", + collate: "Chinese_PRC_90_CI_AS", + poem: "花间一壶酒,独酌无相亲。", + }, + { + name: "ja_jp", + collate: "Japanese_CI_AS", + poem: "月日は百代の過客にして、行かふ年も又旅人也", + }, + { + name: "ko_kr", + collate: "Korean_Wansung_CI_AS", + poem: "꽃 사이 놓인 한 동이 술을 친한 이 없이 혼자 마시네.", + }, + } - // insert poem of test case - s, err := db.Prepare("insert into dbo.temp (poem) values (?)") - if err != nil { - t.Fatal(err) - } - defer s.Close() + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + db.Exec("drop table dbo.temp") + exec(t, db, "create table dbo.temp (poem varchar(200) collate "+tc.collate+")") - _, err = s.Exec(testCase.poem) - if err != nil { - t.Fatal(err) - } + stmt, err := db.Prepare("insert into dbo.temp (poem) values (?)") + if err != nil { + t.Fatal(err) + } + defer stmt.Close() - // query inserted poem and compare with testCase.poem - var rowPoem string - if err = db.QueryRow( - "select cast(poem as nvarchar(max)) from dbo.temp", - ).Scan(&rowPoem); err != nil { - t.Fatalf("Scan: %v", err) - } + if _, err := stmt.Exec(tc.poem); err != nil { + t.Fatal(err) + } - if same := testCase.poem == rowPoem; !same { - t.Fatalf("poem missmatch, want=%v, got=%v", testCase.poem, rowPoem) - } - }() - } - }() + var got string + if err := db.QueryRow("select cast(poem as nvarchar(max)) from dbo.temp").Scan(&got); err != nil { + t.Fatal(err) + } + if got != tc.poem { + t.Fatalf("poem mismatch, want=%v, got=%v", tc.poem, got) + } + + exec(t, db, "drop table dbo.temp") + }) + } } From 8a99baf4aae64e2d8ce6642389bd76571a53d8bf Mon Sep 17 00:00:00 2001 From: Saxon Chuang Date: Wed, 4 Mar 2026 15:50:58 +0800 Subject: [PATCH 4/6] test: update TestMSSQLIssue178 to use NVARCHAR for proper collation handling --- mssql_test.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/mssql_test.go b/mssql_test.go index da99280..108d48f 100644 --- a/mssql_test.go +++ b/mssql_test.go @@ -2020,8 +2020,23 @@ func TestMSSQLIssue178(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { db.Exec("drop table dbo.temp") - exec(t, db, "create table dbo.temp (poem varchar(200) collate "+tc.collate+")") - + exec(t, db, "create table dbo.temp (poem nvarchar(200) collate "+tc.collate+")") + + // when the column is VARCHAR with a specific collation, converting + // the parameter on the server side using that collation ensures the + // NVARCHAR value we send is mapped into the correct code page. if + // we simply send the parameter as NVARCHAR and insert directly into a + // VARCHAR column the conversion uses the database default collation and + // we lose characters for languages not covered by that code page. + // + // cast the incoming nvarchar parameter to varchar and give the + // cast the desired collation. this way the conversion from + // Unicode to the column code page uses the correct collation, + // not the database default. + // + // column is NVARCHAR so we can insert directly without worrying + // about code page conversions; the collation on nvarchar only + // affects comparisons and sort order, not storage. stmt, err := db.Prepare("insert into dbo.temp (poem) values (?)") if err != nil { t.Fatal(err) From 4100c147b419ead070c2d48b61fec3b6294eca49 Mon Sep 17 00:00:00 2001 From: Saxon Chuang Date: Wed, 4 Mar 2026 16:07:42 +0800 Subject: [PATCH 5/6] test: add unit test for parameter size description in MSSQL --- mssql_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ param.go | 5 +++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mssql_test.go b/mssql_test.go index 108d48f..8c25651 100644 --- a/mssql_test.go +++ b/mssql_test.go @@ -2059,3 +2059,68 @@ func TestMSSQLIssue178(t *testing.T) { }) } } + +// https://github.com/alexbrainman/odbc/issues/178 +// verify that an SQL statement which provides a parameter description +// causes BindValue to honor the described size rather than the length of +// the actual string. this test will fail if the size override lines in +// param.go are removed (they were added in #178). +func TestMSSQLDescribeParameterSize(t *testing.T) { + if testing.Short() { + t.Skip("skip in short mode") + } + db, _, err := mssqlConnectWithParams(newConnParams()) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + // first, make sure the driver actually describes the parameter and returns + // the expected size. we use the Raw method to gain access to the + // underlying *Conn and call PrepareODBCStmt directly. + conn, err2 := db.Conn(context.Background()) + if err2 != nil { + t.Fatalf("failed to get raw connection: %v", err2) + } + defer conn.Close() + err = conn.Raw(func(dc interface{}) error { + c := dc.(*Conn) + stmt, err := c.PrepareODBCStmt("select cast(? as varchar(5))") + if err != nil { + return err + } + if len(stmt.Parameters) != 1 { + return fmt.Errorf("unexpected param count %d", len(stmt.Parameters)) + } + p := stmt.Parameters[0] + if !p.isDescribed || p.Size != 5 { + return fmt.Errorf("expected described size 5, got isDescribed=%v size=%d", p.isDescribed, p.Size) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + + // intercept SQLBindParameter to capture the size argument used by BindValue + var boundSize api.SQLULEN + orig := sqlBindParameter + sqlBindParameter = func(h api.SQLHSTMT, paramNumber api.SQLUSMALLINT, + inputOutputType api.SQLSMALLINT, cType api.SQLSMALLINT, sqlType api.SQLSMALLINT, + size api.SQLULEN, decimal api.SQLSMALLINT, + buffer api.SQLPOINTER, bufferLength api.SQLLEN, strLenOrIndPtr *api.SQLLEN, + ) api.SQLRETURN { + boundSize = size + return orig(h, paramNumber, inputOutputType, cType, sqlType, + size, decimal, buffer, bufferLength, strLenOrIndPtr) + } + defer func() { sqlBindParameter = orig }() + + // execute the statement once; the cast will raise a truncation error when + // the input exceeds five characters, but that is irrelevant to capturing + // the bound size. + _, _ = db.Exec("select cast(? as varchar(5))", "abcdef") + if boundSize != 5 { + t.Fatalf("expected bound size 5, got %d", boundSize) + } +} diff --git a/param.go b/param.go index 0978648..ea279f8 100644 --- a/param.go +++ b/param.go @@ -32,6 +32,9 @@ func (p *Parameter) StoreStrLen_or_IndPtr(v api.SQLLEN) *api.SQLLEN { } +// exposeable hook for binding; tests override this to capture arguments. +var sqlBindParameter = api.SQLBindParameter + func (p *Parameter) BindValue(h api.SQLHSTMT, idx int, v driver.Value, conn *Conn) error { // TODO(brainman): Reuse memory for previously bound values. If memory // is reused, we, probably, do not need to call SQLBindParameter either. @@ -166,7 +169,7 @@ func (p *Parameter) BindValue(h api.SQLHSTMT, idx int, v driver.Value, conn *Con default: return fmt.Errorf("unsupported type %T", v) } - ret := api.SQLBindParameter(h, api.SQLUSMALLINT(idx+1), + ret := sqlBindParameter(h, api.SQLUSMALLINT(idx+1), api.SQL_PARAM_INPUT, ctype, sqltype, size, decimal, api.SQLPOINTER(buf), buflen, plen) if IsError(ret) { From f93fff2461679fca3a6ed8d6c23ed955cb70e91d Mon Sep 17 00:00:00 2001 From: Saxon Chuang Date: Wed, 4 Mar 2026 16:08:11 +0800 Subject: [PATCH 6/6] test: rename TestMSSQLIssue178 to TestMSSQLNVarcharCollationPreservesUnicode and update comments for clarity --- mssql_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mssql_test.go b/mssql_test.go index 8c25651..520849a 100644 --- a/mssql_test.go +++ b/mssql_test.go @@ -1980,7 +1980,12 @@ func TestMSSQLQueryContextCancel(t *testing.T) { } // https://github.com/alexbrainman/odbc/issues/178 -func TestMSSQLIssue178(t *testing.T) { +// verify that inserting unicode text into an NVARCHAR column +// with a specified collation preserves the original characters when +// the parameter is sent from Go. The collation on NVARCHAR only affects +// comparisons and sort order, not storage. This test reproduces +// behavior originally reported in issue #178. +func TestMSSQLNVarcharCollationPreservesUnicode(t *testing.T) { db, sc, err := mssqlConnect() if err != nil { t.Fatal(err)