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:
2026-06-12 15:46:03 +07:00
parent 5f0f6f3fcd
commit 2dc78733a1
2 changed files with 0 additions and 197 deletions
-121
View File
@@ -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
}
-76
View File
@@ -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)
}