Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: Expose prepared statement column and parameter metadata
This adds public APIs to access metadata from COM_STMT_PREPARE responses:

- Add FieldMetadata struct to expose column/parameter information
- Add StmtMetadata interface with ColumnMetadata() and ParamMetadata() methods
- Modify Prepare() to read parameter metadata instead of skipping it
- Modify Prepare() to always read column metadata (not just with cache capability)

The FieldMetadata struct exposes:
- TableName, Name, Length, Decimals
- DatabaseTypeName (e.g., "INT", "VARCHAR", "TEXT")
- Nullable and Unsigned flags

This allows tools like sqlc to get type information for query parameters
and result columns directly from prepared statements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
  • Loading branch information
kyleconroy and claude committed Nov 29, 2025
commit d81e1cac6db21732bcc5b07c114c5cc9ad22029a
14 changes: 5 additions & 9 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,16 @@ func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
columnCount, err := stmt.readPrepareResultPacket()
if err == nil {
if stmt.paramCount > 0 {
if err = mc.skipColumns(stmt.paramCount); err != nil {
// Read parameter metadata instead of skipping it
if stmt.params, err = mc.readColumns(stmt.paramCount, nil); err != nil {
return nil, err
}
}

if columnCount > 0 {
if mc.extCapabilities&clientCacheMetadata != 0 {
if stmt.columns, err = mc.readColumns(int(columnCount), nil); err != nil {
return nil, err
}
} else {
if err = mc.skipColumns(int(columnCount)); err != nil {
return nil, err
}
// Always read column metadata
if stmt.columns, err = mc.readColumns(int(columnCount), stmt.columns); err != nil {
return nil, err
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,38 @@ type mysqlField struct {
charSet uint8
}

// FieldMetadata represents metadata about a column or parameter from a prepared statement.
// This is a public struct that exposes the metadata from mysqlField.
type FieldMetadata struct {
// TableName is the name of the table this field belongs to (may be empty for expressions)
TableName string
// Name is the name or alias of the field
Name string
// Length is the maximum length of the field
Length uint32
// Decimals is the number of decimals for numeric types
Decimals byte
// DatabaseTypeName returns the MySQL type name (e.g., "INT", "VARCHAR", "TEXT")
DatabaseTypeName string
// Nullable indicates whether the field can be NULL
Nullable bool
// Unsigned indicates whether a numeric field is unsigned
Unsigned bool
}

// toFieldMetadata converts an internal mysqlField to a public FieldMetadata
func (mf *mysqlField) toFieldMetadata() FieldMetadata {
return FieldMetadata{
TableName: mf.tableName,
Name: mf.name,
Length: mf.length,
Decimals: mf.decimals,
DatabaseTypeName: mf.typeDatabaseName(),
Nullable: mf.flags&flagNotNULL == 0,
Unsigned: mf.flags&flagUnsigned != 0,
}
}

func (mf *mysqlField) scanType() reflect.Type {
switch mf.fieldType {
case fieldTypeTiny:
Expand Down
48 changes: 48 additions & 0 deletions statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,38 @@ import (
"reflect"
)

// StmtMetadata is an interface that provides access to prepared statement metadata.
// It can be used via type assertion on the driver.Stmt returned by Conn.Prepare.
//
// Example usage with database/sql:
//
// conn, _ := db.Conn(ctx)
// conn.Raw(func(driverConn any) error {
// if preparer, ok := driverConn.(driver.ConnPrepareContext); ok {
// stmt, _ := preparer.PrepareContext(ctx, query)
// if meta, ok := stmt.(mysql.StmtMetadata); ok {
// columns := meta.ColumnMetadata()
// params := meta.ParamMetadata()
// }
// }
// return nil
// })
type StmtMetadata interface {
// ColumnMetadata returns metadata about result columns
ColumnMetadata() []FieldMetadata
// ParamMetadata returns metadata about query parameters
ParamMetadata() []FieldMetadata
}

// Verify that mysqlStmt implements StmtMetadata
var _ StmtMetadata = (*mysqlStmt)(nil)

type mysqlStmt struct {
mc *mysqlConn
id uint32
paramCount int
columns []mysqlField
params []mysqlField
}

func (stmt *mysqlStmt) Close() error {
Expand All @@ -42,6 +69,27 @@ func (stmt *mysqlStmt) NumInput() int {
return stmt.paramCount
}

// ColumnMetadata returns metadata about the columns that will be returned by this prepared statement.
// This information is obtained from the MySQL server during the PREPARE phase.
func (stmt *mysqlStmt) ColumnMetadata() []FieldMetadata {
result := make([]FieldMetadata, len(stmt.columns))
for i, col := range stmt.columns {
result[i] = col.toFieldMetadata()
}
return result
}

// ParamMetadata returns metadata about the parameters expected by this prepared statement.
// This information is obtained from the MySQL server during the PREPARE phase.
// Note: MySQL may return limited parameter metadata depending on the query structure.
func (stmt *mysqlStmt) ParamMetadata() []FieldMetadata {
result := make([]FieldMetadata, len(stmt.params))
for i, param := range stmt.params {
result[i] = param.toFieldMetadata()
}
return result
}

func (stmt *mysqlStmt) ColumnConverter(idx int) driver.ValueConverter {
return converter{}
}
Expand Down