Skip to content

Commit

Permalink
Add support for binary expressions, filtering via WHERE (eatonphil#3)
Browse files Browse the repository at this point in the history
* Start support for binary expressions

* Everything building, tests passing

* Gofmt

* Filtering generally working

* Update readme, add tablewriter library

* Add readline support

* Add makefile and more info to readme
  • Loading branch information
eatonphil authored Apr 1, 2020
1 parent d0aec47 commit bd6a5d0
Show file tree
Hide file tree
Showing 13 changed files with 779 additions and 269 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fmt:
gofmt -w -s .

test:
go test -race .
72 changes: 59 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# gosql

An early PostgreSQL implementation in Go (WIP)
An early PostgreSQL implementation in Go.

## Example

Expand All @@ -9,21 +9,67 @@ $ git clone [email protected]:eatonphil/gosql
$ cd gosql
$ go run cmd/main.go
Welcome to gosql.
# CREATE TABLE users (id INT, name TEXT);
# CREATE TABLE users (name TEXT, age INT);
ok
# INSERT INTO users VALUES (1, 'Phil');
# INSERT INTO users VALUES ('Stephen', 16);
ok
# SELECT id, name FROM users;
| id | name |
====================
| 1 | Phil |
# SELECT name, age FROM users;
name | age
----------+------
Stephen | 16
(1 result)
ok
# INSERT INTO users VALUES (2, 'Kate');
# INSERT INTO users VALUES ('Adrienne', 23);
ok
# SELECT name, id FROM users;
| name | id |
====================
| Phil | 1 |
| Kate | 2 |
# SELECT age + 2, name FROM users WHERE age = 23;
age | name
------+-----------
25 | Adrienne
(1 result)
ok
# SELECT name FROM users;
name
------------
Stephen
Adrienne
(2 results)
ok
```

## Architecture

* [cmd/main.go](./cmd/main.go)
* Contains the REPL and high-level interface to the project
* Dataflow is: user input -> lexer -> parser -> in-memory backend
* [lexer.go](./lexer.go)
* Handles breaking user input into tokens for the parser
* [parser.go](./parser.go)
* Matches a list of tokens into an AST or fails if the user input is not a valid program
* [memory.go](./memory.go)
* An example, in-memory backend supporting the Backend interface (defined in backend.go)

## Contributing

* Add a new operator (such as `<`, `>`, etc.) supported by PostgreSQL
* Add a new data type supported by PostgreSQL

In each case, you'll probably have to add support in the lexer,
parser, and in-memory backend. I recommend going in that order.

In all cases, make sure the code is formatted (`make fmt`) and passes
tests (`make test`). New code should have tests.

## Blog series

* [https://notes.eatonphil.com/database-basics.html](Writing a SQL database from scratch in Go)

## Further reading

Here are some similar projects written in Go.

* [go-mysql-server](https://github.com/src-d/go-mysql-server)
* This is a MySQL frontend (with an in-memory backend for testing only).
* [ramsql](https://github.com/proullon/ramsql)
* This is a WIP PostgreSQL-compatible in-memory database.
* [CockroachDB](https://github.com/cockroachdb/cockroach)
* This is a production-ready PostgreSQL-compatible database.
13 changes: 11 additions & 2 deletions ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ type expressionKind uint

const (
literalKind expressionKind = iota
binaryKind
)

type binaryExpression struct {
a expression
b expression
op token
}

type expression struct {
literal *token
binary *binaryExpression
kind expressionKind
}

Expand All @@ -24,8 +32,9 @@ type fromItem struct {
}

type SelectStatement struct {
item *[]*selectItem
from *fromItem
item *[]*selectItem
from *fromItem
where *expression
}

type columnDefinition struct {
Expand Down
2 changes: 2 additions & 0 deletions interface.go → backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ type ColumnType uint
const (
TextType ColumnType = iota
IntType
BoolType
)

type Cell interface {
AsText() string
AsInt() int32
AsBool() bool
}

type Results struct {
Expand Down
143 changes: 98 additions & 45 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,134 @@
package main

import (
"bufio"
"fmt"
"io"
"os"
"strings"

"github.com/eatonphil/gosql"

"github.com/chzyer/readline"
"github.com/olekukonko/tablewriter"
)

func doSelect(mb gosql.Backend, slct *gosql.SelectStatement) error {
results, err := mb.Select(slct)
if err != nil {
return err
}

if len(results.Rows) == 0 {
fmt.Println("(no results)")
return nil
}

table := tablewriter.NewWriter(os.Stdout)
header := []string{}
for _, col := range results.Columns {
header = append(header, col.Name)
}
table.SetHeader(header)
table.SetAutoFormatHeaders(false)

rows := [][]string{}
for _, result := range results.Rows {
row := []string{}
for i, cell := range result {
typ := results.Columns[i].Type
s := ""
switch typ {
case gosql.IntType:
s = fmt.Sprintf("%d", cell.AsInt())
case gosql.TextType:
s = cell.AsText()
case gosql.BoolType:
s = "true"
if !cell.AsBool() {
s = "false"
}
}

row = append(row, s)
}

rows = append(rows, row)
}

table.SetBorder(false)
table.AppendBulk(rows)
table.Render()

if len(rows) == 1 {
fmt.Println("(1 result)")
} else {
fmt.Printf("(%d results)\n", len(rows))
}

return nil
}

func main() {
mb := gosql.NewMemoryBackend()

reader := bufio.NewReader(os.Stdin)
l, err := readline.NewEx(&readline.Config{
Prompt: "# ",
HistoryFile: "/tmp/gosql.tmp",
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
panic(err)
}
defer l.Close()

fmt.Println("Welcome to gosql.")
repl:
for {
fmt.Print("# ")
text, err := reader.ReadString('\n')
text = strings.Replace(text, "\n", "", -1)
line, err := l.Readline()
if err == readline.ErrInterrupt {
if len(line) == 0 {
break
} else {
continue repl
}
} else if err == io.EOF {
break
}
if err != nil {
fmt.Println("Error while reading line:", err)
continue repl
}

ast, err := gosql.Parse(text)
ast, err := gosql.Parse(line)
if err != nil {
panic(err)
fmt.Println("Error while parsing:", err)
continue repl
}

for _, stmt := range ast.Statements {
switch stmt.Kind {
case gosql.CreateTableKind:
err = mb.CreateTable(ast.Statements[0].CreateTableStatement)
if err != nil {
panic(err)
fmt.Println("Error creating table", err)
continue repl
}
fmt.Println("ok")
case gosql.InsertKind:
err = mb.Insert(stmt.InsertStatement)
if err != nil {
panic(err)
fmt.Println("Error inserting values:", err)
continue repl
}

fmt.Println("ok")
case gosql.SelectKind:
results, err := mb.Select(stmt.SelectStatement)
err := doSelect(mb, stmt.SelectStatement)
if err != nil {
panic(err)
}

for _, col := range results.Columns {
fmt.Printf("| %s ", col.Name)
fmt.Println("Error selecting values:", err)
continue repl
}
fmt.Println("|")

for i := 0; i < 20; i++ {
fmt.Printf("=")
}
fmt.Println()

for _, result := range results.Rows {
fmt.Printf("|")

for i, cell := range result {
typ := results.Columns[i].Type
s := ""
switch typ {
case gosql.IntType:
s = fmt.Sprintf("%d", cell.AsInt())
case gosql.TextType:
s = cell.AsText()
}

fmt.Printf(" %s | ", s)
}

fmt.Println()
}

fmt.Println("ok")
}
}

fmt.Println("ok")
}
}
2 changes: 2 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ var (
ErrInvalidSelectItem = errors.New("Select item is not valid")
ErrInvalidDatatype = errors.New("Invalid datatype")
ErrMissingValues = errors.New("Missing values")
ErrInvalidCell = errors.New("Cell is invalid")
ErrInvalidOperands = errors.New("Operands are invalid")
)
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@ module github.com/eatonphil/gosql

go 1.13

require github.com/stretchr/testify v1.5.1
require (
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/stretchr/testify v1.5.1
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
Loading

0 comments on commit bd6a5d0

Please sign in to comment.