Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfb360a

Browse files
committed
feat(editor): explain
1 parent 7873cb7 commit bfb360a

6 files changed

Lines changed: 122 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Press **space** to open the palette, then the **second** key (e.g. **space** the
206206
| Panel | Commands |
207207
| -------- | --------------------------------------------------------------------------------------------------------------------- |
208208
| Explorer | `n` add connection, `e` edit, `d` delete, `R` refresh, `t` toggle explorer, `a` toggle AI pane, `f` fullscreen |
209-
| Editor | `x` execute, `c` clear, `D` close tab (confirm), `t` toggle explorer, `a` toggle AI pane, `f` fullscreen |
209+
| Editor | `x` execute, `e` explain, `c` clear, `D` close tab (confirm), `t` toggle explorer, `a` toggle AI pane, `f` fullscreen |
210210
| Results | `y` copy cell, `Y` copy row, `e` export CSV, `j` export JSON, `t` toggle explorer, `a` toggle AI pane, `f` fullscreen |
211211
| AI | `t` toggle explorer, `a` toggle AI pane, `f` fullscreen |
212212

internal/app/app.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
678678
}
679679
return m, tea.Batch(cmds...)
680680

681+
case explainQueryFromPaletteMsg:
682+
drv := ""
683+
if c := m.findConn(m.activeConnID); c != nil {
684+
drv = c.Driver
685+
}
686+
if m.editor.WrapCurrentQueryExplain(drv) {
687+
m.persistEditorDraft()
688+
}
689+
return m, nil
690+
681691
case clearEditorMsg:
682692
m.editor.ClearUndoable()
683693
m.persistEditorDraft()
@@ -1201,6 +1211,7 @@ func (m *Model) openPalette() {
12011211
title = "Editor Commands"
12021212
commands = []cmdpalette.Command{
12031213
{Key: "x", Description: "Execute query (enter)", Action: func() tea.Msg { return execQueryFromPaletteMsg{} }},
1214+
{Key: "e", Description: "Explain current query", Action: func() tea.Msg { return explainQueryFromPaletteMsg{} }},
12041215
{Key: "c", Description: "Clear editor", Action: func() tea.Msg { return clearEditorMsg{} }},
12051216
{Key: "D", Description: "Close tab (confirm)", Action: func() tea.Msg { return closeTabPromptMsg{} }},
12061217
{Key: "t", Description: "Toggle explorer", Action: func() tea.Msg { return toggleExplorerMsg{} }},
@@ -1658,6 +1669,7 @@ const helpScreenText = `
16581669
r Focus results
16591670
a Focus AI pane (shows it if hidden; does not hide — use palette)
16601671
space Open command palette (context-aware)
1672+
space+e Editor palette: wrap query for EXPLAIN (driver-specific)
16611673
space+f Toggle fullscreen for current panel
16621674
? Toggle this help
16631675
@@ -1726,7 +1738,7 @@ const helpScreenText = `
17261738
17271739
COMMAND PALETTE (space, then key)
17281740
Explorer: n=add connection e=edit d=delete R=refresh t=toggle explorer a=toggle AI pane f=fullscreen
1729-
Editor: x=execute c=clear D=close tab (popup: y/enter · n esc q) t=toggle explorer a=toggle AI pane f=fullscreen
1741+
Editor: x=execute e=explain c=clear D=close tab (popup: y/enter · n esc q) t=toggle explorer a=toggle AI pane f=fullscreen
17301742
Results: y=copy cell Y=copy row e=export CSV j=export JSON t=toggle explorer a=toggle AI pane f=fullscreen
17311743
AI: t=toggle explorer a=toggle AI pane f=fullscreen
17321744
`
@@ -1966,7 +1978,8 @@ func (m *Model) execQueryCmd(query string) tea.Cmd {
19661978
var result *db.QueryResult
19671979
if strings.HasPrefix(trimmed, "SELECT") || strings.HasPrefix(trimmed, "WITH") ||
19681980
strings.HasPrefix(trimmed, "SHOW") || strings.HasPrefix(trimmed, "EXPLAIN") ||
1969-
strings.HasPrefix(trimmed, "DESCRIBE") || strings.HasPrefix(trimmed, "DESC") {
1981+
strings.HasPrefix(trimmed, "DESCRIBE") || strings.HasPrefix(trimmed, "DESC") ||
1982+
strings.HasPrefix(trimmed, "SET SHOWPLAN_ALL ON") {
19701983
result, err = driver.Query(ctx, dbName, query)
19711984
} else {
19721985
result, err = driver.Exec(ctx, dbName, query)

internal/app/messages.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ type refreshSchemaMsg struct{}
7979
// execQueryFromPaletteMsg executes the current query from the palette.
8080
type execQueryFromPaletteMsg struct{}
8181

82+
// explainQueryFromPaletteMsg wraps the current query for EXPLAIN via the editor palette.
83+
type explainQueryFromPaletteMsg struct{}
84+
8285
// clearEditorMsg clears the current editor buffer.
8386
type clearEditorMsg struct{}
8487

internal/sqlutil/explain.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package sqlutil
2+
3+
import "strings"
4+
5+
// WrapQueryForExplain returns SQL wrapped so the engine returns a query plan instead of
6+
// (or in addition to) normal results. Returns ok false when query is empty or already
7+
// appears to request a plan.
8+
func WrapQueryForExplain(driver, query string) (wrapped string, ok bool) {
9+
query = strings.TrimSpace(query)
10+
if query == "" {
11+
return "", false
12+
}
13+
if queryAlreadyExplained(driver, query) {
14+
return "", false
15+
}
16+
d := strings.ToLower(strings.TrimSpace(driver))
17+
switch d {
18+
case "sqlite", "sqlite3":
19+
return "EXPLAIN QUERY PLAN " + query, true
20+
case "mssql", "sqlserver":
21+
return "SET SHOWPLAN_ALL ON;\n" + query + "\nSET SHOWPLAN_ALL OFF", true
22+
default:
23+
// postgres, mysql, and unknown drivers
24+
return "EXPLAIN " + query, true
25+
}
26+
}
27+
28+
func queryAlreadyExplained(driver, query string) bool {
29+
t := strings.TrimSpace(strings.ToUpper(query))
30+
if strings.HasPrefix(t, "EXPLAIN") {
31+
return true
32+
}
33+
d := strings.ToLower(strings.TrimSpace(driver))
34+
if d == "mssql" || d == "sqlserver" {
35+
if strings.Contains(t, "SET SHOWPLAN_ALL ON") {
36+
return true
37+
}
38+
}
39+
return false
40+
}

internal/sqlutil/explain_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package sqlutil
2+
3+
import "testing"
4+
5+
func TestWrapQueryForExplain(t *testing.T) {
6+
tests := []struct {
7+
driver string
8+
in string
9+
want string
10+
wantOK bool
11+
skipWant bool // only check ok == false
12+
}{
13+
{"postgres", "SELECT 1", "EXPLAIN SELECT 1", true, false},
14+
{"mysql", "SELECT 1", "EXPLAIN SELECT 1", true, false},
15+
{"sqlite", "SELECT 1", "EXPLAIN QUERY PLAN SELECT 1", true, false},
16+
{"mssql", "SELECT 1", "SET SHOWPLAN_ALL ON;\nSELECT 1\nSET SHOWPLAN_ALL OFF", true, false},
17+
{"postgres", " SELECT 1 ", "EXPLAIN SELECT 1", true, false},
18+
{"postgres", "", "", false, true},
19+
{"postgres", " \n\t ", "", false, true},
20+
{"postgres", "EXPLAIN SELECT 1", "", false, true},
21+
{"sqlite", "EXPLAIN QUERY PLAN SELECT 1", "", false, true},
22+
{"mssql", "SET SHOWPLAN_ALL ON;\nSELECT 1", "", false, true},
23+
}
24+
for _, tt := range tests {
25+
got, ok := WrapQueryForExplain(tt.driver, tt.in)
26+
if ok != tt.wantOK {
27+
t.Errorf("WrapQueryForExplain(%q, %q) ok=%v want ok=%v", tt.driver, tt.in, ok, tt.wantOK)
28+
continue
29+
}
30+
if !tt.skipWant && got != tt.want {
31+
t.Errorf("WrapQueryForExplain(%q, %q) = %q want %q", tt.driver, tt.in, got, tt.want)
32+
}
33+
}
34+
}

internal/ui/editor/editor.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/charmbracelet/x/ansi"
1111
"github.com/charmbracelet/x/cellbuf"
1212

13+
"github.com/robertn/dbx/internal/sqlutil"
1314
"github.com/robertn/dbx/internal/ui/theme"
1415
"github.com/robertn/dbx/internal/util"
1516
)
@@ -455,6 +456,34 @@ func (m Model) CurrentQuery() string {
455456
return m.currentQuery(m.lines())
456457
}
457458

459+
// WrapCurrentQueryExplain replaces the current query block with a driver-specific EXPLAIN
460+
// (or equivalent) form. Returns false when there is no query under the cursor, the block is
461+
// empty, or the text already looks like an explain request.
462+
func (m *Model) WrapCurrentQueryExplain(driver string) bool {
463+
lines := m.lines()
464+
start, end := m.currentQueryBounds(lines)
465+
if start > end {
466+
return false
467+
}
468+
old := strings.TrimSpace(strings.Join(lines[start:end+1], "\n"))
469+
newQ, ok := sqlutil.WrapQueryForExplain(driver, old)
470+
if !ok {
471+
return false
472+
}
473+
m.pushUndoPoint()
474+
m.vim.mode = ModeNormal
475+
m.insertUndoSeeded = false
476+
chunk := strings.Split(newQ, "\n")
477+
lines = append(lines[:start], append(chunk, lines[end+1:]...)...)
478+
m.setLines(lines)
479+
m.vim.row = start
480+
m.vim.col = 0
481+
m.compVisible = false
482+
m.clampCursor()
483+
m.adjustScroll()
484+
return true
485+
}
486+
458487
// SetContent replaces the current tab content (e.g. when pressing 's' in explorer).
459488
func (m *Model) SetContent(content string) {
460489
m.clearTabUndo(tabStoreKey(m.connKey))

0 commit comments

Comments
 (0)