diff --git a/dbPrompt.go b/dbPrompt.go new file mode 100644 index 0000000..4560d2d --- /dev/null +++ b/dbPrompt.go @@ -0,0 +1,211 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +// Config struct for database connection +type Config struct { + Username string `json:"username"` + Password string `json:"password"` + Hostname string `json:"hostname"` + Port int `json:"port"` + Database string `json:"database"` +} + +// QueryRequest from the frontend +type QueryRequest struct { + ID string `json:"id"` + Query string `json:"query"` +} + +// QueryResult represents a single historical query execution +type QueryResult struct { + ID string `json:"id"` + Query string `json:"query"` + Timestamp int64 `json:"timestamp"` + Error string `json:"error,omitempty"` + Columns []string `json:"columns,omitempty"` + Rows [][]interface{} `json:"rows,omitempty"` + RowsAffected int64 `json:"rowsAffected,omitempty"` +} + +var db *sql.DB +var historyDir = "./history" + +func main() { + // Load configuration + file, err := os.Open("config.json") + if err != nil { + log.Fatalf("Failed to open config file: %v", err) + } + defer file.Close() + + var config Config + decoder := json.NewDecoder(file) + if err := decoder.Decode(&config); err != nil { + log.Fatalf("Failed to decode config: %v", err) + } + + // Connect to the database + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", + config.Username, config.Password, config.Hostname, config.Port, config.Database) + db, err = sql.Open("mysql", dsn) + if err != nil { + log.Fatalf("Failed to open database connection: %v", err) + } + defer db.Close() + + if err := db.Ping(); err != nil { + log.Fatalf("Failed to connect to the database: %v", err) + } + + log.Println("Successfully connected to the database.") + + // Ensure history directory exists + if _, err := os.Stat(historyDir); os.IsNotExist(err) { + os.Mkdir(historyDir, 0755) + } + + // HTTP Handlers + http.HandleFunc("/", serveIndex) + http.HandleFunc("/query", handleQuery) + http.HandleFunc("/history", handleHistory) + + log.Println("Starting server on :8080...") + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatalf("Server failed: %v", err) + } +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "index.html") +} + +func handleHistory(w http.ResponseWriter, r *http.Request) { + files, err := ioutil.ReadDir(historyDir) + if err != nil { + http.Error(w, "Could not read history", http.StatusInternalServerError) + return + } + + var history []QueryResult + for _, file := range files { + if filepath.Ext(file.Name()) == ".json" { + data, err := ioutil.ReadFile(filepath.Join(historyDir, file.Name())) + if err != nil { + continue + } + var qr QueryResult + if err := json.Unmarshal(data, &qr); err == nil { + history = append(history, qr) + } + } + } + + // Sort by timestamp, oldest first + sort.Slice(history, func(i, j int) bool { + return history[i].Timestamp < history[j].Timestamp + }) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(history) +} + +func handleQuery(w http.ResponseWriter, r *http.Request) { + var req QueryRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + query := strings.TrimSpace(req.Query) + isSelect := strings.HasPrefix(strings.ToLower(query), "select") || strings.HasPrefix(strings.ToLower(query), "show") || strings.HasPrefix(strings.ToLower(query), "describe") + + result := QueryResult{ + ID: req.ID, + Query: req.Query, + Timestamp: time.Now().UnixNano(), + } + + // Generate new ID if it's a new query + if result.ID == "" { + result.ID = strconv.FormatInt(result.Timestamp, 10) + } + + if isSelect { + // It's a query that returns rows + rows, err := db.Query(query) + if err != nil { + result.Error = err.Error() + } else { + defer rows.Close() + cols, _ := rows.Columns() + result.Columns = cols + + for rows.Next() { + // Create a slice of interface{}'s to represent a row + columns := make([]interface{}, len(cols)) + columnPointers := make([]interface{}, len(cols)) + for i := range columns { + columnPointers[i] = &columns[i] + } + + // Scan the result into the column pointers... + if err := rows.Scan(columnPointers...); err != nil { + result.Error = err.Error() + break + } + + // Convert byte slices to strings for better JSON representation + for i, col := range columns { + if b, ok := col.([]byte); ok { + columns[i] = string(b) + } + } + + result.Rows = append(result.Rows, columns) + } + if err := rows.Err(); err != nil { + result.Error = err.Error() + } + } + } else { + // It's a statement like INSERT, UPDATE, DELETE + res, err := db.Exec(query) + if err != nil { + result.Error = err.Error() + } else { + affected, err := res.RowsAffected() + if err != nil { + result.Error = err.Error() + } else { + result.RowsAffected = affected + } + } + } + + // Save to history + filePath := filepath.Join(historyDir, result.ID+".json") + fileData, _ := json.MarshalIndent(result, "", " ") + if err := ioutil.WriteFile(filePath, fileData, 0644); err != nil { + log.Printf("Failed to write history file %s: %v", filePath, err) + // Don't send a server error back to client, they can still see the result + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3698ead --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module dbPrompt + +go 1.23.2 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bcdcfa --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= diff --git a/index.html b/index.html new file mode 100644 index 0000000..a047ca0 --- /dev/null +++ b/index.html @@ -0,0 +1,173 @@ + + + + + + dbPrompt + + + + +
dbPrompt
+ +
+ +
+ + + +