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.*
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)
}
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
import (
"log"
"fmt"
"math"
"os"
"strconv"
"strings"
engine "git.pablu.de/pablu/sqv-engine"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/charmbracelet/log"
)
func tabBorderWithBottom(left, middle, right string) lipgloss.Border {
border := lipgloss.RoundedBorder()
border.BottomLeft = left
border.Bottom = middle
border.BottomRight = right
return border
type Focused int
const (
EDITOR Focused = iota
TABLE
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 (
inactiveTabBorder = tabBorderWithBottom("┴", "─", "┴")
activeTabBorder = tabBorderWithBottom("┘", " ", "└")
docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2)
highlightColor = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true).BorderForeground(highlightColor).Padding(0, 1)
activeTabStyle = inactiveTabStyle.Border(activeTabBorder, true)
windowStyle = lipgloss.NewStyle().BorderForeground(highlightColor).Padding(2, 0).Align(lipgloss.Center).Border(lipgloss.NormalBorder()).UnsetBorderTop()
defaultStyle = lipgloss.NewStyle().
Align(lipgloss.Center).
BorderStyle(lipgloss.NormalBorder())
focusedStyle = defaultStyle.BorderForeground(lipgloss.Color("202"))
)
type model struct {
tables []table.Model
tableNames []string
textarea textarea.Model
currTable int
type item struct {
title string
}
func (m model) Init() tea.Cmd {
return nil
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 model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
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
}
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
}
}
type tableMsg struct {
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) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
case "ctrl+c":
return m, tea.Quit
case "right", "l", "n", "tab":
m.currTable = min(m.currTable+1, len(m.tables)-1)
return m, nil
case "left", "h", "p", "shift+tab":
m.currTable = max(m.currTable-1, 0)
return m, nil
}
}
case "tab":
if m.focused == EDITOR {
m.focused = TABLE
m.editor.Blur()
} else {
m.focused = EDITOR
}
case "j":
m.table.Offset(50)
case "ctrl+e":
if m.focused == PICKER {
m.focused = m.lastFocused
} else {
// Maybe blur is not even needed
m.editor.Blur()
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))
}
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 {
style = inactiveTabStyle
}
border, _, _, _, _ := style.GetBorder()
if isFirst && isActive {
border.BottomLeft = "│"
} else if isFirst && !isActive {
border.BottomLeft = "├"
} else if isLast && isActive {
border.BottomRight = "│"
} else if isLast && !isActive {
border.BottomRight = "┤"
}
style = style.Border(border)
renderedTabs = append(renderedTabs, style.Render(t))
}
row := lipgloss.JoinHorizontal(lipgloss.Top, renderedTabs...)
doc.WriteString(row)
doc.WriteString("\n")
windowWidth := (lipgloss.Width(row) - windowStyle.GetHorizontalFrameSize())
doc.WriteString(windowStyle.Width(windowWidth).Render(m.tables[m.currTable].View()))
doc.WriteString("\n")
// Is this correct??
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) {
tables := m.GetTables()
res := make([]table.Model, len(tables))
resNames := make([]string, len(tables))
for j, t := range tables {
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),
m.focused = m.lastFocused
if m.focused == EDITOR {
m.editor.Focus()
}
}
}
case tea.WindowSizeMsg:
m = m.updateStyles(msg.Width, msg.Height)
cmds = tea.Batch(cmds, m.GetFirstTable)
for k := range 50 {
row := engine.Row{Values: make([]string, len(t.Columns))}
for i := range t.Columns {
row.Values[i] = "test " + strconv.Itoa(k)
}
t.Rows = append(t.Rows, row)
}
case tableMsg:
m.table.Offset(0)
m.table.ClearRows()
m.table.Headers(msg.Columns...)
m.table.Rows(msg.Rows...)
rows := make([]table.Row, len(t.Rows))
for i, row := range t.Rows {
rows[i] = row.Values
}
resTable := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(25),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
resTable.SetStyles(s)
resNames[j] = t.Name
res[j] = resTable
case tableDefinitionsMsg:
pickerCmd = m.picker.SetItems(msg.Tables)
}
return resNames, res
switch m.focused {
case EDITOR:
m.editor.Focus()
m.editor, edCmd = m.editor.Update(msg)
case PICKER:
m.picker, pickerCmd = m.picker.Update(msg)
}
return m, tea.Batch(edCmd, cmds, pickerCmd)
}
func (m mainModel) updateStyles(width, height int) mainModel {
m.width = width
m.height = height
h, v := defaultStyle.GetFrameSize()
topHeight := math.Ceil(float64(m.height)*3/4 - float64(h))
editorHeight := math.Ceil(float64(m.height)*1/4 - float64(h))
m.editor.SetWidth(m.width - v)
m.editor.SetHeight(int(editorHeight))
m.table.Width(m.width - v)
m.table.Height(int(topHeight) - h*2)
m.picker.SetSize(m.width*7/10, m.height*7/10)
return m
}
func (m mainModel) View() string {
var (
view, editor string
)
h, _ := defaultStyle.GetFrameSize()
topHeight := (m.height * 3 / 4) - h
editorHeight := (m.height * 1 / 4) - h
switch m.focused {
case EDITOR:
view = defaultStyle.
Height(topHeight).
Render(m.table.Render())
editor = focusedStyle.
Height(editorHeight).
Render(m.editor.View())
case TABLE:
view = focusedStyle.
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())
}
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() {
file, err := os.ReadFile("test.sql")
f, err := tea.LogToFile("debug.log", "debug")
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))
m.Start()
err = m.Start()
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
tableNames, tables := convertToViewTable(m)
table.New()
ta := textarea.New()
ta.Placeholder = "select * from ?"
ta.Focus()
ta.Prompt = "| "
ta.CharLimit = 280
p := tea.NewProgram(model{tables: tables, tableNames: tableNames, textarea: ta}, tea.WithAltScreen())
p := tea.NewProgram(newMainModerl(m), tea.WithAltScreen())
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
import (
"flag"
"log"
engine "git.pablu.de/pablu/sqv-engine"
@@ -8,72 +9,170 @@ import (
"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() {
flag.Parse()
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)
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 {
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()
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")
}
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)
if err != nil {
panic(err)
if !ok {
panic("AHHHHHHH")
}
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))
}
}
populateTable(tableView, t)
})
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.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)
}

7
go.mod
View File

@@ -3,7 +3,6 @@ module git.pablu.de/pablu/sqv-engine
go 1.25.3
require (
github.com/charmbracelet/lipgloss v1.1.0
github.com/gdamore/tcell/v2 v2.8.1
github.com/mattn/go-sqlite3 v1.14.32
github.com/rivo/tview v0.42.0
@@ -18,6 +17,7 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/mattn/go-isatty v0.0.20 // 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/termenv v0.16.0 // 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
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.25.0 // indirect
)
@@ -34,5 +36,8 @@ require (
require (
github.com/charmbracelet/bubbles v0.21.0
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
)

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/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
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-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/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
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/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
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/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/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/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
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/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/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.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
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/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
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-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
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.8.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)
}
schema := strings.Join(sqls, ";")
schema := strings.Join(sqls, ";\n")
schema += ";"
// fmt.Println(schema)
return &Manager{
parser: engine.NewParser(strings.NewReader(schema)),
@@ -62,10 +61,10 @@ func NewManager(path string) (*Manager, error) {
}
func (m *Manager) Start() error {
createTableStatements := make([]*engine.CreateTableStatement, 0)
for {
stmt, err := m.parser.Parse()
if err != nil && errors.Is(err, io.EOF) {
fmt.Println("Finished parsing")
break
} else if err != nil {
return err
@@ -78,12 +77,21 @@ func (m *Manager) Start() error {
return err
}
m.tables = append(m.tables, t)
createTableStatements = append(createTableStatements, v)
default:
panic("NOT IMPLEMENTED")
}
}
// Rethink how to do this cleanly
for _, cts := range createTableStatements {
err := m.references(cts)
if err != nil {
return err
}
}
return nil
}
@@ -111,27 +119,124 @@ func (m *Manager) GetTable(name string) (Table, bool) {
return table, true
}
func (m *Manager) LoadTable(table *Table) error {
rows, err := m.conn.Query(fmt.Sprintf("SELECT * FROM %v", table.Name))
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
}
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 {
return err
}
table.Rows = make([]Row, 0)
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")
cols := make([]any, len(fields))
for i, column := range fields {
if column.Flags.Has(NOT_NULL) {
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")
}
} 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")
}
}
}
@@ -148,6 +253,16 @@ func (m *Manager) LoadTable(table *Table) error {
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 {
res := make([]string, len(a))
@@ -160,7 +275,30 @@ func anyToStr(a []any) []string {
case *float64:
res[i] = strconv.FormatFloat(*v, 'f', 2, 64)
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:
panic("THIS SHOULD NEVER HAPPEN, WE GOT SERVED AN UNKNOWN TYPE")
}
@@ -169,6 +307,43 @@ func anyToStr(a []any) []string {
return res
}
func (m *Manager) references(cts *engine.CreateTableStatement) error {
table, ok := m.GetTable(cts.TableName)
if !ok {
return fmt.Errorf("No table with name found, name: %v", cts.TableName)
}
for i, column := range cts.Columns {
flags := extrasToFlags(column.Extra)
if !flags.Has(FOREIGN_KEY) {
continue
}
index := slices.IndexFunc(column.Extra, func(c string) bool {
return strings.HasPrefix(c, "ref")
})
refExtra := column.Extra[index]
refStr := strings.Split(refExtra, " ")[1]
s := strings.Split(refStr, ".")
tableName := s[0]
columnName := s[1]
refTable, ok := m.GetTable(tableName)
if !ok {
return fmt.Errorf("Reference table '%v' not found", tableName)
} else {
colIndex := slices.IndexFunc(refTable.Columns, func(c Column) bool {
return c.Name == columnName
})
table.Columns[i].Reference = &refTable.Columns[colIndex]
}
}
return nil
}
func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStatement) (Table, error) {
res := Table{
Name: cts.TableName,
@@ -178,31 +353,6 @@ func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStat
for i, column := range cts.Columns {
flags := extrasToFlags(column.Extra)
var ref *Column = nil
if flags.Has(FOREIGN_KEY) {
index := slices.IndexFunc(column.Extra, func(c string) bool {
return strings.HasPrefix(c, "ref")
})
refExtra := column.Extra[index]
refStr := strings.Split(refExtra, " ")[1]
s := strings.Split(refStr, ".")
tableName := s[0]
columnName := s[1]
refTable, ok := m.GetTable(tableName)
if !ok {
fmt.Println(m.tables)
return Table{}, fmt.Errorf("Reference table '%v' not found", tableName)
}
colIndex := slices.IndexFunc(refTable.Columns, func(c Column) bool {
return c.Name == columnName
})
ref = &refTable.Columns[colIndex]
}
var columnType ColumnType
switch column.Type {
case "REAL":
@@ -211,17 +361,16 @@ func (m *Manager) convertCreateTableStatementToTable(cts *engine.CreateTableStat
columnType = BLOB
case "TEXT":
columnType = TEXT
case "INTEGER":
case "INTEGER", "NUMERIC":
columnType = INTEGER
default:
panic("This shouldnt happen")
}
res.Columns[i] = Column{
Type: columnType,
Name: column.Name,
Reference: ref,
Flags: flags,
Type: columnType,
Name: column.Name,
Flags: flags,
}
}
@@ -240,6 +389,8 @@ func extrasToFlags(extras []string) ColumnFlag {
res |= FOREIGN_KEY
case "NOT_NULL":
res |= NOT_NULL
case "AUTOINCREMENT":
res |= AUTO_INCREMENT
default:
log.Panicf("NOT IMPLEMENTED EXTRA: %v", extra)
}

View File

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

View File

@@ -21,11 +21,23 @@ const (
COMMA
ASTERIKS
ASSIGN
BACKQUOTE
QUOTE
SINGLE_QUOTE
// TYPES
TYPE_NUMERIC
TYPE_TEXT
// Keywords
CREATE
TABLE
INSERT
INTO
VALUES
RETURNING
SELECT
FROM
WHERE
@@ -34,6 +46,8 @@ const (
ORDER
TOP
DELETE
PRIMARY
FOREIGN
REFERENCES
@@ -41,36 +55,47 @@ const (
NOT
IF
EXISTS
AUTOINCREMENT
CONSTRAINT
TEXT
INTEGER
NULL
REAL
BLOB
NUMERIC
)
var keywords map[string]Token = map[string]Token{
"CREATE": CREATE,
"TABLE": TABLE,
"PRIMARY": PRIMARY,
"FOREIGN": FOREIGN,
"REFERENCES": REFERENCES,
"KEY": KEY,
"NOT": NOT,
"TEXT": TEXT,
"INTEGER": INTEGER,
"NULL": NULL,
"IF": IF,
"EXISTS": EXISTS,
"SELECT": SELECT,
"FROM": FROM,
"WHERE": WHERE,
"AND": AND,
"OR": OR,
"ORDER": ORDER,
"TOP": TOP,
"REAL": REAL,
"BLOB": BLOB,
"CREATE": CREATE,
"TABLE": TABLE,
"PRIMARY": PRIMARY,
"FOREIGN": FOREIGN,
"REFERENCES": REFERENCES,
"KEY": KEY,
"NOT": NOT,
"TEXT": TEXT,
"INTEGER": INTEGER,
"NULL": NULL,
"IF": IF,
"EXISTS": EXISTS,
"SELECT": SELECT,
"FROM": FROM,
"WHERE": WHERE,
"AND": AND,
"OR": OR,
"ORDER": ORDER,
"TOP": TOP,
"REAL": REAL,
"BLOB": BLOB,
"AUTOINCREMENT": AUTOINCREMENT,
"CONSTRAINT": CONSTRAINT,
"NUMERIC": NUMERIC,
"INSERT": INSERT,
"INTO": INTO,
"VALUES": VALUES,
"RETURNING": RETURNING,
"DELETE": DELETE,
}
type Position struct {
@@ -116,6 +141,12 @@ func (l *Lexer) Lex() (Position, Token, string) {
return l.pos, ASTERIKS, "*"
case '=':
return l.pos, ASSIGN, "="
case '`':
return l.pos, BACKQUOTE, "`"
case '"':
return l.pos, QUOTE, "\""
case '\'':
return l.pos, SINGLE_QUOTE, "'"
default:
if unicode.IsSpace(r) {
continue
@@ -130,6 +161,12 @@ func (l *Lexer) Lex() (Position, Token, string) {
}
return startPos, IDENT, lit
} else if unicode.IsNumber(r) {
startPos := l.pos
l.backup()
lit := l.lexIdent()
return startPos, TYPE_NUMERIC, lit
} else {
return l.pos, ILLEGAL, string(r)
}
@@ -149,7 +186,9 @@ func (l *Lexer) lexIdent() string {
}
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
lit = lit + string(r)
} 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) {
tok, ok := p.expectOne(CREATE, EOF)
tok, ok := p.expectOne(CREATE, EOF, SELECT, INSERT, DELETE)
if !ok {
return nil, p.unexpectedToken(CREATE, EOF)
return nil, p.unexpectedToken(CREATE, EOF, SELECT, INSERT, DELETE)
} else if tok == 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 {
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)
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:
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)}
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)
}
switch tok {
case EOF:
return nil, io.EOF
case CREATE:
return p.parseCreateTable()
case SELECT:
return p.parseSelect()
case INSERT:
return p.parseInsert()
case DELETE:
return p.parseDelete()
default:
panic("SHOULD NEVER BE REACHED")
}
}
@@ -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 {
for _, tok := range token {
if !p.expectNext(tok) {
@@ -237,6 +93,48 @@ 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) 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) {
if p.buf.avail {
p.buf.avail = false

View File

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