Compare commits
12 Commits
6182129022
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0f7d74adc | ||
|
|
3dd1cfef55 | ||
|
|
555ca1f6ab | ||
|
|
1af6539deb | ||
|
|
a8c7ad60c3 | ||
|
|
e07bb9e496 | ||
|
|
f6ca16b1f0 | ||
|
|
b7147d03c2 | ||
|
|
9c51424bf8 | ||
|
|
c4218cee51 | ||
|
|
c41b4cc5da | ||
|
|
694bbb7934 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
db.*
|
||||
main
|
||||
debug.log
|
||||
|
||||
5
TASKS.md
Normal file
5
TASKS.md
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
208
cmd/sqv-tea/util.go
Normal 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)
|
||||
@@ -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
7
go.mod
@@ -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
22
go.sum
@@ -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=
|
||||
|
||||
249
manager.go
249
manager.go
@@ -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)
|
||||
}
|
||||
|
||||
12
sql/ast.go
12
sql/ast.go
@@ -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() {}
|
||||
|
||||
83
sql/lexer.go
83
sql/lexer.go
@@ -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
219
sql/parseCreateStatement.go
Normal 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
|
||||
}
|
||||
15
sql/parseDeleteStatement.go
Normal file
15
sql/parseDeleteStatement.go
Normal 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
|
||||
}
|
||||
69
sql/parseInsertStatement.go
Normal file
69
sql/parseInsertStatement.go
Normal 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
|
||||
}
|
||||
47
sql/parseSelectStatement.go
Normal file
47
sql/parseSelectStatement.go
Normal 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
|
||||
}
|
||||
216
sql/parser.go
216
sql/parser.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user