-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f22ac7a
Showing
17 changed files
with
608 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package exodus | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
// Migration is a fully-formed SQL command that can be ran against | ||
// a database connection. | ||
type Migration string | ||
|
||
// MigrationInterface ... | ||
type MigrationInterface interface { | ||
Up() Migration | ||
Down() Migration | ||
} | ||
|
||
// Create generates an SQL command to create a table using the | ||
// schema provided. | ||
func Create(table string, schema Schema) Migration { | ||
sql := strings.Join(loadColumnSQL(schema), ", ") | ||
|
||
return Migration(fmt.Sprintf("CREATE TABLE %s ( %s );", table, sql)) | ||
} | ||
|
||
// Drop generates an SQL command to drop the given table. | ||
func Drop(table string) Migration { | ||
return Migration(fmt.Sprintf("DROP TABLE %s", table)) | ||
} | ||
|
||
// loadColumnSQL iterates through the Columns defined in the | ||
// Schema and calls the toSQL command on them. The resulting | ||
// SQL for each column is stored in a slice of strings and | ||
// returned. | ||
func loadColumnSQL(schema Schema) (commands []string) { | ||
for _, col := range schema { | ||
commands = append(commands, col.ToSQL()) | ||
} | ||
|
||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package exodus | ||
|
||
import ( | ||
"database/sql" | ||
"fmt" | ||
"log" | ||
"reflect" | ||
) | ||
|
||
// supportedDrivers lists the drivers that currently work with | ||
// the migration framework. | ||
var supportedDrivers = []string{ | ||
"sqlite3", | ||
} | ||
|
||
// Migrator is responsible for receiving the incoming migrations | ||
// and running their SQL. | ||
type Migrator struct { | ||
DB *sql.DB | ||
Batch int | ||
} | ||
|
||
// NewMigrator creates a new instance of a migrator. | ||
func NewMigrator(db *sql.DB) (*Migrator, error) { | ||
m := Migrator{ | ||
DB: db, | ||
} | ||
|
||
if !m.driverIsSupported(m.getDriverName()) { | ||
return nil, fmt.Errorf("the %s driver is currently unsupported", m.getDriverName()) | ||
} | ||
|
||
return &m, nil | ||
} | ||
|
||
// TableExists determines if a table exists on the database. | ||
// TODO: Probably a better way of doing this. | ||
func (m *Migrator) TableExists(table string, database *sql.DB) bool { | ||
sql := fmt.Sprintf("SELECT * FROM %s LIMIT 1", table) | ||
if _, err := database.Exec(sql); err != nil { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
// Fresh drops all tables in the database. | ||
func (m *Migrator) Fresh(database *sql.DB) { | ||
if err := m.dropAllTables(database); err != nil { | ||
log.Fatalln(err) | ||
} | ||
} | ||
|
||
// dropAllTables grabs the tables from the database and drops | ||
// them in turn, stopping if there is an error. | ||
// TODO: Wrap this in a transaction, so it is cancelled if any | ||
// of the drops fail? | ||
func (m *Migrator) dropAllTables(database *sql.DB) error { | ||
// Get the SQL command to drop all tables for the current | ||
// SQL driver provided in the database connection. | ||
dropSQL, err := m.getDropSQLForDriver(m.getDriverName()) | ||
if err != nil { | ||
// If support for the driver does not exist, log a | ||
// fatal error. | ||
log.Fatalln("Unable to drop tables:", err) | ||
} | ||
|
||
rows, err := database.Query(dropSQL) | ||
if err != nil { | ||
return err | ||
} | ||
defer rows.Close() | ||
|
||
// tables is the list of tables returned from the database. | ||
var tables []string | ||
|
||
// for each row returned, add the name of it to the | ||
// tables slice. | ||
for rows.Next() { | ||
var name string | ||
if err := rows.Scan(&name); err != nil { | ||
return err | ||
} | ||
if name == "sqlite_sequence" { | ||
continue | ||
} | ||
tables = append(tables, name) | ||
} | ||
if err := rows.Err(); err != nil { | ||
return err | ||
} | ||
|
||
for _, table := range tables { | ||
if _, err := database.Exec("DROP TABLE IF EXISTS " + table); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (m *Migrator) getDropSQLForDriver(d string) (string, error) { | ||
// TODO: Add more driver support. | ||
// Postgres? Then that'll do. | ||
if d == "sqlite3" { | ||
return "SELECT name FROM sqlite_master WHERE type='table'", nil | ||
} | ||
|
||
if d == "mysql" { | ||
return "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'", nil | ||
} | ||
|
||
return "", fmt.Errorf("`%s` driver is not yet supported", d) | ||
} | ||
|
||
// nextBatchNumber retreives the highest batch number from the | ||
// migrations table and increments it by one. | ||
func (m *Migrator) nextBatchNumber() int { | ||
return m.lastBatchNumber() + 1 | ||
} | ||
|
||
// lastBatchNumber retrieves the number of the last batch ran | ||
// on the migrations table. | ||
func (m *Migrator) lastBatchNumber() int { | ||
r := m.DB.QueryRow("SELECT MAX(batch) FROM migrations") | ||
var num int | ||
r.Scan(&num) | ||
return num | ||
} | ||
|
||
// TODO: Wrap this in a transaction and reverse it | ||
func (m *Migrator) Run(migrations ...MigrationInterface) error { | ||
m.verifyMigrationsTable() | ||
|
||
batch := m.nextBatchNumber() | ||
|
||
for _, migration := range migrations { | ||
if _, err := m.DB.Exec(string(migration.Up())); err != nil { | ||
return err | ||
} | ||
|
||
m.addBatchToMigrationsTable(migration, batch) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (m *Migrator) addBatchToMigrationsTable(migration MigrationInterface, batch int) { | ||
stmt, err := m.DB.Prepare("INSERT INTO migrations (migration, batch) VALUES ( ?, ? )") | ||
if err != nil { | ||
log.Fatalln("Cannot create `migrations` batch statement. ") | ||
} | ||
defer stmt.Close() | ||
|
||
if _, err = stmt.Exec(reflect.TypeOf(migration).String(), batch); err != nil { | ||
log.Fatalln(err) | ||
} | ||
} | ||
|
||
// prepMigrations ensures that the migrations are ready to | ||
// be ran. | ||
func (m *Migrator) verifyMigrationsTable() { | ||
if !m.TableExists("migrations", m.DB) { | ||
if err := m.createMigrationsTable(); err != nil { | ||
log.Fatalln("Could not create `migrations` table: ", err) | ||
} | ||
} | ||
} | ||
|
||
func (m *Migrator) driverIsSupported(driver string) bool { | ||
for _, d := range supportedDrivers { | ||
if d == driver { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// getDriverName returns the name of the SQL driver currently | ||
// associated with the Migrator. | ||
func (m *Migrator) getDriverName() string { | ||
sqlDriverNamesByType := map[reflect.Type]string{} | ||
|
||
for _, driverName := range sql.Drivers() { | ||
// Tested empty string DSN with MySQL, PostgreSQL, and SQLite3 drivers. | ||
db, _ := sql.Open(driverName, "") | ||
|
||
if db != nil { | ||
driverType := reflect.TypeOf(db.Driver()) | ||
sqlDriverNamesByType[driverType] = driverName | ||
} | ||
} | ||
|
||
driverType := reflect.TypeOf(m.DB.Driver()) | ||
if driverName, found := sqlDriverNamesByType[driverType]; found { | ||
return driverName | ||
} | ||
|
||
return "" | ||
} | ||
|
||
// createMigrationsTable makes a table to hold migrations and | ||
// the order that they were executed. | ||
func (m *Migrator) createMigrationsTable() error { | ||
migrationSchema := fmt.Sprintf( | ||
"CREATE TABLE migrations ( %s, %s, %s )", | ||
"id integer not null primary key autoincrement", | ||
"migration varchar not null", | ||
"batch integer not null", | ||
) | ||
|
||
if _, err := m.DB.Exec(migrationSchema); err != nil { | ||
return fmt.Errorf("error creating migrations table: %s", err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# Exodus | ||
|
||
Database Migrations in Go. | ||
|
||
> Currently, only the SQLite3 driver is supported. Obviously this is not ideal. Support | ||
> for at least MySQL and Postgresql will come in the future. | ||
> **Notice:** This is very beta, and is very subject to change. It may be that eventually | ||
> the package will be rereleased with breaking changes and improvements down the line. | ||
> Please don't rely on this for anything critical. | ||
## Installation | ||
|
||
Use Go Modules. | ||
|
||
`TODO: Add Installation instructions` | ||
|
||
## Usage | ||
|
||
There's not exactly a Laravel / Rails / Zend / <Framework> way of running the migrations, | ||
yet, in that there is no command line utility to run or generate the migrations. Much | ||
of this will be streamlined in future releases. | ||
|
||
1. Create a new struct type. The type should be the name of the migration: | ||
|
||
```go | ||
type CreateUsersTable struct{} | ||
``` | ||
|
||
2. Define two methods on the created struct: `Up()` and `Down()`. These should both | ||
return an `exodus.Migration`. This satisfies the `exodus.MigrationInterface`. | ||
|
||
The `Up()` function should run the *creative* side of the migration, e.g., creating | ||
a new table. The `Down()` function should run the *destructive* side of the migration, | ||
e.g., dropping the table. | ||
|
||
```go | ||
func (m CreateUsersTable) Up() exodus.Migration { | ||
return exodus.Create("users", exodus.Schema{ | ||
column.Int("id").Increments().PrimaryKey(), | ||
column.String("email", 100).NotNullable().Unique(), | ||
column.String("name", 60).NotNullable(), | ||
column.Timestamp("activated_at"), | ||
column.Date("birthday"), | ||
|
||
column.UniqueSet("unique_name_birthday", "name", "birthday"), | ||
}) | ||
} | ||
|
||
// Down reverts the changes on the database. | ||
func (m CreateUsersTable) Down() exodus.Migration { | ||
return exodus.Drop("users") | ||
} | ||
``` | ||
|
||
3. As you can see above, there exists a Create method and a Drop method. More methods | ||
(change, add, remove column) will be added at some point. | ||
|
||
The `exodus.Create` method accepts a table name as a string, and an `exodus.Schema`, which | ||
is a slice of items that implement the [`exodus.Columnable`](column/Column.go) interface. | ||
It's easy to add columns to this schema, as you can see in the above `Up()` migration. | ||
|
||
The supported column types are: | ||
|
||
- `column.Binary`: creates a `binary` column. | ||
- `column.Boolean`: creates a `boolean` column. | ||
- `column.Char`: creates a `char` column. Must be passed a length as the second parameter. | ||
- `column.Date`: creates a `date` column. | ||
- `column.DateTime`: creates a `datetime` column. | ||
- `column.Int`: creates an `int` column. Currently only `int` is supported. | ||
- `column.String`: creates a `varchar` column. Must be passed a length as the second parameter. | ||
- `column.Text`: creates a `text` column. | ||
- `column.Timestamp`: creates a `timestamp` column. | ||
|
||
These columns can have modifiers chained to them, as you can see in the `Up()` migration | ||
above. Their effects should be obvious: | ||
|
||
- `Unique()` | ||
- `Default(value string)` | ||
- `Increments()` | ||
- `PrimaryKey()` | ||
- `NotNullable()` | ||
- `Nullable()` | ||
- `Length()` | ||
|
||
4. When your migrations have been created, create an `exodus.Migrator`, and pass it an `*sql.DB`. | ||
The function will return an error if the DB driver passed in is not supported. | ||
|
||
```go | ||
db, _ := sql.Open("sqlite3", "./database.db") | ||
defer db.Close() | ||
|
||
migrator, err := exodus.NewMigrator(db) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
``` | ||
|
||
5. Finally, use the migrator to run the Migrations. You can pass as many migrations | ||
as you like into the Run function: | ||
|
||
```go | ||
migrator.Run(migrations ...MigrationInterface) | ||
``` | ||
|
||
The tables should now exist in your database. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package exodus | ||
|
||
import "github.com/gostalt/exodus/column" | ||
|
||
// Schema is a slice of items that satisfy the Columnable interface. | ||
type Schema []column.Columnable |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package column | ||
|
||
// Binary creates a binary column. | ||
func Binary(name string) *Column { | ||
return &Column{ | ||
Name: name, | ||
datatype: "binary", | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package column | ||
|
||
// Boolean returns a bool column. | ||
func Boolean(name string) *Column { | ||
return &Column{ | ||
Name: name, | ||
datatype: "boolean", | ||
} | ||
} |
Oops, something went wrong.