From a0353bf18ebe7aeb48483548339d521562615651 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 13 Jul 2026 12:28:33 +0200 Subject: [PATCH] Fix corrupted SQL dumps for nullable unsigned BIGINT and VARBINARY columns --- pkg/util/mysql/go_mysqldump/dump.go | 8 ++- pkg/util/mysql/go_mysqldump/dump_test.go | 70 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/pkg/util/mysql/go_mysqldump/dump.go b/pkg/util/mysql/go_mysqldump/dump.go index 2eb53c7..8700261 100644 --- a/pkg/util/mysql/go_mysqldump/dump.go +++ b/pkg/util/mysql/go_mysqldump/dump.go @@ -442,7 +442,7 @@ func reflectColumnType(tp *sql.ColumnType) reflect.Type { // determine by name switch tp.DatabaseTypeName() { - case "BLOB", "BINARY": + case "BLOB", "BINARY", "VARBINARY": return reflect.TypeOf(sql.RawBytes{}) case "VARCHAR", "TEXT", "DECIMAL", "JSON", "DATETIME", "DATE", "TIMESTAMP": return reflect.TypeOf(sql.NullString{}) @@ -507,6 +507,12 @@ func (table *table) RowBuffer() *bytes.Buffer { } else { b.WriteString(nullType) } + case *sql.Null[uint64]: + if s.Valid { + fmt.Fprintf(&b, "%d", s.V) + } else { + b.WriteString(nullType) + } case *sql.NullFloat64: if s.Valid { fmt.Fprintf(&b, "%f", s.Float64) diff --git a/pkg/util/mysql/go_mysqldump/dump_test.go b/pkg/util/mysql/go_mysqldump/dump_test.go index 7c21978..88835c7 100644 --- a/pkg/util/mysql/go_mysqldump/dump_test.go +++ b/pkg/util/mysql/go_mysqldump/dump_test.go @@ -3,6 +3,7 @@ package go_mysqldump import ( "bytes" "database/sql" + "encoding/hex" "reflect" "strings" "testing" @@ -293,6 +294,75 @@ func TestCreateTableRowValuesUnsignedBigint(t *testing.T) { } } +// The MySQL driver scans a nullable BIGINT UNSIGNED column into a +// sql.Null[uint64], distinct from the non-nullable case above which scans +// into a plain uint64. Reproduces the corrupted-dump bug where such values +// were rendered via a Go fmt bad-verb diagnostic string instead of the +// numeric literal. +func TestCreateTableRowValuesNullableUnsignedBigint(t *testing.T) { + data, mock, err := getMockData() + assert.NoError(t, err, "an error was not expected when opening a stub database connection") + defer data.Close() + + cols := sqlmock.NewRows([]string{"Field", "Extra"}). + AddRow("version", "") + + rows := sqlmock.NewRowsWithColumnDefinition( + sqlmock.NewColumn("version").OfType("UNSIGNED BIGINT", sql.Null[uint64]{}).Nullable(true), + ). + AddRow(uint64(1469)) + + mock.ExpectQuery("^SHOW COLUMNS FROM `test`$").WillReturnRows(cols) + mock.ExpectQuery("^SELECT (.+) FROM `test` WHERE TRUE$").WillReturnRows(rows) + + table := data.createTable("test") + + assert.True(t, table.Next()) + result := table.RowValues() + assert.NoError(t, table.Err) + + // we make sure that all expectations were met + assert.NoError(t, mock.ExpectationsWereMet(), "there were unfulfilled expections") + + assert.Equal(t, "(1469)", result) + assert.NotContains(t, result, "%!s", "row values must not contain a Go fmt bad-verb diagnostic string") +} + +// The MySQL driver scans VARBINARY columns (e.g. UUIDs stored as raw bytes) +// into a plain []byte, distinct from BINARY which is handled explicitly. +// Reproduces the corrupted-dump bug where such values were rendered as a +// quoted string with a stray leading '&' instead of a `_binary 0x...` literal. +func TestCreateTableRowValuesVarbinary(t *testing.T) { + data, mock, err := getMockData() + assert.NoError(t, err, "an error was not expected when opening a stub database connection") + defer data.Close() + + uuid := "5430b48d-cfd5-47c7-ae6d-04c7e2ed2a21" + + cols := sqlmock.NewRows([]string{"Field", "Extra"}). + AddRow("id", "") + + rows := sqlmock.NewRowsWithColumnDefinition( + sqlmock.NewColumn("id").OfType("VARBINARY", []byte{}).Nullable(false), + ). + AddRow([]byte(uuid)) + + mock.ExpectQuery("^SHOW COLUMNS FROM `test`$").WillReturnRows(cols) + mock.ExpectQuery("^SELECT (.+) FROM `test` WHERE TRUE$").WillReturnRows(rows) + + table := data.createTable("test") + + assert.True(t, table.Next()) + result := table.RowValues() + assert.NoError(t, table.Err) + + // we make sure that all expectations were met + assert.NoError(t, mock.ExpectationsWereMet(), "there were unfulfilled expections") + + assert.Equal(t, "(_binary 0x"+hex.EncodeToString([]byte(uuid))+")", result) + assert.NotContains(t, result, "&"+uuid, "row values must not contain the raw UUID string with a stray leading '&'") +} + func TestCreateTableOk(t *testing.T) { data, mock, err := getMockData() assert.NoError(t, err, "an error was not expected when opening a stub database connection")