mirror of
https://github.com/hanzoai/migrate.git
synced 2026-09-30 22:55:10 +00:00
Merge branch 'master' into clickhouse_create_database_migrations
This commit is contained in:
+19
-13
@@ -9,10 +9,10 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
go-version: "1.21.x"
|
||||
- uses: actions/checkout@v4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
@@ -22,11 +22,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.18.x", "1.19.x"]
|
||||
go: ["1.20.x", "1.21.x"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
@@ -60,19 +60,19 @@ jobs:
|
||||
# 3. When the workflow is triggered by a tag with `v` prefix
|
||||
if: ${{ success() && github.repository == 'golang-migrate/migrate' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.19.x"
|
||||
go-version: "1.21.x"
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: golangmigrate
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
- run: echo "SOURCE=$(make echo-source)" >> $GITHUB_ENV
|
||||
- run: echo "DATABASE=$(make echo-database)" >> $GITHUB_ENV
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
@@ -94,9 +94,15 @@ jobs:
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/focal dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/ubuntu/jammy dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/buster dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/bullseye dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
- run: package_cloud push golang-migrate/migrate/debian/bookworm dist/migrate.linux-amd64.deb
|
||||
env:
|
||||
PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }}
|
||||
|
||||
@@ -87,6 +87,7 @@ release:
|
||||
prerelease: auto
|
||||
source:
|
||||
enabled: true
|
||||
rlcp: true
|
||||
format: zip
|
||||
changelog:
|
||||
skip: false
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19-alpine3.16 AS builder
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache git gcc musl-dev make
|
||||
@@ -15,7 +15,7 @@ COPY . ./
|
||||
|
||||
RUN make build-docker
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
and whenever we want, not just once at the beginning of all tests.
|
||||
|
||||
#### Can I maintain my driver in my own repository?
|
||||
Yes, technically thats possible. We want to encourage you to contribute your driver to this respository though.
|
||||
Yes, technically thats possible. We want to encourage you to contribute your driver to this repository though.
|
||||
The driver's functionality is dictated by migrate's interfaces. That means there should really
|
||||
just be one driver for a database/ source. We want to prevent a future where several drivers doing the exact same thing,
|
||||
just implemented a bit differently, co-exist somewhere on GitHub. If users have to do research first to find the
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ migrate -database YOUR_DATABASE_URL -path PATH_TO_YOUR_MIGRATIONS up
|
||||
|
||||
Just add the code to your app and you're ready to go!
|
||||
|
||||
Before commiting your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
Before committing your migrations you should run your migrations up, down, and then up again to see if migrations are working properly both ways.
|
||||
(e.g. if you created a table in a migration but reverse migration did not delete it, you will encounter an error when running the forward migration again)
|
||||
It's also worth checking your migrations in a separate, containerized environment. You can find some tools at the [end of this document](#further-reading).
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SOURCE ?= file go_bindata github github_ee bitbucket aws_s3 google_cloud_storage godoc_vfs gitlab
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx
|
||||
DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb yugabytedb clickhouse mongodb sqlserver firebird neo4j pgx pgx5 rqlite
|
||||
DATABASE_TEST ?= $(DATABASE) sqlite sqlite3 sqlcipher
|
||||
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
|
||||
TEST_FLAGS ?=
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[](https://github.com/golang-migrate/migrate/actions/workflows/ci.yaml?query=branch%3Amaster)
|
||||
[](https://github.com/golang-migrate/migrate/actions/workflows/ci.yaml?query=branch%3Amaster)
|
||||
[](https://pkg.go.dev/github.com/golang-migrate/migrate/v4)
|
||||
[](https://coveralls.io/github/golang-migrate/migrate?branch=master)
|
||||
[](https://packagecloud.io/golang-migrate/migrate?filter=debs)
|
||||
[](https://hub.docker.com/r/migrate/migrate/)
|
||||

|
||||

|
||||
[](https://github.com/golang-migrate/migrate/releases)
|
||||
[](https://goreportcard.com/report/github.com/golang-migrate/migrate/v4)
|
||||
|
||||
@@ -24,14 +24,15 @@ Forked from [mattes/migrate](https://github.com/mattes/migrate)
|
||||
Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||
|
||||
* [PostgreSQL](database/postgres)
|
||||
* [PGX](database/pgx)
|
||||
* [PGX v4](database/pgx)
|
||||
* [PGX v5](database/pgx/v5)
|
||||
* [Redshift](database/redshift)
|
||||
* [Ql](database/ql)
|
||||
* [Cassandra](database/cassandra)
|
||||
* [Cassandra / ScyllaDB](database/cassandra)
|
||||
* [SQLite](database/sqlite)
|
||||
* [SQLite3](database/sqlite3) ([todo #165](https://github.com/mattes/migrate/issues/165))
|
||||
* [SQLCipher](database/sqlcipher)
|
||||
* [MySQL/ MariaDB](database/mysql)
|
||||
* [MySQL / MariaDB](database/mysql)
|
||||
* [Neo4j](database/neo4j)
|
||||
* [MongoDB](database/mongodb)
|
||||
* [CrateDB](database/crate) ([todo #170](https://github.com/mattes/migrate/issues/170))
|
||||
@@ -42,6 +43,7 @@ Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||
* [ClickHouse](database/clickhouse)
|
||||
* [Firebird](database/firebird)
|
||||
* [MS SQL Server](database/sqlserver)
|
||||
* [RQLite](database/rqlite)
|
||||
|
||||
### Database URLs
|
||||
|
||||
@@ -169,6 +171,11 @@ Each migration has an up and down migration. [Why?](FAQ.md#why-two-separate-file
|
||||
|
||||
[Best practices: How to write migrations.](MIGRATIONS.md)
|
||||
|
||||
## Coming from another db migration tool?
|
||||
|
||||
Check out [migradaptor](https://github.com/musinit/migradaptor/).
|
||||
*Note: migradaptor is not affliated or supported by this project*
|
||||
|
||||
## Versions
|
||||
|
||||
Version | Supported? | Import | Notes
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[Release Downloads](https://github.com/golang-migrate/migrate/releases)
|
||||
|
||||
```bash
|
||||
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$platform-amd64.tar.gz | tar xvz
|
||||
$ curl -L https://github.com/golang-migrate/migrate/releases/download/$version/migrate.$os-$arch.tar.gz | tar xvz
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
# Cassandra
|
||||
# Cassandra / ScyllaDB
|
||||
|
||||
* Drop command will not work on Cassandra 2.X because it rely on
|
||||
* `Drop()` method will not work on Cassandra 2.X because it rely on
|
||||
system_schema table which comes with 3.X
|
||||
* Other commands should work properly but are **not tested**
|
||||
* Other methods should work properly but are **not tested**
|
||||
* The Cassandra driver (gocql) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
|
||||
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
|
||||
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.
|
||||
|
||||
**ScyllaDB**
|
||||
|
||||
* No additional configuration is required since it is a drop-in replacement for Cassandra.
|
||||
* The `Drop()` method` works for ScyllaDB 5.1
|
||||
|
||||
|
||||
## Usage
|
||||
`cassandra://host:port/keyspace?param1=value¶m2=value2`
|
||||
|
||||
@@ -25,9 +25,11 @@ var (
|
||||
// Although Cassandra 2.x is supported by the Apache Foundation,
|
||||
// the migrate db driver only supports Cassandra 3.x since it uses
|
||||
// the system_schema keyspace.
|
||||
// last ScyllaDB version tested is 5.1.11
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "cassandra:3.0", Options: opts},
|
||||
{ImageName: "cassandra:3.11", Options: opts},
|
||||
{ImageName: "scylladb/scylla:5.1.11", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ func (ch *ClickHouse) ensureVersionTable() (err error) {
|
||||
|
||||
var (
|
||||
table string
|
||||
query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
|
||||
query = "SHOW TABLES FROM " + quoteIdentifier(ch.config.DatabaseName) + " LIKE '" + ch.config.MigrationsTable + "'"
|
||||
)
|
||||
// check if migration table exists
|
||||
if err := ch.conn.QueryRow(query).Scan(&table); err != nil {
|
||||
@@ -259,7 +259,7 @@ func (ch *ClickHouse) ensureVersionTable() (err error) {
|
||||
}
|
||||
|
||||
func (ch *ClickHouse) Drop() (err error) {
|
||||
query := "SHOW TABLES FROM " + ch.config.DatabaseName
|
||||
query := "SHOW TABLES FROM " + quoteIdentifier(ch.config.DatabaseName)
|
||||
tables, err := ch.conn.Query(query)
|
||||
|
||||
if err != nil {
|
||||
@@ -277,7 +277,7 @@ func (ch *ClickHouse) Drop() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
|
||||
query = "DROP TABLE IF EXISTS " + quoteIdentifier(ch.config.DatabaseName) + "." + quoteIdentifier(table)
|
||||
|
||||
if _, err := ch.conn.Exec(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
@@ -305,3 +305,12 @@ func (ch *ClickHouse) Unlock() error {
|
||||
return nil
|
||||
}
|
||||
func (ch *ClickHouse) Close() error { return ch.conn.Close() }
|
||||
|
||||
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
|
||||
func quoteIdentifier(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return `"` + strings.Replace(name, `"`, `""`, -1) + `"`
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
const reservedChars = "!#$%&'()*+,/:;=?@[]"
|
||||
const reservedCharTestNamePrefix = "reserved char "
|
||||
|
||||
const baseUsername = "username"
|
||||
|
||||
const scheme = "database://"
|
||||
|
||||
// TestUserUnencodedReservedURLChars documents the behavior of using unencoded reserved characters in usernames with
|
||||
// net/url Parse()
|
||||
func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
scheme := "database://"
|
||||
urlSuffix := "password@localhost:12345/myDB?someParam=true"
|
||||
urlSuffixAndSep := ":" + urlSuffix
|
||||
|
||||
@@ -64,7 +66,7 @@ func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
s := scheme + baseUsername + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -98,13 +100,12 @@ func TestUserUnencodedReservedURLChars(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserEncodedReservedURLChars(t *testing.T) {
|
||||
scheme := "database://"
|
||||
urlSuffix := "password@localhost:12345/myDB?someParam=true"
|
||||
urlSuffixAndSep := ":" + urlSuffix
|
||||
|
||||
for _, c := range reservedChars {
|
||||
c := string(c)
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := scheme + baseUsername + encodedChar + urlSuffixAndSep
|
||||
expectedUsername := baseUsername + c
|
||||
@@ -126,7 +127,7 @@ func TestUserEncodedReservedURLChars(t *testing.T) {
|
||||
// with net/url Parse()
|
||||
func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
@@ -174,7 +175,7 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
testedChars := make([]string, 0, len(reservedChars))
|
||||
for _, tc := range testcases {
|
||||
testedChars = append(testedChars, tc.char)
|
||||
t.Run("reserved char "+tc.char, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+tc.char, func(t *testing.T) {
|
||||
s := schemeAndUsernameAndSep + basePassword + tc.char + urlSuffixAndSep
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
@@ -213,13 +214,13 @@ func TestPasswordUnencodedReservedURLChars(t *testing.T) {
|
||||
|
||||
func TestPasswordEncodedReservedURLChars(t *testing.T) {
|
||||
username := baseUsername
|
||||
schemeAndUsernameAndSep := "database://" + username + ":"
|
||||
schemeAndUsernameAndSep := scheme + username + ":"
|
||||
basePassword := "password"
|
||||
urlSuffixAndSep := "@localhost:12345/myDB?someParam=true"
|
||||
|
||||
for _, c := range reservedChars {
|
||||
c := string(c)
|
||||
t.Run("reserved char "+c, func(t *testing.T) {
|
||||
t.Run(reservedCharTestNamePrefix+c, func(t *testing.T) {
|
||||
encodedChar := "%" + hex.EncodeToString([]byte(c))
|
||||
s := schemeAndUsernameAndSep + basePassword + encodedChar + urlSuffixAndSep
|
||||
expectedPassword := basePassword + c
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# pgx
|
||||
|
||||
This package is for [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). A backend for the newer [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5) is [also available](v5).
|
||||
|
||||
`pgx://user:password@host:port/dbname?query`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@ import (
|
||||
func init() {
|
||||
db := Postgres{}
|
||||
database.Register("pgx", &db)
|
||||
database.Register("pgx4", &db)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -150,7 +151,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
|
||||
// i.e. pgx://user:password@host:port/db => postgres://user:password@host:port/db
|
||||
purl.Scheme = "postgres"
|
||||
|
||||
db, err := sql.Open("pgx", migrate.FilterCustomQuery(purl).String())
|
||||
db, err := sql.Open("pgx/v4", migrate.FilterCustomQuery(purl).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,19 +8,17 @@ import (
|
||||
sqldriver "database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
@@ -41,6 +39,9 @@ var (
|
||||
{ImageName: "postgres:10", Options: opts},
|
||||
{ImageName: "postgres:11", Options: opts},
|
||||
{ImageName: "postgres:12", Options: opts},
|
||||
{ImageName: "postgres:13", Options: opts},
|
||||
{ImageName: "postgres:14", Options: opts},
|
||||
{ImageName: "postgres:15", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# pgx
|
||||
|
||||
This package is for [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5). A backend for the older [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). is [also available](..).
|
||||
|
||||
`pgx5://user:password@host:port/dbname?query`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table |
|
||||
| `x-migrations-table-quoted` | `MigrationsTableQuoted` | By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting and naively checks that you have quoted the migration table name. e.g. `"my_schema"."schema_migrations"` |
|
||||
| `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds |
|
||||
| `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) |
|
||||
| `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) |
|
||||
| `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 <current_version>`.
|
||||
|
||||
## Multi-statement mode
|
||||
|
||||
In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction. Sometimes this
|
||||
behavior is not desirable because some statements can be only run outside of transaction (e.g.
|
||||
`CREATE INDEX CONCURRENTLY`). If you want to use `CREATE INDEX CONCURRENTLY` without activating multi-statement mode
|
||||
you have to put such statements in a separate migration files.
|
||||
@@ -0,0 +1,486 @@
|
||||
//go:build go1.9
|
||||
// +build go1.9
|
||||
|
||||
package pgx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/golang-migrate/migrate/v4/database/multistmt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db := Postgres{}
|
||||
database.Register("pgx5", &db)
|
||||
}
|
||||
|
||||
var (
|
||||
multiStmtDelimiter = []byte(";")
|
||||
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNilConfig = fmt.Errorf("no config")
|
||||
ErrNoDatabaseName = fmt.Errorf("no database name")
|
||||
ErrNoSchema = fmt.Errorf("no schema")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MigrationsTable string
|
||||
DatabaseName string
|
||||
SchemaName string
|
||||
migrationsSchemaName string
|
||||
migrationsTableName string
|
||||
StatementTimeout time.Duration
|
||||
MigrationsTableQuoted bool
|
||||
MultiStatementEnabled bool
|
||||
MultiStatementMaxSize int
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
// Locking and unlocking need to use the same connection
|
||||
conn *sql.Conn
|
||||
db *sql.DB
|
||||
isLocked atomic.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 config.DatabaseName == "" {
|
||||
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 config.SchemaName == "" {
|
||||
query := `SELECT CURRENT_SCHEMA()`
|
||||
var schemaName string
|
||||
if err := instance.QueryRow(query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(schemaName) == 0 {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
config.migrationsSchemaName = config.SchemaName
|
||||
config.migrationsTableName = config.MigrationsTable
|
||||
if config.MigrationsTableQuoted {
|
||||
re := regexp.MustCompile(`"(.*?)"`)
|
||||
result := re.FindAllStringSubmatch(config.MigrationsTable, -1)
|
||||
config.migrationsTableName = result[len(result)-1][1]
|
||||
if len(result) == 2 {
|
||||
config.migrationsSchemaName = result[0][1]
|
||||
} else if len(result) > 2 {
|
||||
return nil, fmt.Errorf("\"%s\" MigrationsTable contains too many dot characters", config.MigrationsTable)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Driver is registered as pgx, but connection string must use postgres schema
|
||||
// when making actual connection
|
||||
// i.e. pgx://user:password@host:port/db => postgres://user:password@host:port/db
|
||||
purl.Scheme = "postgres"
|
||||
|
||||
db, err := sql.Open("pgx/v5", migrate.FilterCustomQuery(purl).String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migrationsTable := purl.Query().Get("x-migrations-table")
|
||||
migrationsTableQuoted := false
|
||||
if s := purl.Query().Get("x-migrations-table-quoted"); len(s) > 0 {
|
||||
migrationsTableQuoted, err = strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse option x-migrations-table-quoted: %w", err)
|
||||
}
|
||||
}
|
||||
if (len(migrationsTable) > 0) && (migrationsTableQuoted) && ((migrationsTable[0] != '"') || (migrationsTable[len(migrationsTable)-1] != '"')) {
|
||||
return nil, fmt.Errorf("x-migrations-table must be quoted (for instance '\"migrate\".\"schema_migrations\"') when x-migrations-table-quoted is enabled, current value is: %s", migrationsTable)
|
||||
}
|
||||
|
||||
statementTimeoutString := purl.Query().Get("x-statement-timeout")
|
||||
statementTimeout := 0
|
||||
if statementTimeoutString != "" {
|
||||
statementTimeout, err = strconv.Atoi(statementTimeoutString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
multiStatementMaxSize := DefaultMultiStatementMaxSize
|
||||
if s := purl.Query().Get("x-multi-statement-max-size"); len(s) > 0 {
|
||||
multiStatementMaxSize, err = strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if multiStatementMaxSize <= 0 {
|
||||
multiStatementMaxSize = DefaultMultiStatementMaxSize
|
||||
}
|
||||
}
|
||||
|
||||
multiStatementEnabled := false
|
||||
if s := purl.Query().Get("x-multi-statement"); len(s) > 0 {
|
||||
multiStatementEnabled, err = strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse option x-multi-statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
px, err := WithInstance(db, &Config{
|
||||
DatabaseName: purl.Path,
|
||||
MigrationsTable: migrationsTable,
|
||||
MigrationsTableQuoted: migrationsTableQuoted,
|
||||
StatementTimeout: time.Duration(statementTimeout) * time.Millisecond,
|
||||
MultiStatementEnabled: multiStatementEnabled,
|
||||
MultiStatementMaxSize: multiStatementMaxSize,
|
||||
})
|
||||
|
||||
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 {
|
||||
return database.CasRestoreOnErr(&p.isLocked, false, true, database.ErrLocked, func() error {
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This will wait indefinitely until the lock can be acquired.
|
||||
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)}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Postgres) Unlock() error {
|
||||
return database.CasRestoreOnErr(&p.isLocked, true, false, database.ErrNotLocked, func() error {
|
||||
aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
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)}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Postgres) Run(migration io.Reader) error {
|
||||
if p.config.MultiStatementEnabled {
|
||||
var err error
|
||||
if e := multistmt.Parse(migration, multiStmtDelimiter, p.config.MultiStatementMaxSize, func(m []byte) bool {
|
||||
if err = p.runStatement(m); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}); e != nil {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
}
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.runStatement(migr)
|
||||
}
|
||||
|
||||
func (p *Postgres) runStatement(statement []byte) error {
|
||||
ctx := context.Background()
|
||||
if p.config.StatementTimeout != 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, p.config.StatementTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
query := string(statement)
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := p.conn.ExecContext(ctx, query); err != nil {
|
||||
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok {
|
||||
var line uint
|
||||
var col uint
|
||||
var lineColOK bool
|
||||
line, col, lineColOK = computeLineFromPos(query, int(pgErr.Position))
|
||||
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: statement, Line: line}
|
||||
}
|
||||
return database.Error{OrigErr: err, Err: "migration failed", Query: statement}
|
||||
}
|
||||
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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName)
|
||||
if _, err := tx.Exec(query); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
// Also re-write the schema version for nil dirty versions to prevent
|
||||
// empty schema version for failed down migration on the first migration
|
||||
// See: https://github.com/golang-migrate/migrate/issues/330
|
||||
if version >= 0 || (version == database.NilVersion && dirty) {
|
||||
query = `INSERT INTO ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName) + ` (version, dirty) VALUES ($1, $2)`
|
||||
if _, err := tx.Exec(query, version, dirty); err != nil {
|
||||
if errRollback := tx.Rollback(); errRollback != nil {
|
||||
err = multierror.Append(err, errRollback)
|
||||
}
|
||||
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 ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName) + ` 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.(*pgconn.PgError); ok {
|
||||
if e.SQLState() == pgerrcode.UndefinedTable {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
}
|
||||
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
|
||||
default:
|
||||
return version, dirty, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Postgres) Drop() (err 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 func() {
|
||||
if errClose := tables.Close(); errClose != nil {
|
||||
err = multierror.Append(err, errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 err := tables.Err(); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(tableNames) > 0 {
|
||||
// delete one by one ...
|
||||
for _, t := range tableNames {
|
||||
query = `DROP TABLE IF EXISTS ` + quoteIdentifier(t) + ` CASCADE`
|
||||
if _, err := p.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.
|
||||
// Note that this function locks the database, which deviates from the usual
|
||||
// convention of "caller locks" in the Postgres type.
|
||||
func (p *Postgres) ensureVersionTable() (err error) {
|
||||
if err = p.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := p.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// This block checks whether the `MigrationsTable` already exists. This is useful because it allows read only postgres
|
||||
// users to also check the current version of the schema. Previously, even if `MigrationsTable` existed, the
|
||||
// `CREATE TABLE IF NOT EXISTS...` query would fail because the user does not have the CREATE permission.
|
||||
// Taken from https://github.com/mattes/migrate/blob/master/database/postgres/postgres.go#L258
|
||||
query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 LIMIT 1`
|
||||
row := p.conn.QueryRowContext(context.Background(), query, p.config.migrationsSchemaName, p.config.migrationsTableName)
|
||||
|
||||
var count int
|
||||
err = row.Scan(&count)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query = `CREATE TABLE IF NOT EXISTS ` + quoteIdentifier(p.config.migrationsSchemaName) + `.` + quoteIdentifier(p.config.migrationsTableName) + ` (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
|
||||
}
|
||||
|
||||
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
|
||||
func quoteIdentifier(name string) string {
|
||||
end := strings.IndexRune(name, 0)
|
||||
if end > -1 {
|
||||
name = name[:end]
|
||||
}
|
||||
return `"` + strings.Replace(name, `"`, `""`, -1) + `"`
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
package pgx
|
||||
|
||||
// error codes https://github.com/jackc/pgerrcode/blob/master/errcode.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
sqldriver "database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
const (
|
||||
pgPassword = "postgres"
|
||||
)
|
||||
|
||||
var (
|
||||
opts = dktest.Options{
|
||||
Env: map[string]string{"POSTGRES_PASSWORD": pgPassword},
|
||||
PortRequired: true, ReadyFunc: isReady}
|
||||
// Supported versions: https://www.postgresql.org/support/versioning/
|
||||
specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "postgres:9.5", Options: opts},
|
||||
{ImageName: "postgres:9.6", Options: opts},
|
||||
{ImageName: "postgres:10", Options: opts},
|
||||
{ImageName: "postgres:11", Options: opts},
|
||||
{ImageName: "postgres:12", Options: opts},
|
||||
{ImageName: "postgres:13", Options: opts},
|
||||
{ImageName: "postgres:14", Options: opts},
|
||||
{ImageName: "postgres:15", Options: opts},
|
||||
}
|
||||
)
|
||||
|
||||
func pgConnectionString(host, port string, options ...string) string {
|
||||
options = append(options, "sslmode=disable")
|
||||
return fmt.Sprintf("postgres://postgres:%s@%s:%s/postgres?%s", pgPassword, host, port, strings.Join(options, "&"))
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", pgConnectionString(ip, port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
log.Println("close error:", err)
|
||||
}
|
||||
}()
|
||||
if err = db.PingContext(ctx); err != nil {
|
||||
switch err {
|
||||
case sqldriver.ErrBadConn, io.EOF:
|
||||
return false
|
||||
default:
|
||||
log.Println(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func mustRun(t *testing.T, d database.Driver, statements []string) {
|
||||
for _, statement := range statements {
|
||||
if err := d.Run(strings.NewReader(statement)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
m, err := migrate.NewWithDatabaseInstance("file://../examples/migrations", "pgx", d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleStatements(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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
if err := d.Run(strings.NewReader("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 TestMultipleStatementsInMultiStatementMode(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 := pgConnectionString(ip, port, "x-multi-statement=true")
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE INDEX CONCURRENTLY idx_foo ON foo (foo);")); err != nil {
|
||||
t.Fatalf("expected err to be nil, got %v", err)
|
||||
}
|
||||
|
||||
// make sure created index exists
|
||||
var exists bool
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM pg_indexes WHERE schemaname = (SELECT current_schema()) AND indexname = 'idx_foo')").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table bar to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
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: ERROR: syntax error at or near "TABLEE" (SQLSTATE 42601))`
|
||||
if err := d.Run(strings.NewReader("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) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.FirstPort()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := pgConnectionString(ip, port, "x-custom=foobar")
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithSchema(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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create foobar schema
|
||||
if err := d.Run(strings.NewReader("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(pgConnectionString(ip, port, "search_path=foobar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
version, _, err := d2.Version()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version != database.NilVersion {
|
||||
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 TestMigrationTableOption(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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, _ := p.Open(addr)
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create migrate schema
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA migrate AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// bad unquoted x-migrations-table parameter
|
||||
wantErr := "x-migrations-table must be quoted (for instance '\"migrate\".\"schema_migrations\"') when x-migrations-table-quoted is enabled, current value is: migrate.schema_migrations"
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if (err != nil) && (err.Error() != wantErr) {
|
||||
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
||||
}
|
||||
|
||||
// too many quoted x-migrations-table parameters
|
||||
wantErr = "\"\"migrate\".\"schema_migrations\".\"toomany\"\" MigrationsTable contains too many dot characters"
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\".\"toomany\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if (err != nil) && (err.Error() != wantErr) {
|
||||
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
||||
}
|
||||
|
||||
// good quoted x-migrations-table parameter
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"migrate\".\"schema_migrations\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make sure migrate.schema_migrations table exists
|
||||
var exists bool
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'schema_migrations' AND table_schema = 'migrate')").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table migrate.schema_migrations to exist")
|
||||
}
|
||||
|
||||
d, err = p.Open(fmt.Sprintf("postgres://postgres:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=migrate.schema_migrations",
|
||||
pgPassword, ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.(*Postgres).conn.QueryRowContext(context.Background(), "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'migrate.schema_migrations' AND table_schema = (SELECT current_schema()))").Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("expected table 'migrate.schema_migrations' to exist")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailToCreateTableWithoutPermissions(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 := pgConnectionString(ip, port)
|
||||
|
||||
// Check that opening the postgres connection returns NilVersion
|
||||
p := &Postgres{}
|
||||
|
||||
d, err := p.Open(addr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine
|
||||
// since this is a test environment and we're not expecting to the pgPassword to be malicious
|
||||
mustRun(t, d, []string{
|
||||
"CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'",
|
||||
"CREATE SCHEMA barfoo AUTHORIZATION postgres",
|
||||
"GRANT USAGE ON SCHEMA barfoo TO not_owner",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
defer func() {
|
||||
if d2 == nil {
|
||||
return
|
||||
}
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
var e *database.Error
|
||||
if !errors.As(err, &e) || err == nil {
|
||||
t.Fatal("Unexpected error, want permission denied error. Got: ", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") {
|
||||
t.Fatal(e)
|
||||
}
|
||||
|
||||
// re-connect using that x-migrations-table and x-migrations-table-quoted
|
||||
d2, err = p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&x-migrations-table=\"barfoo\".\"schema_migrations\"&x-migrations-table-quoted=1",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if !errors.As(err, &e) || err == nil {
|
||||
t.Fatal("Unexpected error, want permission denied error. Got: ", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(e.OrigErr.Error(), "permission denied for schema barfoo") {
|
||||
t.Fatal(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckBeforeCreateTable(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 := pgConnectionString(ip, port)
|
||||
|
||||
// Check that opening the postgres connection returns NilVersion
|
||||
p := &Postgres{}
|
||||
|
||||
d, err := p.Open(addr)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create user who is not the owner. Although we're concatenating strings in an sql statement it should be fine
|
||||
// since this is a test environment and we're not expecting to the pgPassword to be malicious
|
||||
mustRun(t, d, []string{
|
||||
"CREATE USER not_owner WITH ENCRYPTED PASSWORD '" + pgPassword + "'",
|
||||
"CREATE SCHEMA barfoo AUTHORIZATION postgres",
|
||||
"GRANT USAGE ON SCHEMA barfoo TO not_owner",
|
||||
"GRANT CREATE ON SCHEMA barfoo TO not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d2, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := d2.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// revoke privileges
|
||||
mustRun(t, d, []string{
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM PUBLIC",
|
||||
"REVOKE CREATE ON SCHEMA barfoo FROM not_owner",
|
||||
})
|
||||
|
||||
// re-connect using that schema
|
||||
d3, err := p.Open(fmt.Sprintf("postgres://not_owner:%s@%v:%v/postgres?sslmode=disable&search_path=barfoo",
|
||||
pgPassword, ip, port))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
version, _, err := d3.Version()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if version != database.NilVersion {
|
||||
t.Fatal("Unexpected version, want database.NilVersion. Got: ", version)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d3.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelSchema(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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// create foo and bar schemas
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA foo AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Run(strings.NewReader("CREATE SCHEMA bar AUTHORIZATION postgres")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// re-connect using that schemas
|
||||
dfoo, err := p.Open(pgConnectionString(ip, port, "search_path=foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := dfoo.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
dbar, err := p.Open(pgConnectionString(ip, port, "search_path=bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := dbar.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := dfoo.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dbar.Lock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dbar.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dfoo.Unlock(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgres_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 := pgConnectionString(ip, port)
|
||||
p := &Postgres{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(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 TestWithInstance_Concurrent(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)
|
||||
}
|
||||
|
||||
// The number of concurrent processes running WithInstance
|
||||
const concurrency = 30
|
||||
|
||||
// We can instantiate a single database handle because it is
|
||||
// actually a connection pool, and so, each of the below go
|
||||
// routines will have a high probability of using a separate
|
||||
// connection, which is something we want to exercise.
|
||||
db, err := sql.Open("pgx", pgConnectionString(ip, port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
db.SetMaxIdleConns(concurrency)
|
||||
db.SetMaxOpenConns(concurrency)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Add(concurrency)
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
_, err := WithInstance(db, &Config{})
|
||||
if err != nil {
|
||||
t.Errorf("process %d error: %s", i, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ If there were no errors, we should have two files available under `db/migrations
|
||||
Note the `sql` extension that we provided.
|
||||
|
||||
In the `.up.sql` file let's create the table:
|
||||
```
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
user_id serial PRIMARY KEY,
|
||||
username VARCHAR (50) UNIQUE NOT NULL,
|
||||
@@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
);
|
||||
```
|
||||
And in the `.down.sql` let's delete it:
|
||||
```
|
||||
```sql
|
||||
DROP TABLE IF EXISTS users;
|
||||
```
|
||||
By adding `IF EXISTS/IF NOT EXISTS` we are making migrations idempotent - you can read more about idempotency in [getting started](../../GETTING_STARTED.md#create-migrations)
|
||||
@@ -79,7 +79,7 @@ Again, it should create for us two migrations files:
|
||||
In Postgres, when we want our queries to be done in a transaction, we need to wrap it with `BEGIN` and `COMMIT` commands.
|
||||
In our example, we are going to add a column to our database that can only accept enumerable values or NULL.
|
||||
Migration up:
|
||||
```
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE enum_mood AS ENUM (
|
||||
@@ -92,7 +92,7 @@ ALTER TABLE users ADD COLUMN mood enum_mood;
|
||||
COMMIT;
|
||||
```
|
||||
Migration down:
|
||||
```
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE users DROP COLUMN mood;
|
||||
@@ -124,7 +124,7 @@ Indexes:
|
||||
|
||||
## Optional: Run migrations within your Go app
|
||||
Here is a very simple app running migrations for the above configuration:
|
||||
```
|
||||
```go
|
||||
import (
|
||||
"log"
|
||||
|
||||
|
||||
@@ -90,16 +90,16 @@ func WithConnection(ctx context.Context, conn *sql.Conn, config *Config) (*Postg
|
||||
|
||||
if config.SchemaName == "" {
|
||||
query := `SELECT CURRENT_SCHEMA()`
|
||||
var schemaName string
|
||||
var schemaName sql.NullString
|
||||
if err := conn.QueryRowContext(ctx, query).Scan(&schemaName); err != nil {
|
||||
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
if len(schemaName) == 0 {
|
||||
if !schemaName.Valid {
|
||||
return nil, ErrNoSchema
|
||||
}
|
||||
|
||||
config.SchemaName = schemaName
|
||||
config.SchemaName = schemaName.String
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# rqlite
|
||||
|
||||
`rqlite://admin:[email protected]:4001/?level=strong&timeout=5`
|
||||
|
||||
The `rqlite` url scheme is used for both secure and insecure connections. If connecting to an insecure database, pass `x-connect-insecure` in your URL query, or use `WithInstance` to pass an established connection.
|
||||
|
||||
The migrations table name is configurable through the `x-migrations-table` URL query parameter, or by using `WithInstance` and passing `MigrationsTable` through `Config`.
|
||||
|
||||
Other connect parameters are directly passed through to the database driver. For examples of connection strings, see https://github.com/rqlite/gorqlite#examples.
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| `x-connect-insecure` | n/a: set on instance | Boolean to indicate whether to use an insecure connection. Defaults to `false`. |
|
||||
| `x-migrations-table` | `MigrationsTable` | Name of the migrations table. Defaults to `schema_migrations`. |
|
||||
|
||||
## Notes
|
||||
|
||||
* Uses the https://github.com/rqlite/gorqlite driver
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE TABLE pets (
|
||||
name string
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS pets;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE pets ADD predator bool;
|
||||
@@ -0,0 +1,334 @@
|
||||
package rqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rqlite/gorqlite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Register("rqlite", &Rqlite{})
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultMigrationsTable defines the default rqlite migrations table
|
||||
DefaultMigrationsTable = "schema_migrations"
|
||||
|
||||
// DefaultConnectInsecure defines the default setting for connect insecure
|
||||
DefaultConnectInsecure = false
|
||||
)
|
||||
|
||||
// ErrNilConfig is returned if no configuration was passed to WithInstance
|
||||
var ErrNilConfig = fmt.Errorf("no config")
|
||||
|
||||
// ErrBadConfig is returned if configuration was invalid
|
||||
var ErrBadConfig = fmt.Errorf("bad parameter")
|
||||
|
||||
// Config defines the driver configuration
|
||||
type Config struct {
|
||||
// ConnectInsecure sets whether the connection uses TLS. Ineffectual when using WithInstance
|
||||
ConnectInsecure bool
|
||||
// MigrationsTable configures the migrations table name
|
||||
MigrationsTable string
|
||||
}
|
||||
|
||||
type Rqlite struct {
|
||||
db *gorqlite.Connection
|
||||
isLocked atomic.Bool
|
||||
|
||||
config *Config
|
||||
}
|
||||
|
||||
// WithInstance creates a rqlite database driver with an existing gorqlite database connection
|
||||
// and a Config struct
|
||||
func WithInstance(instance *gorqlite.Connection, config *Config) (database.Driver, error) {
|
||||
if config == nil {
|
||||
return nil, ErrNilConfig
|
||||
}
|
||||
|
||||
// we use the consistency level check as a database ping
|
||||
if _, err := instance.ConsistencyLevel(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(config.MigrationsTable) == 0 {
|
||||
config.MigrationsTable = DefaultMigrationsTable
|
||||
}
|
||||
|
||||
driver := &Rqlite{
|
||||
db: instance,
|
||||
config: config,
|
||||
}
|
||||
|
||||
if err := driver.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// OpenURL creates a rqlite database driver from a connect URL
|
||||
func OpenURL(url string) (database.Driver, error) {
|
||||
d := &Rqlite{}
|
||||
return d.Open(url)
|
||||
}
|
||||
|
||||
func (r *Rqlite) ensureVersionTable() (err error) {
|
||||
if err = r.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := r.Unlock(); e != nil {
|
||||
if err == nil {
|
||||
err = e
|
||||
} else {
|
||||
err = multierror.Append(err, e)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
stmts := []string{
|
||||
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (version uint64, dirty bool)`, r.config.MigrationsTable),
|
||||
fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version)`, r.config.MigrationsTable),
|
||||
}
|
||||
|
||||
if _, err := r.db.Write(stmts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open returns a new driver instance configured with parameters
|
||||
// coming from the URL string. Migrate will call this function
|
||||
// only once per instance.
|
||||
func (r *Rqlite) Open(url string) (database.Driver, error) {
|
||||
dburl, config, err := parseUrl(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config = config
|
||||
|
||||
r.db, err = gorqlite.Open(dburl.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.ensureVersionTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Close closes the underlying database instance managed by the driver.
|
||||
// Migrate will call this function only once per instance.
|
||||
func (r *Rqlite) Close() error {
|
||||
r.db.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock should acquire a database lock so that only one migration process
|
||||
// can run at a time. Migrate will call this function before Run is called.
|
||||
// If the implementation can't provide this functionality, return nil.
|
||||
// Return database.ErrLocked if database is already locked.
|
||||
func (r *Rqlite) Lock() error {
|
||||
if !r.isLocked.CAS(false, true) {
|
||||
return database.ErrLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock should release the lock. Migrate will call this function after
|
||||
// all migrations have been run.
|
||||
func (r *Rqlite) Unlock() error {
|
||||
if !r.isLocked.CAS(true, false) {
|
||||
return database.ErrNotLocked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run applies a migration to the database. migration is guaranteed to be not nil.
|
||||
func (r *Rqlite) Run(migration io.Reader) error {
|
||||
migr, err := io.ReadAll(migration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := string(migr[:])
|
||||
if _, err := r.db.WriteOne(query); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVersion saves version and dirty state.
|
||||
// Migrate will call this function before and after each call to Run.
|
||||
// version must be >= -1. -1 means NilVersion.
|
||||
func (r *Rqlite) SetVersion(version int, dirty bool) error {
|
||||
deleteQuery := fmt.Sprintf(`DELETE FROM %s`, r.config.MigrationsTable)
|
||||
statements := []gorqlite.ParameterizedStatement{
|
||||
{
|
||||
Query: deleteQuery,
|
||||
},
|
||||
}
|
||||
|
||||
// Also re-write the schema version for nil dirty versions to prevent
|
||||
// empty schema version for failed down migration on the first migration
|
||||
// See: https://github.com/golang-migrate/migrate/issues/330
|
||||
insertQuery := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, r.config.MigrationsTable)
|
||||
if version >= 0 || (version == database.NilVersion && dirty) {
|
||||
statements = append(statements, gorqlite.ParameterizedStatement{
|
||||
Query: insertQuery,
|
||||
Arguments: []interface{}{
|
||||
version,
|
||||
dirty,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
wr, err := r.db.WriteParameterized(statements)
|
||||
if err != nil {
|
||||
for i, res := range wr {
|
||||
if res.Err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(statements[i].Query)}
|
||||
}
|
||||
}
|
||||
|
||||
// if somehow we're still here, return the original error with combined queries
|
||||
return &database.Error{OrigErr: err, Query: []byte(deleteQuery + "\n" + insertQuery)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version returns the currently active version and if the database is dirty.
|
||||
// When no migration has been applied, it must return version -1.
|
||||
// Dirty means, a previous migration failed and user interaction is required.
|
||||
func (r *Rqlite) Version() (version int, dirty bool, err error) {
|
||||
query := "SELECT version, dirty FROM " + r.config.MigrationsTable + " LIMIT 1"
|
||||
|
||||
qr, err := r.db.QueryOne(query)
|
||||
if err != nil {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
|
||||
if !qr.Next() {
|
||||
return database.NilVersion, false, nil
|
||||
}
|
||||
|
||||
if err := qr.Scan(&version, &dirty); err != nil {
|
||||
return database.NilVersion, false, &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return version, dirty, nil
|
||||
}
|
||||
|
||||
// Drop deletes everything in the database.
|
||||
// Note that this is a breaking action, a new call to Open() is necessary to
|
||||
// ensure subsequent calls work as expected.
|
||||
func (r *Rqlite) Drop() error {
|
||||
query := `SELECT name FROM sqlite_master WHERE type = 'table'`
|
||||
|
||||
tables, err := r.db.QueryOne(query)
|
||||
if err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
statements := make([]string, 0)
|
||||
for tables.Next() {
|
||||
var tableName string
|
||||
if err := tables.Scan(&tableName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tableName) > 0 {
|
||||
statement := fmt.Sprintf(`DROP TABLE %s`, tableName)
|
||||
statements = append(statements, statement)
|
||||
}
|
||||
}
|
||||
|
||||
// return if nothing to do
|
||||
if len(statements) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wr, err := r.db.Write(statements)
|
||||
if err != nil {
|
||||
for i, res := range wr {
|
||||
if res.Err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(statements[i])}
|
||||
}
|
||||
}
|
||||
|
||||
// if somehow we're still here, return the original error with combined queries
|
||||
return &database.Error{OrigErr: err, Query: []byte(strings.Join(statements, "\n"))}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUrl(url string) (*nurl.URL, *Config, error) {
|
||||
parsedUrl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
config, err := parseConfigFromQuery(parsedUrl.Query())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if parsedUrl.Scheme != "rqlite" {
|
||||
return nil, nil, errors.Wrap(ErrBadConfig, "bad scheme")
|
||||
}
|
||||
|
||||
// adapt from rqlite to http/https schemes
|
||||
if config.ConnectInsecure {
|
||||
parsedUrl.Scheme = "http"
|
||||
} else {
|
||||
parsedUrl.Scheme = "https"
|
||||
}
|
||||
|
||||
filteredUrl := migrate.FilterCustomQuery(parsedUrl)
|
||||
|
||||
return filteredUrl, config, nil
|
||||
}
|
||||
|
||||
func parseConfigFromQuery(queryVals nurl.Values) (*Config, error) {
|
||||
c := Config{
|
||||
ConnectInsecure: DefaultConnectInsecure,
|
||||
MigrationsTable: DefaultMigrationsTable,
|
||||
}
|
||||
|
||||
migrationsTable := queryVals.Get("x-migrations-table")
|
||||
if migrationsTable != "" {
|
||||
if strings.HasPrefix(migrationsTable, "sqlite_") {
|
||||
return nil, errors.Wrap(ErrBadConfig, "invalid value for x-migrations-table")
|
||||
}
|
||||
c.MigrationsTable = migrationsTable
|
||||
}
|
||||
|
||||
connectInsecureStr := queryVals.Get("x-connect-insecure")
|
||||
if connectInsecureStr != "" {
|
||||
connectInsecure, err := strconv.ParseBool(connectInsecureStr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrBadConfig, "invalid value for x-connect-insecure")
|
||||
}
|
||||
c.ConnectInsecure = connectInsecure
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package rqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/rqlite/gorqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
||||
"github.com/golang-migrate/migrate/v4/dktesting"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
var defaultPort uint16 = 4001
|
||||
|
||||
var opts = dktest.Options{
|
||||
Env: map[string]string{"NODE_ID": "1"},
|
||||
PortRequired: true,
|
||||
ReadyFunc: isReady,
|
||||
}
|
||||
var specs = []dktesting.ContainerSpec{
|
||||
{ImageName: "rqlite/rqlite:7.21.4", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.0.6", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.11.1", Options: opts},
|
||||
{ImageName: "rqlite/rqlite:8.12.3", Options: opts},
|
||||
}
|
||||
|
||||
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
fmt.Println("error getting port")
|
||||
return false
|
||||
}
|
||||
|
||||
statusString := fmt.Sprintf("http://%s:%s/status", ip, port)
|
||||
fmt.Println(statusString)
|
||||
|
||||
var readyResp struct {
|
||||
Store struct {
|
||||
Ready bool `json:"ready"`
|
||||
} `json:"store"`
|
||||
}
|
||||
|
||||
resp, err := http.Get(statusString)
|
||||
if err != nil {
|
||||
fmt.Println("error getting status")
|
||||
return false
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
fmt.Println("statusCode != 200")
|
||||
return false
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("error reading body")
|
||||
return false
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &readyResp); err != nil {
|
||||
fmt.Println("error unmarshaling body")
|
||||
return false
|
||||
}
|
||||
|
||||
return readyResp.Store.Ready
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?level=strong&disableClusterDiscovery=true&x-connect-insecure=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
r := &Rqlite{}
|
||||
d, err := r.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dt.Test(t, d, []byte("CREATE TABLE t (Qty int, Name string);"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?level=strong&disableClusterDiscovery=true&x-connect-insecure=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
driver, err := OpenURL(connectString)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
dt.TestMigrate(t, m)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBadConnectInsecureParam(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("rqlite://%s:%s?x-connect-insecure=foo", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
_, err = OpenURL(connectString)
|
||||
assert.ErrorIs(t, err, ErrBadConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBadProtocol(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
connectString := fmt.Sprintf("postgres://%s:%s/database", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
|
||||
_, err = OpenURL(connectString)
|
||||
assert.ErrorIs(t, err, ErrBadConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoConfig(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = WithInstance(db, nil)
|
||||
assert.ErrorIs(t, err, ErrNilConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithInstanceEmptyConfig(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
driver, err := WithInstance(db, &Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("UP")
|
||||
err = m.Up()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", DefaultMigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("DOWN")
|
||||
err = m.Down()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMigrationTable(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// gorqlite expects http(s) schemes
|
||||
connectString := fmt.Sprintf("http://%s:%s?level=strong&disableClusterDiscovery=true", ip, port)
|
||||
t.Logf("DB connect string : %s\n", connectString)
|
||||
db, err := gorqlite.Open(connectString)
|
||||
assert.NoError(t, err)
|
||||
|
||||
config := Config{MigrationsTable: "my_migration_table"}
|
||||
driver, err := WithInstance(db, &config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if err := driver.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := migrate.NewWithDatabaseInstance(
|
||||
"file://./examples/migrations",
|
||||
"ql", driver)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log("UP")
|
||||
err = m.Up()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.WriteOne(`INSERT INTO pets (name, predator) VALUES ("franklin", true)`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := db.QueryOne(`SELECT name, predator FROM pets LIMIT 1`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_ = res.Next()
|
||||
|
||||
// make sure we can use the migrated table
|
||||
var petName string
|
||||
var petPredator int
|
||||
err = res.Scan(&petName, &petPredator)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, petName, "franklin")
|
||||
assert.Equal(t, petPredator, 1)
|
||||
|
||||
t.Log("DOWN")
|
||||
err = m.Down()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.QueryOne(fmt.Sprintf("SELECT * FROM %s", config.MigrationsTable))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseUrl(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
passedUrl string
|
||||
expectedUrl string
|
||||
expectedConfig *Config
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
"defaults",
|
||||
"rqlite://localhost:4001",
|
||||
"https://localhost:4001",
|
||||
&Config{ConnectInsecure: DefaultConnectInsecure, MigrationsTable: DefaultMigrationsTable},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"configure migration table",
|
||||
"rqlite://localhost:4001?x-migrations-table=foo",
|
||||
"https://localhost:4001",
|
||||
&Config{ConnectInsecure: DefaultConnectInsecure, MigrationsTable: "foo"},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"configure connect insecure",
|
||||
"rqlite://localhost:4001?x-connect-insecure=true",
|
||||
"http://localhost:4001",
|
||||
&Config{ConnectInsecure: true, MigrationsTable: DefaultMigrationsTable},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"invalid migration table",
|
||||
"rqlite://localhost:4001?x-migrations-table=sqlite_bar",
|
||||
"",
|
||||
nil,
|
||||
"invalid value for x-migrations-table: bad parameter",
|
||||
},
|
||||
{
|
||||
"invalid connect insecure",
|
||||
"rqlite://localhost:4001?x-connect-insecure=baz",
|
||||
"",
|
||||
nil,
|
||||
"invalid value for x-connect-insecure: bad parameter",
|
||||
},
|
||||
{
|
||||
"invalid url",
|
||||
string([]byte{0x7f}),
|
||||
"",
|
||||
nil,
|
||||
"parse \"\\x7f\": net/url: invalid control character in URL",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actualUrl, actualConfig, actualErr := parseUrl(tt.passedUrl)
|
||||
if tt.expectedUrl != "" {
|
||||
assert.Equal(t, tt.expectedUrl, actualUrl.String())
|
||||
} else {
|
||||
assert.Nil(t, actualUrl)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedConfig, actualConfig)
|
||||
|
||||
if tt.expectedErr == "" {
|
||||
assert.NoError(t, actualErr)
|
||||
} else {
|
||||
assert.EqualError(t, actualErr, tt.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ as described in [README.md#database-urls](../../README.md#database-urls)
|
||||
## DDL with comments
|
||||
|
||||
At the moment the GCP Spanner backed does not seem to allow for comments (See https://issuetracker.google.com/issues/159730604)
|
||||
so in order to be able to use migration with DDL containing comments `x-clean-stamements` is required
|
||||
so in order to be able to use migration with DDL containing comments `x-clean-statements` is required
|
||||
|
||||
## Multiple statements
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
|
||||
adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
uatomic "go.uber.org/atomic"
|
||||
"google.golang.org/api/iterator"
|
||||
adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
module github.com/golang-migrate/migrate/v4
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go/spanner v1.40.0
|
||||
cloud.google.com/go/storage v1.27.0
|
||||
cloud.google.com/go/spanner v1.51.0
|
||||
cloud.google.com/go/storage v1.30.1
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.16
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3
|
||||
github.com/aws/aws-sdk-go v1.33.0
|
||||
github.com/aws/aws-sdk-go v1.34.0
|
||||
github.com/cenkalti/backoff/v4 v4.1.2
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||
github.com/dhui/dktest v0.3.13
|
||||
github.com/docker/docker v20.10.19+incompatible
|
||||
github.com/dhui/dktest v0.4.0
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/fsouza/fake-gcs-server v1.17.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
@@ -17,150 +19,174 @@ require (
|
||||
github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556
|
||||
github.com/google/go-github/v39 v39.2.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jackc/pgconn v1.8.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
|
||||
github.com/jackc/pgx/v4 v4.10.1
|
||||
github.com/jackc/pgconn v1.14.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
|
||||
github.com/jackc/pgx/v4 v4.18.1
|
||||
github.com/jackc/pgx/v5 v5.3.1
|
||||
github.com/ktrysmt/go-bitbucket v0.6.4
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/markbates/pkger v0.15.1
|
||||
github.com/mattn/go-sqlite3 v1.14.14
|
||||
github.com/microsoft/go-mssqldb v0.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/microsoft/go-mssqldb v1.0.0
|
||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.0
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8
|
||||
github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba
|
||||
github.com/snowflakedb/gosnowflake v1.6.3
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/snowflakedb/gosnowflake v1.6.19
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/xanzy/go-gitlab v0.15.0
|
||||
go.mongodb.org/mongo-driver v1.7.0
|
||||
go.mongodb.org/mongo-driver v1.7.5
|
||||
go.uber.org/atomic v1.7.0
|
||||
golang.org/x/oauth2 v0.1.0
|
||||
golang.org/x/tools v0.1.12
|
||||
google.golang.org/api v0.102.0
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
golang.org/x/tools v0.10.0
|
||||
google.golang.org/api v0.150.0
|
||||
modernc.org/ql v1.0.0
|
||||
modernc.org/sqlite v1.18.0
|
||||
modernc.org/sqlite v1.18.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.6.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.1.1 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/azure-storage-blob-go v0.14.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
cloud.google.com/go v0.110.10 // indirect
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.5 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.4 // indirect
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/99designs/keyring v1.2.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1 // indirect
|
||||
github.com/aws/smithy-go v1.8.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/apache/arrow/go/v10 v10.0.1 // indirect
|
||||
github.com/apache/thrift v0.16.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.11 // indirect
|
||||
github.com/aws/smithy-go v1.13.3 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 // indirect
|
||||
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.3 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.13 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.11.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.0+incompatible // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/flatbuffers v2.0.8+incompatible // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gorilla/handlers v1.4.2 // indirect
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.0.7 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.6.2 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/k0kubun/pp v2.3.0+incompatible // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/asmfmt v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.15.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.15.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.16 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.2 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.0.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.2 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/b v1.0.0 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/cc/v3 v3.36.3 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||
modernc.org/db v1.0.0 // indirect
|
||||
modernc.org/file v1.0.0 // indirect
|
||||
modernc.org/fileutil v1.0.0 // indirect
|
||||
modernc.org/golex v1.0.0 // indirect
|
||||
modernc.org/internal v1.0.0 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/libc v1.17.1 // indirect
|
||||
modernc.org/lldb v1.0.0 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.2.1 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sortutil v1.1.0 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
modernc.org/zappy v1.0.0 // indirect
|
||||
)
|
||||
|
||||
go 1.18
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build pgx5
|
||||
// +build pgx5
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build rqlite
|
||||
// +build rqlite
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
_ "github.com/golang-migrate/migrate/v4/database/rqlite"
|
||||
)
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
)
|
||||
|
||||
const scheme = "file://"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -29,7 +31,7 @@ func Test(t *testing.T) {
|
||||
mustWriteFile(t, tmpDir, "7_foobar.down.sql", "7 down")
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://" + tmpDir)
|
||||
d, err := f.Open(scheme + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -48,7 +50,7 @@ func TestOpen(t *testing.T) {
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
_, err := f.Open("file://" + tmpDir) // absolute path
|
||||
_, err := f.Open(scheme + tmpDir) // absolute path
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -108,7 +110,7 @@ func TestOpenDefaultsToCurrentDirectory(t *testing.T) {
|
||||
}
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://")
|
||||
d, err := f.Open(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -125,7 +127,7 @@ func TestOpenWithDuplicateVersion(t *testing.T) {
|
||||
mustWriteFile(t, tmpDir, "1_bar.up.sql", "") // 1 up
|
||||
|
||||
f := &File{}
|
||||
_, err := f.Open("file://" + tmpDir)
|
||||
_, err := f.Open(scheme + tmpDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
@@ -135,7 +137,7 @@ func TestClose(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
f := &File{}
|
||||
d, err := f.Open("file://" + tmpDir)
|
||||
d, err := f.Open(scheme + tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -172,7 +174,7 @@ func BenchmarkOpen(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
f := &File{}
|
||||
_, err := f.Open("file://" + dir)
|
||||
_, err := f.Open(scheme + dir)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
@@ -188,7 +190,7 @@ func BenchmarkNext(b *testing.B) {
|
||||
}
|
||||
}()
|
||||
f := &File{}
|
||||
d, _ := f.Open("file://" + dir)
|
||||
d, _ := f.Open(scheme + dir)
|
||||
b.ResetTimer()
|
||||
v, err := d.First()
|
||||
for n := 0; n < b.N; n++ {
|
||||
|
||||
+4
-16
@@ -178,7 +178,7 @@ func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err e
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Up(version); ok {
|
||||
file, _, _, err := g.client.Repositories.GetContents(
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
@@ -189,13 +189,7 @@ func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err e
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if file != nil {
|
||||
r, err := file.GetContent()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(r)), m.Identifier, nil
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
@@ -204,7 +198,7 @@ func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Down(version); ok {
|
||||
file, _, _, err := g.client.Repositories.GetContents(
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
@@ -215,13 +209,7 @@ func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if file != nil {
|
||||
r, err := file.GetContent()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(r)), m.Identifier, nil
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user