From d81e1cac6db21732bcc5b07c114c5cc9ad22029a Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Sat, 29 Nov 2025 15:31:04 -0800 Subject: [PATCH] feat: Expose prepared statement column and parameter metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- connection.go | 14 +++++--------- fields.go | 32 ++++++++++++++++++++++++++++++++ statement.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/connection.go b/connection.go index 5648e47d..684b5712 100644 --- a/connection.go +++ b/connection.go @@ -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 } } } diff --git a/fields.go b/fields.go index 25a16628..d0abaa72 100644 --- a/fields.go +++ b/fields.go @@ -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: diff --git a/statement.go b/statement.go index 2db8960e..1e05b905 100644 --- a/statement.go +++ b/statement.go @@ -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 { @@ -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{} }