Repository: vitessio/vitess
Pull request #6132 (2020-04-30 04:11:31)

Implements Send Primitive's GetFields
Signed-off-by: Harshit Gangal <harshit@planetscale.com>


Patch:
diff --git a/go/mysql/client.go b/go/mysql/client.go
index 7785760dea..a76ad0c030 100644
--- a/go/mysql/client.go
+++ b/go/mysql/client.go
@@ -177,10 +177,8 @@ func Connect(ctx context.Context, params *ConnParams) (*Conn, error) {
 func (c *Conn) Ping() error {
 	// This is a new command, need to reset the sequence.
 	c.sequence = 0
-	data, pos := c.startEphemeralPacketWithHeader(1)
-	data[pos] = ComPing
 
-	if err := c.writeEphemeralPacket(); err != nil {
+	if err := c.writePacket([]byte{ComPing}); err != nil {
 		return NewSQLError(CRServerGone, SSUnknownSQLState, "%v", err)
 	}
 	data, err := c.readEphemeralPacket()
@@ -544,7 +542,8 @@ func (c *Conn) writeSSLRequest(capabilities uint32, characterSet uint8, params *
 		flags |= CapabilityClientConnectWithDB
 	}
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	// Client capability flags.
 	pos = writeUint32(data, pos, flags)
@@ -606,7 +605,8 @@ func (c *Conn) writeHandshakeResponse41(capabilities uint32, scrambledPassword [
 		length++
 	}
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	// Client capability flags.
 	pos = writeUint32(data, pos, flags)
@@ -672,7 +672,8 @@ func parseAuthSwitchRequest(data []byte) (string, []byte, error) {
 // Returns a SQLError.
 func (c *Conn) writeClearTextPassword(params *ConnParams) error {
 	length := len(params.Pass) + 1
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	pos = writeNullString(data, pos, params.Pass)
 	// Sanity check.
 	if pos != len(data) {
@@ -685,7 +686,8 @@ func (c *Conn) writeClearTextPassword(params *ConnParams) error {
 // Returns a SQLError.
 func (c *Conn) writeMysqlNativePassword(params *ConnParams, salt []byte) error {
 	scrambledPassword := ScramblePassword(salt, []byte(params.Pass))
-	data, pos := c.startEphemeralPacketWithHeader(len(scrambledPassword))
+	data := c.startEphemeralPacket(len(scrambledPassword))
+	pos := 0
 	pos += copy(data[pos:], scrambledPassword)
 	// Sanity check.
 	if pos != len(data) {
diff --git a/go/mysql/conn.go b/go/mysql/conn.go
index d515ec6c7a..8cb4d04041 100644
--- a/go/mysql/conn.go
+++ b/go/mysql/conn.go
@@ -45,10 +45,6 @@ const (
 	// connBufferSize is how much we buffer for reading and
 	// writing. It is also how much we allocate for ephemeral buffers.
 	connBufferSize = 16 * 1024
-
-	// packetHeaderSize is the 4 bytes of header per MySQL packet
-	// sent over
-	packetHeaderSize = 4
 )
 
 // Constants for how ephemeral buffers were used for reading / writing.
@@ -164,7 +160,7 @@ type Conn struct {
 	// Keep track of how and of the buffer we allocated for an
 	// ephemeral packet on the read and write sides.
 	// These fields are used by:
-	// - startEphemeralPacketWithHeader / writeEphemeralPacket methods for writes.
+	// - startEphemeralPacket / writeEphemeralPacket methods for writes.
 	// - readEphemeralPacket / recycleReadPacket methods for reads.
 	currentEphemeralPolicy int
 	// currentEphemeralBuffer for tracking allocated temporary buffer for writes and reads respectively.
@@ -301,7 +297,7 @@ func (c *Conn) getReader() io.Reader {
 }
 
 func (c *Conn) readHeaderFrom(r io.Reader) (int, error) {
-	var header [packetHeaderSize]byte
+	var header [4]byte
 	// Note io.ReadFull will return two different types of errors:
 	// 1. if the socket is already closed, and the go runtime knows it,
 	//   then ReadFull will return an error (different than EOF),
@@ -515,49 +511,47 @@ func (c *Conn) ReadPacket() ([]byte, error) {
 // writePacket writes a packet, possibly cutting it into multiple
 // chunks.  Note this is not very efficient, as the client probably
 // has to build the []byte and that makes a memory copy.
-// Try to use startEphemeralPacketWithHeader/writeEphemeralPacket instead.
+// Try to use startEphemeralPacket/writeEphemeralPacket instead.
 //
 // This method returns a generic error, not a SQLError.
 func (c *Conn) writePacket(data []byte) error {
 	index := 0
-	dataLength := len(data) - packetHeaderSize
+	length := len(data)
 
 	w, unget := c.getWriter()
 	defer unget()
 
-	var header [packetHeaderSize]byte
 	for {
-		// toBeSent is capped to MaxPacketSize.
-		toBeSent := dataLength
-		if toBeSent > MaxPacketSize {
-			toBeSent = MaxPacketSize
+		// Packet length is capped to MaxPacketSize.
+		packetLength := length
+		if packetLength > MaxPacketSize {
+			packetLength = MaxPacketSize
 		}
 
-		// save the first 4 bytes of the payload, we will overwrite them with the
-		// header below
-		copy(header[0:packetHeaderSize], data[index:index+packetHeaderSize])
-
 		// Compute and write the header.
-		data[index] = byte(toBeSent)
-		data[index+1] = byte(toBeSent >> 8)
-		data[index+2] = byte(toBeSent >> 16)
-		data[index+3] = c.sequence
+		var header [4]byte
+		header[0] = byte(packetLength)
+		header[1] = byte(packetLength >> 8)
+		header[2] = byte(packetLength >> 16)
+		header[3] = c.sequence
+		if n, err := w.Write(header[:]); err != nil {
+			return vterrors.Wrapf(err, "Write(header) failed")
+		} else if n != 4 {
+			return vterrors.Errorf(vtrpc.Code_INTERNAL, "Write(header) returned a short write: %v < 4", n)
+		}
 
 		// Write the body.
-		if n, err := w.Write(data[index : index+toBeSent+packetHeaderSize]); err != nil {
+		if n, err := w.Write(data[index : index+packetLength]); err != nil {
 			return vterrors.Wrapf(err, "Write(packet) failed")
-		} else if n != (toBeSent + packetHeaderSize) {
-			return vterrors.Errorf(vtrpc.Code_INTERNAL, "Write(packet) returned a short write: %v < %v", n, (toBeSent + packetHeaderSize))
+		} else if n != packetLength {
+			return vterrors.Errorf(vtrpc.Code_INTERNAL, "Write(packet) returned a short write: %v < %v", n, packetLength)
 		}
 
-		// restore the first 4 bytes once the network send is done
-		copy(data[index:index+packetHeaderSize], header[0:packetHeaderSize])
-
 		// Update our state.
 		c.sequence++
-		dataLength -= toBeSent
-		if dataLength == 0 {
-			if toBeSent == MaxPacketSize {
+		length -= packetLength
+		if length == 0 {
+			if packetLength == MaxPacketSize {
 				// The packet we just sent had exactly
 				// MaxPacketSize size, we need to
 				// sent a zero-size packet too.
@@ -567,30 +561,30 @@ func (c *Conn) writePacket(data []byte) error {
 				header[3] = c.sequence
 				if n, err := w.Write(header[:]); err != nil {
 					return vterrors.Wrapf(err, "Write(empty header) failed")
-				} else if n != packetHeaderSize {
+				} else if n != 4 {
 					return vterrors.Errorf(vtrpc.Code_INTERNAL, "Write(empty header) returned a short write: %v < 4", n)
 				}
 				c.sequence++
 			}
 			return nil
 		}
-		index += toBeSent
+		index += packetLength
 	}
 }
 
-func (c *Conn) startEphemeralPacketWithHeader(length int) ([]byte, int) {
+func (c *Conn) startEphemeralPacket(length int) []byte {
 	if c.currentEphemeralPolicy != ephemeralUnused {
-		panic("startEphemeralPacketWithHeader cannot be used while a packet is already started.")
+		panic("startEphemeralPacket cannot be used while a packet is already started.")
 	}
 
 	c.currentEphemeralPolicy = ephemeralWrite
 	// get buffer from pool or it'll be allocated if length is too big
-	c.currentEphemeralBuffer = bufPool.Get(length + packetHeaderSize)
-	return *c.currentEphemeralBuffer, packetHeaderSize
+	c.currentEphemeralBuffer = bufPool.Get(length)
+	return *c.currentEphemeralBuffer
 }
 
 // writeEphemeralPacket writes the packet that was allocated by
-// startEphemeralPacketWithHeader.
+// startEphemeralPacket.
 func (c *Conn) writeEphemeralPacket() error {
 	defer c.recycleWritePacket()
 
@@ -628,8 +622,8 @@ func (c *Conn) writeComQuit() error {
 	// This is a new command, need to reset the sequence.
 	c.sequence = 0
 
-	data, pos := c.startEphemeralPacketWithHeader(1)
-	data[pos] = ComQuit
+	data := c.startEphemeralPacket(1)
+	data[0] = ComQuit
 	if err := c.writeEphemeralPacket(); err != nil {
 		return NewSQLError(CRServerGone, SSUnknownSQLState, err.Error())
 	}
@@ -679,7 +673,8 @@ func (c *Conn) writeOKPacket(affectedRows, lastInsertID uint64, flags uint16, wa
 		lenEncIntSize(lastInsertID) +
 		2 + // flags
 		2 // warnings
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	pos = writeByte(data, pos, OKPacket)
 	pos = writeLenEncInt(data, pos, affectedRows)
 	pos = writeLenEncInt(data, pos, lastInsertID)
@@ -700,7 +695,8 @@ func (c *Conn) writeOKPacketWithEOFHeader(affectedRows, lastInsertID uint64, fla
 		lenEncIntSize(lastInsertID) +
 		2 + // flags
 		2 // warnings
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	pos = writeByte(data, pos, EOFPacket)
 	pos = writeLenEncInt(data, pos, affectedRows)
 	pos = writeLenEncInt(data, pos, lastInsertID)
@@ -716,7 +712,8 @@ func (c *Conn) writeOKPacketWithEOFHeader(affectedRows, lastInsertID uint64, fla
 func (c *Conn) writeErrorPacket(errorCode uint16, sqlState string, format string, args ...interface{}) error {
 	errorMessage := fmt.Sprintf(format, args...)
 	length := 1 + 2 + 1 + 5 + len(errorMessage)
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	pos = writeByte(data, pos, ErrPacket)
 	pos = writeUint16(data, pos, errorCode)
 	pos = writeByte(data, pos, '#')
@@ -746,7 +743,8 @@ func (c *Conn) writeErrorPacketFromError(err error) error {
 // doesn't flush (as it is used as part of a query result).
 func (c *Conn) writeEOFPacket(flags uint16, warnings uint16) error {
 	length := 5
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	pos = writeByte(data, pos, EOFPacket)
 	pos = writeUint16(data, pos, warnings)
 	_ = writeUint16(data, pos, flags)
diff --git a/go/mysql/query.go b/go/mysql/query.go
index 37cb8e50e9..ef37f6eb5a 100644
--- a/go/mysql/query.go
+++ b/go/mysql/query.go
@@ -42,10 +42,9 @@ func (c *Conn) WriteComQuery(query string) error {
 	// This is a new command, need to reset the sequence.
 	c.sequence = 0
 
-	data, pos := c.startEphemeralPacketWithHeader(len(query) + 1)
-	data[pos] = ComQuery
-	pos++
-	copy(data[pos:], query)
+	data := c.startEphemeralPacket(len(query) + 1)
+	data[0] = ComQuery
+	copy(data[1:], query)
 	if err := c.writeEphemeralPacket(); err != nil {
 		return NewSQLError(CRServerGone, SSUnknownSQLState, err.Error())
 	}
@@ -56,10 +55,9 @@ func (c *Conn) WriteComQuery(query string) error {
 // Client -> Server.
 // Returns SQLError(CRServerGone) if it can't.
 func (c *Conn) writeComInitDB(db string) error {
-	data, pos := c.startEphemeralPacketWithHeader(len(db) + 1)
-	data[pos] = ComInitDB
-	pos++
-	copy(data[pos:], db)
+	data := c.startEphemeralPacket(len(db) + 1)
+	data[0] = ComInitDB
+	copy(data[1:], db)
 	if err := c.writeEphemeralPacket(); err != nil {
 		return NewSQLError(CRServerGone, SSUnknownSQLState, err.Error())
 	}
@@ -69,10 +67,9 @@ func (c *Conn) writeComInitDB(db string) error {
 // writeComSetOption changes the connection's capability of executing multi statements.
 // Returns SQLError(CRServerGone) if it can't.
 func (c *Conn) writeComSetOption(operation uint16) error {
-	data, pos := c.startEphemeralPacketWithHeader(16 + 1)
-	data[pos] = ComSetOption
-	pos++
-	writeUint16(data, pos, operation)
+	data := c.startEphemeralPacket(16 + 1)
+	data[0] = ComSetOption
+	writeUint16(data, 1, operation)
 	if err := c.writeEphemeralPacket(); err != nil {
 		return NewSQLError(CRServerGone, SSUnknownSQLState, err.Error())
 	}
@@ -864,8 +861,8 @@ func (c *Conn) parseComInitDB(data []byte) string {
 
 func (c *Conn) sendColumnCount(count uint64) error {
 	length := lenEncIntSize(count)
-	data, pos := c.startEphemeralPacketWithHeader(length)
-	writeLenEncInt(data, pos, count)
+	data := c.startEphemeralPacket(length)
+	writeLenEncInt(data, 0, count)
 	return c.writeEphemeralPacket()
 }
 
@@ -892,7 +889,8 @@ func (c *Conn) writeColumnDefinition(field *querypb.Field) error {
 		flags = int64(field.Flags)
 	}
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	pos = writeLenEncString(data, pos, "def") // Always the same.
 	pos = writeLenEncString(data, pos, field.Database)
@@ -926,7 +924,8 @@ func (c *Conn) writeRow(row []sqltypes.Value) error {
 		}
 	}
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 	for _, val := range row {
 		if val.IsNull() {
 			pos = writeByte(data, pos, NullValue)
@@ -937,6 +936,10 @@ func (c *Conn) writeRow(row []sqltypes.Value) error {
 		}
 	}
 
+	if pos != length {
+		return vterrors.Errorf(vtrpc.Code_INTERNAL, "packet row: got %v bytes but expected %v", pos, length)
+	}
+
 	return c.writeEphemeralPacket()
 }
 
@@ -1009,7 +1012,8 @@ func (c *Conn) writePrepare(fld []*querypb.Field, prepare *PrepareData) error {
 		prepare.ColumnNames = make([]string, columnCount)
 	}
 
-	data, pos := c.startEphemeralPacketWithHeader(12)
+	data := c.startEphemeralPacket(12)
+	pos := 0
 
 	pos = writeByte(data, pos, 0x00)
 	pos = writeUint32(data, pos, uint32(prepare.StatementID))
@@ -1077,7 +1081,8 @@ func (c *Conn) writeBinaryRow(fields []*querypb.Field, row []sqltypes.Value) err
 
 	length += nullBitMapLen + 1
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	pos = writeByte(data, pos, 0x00)
 
@@ -1100,6 +1105,10 @@ func (c *Conn) writeBinaryRow(fields []*querypb.Field, row []sqltypes.Value) err
 		}
 	}
 
+	if pos != length {
+		return fmt.Errorf("internal error packet row: got %v bytes but expected %v", pos, length)
+	}
+
 	return c.writeEphemeralPacket()
 }
 
diff --git a/go/mysql/replication.go b/go/mysql/replication.go
index d10925b6dd..dcc4c5e20c 100644
--- a/go/mysql/replication.go
+++ b/go/mysql/replication.go
@@ -28,8 +28,8 @@ func (c *Conn) WriteComBinlogDump(serverID uint32, binlogFilename string, binlog
 		2 + // flags
 		4 + // server-id
 		len(binlogFilename) // binlog-filename
-	data, pos := c.startEphemeralPacketWithHeader(length)
-	pos = writeByte(data, pos, ComBinlogDump)
+	data := c.startEphemeralPacket(length)
+	pos := writeByte(data, 0, ComBinlogDump)
 	pos = writeUint32(data, pos, binlogPos)
 	pos = writeUint16(data, pos, flags)
 	pos = writeUint32(data, pos, serverID)
@@ -53,8 +53,8 @@ func (c *Conn) WriteComBinlogDumpGTID(serverID uint32, binlogFilename string, bi
 		8 + // binlog-pos
 		4 + // data-size
 		len(gtidSet) // data
-	data, pos := c.startEphemeralPacketWithHeader(length)
-	pos = writeByte(data, pos, ComBinlogDumpGTID)
+	data := c.startEphemeralPacket(length)
+	pos := writeByte(data, 0, ComBinlogDumpGTID)
 	pos = writeUint16(data, pos, flags)
 	pos = writeUint32(data, pos, serverID)
 	pos = writeUint32(data, pos, uint32(len(binlogFilename)))
diff --git a/go/mysql/server.go b/go/mysql/server.go
index 8eb72a2d87..53e405b9f0 100644
--- a/go/mysql/server.go
+++ b/go/mysql/server.go
@@ -520,7 +520,8 @@ func (c *Conn) writeHandshakeV10(serverVersion string, authServer AuthServer, en
 			13 + // auth-plugin-data
 			lenNullString(MysqlNativePassword) // auth-plugin-name
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	// Protocol version.
 	pos = writeByte(data, pos, protocolVersion)
@@ -766,7 +767,8 @@ func (c *Conn) writeAuthSwitchRequest(pluginName string, pluginData []byte) erro
 		len(pluginName) + 1 + // 0-terminated pluginName
 		len(pluginData)
 
-	data, pos := c.startEphemeralPacketWithHeader(length)
+	data := c.startEphemeralPacket(length)
+	pos := 0
 
 	// Packet header.
 	pos = writeByte(data, pos, AuthSwitchRequestPacket)
diff --git a/go/vt/vtgate/engine/send.go b/go/vt/vtgate/engine/send.go
index c812fa53ac..1934b9f674 100644
--- a/go/vt/vtgate/engine/send.go
+++ b/go/vt/vtgate/engine/send.go
@@ -141,13 +141,12 @@ func (s *Send) StreamExecute(vcursor VCursor, bindVars map[string]*querypb.BindV
 
 // GetFields implements Primitive interface
 func (s *Send) GetFields(vcursor VCursor, bindVars map[string]*querypb.BindVariable) (*sqltypes.Result, error) {
-	// We don't need to worry about GetFields being needed for joins since the Send primitive currently doesn't
-	// get nested with other types of primitives.
-	// However, GetFields is used for prepared statements. Because of that, prepared statements are not yet
-	// compatible with the Send primitive.
-	//
-	// TODO: implement this
-	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable")
+	qr, err := s.Execute(vcursor, bindVars, false)
+	if err != nil {
+		return nil, vterrors.Wrap(err, "sendGetFields")
+	}
+	qr.Rows = nil
+	return qr, nil
 }
 
 func (s *Send) description() PrimitiveDescription {


Test patch:
diff --git a/go/mysql/conn_test.go b/go/mysql/conn_test.go
index d8ab1a8526..405b23c40d 100644
--- a/go/mysql/conn_test.go
+++ b/go/mysql/conn_test.go
@@ -77,12 +77,7 @@ func useWritePacket(t *testing.T, cConn *Conn, data []byte) {
 			t.Fatalf("%v", x)
 		}
 	}()
-
-	dataLen := len(data)
-	dataWithHeader := make([]byte, packetHeaderSize+dataLen)
-	copy(dataWithHeader[packetHeaderSize:], data)
-
-	if err := cConn.writePacket(dataWithHeader); err != nil {
+	if err := cConn.writePacket(data); err != nil {
 		t.Fatalf("writePacket failed: %v", err)
 	}
 }
@@ -96,8 +91,8 @@ func useWriteEphemeralPacketBuffered(t *testing.T, cConn *Conn, data []byte) {
 	cConn.startWriterBuffering()
 	defer cConn.endWriterBuffering()
 
-	buf, pos := cConn.startEphemeralPacketWithHeader(len(data))
-	copy(buf[pos:], data)
+	buf := cConn.startEphemeralPacket(len(data))
+	copy(buf, data)
 	if err := cConn.writeEphemeralPacket(); err != nil {
 		t.Fatalf("writeEphemeralPacket(false) failed: %v", err)
 	}
@@ -110,8 +105,8 @@ func useWriteEphemeralPacketDirect(t *testing.T, cConn *Conn, data []byte) {
 		}
 	}()
 
-	buf, pos := cConn.startEphemeralPacketWithHeader(len(data))
-	copy(buf[pos:], data)
+	buf := cConn.startEphemeralPacket(len(data))
+	copy(buf, data)
 	if err := cConn.writeEphemeralPacket(); err != nil {
 		t.Fatalf("writeEphemeralPacket(true) failed: %v", err)
 	}
diff --git a/go/mysql/query_test.go b/go/mysql/query_test.go
index f0529a1bc9..77be4940d2 100644
--- a/go/mysql/query_test.go
+++ b/go/mysql/query_test.go
@@ -31,9 +31,9 @@ import (
 
 // Utility function to write sql query as packets to test parseComPrepare
 func MockQueryPackets(t *testing.T, query string) []byte {
-	data := make([]byte, len(query)+1+packetHeaderSize)
+	data := make([]byte, len(query)+1)
 	// Not sure if it makes a difference
-	pos := packetHeaderSize
+	pos := 0
 	pos = writeByte(data, pos, ComPrepare)
 	copy(data[pos:], query)
 	return data
diff --git a/go/vt/vtgate/engine/send_test.go b/go/vt/vtgate/engine/send_test.go
index 162018aac2..a0423e9dfd 100644
--- a/go/vt/vtgate/engine/send_test.go
+++ b/go/vt/vtgate/engine/send_test.go
@@ -20,6 +20,8 @@ import (
 	"errors"
 	"testing"
 
+	"vitess.io/vitess/go/sqltypes"
+
 	"github.com/stretchr/testify/require"
 
 	"vitess.io/vitess/go/vt/key"
@@ -264,3 +266,34 @@ func TestSendTable_StreamExecute(t *testing.T) {
 		})
 	}
 }
+
+func TestSendGetFields(t *testing.T) {
+	results := []*sqltypes.Result{sqltypes.MakeTestResult(
+		sqltypes.MakeTestFields(
+			"id|c1|c2|c3",
+			"int64|int64|int64|int64",
+		),
+		"1|4|5|6",
+		"2|7|8|9",
+	)}
+
+	send := &Send{
+		Keyspace: &vindexes.Keyspace{
+			Name:    "ks",
+			Sharded: true,
+		},
+		Query:             "dummy_query",
+		TargetDestination: key.DestinationAllShards{},
+		IsDML:             true,
+		SingleShardOnly:   false,
+	}
+	vc := &loggingVCursor{shards: []string{"-20", "20-"}, results: results}
+	qr, err := send.GetFields(vc, map[string]*querypb.BindVariable{})
+	require.NoError(t, err)
+	vc.ExpectLog(t, []string{
+		`ResolveDestinations ks [] Destinations:DestinationAllShards()`,
+		`ExecuteMultiShard ks.-20: dummy_query {} ks.20-: dummy_query {} true false`,
+	})
+	require.Nil(t, qr.Rows)
+	require.Equal(t, 4, len(qr.Fields))
+}