Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
tmus committed May 22, 2019
0 parents commit f22ac7a
Show file tree
Hide file tree
Showing 17 changed files with 608 additions and 0 deletions.
41 changes: 41 additions & 0 deletions Migration.go
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
}
218 changes: 218 additions & 0 deletions Migrator.go
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
}
106 changes: 106 additions & 0 deletions Readme.md
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.
6 changes: 6 additions & 0 deletions Schema.go
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
9 changes: 9 additions & 0 deletions column/Binary.go
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",
}
}
9 changes: 9 additions & 0 deletions column/Boolean.go
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",
}
}
Loading

0 comments on commit f22ac7a

Please sign in to comment.