From 2cc9320be6dda4de953b388c372b0e50aa26713f Mon Sep 17 00:00:00 2001 From: Bulat Khasanov Date: Thu, 22 Aug 2024 11:57:12 +0300 Subject: [PATCH] Add redis lua migrations --- Makefile | 2 +- database/redis/README.md | 24 ++ .../migrations/1578421040_set_key.down.lua | 1 + .../migrations/1578421040_set_key.up.lua | 1 + database/redis/redis.go | 228 +++++++++++++++ database/redis/redis_test.go | 259 ++++++++++++++++++ database/redis/uri.go | 157 +++++++++++ database/redis/uri_test.go | 27 ++ go.mod | 45 ++- go.sum | 8 + internal/cli/build_redis.go | 8 + internal/cli/main.go | 6 +- 12 files changed, 741 insertions(+), 25 deletions(-) create mode 100644 database/redis/README.md create mode 100644 database/redis/examples/migrations/1578421040_set_key.down.lua create mode 100644 database/redis/examples/migrations/1578421040_set_key.up.lua create mode 100644 database/redis/redis.go create mode 100644 database/redis/redis_test.go create mode 100644 database/redis/uri.go create mode 100644 database/redis/uri_test.go create mode 100644 internal/cli/build_redis.go diff --git a/Makefile b/Makefile index 8e23a43c7..fd1d2f996 100644 --- a/Makefile +++ b/Makefile @@ -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 pgx5 rqlite +DATABASE ?= postgres mysql redis 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 ?= diff --git a/database/redis/README.md b/database/redis/README.md new file mode 100644 index 000000000..3f3805f37 --- /dev/null +++ b/database/redis/README.md @@ -0,0 +1,24 @@ +# redis + +URL format: + +- standalone connection: + +`redis://:@:/` + +- failover connection: + +`redis://:@/?sentinel_addr=:` + +- cluster connection: + +`redis://:@:?addr=:&addr=:` + +`rediss://:@:?addr=:&addr=:` + +| URL Query | WithInstance Config | Description | +|--------------------|---------------------|---------------------------------------------| +| `x-mode` | - | The Mode that used to choose client type | +| `x-migrations-key` | `MigrationsKey` | Specify the key where migrations are stored | +| `x-lock-key` | `LockKey` | Specify the key where locks are stored | +| `x-lock-timeout` | `LockTimeout` | Specify the timeout of lock | diff --git a/database/redis/examples/migrations/1578421040_set_key.down.lua b/database/redis/examples/migrations/1578421040_set_key.down.lua new file mode 100644 index 000000000..7ca205dc0 --- /dev/null +++ b/database/redis/examples/migrations/1578421040_set_key.down.lua @@ -0,0 +1 @@ +return redis.call("DEL", "test_key") diff --git a/database/redis/examples/migrations/1578421040_set_key.up.lua b/database/redis/examples/migrations/1578421040_set_key.up.lua new file mode 100644 index 000000000..3b6008ba0 --- /dev/null +++ b/database/redis/examples/migrations/1578421040_set_key.up.lua @@ -0,0 +1 @@ +return redis.call("SET", "test_key", "1") diff --git a/database/redis/redis.go b/database/redis/redis.go new file mode 100644 index 000000000..36de7c244 --- /dev/null +++ b/database/redis/redis.go @@ -0,0 +1,228 @@ +package redis + +import ( + "context" + "fmt" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database" + "github.com/redis/go-redis/v9" + "go.uber.org/atomic" + "io" + neturl "net/url" + "strconv" + "strings" + "time" +) + +func init() { + db := Redis{} + database.Register("redis", &db) + database.Register("rediss", &db) +} + +var ( + DefaultMigrationsKey = "schema_migrations" + DefaultLockKey = "lock:schema_migrations" + DefaultLockTimeout = 15 * time.Second +) + +func convertVersionFromDB(result []interface{}) (int, bool, error) { + if result[0] == nil || result[1] == nil { + return database.NilVersion, false, nil + } + + version, err := strconv.Atoi(result[0].(string)) + if err != nil { + return 0, false, fmt.Errorf("can't parse version: %w", err) + } + + dirty, err := strconv.ParseBool(result[1].(string)) + if err != nil { + return 0, false, fmt.Errorf("can't parse dirty: %w", err) + } + + return version, dirty, nil +} + +type Mode int8 + +const ( + ModeUnspecified Mode = iota + ModeStandalone + ModeFailover + ModeCluster +) + +var rawModeToMode = map[string]Mode{ + "": ModeUnspecified, + "standalone": ModeStandalone, + "failover": ModeFailover, + "cluster": ModeCluster, +} + +func parseMode(rawMode string) (Mode, error) { + mode, ok := rawModeToMode[strings.ToLower(rawMode)] + if ok { + return mode, nil + } + + return ModeUnspecified, fmt.Errorf("unexpected mode: %q", rawMode) +} + +type Config struct { + MigrationsKey string + LockKey string + LockTimeout time.Duration +} + +func newClient(url string, mode Mode) (redis.UniversalClient, error) { + if mode == ModeUnspecified { + var err error + + mode, err = determineMode(url) + if err != nil { + return nil, err + } + } + + switch mode { + case ModeStandalone: + options, err := redis.ParseURL(url) + if err != nil { + return nil, err + } + + return redis.NewClient(options), nil + case ModeFailover: + options, err := parseFailoverURL(url) + if err != nil { + return nil, err + } + + return redis.NewFailoverClient(options), nil + case ModeCluster: + options, err := redis.ParseClusterURL(url) + if err != nil { + return nil, err + } + + return redis.NewClusterClient(options), nil + default: + return nil, fmt.Errorf("unexpected mode: %q", mode) + } +} + +func WithInstance(client redis.UniversalClient, config *Config) (database.Driver, error) { + if config.MigrationsKey == "" { + config.MigrationsKey = DefaultMigrationsKey + } + + if config.LockKey == "" { + config.LockKey = DefaultLockKey + } + + if config.LockTimeout == 0 { + config.LockTimeout = DefaultLockTimeout + } + + return &Redis{ + client: client, + config: config, + }, nil +} + +type Redis struct { + client redis.UniversalClient + isLocked atomic.Bool + config *Config +} + +func (r *Redis) Open(url string) (database.Driver, error) { + purl, err := neturl.Parse(url) + if err != nil { + return nil, err + } + + query := purl.Query() + + mode, err := parseMode(query.Get("x-mode")) + if err != nil { + return nil, err + } + + var lockTimeout time.Duration + rawLockTimeout := query.Get("x-lock-timeout") + if rawLockTimeout != "" { + lockTimeout, err = time.ParseDuration(rawLockTimeout) + if err != nil { + return nil, fmt.Errorf("invalid x-lock-timeout: %w", err) + } + } + + client, err := newClient(migrate.FilterCustomQuery(purl).String(), mode) + if err != nil { + return nil, fmt.Errorf("can't create client: %w", err) + } + + return WithInstance( + client, + &Config{ + MigrationsKey: query.Get("x-migrations-key"), + LockKey: query.Get("x-lock-key"), + LockTimeout: lockTimeout, + }, + ) +} + +func (r *Redis) Close() error { + return r.client.Close() +} + +func (r *Redis) Lock() error { + return database.CasRestoreOnErr(&r.isLocked, false, true, database.ErrLocked, func() error { + return r.client.SetArgs(context.Background(), r.config.LockKey, 1, redis.SetArgs{ + Mode: "NX", + TTL: r.config.LockTimeout, + }).Err() + }) +} + +func (r *Redis) Unlock() error { + return database.CasRestoreOnErr(&r.isLocked, true, false, database.ErrNotLocked, func() error { + return r.client.Del(context.Background(), r.config.LockKey).Err() + }) +} + +func (r *Redis) Run(migration io.Reader) error { + script, err := io.ReadAll(migration) + if err != nil { + return err + } + + if err = r.client.Eval(context.Background(), string(script), nil).Err(); err != nil { + return fmt.Errorf("migration failed: %w", err) + } + + return nil +} + +func (r *Redis) SetVersion(version int, dirty bool) error { + if version > 0 || (version == database.NilVersion && dirty) { + return r.client.HMSet(context.Background(), r.config.MigrationsKey, "version", version, "dirty", dirty).Err() + } + + return r.client.Del(context.Background(), r.config.MigrationsKey).Err() +} + +func (r *Redis) Version() (version int, dirty bool, err error) { + result, err := r.client.HMGet(context.Background(), r.config.MigrationsKey, "version", "dirty").Result() + if err != nil { + return 0, false, err + } + + return convertVersionFromDB(result) +} + +func (r *Redis) Drop() error { + return r.client.FlushDB(context.Background()).Err() +} diff --git a/database/redis/redis_test.go b/database/redis/redis_test.go new file mode 100644 index 000000000..b555dc6cd --- /dev/null +++ b/database/redis/redis_test.go @@ -0,0 +1,259 @@ +package redis + +import ( + "context" + "fmt" + "github.com/dhui/dktest" + "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" + "github.com/redis/go-redis/v9" + "log" + "net" + "strings" + "testing" +) + +const ( + redisPassword = "password" +) + +var ( + opts = dktest.Options{ + Env: map[string]string{"REDIS_PASSWORD": redisPassword}, + PortRequired: true, + ReadyFunc: isReady, + } + specs = []dktesting.ContainerSpec{ + {ImageName: "bitnami/redis:6.2", Options: opts}, + {ImageName: "bitnami/redis:7.4", Options: opts}, + } +) + +func redisConnectionString(host, port string, options ...string) string { + return fmt.Sprintf("redis://:%s@%s/0?%s", redisPassword, net.JoinHostPort(host, port), strings.Join(options, "&")) +} + +func isReady(ctx context.Context, c dktest.ContainerInfo) bool { + ip, port, err := c.FirstPort() + if err != nil { + return false + } + + client := redis.NewClient(&redis.Options{ + Network: "tcp", + Addr: net.JoinHostPort(ip, port), + Password: redisPassword, + ContextTimeoutEnabled: true, + DisableIndentity: true, + }) + + defer func() { + if err := client.Close(); err != nil { + log.Println("close error:", err) + } + }() + + err = client.Ping(ctx).Err() + + return err == nil +} + +func Test(t *testing.T) { + t.Run("test", test) + t.Run("testMigrate", testMigrate) + t.Run("testErrorParsing", testErrorParsing) + t.Run("testFilterCustomQuery", testFilterCustomQuery) + t.Run("testMigrationsKeyOption", testMigrationsKeyOption) + t.Run("testRedisLock", testRedisLock) + + t.Cleanup(func() { + for _, spec := range specs { + t.Log("Cleaning up ", spec.ImageName) + if err := spec.Cleanup(); err != nil { + t.Error("Error removing ", spec.ImageName, "error:", 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 := redisConnectionString(ip, port) + r := &Redis{} + d, err := r.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + dt.Test(t, d, []byte("return 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 := redisConnectionString(ip, port) + r := &Redis{} + d, err := r.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", "redis", d) + if err != nil { + t.Fatal(err) + } + dt.TestMigrate(t, m) + }) +} + +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 := redisConnectionString(ip, port) + p := &Redis{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + + wantErrSubstring := "Script attempted to access nonexistent global variable 'asdad'" + if err := d.Run(strings.NewReader("return asdad")); err == nil { + t.Fatal("expected err but got nil") + } else if !strings.Contains(err.Error(), "Script attempted to access nonexistent global variable 'asdad'") { + t.Fatalf("expected substring '%s' but got '%s'", wantErrSubstring, 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 := redisConnectionString(ip, port, "x-custom=foobar") + r := &Redis{} + d, err := r.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + }) +} + +func testMigrationsKeyOption(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) + } + + r := &Redis{} + + migrationsKey := "my_migrations" + + // good quoted x-migrations-table parameter + d, err := r.Open(redisConnectionString(ip, port, fmt.Sprintf("x-migrations-key=%s", migrationsKey))) + if err != nil { + t.Fatal(err) + } + + defer func() { + if err := d.Close(); err != nil { + t.Fatal(err) + } + }() + + m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "redis", d) + if err != nil { + t.Fatal(err) + } + + if err = m.Up(); err != nil { + t.Fatal(err) + } + + // NOTE: redis create migrations hash during first migration automatically. + existsCount, err := d.(*Redis).client.Exists(context.Background(), migrationsKey).Result() + if err != nil { + t.Fatal(err) + } + if existsCount == 0 { + t.Fatalf("expected key %s not exist", migrationsKey) + } + }) +} + +func testRedisLock(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 := redisConnectionString(ip, port) + p := &Redis{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + + dt.Test(t, d, []byte("return 1")) + + ps := d.(*Redis) + + err = ps.Lock() + if err != nil { + t.Fatal(err) + } + + err = ps.Unlock() + if err != nil { + t.Fatal(err) + } + + err = ps.Lock() + if err != nil { + t.Fatal(err) + } + + err = ps.Unlock() + if err != nil { + t.Fatal(err) + } + }) +} diff --git a/database/redis/uri.go b/database/redis/uri.go new file mode 100644 index 000000000..b9c976ff3 --- /dev/null +++ b/database/redis/uri.go @@ -0,0 +1,157 @@ +package redis + +import ( + "errors" + "fmt" + "github.com/redis/go-redis/v9" + neturl "net/url" +) + +func queryContains(values neturl.Values, queries []string) bool { + for _, query := range queries { + if values.Has(query) { + return true + } + } + + return false +} + +var failoverSpecificQueries = []string{ + "sentinel_addr", + "master_name", + "sentinel_username", + "sentinel_password", + "replica_only", + "use_disconnected_replicas", +} + +var clusterSpecificQueries = []string{ + "addr", + "max_redirects", + "read_only", + "route_by_latency", + "route_randomly", +} + +type queryOptions struct { + q neturl.Values + err error +} + +func (o *queryOptions) string(name string) string { + vs := o.q[name] + if len(vs) == 0 { + return "" + } + delete(o.q, name) // enable detection of unknown parameters + return vs[len(vs)-1] +} + +func (o *queryOptions) strings(name string) []string { + vs := o.q[name] + delete(o.q, name) + return vs +} + +func (o *queryOptions) bool(name string) bool { + switch s := o.string(name); s { + case "true", "1": + return true + case "false", "0", "": + return false + default: + if o.err == nil { + o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s) + } + return false + } +} + +func parseFailoverURL(url string) (*redis.FailoverOptions, error) { + u, err := neturl.Parse(url) + if err != nil { + return nil, err + } + + q := queryOptions{q: u.Query()} + + masterName := q.string("master_name") + sentinelAddrs := q.strings("sentinel_addr") + sentinelUsername := q.string("sentinel_username") + sentinelPassword := q.string("sentinel_password") + routeByLatency := q.bool("route_by_latency") + routeRandomly := q.bool("route_randomly") + replicaOnly := q.bool("replica_only") + useDisconnectedReplicas := q.bool("use_disconnected_replicas") + + if len(sentinelAddrs) == 0 { + return nil, errors.New("sentinel_addr is empty") + } + + if q.err != nil { + return nil, q.err + } + + u.RawQuery = q.q.Encode() + + options, err := redis.ParseURL(u.String()) + if err != nil { + return nil, err + } + + return &redis.FailoverOptions{ + MasterName: masterName, + SentinelAddrs: sentinelAddrs, + ClientName: options.ClientName, + SentinelUsername: sentinelUsername, + SentinelPassword: sentinelPassword, + RouteByLatency: routeByLatency, + RouteRandomly: routeRandomly, + ReplicaOnly: replicaOnly, + UseDisconnectedReplicas: useDisconnectedReplicas, + Dialer: options.Dialer, + OnConnect: options.OnConnect, + Protocol: options.Protocol, + Username: options.Username, + Password: options.Password, + DB: options.DB, + MaxRetries: options.MaxRetries, + MinRetryBackoff: options.MinRetryBackoff, + MaxRetryBackoff: options.MaxRetryBackoff, + DialTimeout: options.DialTimeout, + ReadTimeout: options.ReadTimeout, + WriteTimeout: options.WriteTimeout, + ContextTimeoutEnabled: options.ContextTimeoutEnabled, + PoolFIFO: options.PoolFIFO, + PoolSize: options.PoolSize, + PoolTimeout: options.PoolTimeout, + MinIdleConns: options.MinIdleConns, + MaxIdleConns: options.MaxIdleConns, + MaxActiveConns: options.MaxActiveConns, + ConnMaxIdleTime: options.ConnMaxIdleTime, + ConnMaxLifetime: options.ConnMaxLifetime, + TLSConfig: options.TLSConfig, + DisableIndentity: options.DisableIndentity, + IdentitySuffix: options.IdentitySuffix, + }, nil +} + +func determineMode(url string) (Mode, error) { + u, err := neturl.Parse(url) + if err != nil { + return ModeUnspecified, err + } + + values := u.Query() + + if queryContains(values, failoverSpecificQueries) { + return ModeFailover, nil + } + + if queryContains(values, clusterSpecificQueries) { + return ModeCluster, nil + } + + return ModeStandalone, nil +} diff --git a/database/redis/uri_test.go b/database/redis/uri_test.go new file mode 100644 index 000000000..a487ace62 --- /dev/null +++ b/database/redis/uri_test.go @@ -0,0 +1,27 @@ +package redis + +import ( + "github.com/redis/go-redis/v9" + "reflect" + "testing" +) + +func TestParseFailoverURL(t *testing.T) { + t.Parallel() + + url := `redis://:password@?sentinel_addr=sentinel_host:26379&master_name=mymaster` + expectedOptions := &redis.FailoverOptions{ + SentinelAddrs: []string{"sentinel_host:26379"}, + Password: "password", + MasterName: "mymaster", + } + + actualOptions, err := parseFailoverURL(url) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actualOptions, expectedOptions) { + t.Fatalf("expected %+v,\ngot %+v", expectedOptions, actualOptions) + } +} diff --git a/go.mod b/go.mod index 851054ffd..cf6bb2668 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( 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/redis/go-redis/v9 v9.6.1 github.com/snowflakedb/gosnowflake v1.6.19 github.com/stretchr/testify v1.9.0 github.com/xanzy/go-gitlab v0.15.0 @@ -45,29 +46,6 @@ require ( modernc.org/sqlite v1.18.1 ) -require ( - github.com/distribution/reference v0.6.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect -) - require ( cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/compute v1.25.1 // indirect @@ -108,13 +86,18 @@ require ( 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect github.com/envoyproxy/go-control-plane v0.12.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect @@ -142,19 +125,25 @@ require ( github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // 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/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.15.11 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.6 // 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/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // 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 @@ -166,9 +155,11 @@ require ( 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/rogpeppe/go-internal v1.12.0 // indirect github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79 github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect @@ -176,6 +167,14 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/mod v0.21.0 // indirect diff --git a/go.sum b/go.sum index 846e435b8..9e5b85077 100644 --- a/go.sum +++ b/go.sum @@ -121,6 +121,10 @@ github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAK github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -153,6 +157,8 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -526,6 +532,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= diff --git a/internal/cli/build_redis.go b/internal/cli/build_redis.go new file mode 100644 index 000000000..923aa331d --- /dev/null +++ b/internal/cli/build_redis.go @@ -0,0 +1,8 @@ +//go:build redis +// +build redis + +package cli + +import ( + _ "github.com/golang-migrate/migrate/v4/database/redis" +) diff --git a/internal/cli/main.go b/internal/cli/main.go index c7a3bd74a..8b4a9ad8d 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/signal" + "sort" "strconv" "strings" "syscall" @@ -69,6 +70,9 @@ func Main(version string) { databasePtr := flag.String("database", "", "") sourcePtr := flag.String("source", "", "") + databases := database.List() + sort.Strings(databases) + flag.Usage = func() { fmt.Fprintf(os.Stderr, `Usage: migrate OPTIONS COMMAND [arg...] @@ -94,7 +98,7 @@ Commands: version Print current migration version Source drivers: `+strings.Join(source.List(), ", ")+` -Database drivers: `+strings.Join(database.List(), ", ")+"\n", createUsage, gotoUsage, upUsage, downUsage, dropUsage, forceUsage) +Database drivers: `+strings.Join(databases, ", ")+"\n", createUsage, gotoUsage, upUsage, downUsage, dropUsage, forceUsage) } flag.Parse()