Skip to content

mysql: auto-reprepare prepared statements on ER_NEED_REPREPARE to keep scans correct after DDL - #1740

Draft
ljluestc wants to merge 1 commit into
go-sql-driver:masterfrom
ljluestc:issue-563
Draft

mysql: auto-reprepare prepared statements on ER_NEED_REPREPARE to keep scans correct after DDL#1740
ljluestc wants to merge 1 commit into
go-sql-driver:masterfrom
ljluestc:issue-563

Conversation

@ljluestc

@ljluestc ljluestc commented Nov 2, 2025

Copy link
Copy Markdown

Title

mysql: auto-reprepare prepared statements on ER_NEED_REPREPARE to keep scans correct after DDL

Summary

This PR makes prepared statements resilient to schema changes by automatically re-preparing and retrying once when the server returns ER_NEED_REPREPARE (1615). It fixes incorrect scan results after DDL (e.g., changing a column type) without requiring an app restart. Adds integration tests reproducing and verifying the behavior.

Fixes #563

Background

Some servers invalidate prepared-statement metadata after DDL (e.g., ALTER TABLE ... MODIFY column TYPE). Applications may then see wrong values (e.g., zero timestamps) or receive ER_NEED_REPREPARE until they restart. The driver previously didn’t auto-reprepare.

Changes

  • Store original SQL on statements:
    • Add queryString string to mysqlStmt, set in mysqlConn.Prepare.
  • Auto-reprepare on schema changes:
    • Add mysqlStmt.reprepare() to close the current stmt ID and prepare again (same connection, same SQL).
    • Update mysqlStmt.Exec and mysqlStmt.Query to retry the operation once when readResultSetHeaderPacket() returns ER_NEED_REPREPARE (1615).
  • Tests:
    • TestPreparedStmtReprepareAfterDDL: verifies scans remain correct across a type change with parseTime=true.
    • TestPreparedStmtExecReprepareAfterDDL: verifies Exec continues to work after a type change.
    • TestPreparedStmtReprepareMultipleScansAfterDDL_NullTime: repeated scans across DDL using sql.NullTime.

Behavior

  • Transparent to users of database/sql.
  • On ER_NEED_REPREPARE:
    • Reprepare once and retry the same Exec/Query.
    • If reprepare fails, return the error.
  • No overhead on the fast path; only triggers on the error path.

Compatibility

  • No public API changes.
  • Metadata caching semantics preserved (respects clientCacheMetadata).
  • Works regardless of whether the server emits ER_NEED_REPREPARE. If not emitted, behavior is unchanged.

Performance impact

  • None on the normal path.
  • On schema invalidation: one COM_STMT_CLOSE + COM_STMT_PREPARE roundtrip and a single retry.

Edge cases and safeguards

  • Single retry only (avoids masking persistent failures).
  • Returns driver.ErrBadConn / ErrInvalidConn as before if the statement/connection is invalid.
  • Closes the old server-side stmt ID to avoid leaks.

Files touched

  • statement.go: add queryString, implement reprepare, add single-retry in Exec/Query.
  • connection.go: set stmt.queryString = query in Prepare.
  • reprepare_test.go: new integration tests.
  • README.md: note about prepared statements being resilient to DDL (ER_NEED_REPREPARE).

Testing

  • Automated:
    • go test -run Reprepare -count=1 ./... passes with a live MySQL.
    • Full suite passes given environment (LOCAL INFILE may need enabling).
  • Manual repro (optional):
    • Prepare a SELECT, run it, ALTER TABLE ... MODIFY state INT, scan again; values remain correct.
    • Prepare an INSERT, ALTER TABLE ... MODIFY value BIGINT, insert again; succeeds.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Retry limits, statement replacement ordering, deterministic coverage, and formatting need correction.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds automatic prepared-statement recovery after schema changes that produce ER_NEED_REPREPARE.

Changes:

  • Stores original SQL for re-preparation.
  • Retries prepared Exec/Query operations after error 1615.
  • Adds DDL integration tests.
File summaries
File Description
statement.go Implements re-preparation and retries.
connection.go Retains prepared SQL text.
reprepare_test.go Tests prepared statements across DDL changes.
Review details

Suppressed comments (3)

statement.go:72

  • The second execution can also enter this branch because the condition does not use attempt. If it returns 1615 again, the code re-prepares a second time, exits the loop, and reports ErrInvalidConn instead of the server error. Gate re-preparation to the first attempt so this remains a single retry.
            if me, ok := err.(*MySQLError); ok && me.Number == 1615 /* ER_NEED_REPREPARE */ {

statement.go:134

  • The Query path has the same retry-boundary bug: a second 1615 triggers another re-prepare and then falls out as ErrInvalidConn. Only the first failed attempt should enter this branch; the retry's error should be returned unchanged.
            if me, ok := err.(*MySQLError); ok && me.Number == 1615 /* ER_NEED_REPREPARE */ {

reprepare_test.go:56

  • This DDL does not deterministically exercise the new 1615 handling—the test itself notes that environments may not emit ER_NEED_REPREPARE—so these tests can pass even if the retry code is removed. Add a protocol-level test that injects 1615 followed by success, plus a persistent-1615 case, to verify both the retry and its one-attempt limit.
        // Change the column type that participates in the prepared statement's result set.
        dbt.mustExec("ALTER TABLE reprepare_test MODIFY state INT")
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread statement.go
Comment on lines +182 to +183
// Prepare a new statement on the same connection.
newStmt, err := stmt.mc.Prepare(stmt.queryString)
Comment thread connection.go
Comment on lines +219 to +222
stmt := &mysqlStmt{
mc: mc,
queryString: query,
}
Comment thread reprepare_test.go
Comment on lines +11 to +15
import (
"database/sql"
"testing"
"time"
)
Comment thread statement.go
id uint32
paramCount int
columns []mysqlField
queryString string
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

Successfully merging this pull request may close these issues.

Get error timestamp data when scan to struct after db ddl

2 participants