Added Firebird support (#191)

* Added Firebird support

* Fixed typo

* Refactoring

* Schema migrations table name don't have to be upper case

* Fixed readme

* Added Firebird 2.5 support

* Removed SchemaName

* Refactoring
This commit is contained in:
Kirill Muratov 2019-03-21 11:51:49 +03:00 committed by Dale Hui
parent 46fb82ce3d
commit 2c9773ed89
19 changed files with 501 additions and 2 deletions

View File

@ -36,6 +36,7 @@ Database drivers run migrations. [Add a new database?](database/driver.go)
* [Google Cloud Spanner](database/spanner)
* [CockroachDB](database/cockroachdb)
* [ClickHouse](database/clickhouse)
* [Firebird](database/firebird)
### Database URLs

View File

@ -0,0 +1,12 @@
# firebird
`firebirdsql://user:password@servername[:port_number]/database_name_or_file[?params1=value1[&param2=value2]...]`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
| `auth_plugin_name` | | Authentication plugin name. Srp256/Srp/Legacy_Auth are available. (default is Srp) |
| `column_name_to_lower` | | Force column name to lower. (default is false) |
| `role` | | Role name |
| `tzname` | | Time Zone name. (For Firebird 4.0+) |
| `wire_crypt` | | Enable wire data encryption or not. For Firebird 3.0+ (default is true) |

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,5 @@
CREATE TABLE users (
user_id integer unique,
name varchar(40),
email varchar(40)
);

View File

@ -0,0 +1 @@
ALTER TABLE users DROP city;

View File

@ -0,0 +1,3 @@
ALTER TABLE users ADD city varchar(100);

View File

@ -0,0 +1 @@
DROP INDEX users_email_index;

View File

@ -0,0 +1,3 @@
CREATE UNIQUE INDEX users_email_index ON users (email);
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.

View File

@ -0,0 +1 @@
DROP TABLE books;

View File

@ -0,0 +1,5 @@
CREATE TABLE books (
user_id integer,
name varchar(40),
author varchar(40)
);

View File

@ -0,0 +1 @@
DROP TABLE movies;

View File

@ -0,0 +1,5 @@
CREATE TABLE movies (
user_id integer,
name varchar(40),
director varchar(40)
);

View File

@ -0,0 +1,246 @@
// +build go1.9
package firebird
import (
"context"
"database/sql"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database"
"github.com/hashicorp/go-multierror"
_ "github.com/nakagami/firebirdsql"
"io"
"io/ioutil"
nurl "net/url"
)
func init() {
db := Firebird{}
database.Register("firebird", &db)
database.Register("firebirdsql", &db)
}
var DefaultMigrationsTable = "schema_migrations"
var (
ErrNilConfig = fmt.Errorf("no config")
)
type Config struct {
DatabaseName string
MigrationsTable string
}
type Firebird struct {
// Locking and unlocking need to use the same connection
conn *sql.Conn
db *sql.DB
isLocked bool
// Open and WithInstance need to guarantee that config is never nil
config *Config
}
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}
if err := instance.Ping(); err != nil {
return nil, err
}
if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}
conn, err := instance.Conn(context.Background())
if err != nil {
return nil, err
}
fb := &Firebird{
conn: conn,
db: instance,
config: config,
}
if err := fb.ensureVersionTable(); err != nil {
return nil, err
}
return fb, nil
}
func (f *Firebird) Open(dsn string) (database.Driver, error) {
purl, err := nurl.Parse(dsn)
if err != nil {
return nil, err
}
db, err := sql.Open("firebirdsql", migrate.FilterCustomQuery(purl).String())
if err != nil {
return nil, err
}
px, err := WithInstance(db, &Config{
MigrationsTable: purl.Query().Get("x-migrations-table"),
DatabaseName: purl.Path,
})
if err != nil {
return nil, err
}
return px, nil
}
func (f *Firebird) Close() error {
connErr := f.conn.Close()
dbErr := f.db.Close()
if connErr != nil || dbErr != nil {
return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
}
return nil
}
func (f *Firebird) Lock() error {
if f.isLocked {
return database.ErrLocked
}
f.isLocked = true
return nil
}
func (f *Firebird) Unlock() error {
f.isLocked = false
return nil
}
func (f *Firebird) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}
// run migration
query := string(migr[:])
if _, err := f.conn.ExecContext(context.Background(), query); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}
return nil
}
func (f *Firebird) SetVersion(version int, dirty bool) error {
if version < 0 {
return nil
}
query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
DELETE FROM "%v";
INSERT INTO "%v" (version, dirty) VALUES (%v, %v);
END;`,
f.config.MigrationsTable, f.config.MigrationsTable, version, btoi(dirty))
if _, err := f.conn.ExecContext(context.Background(), query, version, btoi(dirty)); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
func (f *Firebird) Version() (version int, dirty bool, err error) {
var d int
query := fmt.Sprintf(`SELECT FIRST 1 version, dirty FROM "%v"`, f.config.MigrationsTable)
err = f.conn.QueryRowContext(context.Background(), query).Scan(&version, &d)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil
case err != nil:
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
default:
return version, itob(d), nil
}
}
func (f *Firebird) Drop() error {
// select all tables
query := `SELECT rdb$relation_name FROM rdb$relations WHERE rdb$view_blr IS NULL AND (rdb$system_flag IS NULL OR rdb$system_flag = 0);`
tables, err := f.conn.QueryContext(context.Background(), query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()
// delete one table after another
tableNames := make([]string, 0)
for tables.Next() {
var tableName string
if err := tables.Scan(&tableName); err != nil {
return err
}
if len(tableName) > 0 {
tableNames = append(tableNames, tableName)
}
}
// delete one by one ...
for _, t := range tableNames {
query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
if (not exists(select 1 from rdb$relations where rdb$relation_name = '%v')) then
execute statement 'drop table "%v"';
END;`,
t, t)
if _, err := f.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
return nil
}
// ensureVersionTable checks if versions table exists and, if not, creates it.
func (f *Firebird) ensureVersionTable() (err error) {
if err = f.Lock(); err != nil {
return err
}
defer func() {
if e := f.Unlock(); e != nil {
if err == nil {
err = e
} else {
err = multierror.Append(err, e)
}
}
}()
query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
if (not exists(select 1 from rdb$relations where rdb$relation_name = '%v')) then
execute statement 'create table "%v" (version bigint not null primary key, dirty smallint not null)';
END;`,
f.config.MigrationsTable, f.config.MigrationsTable)
if _, err = f.conn.ExecContext(context.Background(), query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
// btoi converts bool to int
func btoi(v bool) int {
if v {
return 1
}
return 0
}
// itob converts int to bool
func itob(v int) bool {
return v != 0
}

View File

@ -0,0 +1,198 @@
package firebird
import (
"context"
"database/sql"
sqldriver "database/sql/driver"
"fmt"
"github.com/golang-migrate/migrate/v4"
"io"
"strings"
"testing"
"github.com/dhui/dktest"
dt "github.com/golang-migrate/migrate/v4/database/testing"
"github.com/golang-migrate/migrate/v4/dktesting"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/nakagami/firebirdsql"
)
const (
user = "test_user"
password = "123456"
dbName = "test.fdb"
)
var (
opts = dktest.Options{
PortRequired: true,
ReadyFunc: isReady,
Env: map[string]string{
"FIREBIRD_DATABASE": dbName,
"FIREBIRD_USER": user,
"FIREBIRD_PASSWORD": password,
},
}
specs = []dktesting.ContainerSpec{
{ImageName: "jacobalberty/firebird:2.5-ss", Options: opts},
{ImageName: "jacobalberty/firebird:3.0", Options: opts},
}
)
func fbConnectionString(host, port string) string {
//firebird://user:password@servername[:port_number]/database_name_or_file[?params1=value1[&param2=value2]...]
return fmt.Sprintf("firebird://%s:%s@%s:%s//firebird/data/%s", user, password, host, port, dbName)
}
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
ip, port, err := c.FirstPort()
if err != nil {
return false
}
db, err := sql.Open("firebirdsql", fbConnectionString(ip, port))
if err != nil {
return false
}
defer db.Close()
if err = db.PingContext(ctx); err != nil {
switch err {
case sqldriver.ErrBadConn, io.EOF:
return false
default:
fmt.Println(err)
}
return false
}
return true
}
func Test(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.FirstPort()
if err != nil {
t.Fatal(err)
}
addr := fbConnectionString(ip, port)
p := &Firebird{}
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
dt.Test(t, d, []byte("SELECT Count(*) FROM rdb$relations"))
})
}
func TestMigrate(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.FirstPort()
if err != nil {
t.Fatal(err)
}
addr := fbConnectionString(ip, port)
p := &Firebird{}
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "firebirdsql", d)
if err != nil {
t.Fatalf("%v", err)
}
dt.TestMigrate(t, m, []byte("SELECT Count(*) FROM rdb$relations"))
})
}
func TestErrorParsing(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.FirstPort()
if err != nil {
t.Fatal(err)
}
addr := fbConnectionString(ip, port)
p := &Firebird{}
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
wantErr := `migration failed in line 0: CREATE TABLEE foo (foo varchar(40)); (details: Dynamic SQL Error
SQL error code = -104
Token unknown - line 1, column 8
TABLEE
)`
if err := d.Run(strings.NewReader("CREATE TABLEE foo (foo varchar(40));")); err == nil {
t.Fatal("expected err but got nil")
} else if err.Error() != wantErr {
msg := err.Error()
t.Fatalf("expected '%s' but got '%s'", wantErr, msg)
}
})
}
func TestFilterCustomQuery(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.FirstPort()
if err != nil {
t.Fatal(err)
}
addr := fbConnectionString(ip, port) + "?sslmode=disable&x-custom=foobar"
p := &Firebird{}
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
defer d.Close()
})
}
func Test_Lock(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.FirstPort()
if err != nil {
t.Fatal(err)
}
addr := fbConnectionString(ip, port)
p := &Firebird{}
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT Count(*) FROM rdb$relations"))
ps := d.(*Firebird)
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
err = ps.Lock()
if err != nil {
t.Fatal(err)
}
err = ps.Unlock()
if err != nil {
t.Fatal(err)
}
})
}

View File

@ -45,7 +45,7 @@ type Postgres struct {
db *sql.DB
isLocked bool
// Open and WithInstance need to garantuee that config is never nil
// Open and WithInstance need to guarantee that config is never nil
config *Config
}

View File

@ -40,7 +40,7 @@ type Redshift struct {
conn *sql.Conn
db *sql.DB
// Open and WithInstance need to garantuee that config is never nil
// Open and WithInstance need to guarantee that config is never nil
config *Config
}

3
go.mod
View File

@ -21,11 +21,13 @@ require (
github.com/hashicorp/go-multierror v1.0.0
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/jackc/pgx v3.2.0+incompatible // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kshvakov/clickhouse v1.3.5
github.com/lib/pq v1.0.0
github.com/mattn/go-sqlite3 v1.10.0
github.com/mongodb/mongo-go-driver v0.3.0
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8
github.com/pkg/errors v0.8.1 // indirect
github.com/satori/go.uuid v1.2.0 // indirect
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
@ -35,6 +37,7 @@ require (
github.com/xanzy/go-gitlab v0.15.0
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
github.com/xdg/stringprep v1.0.0 // indirect
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
go.opencensus.io v0.19.0 // indirect
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 // indirect
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95

6
go.sum
View File

@ -131,6 +131,8 @@ github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGk
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -152,6 +154,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/mongodb/mongo-go-driver v0.3.0 h1:00tKWMrabkVU1e57/TTP4ZBIfhn/wmjlSiRnIM9d0T8=
github.com/mongodb/mongo-go-driver v0.3.0/go.mod h1:NK/HWDIIZkaYsnYa0hmtP443T5ELr0KDecmIioVuuyU=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
@ -220,6 +224,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.19.0 h1:+jrnNy8MR4GZXvwF9PEuSyHxA4NaTf6601oNRwCSXq0=
go.opencensus.io v0.19.0/go.mod h1:AYeH0+ZxYyghG8diqaaIq/9P3VgCCt5GF2ldCY4dkFg=

View File

@ -0,0 +1,7 @@
// +build firebird
package cli
import (
_ "github.com/golang-migrate/migrate/v4/database/firebird"
)