Add SQLite support

This commit is contained in:
Adrian Macneil 2015-12-01 11:38:31 -08:00
parent 80eec3fe7b
commit d5f862c00b
6 changed files with 303 additions and 6 deletions

View file

@ -2,11 +2,10 @@ FROM alpine:edge
ENV GOPATH /go ENV GOPATH /go
ENV PATH /go/bin:$PATH ENV PATH /go/bin:$PATH
ENV CGO_ENABLED 0
# install build dependencies # install build dependencies
RUN echo "http://dl-4.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ RUN echo "http://dl-4.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
apk add -U --no-progress go go-tools git ca-certificates apk add -U --no-progress alpine-sdk go go-tools
RUN go get \ RUN go get \
github.com/golang/lint/golint \ github.com/golang/lint/golint \
github.com/kisielk/errcheck \ github.com/kisielk/errcheck \

View file

@ -6,7 +6,7 @@ Dbmate is a database migration tool, to keep your database schema in sync across
## Features ## Features
* Supports PostgreSQL and MySQL. * Supports MySQL, PostgreSQL, and SQLite.
* Powerful, [purpose-built DSL](https://en.wikipedia.org/wiki/SQL#Data_definition) for writing schema migrations. * Powerful, [purpose-built DSL](https://en.wikipedia.org/wiki/SQL#Data_definition) for writing schema migrations.
* Migrations are timestamp-versioned, to avoid version number conflicts with multiple developers. * Migrations are timestamp-versioned, to avoid version number conflicts with multiple developers.
* Supports creating and dropping databases (handy in development/test). * Supports creating and dropping databases (handy in development/test).
@ -53,7 +53,7 @@ DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_development?sslmode=disab
protocol://username:password@host:port/database_name?options protocol://username:password@host:port/database_name?options
``` ```
* `protocol` must be either `mysql` or `postgres` * `protocol` must be one of `mysql`, `postgres`, `postgresql`, `sqlite`, `sqlite3`
* `host` can be specified either as a hostname or IP address * `host` can be specified either as a hostname or IP address
* `options` are driver-specific (refer to the underlying Go SQL drivers if you wish to use these) * `options` are driver-specific (refer to the underlying Go SQL drivers if you wish to use these)

View file

@ -38,6 +38,7 @@ func testURLs(t *testing.T) []*url.URL {
return []*url.URL{ return []*url.URL{
postgresTestURL(t), postgresTestURL(t),
mySQLTestURL(t), mySQLTestURL(t),
sqliteTestURL(t),
} }
} }
@ -155,7 +156,7 @@ func testRollbackCommandURL(t *testing.T, u *url.URL) {
err = db.QueryRow("select count(*) from users").Scan(&count) err = db.QueryRow("select count(*) from users").Scan(&count)
require.NotNil(t, err) require.NotNil(t, err)
require.Regexp(t, "(does not exist|doesn't exist)", err.Error()) require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error())
} }
func TestRollbackCommand(t *testing.T) { func TestRollbackCommand(t *testing.T) {

View file

@ -28,8 +28,10 @@ func GetDriver(name string) (Driver, error) {
switch name { switch name {
case "mysql": case "mysql":
return MySQLDriver{}, nil return MySQLDriver{}, nil
case "postgres": case "postgres", "postgresql":
return PostgresDriver{}, nil return PostgresDriver{}, nil
case "sqlite", "sqlite3":
return SQLiteDriver{}, nil
default: default:
return nil, fmt.Errorf("Unknown driver: %s", name) return nil, fmt.Errorf("Unknown driver: %s", name)
} }

118
sqlite.go Normal file
View file

@ -0,0 +1,118 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
"net/url"
"os"
"regexp"
)
// SQLiteDriver provides top level database functions
type SQLiteDriver struct {
}
func sqlitePath(u *url.URL) string {
// strip one leading slash
// absolute URLs can be specified as sqlite:////tmp/foo.sqlite3
str := regexp.MustCompile("^/").ReplaceAllString(u.Path, "")
return str
}
// Open creates a new database connection
func (drv SQLiteDriver) Open(u *url.URL) (*sql.DB, error) {
return sql.Open("sqlite3", sqlitePath(u))
}
// CreateDatabase creates the specified database
func (drv SQLiteDriver) CreateDatabase(u *url.URL) error {
fmt.Printf("Creating: %s\n", sqlitePath(u))
db, err := drv.Open(u)
if err != nil {
return err
}
defer mustClose(db)
return db.Ping()
}
// DropDatabase drops the specified database (if it exists)
func (drv SQLiteDriver) DropDatabase(u *url.URL) error {
path := sqlitePath(u)
fmt.Printf("Dropping: %s\n", path)
exists, err := drv.DatabaseExists(u)
if err != nil {
return err
}
if !exists {
return nil
}
return os.Remove(path)
}
// DatabaseExists determines whether the database exists
func (drv SQLiteDriver) DatabaseExists(u *url.URL) (bool, error) {
_, err := os.Stat(sqlitePath(u))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// CreateMigrationsTable creates the schema_migrations table
func (drv SQLiteDriver) CreateMigrationsTable(db *sql.DB) error {
_, err := db.Exec(`create table if not exists schema_migrations (
version varchar(255) primary key)`)
return err
}
// SelectMigrations returns a list of applied migrations
// with an optional limit (in descending order)
func (drv SQLiteDriver) SelectMigrations(db *sql.DB, limit int) (map[string]bool, error) {
query := "select version from schema_migrations order by version desc"
if limit >= 0 {
query = fmt.Sprintf("%s limit %d", query, limit)
}
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer mustClose(rows)
migrations := map[string]bool{}
for rows.Next() {
var version string
if err := rows.Scan(&version); err != nil {
return nil, err
}
migrations[version] = true
}
return migrations, nil
}
// InsertMigration adds a new migration record
func (drv SQLiteDriver) InsertMigration(db Transaction, version string) error {
_, err := db.Exec("insert into schema_migrations (version) values (?)", version)
return err
}
// DeleteMigration removes a migration record
func (drv SQLiteDriver) DeleteMigration(db Transaction, version string) error {
_, err := db.Exec("delete from schema_migrations where version = ?", version)
return err
}

177
sqlite_test.go Normal file
View file

@ -0,0 +1,177 @@
package main
import (
"database/sql"
"github.com/stretchr/testify/require"
"net/url"
"os"
"testing"
)
func sqliteTestURL(t *testing.T) *url.URL {
u, err := url.Parse("sqlite3:////tmp/dbmate.sqlite3")
require.Nil(t, err)
return u
}
func prepTestSQLiteDB(t *testing.T) *sql.DB {
drv := SQLiteDriver{}
u := sqliteTestURL(t)
// drop any existing database
err := drv.DropDatabase(u)
require.Nil(t, err)
// create database
err = drv.CreateDatabase(u)
require.Nil(t, err)
// connect database
db, err := drv.Open(u)
require.Nil(t, err)
return db
}
func TestSQLiteCreateDropDatabase(t *testing.T) {
drv := SQLiteDriver{}
u := sqliteTestURL(t)
// drop any existing database
err := drv.DropDatabase(u)
require.Nil(t, err)
// create database
err = drv.CreateDatabase(u)
require.Nil(t, err)
// check that database exists
_, err = os.Stat(sqlitePath(u))
require.Nil(t, err)
// drop the database
err = drv.DropDatabase(u)
require.Nil(t, err)
// check that database no longer exists
_, err = os.Stat(sqlitePath(u))
require.NotNil(t, err)
require.Equal(t, true, os.IsNotExist(err))
}
func TestSQLiteDatabaseExists(t *testing.T) {
drv := SQLiteDriver{}
u := sqliteTestURL(t)
// drop any existing database
err := drv.DropDatabase(u)
require.Nil(t, err)
// DatabaseExists should return false
exists, err := drv.DatabaseExists(u)
require.Nil(t, err)
require.Equal(t, false, exists)
// create database
err = drv.CreateDatabase(u)
require.Nil(t, err)
// DatabaseExists should return true
exists, err = drv.DatabaseExists(u)
require.Nil(t, err)
require.Equal(t, true, exists)
}
func TestSQLiteCreateMigrationsTable(t *testing.T) {
drv := SQLiteDriver{}
db := prepTestSQLiteDB(t)
defer mustClose(db)
// migrations table should not exist
count := 0
err := db.QueryRow("select count(*) from schema_migrations").Scan(&count)
require.Regexp(t, "no such table: schema_migrations", err.Error())
// create table
err = drv.CreateMigrationsTable(db)
require.Nil(t, err)
// migrations table should exist
err = db.QueryRow("select count(*) from schema_migrations").Scan(&count)
require.Nil(t, err)
// create table should be idempotent
err = drv.CreateMigrationsTable(db)
require.Nil(t, err)
}
func TestSQLiteSelectMigrations(t *testing.T) {
drv := SQLiteDriver{}
db := prepTestSQLiteDB(t)
defer mustClose(db)
err := drv.CreateMigrationsTable(db)
require.Nil(t, err)
_, err = db.Exec(`insert into schema_migrations (version)
values ('abc2'), ('abc1'), ('abc3')`)
require.Nil(t, err)
migrations, err := drv.SelectMigrations(db, -1)
require.Nil(t, err)
require.Equal(t, true, migrations["abc1"])
require.Equal(t, true, migrations["abc2"])
require.Equal(t, true, migrations["abc2"])
// test limit param
migrations, err = drv.SelectMigrations(db, 1)
require.Nil(t, err)
require.Equal(t, true, migrations["abc3"])
require.Equal(t, false, migrations["abc1"])
require.Equal(t, false, migrations["abc2"])
}
func TestSQLiteInsertMigration(t *testing.T) {
drv := SQLiteDriver{}
db := prepTestSQLiteDB(t)
defer mustClose(db)
err := drv.CreateMigrationsTable(db)
require.Nil(t, err)
count := 0
err = db.QueryRow("select count(*) from schema_migrations").Scan(&count)
require.Nil(t, err)
require.Equal(t, 0, count)
// insert migration
err = drv.InsertMigration(db, "abc1")
require.Nil(t, err)
err = db.QueryRow("select count(*) from schema_migrations where version = 'abc1'").
Scan(&count)
require.Nil(t, err)
require.Equal(t, 1, count)
}
func TestSQLiteDeleteMigration(t *testing.T) {
drv := SQLiteDriver{}
db := prepTestSQLiteDB(t)
defer mustClose(db)
err := drv.CreateMigrationsTable(db)
require.Nil(t, err)
_, err = db.Exec(`insert into schema_migrations (version)
values ('abc1'), ('abc2')`)
require.Nil(t, err)
err = drv.DeleteMigration(db, "abc2")
require.Nil(t, err)
count := 0
err = db.QueryRow("select count(*) from schema_migrations").Scan(&count)
require.Nil(t, err)
require.Equal(t, 1, count)
}