From 8381ea0307c32897c04204a5d3ef53679781014c Mon Sep 17 00:00:00 2001 From: Andrei Mackenzie Date: Sat, 3 Nov 2018 18:04:22 -0400 Subject: [PATCH] Clone the 'postgres' driver as 'redshift2' Avoid stepping on the 'redshift' driver for the time being. Driver registration is also modified to identify the clone as 'redshift2' rather than 'postgres'. --- Makefile | 2 +- cli/build_redshift2.go | 7 + database/redshift2/README.md | 28 ++ .../1085649617_create_users_table.down.sql | 1 + .../1085649617_create_users_table.up.sql | 5 + .../1185749658_add_city_to_users.down.sql | 1 + .../1185749658_add_city_to_users.up.sql | 3 + ...85849751_add_index_on_user_emails.down.sql | 1 + ...1285849751_add_index_on_user_emails.up.sql | 3 + .../1385949617_create_books_table.down.sql | 1 + .../1385949617_create_books_table.up.sql | 5 + .../1485949617_create_movies_table.down.sql | 1 + .../1485949617_create_movies_table.up.sql | 5 + .../1585849751_just_a_comment.up.sql | 1 + .../1685849751_another_comment.up.sql | 1 + .../1785849751_another_comment.up.sql | 1 + .../1885849751_another_comment.up.sql | 1 + database/redshift2/redshift.go | 338 ++++++++++++++++++ database/redshift2/redshift_test.go | 300 ++++++++++++++++ 19 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 cli/build_redshift2.go create mode 100644 database/redshift2/README.md create mode 100644 database/redshift2/examples/migrations/1085649617_create_users_table.down.sql create mode 100644 database/redshift2/examples/migrations/1085649617_create_users_table.up.sql create mode 100644 database/redshift2/examples/migrations/1185749658_add_city_to_users.down.sql create mode 100644 database/redshift2/examples/migrations/1185749658_add_city_to_users.up.sql create mode 100644 database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.down.sql create mode 100644 database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.up.sql create mode 100644 database/redshift2/examples/migrations/1385949617_create_books_table.down.sql create mode 100644 database/redshift2/examples/migrations/1385949617_create_books_table.up.sql create mode 100644 database/redshift2/examples/migrations/1485949617_create_movies_table.down.sql create mode 100644 database/redshift2/examples/migrations/1485949617_create_movies_table.up.sql create mode 100644 database/redshift2/examples/migrations/1585849751_just_a_comment.up.sql create mode 100644 database/redshift2/examples/migrations/1685849751_another_comment.up.sql create mode 100644 database/redshift2/examples/migrations/1785849751_another_comment.up.sql create mode 100644 database/redshift2/examples/migrations/1885849751_another_comment.up.sql create mode 100644 database/redshift2/redshift.go create mode 100644 database/redshift2/redshift_test.go diff --git a/Makefile b/Makefile index e54d6b2..636efc5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs -DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse +DATABASE ?= postgres mysql redshift redshift2 cassandra spanner cockroachdb clickhouse VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-) TEST_FLAGS ?= REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)") diff --git a/cli/build_redshift2.go b/cli/build_redshift2.go new file mode 100644 index 0000000..5839afb --- /dev/null +++ b/cli/build_redshift2.go @@ -0,0 +1,7 @@ +// +build redshift2 + +package main + +import ( + _ "github.com/golang-migrate/migrate/v4/database/redshift2" +) diff --git a/database/redshift2/README.md b/database/redshift2/README.md new file mode 100644 index 0000000..f631239 --- /dev/null +++ b/database/redshift2/README.md @@ -0,0 +1,28 @@ +# postgres + +`postgres://user:password@host:port/dbname?query` (`postgresql://` works, too) + +| URL Query | WithInstance Config | Description | +|------------|---------------------|-------------| +| `x-migrations-table` | `MigrationsTable` | Name of the migrations table | +| `dbname` | `DatabaseName` | The name of the database to connect to | +| `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | +| `user` | | The user to sign in as | +| `password` | | The user's password | +| `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) | +| `port` | | The port to bind to. (default is 5432) | +| `fallback_application_name` | | An application_name to fall back to if one isn't provided. | +| `connect_timeout` | | Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely. | +| `sslcert` | | Cert file location. The file must contain PEM encoded data. | +| `sslkey` | | Key file location. The file must contain PEM encoded data. | +| `sslrootcert` | | The location of the root certificate file. The file must contain PEM encoded data. | +| `sslmode` | | Whether or not to use SSL (disable\|require\|verify-ca\|verify-full) | + + +## Upgrading from v1 + +1. Write down the current migration version from schema_migrations +1. `DROP TABLE schema_migrations` +2. Wrap your existing migrations in transactions ([BEGIN/COMMIT](https://www.postgresql.org/docs/current/static/transaction-iso.html)) if you use multiple statements within one migration. +3. Download and install the latest migrate version. +4. Force the current migration version with `migrate force `. diff --git a/database/redshift2/examples/migrations/1085649617_create_users_table.down.sql b/database/redshift2/examples/migrations/1085649617_create_users_table.down.sql new file mode 100644 index 0000000..c99ddcd --- /dev/null +++ b/database/redshift2/examples/migrations/1085649617_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; diff --git a/database/redshift2/examples/migrations/1085649617_create_users_table.up.sql b/database/redshift2/examples/migrations/1085649617_create_users_table.up.sql new file mode 100644 index 0000000..92897dc --- /dev/null +++ b/database/redshift2/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/redshift2/examples/migrations/1185749658_add_city_to_users.down.sql b/database/redshift2/examples/migrations/1185749658_add_city_to_users.down.sql new file mode 100644 index 0000000..940c607 --- /dev/null +++ b/database/redshift2/examples/migrations/1185749658_add_city_to_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN IF EXISTS city; diff --git a/database/redshift2/examples/migrations/1185749658_add_city_to_users.up.sql b/database/redshift2/examples/migrations/1185749658_add_city_to_users.up.sql new file mode 100644 index 0000000..67823ed --- /dev/null +++ b/database/redshift2/examples/migrations/1185749658_add_city_to_users.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN city varchar(100); + + diff --git a/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.down.sql b/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.down.sql new file mode 100644 index 0000000..3e87dd2 --- /dev/null +++ b/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS users_email_index; diff --git a/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.up.sql b/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.up.sql new file mode 100644 index 0000000..fbeb4ab --- /dev/null +++ b/database/redshift2/examples/migrations/1285849751_add_index_on_user_emails.up.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX CONCURRENTLY 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/redshift2/examples/migrations/1385949617_create_books_table.down.sql b/database/redshift2/examples/migrations/1385949617_create_books_table.down.sql new file mode 100644 index 0000000..1a0b1a2 --- /dev/null +++ b/database/redshift2/examples/migrations/1385949617_create_books_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS books; diff --git a/database/redshift2/examples/migrations/1385949617_create_books_table.up.sql b/database/redshift2/examples/migrations/1385949617_create_books_table.up.sql new file mode 100644 index 0000000..f1503b5 --- /dev/null +++ b/database/redshift2/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/redshift2/examples/migrations/1485949617_create_movies_table.down.sql b/database/redshift2/examples/migrations/1485949617_create_movies_table.down.sql new file mode 100644 index 0000000..3a51876 --- /dev/null +++ b/database/redshift2/examples/migrations/1485949617_create_movies_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS movies; diff --git a/database/redshift2/examples/migrations/1485949617_create_movies_table.up.sql b/database/redshift2/examples/migrations/1485949617_create_movies_table.up.sql new file mode 100644 index 0000000..f0ef594 --- /dev/null +++ b/database/redshift2/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/redshift2/examples/migrations/1585849751_just_a_comment.up.sql b/database/redshift2/examples/migrations/1585849751_just_a_comment.up.sql new file mode 100644 index 0000000..9b6b57a --- /dev/null +++ b/database/redshift2/examples/migrations/1585849751_just_a_comment.up.sql @@ -0,0 +1 @@ +-- 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/redshift2/examples/migrations/1685849751_another_comment.up.sql b/database/redshift2/examples/migrations/1685849751_another_comment.up.sql new file mode 100644 index 0000000..9b6b57a --- /dev/null +++ b/database/redshift2/examples/migrations/1685849751_another_comment.up.sql @@ -0,0 +1 @@ +-- 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/redshift2/examples/migrations/1785849751_another_comment.up.sql b/database/redshift2/examples/migrations/1785849751_another_comment.up.sql new file mode 100644 index 0000000..9b6b57a --- /dev/null +++ b/database/redshift2/examples/migrations/1785849751_another_comment.up.sql @@ -0,0 +1 @@ +-- 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/redshift2/examples/migrations/1885849751_another_comment.up.sql b/database/redshift2/examples/migrations/1885849751_another_comment.up.sql new file mode 100644 index 0000000..9b6b57a --- /dev/null +++ b/database/redshift2/examples/migrations/1885849751_another_comment.up.sql @@ -0,0 +1 @@ +-- 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/redshift2/redshift.go b/database/redshift2/redshift.go new file mode 100644 index 0000000..8a0b328 --- /dev/null +++ b/database/redshift2/redshift.go @@ -0,0 +1,338 @@ +// +build go1.9 + +package redshift2 + +import ( + "context" + "database/sql" + "fmt" + "io" + "io/ioutil" + nurl "net/url" + "strconv" + "strings" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database" + "github.com/lib/pq" +) + +func init() { + db := Postgres{} + database.Register("redshift2", &db) +} + +var DefaultMigrationsTable = "schema_migrations" + +var ( + ErrNilConfig = fmt.Errorf("no config") + ErrNoDatabaseName = fmt.Errorf("no database name") + ErrNoSchema = fmt.Errorf("no schema") + ErrDatabaseDirty = fmt.Errorf("database is dirty") +) + +type Config struct { + MigrationsTable string + DatabaseName string +} + +type Postgres struct { + // Locking and unlocking need to use the same connection + conn *sql.Conn + db *sql.DB + isLocked bool + + // Open and WithInstance need to garantuee 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 + } + + query := `SELECT CURRENT_DATABASE()` + var databaseName string + if err := instance.QueryRow(query).Scan(&databaseName); err != nil { + return nil, &database.Error{OrigErr: err, Query: []byte(query)} + } + + if len(databaseName) == 0 { + return nil, ErrNoDatabaseName + } + + config.DatabaseName = databaseName + + if len(config.MigrationsTable) == 0 { + config.MigrationsTable = DefaultMigrationsTable + } + + conn, err := instance.Conn(context.Background()) + + if err != nil { + return nil, err + } + + px := &Postgres{ + conn: conn, + db: instance, + config: config, + } + + if err := px.ensureVersionTable(); err != nil { + return nil, err + } + + return px, nil +} + +func (p *Postgres) Open(url string) (database.Driver, error) { + purl, err := nurl.Parse(url) + if err != nil { + return nil, err + } + + db, err := sql.Open("postgres", migrate.FilterCustomQuery(purl).String()) + if err != nil { + return nil, err + } + + migrationsTable := purl.Query().Get("x-migrations-table") + if len(migrationsTable) == 0 { + migrationsTable = DefaultMigrationsTable + } + + px, err := WithInstance(db, &Config{ + DatabaseName: purl.Path, + MigrationsTable: migrationsTable, + }) + if err != nil { + return nil, err + } + + return px, nil +} + +func (p *Postgres) Close() error { + connErr := p.conn.Close() + dbErr := p.db.Close() + if connErr != nil || dbErr != nil { + return fmt.Errorf("conn: %v, db: %v", connErr, dbErr) + } + return nil +} + +// https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS +func (p *Postgres) Lock() error { + if p.isLocked { + return database.ErrLocked + } + + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName) + if err != nil { + return err + } + + // This will either obtain the lock immediately and return true, + // or return false if the lock cannot be acquired immediately. + query := `SELECT pg_advisory_lock($1)` + if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil { + return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)} + } + + p.isLocked = true + return nil +} + +func (p *Postgres) Unlock() error { + if !p.isLocked { + return nil + } + + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName) + if err != nil { + return err + } + + query := `SELECT pg_advisory_unlock($1)` + if _, err := p.conn.ExecContext(context.Background(), query, aid); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + p.isLocked = false + return nil +} + +func (p *Postgres) Run(migration io.Reader) error { + migr, err := ioutil.ReadAll(migration) + if err != nil { + return err + } + + // run migration + query := string(migr[:]) + if _, err := p.conn.ExecContext(context.Background(), query); err != nil { + if pgErr, ok := err.(*pq.Error); ok { + var line uint + var col uint + var lineColOK bool + if pgErr.Position != "" { + if pos, err := strconv.ParseUint(pgErr.Position, 10, 64); err == nil { + line, col, lineColOK = computeLineFromPos(query, int(pos)) + } + } + message := fmt.Sprintf("migration failed: %s", pgErr.Message) + if lineColOK { + message = fmt.Sprintf("%s (column %d)", message, col) + } + if pgErr.Detail != "" { + message = fmt.Sprintf("%s, %s", message, pgErr.Detail) + } + return database.Error{OrigErr: err, Err: message, Query: migr, Line: line} + } + return database.Error{OrigErr: err, Err: "migration failed", Query: migr} + } + + return nil +} + +func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) { + // replace crlf with lf + s = strings.Replace(s, "\r\n", "\n", -1) + // pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes + runes := []rune(s) + if pos > len(runes) { + return 0, 0, false + } + sel := runes[:pos] + line = uint(runesCount(sel, newLine) + 1) + col = uint(pos - 1 - runesLastIndex(sel, newLine)) + return line, col, true +} + +const newLine = '\n' + +func runesCount(input []rune, target rune) int { + var count int + for _, r := range input { + if r == target { + count++ + } + } + return count +} + +func runesLastIndex(input []rune, target rune) int { + for i := len(input) - 1; i >= 0; i-- { + if input[i] == target { + return i + } + } + return -1 +} + +func (p *Postgres) SetVersion(version int, dirty bool) error { + tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return &database.Error{OrigErr: err, Err: "transaction start failed"} + } + + query := `TRUNCATE "` + p.config.MigrationsTable + `"` + if _, err := tx.Exec(query); err != nil { + tx.Rollback() + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + if version >= 0 { + query = `INSERT INTO "` + p.config.MigrationsTable + `" (version, dirty) VALUES ($1, $2)` + if _, err := tx.Exec(query, version, dirty); err != nil { + tx.Rollback() + return &database.Error{OrigErr: err, Query: []byte(query)} + } + } + + if err := tx.Commit(); err != nil { + return &database.Error{OrigErr: err, Err: "transaction commit failed"} + } + + return nil +} + +func (p *Postgres) Version() (version int, dirty bool, err error) { + query := `SELECT version, dirty FROM "` + p.config.MigrationsTable + `" LIMIT 1` + err = p.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty) + switch { + case err == sql.ErrNoRows: + return database.NilVersion, false, nil + + case err != nil: + if e, ok := err.(*pq.Error); ok { + if e.Code.Name() == "undefined_table" { + return database.NilVersion, false, nil + } + } + return 0, false, &database.Error{OrigErr: err, Query: []byte(query)} + + default: + return version, dirty, nil + } +} + +func (p *Postgres) Drop() error { + // select all tables in current schema + query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'` + tables, err := p.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) + } + } + + if len(tableNames) > 0 { + // delete one by one ... + for _, t := range tableNames { + query = `DROP TABLE IF EXISTS ` + t + ` CASCADE` + if _, err := p.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + } + if err := p.ensureVersionTable(); err != nil { + return err + } + } + + return nil +} + +func (p *Postgres) ensureVersionTable() error { + // check if migration table exists + var count int + query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1` + if err := p.conn.QueryRowContext(context.Background(), query, p.config.MigrationsTable).Scan(&count); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + if count == 1 { + return nil + } + + // if not, create the empty migration table + query = `CREATE TABLE "` + p.config.MigrationsTable + `" (version bigint not null primary key, dirty boolean not null)` + if _, err := p.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + return nil +} diff --git a/database/redshift2/redshift_test.go b/database/redshift2/redshift_test.go new file mode 100644 index 0000000..2d5afa1 --- /dev/null +++ b/database/redshift2/redshift_test.go @@ -0,0 +1,300 @@ +package redshift2 + +// error codes https://github.com/lib/pq/blob/master/error.go + +import ( + "bytes" + "context" + "database/sql" + sqldriver "database/sql/driver" + "fmt" + "io" + "strconv" + "strings" + "testing" + + dt "github.com/golang-migrate/migrate/v4/database/testing" + mt "github.com/golang-migrate/migrate/v4/testing" +) + +var versions = []mt.Version{ + {Image: "postgres:10"}, + {Image: "postgres:9.6"}, + {Image: "postgres:9.5"}, + {Image: "postgres:9.4"}, + {Image: "postgres:9.3"}, +} + +func pgConnectionString(host string, port uint) string { + return fmt.Sprintf("postgres://postgres@%s:%v/postgres?sslmode=disable", host, port) +} + +func isReady(i mt.Instance) bool { + db, err := sql.Open("postgres", pgConnectionString(i.Host(), i.Port())) + if err != nil { + return false + } + defer db.Close() + if err = db.Ping(); err != nil { + switch err { + case sqldriver.ErrBadConn, io.EOF: + return false + default: + fmt.Println(err) + } + return false + } + + return true +} + +func Test(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := pgConnectionString(i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + defer d.Close() + dt.Test(t, d, []byte("SELECT 1")) + }) +} + +func TestMultiStatement(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := pgConnectionString(i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + defer d.Close() + if err := d.Run(bytes.NewReader([]byte("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);"))); err != nil { + t.Fatalf("expected err to be nil, got %v", err) + } + + // make sure second table exists + var exists bool + if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil { + t.Fatal(err) + } + if !exists { + t.Fatalf("expected table bar to exist") + } + }) +} + +func TestErrorParsing(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := pgConnectionString(i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + defer d.Close() + + wantErr := `migration failed: syntax error at or near "TABLEE" (column 37) in line 1: CREATE TABLE foo ` + + `(foo text); CREATE TABLEE bar (bar text); (details: pq: syntax error at or near "TABLEE")` + if err := d.Run(bytes.NewReader([]byte("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);"))); err == nil { + t.Fatal("expected err but got nil") + } else if err.Error() != wantErr { + t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) + } + }) +} + +func TestFilterCustomQuery(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&x-custom=foobar", i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + defer d.Close() + }) +} + +func TestWithSchema(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := pgConnectionString(i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + defer d.Close() + + // create foobar schema + if err := d.Run(bytes.NewReader([]byte("CREATE SCHEMA foobar AUTHORIZATION postgres"))); err != nil { + t.Fatal(err) + } + if err := d.SetVersion(1, false); err != nil { + t.Fatal(err) + } + + // re-connect using that schema + d2, err := p.Open(fmt.Sprintf("postgres://postgres@%v:%v/postgres?sslmode=disable&search_path=foobar", i.Host(), i.Port())) + if err != nil { + t.Fatalf("%v", err) + } + defer d2.Close() + + version, _, err := d2.Version() + if err != nil { + t.Fatal(err) + } + if version != -1 { + t.Fatal("expected NilVersion") + } + + // now update version and compare + if err := d2.SetVersion(2, false); err != nil { + t.Fatal(err) + } + version, _, err = d2.Version() + if err != nil { + t.Fatal(err) + } + if version != 2 { + t.Fatal("expected version 2") + } + + // meanwhile, the public schema still has the other version + version, _, err = d.Version() + if err != nil { + t.Fatal(err) + } + if version != 1 { + t.Fatal("expected version 2") + } + }) +} + +func TestWithInstance(t *testing.T) { + +} + +func TestPostgres_Lock(t *testing.T) { + mt.ParallelTest(t, versions, isReady, + func(t *testing.T, i mt.Instance) { + p := &Postgres{} + addr := pgConnectionString(i.Host(), i.Port()) + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + + dt.Test(t, d, []byte("SELECT 1")) + + ps := d.(*Postgres) + + 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) + } + }) +} + +func Test_computeLineFromPos(t *testing.T) { + testcases := []struct { + pos int + wantLine uint + wantCol uint + input string + wantOk bool + }{ + { + 15, 2, 6, "SELECT *\nFROM foo", true, // foo table does not exists + }, + { + 16, 3, 6, "SELECT *\n\nFROM foo", true, // foo table does not exists, empty line + }, + { + 25, 3, 7, "SELECT *\nFROM foo\nWHERE x", true, // x column error + }, + { + 27, 5, 7, "SELECT *\n\nFROM foo\n\nWHERE x", true, // x column error, empty lines + }, + { + 10, 2, 1, "SELECT *\nFROMM foo", true, // FROMM typo + }, + { + 11, 3, 1, "SELECT *\n\nFROMM foo", true, // FROMM typo, empty line + }, + { + 17, 2, 8, "SELECT *\nFROM foo", true, // last character + }, + { + 18, 0, 0, "SELECT *\nFROM foo", false, // invalid position + }, + } + for i, tc := range testcases { + t.Run("tc"+strconv.Itoa(i), func(t *testing.T) { + run := func(crlf bool, nonASCII bool) { + var name string + if crlf { + name = "crlf" + } else { + name = "lf" + } + if nonASCII { + name += "-nonascii" + } else { + name += "-ascii" + } + t.Run(name, func(t *testing.T) { + input := tc.input + if crlf { + input = strings.Replace(input, "\n", "\r\n", -1) + } + if nonASCII { + input = strings.Replace(input, "FROM", "FRÖM", -1) + } + gotLine, gotCol, gotOK := computeLineFromPos(input, tc.pos) + + if tc.wantOk { + t.Logf("pos %d, want %d:%d, %#v", tc.pos, tc.wantLine, tc.wantCol, input) + } + + if gotOK != tc.wantOk { + t.Fatalf("expected ok %v but got %v", tc.wantOk, gotOK) + } + if gotLine != tc.wantLine { + t.Fatalf("expected line %d but got %d", tc.wantLine, gotLine) + } + if gotCol != tc.wantCol { + t.Fatalf("expected col %d but got %d", tc.wantCol, gotCol) + } + }) + } + run(false, false) + run(true, false) + run(false, true) + run(true, true) + }) + } + +}