Compare commits
23
Commits
8567ed5f83
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2324b80c | ||
|
|
deb125db4b | ||
|
|
23fd1d5a1e | ||
|
|
e666b26d3d | ||
|
|
318295f677 | ||
|
|
e625b29d3c | ||
|
|
c719848c25 | ||
|
|
b2a61c66e4 | ||
|
|
1ec9de4b0b | ||
|
|
208820217c | ||
|
|
5550c92c23 | ||
|
|
d97308b4f1 | ||
|
|
a522c63f47 | ||
|
|
df75480238 | ||
|
|
c3016be8e9 | ||
|
|
4968b2417f | ||
|
|
82f77db592 | ||
|
|
aab9ad70c9 | ||
|
|
d2a5477217 | ||
|
|
b5a31e7775 | ||
|
|
ecc188de14 | ||
|
|
4e273a2037 | ||
|
|
8f6ad9817b |
@@ -0,0 +1 @@
|
|||||||
|
tmp
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"godb/internals/repl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
repl.Run()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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")
|
||||||
|
case ExecuteFail:
|
||||||
|
//todo
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
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
|
||||||
|
StatementUnrecognized
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
ExecuteFail
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
statement := &Statement{}
|
||||||
|
return statement
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
statement.stype = StatementUnrecognized
|
||||||
|
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 = ExecuteFail
|
||||||
|
switch statement.stype {
|
||||||
|
case StatementInsert:
|
||||||
|
result = statement.execInsert(table)
|
||||||
|
case StatementSelect:
|
||||||
|
result = statement.execSelect(table)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 TakshakRamteke
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
BINARY_NAME=godb
|
||||||
|
|
||||||
|
run:
|
||||||
|
@go run cmd/godb/main.go
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o tmp/${BINARY_NAME} cmd/godb/main.go
|
||||||
|
|
||||||
|
start:
|
||||||
|
@./tmp/${BINARY_NAME}
|
||||||
|
|
||||||
|
test-unit:
|
||||||
|
@go test ./internals/... -v
|
||||||
|
|
||||||
|
test-inte:
|
||||||
|
@go test ./tests/... -v
|
||||||
|
|
||||||
|
test: build test-unit test-inte
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@go clean
|
||||||
|
@rm -rf ${BINARY_NAME}
|
||||||
|
|
||||||
|
reset: clean build test clean
|
||||||
|
|
||||||
|
.PHONY: clean build test
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
### Godb
|
||||||
|
|
||||||
|
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```
|
||||||
|
|
||||||
|
Test
|
||||||
|
|
||||||
|
```make test```
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user