├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── dbmigrate.go └── dbmigrate_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | bin 24 | src 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - release 4 | 5 | before_script: 6 | - go get 7 | 8 | script: 9 | - go test -v 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tanel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GOPATH=$(shell pwd) 2 | 3 | default: fmt build test 4 | 5 | fmt: 6 | go fmt 7 | 8 | build: 9 | go build 10 | 11 | test: 12 | go test 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hello :) There is a much better tool available now, check out [github.com/wallester/migrate](https://github.com/wallester/migrate) 2 | 3 | -------------------------------------------------------------------------------- /dbmigrate.go: -------------------------------------------------------------------------------- 1 | package dbmigrate 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/gocql/gocql" 13 | ) 14 | 15 | // Database interface needs to be inmplemented to migrate a new type of database 16 | 17 | type Database interface { 18 | CreateMigrationsTable() error 19 | HasMigrated(filename string) (bool, error) 20 | Migrate(filename string, migration string) error 21 | } 22 | 23 | // CassandraDatabase migrates Cassandra databases 24 | 25 | type CassandraDatabase struct { 26 | readerSession *gocql.Session 27 | writerSession *gocql.Session 28 | } 29 | 30 | func (cassandra *CassandraDatabase) CreateMigrationsTable() error { 31 | err := cassandra.writerSession.Query(` 32 | CREATE TABLE migrations ( 33 | name TEXT, 34 | created_at TIMEUUID, 35 | PRIMARY KEY (name) 36 | ); 37 | `).Exec() 38 | if err != nil { 39 | if !strings.Contains(err.Error(), "Cannot add already existing column family") { 40 | return err 41 | } 42 | } 43 | fmt.Println("Created migrations table") 44 | return nil 45 | } 46 | 47 | func (cassandra *CassandraDatabase) HasMigrated(filename string) (bool, error) { 48 | var count int 49 | iter := cassandra.readerSession.Query(` 50 | SELECT COUNT(*) FROM migrations WHERE name = ? 51 | `, filename).Iter() 52 | if !iter.Scan(&count) { 53 | return false, iter.Close() 54 | } 55 | if err := iter.Close(); err != nil { 56 | return false, err 57 | } 58 | return count > 0, nil 59 | } 60 | 61 | func (cassandra *CassandraDatabase) Migrate(filename string, migration string) error { 62 | if err := cassandra.writerSession.Query(migration).Exec(); err != nil { 63 | return err 64 | } 65 | return cassandra.writerSession.Query(` 66 | INSERT INTO migrations(name, created_at) 67 | VALUES(?, NOW()) 68 | `, filename).Exec() 69 | } 70 | 71 | func NewCassandraDatabase(readerSession *gocql.Session, writerSession *gocql.Session) *CassandraDatabase { 72 | return &CassandraDatabase{ 73 | readerSession: readerSession, 74 | writerSession: writerSession, 75 | } 76 | } 77 | 78 | // PostgresDatabase migrates Postgresql databases 79 | 80 | type PostgresDatabase struct { 81 | database *sql.DB 82 | } 83 | 84 | func (postgres *PostgresDatabase) CreateMigrationsTable() error { 85 | _, err := postgres.database.Exec(` 86 | CREATE TABLE IF NOT EXISTS migrations ( 87 | id serial, 88 | name text NOT NULL, 89 | created_at timestamp with time zone NOT NULL, 90 | CONSTRAINT "PK_migrations_id" PRIMARY KEY (id) 91 | );`) 92 | if err != nil { 93 | return err 94 | } 95 | _, err = postgres.database.Exec("create unique index idx_migrations_name on migrations(name)") 96 | if err != nil { 97 | if !strings.Contains(err.Error(), "already exists") { 98 | return err 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | func (postgres *PostgresDatabase) HasMigrated(filename string) (bool, error) { 105 | var count int 106 | err := postgres.database.QueryRow("select count(1) from migrations where name = $1", filename).Scan(&count) 107 | if err != nil { 108 | return false, err 109 | } 110 | return count > 0, nil 111 | } 112 | 113 | func (postgres *PostgresDatabase) Migrate(filename string, migration string) error { 114 | _, err := postgres.database.Exec(migration) 115 | if err != nil { 116 | return err 117 | } 118 | _, err = postgres.database.Exec("insert into migrations(name, created_at) values($1, current_timestamp)", filename) 119 | return err 120 | } 121 | 122 | func NewPostgresDatabase(db *sql.DB) *PostgresDatabase { 123 | return &PostgresDatabase{database: db} 124 | } 125 | 126 | // By default, apply Postgresql migrations, as in older versions 127 | func Run(db *sql.DB, migrationsFolder string) error { 128 | postgres := NewPostgresDatabase(db) 129 | return ApplyMigrations(postgres, migrationsFolder) 130 | } 131 | 132 | // Run applies migrations from migrationsFolder to database. 133 | func ApplyMigrations(database Database, migrationsFolder string) error { 134 | // Initialize migrations table, if it does not exist yet 135 | if err := database.CreateMigrationsTable(); err != nil { 136 | return err 137 | } 138 | 139 | // Scan migration file names in migrations folder 140 | d, err := os.Open(migrationsFolder) 141 | if err != nil { 142 | return err 143 | } 144 | dir, err := d.Readdir(-1) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // Run migrations 150 | sqlFiles := make([]string, 0) 151 | for _, f := range dir { 152 | ext := filepath.Ext(f.Name()) 153 | if ".sql" == ext || ".cql" == ext { 154 | sqlFiles = append(sqlFiles, f.Name()) 155 | } 156 | } 157 | sort.Strings(sqlFiles) 158 | for _, filename := range sqlFiles { 159 | // if exists in migrations table, leave it 160 | // else execute sql 161 | migrated, err := database.HasMigrated(filename) 162 | if err != nil { 163 | return err 164 | } 165 | fullpath := filepath.Join(migrationsFolder, filename) 166 | if migrated { 167 | fmt.Println("Already migrated", fullpath) 168 | continue 169 | } 170 | b, err := ioutil.ReadFile(fullpath) 171 | if err != nil { 172 | return err 173 | } 174 | migration := string(b) 175 | if len(migration) == 0 { 176 | fmt.Println("Skipping empty file", fullpath) 177 | continue // empty file 178 | } 179 | err = database.Migrate(filename, migration) 180 | if err != nil { 181 | return err 182 | } 183 | fmt.Println("Migrated", fullpath) 184 | } 185 | 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /dbmigrate_test.go: -------------------------------------------------------------------------------- 1 | package dbmigrate 2 | 3 | import "testing" 4 | 5 | // There are no tests yet. Sorry. 6 | 7 | func TestTimeConsuming(t *testing.T) { 8 | if testing.Short() { 9 | t.Skip("skipping test in short mode.") 10 | } 11 | } 12 | --------------------------------------------------------------------------------