Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scanner should return one of int64,float64,bool,[]byte,string,time.Time,nil , but I got uint64 #1575

Open
bronze1man opened this issue Mar 25, 2024 · 3 comments

Comments

@bronze1man
Copy link

bronze1man commented Mar 25, 2024

It maybe a bug of database/sql , if it is ,please tell me.

Issue description

https://pkg.go.dev/database/sql#Scanner says Scanner should return one of int64,float64,bool,[]byte,string,time.Time,nil , but I got uint64

Example code

the sql:

select * from INFORMATION_SCHEMA.TABLES

some of my scaner_t implement.

type scaner_t struct{
	src any
}
func (s *scaner_t) Scan(src any) error{
	s.src = src
	return nil
}
func (s *scaner_t) AsString() (outS string,err error){
	switch obj:=s.src.(type) {
	case int64:
		return strconv.FormatInt(obj,10),nil
	case float64:
		return strconv.FormatFloat(obj,'f',-1,64),nil
	case bool:
		return strconv.FormatBool(obj),nil
	case []byte:
		return string(obj),nil
	case string:
		return obj,nil
	case time.Time:
		b,err:=obj.MarshalText()
		if err!=nil{
			return "",err
		}
		return string(b),nil
	case nil:
		return "",nil
	default:
		return "",errors.New("unknow type "+fmt.Sprintf("%T",s.src))
	}
}

Error log

unknow type uint64

Configuration

Driver version (or git SHA):
github.com/go-sql-driver/mysql v1.8.0

Go version: run go version in your console
go version go1.19.2 darwin/amd64

Server version: E.g. MySQL 5.6, MariaDB 10.0.20

mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.44    |
+-----------+

Server OS: E.g. Debian 8.1 (Jessie), Windows 10
mac os 14.2.1

@methane
Copy link
Member

methane commented Mar 25, 2024

Yes, it violates database/sql convention.
But this violation is necessary to support uint64. So we can not fix it until database/sql supports uint64 natively.

@bronze1man
Copy link
Author

@methane Is there any document or examples about how to implement sql.Scanner to work with https://github.com/go-sql-driver/mysql?
I do not like try and fail again and again.

@methane
Copy link
Member

methane commented Mar 26, 2024

There is perfect document.

https://cs.opensource.google/go/go/+/master:src/database/sql/convert.go;l=219-480

mysql/packets.go

Lines 769 to 856 in d7ddb8b

func (rows *textRows) readRow(dest []driver.Value) error {
mc := rows.mc
if rows.rs.done {
return io.EOF
}
data, err := mc.readPacket()
if err != nil {
return err
}
// EOF Packet
if data[0] == iEOF && len(data) == 5 {
// server_status [2 bytes]
rows.mc.status = readStatus(data[3:])
rows.rs.done = true
if !rows.HasNextResultSet() {
rows.mc = nil
}
return io.EOF
}
if data[0] == iERR {
rows.mc = nil
return mc.handleErrorPacket(data)
}
// RowSet Packet
var (
n int
isNull bool
pos int = 0
)
for i := range dest {
// Read bytes and convert to string
var buf []byte
buf, isNull, n, err = readLengthEncodedString(data[pos:])
pos += n
if err != nil {
return err
}
if isNull {
dest[i] = nil
continue
}
switch rows.rs.columns[i].fieldType {
case fieldTypeTimestamp,
fieldTypeDateTime,
fieldTypeDate,
fieldTypeNewDate:
if mc.parseTime {
dest[i], err = parseDateTime(buf, mc.cfg.Loc)
} else {
dest[i] = buf
}
case fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeYear, fieldTypeLong:
dest[i], err = strconv.ParseInt(string(buf), 10, 64)
case fieldTypeLongLong:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
dest[i], err = strconv.ParseUint(string(buf), 10, 64)
} else {
dest[i], err = strconv.ParseInt(string(buf), 10, 64)
}
case fieldTypeFloat:
var d float64
d, err = strconv.ParseFloat(string(buf), 32)
dest[i] = float32(d)
case fieldTypeDouble:
dest[i], err = strconv.ParseFloat(string(buf), 64)
default:
dest[i] = buf
}
if err != nil {
return err
}
}
return nil
}

mysql/packets.go

Lines 1230 to 1403 in d7ddb8b

func (rows *binaryRows) readRow(dest []driver.Value) error {
data, err := rows.mc.readPacket()
if err != nil {
return err
}
// packet indicator [1 byte]
if data[0] != iOK {
// EOF Packet
if data[0] == iEOF && len(data) == 5 {
rows.mc.status = readStatus(data[3:])
rows.rs.done = true
if !rows.HasNextResultSet() {
rows.mc = nil
}
return io.EOF
}
mc := rows.mc
rows.mc = nil
// Error otherwise
return mc.handleErrorPacket(data)
}
// NULL-bitmap, [(column-count + 7 + 2) / 8 bytes]
pos := 1 + (len(dest)+7+2)>>3
nullMask := data[1:pos]
for i := range dest {
// Field is NULL
// (byte >> bit-pos) % 2 == 1
if ((nullMask[(i+2)>>3] >> uint((i+2)&7)) & 1) == 1 {
dest[i] = nil
continue
}
// Convert to byte-coded string
switch rows.rs.columns[i].fieldType {
case fieldTypeNULL:
dest[i] = nil
continue
// Numeric Types
case fieldTypeTiny:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
dest[i] = int64(data[pos])
} else {
dest[i] = int64(int8(data[pos]))
}
pos++
continue
case fieldTypeShort, fieldTypeYear:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
dest[i] = int64(binary.LittleEndian.Uint16(data[pos : pos+2]))
} else {
dest[i] = int64(int16(binary.LittleEndian.Uint16(data[pos : pos+2])))
}
pos += 2
continue
case fieldTypeInt24, fieldTypeLong:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
dest[i] = int64(binary.LittleEndian.Uint32(data[pos : pos+4]))
} else {
dest[i] = int64(int32(binary.LittleEndian.Uint32(data[pos : pos+4])))
}
pos += 4
continue
case fieldTypeLongLong:
if rows.rs.columns[i].flags&flagUnsigned != 0 {
val := binary.LittleEndian.Uint64(data[pos : pos+8])
if val > math.MaxInt64 {
dest[i] = uint64ToString(val)
} else {
dest[i] = int64(val)
}
} else {
dest[i] = int64(binary.LittleEndian.Uint64(data[pos : pos+8]))
}
pos += 8
continue
case fieldTypeFloat:
dest[i] = math.Float32frombits(binary.LittleEndian.Uint32(data[pos : pos+4]))
pos += 4
continue
case fieldTypeDouble:
dest[i] = math.Float64frombits(binary.LittleEndian.Uint64(data[pos : pos+8]))
pos += 8
continue
// Length coded Binary Strings
case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar,
fieldTypeBit, fieldTypeEnum, fieldTypeSet, fieldTypeTinyBLOB,
fieldTypeMediumBLOB, fieldTypeLongBLOB, fieldTypeBLOB,
fieldTypeVarString, fieldTypeString, fieldTypeGeometry, fieldTypeJSON:
var isNull bool
var n int
dest[i], isNull, n, err = readLengthEncodedString(data[pos:])
pos += n
if err == nil {
if !isNull {
continue
} else {
dest[i] = nil
continue
}
}
return err
case
fieldTypeDate, fieldTypeNewDate, // Date YYYY-MM-DD
fieldTypeTime, // Time [-][H]HH:MM:SS[.fractal]
fieldTypeTimestamp, fieldTypeDateTime: // Timestamp YYYY-MM-DD HH:MM:SS[.fractal]
num, isNull, n := readLengthEncodedInteger(data[pos:])
pos += n
switch {
case isNull:
dest[i] = nil
continue
case rows.rs.columns[i].fieldType == fieldTypeTime:
// database/sql does not support an equivalent to TIME, return a string
var dstlen uint8
switch decimals := rows.rs.columns[i].decimals; decimals {
case 0x00, 0x1f:
dstlen = 8
case 1, 2, 3, 4, 5, 6:
dstlen = 8 + 1 + decimals
default:
return fmt.Errorf(
"protocol error, illegal decimals value %d",
rows.rs.columns[i].decimals,
)
}
dest[i], err = formatBinaryTime(data[pos:pos+int(num)], dstlen)
case rows.mc.parseTime:
dest[i], err = parseBinaryDateTime(num, data[pos:], rows.mc.cfg.Loc)
default:
var dstlen uint8
if rows.rs.columns[i].fieldType == fieldTypeDate {
dstlen = 10
} else {
switch decimals := rows.rs.columns[i].decimals; decimals {
case 0x00, 0x1f:
dstlen = 19
case 1, 2, 3, 4, 5, 6:
dstlen = 19 + 1 + decimals
default:
return fmt.Errorf(
"protocol error, illegal decimals value %d",
rows.rs.columns[i].decimals,
)
}
}
dest[i], err = formatBinaryDateTime(data[pos:pos+int(num)], dstlen)
}
if err == nil {
pos += int(num)
continue
} else {
return err
}
// Please report if this happens!
default:
return fmt.Errorf("unknown field type %d", rows.rs.columns[i].fieldType)
}
}

It must be readable than my English.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants