diff --git a/pkg/database/connection.go b/pkg/database/connection.go index ec41586b..1752e08b 100644 --- a/pkg/database/connection.go +++ b/pkg/database/connection.go @@ -19,5 +19,6 @@ func (c Connection) MySQL() string { config.Passwd = c.Pass config.Net = "tcp" config.Addr = fmt.Sprintf("%s:%d", c.Host, c.Port) + config.ParseTime = true return config.FormatDSN() } diff --git a/pkg/database/mysql/date.go b/pkg/database/mysql/date.go new file mode 100644 index 00000000..722873e2 --- /dev/null +++ b/pkg/database/mysql/date.go @@ -0,0 +1,48 @@ +package mysql + +import ( + "database/sql/driver" + "fmt" + "time" +) + +// NullDate represents a time.Time that may be null. +// NullDate implements the Scanner interface so +// it can be used as a scan destination, similar to NullString. +// It is distinct from sql.NullTime in that it can output formats only as a date +type NullDate struct { + Date time.Time + Valid bool +} + +// Scan implements the Scanner interface. +func (n *NullDate) Scan(value any) error { + if value == nil { + n.Date, n.Valid = time.Time{}, false + return nil + } + switch s := value.(type) { + case string: + t, err := time.Parse("2006-01-02", s) + if err != nil { + return err + } + n.Date = t + n.Valid = true + return nil + case time.Time: + n.Date = s + n.Valid = true + return nil + } + n.Valid = false + return fmt.Errorf("unknown type %T for NullDate", value) +} + +// Value implements the driver Valuer interface. +func (n NullDate) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Date.Format("2006-01-02"), nil +} diff --git a/pkg/database/mysql/dump.go b/pkg/database/mysql/dump.go index f1cc813f..236883d8 100644 --- a/pkg/database/mysql/dump.go +++ b/pkg/database/mysql/dump.go @@ -488,6 +488,12 @@ func reflectColumnType(tp *sql.ColumnType) reflect.Type { return reflect.TypeOf(sql.NullInt64{}) case "DOUBLE": return reflect.TypeOf(sql.NullFloat64{}) + case "TIMESTAMP", "DATETIME": + return reflect.TypeOf(sql.NullTime{}) + case "DATE": + return reflect.TypeOf(NullDate{}) + case "TIME": + return reflect.TypeOf(sql.NullString{}) } // unknown datatype @@ -557,6 +563,18 @@ func (table *table) RowBuffer() *bytes.Buffer { } else { fmt.Fprintf(&b, "_binary '%s'", sanitize(string(*s))) } + case *NullDate: + if s.Valid { + fmt.Fprintf(&b, "'%s'", sanitize(s.Date.Format("2006-01-02"))) + } else { + b.WriteString(nullType) + } + case *sql.NullTime: + if s.Valid { + fmt.Fprintf(&b, "'%s'", sanitize(s.Time.Format("2006-01-02 15:04:05"))) + } else { + b.WriteString(nullType) + } default: fmt.Fprintf(&b, "'%s'", value) } diff --git a/test/backup_test.go b/test/backup_test.go index f0de662c..1e62bebe 100644 --- a/test/backup_test.go +++ b/test/backup_test.go @@ -303,7 +303,17 @@ func (d *dockerContext) createBackupFile(mysqlCID, mysqlUser, mysqlPass, outfile ctx := context.Background() // Create and populate the table - mysqlCreateCmd := []string{"mysql", "-hlocalhost", "--protocol=tcp", fmt.Sprintf("-u%s", mysqlUser), fmt.Sprintf("-p%s", mysqlPass), "-e", `use tester; create table t1 (id INT, name VARCHAR(20)); INSERT INTO t1 (id,name) VALUES (1, "John"), (2, "Jill"), (3, "Sam"), (4, "Sarah");`} + mysqlCreateCmd := []string{"mysql", "-hlocalhost", "--protocol=tcp", fmt.Sprintf("-u%s", mysqlUser), fmt.Sprintf("-p%s", mysqlPass), "-e", ` + use tester; + create table t1 + (id int, name varchar(20), d date, t time, dt datetime, ts timestamp); + INSERT INTO t1 (id,name,d,t,dt,ts) + VALUES + (1, "John", "2012-11-01", "00:15:00", "2012-11-01 00:15:00", "2012-11-01 00:15:00"), + (2, "Jill", "2012-11-02", "00:16:00", "2012-11-02 00:16:00", "2012-11-02 00:16:00"), + (3, "Sam", "2012-11-03", "00:17:00", "2012-11-03 00:17:00", "2012-11-03 00:17:00"), + (4, "Sarah", "2012-11-04", "00:18:00", "2012-11-04 00:18:00", "2012-11-04 00:18:00"); + `} attachResp, exitCode, err := d.execInContainer(ctx, mysqlCID, mysqlCreateCmd) if err != nil { return fmt.Errorf("failed to attach to exec: %w", err)