alexbrainman / odbc

odbc driver written in go
BSD 3-Clause "New" or "Revised" License
348 stars 139 forks source link

向 varchar字段保存中文中会被截断 #182

Open dotqi opened 1 year ago

dotqi commented 1 year ago

向 varchar字段保存中文中会被截断。 测试发现。文件param.go绑定strinig时。size取的是转为UTF16后的长度。 但一个中文在UTF8时占用3个字节。size应该取UTF8的长度。 这样才可以保证向varchar字段保存中文时不会被截断。

Salmon-x commented 1 year ago

所以应该改动哪里呢

dotqi commented 1 year ago

我个人的处理办法是在param.go的67行下增加

        if p.isDescribed && (p.SQLType == api.SQL_VARCHAR || p.SQLType == api.SQL_CHAR) {
            size = api.SQLULEN(len(d))
            if size < 1 {
                // size cannot be less then 1 even for empty fields
                size = 1
            }
        }

个人能力有限。你要好好测试一下。

fanybook commented 1 year ago

@dotqi 好奇请教一下

UTF16 和 UTF8 在中文长度上有区别么?

dotqi commented 1 year ago

package main

import ( "fmt" "unicode/utf16" )

func main() { val := "你好" fmt.Println(val) u16 := StringToUTF16(val) fmt.Println("u16", len(u16), u16) // u16 3 [20320 22909 0] u8 := []byte(val) fmt.Println("u8", len(u8), u8) // u8 6 [228 189 160 229 165 189] }

func StringToUTF16(s string) []uint16 { return utf16.Encode([]rune(s + "\x00")) }

fanybook commented 1 year ago

package main

import ( "fmt" "unicode/utf16" )

func main() { val := "你好" fmt.Println(val) u16 := StringToUTF16(val) fmt.Println("u16", len(u16), u16) // u16 3 [20320 22909 0] u8 := []byte(val) fmt.Println("u8", len(u8), u8) // u8 6 [228 189 160 229 165 189] }

func StringToUTF16(s string) []uint16 { return utf16.Encode([]rune(s + "\x00")) }

@dotqi 你这个实验,我做了,感觉你这个不对

  1. UTF16 和 UTF8 都是可变长度的编码,区别在于 UTF16 最小是两字节,UTF8 最小是一字节
  2. 你这个计算的是 []uint16 的长度,而非 []byte 长度
    u16 := StringToUTF16(val)
    fmt.Println("u16", len(u16), u16)
  3. 你这个计算的是 []byte 的长度
    u8 := []byte(val)
    fmt.Println("u8", len(u8), u8)

但实际上,u16 可以转回成 []rune,他们的长度是一模一样的

val := "你好𠀾" []uint16 = [20320 22909 55360 56382] []rune = [20320 22909 131134] []byte = [228 189 160 229 165 189 240 160 128 190]

rune 实际是 int32,比 uint16 表示的多,131134 和 55360 56382 都表示 “𠀾”,它实际占 4 个字节 [240 160 128 190]

fanybook commented 1 year ago

@dotqi

分析:

  1. size = api.SQLULEN(len(d)) 看你改的方式,实际上是 len(s),其实就是 []byte 的长度
  2. 看这个库原来的方式,比较奇怪,它底层是用的 cgo 调的 c 的 odbc,不明白它为什么转 utf16 而不是转 byte,因为 c 里边都是 char,api.StringToUTF16 给字符串后面加了一个 \0,就是告诉 c 这是一个字符串,它转完 utf16 又给长度*2 做了 buflen
        b := api.StringToUTF16(d)
        p.Data = b
        buf = unsafe.Pointer(&b[0])
        l := len(b)
        l -= 1 // remove terminating 0
        size = api.SQLULEN(l)
        if size < 1 {
            // size cannot be less then 1 even for empty fields
            size = 1
        }
        l *= 2 // every char takes 2 bytes
        buflen = api.SQLLEN(l)
        plen = p.StoreStrLen_or_IndPtr(buflen)
  3. []byte 长度可以为 0,string 就必须是1,应该是因为 \0 的缘故

错误的点: l *= 2 // every char takes 2 bytes 4字节的 utf16 转换后是 2 个 uint16,2 没问题 但是 3字节的 utf16 转换后可能是 1 个 uint16,这里 2 就错了,*3才对

按照 case []byte: 的改写:

    case string:
        ctype = api.SQL_C_WCHAR
        // b := api.StringToUTF16(d)
        // p.Data = b
        // buf = unsafe.Pointer(&b[0])
        // l := len(b)
        // l -= 1 // remove terminating 0
        // size = api.SQLULEN(l)
        // if size < 1 {
        //  // size cannot be less then 1 even for empty fields
        //  size = 1
        // }
        // l *= 2 // every char takes 2 bytes
        // buflen = api.SQLLEN(l)
        // plen = p.StoreStrLen_or_IndPtr(buflen)
        b := []byte(d)
        p.Data = append(b, byte(0))
        buf = unsafe.Pointer(&b[0])
        buflen = api.SQLLEN(len(b))
        plen = p.StoreStrLen_or_IndPtr(buflen)
        size = api.SQLULEN(len(b))
        if size < 1 {
            // size cannot be less then 1 even for empty fields
            size = 1
        }

大胆点:因为使用了 append 所以不用再判断 size < 1 了,我不太明白为什么 buflen 不能包含 \0 的长度,所以参照原来减 1 了

https://learn.microsoft.com/zh-Cn/sql/odbc/reference/syntax/sqlbindparameter-function?view=sql-server-ver16

b := append([]byte(d), byte(0))
p.Data = b
buf = unsafe.Pointer(&b[0])
buflen = api.SQLLEN(len(b) - 1)
plen = p.StoreStrLen_or_IndPtr(buflen)
size = api.SQLULEN(len(b))
dotqi commented 1 year ago

谢谢,回头按你的方式测试一下。

zhangyongding commented 3 months ago

增加一个PR:https://github.com/alexbrainman/odbc/pull/195