From 2dc78733a1e0d628d9ecfed78f09767a2d80de63 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 12 Jun 2026 15:46:03 +0700 Subject: [PATCH] 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. --- cmd/migrate-stock-key/main.go | 121 ----------------------------- cmd/migrate-stock-key/main_test.go | 76 ------------------ 2 files changed, 197 deletions(-) delete mode 100644 cmd/migrate-stock-key/main.go delete mode 100644 cmd/migrate-stock-key/main_test.go diff --git a/cmd/migrate-stock-key/main.go b/cmd/migrate-stock-key/main.go deleted file mode 100644 index d51f85a..0000000 --- a/cmd/migrate-stock-key/main.go +++ /dev/null @@ -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 -} diff --git a/cmd/migrate-stock-key/main_test.go b/cmd/migrate-stock-key/main_test.go deleted file mode 100644 index 1e0b7d6..0000000 --- a/cmd/migrate-stock-key/main_test.go +++ /dev/null @@ -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) -}