318 lines
7.9 KiB
Go
318 lines
7.9 KiB
Go
package nigiri
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type MigrationCommand struct {
|
|
Action string
|
|
Field string
|
|
To string
|
|
Type string
|
|
Store string
|
|
}
|
|
|
|
type MigrationHandler func(data []map[string]any, cmd *MigrationCommand) error
|
|
|
|
type Migrator struct {
|
|
handlers map[string]MigrationHandler
|
|
patterns map[string]*regexp.Regexp
|
|
}
|
|
|
|
// ============================================================================
|
|
// Core Migration Management
|
|
// ============================================================================
|
|
|
|
func NewMigrator() *Migrator {
|
|
m := &Migrator{
|
|
handlers: make(map[string]MigrationHandler),
|
|
patterns: make(map[string]*regexp.Regexp),
|
|
}
|
|
|
|
// Register built-in commands
|
|
m.RegisterCommand("rename", regexp.MustCompile(`^rename\s+(\w+)\s+to\s+(\w+)$`), m.handleRename)
|
|
m.RegisterCommand("add", regexp.MustCompile(`^add\s+(\w+)\s+(\w+)\s+to\s+(\w+)$`), m.handleAdd)
|
|
m.RegisterCommand("remove", regexp.MustCompile(`^remove\s+(\w+)\s+from\s+(\w+)$`), m.handleRemove)
|
|
m.RegisterCommand("change", regexp.MustCompile(`^change\s+(\w+)\s+to\s+(\w+)$`), m.handleChange)
|
|
|
|
return m
|
|
}
|
|
|
|
func (m *Migrator) RegisterCommand(name string, pattern *regexp.Regexp, handler MigrationHandler) {
|
|
m.patterns[name] = pattern
|
|
m.handlers[name] = handler
|
|
}
|
|
|
|
// ============================================================================
|
|
// Command Parsing
|
|
// ============================================================================
|
|
|
|
func (m *Migrator) ParseCommand(input string) (*MigrationCommand, string, error) {
|
|
input = strings.TrimSpace(input)
|
|
|
|
for name, pattern := range m.patterns {
|
|
if matches := pattern.FindStringSubmatch(input); matches != nil {
|
|
cmd := &MigrationCommand{Action: name}
|
|
|
|
switch name {
|
|
case "rename":
|
|
cmd.Field, cmd.To = matches[1], matches[2]
|
|
case "add":
|
|
cmd.Field, cmd.Type, cmd.Store = matches[1], matches[2], matches[3]
|
|
case "remove":
|
|
cmd.Field, cmd.Store = matches[1], matches[2]
|
|
case "change":
|
|
cmd.Field, cmd.Type = matches[1], matches[2]
|
|
default:
|
|
cmd.Field = matches[1]
|
|
if len(matches) > 2 {
|
|
cmd.To = matches[2]
|
|
}
|
|
if len(matches) > 3 {
|
|
cmd.Type = matches[3]
|
|
}
|
|
if len(matches) > 4 {
|
|
cmd.Store = matches[4]
|
|
}
|
|
}
|
|
|
|
return cmd, name, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("unknown command: %s", input)
|
|
}
|
|
|
|
func (m *Migrator) ApplyCommand(data []byte, cmdStr string) ([]byte, error) {
|
|
cmd, name, err := m.ParseCommand(cmdStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var items []map[string]any
|
|
if err := json.Unmarshal(data, &items); err != nil {
|
|
return nil, fmt.Errorf("invalid JSON: %w", err)
|
|
}
|
|
|
|
if err := m.handlers[name](items, cmd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return json.MarshalIndent(items, "", "\t")
|
|
}
|
|
|
|
// ============================================================================
|
|
// File Operations
|
|
// ============================================================================
|
|
|
|
func (m *Migrator) MigrateFile(filename, command string) error {
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("read file: %w", err)
|
|
}
|
|
|
|
result, err := m.ApplyCommand(data, command)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
backupPath := filename + ".backup"
|
|
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
|
return fmt.Errorf("create backup: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filename, result, 0644); err != nil {
|
|
return fmt.Errorf("write result: %w", err)
|
|
}
|
|
|
|
fmt.Printf("✓ Applied: %s\n", command)
|
|
fmt.Printf("✓ Backup: %s\n", backupPath)
|
|
return nil
|
|
}
|
|
|
|
func (m *Migrator) RunScript(dataDir, scriptFile string) error {
|
|
file, err := os.Open(scriptFile)
|
|
if err != nil {
|
|
return fmt.Errorf("open script: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
lineNum := 0
|
|
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("line %d: invalid format, use 'file.json: command'", lineNum)
|
|
}
|
|
|
|
filename := strings.TrimSpace(parts[0])
|
|
command := strings.TrimSpace(parts[1])
|
|
fullPath := filepath.Join(dataDir, filename)
|
|
|
|
fmt.Printf("Line %d: %s -> %s\n", lineNum, filename, command)
|
|
|
|
if err := m.MigrateFile(fullPath, command); err != nil {
|
|
return fmt.Errorf("line %d: %w", lineNum, err)
|
|
}
|
|
}
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
// ============================================================================
|
|
// Built-in Command Handlers
|
|
// ============================================================================
|
|
|
|
func (m *Migrator) handleRename(items []map[string]any, cmd *MigrationCommand) error {
|
|
for _, item := range items {
|
|
if val, exists := item[cmd.Field]; exists {
|
|
item[cmd.To] = val
|
|
delete(item, cmd.Field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Migrator) handleAdd(items []map[string]any, cmd *MigrationCommand) error {
|
|
defaultVal := getDefaultValue(cmd.Type)
|
|
for _, item := range items {
|
|
if _, exists := item[cmd.Field]; !exists {
|
|
item[cmd.Field] = defaultVal
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Migrator) handleRemove(items []map[string]any, cmd *MigrationCommand) error {
|
|
for _, item := range items {
|
|
delete(item, cmd.Field)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Migrator) handleChange(items []map[string]any, cmd *MigrationCommand) error {
|
|
for _, item := range items {
|
|
if val, exists := item[cmd.Field]; exists {
|
|
converted, err := convertType(val, cmd.Type)
|
|
if err != nil {
|
|
return fmt.Errorf("type conversion failed: %w", err)
|
|
}
|
|
item[cmd.Field] = converted
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Type Conversion Utilities
|
|
// ============================================================================
|
|
|
|
func getDefaultValue(fieldType string) any {
|
|
switch fieldType {
|
|
case "string":
|
|
return ""
|
|
case "int":
|
|
return 0
|
|
case "bool":
|
|
return false
|
|
case "time":
|
|
return time.Now().Format(time.RFC3339)
|
|
case "float":
|
|
return 0.0
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func convertType(val any, targetType string) (any, error) {
|
|
switch targetType {
|
|
case "string":
|
|
return fmt.Sprintf("%v", val), nil
|
|
case "int":
|
|
if str, ok := val.(string); ok {
|
|
return strconv.Atoi(str)
|
|
}
|
|
if f, ok := val.(float64); ok {
|
|
return int(f), nil
|
|
}
|
|
return val, nil
|
|
case "float":
|
|
if str, ok := val.(string); ok {
|
|
return strconv.ParseFloat(str, 64)
|
|
}
|
|
if i, ok := val.(int); ok {
|
|
return float64(i), nil
|
|
}
|
|
return val, nil
|
|
case "bool":
|
|
if str, ok := val.(string); ok {
|
|
return strconv.ParseBool(str)
|
|
}
|
|
return val, nil
|
|
default:
|
|
return val, nil
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI Interface
|
|
// ============================================================================
|
|
|
|
type MigrationCLI struct {
|
|
collection *Collection
|
|
}
|
|
|
|
func NewMigrationCLI(collection *Collection) *MigrationCLI {
|
|
return &MigrationCLI{collection: collection}
|
|
}
|
|
|
|
func (cli *MigrationCLI) Run(args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("usage: migrate <file.json> 'command' OR migrate <script.txt>")
|
|
}
|
|
|
|
if strings.HasSuffix(args[0], ".txt") {
|
|
return cli.collection.RunMigrationScript(args[0])
|
|
}
|
|
|
|
if len(args) < 2 {
|
|
return fmt.Errorf("usage: migrate <file.json> 'command'")
|
|
}
|
|
|
|
filename := args[0]
|
|
storeName := strings.TrimSuffix(filepath.Base(filename), ".json")
|
|
command := args[1]
|
|
|
|
return cli.collection.MigrateStore(storeName, command)
|
|
}
|
|
|
|
func (cli *MigrationCLI) PrintUsage() {
|
|
fmt.Println("Migration Commands:")
|
|
fmt.Println(" rename oldfield to newfield")
|
|
fmt.Println(" add fieldname type to store")
|
|
fmt.Println(" remove fieldname from store")
|
|
fmt.Println(" change fieldname to type")
|
|
fmt.Println()
|
|
fmt.Println("Types: string, int, float, bool, time")
|
|
fmt.Println()
|
|
fmt.Println("Usage:")
|
|
fmt.Println(" migrate users.json 'rename Name to FullName'")
|
|
fmt.Println(" migrate migrations/001_schema.txt")
|
|
}
|