Index: perf/go/db/db.go |
diff --git a/perf/go/db/db.go b/perf/go/db/db.go |
index 405d1201856501ab75168705138bb3247efcf9b5..7cc6a6697d202a16e821f8f1a421100afe2e578c 100644 |
--- a/perf/go/db/db.go |
+++ b/perf/go/db/db.go |
@@ -9,66 +9,67 @@ import ( |
"github.com/golang/glog" |
_ "github.com/mattn/go-sqlite3" |
"skia.googlesource.com/buildbot.git/perf/go/metadata" |
+ "skia.googlesource.com/buildbot.git/perf/go/util" |
) |
var ( |
// DB is the sql database where we have commit and annotation information stored. |
DB *sql.DB = nil |
+ |
+ // Keeps track if we are connected to MySQL or SQLite |
+ isMySQL = true |
) |
const ( |
+ // Key of the password for the readwrite user. |
METADATA_KEY = "readwrite" |
+ |
+ // Path where the SQLite database is stored when running locally. |
+ SQLITE_DB_PATH = "./perf.db" |
) |
// Init must be called once before DB is used. |
// |
// Since it used glog, make sure it is also called after flag.Parse is called. |
-func Init() { |
- // Connect to MySQL server. First, get the password from the metadata server. |
- // See https://developers.google.com/compute/docs/metadata#custom. |
- password, err := metadata.Get(METADATA_KEY) |
- if err == nil { |
- // The IP address of the database is found here: |
- // https://console.developers.google.com/project/31977622648/sql/instances/skiaperf/overview |
- // And 3306 is the default port for MySQL. |
- DB, err = sql.Open("mysql", fmt.Sprintf("readwrite:%s@tcp(173.194.104.24:3306)/skia?parseTime=true", password)) |
+func Init(mysqlConnStr string) { |
+ // If there is a connection string then connect to the MySQL server. |
+ // This is for testing only. In production we get the relevant information |
+ // from the metadata server. |
+ if mysqlConnStr != "" { |
+ var err error |
+ glog.Infoln("Opening local MySQL database: ", mysqlConnStr) |
+ DB, err = sql.Open("mysql", mysqlConnStr) |
if err != nil { |
- glog.Fatalln("Failed to open connection to SQL server:", err) |
+ glog.Fatalln("Unable to connect to server: " + mysqlConnStr) |
} |
+ isMySQL = true |
} else { |
- glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) |
- // Fallback to sqlite for local use. |
- DB, err = sql.Open("sqlite3", "./perf.db") |
- if err != nil { |
- glog.Fatalln("Failed to open:", err) |
+ // Connect to MySQL server. First, get the password from the metadata server. |
+ // See https://developers.google.com/compute/docs/metadata#custom. |
+ password, err := metadata.Get(METADATA_KEY) |
+ if err == nil { |
+ // The IP address of the database is found here: |
+ // https://console.developers.google.com/project/31977622648/sql/instances/skiaperf/overview |
+ // And 3306 is the default port for MySQL. |
+ DB, err = sql.Open("mysql", fmt.Sprintf("readwrite:%s@tcp(173.194.104.24:3306)/skia?parseTime=true", password)) |
+ if err != nil { |
+ glog.Fatalln("Failed to open connection to SQL server:", err) |
+ } |
+ } else { |
+ glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) |
+ glog.Infof("Opening local sqlite database at: %s", SQLITE_DB_PATH) |
+ // Fallback to sqlite for local use. |
+ DB, err = sql.Open("sqlite3", SQLITE_DB_PATH) |
+ if err != nil { |
+ glog.Fatalln("Failed to open:", err) |
+ } |
+ isMySQL = false |
} |
+ } |
- sql := `CREATE TABLE clusters ( |
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
- ts TIMESTAMP NOT NULL, |
- hash TEXT NOT NULL, |
- regression FLOAT NOT NULL, |
- cluster MEDIUMTEXT NOT NULL, |
- status TEXT NOT NULL, |
- message TEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for clusters:", err) |
- |
- sql = `CREATE TABLE shortcuts ( |
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
- traces MEDIUMTEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for shortcuts:", err) |
- |
- sql = `CREATE TABLE tries ( |
- issue VARCHAR(255) NOT NULL PRIMARY KEY, |
- lastUpdated TIMESTAMP NOT NULL, |
- results MEDIUMTEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for tries:", err) |
+ // Make sure the migration table exists. |
+ if err := ensureVersionTable(); err != nil { |
+ glog.Fatalln("Unable to guarantee valid version table:", err.Error()) |
} |
// Ping the database to keep the connection fresh. |
@@ -82,3 +83,209 @@ func Init() { |
} |
}() |
} |
+ |
+func Migrate(targetVersion int) error { |
+ if targetVersion < 0 { |
+ glog.Fatalln("Target db version must be at least 0.") |
+ } |
+ |
+ // start a transaction |
+ txn, txErr := DB.Begin() |
+ if txErr != nil { |
+ return txErr |
+ } |
+ defer func() { |
+ if txErr != nil { |
+ txn.Rollback() |
+ } else { |
+ txn.Commit() |
+ } |
+ }() |
+ |
+ currentVersion, txErr := DBVersion() |
+ if txErr != nil { |
+ return txErr |
+ } |
+ |
+ // run through the transactions |
+ runSteps := getMigrations(isMySQL, currentVersion, targetVersion) |
+ for _, step := range runSteps { |
+ for _, stmt := range step { |
+ _, txErr = txn.Exec(stmt) |
+ if txErr != nil { |
+ return txErr |
+ } |
+ } |
+ } |
+ |
+ // update the dbversion table |
+ txErr = setDBVersion(txn, targetVersion) |
+ if txErr != nil { |
+ return txErr |
+ } |
+ |
+ return nil |
+} |
+ |
+func DBVersion() (int, error) { |
+ stmt := ` |
+ SELECT version |
tfarina
2014/09/29 18:56:33
does this fits in one line?
stephana
2014/09/30 13:43:06
It probably could, I find it easier to read this w
|
+ FROM sk_db_version |
+ WHERE id=1` |
+ |
+ var version int |
+ err := DB.QueryRow(stmt).Scan(&version) |
+ return version, err |
+} |
+ |
+func MaxDBVersion() int { |
+ return len(migrationSteps) |
+} |
+ |
+func setDBVersion(txn *sql.Tx, newDBVersion int) error { |
+ stmt := `REPLACE INTO sk_db_version (id, version, updated) VALUES(1, ?, ?)` |
+ _, err := txn.Exec(stmt, newDBVersion, time.Now().Unix()) |
+ return err |
+} |
+ |
+func ensureVersionTable() error { |
+ txn, txnErr := DB.Begin() |
+ defer func() { |
+ if txnErr != nil { |
+ glog.Errorf("Encountered error rolling back: %s", txnErr.Error()) |
+ txn.Rollback() |
+ } else { |
+ glog.Infoln("Version table OK.") |
+ txn.Commit() |
+ } |
+ }() |
+ |
+ if txnErr != nil { |
+ fmt.Errorf("Unable to start database transaction. %s", txnErr.Error()) |
+ } |
+ |
+ glog.Infoln("At 1 ------------------------------------------------") |
+ |
+ stmt := `CREATE TABLE IF NOT EXISTS sk_db_version ( |
jcgregorio
2014/09/29 16:59:07
I'm not sure if the current readwrite user has the
stephana
2014/09/29 18:07:50
Agreed. The readwrite user should not have those p
|
+ id INTEGER NOT NULL PRIMARY KEY, |
+ version INTEGER NOT NULL, |
+ updated BIGINT NOT NULL |
+ )` |
+ if _, txnErr = txn.Exec(stmt); txnErr != nil { |
+ return fmt.Errorf("Creating version table failed: %s", txnErr.Error()) |
+ } |
+ |
+ glog.Infoln("At 2 ------------------------------------------------") |
+ var count int |
+ if txnErr = txn.QueryRow("SELECT COUNT(*) FROM sk_db_version").Scan(&count); txnErr != nil { |
+ return fmt.Errorf("Unable to read version table: %s", txnErr.Error()) |
+ } |
+ |
+ glog.Infoln("At 3 ------------------------------------------------") |
+ |
+ if count == 0 { |
+ if txnErr = setDBVersion(txn, 0); txnErr != nil { |
+ return txnErr |
+ } |
+ } else if count > 1 { |
+ return fmt.Errorf("Version table contains more than one row.") |
+ } |
+ |
+ glog.Infoln("At 4 ------------------------------------------------") |
+ |
+ return txnErr |
+} |
+ |
+// Add any new migration to this list of functions |
+// NOTE: Do not change these once commited, but instead add a migration step. |
+var migrationSteps = []func() ([]string, []string, []string, []string){ |
+ migration_1, |
+} |
+ |
+// Returns the SQL statements base on whether we are using MySQL and the |
+// current and target DB version. |
+func getMigrations(isMySQL bool, currentVersion int, targetVersion int) [][]string { |
+ inc := util.SignInt(targetVersion - currentVersion) |
+ if inc == 0 { |
+ return [][]string{} |
+ } |
+ |
+ delta := util.AbsInt(targetVersion - currentVersion) |
+ result := make([][]string, 0, delta) |
+ for i := currentVersion + inc; (i >= 0) && (i < len(migrationSteps)) && (i != targetVersion); i += inc { |
+ var temp []string |
+ switch { |
+ // using mysqlp |
+ case (inc > 0) && isMySQL: |
+ _, _, temp, _ = migrationSteps[i]() |
+ case (inc < 0) && isMySQL: |
+ _, _, _, temp = migrationSteps[i]() |
+ // using sqlite |
+ case (inc > 0): |
+ temp, _, _, _ = migrationSteps[i]() |
+ case (inc < 0): |
+ _, temp, _, _ = migrationSteps[i]() |
+ } |
+ result = append(result, temp) |
+ } |
+ return result |
+} |
+ |
+func migration_1() (sqLiteUp, sqLiteDown, mySQLUp, mySQLDown []string) { |
+ // SQLite statements to alter the datase. |
+ sqLiteUp = []string{ |
+ `CREATE TABLE clusters ( |
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
+ ts TIMESTAMP NOT NULL, |
+ hash TEXT NOT NULL, |
+ regression FLOAT NOT NULL, |
+ cluster MEDIUMTEXT NOT NULL, |
+ status TEXT NOT NULL, |
+ message TEXT NOT NULL |
+ )`, |
+ `CREATE TABLE shortcuts ( |
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
+ traces MEDIUMTEXT NOT NULL |
+ )`, |
+ `CREATE TABLE tries ( |
+ issue VARCHAR(255) NOT NULL PRIMARY KEY, |
+ lastUpdated TIMESTAMP NOT NULL, |
+ results MEDIUMTEXT NOT NULL |
+ )`, |
+ } |
+ |
+ // When using SQLite we fully reverse the previous step. |
+ sqLiteDown = []string{ |
+ `DROP TABLE IF EXISTS clusters`, |
+ `DROP TABLE IF EXISTS shortcuts`, |
+ `DROP TABLE IF EXISTS tries`, |
+ } |
+ |
+ mySQLUp = []string{ |
+ `CREATE TABLE shortcuts ( |
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, |
+ traces MEDIUMTEXT NOT NULL |
+ )`, |
+ |
+ `CREATE TABLE clusters ( |
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, |
+ ts BIGINT NOT NULL, |
+ hash TEXT NOT NULL, |
+ regression FLOAT NOT NULL, |
+ cluster MEDIUMTEXT NOT NULL, |
+ status TEXT NOT NULL, |
+ message TEXT NOT NULL |
+ )`, |
+ |
+ `CREATE TABLE tries ( |
+ issue VARCHAR(255) NOT NULL PRIMARY KEY, |
+ lastUpdated BIGINT NOT NULL, |
+ results LONGTEXT NOT NULL |
+ )`, |
+ } |
+ |
+ // We are not removing the tables to make sure we don't delete existing |
+ // data. This is just for the first migration. |
+ mySQLDown = []string{} |
+ return |
+} |