Initial working stage, but not with all tests, because IF NOT EXISTS is not implemented

This commit is contained in:
Pablu
2025-10-28 11:29:26 +01:00
commit 15f0d190dc
7 changed files with 526 additions and 0 deletions

79
cmd/sqv/main.go Normal file
View File

@@ -0,0 +1,79 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"strings"
"git.pablu.de/pablu/sqv-engine/sql"
)
func main() {
s := `
CREATE TABLE TEST(
ID text PRIMARY KEY
);
CREATE TABLE sessions (
session_id text PRIMARY KEY,
access_token text NOT NULL,
user_email text NOT NULL,
FOREIGN KEY(user_email) REFERENCES users(email)
);
CREATE TABLE IF NOT EXISTS users (
email text PRIMARY KEY,
username text
);
CREATE TABLE IF NOT EXISTS sessions (
session_id text PRIMARY KEY,
access_token text NOT NULL,
user_email text NOT NULL,
FOREIGN KEY(user_email) REFERENCES users(email)
);
CREATE TABLE IF NOT EXISTS game_settings (
lobby_id text PRIMARY KEY,
max_players integer,
game_mode text,
selected_playlist_id text
);
CREATE TABLE IF NOT EXISTS lobbys (
lobby_id text PRIMARY KEY,
host_email text,
game_settings_id text,
FOREIGN KEY(game_settings_id) REFERENCES game_settings(lobby_id),
FOREIGN KEY(host_email) REFERENCES users(email)
);
CREATE TABLE IF NOT EXISTS users_in_lobbys (
user_email text PRIMARY KEY,
lobby_id text,
FOREIGN KEY(user_email) REFERENCES users(email),
FOREIGN KEY(lobby_id) REFERENCES lobbys(lobby_id)
);
CREATE TABLE IF NOT EXISTS auth_states (
state_id text PRIMARY KEY,
code_verifier text NOT NULL
);
`
parser := sql.NewParser(strings.NewReader(s))
for {
stmt, err := parser.Parse()
if err != nil && errors.Is(err, io.EOF) {
fmt.Println("Finished parsing")
break
} else if err != nil {
log.Fatal(err)
}
stmt.Print()
}
}

6
go.mod Normal file
View File

@@ -0,0 +1,6 @@
module git.pablu.de/pablu/sqv-engine
go 1.25.3
require github.com/rqlite/sql v0.0.0-20250623131620-453fa49cad04

32
go.sum Normal file
View File

@@ -0,0 +1,32 @@
github.com/AllenDang/cimgui-go v1.3.2-0.20250409185506-6b2ff1aa26b5 h1:IulPfLSfrjCzSr7TOn7777rl5qRMu39gEOxSTDm2wes=
github.com/AllenDang/cimgui-go v1.3.2-0.20250409185506-6b2ff1aa26b5/go.mod h1:Fg1LjMFQs91yohW3SccUhqUeuNQEeL5KX3mgEjnDh7A=
github.com/AllenDang/giu v0.14.1 h1:SqMt2peaqb3wWPkZ7m+dnJX4LddZNDduYz5jpo+LOxQ=
github.com/AllenDang/giu v0.14.1/go.mod h1:gD5hmav1LbkjL2/1O3Mpohl2Lb/uqrrOKSeZFeqQjD0=
github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 h1:dKZMqib/yUDoCFigmz2agG8geZ/e3iRq304/KJXqKyw=
github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8/go.mod h1:b4uuDd0s6KRIPa84cEEchdQ9ICh7K0OryZHbSzMca9k=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
github.com/gucio321/glm-go v0.0.0-20241029220517-e1b5a3e011c8 h1:aczNwZRrReVWrZcqxvDjDmxP1NFISTAu+1Cp+3OCbUg=
github.com/gucio321/glm-go v0.0.0-20241029220517-e1b5a3e011c8/go.mod h1:Z3+NtD1rjXUVZg97dojhs70i5oneOrZ1xcFKfF/c2Ts=
github.com/mazznoer/csscolorparser v0.1.6 h1:uK6p5zBA8HaQZJSInHgHVmkVBodUAy+6snSmKJG7pqA=
github.com/mazznoer/csscolorparser v0.1.6/go.mod h1:OQRVvgCyHDCAquR1YWfSwwaDcM0LhnSffGnlbOew/3I=
github.com/napsy/go-css v1.0.0 h1:I1EiqpOJqo8eshGhm6OQXefXOfNgnp1SLOVfqcTeY2U=
github.com/napsy/go-css v1.0.0/go.mod h1:HqZYcKcNnv50fgOTdGUn9YbJa2qC9oJ3kLnyrwwVzUI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/rqlite/sql v0.0.0-20250623131620-453fa49cad04 h1:bjr7gZERAJhYhqkLHXbkBSpjbB+PlgW6e9CRCJ2+J/w=
github.com/rqlite/sql v0.0.0-20250623131620-453fa49cad04/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.design/x/hotkey v0.4.1 h1:zLP/2Pztl4WjyxURdW84GoZ5LUrr6hr69CzJFJ5U1go=
golang.design/x/hotkey v0.4.1/go.mod h1:M8SGcwFYHnKRa83FpTFQoZvPO5vVT+kWPztFqTQKmXA=
golang.design/x/mainthread v0.3.0 h1:UwFus0lcPodNpMOGoQMe87jSFwbSsEY//CA7yVmu4j8=
golang.design/x/mainthread v0.3.0/go.mod h1:vYX7cF2b3pTJMGM/hc13NmN6kblKnf4/IyvHeu259L0=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc=
gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU=

24
sql/ast.go Normal file
View File

@@ -0,0 +1,24 @@
package sql
import "fmt"
type CreateTableStatement struct {
TableName string
Columns []Column
}
func (c *CreateTableStatement) Print() {
fmt.Printf("Name: %v\nColumns:\n", c.TableName)
for _, column := range c.Columns {
fmt.Printf("- Name: %v\n Type: %v\n Extras:\n", column.Name, column.Type)
for _, extra := range column.Extra {
fmt.Printf(" - %v\n", extra)
}
}
}
type Column struct {
Name string
Type string
Extra []string
}

143
sql/lexer.go Normal file
View File

@@ -0,0 +1,143 @@
package sql
import (
"bufio"
"errors"
"io"
"strings"
"unicode"
)
type Token int
const (
EOF Token = iota
ILLEGAL
IDENT
SEMI
LPAREN
RPAREN
COMMA
CREATE
TABLE
PRIMARY
FOREIGN
REFERENCES
KEY
NOT
TEXT
INTEGER
NULL
)
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,
}
type Position struct {
line int
column int
}
type Lexer struct {
pos Position
reader *bufio.Reader
}
func NewLexer(reader io.Reader) *Lexer {
return &Lexer{
pos: Position{line: 1, column: 0},
reader: bufio.NewReader(reader),
}
}
func (l *Lexer) Lex() (Position, Token, string) {
for {
r, _, err := l.reader.ReadRune()
if err != nil && errors.Is(err, io.EOF) {
return l.pos, EOF, ""
} else if err != nil {
// Change this
panic(err)
}
l.pos.column++
switch r {
case '\n':
l.resetPosition()
case ';':
return l.pos, SEMI, ";"
case ',':
return l.pos, COMMA, ","
case '(':
return l.pos, LPAREN, "("
case ')':
return l.pos, RPAREN, ")"
default:
if unicode.IsSpace(r) {
continue
} else if unicode.IsLetter(r) {
startPos := l.pos
l.backup()
lit := l.lexIdent()
litUpper := strings.ToUpper(lit)
if token, ok := keywords[litUpper]; ok {
return startPos, token, litUpper
}
return startPos, IDENT, lit
} else {
return l.pos, ILLEGAL, string(r)
}
}
}
}
func (l *Lexer) lexIdent() string {
var lit string
for {
r, _, err := l.reader.ReadRune()
if err != nil && errors.Is(err, io.EOF) {
return lit
} else if err != nil {
// Change this
panic(err)
}
l.pos.column++
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
// Change this to stringstream or something similar
lit = lit + string(r)
} else {
l.backup()
return lit
}
}
}
func (l *Lexer) backup() {
if err := l.reader.UnreadRune(); err != nil {
panic(err)
}
l.pos.column--
}
func (l *Lexer) resetPosition() {
l.pos.line++
l.pos.column = 0
}

208
sql/parser.go Normal file
View File

@@ -0,0 +1,208 @@
package sql
import (
"errors"
"fmt"
"io"
"slices"
)
type Parser struct {
s *Lexer
last struct {
pos Position
tok Token
lit string
}
}
func NewParser(r io.Reader) *Parser {
return &Parser{s: NewLexer(r)}
}
func (p *Parser) Parse() (*CreateTableStatement, error) {
tok, ok := p.expectOne(CREATE, EOF)
if !ok {
return nil, p.unexpectedToken()
} else if tok == EOF {
return nil, io.EOF
}
if !p.expectNext(TABLE) {
return nil, errors.New("Expect TABLE token")
}
if !p.expectNext(IDENT) {
return nil, errors.New("Expect IDENT token")
}
stmt := CreateTableStatement{
TableName: p.last.lit,
Columns: make([]Column, 0),
}
_, tok, _ = p.scan()
if tok != LPAREN {
return nil, errors.New("Expect LPAREN token")
}
for {
lastTok := p.last.tok
_, tok, _ := p.scan()
switch tok {
case IDENT:
column, err := p.parseColumn()
if err != nil {
return nil, err
}
stmt.Columns = append(stmt.Columns, column)
case RPAREN:
if !p.expectNext(SEMI) {
return nil, p.unexpectedToken()
}
return &stmt, nil
case SEMI:
if lastTok != RPAREN {
return nil, p.unexpectedToken()
}
return &stmt, nil
case FOREIGN:
if !p.expectNext(KEY) {
return nil, p.unexpectedToken()
}
if !p.expectNext(LPAREN) {
return nil, p.unexpectedToken()
}
if !p.expectNext(IDENT) {
return nil, p.unexpectedToken()
}
columnName := p.last.lit
if !p.expectNext(RPAREN) {
return nil, p.unexpectedToken()
}
if !p.expectNext(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)
default:
return nil, p.unexpectedToken()
}
}
}
func (p *Parser) parseColumn() (Column, error) {
column := Column{Name: p.last.lit, Extra: make([]string, 0)}
_, tok, lit := p.scan()
switch tok {
case TEXT:
fallthrough
case INTEGER:
column.Type = lit
default:
return Column{}, p.unexpectedToken()
}
for {
_, tok, lit := p.scan()
switch tok {
case COMMA:
fallthrough
case RPAREN:
return column, nil
case PRIMARY:
fallthrough
case NOT:
if _, ok := p.expectOne(NULL, KEY); !ok {
return Column{}, p.unexpectedToken()
}
column.Extra = append(column.Extra, fmt.Sprintf("%v_%v", lit, p.last.lit))
case REFERENCES:
ref, err := p.references()
if err != nil {
return Column{}, err
}
column.Extra = append(column.Extra, ref)
default:
return Column{}, p.unexpectedToken()
}
}
}
func (p *Parser) unexpectedToken() error {
return fmt.Errorf("Encountered unexpected token: %v lit: '%v' on pos: %v", p.last.tok, p.last.lit, p.last.pos)
}
func (p *Parser) references() (string, error) {
if !p.expectNext(IDENT) {
return "", p.unexpectedToken()
}
referenceTableName := p.last.lit
if !p.expectNext(LPAREN) {
return "", p.unexpectedToken()
}
if !p.expectNext(IDENT) {
return "", p.unexpectedToken()
}
referenceColumnName := p.last.lit
if !p.expectNext(RPAREN) {
return "", p.unexpectedToken()
}
return fmt.Sprintf("ref %v.%v", referenceTableName, referenceColumnName), nil
}
func (p *Parser) expectNext(token Token) bool {
_, tok, _ := p.scan()
return tok == token
}
func (p *Parser) expectOne(token ...Token) (Token, bool) {
_, tok, _ := p.scan()
ok := slices.ContainsFunc(token, func(t Token) bool {
return tok == t
})
return tok, ok
}
func (p *Parser) scan() (Position, Token, string) {
pos, tok, lit := p.s.Lex()
// fmt.Printf("Scanning next Token: %v | pos: %v | lit: %v\n", tok, pos, lit)
p.last = struct {
pos Position
tok Token
lit string
}{
pos,
tok,
lit,
}
return pos, tok, lit
}

34
table.go Normal file
View File

@@ -0,0 +1,34 @@
package engine
type Table struct {
Name string
TableValues []TableValue
Rows []Row
}
type ValueFlags uint32
const (
PRIMARY_KEY ValueFlags = 1 << iota
FOREIGN_KEY
NOT_NULL
)
func (v ValueFlags) Has(flag ValueFlags) bool {
return v&flag == flag
}
type TableValue struct {
Type string
Name string
Reference *TableValue
Flags ValueFlags
}
type Value interface {
Representation() string
}
type Row struct {
Values map[TableValue]Value
}