From 694bbb7934871460d75a2b8dff0c77ab72edcd11 Mon Sep 17 00:00:00 2001 From: Pablu Date: Mon, 1 Dec 2025 16:36:04 +0100 Subject: [PATCH] Working select statements for know tables, with and without semi at the end --- cmd/sqv-tview/main.go | 117 ++++++++++++++++++++++++++++++++++-------- manager.go | 108 ++++++++++++++++++++++++++++++++++++++ sql/parser.go | 83 +++++++++++++++++++++++++++++- 3 files changed, 284 insertions(+), 24 deletions(-) diff --git a/cmd/sqv-tview/main.go b/cmd/sqv-tview/main.go index f6067bb..169aae6 100644 --- a/cmd/sqv-tview/main.go +++ b/cmd/sqv-tview/main.go @@ -11,18 +11,27 @@ import ( func main() { app := tview.NewApplication() - menu := tview.NewList() - table := tview.NewTable().SetBorders(true) - table.SetBorder(true).SetTitle("TABLE") + menuView := tview.NewList() + tableView := tview.NewTable().SetBorders(true) + sqlEditor := tview.NewTextArea() - menu.SetTitle("TABLES").SetBorder(true) - menu.ShowSecondaryText(false).SetDoneFunc(func() { - table.Clear() + sqlEditor.SetTitle("SQL Editor").SetBorder(true) + + tableView.SetBorder(true).SetTitle("TABLE") + + menuView.SetTitle("TABLES").SetBorder(true) + menuView.ShowSecondaryText(false).SetDoneFunc(func() { + tableView.Clear() }) - flex := tview.NewFlex(). - AddItem(menu, 0, 1, true). - AddItem(table, 0, 3, true) + verticalFlex := tview.NewFlex(). + AddItem(menuView, 0, 1, true). + AddItem(tableView, 0, 3, false) + + horizontalFlex := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(verticalFlex, 0, 4, true). + AddItem(sqlEditor, 0, 1, false) m, err := engine.NewManager("db.sqlite") if err != nil { @@ -33,19 +42,20 @@ func main() { tables := m.GetTables() for _, t := range tables { - menu.AddItem(t.Name, "", 0, nil) + menuView.AddItem(t.Name, "", 0, nil) } - menu.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) { - table.Clear() + menuView.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + tableView.Clear() t, ok := m.GetTable(mainText) if !ok { panic("AHHHHHHH") } + tableView.SetFixed(1, 0) for i, c := range t.Columns { color := tcell.ColorDarkGreen - table.SetCell(0, i, tview.NewTableCell(c.Name).SetTextColor(color).SetAlign(tview.AlignCenter)) + tableView.SetCell(0, i, tview.NewTableCell(c.Name).SetTextColor(color).SetAlign(tview.AlignCenter)) } err = m.LoadTable(&t) @@ -55,25 +65,88 @@ func main() { for ri, r := range t.Rows { for rc, c := range r.Values { - table.SetCell(ri+1, rc, tview.NewTableCell(c).SetTextColor(tcell.ColorWhite).SetAlign(tview.AlignCenter)) + tableView.SetCell(ri+1, rc, tview.NewTableCell(c).SetTextColor(tcell.ColorWhite).SetAlign(tview.AlignCenter)) } } }) - menu.SetCurrentItem(1) - table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { - switch key { + // Idk this shouldnt be needed imo but with only 0 it doesnt work, and with 1, well we are on Table 1 not zero, WHICH WE CANT ALWAYS SAY THERE IS + menuView.SetCurrentItem(1) + menuView.SetCurrentItem(0) + + menuView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { case tcell.KeyEscape: app.Stop() case tcell.KeyEnter: - table.SetSelectable(true, true) + app.SetFocus(tableView) + return nil + case tcell.KeyRune: + switch event.Rune() { + case 'j': + return tcell.NewEventKey(tcell.KeyDown, 'j', event.Modifiers()) + case 'k': + return tcell.NewEventKey(tcell.KeyUp, 'k', event.Modifiers()) + } } - }).SetSelectedFunc(func(row, column int) { - table.GetCell(row, column).SetTextColor(tcell.ColorRed) - table.SetSelectable(false, false) + + return event }) - if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil { + sqlEditor.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEnter { + t, err := m.RunSql(sqlEditor.GetText()) + if err != nil { + sqlEditor.SetText(err.Error(), true) + } + + tableView.Clear() + tableView.SetFixed(1, 0) + for i, c := range t.Columns { + color := tcell.ColorDarkGreen + tableView.SetCell(0, i, tview.NewTableCell(c.Name).SetTextColor(color).SetAlign(tview.AlignCenter)) + } + + for ri, r := range t.Rows { + for rc, c := range r.Values { + tableView.SetCell(ri+1, rc, tview.NewTableCell(c).SetTextColor(tcell.ColorWhite).SetAlign(tview.AlignCenter)) + } + } + + return nil + } + + return event + }) + + tableView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyEscape: + app.SetFocus(menuView) + return nil + } + + return event + }) + + menuHidden := false + verticalFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyCtrlH: + if !menuHidden { + verticalFlex.ResizeItem(menuView, 0, 0) + app.SetFocus(tableView) + } else { + verticalFlex.ResizeItem(menuView, 0, 1) + app.SetFocus(menuView) + } + menuHidden = !menuHidden + } + + return event + }) + + if err := app.SetRoot(horizontalFlex, true).EnableMouse(true).Run(); err != nil { panic(err) } diff --git a/manager.go b/manager.go index 2ebfd9c..df40335 100644 --- a/manager.go +++ b/manager.go @@ -61,6 +61,114 @@ func NewManager(path string) (*Manager, error) { }, nil } +func (m *Manager) RunSql(sqlText string) (Table, error) { + p := engine.NewParser(strings.NewReader(sqlText)) + stmt, err := p.Parse() + if err != nil && !errors.Is(err, io.EOF) { + return Table{}, err + } + + selectStmt, ok := stmt.(*engine.SelectStatement) + if !ok { + panic("HELP ITS NOT A SELECT STATMET") + } + + table, ok := m.GetTable(selectStmt.From) + if !ok { + panic("HELP TABLE NOT FOUND") + } + + rows, err := m.conn.Query(sqlText) + if err != nil { + return Table{}, err + } + table.Rows = make([]Row, 0) + + if slices.Contains(selectStmt.Fields, "*") { + for rows.Next() { + cols := make([]any, len(table.Columns)) + + for i, column := range table.Columns { + switch column.Type { + case BLOB: + cols[i] = new([]byte) + case TEXT: + cols[i] = new(string) + case INTEGER: + cols[i] = new(int) + case REAL: + cols[i] = new(float64) + default: + panic("THIS SHOULD NEVER HAPPEN, WE HIT AN UNKNOWN COLUMN.TYPE") + } + } + + err = rows.Scan(cols...) + if err != nil { + return Table{}, err + } + + table.Rows = append(table.Rows, Row{ + Values: anyToStr(cols), + }) + } + + return table, nil + } else { + columns := make([]Column, len(selectStmt.Fields)) + firstTime := true + + for rows.Next() { + + cols := make([]any, len(selectStmt.Fields)) + + for i, s := range selectStmt.Fields { + for _, column := range table.Columns { + if column.Name != s { + continue + } + + if firstTime { + columns[i] = column + } + + switch column.Type { + case BLOB: + cols[i] = new([]byte) + case TEXT: + cols[i] = new(string) + case INTEGER: + cols[i] = new(int) + case REAL: + cols[i] = new(float64) + default: + panic("THIS SHOULD NEVER HAPPEN, WE HIT AN UNKNOWN COLUMN.TYPE") + } + } + } + + firstTime = false + + err = rows.Scan(cols...) + if err != nil { + return Table{}, err + } + + table.Rows = append(table.Rows, Row{ + Values: anyToStr(cols), + }) + } + + nTable := Table{ + Name: selectStmt.From, + Columns: columns, + Rows: table.Rows, + } + + return nTable, nil + } +} + func (m *Manager) Start() error { for { stmt, err := m.parser.Parse() diff --git a/sql/parser.go b/sql/parser.go index 3970ca3..0f8b703 100644 --- a/sql/parser.go +++ b/sql/parser.go @@ -23,18 +23,75 @@ func NewParser(r io.Reader) *Parser { } func (p *Parser) Parse() (Statement, error) { - tok, ok := p.expectOne(CREATE, EOF) + tok, ok := p.expectOne(CREATE, EOF, SELECT) if !ok { return nil, p.unexpectedToken(CREATE, EOF) } else if tok == EOF { return nil, io.EOF } + switch tok { + case EOF: + return nil, io.EOF + case CREATE: + return p.parseCreateTable() + case SELECT: + return p.parseSelect() + default: + panic("SHOULD NEVER BE REACHED") + } +} + +func (p *Parser) parseSelect() (*SelectStatement, error) { + tok, ok := p.expectOne(ASTERIKS, IDENT) + if !ok { + return nil, p.unexpectedToken(ASTERIKS, IDENT) + } + + fields := make([]string, 1) + fields[0] = "*" + if tok == IDENT { + _, _, n := p.rescan() + fields[0] = n + for { + tok, ok := p.expectOne(COMMA, FROM) + if !ok { + return nil, p.unexpectedToken(COMMA, FROM) + } + if tok == FROM { + p.unscan() + break + } + + if !p.expectNext(IDENT) { + return nil, p.unexpectedToken(IDENT) + } + _, _, n := p.rescan() + fields = append(fields, n) + } + } + + if !p.expectSequence(FROM, IDENT) { + return nil, p.unexpectedToken() + } + + _, _, tableName := p.rescan() + if !p.consumeUntilOne(50, SEMI, EOF) { + return nil, fmt.Errorf("Expected semicolon but never found after 50 tries") + } + + return &SelectStatement{ + From: tableName, + Fields: fields, + }, nil +} + +func (p *Parser) parseCreateTable() (*CreateTableStatement, error) { if !p.expectNext(TABLE) { return nil, p.unexpectedToken() } - tok, ok = p.expectOne(IDENT, IF) + tok, ok := p.expectOne(IDENT, IF) if !ok { return nil, p.unexpectedToken(IDENT, IF) } else if tok == IF && !p.expectSequence(NOT, EXISTS, IDENT) { @@ -237,6 +294,28 @@ func (p *Parser) expectOne(token ...Token) (Token, bool) { return tok, ok } +func (p *Parser) consumeUntilOne(max int, token ...Token) bool { + for range max { + _, tok, _ := p.scan() + if slices.ContainsFunc(token, func(t Token) bool { + return tok == t + }) { + return true + } + } + return false +} + +func (p *Parser) consumeUntil(token Token, max int) bool { + for range max { + _, tok, _ := p.scan() + if tok == token { + return true + } + } + return false +} + func (p *Parser) scan() (Position, Token, string) { if p.buf.avail { p.buf.avail = false