diff --git a/README.md b/README.md index 5b4245a..2920d90 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/database/firebird/README.md b/database/firebird/README.md new file mode 100644 index 0000000..bdfef8a --- /dev/null +++ b/database/firebird/README.md @@ -0,0 +1,12 @@ +# firebird + +`firebirdsql://user:password@servername[:port_number]/database_name_or_file[?params1=value1[¶m2=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) | diff --git a/database/firebird/examples/migrations/1085649617_create_users_table.down.sql b/database/firebird/examples/migrations/1085649617_create_users_table.down.sql new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/database/firebird/examples/migrations/1085649617_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/database/firebird/examples/migrations/1085649617_create_users_table.up.sql b/database/firebird/examples/migrations/1085649617_create_users_table.up.sql new file mode 100644 index 0000000..92897dc --- /dev/null +++ b/database/firebird/examples/migrations/1085649617_create_users_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + user_id integer unique, + name varchar(40), + email varchar(40) +); diff --git a/database/firebird/examples/migrations/1185749658_add_city_to_users.down.sql b/database/firebird/examples/migrations/1185749658_add_city_to_users.down.sql new file mode 100644 index 0000000..83aa5c8 --- /dev/null +++ b/database/firebird/examples/migrations/1185749658_add_city_to_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP city; diff --git a/database/firebird/examples/migrations/1185749658_add_city_to_users.up.sql b/database/firebird/examples/migrations/1185749658_add_city_to_users.up.sql new file mode 100644 index 0000000..2add820 --- /dev/null +++ b/database/firebird/examples/migrations/1185749658_add_city_to_users.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD city varchar(100); + + diff --git a/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.down.sql b/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.down.sql new file mode 100644 index 0000000..867ffb4 --- /dev/null +++ b/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.down.sql @@ -0,0 +1 @@ +DROP INDEX users_email_index; diff --git a/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.up.sql b/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.up.sql new file mode 100644 index 0000000..03a0463 --- /dev/null +++ b/database/firebird/examples/migrations/1285849751_add_index_on_user_emails.up.sql @@ -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. diff --git a/database/firebird/examples/migrations/1385949617_create_books_table.down.sql b/database/firebird/examples/migrations/1385949617_create_books_table.down.sql new file mode 100644 index 0000000..3bd92c6 --- /dev/null +++ b/database/firebird/examples/migrations/1385949617_create_books_table.down.sql @@ -0,0 +1 @@ +DROP TABLE books; diff --git a/database/firebird/examples/migrations/1385949617_create_books_table.up.sql b/database/firebird/examples/migrations/1385949617_create_books_table.up.sql new file mode 100644 index 0000000..f1503b5 --- /dev/null +++ b/database/firebird/examples/migrations/1385949617_create_books_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE books ( + user_id integer, + name varchar(40), + author varchar(40) +); diff --git a/database/firebird/examples/migrations/1485949617_create_movies_table.down.sql b/database/firebird/examples/migrations/1485949617_create_movies_table.down.sql new file mode 100644 index 0000000..6f5118f --- /dev/null +++ b/database/firebird/examples/migrations/1485949617_create_movies_table.down.sql @@ -0,0 +1 @@ +DROP TABLE movies; diff --git a/database/firebird/examples/migrations/1485949617_create_movies_table.up.sql b/database/firebird/examples/migrations/1485949617_create_movies_table.up.sql new file mode 100644 index 0000000..f0ef594 --- /dev/null +++ b/database/firebird/examples/migrations/1485949617_create_movies_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE movies ( + user_id integer, + name varchar(40), + director varchar(40) +); diff --git a/database/firebird/firebird.go b/database/firebird/firebird.go new file mode 100644 index 0000000..54ac9d0 --- /dev/null +++ b/database/firebird/firebird.go @@ -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 +} diff --git a/database/firebird/firebird_test.go b/database/firebird/firebird_test.go new file mode 100644 index 0000000..91ef8af --- /dev/null +++ b/database/firebird/firebird_test.go @@ -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[¶m2=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) + } + }) +} diff --git a/database/postgres/postgres.go b/database/postgres/postgres.go index a9570bc..96fc690 100644 --- a/database/postgres/postgres.go +++ b/database/postgres/postgres.go @@ -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 } diff --git a/database/redshift/redshift.go b/database/redshift/redshift.go index ee4dcf6..b772bba 100644 --- a/database/redshift/redshift.go +++ b/database/redshift/redshift.go @@ -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 } diff --git a/go.mod b/go.mod index 8861e2c..0bc8742 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ea1b146..77230e1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/cli/build_firebird.go b/internal/cli/build_firebird.go new file mode 100644 index 0000000..fe396ba --- /dev/null +++ b/internal/cli/build_firebird.go @@ -0,0 +1,7 @@ +// +build firebird + +package cli + +import ( + _ "github.com/golang-migrate/migrate/v4/database/firebird" +)