// Debby is a command line tool that links your dolt database to visidata package main import ( "bufio" "fmt" "os" "os/exec" "strings" ) func main() { var choice string = "version" var table string var query string var args []string var noninteractive bool var err error var tables = ReadTableNames() if len(os.Args) > 1 { noninteractive = true choice = os.Args[1] switch choice { case "create": // Create table table = os.Args[2] case "edit": // Import table table = os.Args[2] case "run": // Run query query = strings.Join(os.Args[2:], " ") case "help": // Print help str := `debby is a command line tool that links your dolt database to visidata Usage: debby [command] [arguments] The commands are: create Create a new table edit Import table run Run query help Print help The create and edit commands require a table name as an argument. The run command requires a query as an argument. Runnning debby without any arguments will start debby in interactive mode. Running debby with any other arguments will run it as a dolt command.` fmt.Println(str) return default: // Run dolt command choice = "" args = os.Args[1:] } } // Menu loop. 1. Run query 2. Edit a table 3. Execute Dolt command 4. Exit for { // Execute choice switch choice { case "create": // Create table err = CreateTable(table) if err != nil { fmt.Println(err) } case "run": // Table name will be empty. Run query err = RunSQL(query, "") if err != nil { fmt.Println(err) } case "edit": // Run query err = RunSQL("select * from "+table+";", table) if err != nil { fmt.Println(err) } case "": // Execute command ExecuteDoltCommand(args...) case "exit", "quit": // Exit return } tables = ReadTableNames() // Refresh table names // If noninteractive, exit if noninteractive { return } // List tables sequentially fmt.Println() fmt.Println("Tables: ", strings.Join(tables, ", ")) fmt.Println() fmt.Println("[run] a query, [create] or [edit] a table, run any dolt command, or [exit]") fmt.Print("> ") // Read user input choicestr := Readline() choices := strings.Split(choicestr, " ") choice = choices[0] switch choice { case "create", "run", "edit", "exit", "quit": query = strings.Join(choices[1:], " ") if len(choices) > 1 { table = choices[1] } default: choice = "" } args = choices } } func Readline() string { // Read a line from stdin reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') rtext := strings.TrimRight(text, "\n") if text == rtext { return "quit" } return rtext } func ExecuteDoltCommand(args ...string) { var nargs []string var join bool // If an arg starts with a quote, start joining everything to the last element until an arg ends with a quote for _, arg := range args { if join { if strings.HasSuffix(arg, "\"") { join = false arg = strings.TrimSuffix(arg, "\"") } nargs[len(nargs)-1] += " " + arg } else { if strings.HasPrefix(arg, "\"") { join = true arg = strings.TrimPrefix(arg, "\"") } nargs = append(nargs, arg) } } // Run dolt command cmd := exec.Command("dolt", nargs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin cmd.Run() } func SaveToTable(table, file string) error { // Run dolt table import -r table file cmd := exec.Command("dolt", "table", "import", "-r", table, file) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin return cmd.Run() } func CreateTable(table string) error { // Create a temporary CSV file file, err := os.CreateTemp("", "debby-*.csv") if err != nil { return err } defer os.Remove(file.Name()) // Create a temporary SQL file for the schema of the table schemafile, err := os.CreateTemp("", "debby-*.sql") if err != nil { return err } defer os.Remove(schemafile.Name()) // Create dummy contents in the CSV file _, err = file.WriteString("a\n1\n") if err != nil { return err } // Open the CSV file in visidata err = OpenVisidata(file.Name()) if err != nil { return err } // Run dolt schema import -c --pks id table CSVfile --dry-run > schemafile cmd := exec.Command("dolt", "schema", "import", "-c", "--pks", "id", table, file.Name(), "--dry-run") cmd.Stdout = schemafile cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err = cmd.Run() if err != nil { return err } // Open the SQL file in the default editor, fall back to vim if not set editor := os.Getenv("EDITOR") if editor == "" { editor = "vim" } cmd = exec.Command(editor, schemafile.Name()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err = cmd.Run() if err != nil { return err } // Run the SQL file to create the table cmd = exec.Command("dolt", "sql", "-f", schemafile.Name()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err = cmd.Run() if err != nil { return err } // Do the normal replacing import err = SaveToTable(table, file.Name()) if err != nil { return err } return nil } func ReadTableNames() []string { // Run dolt ls, ignore the first line and strip spaces for each following line cmd := exec.Command("dolt", "ls") cmd.Stderr = nil out, err := cmd.Output() if err != nil { fmt.Println("Error reading table names:", err) fmt.Println("It seems like you are not in a dolt repository.") os.Exit(1) } lines := strings.Split(string(out), "\n") tables := []string{} for i, line := range lines { line = strings.TrimSpace(line) if i == 0 || line == "" { continue } tables = append(tables, line) } return tables } func RunSQL(query, table string) error { // Create a temporary file to open in visidata f, err := os.CreateTemp("", "debby-*.csv") if err != nil { return err } defer os.Remove(f.Name()) // Run the query and write the output to the file cmd := exec.Command("dolt", "sql", "-q", query, "-r", "csv") cmd.Stdout = f err = cmd.Run() if err != nil { return err } // Open the file in visidata err = OpenVisidata(f.Name(), "-f", "csv") if err != nil { return err } // If table is not empty, save the file to the table if table != "" { fmt.Println("Saving to table...") err = SaveToTable(table, f.Name()) if err != nil { return err } } // Read the file back into memory return nil } func OpenVisidata(args ...string) error { cmd := exec.Command("visidata", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin return cmd.Run() }