Compare commits

...

12 Commits

Author SHA1 Message Date
Pablu
b0f7d74adc looking good so far 2026-01-20 00:39:20 +01:00
Pablu
3dd1cfef55 Update to use lipgloss table, and simplify code significantly 2026-01-19 23:54:33 +01:00
Pablu
555ca1f6ab Remove debug.log 2026-01-19 22:50:00 +01:00
Pablu
1af6539deb Update tea and lexer 2026-01-19 22:49:22 +01:00
Pablu
a8c7ad60c3 Add tea 2026-01-16 14:55:04 +01:00
Pablu
e07bb9e496 Cleanup parser statements, add delete and insert statement, add TASKS.md for tracking tasks 2025-12-02 17:06:27 +01:00
Pablu
f6ca16b1f0 Cleanup and fix raw sql 2025-12-02 10:44:28 +01:00
Pablu
b7147d03c2 Deduplicate code and use loadTableRaw for RunSql 2025-12-02 10:10:57 +01:00
Pablu
9c51424bf8 Remove tea and tidy go mod 2025-12-02 09:50:52 +01:00
Pablu
c4218cee51 Add more syntax to lexer and parser, and make improvements to tview 2025-12-02 09:50:20 +01:00
Pablu
c41b4cc5da Add ctrl+e to hide and unhide editor 2025-12-01 17:05:10 +01:00
Pablu
694bbb7934 Working select statements for know tables, with and without semi at the end 2025-12-01 16:36:04 +01:00
17 changed files with 1317 additions and 417 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
db.* db.*
main
debug.log

5
TASKS.md Normal file
View File

@@ -0,0 +1,5 @@
[] Make lexer detect Text by quotes are same as ident?
[] Make lexer understand numbers
[] Add boolean and NULL types
[] Handle extra fields like WHERE, ORDER etc, in parser, for Delete and select
[] Think about a better way than to return an error with Affected Rows when inserting or deleting

View File

@@ -20,5 +20,16 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
fmt.Print(m) table, err := m.RunSql("select * from videos where id like 'gishi%' limit 1")
if err != nil {
log.Fatal(err)
}
for _, c := range table.Columns {
fmt.Println(c)
}
for _, r := range table.Rows {
fmt.Println(r.Values)
}
} }

View File

@@ -1,185 +1,298 @@
package main package main
import ( import (
"log" "fmt"
"math"
"os" "os"
"strconv"
"strings"
engine "git.pablu.de/pablu/sqv-engine" engine "git.pablu.de/pablu/sqv-engine"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/charmbracelet/log"
) )
func tabBorderWithBottom(left, middle, right string) lipgloss.Border { type Focused int
border := lipgloss.RoundedBorder()
border.BottomLeft = left const (
border.Bottom = middle EDITOR Focused = iota
border.BottomRight = right TABLE
return border PICKER
)
type mainModel struct {
width, height int
manager *engine.Manager
lastFocused, focused Focused
table *table.Table
editor textarea.Model
picker list.Model
debugMsgs []string
} }
var ( var (
inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴") defaultStyle = lipgloss.NewStyle().
activeTabBorder = tabBorderWithBottom("┘", " ", "└") Align(lipgloss.Center).
docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) BorderStyle(lipgloss.NormalBorder())
highlightColor = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true).BorderForeground(highlightColor).Padding(0, 1) focusedStyle = defaultStyle.BorderForeground(lipgloss.Color("202"))
activeTabStyle = inactiveTabStyle.Border(activeTabBorder, true)
windowStyle = lipgloss.NewStyle().BorderForeground(highlightColor).Padding(2, 0).Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop()
) )
type model struct { type item struct {
tables []table.Model title string
tableNames []string
textarea textarea.Model
currTable int
} }
func (m model) Init() tea.Cmd { func (i item) Title() string { return i.title }
func (i item) Description() string { return i.title }
func (i item) FilterValue() string { return i.title }
func newMainModerl(manager *engine.Manager) mainModel {
ed := textarea.New()
ed.Placeholder = "Try \"SELECT * FROM ?;\""
ed.ShowLineNumbers = false
ta := table.New().Border(lipgloss.NormalBorder())
li := list.New(nil, list.NewDefaultDelegate(), 0, 0)
li.Title = "Table Picker"
return mainModel{
editor: ed,
table: ta,
picker: li,
manager: manager,
}
}
func (m mainModel) Init() tea.Cmd {
return m.GetTableDefinitions
}
func (m mainModel) GetTableDefinitions() tea.Msg {
tables := m.manager.GetTables()
tableNames := make([]list.Item, len(tables))
for i, table := range tables {
tableNames[i] = item{table.Name}
}
return tableDefinitionsMsg{
tableNames,
}
}
type tableDefinitionsMsg struct {
Tables []list.Item
}
func (m mainModel) GetFirstTable() tea.Msg {
t := m.manager.GetTables()[0]
cmd := m.GetTable(t.Name)
return cmd()
}
func (m mainModel) GetTable(name string) tea.Cmd {
return func() tea.Msg {
t, ok := m.manager.GetTable(name)
if !ok {
return nil return nil
}
m.manager.LoadTable(&t)
msg := tableMsg{
Columns: make([]string, len(t.Columns)),
Rows: make([][]string, len(t.Rows)),
}
for i, r := range t.Rows {
msg.Rows[i] = r.Values
}
for i, c := range t.Columns {
msg.Columns[i] = c.Name
}
return msg
}
} }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { type tableMsg struct {
var cmd tea.Cmd Columns []string
Rows [][]string
}
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
edCmd tea.Cmd
pickerCmd tea.Cmd
cmds tea.Cmd
)
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "q": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
case "right", "l", "n", "tab": case "tab":
m.currTable = min(m.currTable+1, len(m.tables)-1) if m.focused == EDITOR {
return m, nil m.focused = TABLE
case "left", "h", "p", "shift+tab": m.editor.Blur()
m.currTable = max(m.currTable-1, 0)
return m, nil
}
}
m.tables[m.currTable], cmd = m.tables[m.currTable].Update(msg)
return m, cmd
}
func (m model) View() string {
doc := strings.Builder{}
var renderedTabs []string
for i, t := range m.tableNames {
var style lipgloss.Style
isFirst, isLast, isActive := i == 0, i == len(m.tables)-1, i == m.currTable
if isActive {
style = activeTabStyle
} else { } else {
style = inactiveTabStyle m.focused = EDITOR
} }
border, _, _, _, _ := style.GetBorder() case "j":
if isFirst && isActive { m.table.Offset(50)
border.BottomLeft = "│" case "ctrl+e":
} else if isFirst && !isActive { if m.focused == PICKER {
border.BottomLeft = "├" m.focused = m.lastFocused
} else if isLast && isActive { } else {
border.BottomRight = "│" // Maybe blur is not even needed
} else if isLast && !isActive { m.editor.Blur()
border.BottomRight = "┤" m.lastFocused = m.focused
m.focused = PICKER
}
case "enter":
log.Debug("Enter was pressed")
if m.focused == PICKER {
i, ok := m.picker.SelectedItem().(item)
if ok {
log.Debug("Selected item okay and batched")
cmds = tea.Batch(cmds, m.GetTable(i.title))
} }
style = style.Border(border) m.focused = m.lastFocused
renderedTabs = append(renderedTabs, style.Render(t)) if m.focused == EDITOR {
m.editor.Focus()
}
}
}
case tea.WindowSizeMsg:
m = m.updateStyles(msg.Width, msg.Height)
cmds = tea.Batch(cmds, m.GetFirstTable)
case tableMsg:
m.table.Offset(0)
m.table.ClearRows()
m.table.Headers(msg.Columns...)
m.table.Rows(msg.Rows...)
case tableDefinitionsMsg:
pickerCmd = m.picker.SetItems(msg.Tables)
} }
row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...) switch m.focused {
doc.WriteString(row) case EDITOR:
doc.WriteString("\n") m.editor.Focus()
windowWidth := (lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize()) m.editor, edCmd = m.editor.Update(msg)
doc.WriteString(windowStyle.Width(windowWidth).Render(m.tables[m.currTable].View())) case PICKER:
doc.WriteString("\n") m.picker, pickerCmd = m.picker.Update(msg)
}
// Is this correct?? return m, tea.Batch(edCmd, cmds, pickerCmd)
m.textarea.SetWidth(windowWidth)
doc.WriteString(windowStyle.Width(windowWidth).Render(m.textarea.View()))
return docStyle.Render(doc.String())
// return baseStyle.Render(m.table.View()) + "\n"
} }
func convertToViewTable(m *engine.Manager) ([]string, []table.Model) { func (m mainModel) updateStyles(width, height int) mainModel {
tables := m.GetTables() m.width = width
res := make([]table.Model, len(tables)) m.height = height
resNames := make([]string, len(tables))
for j, t := range tables { h, v := defaultStyle.GetFrameSize()
columns := make([]table.Column, len(t.Columns))
for i, column := range t.Columns {
columns[i] = table.Column{
Title: column.Name,
Width: max(lipgloss.Width(column.Name), 10),
}
}
for k := range 50 { topHeight := math.Ceil(float64(m.height)*3/4 - float64(h))
row := engine.Row{Values: make([]string, len(t.Columns))} editorHeight := math.Ceil(float64(m.height)*1/4 - float64(h))
for i := range t.Columns {
row.Values[i] = "test " + strconv.Itoa(k)
}
t.Rows = append(t.Rows, row)
}
rows := make([]table.Row, len(t.Rows)) m.editor.SetWidth(m.width - v)
for i, row := range t.Rows { m.editor.SetHeight(int(editorHeight))
rows[i] = row.Values
}
resTable := table.New( m.table.Width(m.width - v)
table.WithColumns(columns), m.table.Height(int(topHeight) - h*2)
table.WithRows(rows),
table.WithFocused(true), m.picker.SetSize(m.width*7/10, m.height*7/10)
table.WithHeight(25),
return m
}
func (m mainModel) View() string {
var (
view, editor string
) )
s := table.DefaultStyles() h, _ := defaultStyle.GetFrameSize()
s.Header = s.Header. topHeight := (m.height * 3 / 4) - h
BorderStyle(lipgloss.NormalBorder()). editorHeight := (m.height * 1 / 4) - h
BorderForeground(lipgloss.Color("240")). switch m.focused {
BorderBottom(true). case EDITOR:
Bold(false) view = defaultStyle.
s.Selected = s.Selected. Height(topHeight).
Foreground(lipgloss.Color("229")). Render(m.table.Render())
Background(lipgloss.Color("57")). editor = focusedStyle.
Bold(false) Height(editorHeight).
resTable.SetStyles(s) Render(m.editor.View())
case TABLE:
resNames[j] = t.Name view = focusedStyle.
res[j] = resTable Height(topHeight).
Render(m.table.Render())
editor = defaultStyle.
Height(editorHeight).
Render(m.editor.View())
case PICKER:
view = defaultStyle.
Height(topHeight).
Render(m.table.Render())
editor = defaultStyle.
Height(editorHeight).
Render(m.editor.View())
} }
return resNames, res main := lipgloss.JoinVertical(lipgloss.Top, view, editor)
if m.focused == PICKER {
x := (m.width / 2) - m.picker.Width()/2
y := (m.height / 2) - m.picker.Height()/2
return PlaceOverlay(x, y, focusedStyle.Render(m.picker.View()), main, false)
}
return main
} }
func main() { func main() {
file, err := os.ReadFile("test.sql") f, err := tea.LogToFile("debug.log", "debug")
if err != nil { if err != nil {
log.Fatal(err) fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
log.SetLevel(log.DebugLevel)
log.SetOutput(f)
m, err := engine.NewManager("../vdcmp/db.sqlite")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
} }
m := engine.NewManagerFromFile(string(file)) err = m.Start()
m.Start() if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
tableNames, tables := convertToViewTable(m) table.New()
ta := textarea.New() p := tea.NewProgram(newMainModerl(m), tea.WithAltScreen())
ta.Placeholder = "select * from ?"
ta.Focus()
ta.Prompt = "| "
ta.CharLimit = 280
p := tea.NewProgram(model{tables: tables, tableNames: tableNames, textarea: ta}, tea.WithAltScreen())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
log.Fatalf("Alas, theres been an error: %v", err) fmt.Println("fatal:", err)
os.Exit(1)
} }
} }

208
cmd/sqv-tea/util.go Normal file
View File

@@ -0,0 +1,208 @@
package main
import (
"bytes"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-runewidth"
"github.com/muesli/ansi"
"github.com/muesli/reflow/truncate"
"github.com/muesli/termenv"
)
// Most of this code is borrowed from
// https://github.com/charmbracelet/lipgloss/pull/102
// as well as the lipgloss library, with some modification for what I needed.
// Split a string into lines, additionally returning the size of the widest
// line.
func getLines(s string) (lines []string, widest int) {
lines = strings.Split(s, "\n")
for _, l := range lines {
w := ansi.PrintableRuneWidth(l)
if widest < w {
widest = w
}
}
return lines, widest
}
// PlaceOverlay places fg on top of bg.
func PlaceOverlay(
x, y int,
fg, bg string,
shadow bool, opts ...WhitespaceOption,
) string {
fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines)
fgHeight := len(fgLines)
if shadow {
var shadowbg string = ""
shadowchar := lipgloss.NewStyle().
Foreground(lipgloss.Color("#333333")).
Render("░")
for i := 0; i <= fgHeight; i++ {
if i == 0 {
shadowbg += " " + strings.Repeat(" ", fgWidth) + "\n"
} else {
shadowbg += " " + strings.Repeat(shadowchar, fgWidth) + "\n"
}
}
fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
fgLines, fgWidth = getLines(fg)
fgHeight = len(fgLines)
}
if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = clamp(x, 0, bgWidth-fgWidth)
y = clamp(y, 0, bgHeight-fgHeight)
ws := &whitespace{}
for _, opt := range opts {
opt(ws)
}
var b strings.Builder
for i, bgLine := range bgLines {
if i > 0 {
b.WriteByte('\n')
}
if i < y || i >= y+fgHeight {
b.WriteString(bgLine)
continue
}
pos := 0
if x > 0 {
left := truncate.String(bgLine, uint(x))
pos = ansi.PrintableRuneWidth(left)
b.WriteString(left)
if pos < x {
b.WriteString(ws.render(x - pos))
pos = x
}
}
fgLine := fgLines[i-y]
b.WriteString(fgLine)
pos += ansi.PrintableRuneWidth(fgLine)
right := cutLeft(bgLine, pos)
bgWidth := ansi.PrintableRuneWidth(bgLine)
rightWidth := ansi.PrintableRuneWidth(right)
if rightWidth <= bgWidth-pos {
b.WriteString(ws.render(bgWidth - rightWidth - pos))
}
b.WriteString(right)
}
return b.String()
}
// cutLeft cuts printable characters from the left.
// This function is heavily based on muesli's ansi and truncate packages.
func cutLeft(s string, cutWidth int) string {
var (
pos int
isAnsi bool
ab bytes.Buffer
b bytes.Buffer
)
for _, c := range s {
var w int
if c == ansi.Marker || isAnsi {
isAnsi = true
ab.WriteRune(c)
if ansi.IsTerminator(c) {
isAnsi = false
if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
ab.Reset()
}
}
} else {
w = runewidth.RuneWidth(c)
}
if pos >= cutWidth {
if b.Len() == 0 {
if ab.Len() > 0 {
b.Write(ab.Bytes())
}
if pos-cutWidth > 1 {
b.WriteByte(' ')
continue
}
}
b.WriteRune(c)
}
pos += w
}
return b.String()
}
func clamp(v, lower, upper int) int {
return min(max(v, lower), upper)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
type whitespace struct {
style termenv.Style
chars string
}
// Render whitespaces.
func (w whitespace) render(width int) string {
if w.chars == "" {
w.chars = " "
}
r := []rune(w.chars)
j := 0
b := strings.Builder{}
// Cycle through runes and print them into the whitespace.
for i := 0; i < width; {
b.WriteRune(r[j])
j++
if j >= len(r) {
j = 0
}
i += ansi.PrintableRuneWidth(string(r[j]))
}
// Fill any extra gaps white spaces. This might be necessary if any runes
// are more than one cell wide, which could leave a one-rune gap.
short := width - ansi.PrintableRuneWidth(b.String())
if short > 0 {
b.WriteString(strings.Repeat(" ", short))
}
return w.style.Styled(b.String())
}
// WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace)

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"log" "log"
engine "git.pablu.de/pablu/sqv-engine" engine "git.pablu.de/pablu/sqv-engine"
@@ -8,72 +9,170 @@ import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
func populateTable(tableView *tview.Table, table engine.Table) {
tableView.Clear()
tableView.SetFixed(1, 0)
for i, c := range table.Columns {
color := tcell.ColorDarkGreen
tableView.SetCell(0, i, tview.NewTableCell(c.Name).SetTextColor(color).SetAlign(tview.AlignCenter))
}
for ri, r := range table.Rows {
for rc, c := range r.Values {
tableView.SetCell(ri+1, rc, tview.NewTableCell(c).SetTextColor(tcell.ColorWhite).SetAlign(tview.AlignLeft).SetMaxWidth(30))
}
}
tableView.ScrollToBeginning()
}
var (
dbFile = flag.String("path", "db.sqlite", "Use to set db path")
)
func main() { func main() {
flag.Parse()
app := tview.NewApplication() app := tview.NewApplication()
menu := tview.NewList() menuView := tview.NewList()
table := tview.NewTable().SetBorders(true) tableView := tview.NewTable().SetBorders(true)
table.SetBorder(true).SetTitle("TABLE") sqlEditor := tview.NewTextArea()
menu.SetTitle("TABLES").SetBorder(true) sqlEditor.SetTitle("SQL Editor").SetBorder(true)
menu.ShowSecondaryText(false).SetDoneFunc(func() {
table.Clear() tableView.SetBorder(true).SetTitle("TABLE")
menuView.SetTitle("TABLES").SetBorder(true)
menuView.ShowSecondaryText(false).SetDoneFunc(func() {
tableView.Clear()
}) })
flex := tview.NewFlex(). verticalFlex := tview.NewFlex().
AddItem(menu, 0, 1, true). AddItem(menuView, 0, 1, true).
AddItem(table, 0, 3, true) AddItem(tableView, 0, 3, false)
m, err := engine.NewManager("db.sqlite") horizontalFlex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(verticalFlex, 0, 4, true).
AddItem(sqlEditor, 0, 1, false)
m, err := engine.NewManager(*dbFile)
if err != nil { if err != nil {
log.Fatalf("Ran into an error on opening Manager, err: %v\n", err) log.Fatalf("Ran into an error on opening Manager, err: %v\n", err)
} }
m.Start() err = m.Start()
if err != nil {
log.Fatalf("Ran into an error on starting Manager, err: %v\n", err)
}
tables := m.GetTables() tables := m.GetTables()
for _, t := range tables { 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) { menuView.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
table.Clear() tableView.Clear()
t, ok := m.GetTable(mainText) t, ok := m.GetTable(mainText)
if !ok { if !ok {
panic("AHHHHHHH") panic("AHHHHHHH")
} }
for i, c := range t.Columns {
color := tcell.ColorDarkGreen
table.SetCell(0, i, tview.NewTableCell(c.Name).SetTextColor(color).SetAlign(tview.AlignCenter))
}
err = m.LoadTable(&t) err = m.LoadTable(&t)
if err != nil { if !ok {
panic(err) panic("AHHHHHHH")
} }
for ri, r := range t.Rows { populateTable(tableView, t)
for rc, c := range r.Values {
table.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) { // 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
switch key { menuView.SetCurrentItem(1)
menuView.SetCurrentItem(0)
menuView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyEscape: case tcell.KeyEscape:
app.Stop() app.Stop()
case tcell.KeyEnter: 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.Replace(0, sqlEditor.GetTextLength(), err.Error())
tableView.Clear()
return nil
}
populateTable(tableView, t)
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
editorHidden := false
// logHidden := true
horizontalFlex.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
case tcell.KeyCtrlE:
if !editorHidden {
horizontalFlex.ResizeItem(sqlEditor, 0, 0)
if menuHidden {
app.SetFocus(tableView)
} else {
app.SetFocus(verticalFlex)
}
} else {
horizontalFlex.ResizeItem(sqlEditor, 0, 1)
app.SetFocus(sqlEditor)
}
editorHidden = !editorHidden
}
return event
})
menuView.SetFocusFunc(func() {
menuView.SetBorderColor(tcell.ColorRed)
})
if err := app.SetRoot(horizontalFlex, true).EnableMouse(true).Run(); err != nil {
panic(err) panic(err)
} }

7
go.mod
View File

@@ -3,7 +3,6 @@ module git.pablu.de/pablu/sqv-engine
go 1.25.3 go 1.25.3
require ( require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/gdamore/tcell/v2 v2.8.1 github.com/gdamore/tcell/v2 v2.8.1
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/rivo/tview v0.42.0 github.com/rivo/tview v0.42.0
@@ -18,6 +17,7 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gdamore/encoding v1.0.1 // indirect github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
@@ -26,7 +26,9 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/term v0.28.0 // indirect golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.25.0 // indirect
) )
@@ -34,5 +36,8 @@ require (
require ( require (
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
github.com/muesli/reflow v0.3.0
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
) )

22
go.sum
View File

@@ -1,11 +1,7 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@@ -14,12 +10,14 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -28,6 +26,8 @@ github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uh
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
@@ -35,6 +35,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
@@ -43,14 +44,19 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -59,8 +65,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=

View File

@@ -51,9 +51,8 @@ func NewManager(path string) (*Manager, error) {
sqls = append(sqls, sql) sqls = append(sqls, sql)
} }
schema := strings.Join(sqls, ";") schema := strings.Join(sqls, ";\n")
schema += ";" schema += ";"
// fmt.Println(schema)
return &Manager{ return &Manager{
parser: engine.NewParser(strings.NewReader(schema)), parser: engine.NewParser(strings.NewReader(schema)),
@@ -62,10 +61,10 @@ func NewManager(path string) (*Manager, error) {
} }
func (m *Manager) Start() error { func (m *Manager) Start() error {
createTableStatements := make([]*engine.CreateTableStatement, 0)
for { for {
stmt, err := m.parser.Parse() stmt, err := m.parser.Parse()
if err != nil && errors.Is(err, io.EOF) { if err != nil && errors.Is(err, io.EOF) {
fmt.Println("Finished parsing")
break break
} else if err != nil { } else if err != nil {
return err return err
@@ -78,12 +77,21 @@ func (m *Manager) Start() error {
return err return err
} }
m.tables = append(m.tables, t) m.tables = append(m.tables, t)
createTableStatements = append(createTableStatements, v)
default: default:
panic("NOT IMPLEMENTED") panic("NOT IMPLEMENTED")
} }
} }
// Rethink how to do this cleanly
for _, cts := range createTableStatements {
err := m.references(cts)
if err != nil {
return err
}
}
return nil return nil
} }
@@ -111,16 +119,99 @@ func (m *Manager) GetTable(name string) (Table, bool) {
return table, true return table, true
} }
func (m *Manager) LoadTable(table *Table) error { func (m *Manager) RunSql(sqlText string) (Table, error) {
rows, err := m.conn.Query(fmt.Sprintf("SELECT * FROM %v", table.Name)) p := engine.NewParser(strings.NewReader(sqlText))
stmt, err := p.Parse()
if err != nil && !errors.Is(err, io.EOF) {
return Table{}, err
}
switch v := stmt.(type) {
case *engine.SelectStatement:
return m.tableFromSelectStatement(sqlText, v)
case *engine.InsertStatement:
if !slices.ContainsFunc(m.tables, func(t Table) bool {
return v.Table == t.Name
}) {
return Table{}, fmt.Errorf("Table not found")
}
res, err := m.conn.Exec(sqlText)
if err != nil {
return Table{}, err
}
affected, err := res.RowsAffected()
if err != nil {
return Table{}, err
}
return Table{}, fmt.Errorf("Rows affected: %v", affected)
case *engine.DeleteStatement:
if !slices.ContainsFunc(m.tables, func(t Table) bool {
return v.Table == t.Name
}) {
return Table{}, fmt.Errorf("Table not found")
}
res, err := m.conn.Exec(sqlText)
if err != nil {
return Table{}, err
}
affected, err := res.RowsAffected()
if err != nil {
return Table{}, err
}
return Table{}, fmt.Errorf("Rows affected: %v", affected)
default:
return Table{}, fmt.Errorf("Input statement is not of correct Syntax, select statement")
}
}
func (m *Manager) tableFromSelectStatement(sqlText string, stmt *engine.SelectStatement) (Table, error) {
table, ok := m.GetTable(stmt.From)
if !ok {
return Table{}, fmt.Errorf("Selected Table does not exist, have you perhaps misstyped the table Name?")
}
fields := make([]Column, 0)
if slices.Contains(stmt.Fields, "*") {
fields = table.Columns
} else {
for _, columnName := range stmt.Fields {
index := slices.IndexFunc(table.Columns, func(c Column) bool {
if c.Name == columnName {
return true
}
return false
})
fields = append(fields, table.Columns[index])
}
}
table.Columns = fields
err := m.loadTableRaw(&table, fields, sqlText)
if err != nil {
return Table{}, err
}
return table, nil
}
func (m *Manager) loadTableRaw(table *Table, fields []Column, s string, args ...any) error {
rows, err := m.conn.Query(s, args...)
if err != nil { if err != nil {
return err return err
} }
table.Rows = make([]Row, 0) table.Rows = make([]Row, 0)
for rows.Next() { for rows.Next() {
cols := make([]any, len(table.Columns)) cols := make([]any, len(fields))
for i, column := range table.Columns { for i, column := range fields {
if column.Flags.Has(NOT_NULL) {
switch column.Type { switch column.Type {
case BLOB: case BLOB:
cols[i] = new([]byte) cols[i] = new([]byte)
@@ -133,6 +224,20 @@ func (m *Manager) LoadTable(table *Table) error {
default: default:
panic("THIS SHOULD NEVER HAPPEN, WE HIT AN UNKNOWN COLUMN.TYPE") panic("THIS SHOULD NEVER HAPPEN, WE HIT AN UNKNOWN COLUMN.TYPE")
} }
} else {
switch column.Type {
case BLOB:
cols[i] = new([]byte)
case TEXT:
cols[i] = new(sql.NullString)
case INTEGER:
cols[i] = new(sql.NullInt64)
case REAL:
cols[i] = new(sql.NullFloat64)
default:
panic("THIS SHOULD NEVER HAPPEN, WE HIT AN UNKNOWN COLUMN.TYPE")
}
}
} }
err = rows.Scan(cols...) err = rows.Scan(cols...)
@@ -148,6 +253,16 @@ func (m *Manager) LoadTable(table *Table) error {
return nil return nil
} }
func (m *Manager) LoadTableMaxRows(table *Table, maxRows int) error {
sql := fmt.Sprintf("SELECT * FROM %v LIMIT ?", table.Name)
return m.loadTableRaw(table, table.Columns, sql, maxRows)
}
func (m *Manager) LoadTable(table *Table) error {
sql := fmt.Sprintf("SELECT * FROM %v", table.Name)
return m.loadTableRaw(table, table.Columns, sql)
}
func anyToStr(a []any) []string { func anyToStr(a []any) []string {
res := make([]string, len(a)) res := make([]string, len(a))
@@ -160,7 +275,30 @@ func anyToStr(a []any) []string {
case *float64: case *float64:
res[i] = strconv.FormatFloat(*v, 'f', 2, 64) res[i] = strconv.FormatFloat(*v, 'f', 2, 64)
case *[]byte: case *[]byte:
res[i] = hex.EncodeToString(*v) buf := *v
if len(buf) > 512 {
buf = buf[0:512]
}
res[i] = hex.EncodeToString(buf)
case *sql.NullInt64:
if v.Valid {
res[i] = strconv.Itoa(int(v.Int64))
} else {
res[i] = "NULL"
}
case *sql.NullFloat64:
if v.Valid {
res[i] = strconv.FormatFloat(v.Float64, 'f', 2, 64)
} else {
res[i] = "NULL"
}
case *sql.NullString:
if v.Valid {
res[i] = v.String
} else {
res[i] = "NULL"
}
default: default:
panic("THIS SHOULD NEVER HAPPEN, WE GOT SERVED AN UNKNOWN TYPE") panic("THIS SHOULD NEVER HAPPEN, WE GOT SERVED AN UNKNOWN TYPE")
} }
@@ -169,17 +307,18 @@ func anyToStr(a []any) []string {
return res return res
} }
func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStatement) (Table, error) { func (m *Manager) references(cts *engine.CreateTableStatement) error {
res := Table{ table, ok := m.GetTable(cts.TableName)
Name: cts.TableName, if !ok {
Columns: make([]Column, len(cts.Columns)), return fmt.Errorf("No table with name found, name: %v", cts.TableName)
Rows: make([]Row, 0),
} }
for i, column := range cts.Columns { for i, column := range cts.Columns {
flags := extrasToFlags(column.Extra) flags := extrasToFlags(column.Extra)
var ref *Column = nil if !flags.Has(FOREIGN_KEY) {
if flags.Has(FOREIGN_KEY) { continue
}
index := slices.IndexFunc(column.Extra, func(c string) bool { index := slices.IndexFunc(column.Extra, func(c string) bool {
return strings.HasPrefix(c, "ref") return strings.HasPrefix(c, "ref")
}) })
@@ -192,17 +331,28 @@ func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStat
refTable, ok := m.GetTable(tableName) refTable, ok := m.GetTable(tableName)
if !ok { if !ok {
fmt.Println(m.tables) return fmt.Errorf("Reference table '%v' not found", tableName)
return Table{}, fmt.Errorf("Reference table '%v' not found", tableName) } else {
}
colIndex := slices.IndexFunc(refTable.Columns, func(c Column) bool { colIndex := slices.IndexFunc(refTable.Columns, func(c Column) bool {
return c.Name == columnName return c.Name == columnName
}) })
ref = &refTable.Columns[colIndex] table.Columns[i].Reference = &refTable.Columns[colIndex]
}
} }
return nil
}
func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStatement) (Table, error) {
res := Table{
Name: cts.TableName,
Columns: make([]Column, len(cts.Columns)),
Rows: make([]Row, 0),
}
for i, column := range cts.Columns {
flags := extrasToFlags(column.Extra)
var columnType ColumnType var columnType ColumnType
switch column.Type { switch column.Type {
case "REAL": case "REAL":
@@ -211,7 +361,7 @@ func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStat
columnType = BLOB columnType = BLOB
case "TEXT": case "TEXT":
columnType = TEXT columnType = TEXT
case "INTEGER": case "INTEGER", "NUMERIC":
columnType = INTEGER columnType = INTEGER
default: default:
panic("This shouldnt happen") panic("This shouldnt happen")
@@ -220,7 +370,6 @@ func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStat
res.Columns[i] = Column{ res.Columns[i] = Column{
Type: columnType, Type: columnType,
Name: column.Name, Name: column.Name,
Reference: ref,
Flags: flags, Flags: flags,
} }
} }
@@ -240,6 +389,8 @@ func extrasToFlags(extras []string) ColumnFlag {
res |= FOREIGN_KEY res |= FOREIGN_KEY
case "NOT_NULL": case "NOT_NULL":
res |= NOT_NULL res |= NOT_NULL
case "AUTOINCREMENT":
res |= AUTO_INCREMENT
default: default:
log.Panicf("NOT IMPLEMENTED EXTRA: %v", extra) log.Panicf("NOT IMPLEMENTED EXTRA: %v", extra)
} }

View File

@@ -33,5 +33,17 @@ type SelectStatement struct {
Fields []string Fields []string
} }
type InsertStatement struct {
Table string
Values map[string]any
}
type DeleteStatement struct {
Table string
Extra []string
}
func (_ *CreateTableStatement) isEnumValue() {} func (_ *CreateTableStatement) isEnumValue() {}
func (_ *SelectStatement) isEnumValue() {} func (_ *SelectStatement) isEnumValue() {}
func (_ *InsertStatement) isEnumValue() {}
func (_ *DeleteStatement) isEnumValue() {}

View File

@@ -21,11 +21,23 @@ const (
COMMA COMMA
ASTERIKS ASTERIKS
ASSIGN ASSIGN
BACKQUOTE
QUOTE
SINGLE_QUOTE
// TYPES
TYPE_NUMERIC
TYPE_TEXT
// Keywords // Keywords
CREATE CREATE
TABLE TABLE
INSERT
INTO
VALUES
RETURNING
SELECT SELECT
FROM FROM
WHERE WHERE
@@ -34,6 +46,8 @@ const (
ORDER ORDER
TOP TOP
DELETE
PRIMARY PRIMARY
FOREIGN FOREIGN
REFERENCES REFERENCES
@@ -41,12 +55,15 @@ const (
NOT NOT
IF IF
EXISTS EXISTS
AUTOINCREMENT
CONSTRAINT
TEXT TEXT
INTEGER INTEGER
NULL NULL
REAL REAL
BLOB BLOB
NUMERIC
) )
var keywords map[string]Token = map[string]Token{ var keywords map[string]Token = map[string]Token{
@@ -71,6 +88,14 @@ var keywords map[string]Token = map[string]Token{
"TOP": TOP, "TOP": TOP,
"REAL": REAL, "REAL": REAL,
"BLOB": BLOB, "BLOB": BLOB,
"AUTOINCREMENT": AUTOINCREMENT,
"CONSTRAINT": CONSTRAINT,
"NUMERIC": NUMERIC,
"INSERT": INSERT,
"INTO": INTO,
"VALUES": VALUES,
"RETURNING": RETURNING,
"DELETE": DELETE,
} }
type Position struct { type Position struct {
@@ -116,6 +141,12 @@ func (l *Lexer) Lex() (Position, Token, string) {
return l.pos, ASTERIKS, "*" return l.pos, ASTERIKS, "*"
case '=': case '=':
return l.pos, ASSIGN, "=" return l.pos, ASSIGN, "="
case '`':
return l.pos, BACKQUOTE, "`"
case '"':
return l.pos, QUOTE, "\""
case '\'':
return l.pos, SINGLE_QUOTE, "'"
default: default:
if unicode.IsSpace(r) { if unicode.IsSpace(r) {
continue continue
@@ -130,6 +161,12 @@ func (l *Lexer) Lex() (Position, Token, string) {
} }
return startPos, IDENT, lit return startPos, IDENT, lit
} else if unicode.IsNumber(r) {
startPos := l.pos
l.backup()
lit := l.lexIdent()
return startPos, TYPE_NUMERIC, lit
} else { } else {
return l.pos, ILLEGAL, string(r) return l.pos, ILLEGAL, string(r)
} }
@@ -149,7 +186,9 @@ func (l *Lexer) lexIdent() string {
} }
l.pos.column++ l.pos.column++
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
// Dont allow dot, just for testing with numeric
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '.' {
// Change this to stringstream or something similar // Change this to stringstream or something similar
lit = lit + string(r) lit = lit + string(r)
} else { } else {

219
sql/parseCreateStatement.go Normal file
View File

@@ -0,0 +1,219 @@
package sql
import (
"fmt"
"slices"
)
func (p *Parser) parseCreateTable() (*CreateTableStatement, error) {
if !p.expectNext(TABLE) {
return nil, p.unexpectedToken()
}
tok, ok := p.expectOne(QUOTE, SINGLE_QUOTE, BACKQUOTE, IDENT, IF)
if !ok {
return nil, p.unexpectedToken(IDENT, IF)
}
switch tok {
case IF:
if !p.expectSequence(NOT, EXISTS) {
return nil, p.unexpectedToken()
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
fallthrough
case QUOTE, SINGLE_QUOTE, BACKQUOTE:
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
}
_, _, lit := p.rescan()
stmt := CreateTableStatement{
TableName: lit,
Columns: make([]Column, 0),
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(LPAREN) {
return nil, p.unexpectedToken(LPAREN)
}
for {
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
_, tok, _ := p.scan()
switch tok {
case RPAREN:
if !p.expectNext(SEMI) {
return nil, p.unexpectedToken(SEMI)
}
return &stmt, nil
case IDENT:
column, err := p.parseColumn()
if err != nil {
return nil, err
}
stmt.Columns = append(stmt.Columns, column)
// TODO: HANDLE AND SAVE CONSTRAINTS
case CONSTRAINT:
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
// _, _, constraintName := p.rescan()
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
case FOREIGN:
if !p.expectSequence(KEY, LPAREN) {
return nil, p.unexpectedToken()
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
_, _, columnName := p.rescan()
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectSequence(RPAREN, REFERENCES) {
return nil, p.unexpectedToken()
}
ref, err := p.references()
if err != nil {
return nil, err
}
column := slices.IndexFunc(stmt.Columns, func(c Column) bool {
return c.Name == columnName
})
stmt.Columns[column].Extra = append(stmt.Columns[column].Extra, ref)
case PRIMARY:
if !p.expectSequence(KEY, LPAREN) {
return nil, p.unexpectedToken()
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
primaryKeyNames := make([]string, 0)
_, _, columnName := p.rescan()
primaryKeyNames = append(primaryKeyNames, columnName)
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
for {
tok, ok := p.expectOne(RPAREN, COMMA)
if !ok {
return nil, p.unexpectedToken()
}
if tok == RPAREN {
break
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
_, _, columnName := p.rescan()
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
primaryKeyNames = append(primaryKeyNames, columnName)
}
for _, pkName := range primaryKeyNames {
column := slices.IndexFunc(stmt.Columns, func(c Column) bool {
return c.Name == pkName
})
stmt.Columns[column].Extra = append(stmt.Columns[column].Extra, "PRIMARY_KEY")
}
case COMMA:
continue
default:
return nil, p.unexpectedToken(IDENT, RPAREN, FOREIGN, COMMA)
}
}
}
func (p *Parser) parseColumn() (Column, error) {
_, _, lit := p.rescan()
column := Column{Name: lit, Extra: make([]string, 0)}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if _, ok := p.expectOne(TEXT, INTEGER, REAL, BLOB, NUMERIC); !ok {
return Column{}, p.unexpectedToken(TEXT, INTEGER, REAL, BLOB, NUMERIC)
}
_, _, column.Type = p.rescan()
for {
_, tok, lit := p.scan()
switch tok {
case COMMA:
return column, nil
case RPAREN:
p.unscan()
return column, nil
case PRIMARY:
fallthrough
case NOT:
if _, ok := p.expectOne(NULL, KEY); !ok {
return Column{}, p.unexpectedToken(NULL, KEY)
}
_, _, rlit := p.rescan()
column.Extra = append(column.Extra, fmt.Sprintf("%v_%v", lit, rlit))
case REFERENCES:
ref, err := p.references()
if err != nil {
return Column{}, err
}
column.Extra = append(column.Extra, ref)
case AUTOINCREMENT:
column.Extra = append(column.Extra, "AUTOINCREMENT")
default:
return Column{}, p.unexpectedToken(COMMA, RPAREN, PRIMARY, NOT, REFERENCES)
}
}
}
func (p *Parser) references() (string, error) {
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return "", p.unexpectedToken(IDENT)
}
_, _, referenceTableName := p.rescan()
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(LPAREN) {
return "", p.unexpectedToken()
}
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(IDENT) {
return "", p.unexpectedToken()
}
_, _, referenceColumnName := p.rescan()
p.consumeIfOne(QUOTE, SINGLE_QUOTE, BACKQUOTE)
if !p.expectNext(RPAREN) {
return "", p.unexpectedToken(RPAREN)
}
return fmt.Sprintf("ref %v.%v", referenceTableName, referenceColumnName), nil
}

View File

@@ -0,0 +1,15 @@
package sql
func (p *Parser) parseDelete() (*DeleteStatement, error) {
if !p.expectSequence(FROM, IDENT) {
return nil, p.unexpectedToken(INTO)
}
res := DeleteStatement{}
_, _, res.Table = p.rescan()
p.consumeUntilOne(50, EOF, SEMI)
return &res, nil
}

View File

@@ -0,0 +1,69 @@
package sql
import "fmt"
func (p *Parser) parseInsert() (*InsertStatement, error) {
if !p.expectSequence(INTO, IDENT) {
return nil, p.unexpectedToken(INTO)
}
res := InsertStatement{}
_, _, res.Table = p.rescan()
if !p.expectNext(LPAREN) {
return nil, p.unexpectedToken(LPAREN)
}
fieldNames := make([]string, 0)
for loop := true; loop; {
_, tok, val := p.scan()
switch tok {
case IDENT:
fieldNames = append(fieldNames, val)
case RPAREN:
loop = false
case COMMA:
continue
default:
return nil, p.unexpectedToken(IDENT, RPAREN, COMMA)
}
}
if !p.expectSequence(VALUES, LPAREN) {
return nil, p.unexpectedToken()
}
values := make([]any, 0, len(fieldNames))
for loop := true; loop; {
_, tok, val := p.scan()
switch tok {
case IDENT, TYPE_NUMERIC:
// TODO, convert to actual datatype?
values = append(values, val)
case COMMA, QUOTE, SINGLE_QUOTE, BACKQUOTE:
continue
case RPAREN:
loop = false
default:
return nil, p.unexpectedToken(IDENT, RPAREN, COMMA, TYPE_NUMERIC)
}
}
if len(values) != len(fieldNames) {
return nil, fmt.Errorf("Expected same amount of Values as Fields, but got %v fields, and %v values", fieldNames, values)
}
// Handle things like RETURNING *, also handle multiple Values
if !p.consumeUntilOne(50, SEMI, EOF) {
return nil, fmt.Errorf("Expected semicolon but never found after 50 tries")
}
res.Values = make(map[string]any)
for i, name := range fieldNames {
res.Values[name] = values[i]
}
return &res, nil
}

View File

@@ -0,0 +1,47 @@
package sql
import "fmt"
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
}

View File

@@ -23,151 +23,26 @@ func NewParser(r io.Reader) *Parser {
} }
func (p *Parser) Parse() (Statement, error) { func (p *Parser) Parse() (Statement, error) {
tok, ok := p.expectOne(CREATE, EOF) tok, ok := p.expectOne(CREATE, EOF, SELECT, INSERT, DELETE)
if !ok { if !ok {
return nil, p.unexpectedToken(CREATE, EOF) return nil, p.unexpectedToken(CREATE, EOF, SELECT, INSERT, DELETE)
} else if tok == EOF { } else if tok == EOF {
return nil, io.EOF return nil, io.EOF
} }
if !p.expectNext(TABLE) {
return nil, p.unexpectedToken()
}
tok, ok = p.expectOne(IDENT, IF)
if !ok {
return nil, p.unexpectedToken(IDENT, IF)
} else if tok == IF && !p.expectSequence(NOT, EXISTS, IDENT) {
return nil, p.unexpectedToken()
}
_, _, lit := p.rescan()
stmt := CreateTableStatement{
TableName: lit,
Columns: make([]Column, 0),
}
if !p.expectNext(LPAREN) {
return nil, p.unexpectedToken(LPAREN)
}
for {
_, tok, _ := p.scan()
switch tok { switch tok {
case RPAREN: case EOF:
if !p.expectNext(SEMI) { return nil, io.EOF
return nil, p.unexpectedToken(SEMI) case CREATE:
} return p.parseCreateTable()
return &stmt, nil case SELECT:
return p.parseSelect()
case IDENT: case INSERT:
column, err := p.parseColumn() return p.parseInsert()
if err != nil { case DELETE:
return nil, err return p.parseDelete()
}
stmt.Columns = append(stmt.Columns, column)
case FOREIGN:
if !p.expectSequence(KEY, LPAREN, IDENT) {
return nil, p.unexpectedToken()
}
_, _, columnName := p.rescan()
if !p.expectSequence(RPAREN, REFERENCES) {
return nil, p.unexpectedToken()
}
ref, err := p.references()
if err != nil {
return nil, err
}
column := slices.IndexFunc(stmt.Columns, func(c Column) bool {
return c.Name == columnName
})
stmt.Columns[column].Extra = append(stmt.Columns[column].Extra, ref)
case PRIMARY:
if !p.expectSequence(KEY, LPAREN, IDENT) {
return nil, p.unexpectedToken()
}
primaryKeyNames := make([]string, 0)
_, _, columnName := p.rescan()
primaryKeyNames = append(primaryKeyNames, columnName)
for {
tok, ok := p.expectOne(RPAREN, COMMA)
if !ok {
return nil, p.unexpectedToken()
}
if tok == RPAREN {
break
}
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
_, _, columnName := p.rescan()
primaryKeyNames = append(primaryKeyNames, columnName)
}
for _, pkName := range primaryKeyNames {
column := slices.IndexFunc(stmt.Columns, func(c Column) bool {
return c.Name == pkName
})
stmt.Columns[column].Extra = append(stmt.Columns[column].Extra, "PRIMARY_KEY")
}
case COMMA:
continue
default: default:
return nil, p.unexpectedToken(IDENT, RPAREN, FOREIGN, COMMA) panic("SHOULD NEVER BE REACHED")
}
}
}
func (p *Parser) parseColumn() (Column, error) {
_, _, lit := p.rescan()
column := Column{Name: lit, Extra: make([]string, 0)}
if _, ok := p.expectOne(TEXT, INTEGER, REAL, BLOB); !ok {
return Column{}, p.unexpectedToken(TEXT, INTEGER, REAL, BLOB)
}
_, _, column.Type = p.rescan()
for {
_, tok, lit := p.scan()
switch tok {
case COMMA:
return column, nil
case RPAREN:
p.unscan()
return column, nil
case PRIMARY:
fallthrough
case NOT:
if _, ok := p.expectOne(NULL, KEY); !ok {
return Column{}, p.unexpectedToken(NULL, KEY)
}
_, _, rlit := p.rescan()
column.Extra = append(column.Extra, fmt.Sprintf("%v_%v", lit, rlit))
case REFERENCES:
ref, err := p.references()
if err != nil {
return Column{}, err
}
column.Extra = append(column.Extra, ref)
fmt.Println(ref)
default:
return Column{}, p.unexpectedToken(COMMA, RPAREN, PRIMARY, NOT, REFERENCES)
}
} }
} }
@@ -195,25 +70,6 @@ func (p *Parser) unexpectedToken(expected ...Token) error {
} }
} }
func (p *Parser) references() (string, error) {
if !p.expectNext(IDENT) {
return "", p.unexpectedToken(IDENT)
}
_, _, referenceTableName := p.rescan()
if !p.expectSequence(LPAREN, IDENT) {
return "", p.unexpectedToken()
}
_, _, referenceColumnName := p.rescan()
if !p.expectNext(RPAREN) {
return "", p.unexpectedToken(RPAREN)
}
return fmt.Sprintf("ref %v.%v", referenceTableName, referenceColumnName), nil
}
func (p *Parser) expectSequence(token ...Token) bool { func (p *Parser) expectSequence(token ...Token) bool {
for _, tok := range token { for _, tok := range token {
if !p.expectNext(tok) { if !p.expectNext(tok) {
@@ -237,6 +93,48 @@ func (p *Parser) expectOne(token ...Token) (Token, bool) {
return tok, ok 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) consumeIfOne(token ...Token) {
_, tok, _ := p.scan()
if slices.ContainsFunc(token, func(t Token) bool {
return tok == t
}) {
return
}
p.unscan()
}
func (p *Parser) consumeIf(token Token) {
_, tok, _ := p.scan()
if tok == token {
return
}
p.unscan()
}
func (p *Parser) scan() (Position, Token, string) { func (p *Parser) scan() (Position, Token, string) {
if p.buf.avail { if p.buf.avail {
p.buf.avail = false p.buf.avail = false

View File

@@ -12,6 +12,7 @@ const (
PRIMARY_KEY ColumnFlag = 1 << iota PRIMARY_KEY ColumnFlag = 1 << iota
FOREIGN_KEY FOREIGN_KEY
NOT_NULL NOT_NULL
AUTO_INCREMENT
NONE ColumnFlag = 0 NONE ColumnFlag = 0
) )