diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..6b8b59e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,62 @@ +# Gemini Project Analysis: dbPrompt + +## Overview + +The `dbPrompt` project is a web-based application that provides a user interface for executing SQL queries against a MySQL database. It keeps a history of all executed queries and their results, which are displayed on the main page. + +## Project Structure + +- `dbPrompt.go`: The main application file, written in Go. It contains the web server and all the backend logic. +- `index.html`: A single HTML file that contains the entire frontend UI, including CSS and JavaScript. +- `go.mod` / `go.sum`: Go module files that define the project's dependencies. The only external dependency is the MySQL driver for Go. +- `config.json`: A configuration file for the database connection details (hostname, username, password, etc.). +- `history/`: A directory where the history of executed queries is stored as JSON files. +- `README.md`: The project's README file. +- `LICENSE`: The project's license file. + +## Backend (Go) + +The backend is a simple web server built with the standard `net/http` package in Go. + +- **Database Connection**: It connects to a MySQL database using the `go-sql-driver/mysql` driver. Connection parameters are read from `config.json`. +- **Endpoints**: + - `GET /`: Serves the `index.html` file. + - `POST /query`: Receives a SQL query from the frontend, executes it on the database, and returns the result as JSON. It handles both queries that return rows (e.g., `SELECT`) and statements that modify data (e.g., `INSERT`, `UPDATE`). Each executed query and its result are saved to a JSON file in the `history/` directory. + - `DELETE /history/{id}`: Deletes a specific query history item (JSON file) based on its ID from the `history/` directory. + - `GET /history/`: Reads all query history files from the `history/` directory, sorts them chronologically, and returns them as a single JSON array. +- **Query Execution Metadata**: The backend now captures and includes the `duration` (in milliseconds) of each query execution in the `QueryResult` object before saving it to history and sending it to the frontend. + +## Frontend (HTML, CSS, JavaScript) + +The frontend is a single-page application contained entirely within `index.html`. + +- **User Interface**: The UI is composed of "query blocks". Each block contains a textarea for writing a SQL query, a "Run Query" button, and a result area. +- **History**: On page load, the application fetches the query history from the `/history/` endpoint and populates the page with query blocks for each historical query. One empty query block is always present for entering new queries. +- **Interaction**: + - Users can type a SQL query into a textarea and click "Run Query". + - The JavaScript code sends the query to the `/query` endpoint. + - The result (a table of data, a message with rows affected, or an error) is displayed in the result area of the corresponding query block. + - Successfully running a query in the "new query" block will automatically add a new empty block below it and equip the executed block with a "Delete" button. +- **Delete Functionality**: + - Historical query blocks now include a "Delete" button. + - When clicked, a confirmation dialog appears. If confirmed, a `DELETE` request is sent to the backend (`/history/{id}`), and the block is removed from the UI. + - The designated "new query" block (for new input) does not initially have a delete button. +- **Query Result Collapsibility**: + - Tabular query results are now wrapped in a collapsible section, allowing users to expand and collapse them. + - "Collapse All" and "Expand All" buttons have been added to the header, enabling users to manage the visibility of all result tables simultaneously. + - By default, all result tables are expanded. +- **Query Execution Metadata Display**: Each query result now displays the exact date and time of execution and its processing duration (in milliseconds). + +## How it Works + +1. The user opens the web page, and the browser loads `index.html`. +2. The JavaScript in `index.html` makes a `GET` request to `/history/` to load past queries. +3. The Go backend reads the JSON files from the `history/` directory and returns them. +4. The frontend dynamically creates a "query block" for each past query (with a delete button) and one new empty block (without a delete button). +5. The user enters a new SQL query into the designated "new query" block and clicks "Run Query". +6. The frontend sends a `POST` request to `/query` with the query string. +7. The Go backend executes the query against the MySQL database, measures its duration, and saves the query, result, timestamp, and duration to a new JSON file in the `history/` directory. +8. The backend returns the result (including duration) to the frontend. +9. The frontend displays the result in the corresponding query block, along with the execution date/time and duration. If it was a "new query" block, it gets a delete button, and a new empty "new query" block is added below it. +10. Users can click the "Delete" button on historical queries to remove them. +11. Users can collapse/expand individual result tables or use the global "Collapse All" / "Expand All" buttons. \ No newline at end of file diff --git a/dbPrompt.go b/dbPrompt.go index ffd74be..6378574 100644 --- a/dbPrompt.go +++ b/dbPrompt.go @@ -37,6 +37,7 @@ type QueryResult struct { ID string `json:"id"` Query string `json:"query"` Timestamp int64 `json:"timestamp"` + Duration int64 `json:"duration,omitempty"` // Duration in milliseconds Error string `json:"error,omitempty"` Columns []string `json:"columns,omitempty"` Rows [][]interface{} `json:"rows,omitempty"` @@ -83,7 +84,7 @@ func main() { // HTTP Handlers http.HandleFunc("/", serveIndex) http.HandleFunc("/query", handleQuery) - http.HandleFunc("/history", handleHistory) + http.HandleFunc("/history/", handleHistory) log.Println("Starting server on :8080...") if err := http.ListenAndServe(":8080", nil); err != nil { @@ -96,33 +97,68 @@ func serveIndex(w http.ResponseWriter, r *http.Request) { } 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 - } + switch r.Method { + case http.MethodGet: + 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) + 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) + + case http.MethodDelete: + id := strings.TrimPrefix(r.URL.Path, "/history/") + if id == "" { + http.Error(w, "History ID is required", http.StatusBadRequest) + return + } + + // Basic sanitization to prevent directory traversal + cleanID := filepath.Base(id) + if cleanID != id || strings.Contains(cleanID, "..") { + http.Error(w, "Invalid history ID", http.StatusBadRequest) + return + } + + filePath := filepath.Join(historyDir, cleanID+".json") + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + http.Error(w, "History item not found", http.StatusNotFound) + return + } + + if err := os.Remove(filePath); err != nil { + log.Printf("Failed to delete history file %s: %v", filePath, err) + http.Error(w, "Failed to delete history item", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } - - // 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) { @@ -133,7 +169,8 @@ func handleQuery(w http.ResponseWriter, r *http.Request) { } query := strings.TrimSpace(req.Query) - isSelect := strings.HasPrefix(strings.ToLower(query), "select") || strings.HasPrefix(strings.ToLower(query), "show") || strings.HasPrefix(strings.ToLower(query), "describe") + query_lower := strings.ToLower(query) + isSelect := strings.HasPrefix(query_lower, "select") || strings.HasPrefix(query_lower, "show") || strings.HasPrefix(query_lower, "describe") || strings.HasPrefix(query_lower, "desc") result := QueryResult{ ID: req.ID, @@ -146,6 +183,8 @@ func handleQuery(w http.ResponseWriter, r *http.Request) { result.ID = strconv.FormatInt(result.Timestamp, 10) } + startTime := time.Now() + if isSelect { // It's a query that returns rows rows, err := db.Query(query) @@ -198,6 +237,8 @@ func handleQuery(w http.ResponseWriter, r *http.Request) { } } + result.Duration = time.Since(startTime).Milliseconds() + // Save to history filePath := filepath.Join(historyDir, result.ID+".json") fileData, _ := json.MarshalIndent(result, "", " ") diff --git a/index.html b/index.html index a047ca0..46d1de1 100644 --- a/index.html +++ b/index.html @@ -6,9 +6,23 @@ dbPrompt -
dbPrompt
+
+ dbPrompt +
+ + +
+
@@ -36,11 +93,37 @@ +