Skip to content

Commit

Permalink
feat: simple UPDATE implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
RichardKnop committed Oct 25, 2024
1 parent 7be309b commit 545c07e
Show file tree
Hide file tree
Showing 12 changed files with 452 additions and 141 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ minisql>

I plan to implement more features of traditional relational databases in the future as part of this project simply to learn and discovery how various features I have grown acustomed to over the years are implemented under the hood. However, currently only a very small number of features are implemented:

- simple SQL parser (partial support for `CREATE TABLE`, `INSERT`, `SELECT` queries)
- simple SQL parser (partial support for `CREATE TABLE`, `INSERT`, `SELECT`, `UPDATE` queries)
- only tables supported, no indexes (this means all selects are scanning whole tables for now)
- only `int4`, `int8` and `varchar` columns supported
- no primary key support (tables internally use row ID as key in B tree data structure)
Expand All @@ -30,13 +30,13 @@ I plan to implement more features of traditional relational databases in the fut

### Planned features:

- support additional basic query types such as `UPDATE`, `DELETE`, `DROP TABLE`
- support additional basic query types such as `DELETE`, `DROP TABLE`
- support `NULL` values
- B+ tree and support indexes (starting with unique and primary)
- more column types starting with simpler ones such as `bool` and `timestamp`
- support bigger column types such as `text` that can overflow to more pages via linked list data structure
- joins such as `INNER`, `LEFT`, `RIGHT`
- support `ORDER BY`, `GROUP BY`
- support `ORDER BY`, `LIMIT`, `GROUP BY`
- dedicate first 100B of root page for config similar to how sqlite does it
- support altering tables
- transactions
Expand Down
14 changes: 2 additions & 12 deletions internal/pkg/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,7 @@ func (d *Database) executeUpdate(ctx context.Context, stmt minisql.Statement) (m
return minisql.StatementResult{}, errTableDoesNotExist
}

if err := aTable.Update(ctx, stmt); err != nil {
return minisql.StatementResult{}, err
}

// TODO - calculate rows affected properly
return minisql.StatementResult{RowsAffected: 0}, nil
return aTable.Update(ctx, stmt)
}

func (d *Database) executeDelete(ctx context.Context, stmt minisql.Statement) (minisql.StatementResult, error) {
Expand All @@ -338,10 +333,5 @@ func (d *Database) executeDelete(ctx context.Context, stmt minisql.Statement) (m
return minisql.StatementResult{}, errTableDoesNotExist
}

if err := aTable.Delete(ctx, stmt); err != nil {
return minisql.StatementResult{}, err
}

// TODO - calculate rows affected properly
return minisql.StatementResult{RowsAffected: 0}, nil
return aTable.Delete(ctx, stmt)
}
28 changes: 19 additions & 9 deletions internal/pkg/minisql/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (c *Cursor) LeafNodeInsert(ctx context.Context, key uint64, aRow *Row) erro
}
aPage.LeafNode.Header.Cells += 1

err = saveToCell(ctx, &aPage.LeafNode.Cells[c.CellIdx], key, aRow)
err = saveToCell(&aPage.LeafNode.Cells[c.CellIdx], key, aRow)
return err
}

Expand Down Expand Up @@ -98,7 +98,7 @@ func (c *Cursor) LeafNodeSplitInsert(ctx context.Context, key uint64, aRow *Row)
destCell := &destPage.LeafNode.Cells[cellIdx]

if i == c.CellIdx {
if err := saveToCell(ctx, destCell, key, aRow); err != nil {
if err := saveToCell(destCell, key, aRow); err != nil {
return err
}
} else if i > c.CellIdx {
Expand Down Expand Up @@ -134,7 +134,7 @@ func (c *Cursor) LeafNodeSplitInsert(ctx context.Context, key uint64, aRow *Row)
return c.Table.InternalNodeInsert(ctx, parentPageIdx, newPageIdx)
}

func saveToCell(ctx context.Context, cell *Cell, key uint64, aRow *Row) error {
func saveToCell(cell *Cell, key uint64, aRow *Row) error {
rowBuf, err := aRow.Marshal()
if err != nil {
return err
Expand All @@ -144,32 +144,42 @@ func saveToCell(ctx context.Context, cell *Cell, key uint64, aRow *Row) error {
return nil
}

func (c *Cursor) fetchRow(ctx context.Context) (Row, error) {
func updateCell(cell *Cell, aRow *Row) error {
rowBuf, err := aRow.Marshal()
if err != nil {
return err
}
copy(cell.Value[:], rowBuf)
return nil
}

func (c *Cursor) fetchRow(ctx context.Context) (Row, *Cell, error) {
aPage, err := c.Table.pager.GetPage(ctx, c.Table, c.PageIdx)
if err != nil {
return Row{}, err
return Row{}, nil, err
}
aRow := NewRow(c.Table.Columns)

if err := UnmarshalRow(aPage.LeafNode.Cells[c.CellIdx].Value[:], &aRow); err != nil {
return Row{}, err
return Row{}, nil, err
}
destCell := &aPage.LeafNode.Cells[c.CellIdx]

// There are still more cells in the page, move cursor to next cell and return
if c.CellIdx < aPage.LeafNode.Header.Cells-1 {
c.CellIdx += 1
return aRow, nil
return aRow, destCell, nil
}

// If there is no leaf page to the right, set end of table flag and return
if aPage.LeafNode.Header.NextLeaf == 0 {
c.EndOfTable = true
return aRow, nil
return aRow, destCell, nil
}

// Otherwise, we try to move the cursor to the next leaf page
c.PageIdx = aPage.LeafNode.Header.NextLeaf
c.CellIdx = 0

return aRow, nil
return aRow, destCell, nil
}
4 changes: 2 additions & 2 deletions internal/pkg/minisql/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
)

func (t *Table) Delete(ctx context.Context, stmt Statement) error {
func (t *Table) Delete(ctx context.Context, stmt Statement) (StatementResult, error) {
fmt.Println("TODO - implement DELETE")
return fmt.Errorf("not implemented")
return StatementResult{}, fmt.Errorf("not implemented")
}
29 changes: 15 additions & 14 deletions internal/pkg/minisql/insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestTable_Insert(t *testing.T) {
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: []string{"id", "email", "age"},
Fields: columnNames(testColumns...),
Inserts: [][]any{aRow.Values},
}

Expand Down Expand Up @@ -58,7 +58,7 @@ func TestTable_Insert_MultiInsert(t *testing.T) {
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: []string{"id", "email", "age"},
Fields: columnNames(testColumns...),
Inserts: [][]any{aRow.Values, aRow2.Values, aRow3.Values},
}

Expand Down Expand Up @@ -116,7 +116,7 @@ func TestTable_Insert_SplitRootLeaf(t *testing.T) {
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: []string{"id", "email", "age"},
Fields: columnNames(testColumns...),
Inserts: [][]any{aRow.Values},
}

Expand Down Expand Up @@ -198,19 +198,20 @@ func TestTable_Insert_SplitLeaf(t *testing.T) {
return old
}, nil)

// Insert test rows
// Batch insert test rows
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: columnNames(testBigColumns...),
Inserts: [][]any{},
}
for _, aRow := range rows {
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: []string{"id", "email", "name", "description"},
Inserts: [][]any{aRow.Values},
}

err := aTable.Insert(ctx, stmt)
require.NoError(t, err)
stmt.Inserts = append(stmt.Inserts, aRow.Values)
}

err := aTable.Insert(ctx, stmt)
require.NoError(t, err)

// Assert root node
assert.Equal(t, 3, int(aRootPage.InternalNode.Header.KeysNum))
assert.True(t, aRootPage.InternalNode.Header.IsRoot)
Expand Down Expand Up @@ -293,7 +294,7 @@ func TestTable_Insert_SplitInternalNode_CreateNewRoot(t *testing.T) {
stmt := Statement{
Kind: Insert,
TableName: "foo",
Fields: []string{"id", "email", "name", "description"},
Fields: columnNames(testBigColumns...),
Inserts: [][]any{},
}
for _, aRow := range rows {
Expand Down
47 changes: 37 additions & 10 deletions internal/pkg/minisql/minisql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ func init() {
}
}

func columnNames(columns ...Column) []string {
names := make([]string, 0, len(columns))
for _, aColumn := range columns {
names = append(names, aColumn.Name)
}
return names
}

type dataGen struct {
*gofakeit.Faker
}
Expand All @@ -77,14 +85,6 @@ func newDataGen(seed uint64) *dataGen {
return &g
}

func (g *dataGen) Rows(number int) []Row {
rows := make([]Row, 0, number)
for i := 0; i < number; i++ {
rows = append(rows, g.Row())
}
return rows
}

func (g *dataGen) Row() Row {
return Row{
Columns: testColumns,
Expand All @@ -96,10 +96,20 @@ func (g *dataGen) Row() Row {
}
}

func (g *dataGen) BigRows(number int) []Row {
func (g *dataGen) Rows(number int) []Row {
// Make sure all rows will have unique ID, this is important in some tests
idMap := map[int64]struct{}{}
rows := make([]Row, 0, number)
for i := 0; i < number; i++ {
rows = append(rows, g.BigRow())
aRow := g.Row()
_, ok := idMap[aRow.Values[0].(int64)]
for ok {
aRow = g.Row()
_, ok = idMap[aRow.Values[0].(int64)]
}
rows = append(rows, aRow)
idMap[aRow.Values[0].(int64)] = struct{}{}

}
return rows
}
Expand All @@ -116,6 +126,23 @@ func (g *dataGen) BigRow() Row {
}
}

func (g *dataGen) BigRows(number int) []Row {
// Make sure all rows will have unique ID, this is important in some tests
idMap := map[int64]struct{}{}
rows := make([]Row, 0, number)
for i := 0; i < number; i++ {
aRow := g.BigRow()
_, ok := idMap[aRow.Values[0].(int64)]
for ok {
aRow = g.BigRow()
_, ok = idMap[aRow.Values[0].(int64)]
}
rows = append(rows, aRow)
idMap[aRow.Values[0].(int64)] = struct{}{}
}
return rows
}

func newInternalPageWithCells(iCells []ICell, rightChildIdx uint32) *Page {
aRoot := NewInternalNode()
aRoot.Header.KeysNum = uint32(len(iCells))
Expand Down
70 changes: 66 additions & 4 deletions internal/pkg/minisql/minisqltest/minisqltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ var (
Name: "age",
},
}

testBigColumns = []minisql.Column{
{
Kind: minisql.Int8,
Size: 8,
Name: "id",
},
{
Kind: minisql.Varchar,
Size: 255,
Name: "name",
},
{
Kind: minisql.Varchar,
Size: 255,
Name: "email",
},
{
Kind: minisql.Varchar,
Size: minisql.PageSize - 6 - 8 - 4*8 - 8 - 255 - 255,
Name: "description",
},
}
)

type DataGen struct {
Expand All @@ -43,25 +66,64 @@ func NewDataGen(seed uint64) *DataGen {
return &g
}

func (g *DataGen) Row() minisql.Row {
return minisql.Row{
Columns: testColumns,
Values: []any{
g.Int64(),
g.Email(),
int32(g.IntRange(18, 100)),
},
}
}

func (g *DataGen) Rows(number int) []minisql.Row {
// Make sure all rows will have unique ID, this is important in some tests
idMap := map[int64]struct{}{}
rows := make([]minisql.Row, 0, number)
for i := 0; i < number; i++ {
rows = append(rows, g.Row())
aRow := g.Row()
_, ok := idMap[aRow.Values[0].(int64)]
for ok {
aRow = g.Row()
_, ok = idMap[aRow.Values[0].(int64)]
}
rows = append(rows, aRow)
idMap[aRow.Values[0].(int64)] = struct{}{}

}
return rows
}

func (g *DataGen) Row() minisql.Row {
func (g *DataGen) BigRow() minisql.Row {
return minisql.Row{
Columns: testColumns,
Columns: testBigColumns,
Values: []any{
g.Int64(),
g.Email(),
int32(g.IntRange(18, 100)),
g.Name(),
g.Sentence(15),
},
}
}

func (g *DataGen) BigRows(number int) []minisql.Row {
// Make sure all rows will have unique ID, this is important in some tests
idMap := map[int64]struct{}{}
rows := make([]minisql.Row, 0, number)
for i := 0; i < number; i++ {
aRow := g.BigRow()
_, ok := idMap[aRow.Values[0].(int64)]
for ok {
aRow = g.BigRow()
_, ok = idMap[aRow.Values[0].(int64)]
}
rows = append(rows, aRow)
idMap[aRow.Values[0].(int64)] = struct{}{}
}
return rows
}

func (g *DataGen) NewRootLeafPageWithCells(cells, rowSize int) *minisql.Page {
aRootLeaf := minisql.NewLeafNode(uint64(rowSize))
aRootLeaf.Header.Header.IsRoot = true
Expand Down
Loading

0 comments on commit 545c07e

Please sign in to comment.