package database import ( "database/sql" "fmt" "time" "gorm.io/gorm" ) const ( dumpTimeLayout = "2006-01-02 15:04:05.000000" zeroDumpTime = "0000-00-00 00:00:00" ) func NewMySQLBackup(db *gorm.DB, directory string, now func() time.Time) *DumpBackup { return NewDumpBackup(func() (DumpSource, error) { return openMySQLDumpSource(db) }, directory, now) } type txDumpSource struct { tx *gorm.DB quote func(name string) string convert func(value any) []byte } func openTxDumpSource(db *gorm.DB, quote func(name string) string, convert func(value any) []byte) (*txDumpSource, error) { tx := db.Begin(&sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}) if tx.Error != nil { return nil, tx.Error } return &txDumpSource{tx: tx, quote: quote, convert: convert}, nil } func (s *txDumpSource) Rows(table string) (DumpRows, error) { rows, err := s.tx.Raw("SELECT * FROM " + s.quote(table)).Rows() if err != nil { return nil, err } return &sqlDumpRows{rows: rows, convert: s.convert}, nil } func (s *txDumpSource) Close() error { return s.tx.Rollback().Error } type mysqlDumpSource struct { *txDumpSource } var _ DumpSource = &mysqlDumpSource{} func openMySQLDumpSource(db *gorm.DB) (*mysqlDumpSource, error) { source, err := openTxDumpSource(db, quoteIdentifier, dumpBytes) if err != nil { return nil, err } return &mysqlDumpSource{source}, nil } func (s *mysqlDumpSource) Tables() ([]string, error) { var tables []string err := s.tx.Raw("SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' ORDER BY table_name").Scan(&tables).Error return tables, err } func (s *mysqlDumpSource) CreateStatement(table string) (string, error) { var name, statement string row := s.tx.Raw("SHOW CREATE TABLE " + quoteIdentifier(table)).Row() if err := row.Scan(&name, &statement); err != nil { return "", err } return statement, nil } type sqlDumpRows struct { rows *sql.Rows convert func(value any) []byte } var _ DumpRows = &sqlDumpRows{} func (r *sqlDumpRows) Columns() ([]DumpColumn, error) { types, err := r.rows.ColumnTypes() if err != nil { return nil, err } columns := make([]DumpColumn, len(types)) for index, columnType := range types { columns[index] = DumpColumn{Name: columnType.Name(), Type: columnType.DatabaseTypeName()} } return columns, nil } func (r *sqlDumpRows) Next() bool { return r.rows.Next() } func (r *sqlDumpRows) Values() ([][]byte, error) { columnCount, err := r.rows.Columns() if err != nil { return nil, err } scanned := make([]any, len(columnCount)) targets := make([]any, len(columnCount)) for index := range scanned { targets[index] = &scanned[index] } if err := r.rows.Scan(targets...); err != nil { return nil, err } values := make([][]byte, len(scanned)) for index, value := range scanned { values[index] = r.convert(value) } return values, nil } func (r *sqlDumpRows) Err() error { return r.rows.Err() } func (r *sqlDumpRows) Close() error { return r.rows.Close() } func dumpBytes(value any) []byte { switch typed := value.(type) { case nil: return nil case []byte: return append([]byte{}, typed...) case string: return []byte(typed) case time.Time: return []byte(formatDumpTime(typed)) default: return []byte(fmt.Sprint(typed)) } } func formatDumpTime(value time.Time) string { if value.IsZero() { return zeroDumpTime } return value.UTC().Format(dumpTimeLayout) }