From 15f0d190dc7bf5836d3408c55415b867e1a38693 Mon Sep 17 00:00:00 2001 From: Pablu Date: Tue, 28 Oct 2025 11:29:26 +0100 Subject: [PATCH] Initial working stage, but not with all tests, because IF NOT EXISTS is not implemented --- cmd/sqv/main.go | 79 ++++++++++++++++++ go.mod | 6 ++ go.sum | 32 ++++++++ sql/ast.go | 24 ++++++ sql/lexer.go | 143 +++++++++++++++++++++++++++++++++ sql/parser.go | 208 ++++++++++++++++++++++++++++++++++++++++++++++++ table.go | 34 ++++++++ 7 files changed, 526 insertions(+) create mode 100644 cmd/sqv/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 sql/ast.go create mode 100644 sql/lexer.go create mode 100644 sql/parser.go create mode 100644 table.go diff --git a/cmd/sqv/main.go b/cmd/sqv/main.go new file mode 100644 index 0000000..f881c61 --- /dev/null +++ b/cmd/sqv/main.go @@ -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() + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bde28c3 --- /dev/null +++ b/go.mod @@ -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 + diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eaa7a5f --- /dev/null +++ b/go.sum @@ -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= diff --git a/sql/ast.go b/sql/ast.go new file mode 100644 index 0000000..75f394a --- /dev/null +++ b/sql/ast.go @@ -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 +} diff --git a/sql/lexer.go b/sql/lexer.go new file mode 100644 index 0000000..f1d9115 --- /dev/null +++ b/sql/lexer.go @@ -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 +} diff --git a/sql/parser.go b/sql/parser.go new file mode 100644 index 0000000..af157fd --- /dev/null +++ b/sql/parser.go @@ -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 +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..08528d4 --- /dev/null +++ b/table.go @@ -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 +}