Repository: genjidb/genji (go)

Issue:
Add ALTER TABLE ... RENAME TO ...
This is a sub-issue of #94 that focuses on renaming an existing table.

We should be able to do the following:
```
genji> CREATE TABLE foo;
genji> INSERT INTO foo VALUES {name: "John Doe"};
genji> ALTER TABLE foo RENAME TO bar;
genji> SELECT * FROM bar;
{
  "name": "John Doe"
}
genji> SELECT * FROM foo;
table not found
```
If the table `bar` already exists, we should return an error.


Pull request description:
Add ALTER TABLE ... RENAME TO feature
This PR fixes #95. 

We can now rename a table and all indexes linked to the table are updated as well. 

Patch:
diff --git a/database/config.go b/database/config.go
index 8dab1817..e4d4e554 100644
--- a/database/config.go
+++ b/database/config.go
@@ -148,50 +148,32 @@ func newTableInfoStore(tx engine.Transaction) (*tableInfoStore, error) {
 }
 
 // Insert a new tableInfo for the given table name.
-// It automatically generates a unique storeID for that table.
-func (t *tableInfoStore) Insert(tx engine.Transaction, tableName string, info *TableInfo) ([]byte, error) {
+func (t *tableInfoStore) Insert(tx engine.Transaction, tableName string, info *TableInfo) error {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
 	_, ok := t.tableInfos[tableName]
 	if ok {
-		return nil, ErrTableAlreadyExists
+		return ErrTableAlreadyExists
 	}
 
-	var found bool = true
-	var id [6]byte
-	for found {
-		id = generateStoreID()
-
-		found = false
-		for _, ti := range t.tableInfos {
-			if ti.storeID == id {
-				// A store with this id already exists.
-				// Let's generate a new one.
-				found = true
-				break
-			}
-		}
-	}
-	info.storeID = id
-
 	v, err := msgpack.EncodeDocument(info.ToDocument())
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	st, err := tx.GetStore([]byte(tableInfoStoreName))
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	err = st.Put([]byte(tableName), v)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	t.tableInfos[tableName] = *info
-	return info.storeID[:], err
+	return nil
 }
 
 func (t *tableInfoStore) Get(tableName string) (*TableInfo, error) {
@@ -280,12 +262,27 @@ func (t *tableInfoStore) ListTables() []string {
 	return names
 }
 
-func generateStoreID() [6]byte {
+func (t *tableInfoStore) generateStoreID() [6]byte {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+
+	var found bool = true
 	var id [6]byte
+	for found {
+		binary.BigEndian.PutUint32(id[:], uint32(time.Now().Unix()))
+		if _, err := rand.Reader.Read(id[4:]); err != nil {
+			panic(fmt.Errorf("cannot generate random number: %v;", err))
+		}
 
-	binary.BigEndian.PutUint32(id[:], uint32(time.Now().Unix()))
-	if _, err := rand.Reader.Read(id[4:]); err != nil {
-		panic(fmt.Errorf("cannot generate random number: %v;", err))
+		found = false
+		for _, ti := range t.tableInfos {
+			if ti.storeID == id {
+				// A store with this id already exists.
+				// Let's generate a new one.
+				found = true
+				break
+			}
+		}
 	}
 
 	return id
@@ -388,6 +385,15 @@ func (t *indexStore) Get(indexName string) (*IndexConfig, error) {
 	return &idxopts, nil
 }
 
+func (t *indexStore) Replace(indexName string, cfg IndexConfig) error {
+	v, err := msgpack.EncodeDocument(cfg.ToDocument())
+	if err != nil {
+		return err
+	}
+
+	return t.st.Put([]byte(indexName), v)
+}
+
 func (t *indexStore) Delete(indexName string) error {
 	key := []byte(indexName)
 	err := t.st.Delete(key)
diff --git a/database/table.go b/database/table.go
index 8b57d92a..b20897ba 100644
--- a/database/table.go
+++ b/database/table.go
@@ -32,6 +32,216 @@ func (t *Table) Info() (*TableInfo, error) {
 	return ti, nil
 }
 
+// Name returns the name of the table.
+func (t *Table) Name() string {
+	return t.name
+}
+
+// Truncate deletes all the documents from the table.
+func (t *Table) Truncate() error {
+	return t.Store.Truncate()
+}
+
+// Insert the document into the table.
+// If a primary key has been specified during the table creation, the field is expected to be present
+// in the given document.
+// If no primary key has been selected, a monotonic autoincremented integer key will be generated.
+func (t *Table) Insert(d document.Document) ([]byte, error) {
+	d, err := t.ValidateConstraints(d)
+	if err != nil {
+		return nil, err
+	}
+
+	key, err := t.generateKey(d)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = t.Store.Get(key)
+	if err == nil {
+		return nil, ErrDuplicateDocument
+	}
+
+	v, err := msgpack.EncodeDocument(d)
+	if err != nil {
+		return nil, fmt.Errorf("failed to encode document: %w", err)
+	}
+
+	err = t.Store.Put(key, v)
+	if err != nil {
+		return nil, err
+	}
+
+	indexes, err := t.Indexes()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, idx := range indexes {
+		v, err := idx.Opts.Path.GetValue(d)
+		if err != nil {
+			v = document.NewNullValue()
+		}
+
+		err = idx.Set(v, key)
+		if err != nil {
+			if err == index.ErrDuplicate {
+				return nil, ErrDuplicateDocument
+			}
+
+			return nil, err
+		}
+	}
+
+	return key, nil
+}
+
+// Delete a document by key.
+// Indexes are automatically updated.
+func (t *Table) Delete(key []byte) error {
+	d, err := t.GetDocument(key)
+	if err != nil {
+		return err
+	}
+
+	indexes, err := t.Indexes()
+	if err != nil {
+		return err
+	}
+
+	for _, idx := range indexes {
+		v, err := idx.Opts.Path.GetValue(d)
+		if err != nil {
+			return err
+		}
+
+		err = idx.Delete(v, key)
+		if err != nil {
+			return err
+		}
+	}
+
+	return t.Store.Delete(key)
+}
+
+// Replace a document by key.
+// An error is returned if the key doesn't exist.
+// Indexes are automatically updated.
+func (t *Table) Replace(key []byte, d document.Document) error {
+	d, err := t.ValidateConstraints(d)
+	if err != nil {
+		return err
+	}
+
+	indexes, err := t.Indexes()
+	if err != nil {
+		return err
+	}
+
+	return t.replace(indexes, key, d)
+}
+
+func (t *Table) replace(indexes map[string]Index, key []byte, d document.Document) error {
+	// make sure key exists
+	old, err := t.GetDocument(key)
+	if err != nil {
+		return err
+	}
+
+	// remove key from indexes
+	for _, idx := range indexes {
+		v, err := idx.Opts.Path.GetValue(old)
+		if err != nil {
+			return err
+		}
+
+		err = idx.Delete(v, key)
+		if err != nil {
+			return err
+		}
+	}
+
+	// encode new document
+	v, err := msgpack.EncodeDocument(d)
+	if err != nil {
+		return fmt.Errorf("failed to encode document: %w", err)
+	}
+
+	// replace old document with new document
+	err = t.Store.Put(key, v)
+	if err != nil {
+		return err
+	}
+
+	// update indexes
+	for _, idx := range indexes {
+		v, err := idx.Opts.Path.GetValue(d)
+		if err != nil {
+			continue
+		}
+
+		err = idx.Set(v, key)
+		if err != nil {
+			return err
+		}
+	}
+
+	return err
+}
+
+// Indexes returns a map of all the indexes of a table.
+func (t *Table) Indexes() (map[string]Index, error) {
+	s, err := t.tx.Tx.GetStore([]byte(indexStoreName))
+	if err != nil {
+		return nil, err
+	}
+
+	tb := Table{
+		tx:    t.tx,
+		Store: s,
+		name:  indexStoreName,
+	}
+
+	tableName := []byte(t.name)
+	indexes := make(map[string]Index)
+
+	err = document.NewStream(&tb).
+		Filter(func(d document.Document) (bool, error) {
+			v, err := d.GetByField("tablename")
+			if err != nil {
+				return false, err
+			}
+
+			return bytes.Equal(v.V.([]byte), tableName), nil
+		}).
+		Iterate(func(d document.Document) error {
+			var opts IndexConfig
+			err := opts.ScanDocument(d)
+			if err != nil {
+				return err
+			}
+
+			var idx index.Index
+			if opts.Unique {
+				idx = index.NewUniqueIndex(t.tx.Tx, opts.IndexName)
+			} else {
+				idx = index.NewListIndex(t.tx.Tx, opts.IndexName)
+			}
+
+			indexes[opts.Path.String()] = Index{
+				Index: idx,
+				Opts:  opts,
+			}
+
+			return nil
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	return indexes, nil
+}
+
 type encodedDocumentWithKey struct {
 	msgpack.EncodedDocument
 
@@ -227,18 +437,6 @@ func (t *Table) getSmallestAvailableDocid() (int64, error) {
 	return 0, errors.New("reached maximum number of documents in a table")
 }
 
-func getParentValue(d document.Document, p document.ValuePath) (document.Value, error) {
-	if len(p) == 0 {
-		return document.Value{}, errors.New("empty path")
-	}
-
-	if len(p) == 1 {
-		return document.NewDocumentValue(d), nil
-	}
-
-	return p[:len(p)-1].GetValue(d)
-}
-
 // ValidateConstraints check the table configuration for constraints and validates the document
 // against them. If the types defined by the constraints are different than the ones found in
 // the document, the fields are converted to these types when possible. if the conversion
@@ -375,212 +573,14 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 	return nil
 }
 
-// Insert the document into the table.
-// If a primary key has been specified during the table creation, the field is expected to be present
-// in the given document.
-// If no primary key has been selected, a monotonic autoincremented integer key will be generated.
-func (t *Table) Insert(d document.Document) ([]byte, error) {
-	d, err := t.ValidateConstraints(d)
-	if err != nil {
-		return nil, err
-	}
-
-	key, err := t.generateKey(d)
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = t.Store.Get(key)
-	if err == nil {
-		return nil, ErrDuplicateDocument
-	}
-
-	v, err := msgpack.EncodeDocument(d)
-	if err != nil {
-		return nil, fmt.Errorf("failed to encode document: %w", err)
-	}
-
-	err = t.Store.Put(key, v)
-	if err != nil {
-		return nil, err
-	}
-
-	indexes, err := t.Indexes()
-	if err != nil {
-		return nil, err
-	}
-
-	for _, idx := range indexes {
-		v, err := idx.Opts.Path.GetValue(d)
-		if err != nil {
-			v = document.NewNullValue()
-		}
-
-		err = idx.Set(v, key)
-		if err != nil {
-			if err == index.ErrDuplicate {
-				return nil, ErrDuplicateDocument
-			}
-
-			return nil, err
-		}
-	}
-
-	return key, nil
-}
-
-// Delete a document by key.
-// Indexes are automatically updated.
-func (t *Table) Delete(key []byte) error {
-	d, err := t.GetDocument(key)
-	if err != nil {
-		return err
-	}
-
-	indexes, err := t.Indexes()
-	if err != nil {
-		return err
-	}
-
-	for _, idx := range indexes {
-		v, err := idx.Opts.Path.GetValue(d)
-		if err != nil {
-			return err
-		}
-
-		err = idx.Delete(v, key)
-		if err != nil {
-			return err
-		}
-	}
-
-	return t.Store.Delete(key)
-}
-
-// Replace a document by key.
-// An error is returned if the key doesn't exist.
-// Indexes are automatically updated.
-func (t *Table) Replace(key []byte, d document.Document) error {
-	d, err := t.ValidateConstraints(d)
-	if err != nil {
-		return err
-	}
-
-	indexes, err := t.Indexes()
-	if err != nil {
-		return err
-	}
-
-	return t.replace(indexes, key, d)
-}
-
-func (t *Table) replace(indexes map[string]Index, key []byte, d document.Document) error {
-	// make sure key exists
-	old, err := t.GetDocument(key)
-	if err != nil {
-		return err
-	}
-
-	// remove key from indexes
-	for _, idx := range indexes {
-		v, err := idx.Opts.Path.GetValue(old)
-		if err != nil {
-			return err
-		}
-
-		err = idx.Delete(v, key)
-		if err != nil {
-			return err
-		}
-	}
-
-	// encode new document
-	v, err := msgpack.EncodeDocument(d)
-	if err != nil {
-		return fmt.Errorf("failed to encode document: %w", err)
-	}
-
-	// replace old document with new document
-	err = t.Store.Put(key, v)
-	if err != nil {
-		return err
-	}
-
-	// update indexes
-	for _, idx := range indexes {
-		v, err := idx.Opts.Path.GetValue(d)
-		if err != nil {
-			continue
-		}
-
-		err = idx.Set(v, key)
-		if err != nil {
-			return err
-		}
-	}
-
-	return err
-}
-
-// Truncate deletes all the documents from the table.
-func (t *Table) Truncate() error {
-	return t.Store.Truncate()
-}
-
-// Name returns the name of the table.
-func (t *Table) Name() string {
-	return t.name
-}
-
-// Indexes returns a map of all the indexes of a table.
-func (t *Table) Indexes() (map[string]Index, error) {
-	s, err := t.tx.Tx.GetStore([]byte(indexStoreName))
-	if err != nil {
-		return nil, err
-	}
-
-	tb := Table{
-		tx:    t.tx,
-		Store: s,
-		name:  indexStoreName,
+func getParentValue(d document.Document, p document.ValuePath) (document.Value, error) {
+	if len(p) == 0 {
+		return document.Value{}, errors.New("empty path")
 	}
 
-	tableName := []byte(t.name)
-	indexes := make(map[string]Index)
-
-	err = document.NewStream(&tb).
-		Filter(func(d document.Document) (bool, error) {
-			v, err := d.GetByField("tablename")
-			if err != nil {
-				return false, err
-			}
-
-			return bytes.Equal(v.V.([]byte), tableName), nil
-		}).
-		Iterate(func(d document.Document) error {
-			var opts IndexConfig
-			err := opts.ScanDocument(d)
-			if err != nil {
-				return err
-			}
-
-			var idx index.Index
-			if opts.Unique {
-				idx = index.NewUniqueIndex(t.tx.Tx, opts.IndexName)
-			} else {
-				idx = index.NewListIndex(t.tx.Tx, opts.IndexName)
-			}
-
-			indexes[opts.Path.String()] = Index{
-				Index: idx,
-				Opts:  opts,
-			}
-
-			return nil
-		})
-	if err != nil {
-		return nil, err
+	if len(p) == 1 {
+		return document.NewDocumentValue(d), nil
 	}
 
-	return indexes, nil
+	return p[:len(p)-1].GetValue(d)
 }
diff --git a/database/transaction.go b/database/transaction.go
index 4d551a0b..d6b6cc9f 100644
--- a/database/transaction.go
+++ b/database/transaction.go
@@ -70,12 +70,14 @@ func (tx Transaction) CreateTable(name string, info *TableInfo) error {
 	if info == nil {
 		info = new(TableInfo)
 	}
-	sid, err := tx.tableInfoStore.Insert(tx.Tx, name, info)
+
+	info.storeID = tx.tableInfoStore.generateStoreID()
+	err := tx.tableInfoStore.Insert(tx.Tx, name, info)
 	if err != nil {
 		return err
 	}
 
-	err = tx.Tx.CreateStore(sid)
+	err = tx.Tx.CreateStore(info.storeID[:])
 	if err != nil {
 		return fmt.Errorf("failed to create table %q: %w", name, err)
 	}
@@ -103,6 +105,39 @@ func (tx Transaction) GetTable(name string) (*Table, error) {
 	}, nil
 }
 
+// RenameTable renames a table.
+// If it doesn't exist, it returns ErrTableNotFound.
+func (tx Transaction) RenameTable(oldName, newName string) error {
+	ti, err := tx.tableInfoStore.Get(oldName)
+	if err != nil {
+		return err
+	}
+
+	// Insert the TableInfo keyed by the newName name.
+	err = tx.tableInfoStore.Insert(tx.Tx, newName, ti)
+	if err != nil {
+		return err
+	}
+
+	// Update the indexes.
+	idxs, err := tx.ListIndexes()
+	if err != nil {
+		return err
+	}
+	for _, idx := range idxs {
+		if idx.TableName == oldName {
+			idx.TableName = newName
+			err = tx.indexStore.Replace(idx.IndexName, *idx)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	// Delete the old reference from the tableInfoStore.
+	return tx.tableInfoStore.Delete(tx.Tx, oldName)
+}
+
 // DropTable deletes a table from the database.
 func (tx Transaction) DropTable(name string) error {
 	it := tx.indexStore.st.NewIterator(engine.IteratorConfig{})
@@ -160,7 +195,7 @@ func (tx Transaction) ListTables() []string {
 }
 
 // CreateIndex creates an index with the given name.
-// If it already exists, returns ErrTableAlreadyExists.
+// If it already exists, returns ErrIndexAlreadyExists.
 func (tx Transaction) CreateIndex(opts IndexConfig) error {
 	_, err := tx.GetTable(opts.TableName)
 	if err != nil {
diff --git a/sql/parser/alter.go b/sql/parser/alter.go
new file mode 100644
index 00000000..4b240b65
--- /dev/null
+++ b/sql/parser/alter.go
@@ -0,0 +1,42 @@
+package parser
+
+import (
+	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/scanner"
+)
+
+// parseAlterStatement parses a Alter query string and returns a Statement AST object.
+// This function assumes the ALTER token has already been consumed.
+func (p *Parser) parseAlterStatement() (query.AlterStmt, error) {
+	var stmt query.AlterStmt
+	var err error
+
+	// Parse "TABLE".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TABLE {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE"}, pos)
+	}
+
+	// Parse table name.
+	stmt.TableName, err = p.parseIdent()
+	if err != nil {
+		return stmt, err
+	}
+
+	// Parse "RENAME".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.RENAME {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"RENAME"}, pos)
+	}
+
+	// Parse "TO".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TO {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TO"}, pos)
+	}
+
+	// Parse new table name.
+	stmt.NewTableName, err = p.parseIdent()
+	if err != nil {
+		return stmt, err
+	}
+
+	return stmt, nil
+}
diff --git a/sql/parser/parser.go b/sql/parser/parser.go
index c8d3bd06..5a1748b3 100644
--- a/sql/parser/parser.go
+++ b/sql/parser/parser.go
@@ -57,6 +57,8 @@ func (p *Parser) ParseQuery() (query.Query, error) {
 func (p *Parser) ParseStatement() (query.Statement, error) {
 	tok, pos, lit := p.ScanIgnoreWhitespace()
 	switch tok {
+	case scanner.ALTER:
+		return p.parseAlterStatement()
 	case scanner.SELECT:
 		return p.parseSelectStatement()
 	case scanner.DELETE:
diff --git a/sql/query/alter.go b/sql/query/alter.go
new file mode 100644
index 00000000..3fd981c7
--- /dev/null
+++ b/sql/query/alter.go
@@ -0,0 +1,40 @@
+package query
+
+import (
+	"errors"
+
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/sql/query/expr"
+)
+
+// AlterStmt is a DSL that allows creating a full ALTER TABLE query.
+type AlterStmt struct {
+	TableName    string
+	NewTableName string
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt AlterStmt) IsReadOnly() bool {
+	return false
+}
+
+// Run runs the ALTER TABLE statement in the given transaction.
+// It implements the Statement interface.
+func (stmt AlterStmt) Run(tx *database.Transaction, _ []expr.Param) (Result, error) {
+	var res Result
+
+	if stmt.TableName == "" {
+		return res, errors.New("missing table name")
+	}
+
+	if stmt.NewTableName == "" {
+		return res, errors.New("missing new table name")
+	}
+
+	if stmt.TableName == stmt.NewTableName {
+		return res, database.ErrTableAlreadyExists
+	}
+
+	err := tx.RenameTable(stmt.TableName, stmt.NewTableName)
+	return res, err
+}
diff --git a/sql/scanner/scanner.go b/sql/scanner/scanner.go
index 7f2133ab..23d4b353 100644
--- a/sql/scanner/scanner.go
+++ b/sql/scanner/scanner.go
@@ -25,6 +25,8 @@ func (s *Scanner) read() (ch rune, pos Pos) {
 	return
 }
 
+// ReadRune reads a single UTF-8 encoded Unicode character.
+// It returns io.EOF error if it can't read any more.
 func (s *Scanner) ReadRune() (ch rune, size int, err error) {
 	ch, _ = s.read()
 	if ch == eof {
diff --git a/sql/scanner/token.go b/sql/scanner/token.go
index 5670c816..73473442 100644
--- a/sql/scanner/token.go
+++ b/sql/scanner/token.go
@@ -73,6 +73,7 @@ const (
 
 	keywordBeg
 	// ALL and the following are Genji SQL Keywords
+	ALTER
 	AS
 	ASC
 	BY
@@ -95,6 +96,7 @@ const (
 	ON
 	ORDER
 	PRIMARY
+	RENAME
 	SELECT
 	SET
 	TABLE
@@ -170,6 +172,7 @@ var tokens = [...]string{
 	SEMICOLON:   ";",
 	DOT:         ".",
 
+	ALTER:   "ALTER",
 	AS:      "AS",
 	ASC:     "ASC",
 	BY:      "BY",
@@ -192,6 +195,7 @@ var tokens = [...]string{
 	ON:      "ON",
 	ORDER:   "ORDER",
 	PRIMARY: "PRIMARY",
+	RENAME:  "RENAME",
 	SELECT:  "SELECT",
 	SET:     "SET",
 	TABLE:   "TABLE",


Test patch:
diff --git a/database/config_test.go b/database/config_test.go
index d7d633be..b1eb3dd0 100644
--- a/database/config_test.go
+++ b/database/config_test.go
@@ -26,29 +26,35 @@ func TestTableInfoStore(t *testing.T) {
 		},
 	}
 
-	// Inserting one tableInfo should work.
-	sid, err := tx.tableInfoStore.Insert(tx.Tx, "foo1", info)
+	// Inserting one TableInfo should work.
+	err = tx.tableInfoStore.Insert(tx.Tx, "foo1", info)
 	require.NoError(t, err)
-	require.NotNil(t, sid)
 
-	// Inserting an existing tableInfo should not work.
-	_, err = tx.tableInfoStore.Insert(tx.Tx, "foo1", info)
+	// Inserting an existing TableInfo should not work.
+	err = tx.tableInfoStore.Insert(tx.Tx, "foo1", info)
 	require.Equal(t, err, ErrTableAlreadyExists)
 
-	// Getting an existing tableInfo should work.
+	// Listing all tables should return their name
+	// lexicographically ordered.
+	_ = tx.tableInfoStore.Insert(tx.Tx, "foo3", info)
+	_ = tx.tableInfoStore.Insert(tx.Tx, "foo2", info)
+	lt := tx.tableInfoStore.ListTables()
+	require.Equal(t, []string{"foo1", "foo2", "foo3"}, lt)
+
+	// Getting an existing TableInfo should work.
 	received, err := tx.tableInfoStore.Get("foo1")
 	require.NoError(t, err)
 	require.NotNil(t, received.storeID)
 
-	// Getting a non-existing tableInfo should not work.
+	// Getting a non-existing TableInfo should not work.
 	_, err = tx.tableInfoStore.Get("unknown")
 	require.Equal(t, ErrTableNotFound, err)
 
-	// Deleting an existing tableInfo should work.
+	// Deleting an existing TableInfo should work.
 	err = tx.tableInfoStore.Delete(tx.Tx, "foo1")
 	require.NoError(t, err)
 
-	// Deleting a non-existing tableInfo should not work.
+	// Deleting a non-existing TableInfo should not work.
 	err = tx.tableInfoStore.Delete(tx.Tx, "foo1")
 	require.Equal(t, ErrTableNotFound, err)
 }
@@ -86,6 +92,14 @@ func TestIndexStore(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, &cfg, idxcfg)
 
+		// Updating the index should work
+		cfg.Unique = false
+		err = idxs.Replace(cfg.IndexName, cfg)
+		require.NoError(t, err)
+		idxcfg, err = idxs.Get("idx_test")
+		require.NoError(t, err)
+		require.False(t, idxcfg.Unique)
+
 		err = idxs.Delete("idx_test")
 		require.NoError(t, err)
 
diff --git a/database/transaction_test.go b/database/transaction_test.go
index d2151649..e733b3f5 100644
--- a/database/transaction_test.go
+++ b/database/transaction_test.go
@@ -27,6 +27,7 @@ func newTestDB(t testing.TB) (*database.Transaction, func()) {
 // - GetTable
 // - DropTable
 // - ListTables
+// - RenameTable
 func TestTxTable(t *testing.T) {
 	t.Run("Create", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
@@ -96,6 +97,52 @@ func TestTxTable(t *testing.T) {
 		exp := []string{"bar", "baz", "foo"}
 		require.Equal(t, exp, tables)
 	})
+
+	t.Run("Rename", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		ti := &database.TableInfo{FieldConstraints: []database.FieldConstraint{
+			{Path: []string{"name"}, Type: document.TextValue, IsNotNull: true},
+			{Path: []string{"age"}, Type: document.IntegerValue, IsPrimaryKey: true},
+			{Path: []string{"gender"}, Type: document.TextValue},
+			{Path: []string{"city"}, Type: document.TextValue},
+		}}
+		err := tx.CreateTable("foo", ti)
+		require.NoError(t, err)
+
+		err = tx.CreateIndex(database.IndexConfig{Path: []string{"gender"}, IndexName: "idx_gender", TableName: "foo"})
+		require.NoError(t, err)
+		err = tx.CreateIndex(database.IndexConfig{Path: []string{"city"}, IndexName: "idx_city", TableName: "foo", Unique: true})
+		require.NoError(t, err)
+
+		err = tx.RenameTable("foo", "zoo")
+		require.NoError(t, err)
+
+		// Getting the old table should return an error.
+		_, err = tx.GetTable("foo")
+		require.EqualError(t, database.ErrTableNotFound, err.Error())
+
+		tb, err := tx.GetTable("zoo")
+		require.NoError(t, err)
+
+		// The field constraints should be the same.
+		info, err := tb.Info()
+		require.NoError(t, err)
+		require.Equal(t, ti, info)
+
+		// Check that the indexes have been updated as well.
+		idxs, err := tx.ListIndexes()
+		require.NoError(t, err)
+		require.Len(t, idxs, 2)
+		for _, idx := range idxs {
+			require.Equal(t, "zoo", idx.TableName)
+		}
+
+		// Renaming a non existing table should return an error
+		err = tx.RenameTable("foo", "")
+		require.EqualError(t, database.ErrTableNotFound, err.Error())
+	})
 }
 
 func TestTxCreateIndex(t *testing.T) {
diff --git a/sql/parser/alter_test.go b/sql/parser/alter_test.go
new file mode 100644
index 00000000..e5af45aa
--- /dev/null
+++ b/sql/parser/alter_test.go
@@ -0,0 +1,35 @@
+package parser
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/sql/query"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParserAlterTable(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected query.Statement
+		errored  bool
+	}{
+		{"Basic", "ALTER TABLE foo RENAME TO bar", query.AlterStmt{TableName: "foo", NewTableName: "bar"}, false},
+		{"With error / missing TABLE keyword", "ALTER foo RENAME TO bar", query.AlterStmt{}, true},
+		{"With error / two identifiers for table name", "ALTER TABLE foo baz RENAME TO bar", query.AlterStmt{}, true},
+		{"With error / two identifiers for new table name", "ALTER TABLE foo RENAME TO bar baz", query.AlterStmt{}, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := ParseQuery(test.s)
+			if test.errored {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Len(t, q.Statements, 1)
+			require.EqualValues(t, test.expected, q.Statements[0])
+		})
+	}
+}
diff --git a/sql/query/alter_test.go b/sql/query/alter_test.go
new file mode 100644
index 00000000..e78aec16
--- /dev/null
+++ b/sql/query/alter_test.go
@@ -0,0 +1,41 @@
+package query_test
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/genjidb/genji"
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAlterTable(t *testing.T) {
+	db, err := genji.Open(":memory:")
+	require.NoError(t, err)
+	defer db.Close()
+
+	err = db.Exec("CREATE TABLE foo")
+	require.NoError(t, err)
+
+	// Insert some data into foo
+	err = db.Exec(`INSERT INTO foo VALUES {name: "John Doe", age: 99}`)
+	require.NoError(t, err)
+
+	// Renaming the table to the same name should fail.
+	err = db.Exec("ALTER TABLE foo RENAME TO foo")
+	require.EqualError(t, err, database.ErrTableAlreadyExists.Error())
+
+	err = db.Exec("ALTER TABLE foo RENAME TO bar")
+	require.NoError(t, err)
+
+	// Selecting from the old name should fail.
+	err = db.Exec("SELECT * FROM foo")
+	require.EqualError(t, err, database.ErrTableNotFound.Error())
+
+	d, err := db.QueryDocument("SELECT * FROM bar")
+	var buf bytes.Buffer
+	err = document.ToJSON(&buf, d)
+	require.NoError(t, err)
+	require.JSONEq(t, `{"name": "John Doe", "age": 99}`, buf.String())
+}
diff --git a/sql/scanner/scanner_test.go b/sql/scanner/scanner_test.go
index 20c7f345..8ef7110a 100644
--- a/sql/scanner/scanner_test.go
+++ b/sql/scanner/scanner_test.go
@@ -127,6 +127,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `10x`, tok: scanner.DURATION, lit: `10x`, raw: `10x`}, // non-duration unit, but scanned as a duration value
 
 		// Keywords
+		{s: `ALTER`, tok: scanner.ALTER, raw: `ALTER`},
 		{s: `AS`, tok: scanner.AS, raw: `AS`},
 		{s: `ASC`, tok: scanner.ASC, raw: `ASC`},
 		{s: `BY`, tok: scanner.BY, raw: `BY`},
@@ -142,6 +143,8 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `LIMIT`, tok: scanner.LIMIT, raw: `LIMIT`},
 		{s: `OFFSET`, tok: scanner.OFFSET, raw: `OFFSET`},
 		{s: `ORDER`, tok: scanner.ORDER, raw: `ORDER`},
+		{s: `PRIMARY`, tok: scanner.PRIMARY, raw: `PRIMARY`},
+		{s: `RENAME`, tok: scanner.RENAME, raw: `RENAME`},
 		{s: `SELECT`, tok: scanner.SELECT, raw: `SELECT`},
 		{s: `SET`, tok: scanner.SET, raw: `SET`},
 		{s: `TO`, tok: scanner.TO, raw: `TO`},