diff --git a/README.md b/README.md index f0e3dbf..6b0a13d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# go-sqldump -Create SQL dumps in Go without external dependencies +# Go MYSQL Dump +Create MYSQL dumps in Go without the `mysqldump` CLI as a dependancy. + +[Documentation](https://godoc.org/github.com/JamesStewy/go-mysqldump). diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..0851a97 --- /dev/null +++ b/doc.go @@ -0,0 +1,47 @@ +/* +Create MYSQL dumps in Go without the 'mysqldump' CLI as a dependancy. + +Example + +This example uses the mymysql driver (example 7 https://github.com/ziutek/mymysql) to connect to a mysql instance. + + package main + + import ( + "database/sql" + "fmt" + "github.com/JamesStewy/go-mysqldump" + "github.com/ziutek/mymysql/godrv" + "time" + ) + + func main() { + // Register the mymysql driver + godrv.Register("SET NAMES utf8") + + // Open connection to database + db, err := sql.Open("mymysql", "tcp:host:port*database/user/password") + if err != nil { + fmt.Println("Error opening databse:", err) + return + } + + // Register database with mysqldump + dumper, err := mysqldump.Register(db, "dumps", time.ANSIC) + if err != nil { + fmt.Println("Error registering databse:", err) + return + } + + // Dump database to file + err = dumper.Dump() + if err != nil { + fmt.Println("Error dumping:", err) + return + } + + // Close dumper and connected database + dumper.Close() + } +*/ +package mysqldump diff --git a/dump.go b/dump.go new file mode 100644 index 0000000..2489377 --- /dev/null +++ b/dump.go @@ -0,0 +1,204 @@ +package mysqldump + +import ( + "database/sql" + "errors" + "os" + "path" + "strings" + "text/template" + "time" +) + +type table struct { + Name string + SQL string + Values string +} + +type dump struct { + DumpVersion string + ServerVersion string + Tables []*table + CompleteTime string +} + +const version = "0.1.0" + +const tmpl = `-- Go SQL Dump {{ .DumpVersion }} +-- +-- ------------------------------------------------------ +-- Server version {{ .ServerVersion }} + + +{{range .Tables}} +-- +-- Table structure for table {{ .Name }} +-- + +DROP TABLE IF EXISTS {{ .Name }}; +{{ .SQL }}; +{{ if .Values }} +-- +-- Dumping data for table {{ .Name }} +-- + +LOCK TABLES {{ .Name }} WRITE; +INSERT INTO {{ .Name }} VALUES {{ .Values }}; +UNLOCK TABLES; +{{end}}{{ end }} +-- Dump completed on {{ .CompleteTime }} +` + +// Creates a MYSQL Dump based on the options supplied through the dumper. +func (d *Dumper) Dump() error { + name := time.Now().Format(d.format) + p := path.Join(d.dir, name+".sql") + + // Check dump directory + if e, _ := exists(p); e { + return errors.New("Dump '" + name + "' already exists.") + } + + // Create .sql file + f, err := os.Create(p) + if err != nil { + return err + } + defer f.Close() + + data := dump{ + DumpVersion: version, + Tables: make([]*table, 0), + } + + // Get server version + if data.ServerVersion, err = getServerVersion(d.db); err != nil { + return err + } + + // Get tables + tables, err := getTables(d.db) + if err != nil { + return err + } + + // Get sql for each table + for _, name := range tables { + if t, err := createTable(d.db, name); err == nil { + data.Tables = append(data.Tables, t) + } else { + return err + } + } + + // Set complete time + data.CompleteTime = time.Now().String() + + // Write dump to file + t, err := template.New("mysqldump").Parse(tmpl) + if err != nil { + return err + } + if err = t.Execute(f, data); err != nil { + return err + } + + return nil +} + +func getTables(db *sql.DB) ([]string, error) { + tables := make([]string, 0) + + // Get table list + rows, err := db.Query("SHOW TABLES") + if err != nil { + return tables, err + } + defer rows.Close() + + // Read result + for rows.Next() { + var table string + if err := rows.Scan(&table); err != nil { + return tables, err + } + tables = append(tables, table) + } + return tables, rows.Err() +} + +func getServerVersion(db *sql.DB) (string, error) { + var server_version string + if err := db.QueryRow("SELECT version()").Scan(&server_version); err != nil { + return "", err + } + return server_version, nil +} + +func createTable(db *sql.DB, name string) (*table, error) { + var err error + t := &table{Name: name} + + if t.SQL, err = createTableSQL(db, name); err != nil { + return nil, err + } + + if t.Values, err = createTableValues(db, name); err != nil { + return nil, err + } + + return t, nil +} + +func createTableSQL(db *sql.DB, name string) (string, error) { + // Get table creation SQL + var table_return string + var table_sql string + err := db.QueryRow("SHOW CREATE TABLE "+name).Scan(&table_return, &table_sql) + if err != nil { + return "", err + } + if table_return != name { + return "", errors.New("Returned table is not the same as requested table") + } + + return table_sql, nil +} + +func createTableValues(db *sql.DB, name string) (string, error) { + // Get Data + rows, err := db.Query("SELECT * FROM " + name) + if err != nil { + return "", err + } + defer rows.Close() + + // Get columns + columns, err := rows.Columns() + if err != nil { + return "", err + } + if len(columns) == 0 { + return "", errors.New("No columns in table " + name + ".") + } + + // Read data + data_text := make([]string, 0) + for rows.Next() { + // Init temp data storage + data := make([]string, len(columns)) + ptrs := make([]interface{}, len(columns)) + for i, _ := range data { + ptrs[i] = &data[i] + } + + // Read data + if err := rows.Scan(ptrs...); err != nil { + return "", err + } + data_text = append(data_text, "('"+strings.Join(data, "','")+"')") + } + + return strings.Join(data_text, ","), rows.Err() +} diff --git a/mysqldump.go b/mysqldump.go new file mode 100644 index 0000000..aebe950 --- /dev/null +++ b/mysqldump.go @@ -0,0 +1,71 @@ +package mysqldump + +import ( + "database/sql" + "errors" + "os" +) + +// Dumper represents a database. +type Dumper struct { + db *sql.DB + format string + dir string +} + +/* +Creates a new dumper. + + db: Database that will be dumped (https://golang.org/pkg/database/sql/#DB). + dir: Path to the directory where the dumps will be stored. + format: Format to be used to name each dump file. Uses time.Time.Format (https://golang.org/pkg/time/#Time.Format). format appended with '.sql'. +*/ +func Register(db *sql.DB, dir, format string) (*Dumper, error) { + if !isDir(dir) { + return nil, errors.New("Invalid directory") + } + + return &Dumper{ + db: db, + format: format, + dir: dir, + }, nil +} + +// Closes the dumper. +// Will also close the database the dumper is connected to. +// +// Not required. +func (d *Dumper) Close() error { + defer func() { + d.db = nil + }() + return d.db.Close() +} + +func exists(p string) (bool, os.FileInfo) { + f, err := os.Open(p) + if err != nil { + return false, nil + } + defer f.Close() + fi, err := f.Stat() + if err != nil { + return false, nil + } + return true, fi +} + +func isFile(p string) bool { + if e, fi := exists(p); e { + return fi.Mode().IsRegular() + } + return false +} + +func isDir(p string) bool { + if e, fi := exists(p); e { + return fi.Mode().IsDir() + } + return false +}