Compare commits

...
10 Commits
10 changed files with 405 additions and 148 deletions
+1
View File
@@ -0,0 +1 @@
tmp
+2 -142
View File
@@ -1,149 +1,9 @@
package main package main
import ( import (
"bufio" "godb/internals/repl"
"fmt"
"io"
"os"
"strings"
) )
type metaCommandResults int
type prepareResults int
type statementType int
const (
metaCommandSuccess metaCommandResults = iota
metaCommandUnrecognised
prepareSuccess prepareResults = iota
prepareUnrecognised
prepareSyntaxError
statementSelect statementType = iota
statementInsert
)
type inputBuffer struct {
buffer string
bufferLength int
inputLength int
}
type row struct {
id uint
username string
email string
}
type statement struct {
stype statementType
row row
}
func doMetaCommands(inputBuffer inputBuffer) metaCommandResults {
metacommand := metaCommandUnrecognised
if inputBuffer.buffer == ".exit" {
fmt.Println("bye!")
os.Exit(0)
} else {
metacommand = metaCommandUnrecognised
}
return metacommand
}
func prepareStatements(inputBuffer inputBuffer, statement *statement) prepareResults {
if strings.Split(inputBuffer.buffer, " ")[0] == "insert" {
argsAssigned, err := fmt.Sscanf(inputBuffer.buffer, "insert %v %s %s", &statement.row.id, &statement.row.username, &statement.row.email)
if err != nil {
fmt.Println("error parsing insert statement")
fmt.Println(err.Error())
}
if argsAssigned > 3 {
return prepareSyntaxError
}
statement.stype = statementInsert
return prepareSuccess
}
if strings.Split(inputBuffer.buffer, " ")[0] == "select" {
statement.stype = statementSelect
return prepareSuccess
}
return prepareUnrecognised
}
func executeStatement(statement *statement) {
switch statement.stype {
case statementInsert:
fmt.Println("This is where the logic of insert statement should go")
break
case statementSelect:
fmt.Println("This is where the logic of select statement should go")
break
}
}
func createInputBuffer() inputBuffer {
newInputBuffer := inputBuffer{"", 0, 0}
return newInputBuffer
}
func printConsole() {
fmt.Print("godb > ")
}
func readConsole(inputBuffer inputBuffer) inputBuffer {
var stdin *bufio.Reader
var line []rune
stdin = bufio.NewReader(os.Stdin)
for {
c, _, err := stdin.ReadRune()
if err == io.EOF || c == '\n' {
break
}
if err != nil {
fmt.Println("Error reading input")
os.Exit(1)
}
line = append(line, c)
}
inputBuffer.buffer = string(line)
bytesRead := len(inputBuffer.buffer)
if bytesRead <= 0 {
fmt.Println("Error reading input")
os.Exit(1)
}
inputBuffer.bufferLength = len(inputBuffer.buffer)
inputBuffer.inputLength = bytesRead - 1
return inputBuffer
}
func main() { func main() {
inputBuffer := createInputBuffer() repl.Run()
statement := statement{0, row{0, "", ""}}
condition := true
for condition {
printConsole()
inputBuffer = readConsole(inputBuffer)
if strings.Split(inputBuffer.buffer, "")[0] == "." {
switch doMetaCommands(inputBuffer) {
case metaCommandSuccess:
continue
case metaCommandUnrecognised:
fmt.Printf("unrecognised command %v\n", inputBuffer.buffer)
continue
}
}
switch prepareStatements(inputBuffer, &statement) {
case prepareSuccess:
break
case prepareUnrecognised:
fmt.Printf("Unrecognised keyword at the start of '%s'.\n", inputBuffer.buffer)
continue
}
executeStatement(&statement)
fmt.Println("Executed")
}
} }
+2 -2
View File
@@ -1,3 +1,3 @@
module godb/main module godb
go 1.23.2 go 1.26.3
+30
View File
@@ -0,0 +1,30 @@
package buffer
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
type InputBuffer struct {
Buffer string
Reader *bufio.Reader
}
func NewInputBuffer() *InputBuffer {
return &InputBuffer{
Buffer: "",
Reader: bufio.NewReader(os.Stdin),
}
}
func (inputBuffer *InputBuffer) ReadConsole() {
line, err := inputBuffer.Reader.ReadString('\n')
if err != nil && err != io.EOF {
fmt.Println("Error reading input", err)
os.Exit(1)
}
inputBuffer.Buffer = strings.TrimSpace(line)
}
+23
View File
@@ -0,0 +1,23 @@
package buffer
import (
"bufio"
"strings"
"testing"
)
func TestReadConsole(t *testing.T) {
mockMessage := "Reading for Unit test of readconsole"
inputBuffer := &InputBuffer{
Buffer: "",
Reader: bufio.NewReader(strings.NewReader(mockMessage)),
}
inputBuffer.ReadConsole()
if inputBuffer.Buffer != mockMessage {
t.Errorf("ReadConsole() should be %q but is %q", mockMessage, inputBuffer.Buffer)
}
}
+50
View File
@@ -0,0 +1,50 @@
package repl
import (
"fmt"
"strings"
. "godb/internals/buffer"
. "godb/internals/vm"
)
func PrintConsole() {
fmt.Print("godb > ")
}
func Run() {
inputBuffer := NewInputBuffer()
statement := NewStatement()
table := NewTable()
for {
PrintConsole()
inputBuffer.ReadConsole()
if len(inputBuffer.Buffer) > 0 && strings.HasPrefix(inputBuffer.Buffer, ".") {
switch DoMetaCommands(inputBuffer) {
case MetaCommandSuccess:
continue
case MetaCommandUnrecognised:
fmt.Printf("unrecognised command %v\n", inputBuffer.Buffer)
continue
}
}
switch statement.PrepareStatements(inputBuffer) {
case PrepareSuccess:
//todo
case PrepareSyntaxError:
fmt.Println("Syntax error, could not parse statement")
case PrepareUnrecognised:
fmt.Printf("Unrecognised keyword at the start of '%s'.\n", inputBuffer.Buffer)
}
switch statement.ExecuteStatement(table) {
case ExecuteSuccess:
fmt.Println("Executed.")
case ExecuteTableFull:
fmt.Println("Error: Table is full")
}
}
}
+180
View File
@@ -0,0 +1,180 @@
package vm
import (
"encoding/binary"
"fmt"
"os"
"strings"
. "godb/internals/buffer"
)
type MetaCommandResults int
const (
MetaCommandSuccess MetaCommandResults = iota
MetaCommandUnrecognised
)
type PrepareResults int
const (
PrepareSuccess PrepareResults = iota
PrepareUnrecognised
PrepareSyntaxError
)
type statementType int
const (
StatementSelect statementType = iota
StatementInsert
)
const (
idSize = 8
usernameSize = 32
emailSize = 255
idOffset = 0
usernameOffset = idOffset + idSize
emailOffset = usernameOffset + usernameSize
pageSize = 4096
maxPages = 100
rowSize = idSize + usernameSize + emailSize
rowsPerPage = pageSize / rowSize
maxRows = rowsPerPage * maxPages
)
type ExecuteResult int
const (
ExecuteSuccess ExecuteResult = iota
ExecuteTableFull
)
type row struct {
id uint
username string
email string
}
type Statement struct {
stype statementType
row row
}
type Table struct {
numOfRows uint
pages [maxPages]any
}
func NewStatement() *Statement {
return &Statement{0, row{0, "", ""}}
}
func NewTable() *Table {
newTable := Table{numOfRows: 0}
for i := range maxPages {
newTable.pages[i] = nil
}
return &newTable
}
func rowToUse(table *Table, rowNo uint) []byte {
pageNo := rowNo / rowsPerPage
var page []byte
if table.pages[pageNo] == nil {
page = make([]byte, pageSize)
table.pages[pageNo] = page
} else {
i := table.pages[pageNo]
if t, ok := i.([]byte); !ok {
page = []byte(t)
} else {
page = t
}
}
rowOffset := rowNo % rowsPerPage
byteOffset := rowOffset * rowSize
return page[byteOffset : byteOffset+rowSize]
}
func (row *row) seralizeRow(page []byte) {
binary.NativeEndian.PutUint64(page[idOffset:], uint64(row.id))
usernameBuff := page[usernameOffset : usernameOffset+usernameSize]
clear(usernameBuff)
copy(usernameBuff, row.username)
emailBuff := page[emailOffset : emailOffset+emailSize]
clear(emailBuff)
copy(emailBuff, row.email)
}
func (row *row) deSeralizeRow(page []byte) {
row.id = uint(binary.NativeEndian.Uint64(page[idOffset:idSize]))
row.username = string(page[usernameOffset : usernameOffset+usernameSize])
row.email = string(page[emailOffset : emailOffset+emailSize])
}
func (statement *Statement) execInsert(table *Table) ExecuteResult {
if table.numOfRows >= maxRows {
return ExecuteTableFull
}
statement.row.seralizeRow(rowToUse(table, table.numOfRows))
table.numOfRows += 1
return ExecuteSuccess
}
func (statement *Statement) execSelect(table *Table) ExecuteResult {
var row row
for i := range table.numOfRows {
row.deSeralizeRow(rowToUse(table, i))
fmt.Printf("%v\n", row)
}
return ExecuteSuccess
}
func DoMetaCommands(inputBuffer *InputBuffer) MetaCommandResults {
metacommand := MetaCommandUnrecognised
if inputBuffer.Buffer == ".exit" {
fmt.Println("bye!")
os.Exit(0)
} else {
metacommand = MetaCommandUnrecognised
}
return metacommand
}
func (statement *Statement) PrepareStatements(inputBuffer *InputBuffer) PrepareResults {
if strings.HasPrefix(inputBuffer.Buffer, "insert") {
argsAssigned, err := fmt.Sscanf(inputBuffer.Buffer, "insert %d %s %s", &statement.row.id, &statement.row.username, &statement.row.email)
// if err != nil {
// fmt.Println("error parsing insert statement")
// fmt.Println(err.Error())
// }
if err != nil || argsAssigned > 3 {
return PrepareSyntaxError
}
statement.stype = StatementInsert
return PrepareSuccess
}
if strings.HasPrefix(inputBuffer.Buffer, "select") {
statement.stype = StatementSelect
return PrepareSuccess
}
return PrepareUnrecognised
}
func (statement *Statement) ExecuteStatement(table *Table) ExecuteResult {
var result ExecuteResult
switch statement.stype {
case StatementInsert:
result = statement.execInsert(table)
case StatementSelect:
result = statement.execSelect(table)
}
return result
}
+12 -4
View File
@@ -4,15 +4,23 @@ run:
@go run cmd/godb/main.go @go run cmd/godb/main.go
build: build:
@go build -o ${BINARY_NAME} cmd/godb/main.go @go build -o tmp/${BINARY_NAME} cmd/godb/main.go
start: start:
@./${BINARY_NAME} @./tmp/${BINARY_NAME}
test-unit:
@go test ./internals/... -v
test-inte:
@go test ./tests/... -v
test: build test-unit test-inte
clean: clean:
@go clean @go clean
@rm -rf ${BINARY_NAME} @rm -rf ${BINARY_NAME}
reset: clean build reset: clean build test clean
.PHONY: clean build .PHONY: clean build test
+20
View File
@@ -1,3 +1,23 @@
### Godb ### Godb
A SQLite like database made in golang A SQLite like database made in golang
Inorder to run this locally, after cloning this repo run `go mod tidy`
Then use the appropriate `make` command listed below
Run
```make run```
Build
```make build```
Start
```make start```
Cleanup
```make clean```
+85
View File
@@ -0,0 +1,85 @@
package tests
import (
"bufio"
"fmt"
"io"
"os/exec"
"slices"
"strings"
"testing"
)
func runner(commands []string, t *testing.T) []string {
executablePath := "../tmp/godb"
t.Logf("starting godb from %v\n", executablePath)
executable := exec.Command(executablePath)
results := make([]string, 0)
stdin, err := executable.StdinPipe()
if err != nil {
t.Errorf("Failed to create stdin %q\n", err)
}
stdout, err := executable.StdoutPipe()
if err != nil {
t.Errorf("Failed to create stdout %q\n", err)
}
if err := executable.Start(); err != nil {
t.Errorf("Failed to start executable %q\n", err)
}
go func() {
defer stdin.Close()
for _, c := range commands {
_, _ = io.WriteString(stdin, c+"\n")
}
}()
reader := bufio.NewReader(stdout)
for {
line, err := reader.ReadString('\n')
if line != "" {
formattedline := fmt.Sprintf("%v", strings.Trim(strings.ReplaceAll(line, "\x00", ""), "\n"))
results = append(results, formattedline)
}
if err != nil {
t.Logf("stoped reading due to %q\n", err)
break
}
}
if err := executable.Wait(); err != nil {
t.Logf("Error occured while waiting for cleanup %q\n", err)
}
return results
}
func TestMain(t *testing.T) {
commands := []string{
"insert 1 user1 person1@example.com",
"select",
".exit",
}
expected := []string{
"godb > Executed.",
"godb > {1 user1 person1@example.com}",
"Executed.",
"godb > bye!",
}
results := runner(commands, t)
for i := range results {
results[i] = strings.ReplaceAll(results[i], "\n", "")
}
if !slices.Equal(expected, results) {
t.Errorf("Output Mismatch\nExpected: %v\nGot: %v\n", expected, results)
}
}