mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-07-27 18:21:01 +00:00
chore: remove one-time stock-key migration tool
Migration complete and verified in production; the trading->stock DynamoDB partition copy is no longer needed and the old trading rows have been deleted.
This commit is contained in:
@@ -1,121 +0,0 @@
|
||||
// Command migrate-stock-key copies a module's DynamoDB partition to a new
|
||||
// partition key. It exists for the one-time "trading" -> "stock" module
|
||||
// rename: the framework derives the DynamoDB partition key (pk) from the
|
||||
// module name, so renaming the module orphans every row written under the old
|
||||
// pk. This tool re-homes those rows under the new pk.
|
||||
//
|
||||
// Behaviour:
|
||||
// - Reuses the runtime KVStore (List -> Get -> Put), so the stored JSON
|
||||
// bytes round-trip unchanged; only pk changes.
|
||||
// - Idempotent: a key already present in the destination partition is left
|
||||
// untouched, so a re-run never clobbers data the renamed module has
|
||||
// written since the first migration.
|
||||
// - Non-destructive: source rows are kept in place (free-tier cost is
|
||||
// negligible and they double as a rollback snapshot).
|
||||
//
|
||||
// Defaults to a dry run; pass -apply to perform writes.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// DYNAMODB_TABLE=miti99bot-prod go run ./cmd/migrate-stock-key # dry run
|
||||
// DYNAMODB_TABLE=miti99bot-prod go run ./cmd/migrate-stock-key -apply # migrate
|
||||
// DYNAMODB_LOCAL_URL=http://localhost:8001 ... (point at DynamoDB Local)
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
func main() {
|
||||
from := flag.String("from", "trading", "source DynamoDB partition key (old module name)")
|
||||
to := flag.String("to", "stock", "destination DynamoDB partition key (new module name)")
|
||||
apply := flag.Bool("apply", false, "perform writes; without it the tool only reports what would change")
|
||||
flag.Parse()
|
||||
|
||||
if err := run(context.Background(), *from, *to, *apply); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, from, to string, apply bool) error {
|
||||
table := os.Getenv("DYNAMODB_TABLE")
|
||||
if table == "" {
|
||||
return errors.New("DYNAMODB_TABLE is required")
|
||||
}
|
||||
if from == to {
|
||||
return fmt.Errorf("-from and -to must differ (both %q)", from)
|
||||
}
|
||||
|
||||
client, err := storage.NewDynamoDBClient(ctx, storage.DynamoDBEndpointFromEnv())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := storage.NewDynamoDBProvider(client, table)
|
||||
mode := "DRY RUN"
|
||||
if apply {
|
||||
mode = "APPLY"
|
||||
}
|
||||
fmt.Printf("[%s] table=%s %s -> %s\n", mode, table, from, to)
|
||||
return migrate(ctx, provider.For(from), provider.For(to), from, to, apply, os.Stdout)
|
||||
}
|
||||
|
||||
// migrate copies every key from src to dst, preserving the stored bytes.
|
||||
// Keys already present in dst are skipped (idempotent re-runs never clobber
|
||||
// fresher destination state). Source entries are never deleted. When apply is
|
||||
// false it reports the planned copies without writing. Decoupled from
|
||||
// DynamoDB/env so it is unit-testable against any KVStore pair (the in-memory
|
||||
// provider prefixes keys by module name exactly like DynamoDB partitions).
|
||||
func migrate(ctx context.Context, src, dst storage.KVStore, from, to string, apply bool, out io.Writer) error {
|
||||
// Progress lines are advisory; a failed write to stdout isn't actionable.
|
||||
logf := func(format string, a ...any) { _, _ = fmt.Fprintf(out, format, a...) }
|
||||
|
||||
keys, err := src.List(ctx, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("list source partition %q: %w", from, err)
|
||||
}
|
||||
logf(" %d source keys\n", len(keys))
|
||||
|
||||
var copied, skipped int
|
||||
for _, key := range keys {
|
||||
// Skip keys the destination already has — the renamed module may have
|
||||
// written fresher state; the migration must never overwrite it.
|
||||
if _, err := dst.Get(ctx, key); err == nil {
|
||||
logf(" skip %s (already in %s)\n", key, to)
|
||||
skipped++
|
||||
continue
|
||||
} else if !errors.Is(err, storage.ErrNotFound) {
|
||||
return fmt.Errorf("probe destination %s/%s: %w", to, key, err)
|
||||
}
|
||||
|
||||
if !apply {
|
||||
logf(" copy %s (would migrate)\n", key)
|
||||
copied++
|
||||
continue
|
||||
}
|
||||
|
||||
raw, err := src.Get(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source %s/%s: %w", from, key, err)
|
||||
}
|
||||
if err := dst.Put(ctx, key, raw); err != nil {
|
||||
return fmt.Errorf("write destination %s/%s: %w", to, key, err)
|
||||
}
|
||||
logf(" copy %s (%d bytes)\n", key, len(raw))
|
||||
copied++
|
||||
}
|
||||
|
||||
verb := "would copy"
|
||||
if apply {
|
||||
verb = "copied"
|
||||
}
|
||||
logf("done: %s %d, skipped %d (source rows left in place)\n", verb, copied, skipped)
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/tiennm99/miti99bot/internal/storage"
|
||||
)
|
||||
|
||||
// The in-memory provider key-prefixes by module name, mirroring how the
|
||||
// DynamoDB provider partitions by pk — so it faithfully exercises the
|
||||
// cross-partition copy without needing a live table.
|
||||
func TestMigrate_CopiesSkipsAndPreservesSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
provider := storage.NewMemoryProvider()
|
||||
src := provider.For("trading")
|
||||
dst := provider.For("stock")
|
||||
|
||||
mustPut(t, ctx, src, "user:7", `{"currency":{"VND":1500000}}`)
|
||||
mustPut(t, ctx, src, "user:42", `{"currency":{"VND":1}}`)
|
||||
mustPut(t, ctx, src, "sym:TCB", `{"symbol":"TCB"}`)
|
||||
// Destination already holds a *fresher* user:42 — migration must not clobber it.
|
||||
mustPut(t, ctx, dst, "user:42", `{"currency":{"VND":99999}}`)
|
||||
|
||||
// Dry run writes nothing new.
|
||||
var dry bytes.Buffer
|
||||
if err := migrate(ctx, src, dst, "trading", "stock", false, &dry); err != nil {
|
||||
t.Fatalf("dry run: %v", err)
|
||||
}
|
||||
if got := getString(t, ctx, dst, "user:42"); got != `{"currency":{"VND":99999}}` {
|
||||
t.Fatalf("dry run mutated dst user:42: %q", got)
|
||||
}
|
||||
if _, err := dst.Get(ctx, "user:7"); !errors.Is(err, storage.ErrNotFound) {
|
||||
t.Fatalf("dry run created dst user:7, want ErrNotFound, got %v", err)
|
||||
}
|
||||
|
||||
// Apply.
|
||||
var live bytes.Buffer
|
||||
if err := migrate(ctx, src, dst, "trading", "stock", true, &live); err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
|
||||
// New keys copied byte-for-byte.
|
||||
if got := getString(t, ctx, dst, "user:7"); got != `{"currency":{"VND":1500000}}` {
|
||||
t.Errorf("user:7 not copied correctly: %q", got)
|
||||
}
|
||||
if got := getString(t, ctx, dst, "sym:TCB"); got != `{"symbol":"TCB"}` {
|
||||
t.Errorf("sym:TCB not copied correctly: %q", got)
|
||||
}
|
||||
// Pre-existing destination key left untouched (skip, not overwrite).
|
||||
if got := getString(t, ctx, dst, "user:42"); got != `{"currency":{"VND":99999}}` {
|
||||
t.Errorf("user:42 was clobbered: %q", got)
|
||||
}
|
||||
// Source rows remain in place (rollback safety).
|
||||
if got := getString(t, ctx, src, "user:7"); got != `{"currency":{"VND":1500000}}` {
|
||||
t.Errorf("source user:7 was modified/removed: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func mustPut(t *testing.T, ctx context.Context, kv storage.KVStore, key, val string) {
|
||||
t.Helper()
|
||||
if err := kv.Put(ctx, key, []byte(val)); err != nil {
|
||||
t.Fatalf("put %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getString(t *testing.T, ctx context.Context, kv storage.KVStore, key string) string {
|
||||
t.Helper()
|
||||
raw, err := kv.Get(ctx, key)
|
||||
if err != nil {
|
||||
t.Fatalf("get %s: %v", key, err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user