Repository: pingcap/tidb (Go)

Issue:
execute prepared statement with binary protocol will panic when table schema changed
## Bug Report

Please answer these questions before submitting your issue. Thanks!

### 1. Minimal reproduce step (Required)


```
import (
	"context"
	"database/sql"
	"testing"

	_ "github.com/go-sql-driver/mysql"
	"github.com/stretchr/testify/require"
)

func TestPrepareExecute(t *testing.T) {
	ctx := context.TODO()
	db, err := sql.Open("mysql", "root@tcp(127.0.0.1:4000)/test")
	require.NoError(t, err)

	conn, err := db.Conn(ctx)
	require.NoError(t, err)
	_, err = conn.ExecContext(ctx, "drop table if exists t1")
	require.NoError(t, err)
	_, err = conn.ExecContext(ctx, "create table t1 (id int primary key, v int)")
	require.NoError(t, err)
	_, err = conn.ExecContext(ctx, "insert into t1 values(1, 10)")
	require.NoError(t, err)

	stmt, err := conn.PrepareContext(context.TODO(), "select * from t1")
	require.NoError(t, err)

	require.Equal(t, []int{1, 10}, executePreparedStmtResult(t, stmt, 2))

	// drop one column
	_, err = conn.ExecContext(ctx, "alter table t1 drop column v")
	require.NoError(t, err)

	// the below will fail when get next row
	require.Equal(t, []int{1}, executePreparedStmtResult(t, stmt, 1))
}

func executePreparedStmtResult(t *testing.T, stmt *sql.Stmt, columns int) []int {
	rows, err := stmt.Query()
	require.NoError(t, err)
	if ok := rows.Next(); !ok {
		// fail here for the third execute
		require.Fail(t, rows.Err().Error())
	}
	row := make([]int, columns)
	scan := make([]interface{}, columns)
	for i := range row {
		scan[i] = &row[i]
	}

	require.NoError(t, rows.Scan(scan...))
	require.NoError(t, rows.Close())
	return row
}
```

### 2. What did you expect to see? (Required)

no error

### 3. What did you see instead (Required)

```
=== RUN   TestPrepareExecute
    staleread_test.go:91: 
        	Error Trace:	staleread_test.go:91
        	            				staleread_test.go:84
        	Error:      	Error 1105: runtime error: index out of range [1] with length 1
        	Test:       	TestPrepareExecute
--- FAIL: TestPrepareExecute (0.30s)

FAIL
```

Got a panic in tidb side:

```
[2022/03/28 17:45:17.492 +08:00] [ERROR] [conn.go:1031] ["connection running loop panic"] [conn=4089] [lastSQL="select * from t1"] [err="runtime error: index out of range [1] with length 1"] [stack="goroutine 380115 [running]:
github.com/pingcap/tidb/server.(*clientConn).Run.func1()
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:1029 +0x88
panic({0x10738fac0, 0x14001f98510})
	/usr/local/go/src/runtime/panic.go:844 +0x26c
github.com/pingcap/tidb/server.(*clientConn).writeResultset.func1()
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:2119 +0x3d4
panic({0x10738fac0, 0x14001f98510})
	/usr/local/go/src/runtime/panic.go:844 +0x26c
github.com/pingcap/tidb/util/chunk.Row.IsNull({0x140096b60a0, 0x0}, 0x1)
	/Users/wangchao/Code/pingcap/tidb/util/chunk/row.go:206 +0x74
github.com/pingcap/tidb/server.dumpBinaryRow({0x14001c26000, 0xa, 0x400}, {0x140051f81e0, 0x2, 0x2}, {0x140096b60a0, 0x0}, 0x140056a0780)
	/Users/wangchao/Code/pingcap/tidb/server/util.go:248 +0x1b0
github.com/pingcap/tidb/server.(*clientConn).writeChunks(0x140020ef400, {0x1075a5f10, 0x14003af06c0}, {0x1075ae598, 0x140096b6050}, 0x1, 0x0)
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:2208 +0x390
github.com/pingcap/tidb/server.(*clientConn).writeResultset(0x140020ef400, {0x1075a5f10, 0x14003af06c0}, {0x1075ae598, 0x140096b6050}, 0x1, 0x0, 0x0)
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:2136 +0x23c
github.com/pingcap/tidb/server.(*clientConn).executePreparedStmtAndWriteResult(0x140020ef400, {0x1075a5f10, 0x14003af06c0}, {0x1075bc220, 0x1400d01eaf0}, {0x1090cd0b8, 0x0, 0x0}, 0x0)
	/Users/wangchao/Code/pingcap/tidb/server/conn_stmt.go:263 +0x410
github.com/pingcap/tidb/server.(*clientConn).handleStmtExecute(0x140020ef400, {0x1075a5f10, 0x14003af06c0}, {0x14004c56205, 0x9, 0x9})
	/Users/wangchao/Code/pingcap/tidb/server/conn_stmt.go:209 +0x7c4
github.com/pingcap/tidb/server.(*clientConn).dispatch(0x140020ef400, {0x1075a5e68, 0x140056a0380}, {0x14004c56205, 0x9, 0x9})
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:1367 +0xfd8
github.com/pingcap/tidb/server.(*clientConn).Run(0x140020ef400, {0x1075a5f10, 0x1401408c0c0})
	/Users/wangchao/Code/pingcap/tidb/server/conn.go:1095 +0x7b4
github.com/pingcap/tidb/server.(*Server).onConn(0x140077d00d0, 0x140020ef400)
	/Users/wangchao/Code/pingcap/tidb/server/server.go:551 +0xa64
created by github.com/pingcap/tidb/server.(*Server).startNetworkListener
	/Users/wangchao/Code/pingcap/tidb/server/server.go:448 +0x7a4
"]
```

### 4. What is your TiDB version? (Required)

master

<!-- Paste the output of SELECT tidb_version() -->




Discussion:
Seems introduced by this PR: https://github.com/pingcap/tidb/pull/12388 , and the below code:

https://github.com/pingcap/tidb/blob/4ff3ec260c667fa539374fa67b47c82ffc6a28d4/server/driver_tidb.go#L347-L370

The columns will be cached in prepared statement, but if the table's schema changed, the cache will not be cleared

/type regression



Patch:
diff --git a/planner/core/cache.go b/planner/core/cache.go
index 145caa8499fd0..461258d924d8e 100644
--- a/planner/core/cache.go
+++ b/planner/core/cache.go
@@ -222,7 +222,6 @@ type CachedPrepareStmt struct {
 	PreparedAst         *ast.Prepared
 	StmtDB              string // which DB the statement will be processed over
 	VisitInfos          []visitInfo
-	ColumnInfos         interface{}
 	Executor            interface{}
 	NormalizedSQL       string
 	NormalizedPlan      string
diff --git a/server/driver_tidb.go b/server/driver_tidb.go
index 3092b3b4df37c..2594f9a140b46 100644
--- a/server/driver_tidb.go
+++ b/server/driver_tidb.go
@@ -80,8 +80,7 @@ func (ts *TiDBStatement) Execute(ctx context.Context, args []types.Datum) (rs Re
 		return
 	}
 	rs = &tidbResultSet{
-		recordSet:    tidbRecordset,
-		preparedStmt: ts.ctx.GetSessionVars().PreparedStmts[ts.id].(*core.CachedPrepareStmt),
+		recordSet: tidbRecordset,
 	}
 	return
 }
@@ -302,11 +301,10 @@ func (tc *TiDBContext) GetStmtStats() *stmtstats.StatementStats {
 }
 
 type tidbResultSet struct {
-	recordSet    sqlexec.RecordSet
-	columns      []*ColumnInfo
-	rows         []chunk.Row
-	closed       int32
-	preparedStmt *core.CachedPrepareStmt
+	recordSet sqlexec.RecordSet
+	columns   []*ColumnInfo
+	rows      []chunk.Row
+	closed    int32
 }
 
 func (trs *tidbResultSet) NewChunk(alloc chunk.Allocator) *chunk.Chunk {
@@ -348,23 +346,12 @@ func (trs *tidbResultSet) Columns() []*ColumnInfo {
 	if trs.columns != nil {
 		return trs.columns
 	}
-	// for prepare statement, try to get cached columnInfo array
-	if trs.preparedStmt != nil {
-		ps := trs.preparedStmt
-		if colInfos, ok := ps.ColumnInfos.([]*ColumnInfo); ok {
-			trs.columns = colInfos
-		}
-	}
+
 	if trs.columns == nil {
 		fields := trs.recordSet.Fields()
 		for _, v := range fields {
 			trs.columns = append(trs.columns, convertColumnInfo(v))
 		}
-		if trs.preparedStmt != nil {
-			// if ColumnInfo struct has allocated object,
-			// here maybe we need deep copy ColumnInfo to do caching
-			trs.preparedStmt.ColumnInfos = trs.columns
-		}
 	}
 	return trs.columns
 }


Test patch:
diff --git a/server/tidb_serial_test.go b/server/tidb_serial_test.go
index 39907170ade80..468f840c888ee 100644
--- a/server/tidb_serial_test.go
+++ b/server/tidb_serial_test.go
@@ -348,6 +348,47 @@ func TestPrepareCount(t *testing.T) {
 	require.NoError(t, qctx.Close())
 }
 
+func TestPrepareExecute(t *testing.T) {
+	ts, cleanup := createTidbTestSuite(t)
+	defer cleanup()
+
+	qctx, err := ts.tidbdrv.OpenCtx(uint64(0), 0, uint8(tmysql.DefaultCollationID), "test", nil)
+	require.NoError(t, err)
+
+	ctx := context.Background()
+	_, err = qctx.Execute(ctx, "use test")
+	require.NoError(t, err)
+	_, err = qctx.Execute(ctx, "create table t1(id int primary key, v int)")
+	require.NoError(t, err)
+	_, err = qctx.Execute(ctx, "insert into t1 values(1, 100)")
+	require.NoError(t, err)
+
+	stmt, _, _, err := qctx.Prepare("select * from t1 where id=1")
+	require.NoError(t, err)
+	rs, err := stmt.Execute(ctx, nil)
+	require.NoError(t, err)
+	req := rs.NewChunk(nil)
+	require.NoError(t, rs.Next(ctx, req))
+	require.Equal(t, 2, req.NumCols())
+	require.Equal(t, req.NumCols(), len(rs.Columns()))
+	require.Equal(t, 1, req.NumRows())
+	require.Equal(t, int64(1), req.GetRow(0).GetInt64(0))
+	require.Equal(t, int64(100), req.GetRow(0).GetInt64(1))
+
+	// issue #33509
+	_, err = qctx.Execute(ctx, "alter table t1 drop column v")
+	require.NoError(t, err)
+
+	rs, err = stmt.Execute(ctx, nil)
+	require.NoError(t, err)
+	req = rs.NewChunk(nil)
+	require.NoError(t, rs.Next(ctx, req))
+	require.Equal(t, 1, req.NumCols())
+	require.Equal(t, req.NumCols(), len(rs.Columns()))
+	require.Equal(t, 1, req.NumRows())
+	require.Equal(t, int64(1), req.GetRow(0).GetInt64(0))
+}
+
 func TestDefaultCharacterAndCollation(t *testing.T) {
 	ts, cleanup := createTidbTestSuite(t)
 	defer cleanup()