Repository: genjidb/genji
Pull request #110 (2020-07-06 16:45:10)

Name table stores with unique identifier to prevent having to rename them
This is a first iteration for solving #95. The schema of the DB has been changed, a new way of referencing tables has been introduced. 

Before, the store `_genji.tables` contained the configuration of each table and each store were keyed by the name of the table. But now, `_genji.tables` still holds the configuration of the table but also a `storeID` that contains a generated random value that will be the key of the store which contains the content of the table. This change will allow us to easily rename a table without duplicating its content (see https://github.com/genjidb/genji/issues/95#issuecomment-652399206 for more details).

Major changes:
- `storeID` introduced
- functions of the `Transaction` interface receive a `[]byte` now
- `ListStores` has been removed from the `Transaction` interface
- package `engineutil` has been removed, I removed because the implementation wasn't up to date anymore, we can add it later if needed


Patch:
diff --git a/database/config.go b/database/config.go
index a1cf3d1d..db290c20 100644
--- a/database/config.go
+++ b/database/config.go
@@ -1,6 +1,11 @@
 package database
 
 import (
+	"crypto/rand"
+	"encoding/binary"
+	"fmt"
+	"time"
+
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/document/encoding"
 	"github.com/genjidb/genji/engine"
@@ -121,46 +126,111 @@ func (f *FieldConstraint) ScanDocument(d document.Document) error {
 	return err
 }
 
-type tableConfigStore struct {
-	st engine.Store
+type tableInfo struct {
+	// storeID is a generated ID that acts as a key to reference a table.
+	// The first-4 bytes represents the timestamp in second and the last-2 bytes are
+	// randomly generated.
+	storeID [6]byte
+
+	cfg *TableConfig
 }
 
-func (t *tableConfigStore) Insert(tableName string, cfg TableConfig) error {
-	key := []byte(tableName)
-	_, err := t.st.Get(key)
-	if err == nil {
-		return ErrTableAlreadyExists
+func (ti *tableInfo) ToDocument() document.Document {
+	buf := document.NewFieldBuffer()
+
+	buf.Add("storeID", document.NewBlobValue(ti.storeID[:]))
+	buf.Add("config", document.NewDocumentValue(ti.cfg.ToDocument()))
+
+	return buf
+}
+
+func (ti *tableInfo) ScanDocument(d document.Document) error {
+	v, err := d.GetByField("storeID")
+	if err != nil {
+		return err
 	}
-	if err != engine.ErrKeyNotFound {
+	b, err := v.ConvertToBlob()
+	if err != nil {
 		return err
 	}
+	copy(ti.storeID[:], b)
 
-	v, err := encoding.EncodeDocument(cfg.ToDocument())
+	v, err = d.GetByField("config")
+	if err != nil {
+		return err
+	}
+	doc, err := v.ConvertToDocument()
 	if err != nil {
 		return err
 	}
 
-	return t.st.Put(key, v)
+	ti.cfg = &TableConfig{}
+	return ti.cfg.ScanDocument(doc)
+}
+
+type tableInfoStore struct {
+	st engine.Store
 }
 
-func (t *tableConfigStore) Replace(tableName string, cfg *TableConfig) error {
+// Insert a new tableInfo for the given table name.
+// It automatically generates a unique storeID for that table.
+func (t *tableInfoStore) Insert(tableName string, cfg TableConfig) (*tableInfo, error) {
 	key := []byte(tableName)
 	_, err := t.st.Get(key)
-	if err == engine.ErrKeyNotFound {
-		return ErrTableNotFound
+	if err == nil {
+		return nil, ErrTableAlreadyExists
 	}
+	if err != engine.ErrKeyNotFound {
+		return nil, err
+	}
+
+	var id [6]byte
+	for {
+		id = generateStoreID()
+		_, err = t.st.Get(id[:])
+		if err == nil {
+			// A store with this id already exists.
+			// Let's generate a new one.
+			continue
+		}
+		if err != engine.ErrKeyNotFound {
+			return nil, err
+		}
+		break
+	}
+	ti := tableInfo{
+		storeID: id,
+		cfg:     &cfg,
+	}
+
+	v, err := encoding.EncodeDocument(ti.ToDocument())
+	if err != nil {
+		return nil, err
+	}
+
+	err = t.st.Put(key, v)
+	if err != nil {
+		return nil, err
+	}
+	return &ti, err
+}
+
+func (t *tableInfoStore) Replace(tableName string, cfg *TableConfig) error {
+	ti, err := t.Get(tableName)
 	if err != nil {
 		return err
 	}
 
-	v, err := encoding.EncodeDocument(cfg.ToDocument())
+	ti.cfg = cfg
+	v, err := encoding.EncodeDocument(ti.ToDocument())
 	if err != nil {
 		return err
 	}
-	return t.st.Put(key, v)
+
+	return t.st.Put([]byte(tableName), v)
 }
 
-func (t *tableConfigStore) Get(tableName string) (*TableConfig, error) {
+func (t *tableInfoStore) Get(tableName string) (*tableInfo, error) {
 	key := []byte(tableName)
 	v, err := t.st.Get(key)
 	if err == engine.ErrKeyNotFound {
@@ -170,17 +240,16 @@ func (t *tableConfigStore) Get(tableName string) (*TableConfig, error) {
 		return nil, err
 	}
 
-	var cfg TableConfig
-
-	err = cfg.ScanDocument(encoding.EncodedDocument(v))
+	var ti tableInfo
+	err = ti.ScanDocument(encoding.EncodedDocument(v))
 	if err != nil {
 		return nil, err
 	}
 
-	return &cfg, nil
+	return &ti, nil
 }
 
-func (t *tableConfigStore) Delete(tableName string) error {
+func (t *tableInfoStore) Delete(tableName string) error {
 	key := []byte(tableName)
 	err := t.st.Delete(key)
 	if err == engine.ErrKeyNotFound {
@@ -189,6 +258,28 @@ func (t *tableConfigStore) Delete(tableName string) error {
 	return err
 }
 
+func (t *tableInfoStore) ListTables() ([]string, error) {
+	it := t.st.NewIterator(engine.IteratorConfig{})
+
+	var names []string
+	for it.Seek(nil); it.Valid(); it.Next() {
+		k := it.Item().Key()
+		names = append(names, string(k))
+	}
+	return names, it.Close()
+}
+
+func generateStoreID() [6]byte {
+	var id [6]byte
+
+	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))
+	}
+
+	return id
+}
+
 // IndexConfig holds the configuration of an index.
 type IndexConfig struct {
 	// If set to true, values will be associated with at most one key. False by default.
diff --git a/database/database.go b/database/database.go
index c4c30be9..d67c9ec6 100644
--- a/database/database.go
+++ b/database/database.go
@@ -33,17 +33,17 @@ func New(ng engine.Engine) (*Database, error) {
 	}
 	defer ntx.Rollback()
 
-	_, err = ntx.GetStore(tableConfigStoreName)
+	_, err = ntx.GetStore([]byte(tableInfoStoreName))
 	if err == engine.ErrStoreNotFound {
-		err = ntx.CreateStore(tableConfigStoreName)
+		err = ntx.CreateStore([]byte(tableInfoStoreName))
 	}
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = ntx.GetStore(indexStoreName)
+	_, err = ntx.GetStore([]byte(indexStoreName))
 	if err == engine.ErrStoreNotFound {
-		err = ntx.CreateStore(indexStoreName)
+		err = ntx.CreateStore([]byte(indexStoreName))
 	}
 	if err != nil {
 		return nil, err
@@ -76,7 +76,7 @@ func (db *Database) Begin(writable bool) (*Transaction, error) {
 		writable: writable,
 	}
 
-	tx.tcfgStore, err = tx.getTableConfigStore()
+	tx.tableInfoStore, err = tx.getTableInfoStore()
 	if err != nil {
 		return nil, err
 	}
diff --git a/database/table.go b/database/table.go
index 3602a9d5..b916c3c1 100644
--- a/database/table.go
+++ b/database/table.go
@@ -15,15 +15,19 @@ import (
 
 // A Table represents a collection of documents.
 type Table struct {
-	tx       *Transaction
-	Store    engine.Store
-	name     string
-	cfgStore *tableConfigStore
+	tx        *Transaction
+	Store     engine.Store
+	name      string
+	infoStore *tableInfoStore
 }
 
 // Config of the table.
 func (t *Table) Config() (*TableConfig, error) {
-	return t.cfgStore.Get(t.name)
+	ti, err := t.infoStore.Get(t.name)
+	if err != nil {
+		return nil, err
+	}
+	return ti.cfg, nil
 }
 
 type encodedDocumentWithKey struct {
@@ -92,11 +96,13 @@ func (t *Table) GetDocument(key []byte) (document.Document, error) {
 // if there are no primary key in the table, a default
 // key is generated, called the docid.
 func (t *Table) generateKey(d document.Document) ([]byte, error) {
-	cfg, err := t.cfgStore.Get(t.name)
+	ti, err := t.infoStore.Get(t.name)
 	if err != nil {
 		return nil, err
 	}
 
+	cfg := ti.cfg
+
 	if pk := cfg.GetPrimaryKey(); pk != nil {
 		v, err := pk.Path.GetValue(d)
 		if err == document.ErrFieldNotFound {
@@ -484,14 +490,14 @@ func (t *Table) Truncate() error {
 	return t.Store.Truncate()
 }
 
-// TableName returns the name of the table.
-func (t *Table) TableName() string {
+// 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(indexStoreName)
+	s, err := t.tx.Tx.GetStore([]byte(indexStoreName))
 	if err != nil {
 		return nil, err
 	}
diff --git a/database/transaction.go b/database/transaction.go
index d3445292..c41775ad 100644
--- a/database/transaction.go
+++ b/database/transaction.go
@@ -3,7 +3,6 @@ package database
 import (
 	"errors"
 	"fmt"
-	"strings"
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/document/encoding"
@@ -12,8 +11,8 @@ import (
 )
 
 var (
-	tableConfigStoreName = "__genji.tables"
-	indexStoreName       = "__genji.indexes"
+	tableInfoStoreName = "__genji.tables"
+	indexStoreName     = "__genji.indexes"
 )
 
 // Transaction represents a database transaction. It provides methods for managing the
@@ -21,11 +20,12 @@ var (
 // Transaction is either read-only or read/write. Read-only can be used to read tables
 // and read/write can be used to read, create, delete and modify tables.
 type Transaction struct {
-	db         *Database
-	Tx         engine.Transaction
-	writable   bool
-	tcfgStore  *tableConfigStore
-	indexStore *indexStore
+	db       *Database
+	Tx       engine.Transaction
+	writable bool
+
+	tableInfoStore *tableInfoStore
+	indexStore     *indexStore
 }
 
 // Rollback the transaction. Can be used safely after commit.
@@ -70,12 +70,12 @@ func (tx Transaction) CreateTable(name string, cfg *TableConfig) error {
 	if cfg == nil {
 		cfg = new(TableConfig)
 	}
-	err := tx.tcfgStore.Insert(name, *cfg)
+	ti, err := tx.tableInfoStore.Insert(name, *cfg)
 	if err != nil {
 		return err
 	}
 
-	err = tx.Tx.CreateStore(name)
+	err = tx.Tx.CreateStore(ti.storeID[:])
 	if err != nil {
 		return fmt.Errorf("failed to create table %q: %w", name, err)
 	}
@@ -85,21 +85,21 @@ func (tx Transaction) CreateTable(name string, cfg *TableConfig) error {
 
 // GetTable returns a table by name. The table instance is only valid for the lifetime of the transaction.
 func (tx Transaction) GetTable(name string) (*Table, error) {
-	_, err := tx.tcfgStore.Get(name)
+	ti, err := tx.tableInfoStore.Get(name)
 	if err != nil {
 		return nil, err
 	}
 
-	s, err := tx.Tx.GetStore(name)
+	s, err := tx.Tx.GetStore(ti.storeID[:])
 	if err != nil {
 		return nil, err
 	}
 
 	return &Table{
-		tx:       &tx,
-		Store:    s,
-		name:     name,
-		cfgStore: tx.tcfgStore,
+		tx:        &tx,
+		Store:     s,
+		name:      name,
+		infoStore: tx.tableInfoStore,
 	}, nil
 }
 
@@ -140,35 +140,23 @@ func (tx Transaction) DropTable(name string) error {
 		return err
 	}
 
-	err = tx.tcfgStore.Delete(name)
+	ti, err := tx.tableInfoStore.Get(name)
 	if err != nil {
 		return err
 	}
 
-	return tx.Tx.DropStore(name)
-}
-
-// ListTables lists all the tables.
-func (tx Transaction) ListTables() ([]string, error) {
-	stores, err := tx.Tx.ListStores("")
+	err = tx.tableInfoStore.Delete(name)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	tables := make([]string, 0, len(stores))
-
-	for _, st := range stores {
-		if st == indexStoreName || st == tableConfigStoreName {
-			continue
-		}
-		if strings.HasPrefix(st, index.StorePrefix) {
-			continue
-		}
-
-		tables = append(tables, st)
-	}
+	return tx.Tx.DropStore(ti.storeID[:])
+}
 
-	return tables, nil
+// ListTables lists all the tables.
+// The returned slice is lexicographically ordered.
+func (tx Transaction) ListTables() ([]string, error) {
+	return tx.tableInfoStore.ListTables()
 }
 
 // CreateIndex creates an index with the given name.
@@ -281,18 +269,18 @@ func (tx Transaction) ReIndexAll() error {
 	return nil
 }
 
-func (tx *Transaction) getTableConfigStore() (*tableConfigStore, error) {
-	st, err := tx.Tx.GetStore(tableConfigStoreName)
+func (tx *Transaction) getTableInfoStore() (*tableInfoStore, error) {
+	st, err := tx.Tx.GetStore([]byte(tableInfoStoreName))
 	if err != nil {
 		return nil, err
 	}
-	return &tableConfigStore{
+	return &tableInfoStore{
 		st: st,
 	}, nil
 }
 
 func (tx *Transaction) getIndexStore() (*indexStore, error) {
-	st, err := tx.Tx.GetStore(indexStoreName)
+	st, err := tx.Tx.GetStore([]byte(indexStoreName))
 	if err != nil {
 		return nil, err
 	}
diff --git a/engine/badgerengine/engine.go b/engine/badgerengine/engine.go
index 68980c76..4706337a 100644
--- a/engine/badgerengine/engine.go
+++ b/engine/badgerengine/engine.go
@@ -75,17 +75,17 @@ func (t *Transaction) Commit() error {
 	return t.tx.Commit()
 }
 
-func buildStoreKey(name string) []byte {
+func buildStoreKey(name []byte) []byte {
 	var buf bytes.Buffer
 	buf.Grow(len(storeKey) + 1 + len(name))
 	buf.WriteString(storeKey)
 	buf.WriteByte(separator)
-	buf.WriteString(name)
+	buf.Write(name)
 
 	return buf.Bytes()
 }
 
-func buildStorePrefixKey(name string) []byte {
+func buildStorePrefixKey(name []byte) []byte {
 	prefix := make([]byte, 0, len(name)+3)
 	prefix = append(prefix, storePrefix)
 	prefix = append(prefix, separator)
@@ -95,7 +95,7 @@ func buildStorePrefixKey(name string) []byte {
 }
 
 // GetStore returns a store by name.
-func (t *Transaction) GetStore(name string) (engine.Store, error) {
+func (t *Transaction) GetStore(name []byte) (engine.Store, error) {
 	key := buildStoreKey(name)
 
 	_, err := t.tx.Get(key)
@@ -119,7 +119,7 @@ func (t *Transaction) GetStore(name string) (engine.Store, error) {
 
 // CreateStore creates a store.
 // If the store already exists, returns engine.ErrStoreAlreadyExists.
-func (t *Transaction) CreateStore(name string) error {
+func (t *Transaction) CreateStore(name []byte) error {
 	if !t.writable {
 		return engine.ErrTransactionReadOnly
 	}
@@ -137,7 +137,7 @@ func (t *Transaction) CreateStore(name string) error {
 }
 
 // DropStore deletes the store and all its keys.
-func (t *Transaction) DropStore(name string) error {
+func (t *Transaction) DropStore(name []byte) error {
 	if !t.writable {
 		return engine.ErrTransactionReadOnly
 	}
@@ -152,25 +152,10 @@ func (t *Transaction) DropStore(name string) error {
 		return err
 	}
 
-	err = t.tx.Delete(buildStoreKey(name))
+	err = t.tx.Delete(buildStoreKey([]byte(name)))
 	if err == badger.ErrKeyNotFound {
 		return engine.ErrStoreNotFound
 	}
 
 	return err
 }
-
-// ListStores returns a list of all the store names.
-func (t *Transaction) ListStores(prefix string) ([]string, error) {
-	var names []string
-
-	p := buildStoreKey(prefix)
-	it := t.tx.NewIterator(badger.DefaultIteratorOptions)
-	defer it.Close()
-
-	for it.Seek(p); it.ValidForPrefix(p); it.Next() {
-		names = append(names, string(bytes.TrimPrefix(it.Item().Key(), p[:len(p)-len(prefix)])))
-	}
-
-	return names, nil
-}
diff --git a/engine/badgerengine/store.go b/engine/badgerengine/store.go
index 2dd4c0ba..cbf57b80 100644
--- a/engine/badgerengine/store.go
+++ b/engine/badgerengine/store.go
@@ -13,7 +13,7 @@ type Store struct {
 	tx       *badger.Txn
 	prefix   []byte
 	writable bool
-	name     string
+	name     []byte
 }
 
 // build a long key for each key of a store
diff --git a/engine/boltengine/engine.go b/engine/boltengine/engine.go
index e57283b0..59b54c34 100644
--- a/engine/boltengine/engine.go
+++ b/engine/boltengine/engine.go
@@ -2,7 +2,6 @@
 package boltengine
 
 import (
-	"bytes"
 	"os"
 
 	"github.com/genjidb/genji/engine"
@@ -70,9 +69,8 @@ func (t *Transaction) Commit() error {
 }
 
 // GetStore returns a store by name. The store uses a Bolt bucket.
-func (t *Transaction) GetStore(name string) (engine.Store, error) {
-	bname := []byte(name)
-	b := t.tx.Bucket(bname)
+func (t *Transaction) GetStore(name []byte) (engine.Store, error) {
+	b := t.tx.Bucket(name)
 	if b == nil {
 		return nil, engine.ErrStoreNotFound
 	}
@@ -80,18 +78,18 @@ func (t *Transaction) GetStore(name string) (engine.Store, error) {
 	return &Store{
 		bucket: b,
 		tx:     t.tx,
-		name:   bname,
+		name:   name,
 	}, nil
 }
 
 // CreateStore creates a bolt bucket and returns a store.
 // If the store already exists, returns engine.ErrStoreAlreadyExists.
-func (t *Transaction) CreateStore(name string) error {
+func (t *Transaction) CreateStore(name []byte) error {
 	if !t.writable {
 		return engine.ErrTransactionReadOnly
 	}
 
-	_, err := t.tx.CreateBucket([]byte(name))
+	_, err := t.tx.CreateBucket(name)
 	if err == bolt.ErrBucketExists {
 		return engine.ErrStoreAlreadyExists
 	}
@@ -100,29 +98,15 @@ func (t *Transaction) CreateStore(name string) error {
 }
 
 // DropStore deletes the underlying bucket.
-func (t *Transaction) DropStore(name string) error {
+func (t *Transaction) DropStore(name []byte) error {
 	if !t.writable {
 		return engine.ErrTransactionReadOnly
 	}
 
-	err := t.tx.DeleteBucket([]byte(name))
+	err := t.tx.DeleteBucket(name)
 	if err == bolt.ErrBucketNotFound {
 		return engine.ErrStoreNotFound
 	}
 
 	return err
 }
-
-// ListStores returns a list of all the store names.
-func (t *Transaction) ListStores(prefix string) ([]string, error) {
-	var names []string
-	p := []byte(prefix)
-	err := t.tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
-		if bytes.HasPrefix(name, p) {
-			names = append(names, string(name))
-		}
-		return nil
-	})
-
-	return names, err
-}
diff --git a/engine/engine.go b/engine/engine.go
index 618cac12..02b3ee7d 100644
--- a/engine/engine.go
+++ b/engine/engine.go
@@ -46,15 +46,12 @@ type Transaction interface {
 	// If the transaction was already rolled back or commited, an error is returned.
 	Commit() error
 	// Fetch a store by name. If the store doesn't exist, it returns the ErrStoreNotFound error.
-	GetStore(name string) (Store, error)
+	GetStore(name []byte) (Store, error)
 	// Create a store with the given name. If the store already exists, it returns ErrStoreAlreadyExists.
-	CreateStore(name string) error
+	CreateStore(name []byte) error
 	// Drop a store by name. If the store doesn't exist, it returns ErrStoreNotFound.
 	// It deletes all the values stored in it.
-	DropStore(name string) error
-	// Returns a list of store names lexicographically sorted.
-	// If there are no stores, an empty slice is returned.
-	ListStores(prefix string) ([]string, error)
+	DropStore(name []byte) error
 }
 
 // A Store manages key value pairs. It is an abstraction on top of any data structure that can provide
diff --git a/engine/engineutil/dump.go b/engine/engineutil/dump.go
deleted file mode 100644
index f4a80cba..00000000
--- a/engine/engineutil/dump.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package engineutil
-
-import (
-	"fmt"
-
-	"github.com/genjidb/genji/engine"
-)
-
-func DumpEngine(e engine.Engine) error {
-	tx, err := e.Begin(false)
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback()
-
-	stores, err := tx.ListStores("")
-	if err != nil {
-		return err
-	}
-
-	for _, name := range stores {
-		s, err := tx.GetStore(name)
-		if err != nil {
-			return err
-		}
-		fmt.Println("--------------")
-		fmt.Printf("Store: %s\n", name)
-		fmt.Println("--------------")
-		err = DumpStore(s)
-		if err != nil {
-			return err
-		}
-		fmt.Println("--------------")
-
-	}
-	return nil
-}
-
-func DumpStore(s engine.Store) error {
-	it := s.NewIterator(engine.IteratorConfig{})
-
-	for it.Seek(nil); it.Valid(); it.Next() {
-		item := it.Item()
-		key := item.Key()
-		value, err := item.ValueCopy(nil)
-		if err != nil {
-			return err
-		}
-
-		fmt.Printf("Key\t: %v\n", key)
-		fmt.Printf("Value\t: %v\n\n", value)
-	}
-
-	return nil
-}
diff --git a/index/index.go b/index/index.go
index 2763c5e9..0d3cc4c8 100644
--- a/index/index.go
+++ b/index/index.go
@@ -1,9 +1,9 @@
 package index
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
-	"strings"
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/document/encoding"
@@ -97,14 +97,19 @@ func NewUniqueIndex(tx engine.Transaction, idxName string) *UniqueIndex {
 	}
 }
 
-func buildIndexName(name string, t Type) string {
-	var b strings.Builder
-	b.WriteString(StorePrefix)
-	b.WriteString(name)
-	b.WriteByte(separator)
-	b.WriteByte(byte(t))
+func buildIndexName(name []byte, t Type) []byte {
+	var buf bytes.Buffer
 
-	return b.String()
+	// We can deterministically set the size of the buffer.
+	// The last 2 bytes are for the separator and the Type t.
+	buf.Grow(len(StorePrefix) + len(name) + 2)
+
+	buf.WriteString(StorePrefix)
+	buf.Write(name)
+	buf.WriteByte(separator)
+	buf.WriteByte(byte(t))
+
+	return buf.Bytes()
 }
 
 // A Pivot is a value that is used to seek for a particular value in an index.
@@ -160,7 +165,7 @@ func decodeIndexValueToField(t Type, data []byte) (document.Value, error) {
 }
 
 func getOrCreateStore(tx engine.Transaction, t document.ValueType, name string) (engine.Store, error) {
-	idxName := buildIndexName(name, NewTypeFromValueType(t))
+	idxName := buildIndexName([]byte(name), NewTypeFromValueType(t))
 	st, err := tx.GetStore(idxName)
 	if err == nil {
 		return st, nil
@@ -179,7 +184,7 @@ func getOrCreateStore(tx engine.Transaction, t document.ValueType, name string)
 }
 
 func getStore(tx engine.Transaction, t Type, name string) (engine.Store, error) {
-	idxName := buildIndexName(name, t)
+	idxName := buildIndexName([]byte(name), t)
 	st, err := tx.GetStore(idxName)
 	if err == nil || err == engine.ErrStoreNotFound {
 		return st, nil
@@ -189,7 +194,7 @@ func getStore(tx engine.Transaction, t Type, name string) (engine.Store, error)
 }
 
 func dropStore(tx engine.Transaction, t Type, name string) error {
-	idxName := buildIndexName(name, t)
+	idxName := buildIndexName([]byte(name), t)
 	_, err := tx.GetStore(idxName)
 	if err != nil && err != engine.ErrStoreNotFound {
 		return err


Test patch:
diff --git a/database/config_test.go b/database/config_test.go
index 9add90a7..32ca20fd 100644
--- a/database/config_test.go
+++ b/database/config_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestTableConfigStore(t *testing.T) {
+func TestTableInfoStore(t *testing.T) {
 	ng := memoryengine.NewEngine()
 	defer ng.Close()
 
@@ -16,12 +16,12 @@ func TestTableConfigStore(t *testing.T) {
 	require.NoError(t, err)
 	defer tx.Rollback()
 
-	err = tx.CreateStore("foo")
+	err = tx.CreateStore([]byte("foo"))
 	require.NoError(t, err)
-	st, err := tx.GetStore("foo")
+	st, err := tx.GetStore([]byte("foo"))
 	require.NoError(t, err)
 
-	tcs := tableConfigStore{st}
+	tcs := tableInfoStore{st}
 
 	cfg := TableConfig{
 		FieldConstraints: []FieldConstraint{
@@ -29,29 +29,48 @@ func TestTableConfigStore(t *testing.T) {
 		},
 	}
 
-	// inserting one should work
-	err = tcs.Insert("foo-table", cfg)
+	// Inserting one tableInfo should work.
+	ti, err := tcs.Insert("foo1", cfg)
 	require.NoError(t, err)
 
-	// inserting one with the same name should not work
-	err = tcs.Insert("foo-table", cfg)
+	// Inserting an existing tableInfo should not work.
+	_, err = tcs.Insert("foo1", cfg)
 	require.Equal(t, err, ErrTableAlreadyExists)
 
-	// getting an existing table should work
-	received, err := tcs.Get("foo-table")
+	// Listing all tables should return their name
+	// lexicographically ordered.
+	_, _ = tcs.Insert("foo3", cfg)
+	_, _ = tcs.Insert("foo2", cfg)
+	lt, err := tcs.ListTables()
 	require.NoError(t, err)
-	require.Equal(t, cfg, *received)
+	require.Equal(t, []string{"foo1", "foo2", "foo3"}, lt)
 
-	// getting a non-existing table should not work
+	// Getting an existing tableInfo should work.
+	received, err := tcs.Get("foo1")
+	require.NoError(t, err)
+	require.Equal(t, ti, received)
+
+	// Getting a non-existing tableInfo should not work.
 	_, err = tcs.Get("unknown")
 	require.Equal(t, ErrTableNotFound, err)
 
-	// deleting an existing table should work
-	err = tcs.Delete("foo-table")
+	// Updating the config table.
+	fc := FieldConstraint{Path: []string{"j"}, Type: document.TextValue, IsNotNull: true}
+	cfg.FieldConstraints = append(cfg.FieldConstraints, fc)
+	err = tcs.Replace("foo1", &cfg)
+	require.NoError(t, err)
+
+	received, err = tcs.Get("foo1")
+	require.NoError(t, err)
+	require.Equal(t, ti.storeID, received.storeID)
+	require.Equal(t, cfg, *received.cfg)
+
+	// Deleting an existing tableInfo should work.
+	err = tcs.Delete("foo1")
 	require.NoError(t, err)
 
-	// deleting a non-existing table should not work
-	err = tcs.Delete("foo-table")
+	// Deleting a non-existing tableInfo should not work.
+	err = tcs.Delete("foo1")
 	require.Equal(t, ErrTableNotFound, err)
 }
 
@@ -63,9 +82,9 @@ func TestIndexStore(t *testing.T) {
 	require.NoError(t, err)
 	defer tx.Rollback()
 
-	err = tx.CreateStore("test")
+	err = tx.CreateStore([]byte("test"))
 	require.NoError(t, err)
-	st, err := tx.GetStore("test")
+	st, err := tx.GetStore([]byte("test"))
 	require.NoError(t, err)
 
 	idxs := indexStore{st}
diff --git a/database/table_test.go b/database/table_test.go
index 936b2596..987d9a7f 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -24,6 +24,12 @@ func newTestTable(t testing.TB) (*database.Table, func()) {
 	return tb, fn
 }
 
+func newDocument() *document.FieldBuffer {
+	return document.NewFieldBuffer().
+		Add("fielda", document.NewTextValue("a")).
+		Add("fieldb", document.NewTextValue("b"))
+}
+
 // TestTableIterate verifies Iterate behaviour.
 func TestTableIterate(t *testing.T) {
 	t.Run("Should not fail with no documents", func(t *testing.T) {
@@ -295,7 +301,7 @@ func TestTableInsert(t *testing.T) {
 
 		tests := [][]byte{
 			nil,
-			[]byte{},
+			{},
 			[]byte(nil),
 		}
 
diff --git a/database/transaction_test.go b/database/transaction_test.go
index bdae3a95..55df4e0c 100644
--- a/database/transaction_test.go
+++ b/database/transaction_test.go
@@ -22,54 +22,87 @@ func newTestDB(t testing.TB) (*database.Transaction, func()) {
 	}
 }
 
-func TestTxCreateIndex(t *testing.T) {
-	t.Run("Should create an index and return it", func(t *testing.T) {
+// TestTxTable tests all basic operations on tables:
+// - CreateTable
+// - GetTable
+// - DropTable
+// - ListTables
+func TestTxTable(t *testing.T) {
+	t.Run("Create", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
 		err := tx.CreateTable("test", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateIndex(database.IndexConfig{
-			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
-		})
+		// Creating a table that already exists should fail.
+		err = tx.CreateTable("test", nil)
+		require.EqualError(t, err, database.ErrTableAlreadyExists.Error())
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		err := tx.CreateTable("test", nil)
 		require.NoError(t, err)
-		idx, err := tx.GetIndex("idxFoo")
+
+		table, err := tx.GetTable("test")
 		require.NoError(t, err)
-		require.NotNil(t, idx)
+		require.Equal(t, "test", table.Name())
+
+		// Getting a table that doesn't exist should fail.
+		_, err = tx.GetTable("unknown")
+		require.EqualError(t, err, database.ErrTableNotFound.Error())
 	})
 
-	t.Run("Should fail if it already exists", func(t *testing.T) {
+	t.Run("Drop", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
 		err := tx.CreateTable("test", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateIndex(database.IndexConfig{
-			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
-		})
+		err = tx.DropTable("test")
 		require.NoError(t, err)
 
-		err = tx.CreateIndex(database.IndexConfig{
-			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
-		})
-		require.Equal(t, database.ErrIndexAlreadyExists, err)
+		// Getting a table that has been dropped should fail.
+		_, err = tx.GetTable("test")
+		require.EqualError(t, err, database.ErrTableNotFound.Error())
+
+		// Dropping a table that doesn't exist should fail.
+		err = tx.DropTable("test")
+		require.EqualError(t, err, database.ErrTableNotFound.Error())
 	})
 
-	t.Run("Should fail if table doesn't exists", func(t *testing.T) {
+	t.Run("List", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
-		err := tx.CreateIndex(database.IndexConfig{
-			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
-		})
-		require.Equal(t, database.ErrTableNotFound, err)
+		tables, err := tx.ListTables()
+		require.NoError(t, err)
+		require.Len(t, tables, 0)
+
+		err = tx.CreateTable("foo", nil)
+		require.NoError(t, err)
+
+		err = tx.CreateTable("bar", nil)
+		require.NoError(t, err)
+
+		err = tx.CreateTable("baz", nil)
+		require.NoError(t, err)
+
+		tables, err = tx.ListTables()
+		require.NoError(t, err)
+
+		// The returned slice should be lexicographically ordered.
+		exp := []string{"bar", "baz", "foo"}
+		require.Equal(t, exp, tables)
 	})
 }
 
-func TestTxDropTable(t *testing.T) {
-	t.Run("Should drop a table and its indexes", func(t *testing.T) {
+func TestTxCreateIndex(t *testing.T) {
+	t.Run("Should create an index and return it", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
@@ -80,27 +113,36 @@ func TestTxDropTable(t *testing.T) {
 			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
 		})
 		require.NoError(t, err)
-
-		err = tx.DropTable("test")
+		idx, err := tx.GetIndex("idxFoo")
 		require.NoError(t, err)
+		require.NotNil(t, idx)
+	})
 
-		_, err = tx.GetTable("test")
-		require.Error(t, err)
+	t.Run("Should fail if it already exists", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
 
-		err = tx.CreateTable("test", nil)
+		err := tx.CreateTable("test", nil)
 		require.NoError(t, err)
 
 		err = tx.CreateIndex(database.IndexConfig{
 			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
 		})
 		require.NoError(t, err)
+
+		err = tx.CreateIndex(database.IndexConfig{
+			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
+		})
+		require.Equal(t, database.ErrIndexAlreadyExists, err)
 	})
 
-	t.Run("Should fail if it doesn't exist", func(t *testing.T) {
+	t.Run("Should fail if table doesn't exists", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
-		err := tx.DropTable("foo")
+		err := tx.CreateIndex(database.IndexConfig{
+			IndexName: "idxFoo", TableName: "test", Path: document.NewValuePath("foo"),
+		})
 		require.Equal(t, database.ErrTableNotFound, err)
 	})
 }
@@ -283,48 +325,3 @@ func TestReIndexAll(t *testing.T) {
 		require.NoError(t, err)
 	})
 }
-
-func newDocument() *document.FieldBuffer {
-	return document.NewFieldBuffer().
-		Add("fielda", document.NewTextValue("a")).
-		Add("fieldb", document.NewTextValue("b"))
-}
-
-func TestTxListTables(t *testing.T) {
-	t.Run("Should succeed if no tables", func(t *testing.T) {
-		tx, cleanup := newTestDB(t)
-		defer cleanup()
-
-		list, err := tx.ListTables()
-		require.NoError(t, err)
-		require.Len(t, list, 0)
-	})
-
-	t.Run("Should return the right tables", func(t *testing.T) {
-		tx, cleanup := newTestDB(t)
-		defer cleanup()
-
-		err := tx.CreateTable("a", nil)
-		require.NoError(t, err)
-		err = tx.CreateTable("b", nil)
-		require.NoError(t, err)
-
-		err = tx.CreateIndex(database.IndexConfig{
-			IndexName: "name",
-			TableName: "a",
-			Path:      document.NewValuePath("foo"),
-		})
-		require.NoError(t, err)
-
-		// insert some data to make sure indexes are actually created by the engine
-		ta, err := tx.GetTable("a")
-		require.NoError(t, err)
-		_, err = ta.Insert(document.NewFieldBuffer().Add("foo", document.NewBoolValue(true)))
-		require.NoError(t, err)
-
-		list, err := tx.ListTables()
-		require.NoError(t, err)
-		require.Len(t, list, 2)
-		require.Equal(t, []string{"a", "b"}, list)
-	})
-}
diff --git a/engine/enginetest/testing.go b/engine/enginetest/testing.go
index 8bcb6827..2bcb998b 100644
--- a/engine/enginetest/testing.go
+++ b/engine/enginetest/testing.go
@@ -4,7 +4,6 @@ package enginetest
 
 import (
 	"bytes"
-	"fmt"
 	"testing"
 
 	"github.com/genjidb/genji"
@@ -33,7 +32,6 @@ func TestSuite(t *testing.T, builder Builder) {
 		{"Transaction/Store", TestTransactionStore},
 		{"Transaction/CreateStore", TestTransactionCreateStore},
 		{"Transaction/DropStore", TestTransactionDropStore},
-		{"Transaction/ListStores", TestTransactionListStores},
 		{"Store/Iterator", TestStoreIterator},
 		{"Store/Put", TestStorePut},
 		{"Store/Get", TestStoreGet},
@@ -128,7 +126,7 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 
 		// create store for testing store methods
-		err = tx.CreateStore("store1")
+		err = tx.CreateStore([]byte("store1"))
 		require.NoError(t, err)
 
 		err = tx.Commit()
@@ -139,7 +137,7 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 		defer tx.Rollback()
 
 		// fetch the store and the index
-		st, err := tx.GetStore("store1")
+		st, err := tx.GetStore([]byte("store1"))
 		require.NoError(t, err)
 
 		tests := []struct {
@@ -147,8 +145,8 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 			err  error
 			fn   func(*error)
 		}{
-			{"CreateStore", engine.ErrTransactionReadOnly, func(err *error) { *err = tx.CreateStore("store") }},
-			{"DropStore", engine.ErrTransactionReadOnly, func(err *error) { *err = tx.DropStore("store") }},
+			{"CreateStore", engine.ErrTransactionReadOnly, func(err *error) { *err = tx.CreateStore([]byte("store")) }},
+			{"DropStore", engine.ErrTransactionReadOnly, func(err *error) { *err = tx.DropStore([]byte("store")) }},
 			{"StorePut", engine.ErrTransactionReadOnly, func(err *error) { *err = st.Put([]byte("id"), nil) }},
 			{"StoreDelete", engine.ErrTransactionReadOnly, func(err *error) { *err = st.Delete([]byte("id")) }},
 			{"StoreTruncate", engine.ErrTransactionReadOnly, func(err *error) { *err = st.Truncate() }},
@@ -175,25 +173,25 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 			{
 				"CreateStore",
 				nil,
-				func(tx engine.Transaction, err *error) { *err = tx.CreateStore("store") },
-				func(tx engine.Transaction, err *error) { _, *err = tx.GetStore("store") },
+				func(tx engine.Transaction, err *error) { *err = tx.CreateStore([]byte("store")) },
+				func(tx engine.Transaction, err *error) { _, *err = tx.GetStore([]byte("store")) },
 			},
 			{
 				"DropStore",
-				func(tx engine.Transaction) error { return tx.CreateStore("store") },
-				func(tx engine.Transaction, err *error) { *err = tx.DropStore("store") },
-				func(tx engine.Transaction, err *error) { *err = tx.CreateStore("store") },
+				func(tx engine.Transaction) error { return tx.CreateStore([]byte("store")) },
+				func(tx engine.Transaction, err *error) { *err = tx.DropStore([]byte("store")) },
+				func(tx engine.Transaction, err *error) { *err = tx.CreateStore([]byte("store")) },
 			},
 			{
 				"StorePut",
-				func(tx engine.Transaction) error { return tx.CreateStore("store") },
+				func(tx engine.Transaction) error { return tx.CreateStore([]byte("store")) },
 				func(tx engine.Transaction, err *error) {
-					st, er := tx.GetStore("store")
+					st, er := tx.GetStore([]byte("store"))
 					require.NoError(t, er)
 					require.NoError(t, st.Put([]byte("foo"), []byte("FOO")))
 				},
 				func(tx engine.Transaction, err *error) {
-					st, er := tx.GetStore("store")
+					st, er := tx.GetStore([]byte("store"))
 					require.NoError(t, er)
 					_, *err = st.Get([]byte("foo"))
 				},
@@ -283,8 +281,8 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 		}{
 			{
 				"CreateStore",
-				func(tx engine.Transaction, err *error) { *err = tx.CreateStore("store") },
-				func(tx engine.Transaction, err *error) { _, *err = tx.GetStore("store") },
+				func(tx engine.Transaction, err *error) { *err = tx.CreateStore([]byte("store")) },
+				func(tx engine.Transaction, err *error) { _, *err = tx.GetStore([]byte("store")) },
 			},
 		}
 
@@ -317,10 +315,10 @@ func TestTransactionCreateStore(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 		defer tx.Rollback()
 
-		err = tx.CreateStore("store")
+		err = tx.CreateStore([]byte("store"))
 		require.NoError(t, err)
 
-		st, err := tx.GetStore("store")
+		st, err := tx.GetStore([]byte("store"))
 		require.NoError(t, err)
 		require.NotNil(t, st)
 	})
@@ -333,9 +331,9 @@ func TestTransactionCreateStore(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 		defer tx.Rollback()
 
-		err = tx.CreateStore("store")
+		err = tx.CreateStore([]byte("store"))
 		require.NoError(t, err)
-		err = tx.CreateStore("store")
+		err = tx.CreateStore([]byte("store"))
 		require.Equal(t, engine.ErrStoreAlreadyExists, err)
 	})
 }
@@ -350,7 +348,7 @@ func TestTransactionStore(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 		defer tx.Rollback()
 
-		_, err = tx.GetStore("store")
+		_, err = tx.GetStore([]byte("store"))
 		require.Equal(t, engine.ErrStoreNotFound, err)
 	})
 
@@ -363,18 +361,18 @@ func TestTransactionStore(t *testing.T, builder Builder) {
 		defer tx.Rollback()
 
 		// create two stores
-		err = tx.CreateStore("storea")
+		err = tx.CreateStore([]byte("storea"))
 		require.NoError(t, err)
 
-		err = tx.CreateStore("storeb")
+		err = tx.CreateStore([]byte("storeb"))
 		require.NoError(t, err)
 
 		// fetch first store
-		sta, err := tx.GetStore("storea")
+		sta, err := tx.GetStore([]byte("storea"))
 		require.NoError(t, err)
 
 		// fetch second store
-		stb, err := tx.GetStore("storeb")
+		stb, err := tx.GetStore([]byte("storeb"))
 		require.NoError(t, err)
 
 		// insert data in first store
@@ -402,13 +400,13 @@ func TestTransactionDropStore(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 		defer tx.Rollback()
 
-		err = tx.CreateStore("store")
+		err = tx.CreateStore([]byte("store"))
 		require.NoError(t, err)
 
-		err = tx.DropStore("store")
+		err = tx.DropStore([]byte("store"))
 		require.NoError(t, err)
 
-		_, err = tx.GetStore("store")
+		_, err = tx.GetStore([]byte("store"))
 		require.Equal(t, engine.ErrStoreNotFound, err)
 	})
 
@@ -420,67 +418,18 @@ func TestTransactionDropStore(t *testing.T, builder Builder) {
 		require.NoError(t, err)
 		defer tx.Rollback()
 
-		err = tx.DropStore("store")
+		err = tx.DropStore([]byte("store"))
 		require.Equal(t, engine.ErrStoreNotFound, err)
 	})
 }
 
-// TestTransactionListStores verifies ListStores behaviour.
-func TestTransactionListStores(t *testing.T, builder Builder) {
-	t.Run("With no prefix, should list all stores", func(t *testing.T) {
-		ng, cleanup := builder()
-		defer cleanup()
-
-		tx, err := ng.Begin(true)
-		require.NoError(t, err)
-		defer tx.Rollback()
-
-		for i := 0; i < 10; i++ {
-			err = tx.CreateStore(fmt.Sprintf("store%d", i))
-			require.NoError(t, err)
-		}
-
-		list, err := tx.ListStores("")
-		require.NoError(t, err)
-		require.Len(t, list, 10)
-		for i, name := range list {
-			require.Equal(t, fmt.Sprintf("store%d", i), name)
-		}
-	})
-
-	t.Run("With a prefix, should list some stores", func(t *testing.T) {
-		ng, cleanup := builder()
-		defer cleanup()
-
-		tx, err := ng.Begin(true)
-		require.NoError(t, err)
-		defer tx.Rollback()
-
-		for i := 0; i < 10; i++ {
-			if i%2 == 0 {
-				err = tx.CreateStore(fmt.Sprintf("foo%d", i))
-			} else {
-				err = tx.CreateStore(fmt.Sprintf("bar%d", i))
-			}
-			require.NoError(t, err)
-		}
-
-		list, err := tx.ListStores("f")
-		require.NoError(t, err)
-		require.Len(t, list, 5)
-		for i, name := range list {
-			require.Equal(t, fmt.Sprintf("foo%d", i*2), name)
-		}
-	})
-}
-
 func storeBuilder(t testing.TB, builder Builder) (engine.Store, func()) {
 	ng, cleanup := builder()
 	tx, err := ng.Begin(true)
 	require.NoError(t, err)
-	err = tx.CreateStore("test")
+	err = tx.CreateStore([]byte("test"))
 	require.NoError(t, err)
-	st, err := tx.GetStore("test")
+	st, err := tx.GetStore([]byte("test"))
 	require.NoError(t, err)
 	return st, func() {
 		tx.Rollback()