Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ GoDB is a tiny educational database engine written in Go. It exists as a playgro

## Features

- Pluggable storage engines:
- In-memory store for quick experimentation
- Experimental on-disk filestore with a simple WAL (write-ahead log)
- In-memory storage engine for quick experimentation
- Simple SQL support:
- `CREATE TABLE`
- `INSERT INTO ... VALUES (...)`
Expand All @@ -34,7 +32,7 @@ GoDB is a tiny educational database engine written in Go. It exists as a playgro
git clone https://github.com/askorykh/godb.git
cd godb

# Run the REPL server (creates ./data when using the filestore)
# Run the REPL server (in-memory storage)
go run ./cmd/godb-server
```

Expand All @@ -50,15 +48,13 @@ COMMIT;
```


### Storage backends
### Storage backend

By default the REPL wires the engine to the on-disk filestore located in `./data`. It uses a straightforward file format and an append-only WAL for durability. On startup, the filestore replays committed WAL entries to rebuild table files. Rollbacks still only cancel the in-memory engine transaction—the on-disk table files are not reverted yet. See [`internal/storage/filestore/README.md`](internal/storage/filestore/README.md) for details.

If you want a pure in-memory experience (no files written), switch to the `memstore` engine inside `cmd/godb-server/main.go` by swapping the initialization block.
The REPL uses the in-memory storage engine to match the article walkthroughs and keep the footprint tiny.

### Transactions

The engine understands `BEGIN`, `COMMIT`, and `ROLLBACK` to group multiple statements. Transactions are executed against the configured storage backend. With the default filestore backend, commits fsync the WAL before returning; rollbacks do not undo writes on disk yet, but committed WAL entries are replayed on startup.
The engine understands `BEGIN`, `COMMIT`, and `ROLLBACK` to group multiple statements. Transactions are executed against the configured storage backend. In the in-memory backend used for the articles, commit simply swaps the staged tables into place and rollback is a no-op.

## Running tests

Expand All @@ -75,10 +71,7 @@ internal/
engine/ # DB engine, execution planner, and simple evaluator
sql/ # SQL parser and AST definitions
storage/
filestore/ # On-disk storage with WAL and recovery
memstore/ # In-memory storage implementation
index/
btree/ # WIP B-tree index structures used by the filestore
```

## Architecture
Expand All @@ -88,20 +81,15 @@ graph TD;
REPL --> Parser[SQL parser];
Parser --> Engine[Execution engine];
Engine --> Storage[Storage interface];
Storage --> Filestore[On-disk store + WAL];
Storage --> Memstore[In-memory store];
```

- `cmd/godb-server` reads input, handles meta commands, and forwards SQL to the engine.
- `internal/sql` parses SQL into AST nodes and validates column types.
- `internal/engine` executes statements (create, insert, select, update, delete) against the storage implementation.
- `internal/storage/filestore` provides the default on-disk storage layer with WAL and recovery.
- `internal/storage/memstore` provides an in-memory table storage layer used for testing/experiments.

## Roadmap (very rough)

- Improve on-disk storage (rollback/undo, durability tests, compaction)
- Better query planner / optimizer
- Indexes integrated into query execution
- Richer SQL surface and multi-statement transaction semantics
- Maybe: distributed experiments later
22 changes: 9 additions & 13 deletions cmd/godb-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bufio"
"errors"
"fmt"
"goDB/internal/storage/filestore"
"goDB/internal/storage/memstore"
"io"
"log"

Expand All @@ -17,21 +17,15 @@ import (
func main() {
fmt.Println("GoDB server starting (REPL mode)…")

// choose storage implementation
// mem := memstore.New()
// eng := engine.New(mem)

fs, err := filestore.New("./data")
if err != nil {
log.Fatalf("failed to init filestore: %v", err)
}
eng := engine.New(fs)
// choose storage implementation (in-memory for the article code)
mem := memstore.New()
eng := engine.New(mem)

if err := eng.Start(); err != nil {
log.Fatalf("engine start failed: %v", err)
}

fmt.Println("Engine started successfully (using on-disk filestore at ./data).")
fmt.Println("Engine started successfully (using in-memory storage).")
fmt.Println("Type SQL statements like:")
fmt.Println(" CREATE TABLE users (id INT, name STRING, active BOOL);")
fmt.Println(" INSERT INTO users VALUES (1, 'Alice', true);")
Expand Down Expand Up @@ -124,9 +118,11 @@ func handleMetaCommand(line string, eng *engine.DBEngine) bool {
fmt.Println()
fmt.Println(" SELECT * FROM tableName;")
fmt.Println(" SELECT col1, col2, ... FROM tableName;")
fmt.Println(" SELECT col1, col2 FROM tableName WHERE column = literal;")
fmt.Println(" - WHERE: supports only equality (=)")
fmt.Println(" SELECT col1, col2 FROM tableName WHERE column <op> literal;")
fmt.Println(" - WHERE comparisons: =, !=, <, <=, >, >=")
fmt.Println(" - WHERE literals: INT, FLOAT, STRING ('text'), BOOL")
fmt.Println(" - ORDER BY column [ASC|DESC]")
fmt.Println(" - LIMIT n")
fmt.Println()
fmt.Println("Meta commands:")
fmt.Println(" .tables List available tables")
Expand Down
105 changes: 105 additions & 0 deletions internal/engine/article4_select_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package engine

import (
"testing"

"goDB/internal/sql"
"goDB/internal/storage/memstore"
)

// Article 4 coverage: projection, WHERE filtering, comparisons, ORDER BY, and LIMIT.
func TestArticle4_SelectFeatures(t *testing.T) {
store := memstore.New()
eng := New(store)

if err := eng.Start(); err != nil {
t.Fatalf("Start failed: %v", err)
}

createSQL := "CREATE TABLE users (id INT, name STRING, age INT);"
stmt, err := sql.Parse(createSQL)
if err != nil {
t.Fatalf("Parse CREATE failed: %v", err)
}
if _, _, err := eng.Execute(stmt); err != nil {
t.Fatalf("Execute CREATE failed: %v", err)
}

inserts := []string{
"INSERT INTO users VALUES (1, 'Ada', 30);",
"INSERT INTO users VALUES (2, 'Bea', 18);",
"INSERT INTO users VALUES (3, 'Cara', 21);",
"INSERT INTO users VALUES (4, 'Drew', 16);",
"INSERT INTO users VALUES (5, 'Eli', 22);",
}
for _, q := range inserts {
stmt, err := sql.Parse(q)
if err != nil {
t.Fatalf("Parse INSERT failed for %q: %v", q, err)
}
if _, _, err := eng.Execute(stmt); err != nil {
t.Fatalf("Execute INSERT failed for %q: %v", q, err)
}
}

// Projection + WHERE with ">" + ORDER BY + LIMIT
selectSQL := "SELECT name, age FROM users WHERE age > 18 ORDER BY age LIMIT 3;"
selStmt, err := sql.Parse(selectSQL)
if err != nil {
t.Fatalf("Parse SELECT failed: %v", err)
}

cols, rows, err := eng.Execute(selStmt)
if err != nil {
t.Fatalf("Execute SELECT failed: %v", err)
}

if len(cols) != 2 || cols[0] != "name" || cols[1] != "age" {
t.Fatalf("unexpected projection: %#v", cols)
}

if len(rows) != 3 {
t.Fatalf("expected 3 rows after LIMIT, got %d", len(rows))
}

gotNames := []string{rows[0][0].S, rows[1][0].S, rows[2][0].S}
want := []string{"Cara", "Eli", "Ada"} // ages 21, 22, 30 (ordered asc)
for i := range want {
if gotNames[i] != want[i] {
t.Fatalf("unexpected order at %d: got %q want %q (names=%v)", i, gotNames[i], want[i], gotNames)
}
}

// WHERE with equality
eqSQL := "SELECT id FROM users WHERE age = 18;"
eqStmt, err := sql.Parse(eqSQL)
if err != nil {
t.Fatalf("Parse SELECT (=) failed: %v", err)
}
cols, rows, err = eng.Execute(eqStmt)
if err != nil {
t.Fatalf("Execute SELECT (=) failed: %v", err)
}

if len(rows) != 1 || rows[0][0].I64 != 2 {
t.Fatalf("unexpected equality results: cols=%v rows=%v", cols, rows)
}

// WHERE with "<" and ORDER BY to keep determinism
ltSQL := "SELECT name FROM users WHERE age < 18 ORDER BY name;"
ltStmt, err := sql.Parse(ltSQL)
if err != nil {
t.Fatalf("Parse SELECT (<) failed: %v", err)
}
cols, rows, err = eng.Execute(ltStmt)
if err != nil {
t.Fatalf("Execute SELECT (<) failed: %v", err)
}

if len(cols) != 1 || cols[0] != "name" {
t.Fatalf("unexpected projection for < query: %v", cols)
}
if len(rows) != 1 || rows[0][0].S != "Drew" {
t.Fatalf("unexpected < query results: rows=%v", rows)
}
}
4 changes: 0 additions & 4 deletions internal/engine/engine_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ func (e *DBEngine) Execute(stmt sql.Statement) ([]string, []sql.Row, error) {
err := e.CreateTable(s.TableName, s.Columns)
return nil, nil, err

case *sql.CreateIndexStmt:
err := e.store.CreateIndex(s.IndexName, s.TableName, s.ColumnName)
return nil, nil, err

case *sql.InsertStmt:
return nil, nil, e.executeInsert(s)

Expand Down
30 changes: 0 additions & 30 deletions internal/index/btree/README.md

This file was deleted.

Loading
Loading