Repository: genjidb/genji
Pull request #403 (2021-05-22 18:02:17)

Store schemas as SQL
This PR changes the way table and index schemas are stored. Previously, we used to store the parsed representation (*database.TableInfo, *database.IndexInfo) as custom documents.
Instead, we now store the table information in SQL form and parse it at startup.
The motivation behind this change is to reduce the areas that could lead to a breaking change in the future. That way, if we decide to improve the internal representation of table and index information, the SQL representation won't change, neither the storage structure.
This also adds support for prepared statements.

codecov-commenter: # [Codecov](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) Report
> Merging [#403](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (8394eb7) into [main](https://codecov.io/gh/genjidb/genji/commit/2951d6ac868dbb4160cfd15f9be99ce6c0cb1f5b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) (2951d6a) will **decrease** coverage by `0.18%`.
> The diff coverage is `80.36%`.

[![Impacted file tree graph](https://codecov.io/gh/genjidb/genji/pull/403/graphs/tree.svg?width=650&height=150&src=pr&token=nwR1GpvtyH&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)

```diff
@@            Coverage Diff             @@
##             main     #403      +/-   ##
==========================================
- Coverage   67.82%   67.64%   -0.19%     
==========================================
  Files          75       75              
  Lines        7786     7699      -87     
==========================================
- Hits         5281     5208      -73     
- Misses       1821     1850      +29     
+ Partials      684      641      -43     
```


| [Impacted Files](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | Coverage Δ | |
|---|---|---|
| [stringutil/custom.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-c3RyaW5ndXRpbC9jdXN0b20uZ28=) | `83.72% <0.00%> (-4.09%)` | :arrow_down: |
| [stringutil/stringutil.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-c3RyaW5ndXRpbC9zdHJpbmd1dGlsLmdv) | `0.00% <0.00%> (ø)` | |
| [query/query.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-cXVlcnkvcXVlcnkuZ28=) | `61.17% <18.75%> (-10.66%)` | :arrow_down: |
| [database/transaction.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZGF0YWJhc2UvdHJhbnNhY3Rpb24uZ28=) | `51.72% <47.05%> (-8.63%)` | :arrow_down: |
| [query/alter.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-cXVlcnkvYWx0ZXIuZ28=) | `33.33% <50.00%> (ø)` | |
| [open.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-b3Blbi5nbw==) | `62.50% <58.49%> (-1.14%)` | :arrow_down: |
| [database/table.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZGF0YWJhc2UvdGFibGUuZ28=) | `49.48% <66.66%> (-1.25%)` | :arrow_down: |
| [query/explain.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-cXVlcnkvZXhwbGFpbi5nbw==) | `85.71% <75.00%> (ø)` | |
| [db.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-ZGIuZ28=) | `56.25% <75.47%> (-1.09%)` | :arrow_down: |
| [query/create.go](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb#diff-cXVlcnkvY3JlYXRlLmdv) | `81.81% <85.71%> (-2.80%)` | :arrow_down: |
| ... and [29 more](https://codecov.io/gh/genjidb/genji/pull/403/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Last update [2951d6a...8394eb7](https://codecov.io/gh/genjidb/genji/pull/403?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=genjidb).

asdine: Thanks @tzzed !

Patch:
diff --git a/Makefile b/Makefile
index 08bfe9bb..07524b47 100644
--- a/Makefile
+++ b/Makefile
@@ -21,3 +21,8 @@ testtinygo:
 bench:
 	go test -v -run=^\$$ -benchmem -bench=. ./...
 	cd cmd/genji && go test -v -run=^\$$ -benchmem -bench=. ./...
+
+tidy:
+	go mod tidy
+	cd engine/badgerengine && go mod tidy && cd ../..
+	cd cmd/genji && go mod tidy && cd ../..
diff --git a/cmd/genji/dbutil/dump.go b/cmd/genji/dbutil/dump.go
index f69cf5fe..3a53ddfa 100644
--- a/cmd/genji/dbutil/dump.go
+++ b/cmd/genji/dbutil/dump.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"strings"
 
 	"github.com/genjidb/genji"
 	"github.com/genjidb/genji/document"
@@ -135,91 +134,66 @@ func DumpSchema(ctx context.Context, db *genji.DB, w io.Writer, tables ...string
 
 // dumpSchema displays the schema of the given table as SQL statements.
 func dumpSchema(tx *genji.Tx, w io.Writer, tableName string) error {
-	t, err := tx.GetTable(tableName)
+	d, err := tx.QueryDocument("SELECT sql FROM __genji_tables WHERE table_name = ?", tableName)
 	if err != nil {
 		return err
 	}
 
-	_, err = fmt.Fprintf(w, "CREATE TABLE %s", tableName)
+	var tableSchema string
+	err = document.Scan(d, &tableSchema)
 	if err != nil {
 		return err
 	}
 
-	ti := t.Info()
-
-	fcs := ti.FieldConstraints
-	// Fields constraints should be displayed between parenthesis.
-	if len(fcs) > 0 {
-		_, err = fmt.Fprintln(w, " (")
-		if err != nil {
-			return err
-		}
+	_, err = fmt.Fprintf(w, "%s;\n", tableSchema)
+	if err != nil {
+		return err
 	}
 
-	for i, fc := range fcs {
-		// Don't display the last comma.
-		if i > 0 {
-			_, err = fmt.Fprintln(w, ",")
-			if err != nil {
-				return err
-			}
-		}
-
-		// Construct the fields constraints
-		if _, err := fmt.Fprintf(w, " %s %s", fcs[i].Path.String(), strings.ToUpper(fcs[i].Type.String())); err != nil {
-			return err
-		}
-
-		f := ""
-		if fc.IsPrimaryKey {
-			f += " PRIMARY KEY"
-		}
+	// Indexes statements.
+	res, err := tx.Query("SELECT sql FROM __genji_indexes WHERE table_name = ?", tableName)
+	if err != nil {
+		return err
+	}
 
-		if fc.IsNotNull {
-			f += " NOT NULL"
-		}
+	defer res.Close()
 
-		if fc.HasDefaultValue() {
-			if _, err := fmt.Fprintf(w, "%s DEFAULT %s", f, fc.DefaultValue.String()); err != nil {
-				return err
-			}
-		} else {
-			if _, err := fmt.Fprintf(w, f); err != nil {
-				return err
-			}
-		}
-	}
+	return res.Iterate(func(d document.Document) error {
+		var indexQuery string
 
-	// Fields constraints close parenthesis.
-	if len(fcs) > 0 {
-		if _, err := fmt.Fprintln(w, "\n);"); err != nil {
-			return err
-		}
-	} else {
-		if _, err := fmt.Fprintln(w, ";"); err != nil {
+		err = document.Scan(d, &indexQuery)
+		if err != nil {
 			return err
 		}
-	}
-
-	// Indexes statements.
-	indexes := t.Indexes()
 
-	for _, index := range indexes {
-		u := ""
-		if index.Info.Unique {
-			u = " UNIQUE"
-		}
+		_, err = fmt.Fprintf(w, "%s;\n", indexQuery)
+		return err
+	})
+}
 
-		var paths []string
-		for _, path := range index.Info.Paths {
-			paths = append(paths, path.String())
+func ListIndexes(ctx context.Context, db *genji.DB, tableName string) ([]string, error) {
+	var listName []string
+	err := db.View(func(tx *genji.Tx) error {
+		q := "SELECT index_name FROM __genji_indexes"
+		if tableName != "" {
+			q += " WHERE table_name = ?"
 		}
-
-		_, err = fmt.Fprintf(w, "CREATE%s INDEX %s ON %s (%s);\n", u, index.Info.IndexName, index.Info.TableName, strings.Join(paths, ", "))
+		res, err := tx.Query(q, tableName)
 		if err != nil {
 			return err
 		}
-	}
+		defer res.Close()
+
+		return res.Iterate(func(d document.Document) error {
+			var name string
+			err = document.Scan(d, &name)
+			if err != nil {
+				return err
+			}
+			listName = append(listName, name)
+			return nil
+		})
+	})
 
-	return nil
+	return listName, err
 }
diff --git a/cmd/genji/go.mod b/cmd/genji/go.mod
index 7fde89a2..2e316aa5 100644
--- a/cmd/genji/go.mod
+++ b/cmd/genji/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/urfave/cli/v2 v2.3.0
 	go.etcd.io/bbolt v1.3.5
 	go.uber.org/multierr v1.6.0
-	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 )
 
 replace (
diff --git a/cmd/genji/go.sum b/cmd/genji/go.sum
index 2f2a5aaa..5ace5946 100644
--- a/cmd/genji/go.sum
+++ b/cmd/genji/go.sum
@@ -49,8 +49,8 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 github.com/google/flatbuffers v1.12.0 h1:/PtAHvnBY4Kqnx/xCQ3OIV9uYcSFGScBsWI3Oogeh6w=
 github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -121,12 +121,13 @@ github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
-github.com/vmihailenco/msgpack/v5 v5.1.4 h1:6K44/cU6dMNGkVTGGuu7ef2NdSRFMhAFGGLfE3cqtHM=
-github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
-github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
-github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/vmihailenco/msgpack/v5 v5.3.2 h1:MsXyN2rqdM8NM0lLiIpTn610e8Zcoj8ZuHxsMOi9qhI=
+github.com/vmihailenco/msgpack/v5 v5.3.2/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
@@ -147,14 +148,16 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -162,8 +165,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -180,8 +183,11 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -191,10 +197,11 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/cmd/genji/shell/command.go b/cmd/genji/shell/command.go
index 3fa0e5c6..96792518 100644
--- a/cmd/genji/shell/command.go
+++ b/cmd/genji/shell/command.go
@@ -1,6 +1,7 @@
 package shell
 
 import (
+	"bytes"
 	"context"
 	"encoding/csv"
 	"errors"
@@ -13,7 +14,6 @@ import (
 	"github.com/genjidb/genji/cmd/genji/dbutil"
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/stringutil"
 )
 
@@ -123,56 +123,40 @@ func runTablesCmd(db *genji.DB, w io.Writer) error {
 // runIndexesCmd displays a list of indexes. If table is non-empty, it only
 // display that table's indexes. If not, it displays all indexes.
 func runIndexesCmd(db *genji.DB, tableName string, w io.Writer) error {
-	return db.View(func(tx *genji.Tx) error {
-		q := "SELECT * FROM __genji_indexes"
-
-		if tableName != "" {
-			// ensure table exists
+	// ensure table exists
+	if tableName != "" {
+		err := db.View(func(tx *genji.Tx) error {
 			_, err := tx.QueryDocument("SELECT 1 FROM __genji_tables WHERE table_name = ? LIMIT 1", tableName)
 			if err != nil {
 				if err == database.ErrDocumentNotFound {
 					return stringutil.Errorf("%w: %q", database.ErrTableNotFound, tableName)
 				}
-				return err
 			}
-
-			q += " WHERE table_name = ?"
-		}
-
-		res, err := tx.Query(q, tableName)
+			return err
+		})
 		if err != nil {
 			return err
 		}
-		defer res.Close()
-
-		return res.Iterate(func(d document.Document) error {
-			var index database.IndexInfo
-
-			if err := index.ScanDocument(d); err != nil {
-				return err
-			}
+	}
 
-			var paths []string
-			for _, path := range index.Paths {
-				paths = append(paths, path.String())
-			}
+	indexes, err := dbutil.ListIndexes(context.Background(), db, tableName)
+	if err != nil {
+		return err
+	}
 
-			fmt.Fprintf(w, "%s ON %s (%s)\n", index.IndexName, index.TableName, strings.Join(paths, ", "))
+	for _, idx := range indexes {
+		_, err = fmt.Fprintln(w, idx)
+		if err != nil {
+			return err
+		}
+	}
 
-			return nil
-		})
-	})
+	return nil
 }
 
 // runSaveCommand saves the currently opened database at the given path.
 // If a path already exists, existing values in the target database will be overwritten.
 func runSaveCmd(ctx context.Context, db *genji.DB, engineName string, dbPath string) error {
-	tx, err := db.Begin(false)
-	if err != nil {
-		return err
-	}
-	defer tx.Rollback()
-
 	// Open the new database
 	otherDB, err := dbutil.OpenDB(ctx, dbPath, engineName)
 	if err != nil {
@@ -181,96 +165,21 @@ func runSaveCmd(ctx context.Context, db *genji.DB, engineName string, dbPath str
 	otherDB = otherDB.WithContext(ctx)
 	defer otherDB.Close()
 
-	otherTx, err := otherDB.Begin(true)
-	if err != nil {
-		return err
-	}
-	defer otherTx.Rollback()
+	var dbDump bytes.Buffer
 
-	// Find all tables
-	tables, err := tx.Query("SELECT table_name FROM __genji_tables")
+	err = dbutil.Dump(ctx, db, &dbDump)
 	if err != nil {
 		return err
 	}
-	defer tables.Close()
-
-	err = tables.Iterate(func(d document.Document) error {
-		// Get table name.
-		var tableName string
-		if err := document.Scan(d, &tableName); err != nil {
-			return err
-		}
-
-		table, err := tx.GetTable(tableName)
-		if err != nil {
-			return err
-		}
-
-		ti := table.Info()
 
-		err = otherTx.CreateTable(tableName, ti)
-		if err != nil {
-			return err
-		}
-		otherTable, err := otherTx.GetTable(tableName)
-		if err != nil {
-			return err
-		}
-
-		it := table.Store.Iterator(engine.IteratorOptions{})
-		defer it.Close()
-
-		var b []byte
-		for it.Seek(nil); it.Valid(); it.Next() {
-			itm := it.Item()
-			b, err := itm.ValueCopy(b)
-			if err != nil {
-				return err
-			}
-
-			err = otherTable.Store.Put(itm.Key(), b)
-			if err != nil {
-				return err
-			}
-		}
-
-		return err
-	})
+	err = otherDB.Exec(dbDump.String())
 	if err != nil {
 		return err
 	}
 
-	// Find all indexes
-	indexes, err := tx.Query("SELECT * FROM __genji_indexes")
-	if err != nil {
-		return err
-	}
-	defer indexes.Close()
-
-	err = indexes.Iterate(func(d document.Document) error {
-		var index database.IndexInfo
-
-		if err := index.ScanDocument(d); err != nil {
-			return err
-		}
-
-		err = otherTx.CreateIndex(&index)
-		if err != nil {
-			return err
-		}
-
-		return nil
-	})
-	if err != nil {
-		return err
-	}
-
-	err = otherTx.ReIndexAll()
-	if err != nil {
-		return err
-	}
+	dbDump.Reset()
 
-	err = otherTx.Commit()
+	err = dbutil.Dump(ctx, otherDB, &dbDump)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/genji/shell/shell.go b/cmd/genji/shell/shell.go
index 61a9e7dd..9f50a853 100644
--- a/cmd/genji/shell/shell.go
+++ b/cmd/genji/shell/shell.go
@@ -507,16 +507,6 @@ func (sh *Shell) changelivePrefix() (string, bool) {
 	return sh.livePrefix, sh.multiLine
 }
 
-func (sh *Shell) getAllIndexes(ctx context.Context) ([]string, error) {
-	var listName []string
-	err := sh.db.View(func(tx *genji.Tx) error {
-		listName = tx.ListIndexes()
-		return nil
-	})
-
-	return listName, err
-}
-
 // getTables returns all the tables of the database
 func (sh *Shell) getAllTables(ctx context.Context) ([]string, error) {
 	var tables []string
@@ -565,7 +555,7 @@ func (sh *Shell) completer(in prompt.Document) []prompt.Suggest {
 				return suggestions
 			}
 		case "index_name":
-			expected, err = sh.getAllIndexes(context.Background())
+			expected, err = dbutil.ListIndexes(context.Background(), sh.db, "")
 			if err != nil {
 				return suggestions
 			}
diff --git a/database/catalog.go b/database/catalog.go
index a2129953..e53bc0f5 100644
--- a/database/catalog.go
+++ b/database/catalog.go
@@ -1,14 +1,23 @@
 package database
 
 import (
+	"encoding/binary"
 	"errors"
 	"strings"
 	"sync"
 
 	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/stringutil"
 )
 
+const (
+	storePrefix        = 't'
+	internalPrefix     = "__genji_"
+	tableInfoStoreName = internalPrefix + "tables"
+	indexInfoStoreName = internalPrefix + "indexes"
+)
+
 // Catalog holds all table and index informations.
 type Catalog struct {
 	cache *catalogCache
@@ -20,21 +29,11 @@ func NewCatalog() *Catalog {
 	}
 }
 
-func (c *Catalog) Load(tx *Transaction) error {
-	tables, err := tx.getTableStore().ListAll()
-	if err != nil {
-		return err
-	}
-
-	indexes, err := tx.getIndexStore().ListAll()
-	if err != nil {
-		return err
-	}
-
-	tables = append(tables, &TableInfo{
-		tableName: tableInfoStoreName,
-		storeName: []byte(tableInfoStoreName),
-		readOnly:  true,
+func (c *Catalog) Load(tables []TableInfo, indexes []IndexInfo) {
+	tables = append(tables, TableInfo{
+		TableName: tableInfoStoreName,
+		StoreName: []byte(tableInfoStoreName),
+		ReadOnly:  true,
 		FieldConstraints: []*FieldConstraint{
 			{
 				Path: document.Path{
@@ -48,10 +47,10 @@ func (c *Catalog) Load(tx *Transaction) error {
 		},
 	})
 
-	tables = append(tables, &TableInfo{
-		tableName: indexStoreName,
-		storeName: []byte(indexStoreName),
-		readOnly:  true,
+	tables = append(tables, TableInfo{
+		TableName: indexInfoStoreName,
+		StoreName: []byte(indexInfoStoreName),
+		ReadOnly:  true,
 		FieldConstraints: []*FieldConstraint{
 			{
 				Path: document.Path{
@@ -66,7 +65,6 @@ func (c *Catalog) Load(tx *Transaction) error {
 	})
 
 	c.cache.load(tables, indexes)
-	return nil
 }
 
 // Clone the catalog. Mostly used for testing purposes.
@@ -84,7 +82,7 @@ func (c *Catalog) GetTable(tx *Transaction, tableName string) (*Table, error) {
 		return nil, err
 	}
 
-	s, err := tx.tx.GetStore(ti.storeName)
+	s, err := tx.Tx.GetStore(ti.StoreName)
 	if err != nil {
 		return nil, err
 	}
@@ -93,15 +91,15 @@ func (c *Catalog) GetTable(tx *Transaction, tableName string) (*Table, error) {
 	indexes := make([]*Index, 0, len(idxInfos))
 
 	for i := range idxInfos {
-		indexes = append(indexes, NewIndex(tx.tx, idxInfos[i].IndexName, idxInfos[i]))
+		indexes = append(indexes, NewIndex(tx.Tx, idxInfos[i].IndexName, idxInfos[i]))
 	}
 
 	return &Table{
-		tx:      tx,
+		Tx:      tx,
+		Name:    tableName,
 		Store:   s,
-		name:    tableName,
-		indexes: indexes,
-		info:    ti,
+		Info:    ti,
+		Indexes: indexes,
 	}, nil
 }
 
@@ -115,7 +113,7 @@ func (c *Catalog) CreateTable(tx *Transaction, tableName string, info *TableInfo
 	if info == nil {
 		info = new(TableInfo)
 	}
-	info.tableName = tableName
+	info.TableName = tableName
 
 	var err error
 
@@ -125,25 +123,20 @@ func (c *Catalog) CreateTable(tx *Transaction, tableName string, info *TableInfo
 		return err
 	}
 
-	err = c.cache.AddTable(tx, info)
+	err = insertTable(tx, tableName, info)
 	if err != nil {
 		return err
 	}
 
-	err = tx.getTableStore().Insert(tx, tableName, info)
-	if err != nil {
-		return err
-	}
-
-	err = tx.tx.CreateStore(info.storeName)
+	err = tx.Tx.CreateStore(info.StoreName)
 	if err != nil {
 		return stringutil.Errorf("failed to create table %q: %w", tableName, err)
 	}
 
-	return nil
+	return c.cache.AddTable(tx, info)
 }
 
-// DropTable deletes a table from the database.
+// DropTable deletes a table from the
 func (c *Catalog) DropTable(tx *Transaction, tableName string) error {
 	ti, removedIndexes, err := c.cache.DeleteTable(tx, tableName)
 	if err != nil {
@@ -157,52 +150,43 @@ func (c *Catalog) DropTable(tx *Transaction, tableName string) error {
 		}
 	}
 
-	err = tx.getTableStore().Delete(tx, tableName)
+	err = deleteTable(tx, tableName)
 	if err != nil {
 		return err
 	}
 
-	return tx.tx.DropStore(ti.storeName)
+	return tx.Tx.DropStore(ti.StoreName)
 }
 
 // CreateIndex creates an index with the given name.
 // If it already exists, returns ErrIndexAlreadyExists.
-func (c *Catalog) CreateIndex(tx *Transaction, opts *IndexInfo) error {
-	if strings.HasPrefix(opts.IndexName, internalPrefix) {
+func (c *Catalog) CreateIndex(tx *Transaction, info *IndexInfo) error {
+	if strings.HasPrefix(info.IndexName, internalPrefix) {
 		return stringutil.Errorf("table name must not start with %s", internalPrefix)
 	}
 
-	// auto-generate index name
-	if opts.IndexName == "" {
-		seq, err := tx.getIndexStore().st.NextSequence()
-		if err != nil {
-			return err
-		}
-
-		opts.IndexName = stringutil.Sprintf("%sautoindex_%s_%d", internalPrefix, opts.TableName, seq)
-	}
-
-	err := c.cache.AddIndex(tx, opts)
+	err := insertIndex(tx, info)
 	if err != nil {
 		return err
 	}
 
-	err = tx.getIndexStore().Insert(opts)
+	err = c.cache.AddIndex(tx, info)
 	if err != nil {
 		return err
 	}
 
-	idx, err := c.GetIndex(tx, opts.IndexName)
+	idx, err := c.GetIndex(tx, info.IndexName)
 	if err != nil {
 		return err
 	}
 
-	tb, err := c.GetTable(tx, opts.TableName)
+	tb, err := c.GetTable(tx, info.TableName)
 	if err != nil {
 		return err
 	}
 
-	return c.buildIndex(tx, idx, tb)
+	err = c.buildIndex(tx, idx, tb)
+	return err
 }
 
 // GetIndex returns an index by name.
@@ -212,7 +196,7 @@ func (c *Catalog) GetIndex(tx *Transaction, indexName string) (*Index, error) {
 		return nil, err
 	}
 
-	return NewIndex(tx.tx, info.IndexName, info), nil
+	return NewIndex(tx.Tx, info.IndexName, info), nil
 }
 
 // ListIndexes returns an index by name.
@@ -240,20 +224,12 @@ func (c *Catalog) DropIndex(tx *Transaction, name string) error {
 }
 
 func (c *Catalog) dropIndex(tx *Transaction, name string) error {
-	indexStore := tx.getIndexStore()
-
-	opts, err := indexStore.Get(name)
+	err := deleteIndex(tx, name)
 	if err != nil {
 		return err
 	}
-	err = indexStore.Delete(name)
-	if err != nil {
-		return err
-	}
-
-	idx := NewIndex(tx.tx, opts.IndexName, opts)
 
-	return idx.Truncate()
+	return NewIndex(tx.Tx, name, nil).Truncate()
 }
 
 // AddFieldConstraint adds a field constraint to a table.
@@ -265,42 +241,38 @@ func (c *Catalog) AddFieldConstraint(tx *Transaction, tableName string, fc Field
 		return err
 	}
 
-	return tx.getTableStore().Replace(tx, tableName, newTi)
+	return replaceTable(tx, tableName, newTi)
 }
 
 // RenameTable renames a table.
 // If it doesn't exist, it returns ErrTableNotFound.
 func (c *Catalog) RenameTable(tx *Transaction, oldName, newName string) error {
 	newTi, newIdxs, err := c.cache.updateTable(tx, oldName, func(clone *TableInfo) error {
-		clone.tableName = newName
+		clone.TableName = newName
 		return nil
 	})
 	if err != nil {
 		return err
 	}
 
-	tableStore := tx.getTableStore()
-
 	// Insert the TableInfo keyed by the newName name.
-	err = tableStore.Insert(tx, newName, newTi)
+	err = insertTable(tx, newName, newTi)
 	if err != nil {
 		return err
 	}
 
 	if len(newIdxs) > 0 {
-		indexStore := tx.getIndexStore()
-
 		for _, idx := range newIdxs {
 			idx.TableName = newName
-			err = indexStore.Replace(idx.IndexName, *idx)
+			err = replaceIndex(tx, idx.IndexName, *idx)
 			if err != nil {
 				return err
 			}
 		}
 	}
 
-	// Delete the old reference from the tableInfoStore.
-	return tableStore.Delete(tx, oldName)
+	// Delete the old table info.
+	return deleteTable(tx, oldName)
 }
 
 // ReIndex truncates and recreates selected index from scratch.
@@ -376,17 +348,17 @@ func newCatalogCache() *catalogCache {
 	}
 }
 
-func (c *catalogCache) load(tables []*TableInfo, indexes []*IndexInfo) {
+func (c *catalogCache) load(tables []TableInfo, indexes []IndexInfo) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	for _, t := range tables {
-		c.tables[t.tableName] = t
+	for i := range tables {
+		c.tables[tables[i].TableName] = &tables[i]
 	}
 
-	for _, i := range indexes {
-		c.indexes[i.IndexName] = i
-		c.indexesPerTables[i.TableName] = append(c.indexesPerTables[i.TableName], i)
+	for i := range indexes {
+		c.indexes[indexes[i].IndexName] = &indexes[i]
+		c.indexesPerTables[indexes[i].TableName] = append(c.indexesPerTables[indexes[i].TableName], &indexes[i])
 	}
 }
 
@@ -410,17 +382,17 @@ func (c *catalogCache) AddTable(tx *Transaction, info *TableInfo) error {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	if _, ok := c.tables[info.tableName]; ok {
+	if _, ok := c.tables[info.TableName]; ok {
 		return ErrTableAlreadyExists
 	}
 
-	c.tables[info.tableName] = info
+	c.tables[info.TableName] = info
 
-	tx.onRollbackHooks = append(tx.onRollbackHooks, func() {
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
 		c.mu.Lock()
 		defer c.mu.Unlock()
 
-		delete(c.tables, info.tableName)
+		delete(c.tables, info.TableName)
 	})
 
 	return nil
@@ -435,7 +407,7 @@ func (c *catalogCache) DeleteTable(tx *Transaction, tableName string) (*TableInf
 		return nil, nil, ErrTableNotFound
 	}
 
-	if ti.readOnly {
+	if ti.ReadOnly {
 		return nil, nil, errors.New("cannot write to read-only table")
 	}
 
@@ -451,7 +423,7 @@ func (c *catalogCache) DeleteTable(tx *Transaction, tableName string) (*TableInf
 		removedIndexes = append(removedIndexes, idx)
 	}
 
-	tx.onRollbackHooks = append(tx.onRollbackHooks, func() {
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
 		c.mu.Lock()
 		defer c.mu.Unlock()
 
@@ -519,7 +491,7 @@ OUTER:
 	previousIndexes := c.indexesPerTables[info.TableName]
 	c.indexesPerTables[info.TableName] = append(c.indexesPerTables[info.TableName], info)
 
-	tx.onRollbackHooks = append(tx.onRollbackHooks, func() {
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
 		c.mu.Lock()
 		defer c.mu.Unlock()
 
@@ -560,7 +532,7 @@ func (c *catalogCache) DeleteIndex(tx *Transaction, indexName string) (*IndexInf
 	oldIndexList := c.indexesPerTables[info.TableName]
 	c.indexesPerTables[info.TableName] = newIndexlist
 
-	tx.onRollbackHooks = append(tx.onRollbackHooks, func() {
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
 		c.mu.Lock()
 		defer c.mu.Unlock()
 
@@ -611,7 +583,7 @@ func (c *catalogCache) updateTable(tx *Transaction, tableName string, fn func(cl
 		return nil, nil, ErrTableNotFound
 	}
 
-	if ti.readOnly {
+	if ti.ReadOnly {
 		return nil, nil, errors.New("cannot write to read-only table")
 	}
 
@@ -622,33 +594,199 @@ func (c *catalogCache) updateTable(tx *Transaction, tableName string, fn func(cl
 	}
 
 	var oldIndexes, newIndexes []*IndexInfo
-	if clone.tableName != tableName {
+	// if the table has been renamed, we need to modify indexes
+	// to point to the new table
+	if clone.TableName != tableName {
+		// delete the old table name from the table list
 		delete(c.tables, tableName)
 
-		for _, idx := range c.indexes {
-			if idx.TableName == tableName {
-				idxClone := idx.Clone()
-				idxClone.TableName = clone.tableName
-				newIndexes = append(newIndexes, idxClone)
-				oldIndexes = append(oldIndexes, idx)
-				c.indexes[idxClone.IndexName] = idxClone
-			}
+		for _, idx := range c.indexesPerTables[tableName] {
+			idxClone := idx.Clone()
+			idxClone.TableName = clone.TableName
+			newIndexes = append(newIndexes, idxClone)
+			oldIndexes = append(oldIndexes, idx)
+			c.indexes[idxClone.IndexName] = idxClone
+		}
+		if len(newIndexes) > 0 {
+			c.indexesPerTables[clone.TableName] = newIndexes
+			delete(c.indexesPerTables, tableName)
 		}
 	}
 
-	c.tables[clone.tableName] = clone
+	c.tables[clone.TableName] = clone
 
-	tx.onRollbackHooks = append(tx.onRollbackHooks, func() {
+	tx.OnRollbackHooks = append(tx.OnRollbackHooks, func() {
 		c.mu.Lock()
 		defer c.mu.Unlock()
 
-		delete(c.tables, clone.tableName)
+		delete(c.tables, clone.TableName)
 		c.tables[tableName] = ti
 
 		for _, idx := range oldIndexes {
 			c.indexes[idx.IndexName] = idx
 		}
+
+		if clone.TableName != tableName {
+			delete(c.indexesPerTables, clone.TableName)
+			c.indexesPerTables[tableName] = oldIndexes
+		}
 	})
 
 	return clone, newIndexes, nil
 }
+
+func initStores(tx *Transaction) error {
+	_, err := tx.Tx.GetStore([]byte(tableInfoStoreName))
+	if err == engine.ErrStoreNotFound {
+		err = tx.Tx.CreateStore([]byte(tableInfoStoreName))
+	}
+	if err != nil {
+		return err
+	}
+
+	_, err = tx.Tx.GetStore([]byte(indexInfoStoreName))
+	if err == engine.ErrStoreNotFound {
+		err = tx.Tx.CreateStore([]byte(indexInfoStoreName))
+	}
+	return err
+}
+
+func GetTableStore(tx *Transaction) *Table {
+	st, err := tx.Tx.GetStore([]byte(tableInfoStoreName))
+	if err != nil {
+		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", tableInfoStoreName, err))
+	}
+
+	return &Table{
+		Tx:    tx,
+		Store: st,
+		Info: &TableInfo{
+			TableName: tableInfoStoreName,
+			StoreName: []byte(tableInfoStoreName),
+			FieldConstraints: []*FieldConstraint{
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "statements",
+						},
+					},
+					Type: document.TextValue,
+				},
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "table_name",
+						},
+					},
+					Type:         document.TextValue,
+					IsPrimaryKey: true,
+				},
+			},
+		},
+	}
+}
+
+func GetIndexStore(tx *Transaction) *Table {
+	st, err := tx.Tx.GetStore([]byte(indexInfoStoreName))
+	if err != nil {
+		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", indexInfoStoreName, err))
+	}
+
+	return &Table{
+		Tx:    tx,
+		Store: st,
+		Info: &TableInfo{
+			TableName: indexInfoStoreName,
+			StoreName: []byte(indexInfoStoreName),
+			FieldConstraints: []*FieldConstraint{
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "statements",
+						},
+					},
+					Type: document.TextValue,
+				},
+				{
+					Path: document.Path{
+						document.PathFragment{
+							FieldName: "index_name",
+						},
+					},
+					Type:         document.TextValue,
+					IsPrimaryKey: true,
+				},
+			},
+		},
+	}
+}
+
+// insertTable a new tableInfo for the given table name.
+// If info.StoreName is nil, it generates one and stores it in info.
+func insertTable(tx *Transaction, tableName string, info *TableInfo) error {
+	tb := GetTableStore(tx)
+
+	if info.StoreName == nil {
+		seq, err := tb.Store.NextSequence()
+		if err != nil {
+			return err
+		}
+		buf := make([]byte, binary.MaxVarintLen64+1)
+		buf[0] = storePrefix
+		n := binary.PutUvarint(buf[1:], seq)
+		info.StoreName = buf[:n+1]
+	}
+
+	_, err := tb.Insert(info.ToDocument())
+	if err == ErrDuplicateDocument {
+		return ErrTableAlreadyExists
+	}
+
+	return err
+}
+
+func deleteTable(tx *Transaction, tableName string) error {
+	tb := GetTableStore(tx)
+
+	return tb.Delete([]byte(tableName))
+}
+
+// Replace replaces tableName table information with the new info.
+func replaceTable(tx *Transaction, tableName string, info *TableInfo) error {
+	tb := GetTableStore(tx)
+
+	return tb.Replace([]byte(tableName), info.ToDocument())
+}
+
+func insertIndex(tx *Transaction, info *IndexInfo) error {
+	tb := GetIndexStore(tx)
+
+	// auto-generate index name
+	if info.IndexName == "" {
+		seq, err := tb.Store.NextSequence()
+		if err != nil {
+			return err
+		}
+
+		info.IndexName = stringutil.Sprintf("%sautoindex_%s_%d", internalPrefix, info.TableName, seq)
+	}
+
+	_, err := tb.Insert(info.ToDocument())
+	if err == ErrDuplicateDocument {
+		return ErrIndexAlreadyExists
+	}
+
+	return err
+}
+
+func replaceIndex(tx *Transaction, indexName string, info IndexInfo) error {
+	tb := GetIndexStore(tx)
+
+	return tb.Replace([]byte(indexName), info.ToDocument())
+}
+
+func deleteIndex(tx *Transaction, indexName string) error {
+	tb := GetIndexStore(tx)
+
+	return tb.Delete([]byte(indexName))
+}
diff --git a/database/config.go b/database/config.go
index c97e5917..aa8d8960 100644
--- a/database/config.go
+++ b/database/config.go
@@ -1,23 +1,19 @@
 package database
 
 import (
-	"bytes"
-	"encoding/binary"
+	"strings"
 
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/stringutil"
 )
 
-const storePrefix = 't'
-
 // TableInfo contains information about a table.
 type TableInfo struct {
 	// name of the table.
-	tableName string
+	TableName string
 	// name of the store associated with the table.
-	storeName []byte
-	readOnly  bool
+	StoreName []byte
+	ReadOnly  bool
 
 	FieldConstraints FieldConstraints
 }
@@ -37,70 +33,61 @@ func (ti *TableInfo) GetPrimaryKey() *FieldConstraint {
 // ToDocument turns ti into a document.
 func (ti *TableInfo) ToDocument() document.Document {
 	buf := document.NewFieldBuffer()
+	buf.Add("sql", document.NewTextValue(ti.String()))
+	buf.Add("table_name", document.NewTextValue(ti.TableName))
+	buf.Add("store_name", document.NewBlobValue(ti.StoreName))
+	return buf
+}
 
-	buf.Add("table_name", document.NewTextValue(ti.tableName))
-	buf.Add("store_name", document.NewBlobValue(ti.storeName))
+// String returns a SQL representation.
+func (ti *TableInfo) String() string {
+	var s strings.Builder
 
-	vbuf := document.NewValueBuffer()
-	for _, fc := range ti.FieldConstraints {
-		vbuf = vbuf.Append(document.NewDocumentValue(fc.ToDocument()))
+	stringutil.Fprintf(&s, "CREATE TABLE %s", ti.TableName)
+	if len(ti.FieldConstraints) > 0 {
+		s.WriteString(" (")
 	}
 
-	buf.Add("field_constraints", document.NewArrayValue(vbuf))
-
-	buf.Add("read_only", document.NewBoolValue(ti.readOnly))
-	return buf
-}
+	for i, fc := range ti.FieldConstraints {
+		if fc.IsInferred {
+			continue
+		}
 
-// ScanDocument decodes d into ti.
-func (ti *TableInfo) ScanDocument(d document.Document) error {
-	v, err := d.GetByField("table_name")
-	if err != nil {
-		return err
-	}
-	ti.tableName = v.V.(string)
+		if i > 0 {
+			s.WriteString(", ")
+		}
 
-	v, err = d.GetByField("store_name")
-	if err != nil {
-		return err
-	}
-	ti.storeName = make([]byte, len(v.V.([]byte)))
-	copy(ti.storeName, v.V.([]byte))
+		// Path
+		s.WriteString(fc.Path.String())
 
-	v, err = d.GetByField("field_constraints")
-	if err != nil {
-		return err
-	}
-	ar := v.V.(document.Array)
+		// Type
+		if fc.Type != 0 {
+			stringutil.Fprintf(&s, " %s", strings.ToUpper(fc.Type.String()))
+		}
 
-	l, err := document.ArrayLength(ar)
-	if err != nil {
-		return err
-	}
+		// Not null
+		if fc.IsNotNull {
+			s.WriteString(" NOT NULL")
+		}
 
-	ti.FieldConstraints = make([]*FieldConstraint, l)
+		// Default value
+		if fc.HasDefaultValue() {
+			stringutil.Fprintf(&s, " DEFAULT %s", fc.DefaultValue.String())
+		}
 
-	err = ar.Iterate(func(i int, value document.Value) error {
-		var fc FieldConstraint
-		err := fc.ScanDocument(value.V.(document.Document))
-		if err != nil {
-			return err
+		// Primary key
+		if fc.IsPrimaryKey {
+			s.WriteString(" PRIMARY KEY")
 		}
 
-		ti.FieldConstraints[i] = &fc
-		return nil
-	})
-	if err != nil {
-		return err
+		// Unique must not be written as it an index is already created for it
 	}
 
-	v, err = d.GetByField("read_only")
-	if err != nil {
-		return err
+	if len(ti.FieldConstraints) > 0 {
+		s.WriteString(")")
 	}
 
-	ti.readOnly = v.V.(bool)
-	return nil
+	return s.String()
 }
 
 // Clone creates another tableInfo with the same values.
@@ -111,121 +98,6 @@ func (ti *TableInfo) Clone() *TableInfo {
 	return &cp
 }
 
-// tableStore manages table information.
-// It loads table information during database startup
-// and holds it in memory.
-type tableStore struct {
-	db *Database
-	st engine.Store
-}
-
-// List all tables.
-func (t *tableStore) ListAll() ([]*TableInfo, error) {
-	it := t.st.Iterator(engine.IteratorOptions{})
-	defer it.Close()
-
-	var list []*TableInfo
-	var buf []byte
-	var err error
-
-	for it.Seek(nil); it.Valid(); it.Next() {
-		itm := it.Item()
-		buf, err = itm.ValueCopy(buf)
-		if err != nil {
-			return nil, err
-		}
-
-		var ti TableInfo
-		err = ti.ScanDocument(t.db.Codec.NewDocument(buf))
-		if err != nil {
-			return nil, err
-		}
-
-		list = append(list, &ti)
-	}
-	if err := it.Err(); err != nil {
-		return nil, err
-	}
-
-	return list, nil
-}
-
-// Insert a new tableInfo for the given table name.
-// If info.storeName is nil, it generates one and stores it in info.
-func (t *tableStore) Insert(tx *Transaction, tableName string, info *TableInfo) error {
-	tblName := []byte(tableName)
-
-	_, err := t.st.Get(tblName)
-	if err == nil {
-		return ErrTableAlreadyExists
-	}
-	if err != engine.ErrKeyNotFound {
-		return err
-	}
-
-	if info.storeName == nil {
-		seq, err := t.st.NextSequence()
-		if err != nil {
-			return err
-		}
-		buf := make([]byte, binary.MaxVarintLen64+1)
-		buf[0] = storePrefix
-		n := binary.PutUvarint(buf[1:], seq)
-		info.storeName = buf[:n+1]
-	}
-
-	var buf bytes.Buffer
-	enc := t.db.Codec.NewEncoder(&buf)
-	defer enc.Close()
-	err = enc.EncodeDocument(info.ToDocument())
-	if err != nil {
-		return err
-	}
-
-	err = t.st.Put([]byte(tableName), buf.Bytes())
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func (t *tableStore) Delete(tx *Transaction, tableName string) error {
-	err := t.st.Delete([]byte(tableName))
-	if err != nil {
-		if err == engine.ErrKeyNotFound {
-			return stringutil.Errorf("%w: %q", ErrTableNotFound, tableName)
-		}
-
-		return err
-	}
-
-	return nil
-}
-
-// Replace replaces tableName table information with the new info.
-func (t *tableStore) Replace(tx *Transaction, tableName string, info *TableInfo) error {
-	var buf bytes.Buffer
-	enc := t.db.Codec.NewEncoder(&buf)
-	defer enc.Close()
-	err := enc.EncodeDocument(info.ToDocument())
-	if err != nil {
-		return err
-	}
-
-	tbName := []byte(tableName)
-	_, err = t.st.Get(tbName)
-	if err != nil {
-		if err == engine.ErrKeyNotFound {
-			return stringutil.Errorf("%w: %q", ErrTableNotFound, tableName)
-		}
-
-		return err
-	}
-
-	return t.st.Put(tbName, buf.Bytes())
-}
-
 // IndexInfo holds the configuration of an index.
 type IndexInfo struct {
 	TableName string
@@ -242,85 +114,31 @@ type IndexInfo struct {
 // ToDocument creates a document from an IndexConfig.
 func (i *IndexInfo) ToDocument() document.Document {
 	buf := document.NewFieldBuffer()
-
-	buf.Add("unique", document.NewBoolValue(i.Unique))
+	buf.Add("sql", document.NewTextValue(i.String()))
 	buf.Add("index_name", document.NewTextValue(i.IndexName))
 	buf.Add("table_name", document.NewTextValue(i.TableName))
 
-	vb := document.NewValueBuffer()
-	for _, path := range i.Paths {
-		vb.Append(document.NewArrayValue(pathToArray(path)))
-	}
-
-	buf.Add("paths", document.NewArrayValue(vb))
-	if i.Types != nil {
-		types := make([]document.Value, 0, len(i.Types))
-		for _, typ := range i.Types {
-			types = append(types, document.NewIntegerValue(int64(typ)))
-		}
-		buf.Add("types", document.NewArrayValue(document.NewValueBuffer(types...)))
-	}
 	return buf
 }
 
-// ScanDocument implements the document.Scanner interface.
-func (i *IndexInfo) ScanDocument(d document.Document) error {
-	v, err := d.GetByField("unique")
-	if err != nil {
-		return err
-	}
-	i.Unique = v.V.(bool)
-
-	v, err = d.GetByField("index_name")
-	if err != nil {
-		return err
-	}
-	i.IndexName = string(v.V.(string))
+// String returns a SQL representation.
+func (i *IndexInfo) String() string {
+	var s strings.Builder
 
-	v, err = d.GetByField("table_name")
-	if err != nil {
-		return err
-	}
-	i.TableName = string(v.V.(string))
-
-	v, err = d.GetByField("paths")
-	if err != nil {
-		return err
-	}
+	stringutil.Fprintf(&s, "CREATE INDEX %s ON %s (", i.IndexName, i.TableName)
 
-	i.Paths = nil
-	err = v.V.(document.Array).Iterate(func(ii int, pval document.Value) error {
-		p, err := arrayToPath(pval.V.(document.Array))
-		if err != nil {
-			return err
+	for i, p := range i.Paths {
+		if i > 0 {
+			s.WriteString(", ")
 		}
 
-		i.Paths = append(i.Paths, p)
-		return nil
-	})
-
-	if err != nil {
-		return err
+		// Path
+		s.WriteString(p.String())
 	}
 
-	v, err = d.GetByField("types")
-	if err != nil && err != document.ErrFieldNotFound {
-		return err
-	}
-
-	if err == nil {
-		i.Types = nil
-		err = v.V.(document.Array).Iterate(func(ii int, tval document.Value) error {
-			i.Types = append(i.Types, document.ValueType(tval.V.(int64)))
-			return nil
-		})
-
-		if err != nil {
-			return err
-		}
-	}
+	s.WriteString(")")
 
-	return nil
+	return s.String()
 }
 
 // Clone returns a copy of the index information.
@@ -338,101 +156,6 @@ func (i IndexInfo) Clone() *IndexInfo {
 	return &c
 }
 
-type indexStore struct {
-	db *Database
-	st engine.Store
-}
-
-func (t *indexStore) Insert(cfg *IndexInfo) error {
-	key := []byte(cfg.IndexName)
-	_, err := t.st.Get(key)
-	if err == nil {
-		return ErrIndexAlreadyExists
-	}
-	if err != engine.ErrKeyNotFound {
-		return err
-	}
-
-	var buf bytes.Buffer
-	enc := t.db.Codec.NewEncoder(&buf)
-	defer enc.Close()
-	err = enc.EncodeDocument(cfg.ToDocument())
-	if err != nil {
-		return err
-	}
-
-	return t.st.Put(key, buf.Bytes())
-}
-
-func (t *indexStore) Get(indexName string) (*IndexInfo, error) {
-	key := []byte(indexName)
-	v, err := t.st.Get(key)
-	if err == engine.ErrKeyNotFound {
-		return nil, ErrIndexNotFound
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	var idxopts IndexInfo
-	err = idxopts.ScanDocument(t.db.Codec.NewDocument(v))
-	if err != nil {
-		return nil, err
-	}
-
-	return &idxopts, nil
-}
-
-func (t *indexStore) Replace(indexName string, cfg IndexInfo) error {
-	var buf bytes.Buffer
-	enc := t.db.Codec.NewEncoder(&buf)
-	defer enc.Close()
-	err := enc.EncodeDocument(cfg.ToDocument())
-	if err != nil {
-		return err
-	}
-
-	return t.st.Put([]byte(indexName), buf.Bytes())
-}
-
-func (t *indexStore) Delete(indexName string) error {
-	key := []byte(indexName)
-	err := t.st.Delete(key)
-	if err == engine.ErrKeyNotFound {
-		return ErrIndexNotFound
-	}
-	return err
-}
-
-func (t *indexStore) ListAll() ([]*IndexInfo, error) {
-	it := t.st.Iterator(engine.IteratorOptions{})
-	defer it.Close()
-
-	var idxList []*IndexInfo
-	var buf []byte
-	var err error
-	for it.Seek(nil); it.Valid(); it.Next() {
-		item := it.Item()
-		buf, err = item.ValueCopy(buf)
-		if err != nil {
-			return nil, err
-		}
-
-		var opts IndexInfo
-		err = opts.ScanDocument(t.db.Codec.NewDocument(buf))
-		if err != nil {
-			return nil, err
-		}
-
-		idxList = append(idxList, &opts)
-	}
-	if err := it.Err(); err != nil {
-		return nil, err
-	}
-
-	return idxList, nil
-}
-
 type Indexes []*Index
 
 func (i Indexes) GetIndex(name string) *Index {
diff --git a/database/database.go b/database/database.go
index 8d16195d..fb1e5b56 100644
--- a/database/database.go
+++ b/database/database.go
@@ -24,9 +24,7 @@ type Database struct {
 	// Codec used to encode documents. Defaults to MessagePack.
 	Codec encoding.Codec
 
-	// table and index catalog.
-	catalog *Catalog
-
+	Catalog *Catalog
 	// This controls concurrency on read-only and read/write transactions.
 	txmu sync.RWMutex
 }
@@ -42,8 +40,9 @@ func New(ctx context.Context, ng engine.Engine, opts Options) (*Database, error)
 	}
 
 	db := Database{
-		ng:    ng,
-		Codec: opts.Codec,
+		ng:      ng,
+		Codec:   opts.Codec,
+		Catalog: NewCatalog(),
 	}
 
 	tx, err := db.BeginTx(ctx, &TxOptions{})
@@ -52,44 +51,13 @@ func New(ctx context.Context, ng engine.Engine, opts Options) (*Database, error)
 	}
 	defer tx.Rollback()
 
-	err = db.initCatalog(tx)
+	err = initStores(tx)
 	if err != nil {
 		return nil, err
 	}
 
 	err = tx.Commit()
-	if err != nil {
-		return nil, err
-	}
-
-	return &db, nil
-}
-
-func (db *Database) initCatalog(tx *Transaction) error {
-	_, err := tx.tx.GetStore([]byte(tableInfoStoreName))
-	if err == engine.ErrStoreNotFound {
-		err = tx.tx.CreateStore([]byte(tableInfoStoreName))
-	}
-	if err != nil {
-		return err
-	}
-
-	_, err = tx.tx.GetStore([]byte(indexStoreName))
-	if err == engine.ErrStoreNotFound {
-		err = tx.tx.CreateStore([]byte(indexStoreName))
-	}
-	if err != nil {
-		return err
-	}
-
-	c := NewCatalog()
-	err = c.Load(tx)
-	if err != nil {
-		return err
-	}
-
-	db.catalog = c
-	return nil
+	return &db, err
 }
 
 // Close the underlying engine.
@@ -137,9 +105,10 @@ func (db *Database) BeginTx(ctx context.Context, opts *TxOptions) (*Transaction,
 	}
 
 	tx := Transaction{
-		db:       db,
-		tx:       ntx,
-		writable: !opts.ReadOnly,
+		DB:       db,
+		Tx:       ntx,
+		Catalog:  db.Catalog,
+		Writable: !opts.ReadOnly,
 		attached: opts.Attached,
 	}
 
@@ -169,7 +138,3 @@ func (db *Database) GetAttachedTx() *Transaction {
 
 	return db.attachedTransaction
 }
-
-func (db *Database) Catalog() *Catalog {
-	return db.catalog
-}
diff --git a/database/table.go b/database/table.go
index 4851e67f..92acef63 100644
--- a/database/table.go
+++ b/database/table.go
@@ -13,35 +13,17 @@ import (
 
 // A Table represents a collection of documents.
 type Table struct {
-	tx      *Transaction
-	Store   engine.Store
-	name    string
-	info    *TableInfo
-	indexes Indexes
-}
-
-// Tx returns the current transaction.
-func (t *Table) Tx() *Transaction {
-	return t.tx
-}
-
-// Info returns table information.
-// Returned TableInfo may not represent the most up to date data.
-// Always get a fresh Table instance before calling this method.
-func (t *Table) Info() *TableInfo {
-	return t.info
-}
-
-// Indexes returns the list of indexes of this table.
-// Returned Indexes may not represent the most up to date data.
-// Always get a fresh Table instance before calling this method.
-func (t *Table) Indexes() Indexes {
-	return t.indexes
-}
-
-// Name returns the name of the table.
-func (t *Table) Name() string {
-	return t.name
+	Tx    *Transaction
+	Store engine.Store
+	Name  string
+	// Table information.
+	// May not represent the most up to date data.
+	// Always get a fresh Table instance before relying on this field.
+	Info *TableInfo
+	// List of Indexes of this table.
+	// May not represent the most up to date data.
+	// Always get a fresh Table instance relying on this field.
+	Indexes Indexes
 }
 
 // Truncate deletes all the documents from the table.
@@ -55,18 +37,16 @@ func (t *Table) Truncate() error {
 // If no primary key has been selected, a monotonic autoincremented integer key will be generated.
 // It returns the inserted document alongside its key. They key can be accessed using the document.Keyer interface.
 func (t *Table) Insert(d document.Document) (document.Document, error) {
-	info := t.Info()
-
-	if info.readOnly {
+	if t.Info.ReadOnly {
 		return nil, errors.New("cannot write to read-only table")
 	}
 
-	fb, err := info.FieldConstraints.ValidateDocument(d)
+	fb, err := t.Info.FieldConstraints.ValidateDocument(d)
 	if err != nil {
 		return nil, err
 	}
 
-	key, err := t.generateKey(info, fb)
+	key, err := t.generateKey(t.Info, fb)
 	if err != nil {
 		return nil, err
 	}
@@ -77,7 +57,7 @@ func (t *Table) Insert(d document.Document) (document.Document, error) {
 	}
 
 	var buf bytes.Buffer
-	enc := t.tx.db.Codec.NewEncoder(&buf)
+	enc := t.Tx.DB.Codec.NewEncoder(&buf)
 	defer enc.Close()
 	err = enc.EncodeDocument(fb)
 	if err != nil {
@@ -89,9 +69,7 @@ func (t *Table) Insert(d document.Document) (document.Document, error) {
 		return nil, err
 	}
 
-	indexes := t.Indexes()
-
-	for _, idx := range indexes {
+	for _, idx := range t.Indexes {
 		vs := make([]document.Value, 0, len(idx.Info.Paths))
 
 		for _, path := range idx.Info.Paths {
@@ -116,16 +94,14 @@ func (t *Table) Insert(d document.Document) (document.Document, error) {
 	return documentWithKey{
 		Document: d,
 		key:      key,
-		pk:       info.GetPrimaryKey(),
+		pk:       t.Info.GetPrimaryKey(),
 	}, nil
 }
 
 // Delete a document by key.
 // Indexes are automatically updated.
 func (t *Table) Delete(key []byte) error {
-	info := t.Info()
-
-	if info.readOnly {
+	if t.Info.ReadOnly {
 		return errors.New("cannot write to read-only table")
 	}
 
@@ -134,9 +110,7 @@ func (t *Table) Delete(key []byte) error {
 		return err
 	}
 
-	indexes := t.Indexes()
-
-	for _, idx := range indexes {
+	for _, idx := range t.Indexes {
 		vs := make([]document.Value, 0, len(idx.Info.Paths))
 		for _, path := range idx.Info.Paths {
 			v, err := path.GetValueFromDocument(d)
@@ -164,20 +138,16 @@ func (t *Table) Delete(key []byte) error {
 // An error is returned if the key doesn't exist.
 // Indexes are automatically updated.
 func (t *Table) Replace(key []byte, d document.Document) error {
-	info := t.Info()
-
-	if info.readOnly {
+	if t.Info.ReadOnly {
 		return errors.New("cannot write to read-only table")
 	}
 
-	d, err := info.FieldConstraints.ValidateDocument(d)
+	d, err := t.Info.FieldConstraints.ValidateDocument(d)
 	if err != nil {
 		return err
 	}
 
-	indexes := t.Indexes()
-
-	return t.replace(indexes, key, d)
+	return t.replace(t.Indexes, key, d)
 }
 
 func (t *Table) replace(indexes []*Index, key []byte, d document.Document) error {
@@ -206,7 +176,7 @@ func (t *Table) replace(indexes []*Index, key []byte, d document.Document) error
 
 	// encode new document
 	var buf bytes.Buffer
-	enc := t.tx.db.Codec.NewEncoder(&buf)
+	enc := t.Tx.DB.Codec.NewEncoder(&buf)
 	defer enc.Close()
 	err = enc.EncodeDocument(d)
 	if err != nil {
@@ -344,15 +314,13 @@ func (t *Table) Iterate(fn func(d document.Document) error) error {
 // It can be used to manually add a new entry to the store or to compare
 // with other keys during table iteration.
 func (t *Table) EncodeValue(v document.Value) ([]byte, error) {
-	info := t.Info()
-
-	return t.encodeValueToKey(info, v)
+	return t.encodeValueToKey(t.Info, v)
 }
 
 func (t *Table) encodeValueToKey(info *TableInfo, v document.Value) ([]byte, error) {
 	var err error
 
-	pk := info.GetPrimaryKey()
+	pk := t.Info.GetPrimaryKey()
 	if pk == nil {
 		// if no primary key was defined, convert the pivot to an integer then to an unsigned integer
 		// and encode it as a varint
@@ -416,12 +384,10 @@ func (t *Table) DescendLessOrEqual(pivot document.Value, fn func(d document.Docu
 func (t *Table) iterate(pivot document.Value, reverse bool, fn func(d document.Document) error) error {
 	var seek []byte
 
-	info := t.Info()
-
 	// if there is a pivot, convert it to the right type
 	if !pivot.Type.IsAny() && pivot.V != nil {
 		var err error
-		seek, err = t.encodeValueToKey(info, pivot)
+		seek, err = t.encodeValueToKey(t.Info, pivot)
 		if err != nil {
 			return err
 		}
@@ -430,10 +396,10 @@ func (t *Table) iterate(pivot document.Value, reverse bool, fn func(d document.D
 	// To avoid unnecessary allocations, we create the struct once and reuse
 	// it during each iteration.
 	d := lazilyDecodedDocument{
-		codec: t.tx.db.Codec,
+		codec: t.Tx.DB.Codec,
 	}
 
-	d.pk = info.GetPrimaryKey()
+	d.pk = t.Info.GetPrimaryKey()
 
 	it := t.Store.Iterator(engine.IteratorOptions{Reverse: reverse})
 	defer it.Close()
@@ -466,12 +432,10 @@ func (t *Table) GetDocument(key []byte) (document.Document, error) {
 		return nil, stringutil.Errorf("failed to fetch document %q: %w", key, err)
 	}
 
-	info := t.Info()
-
 	var d documentWithKey
-	d.Document = t.tx.db.Codec.NewDocument(v)
+	d.Document = t.Tx.DB.Codec.NewDocument(v)
 	d.key = key
-	d.pk = info.GetPrimaryKey()
+	d.pk = t.Info.GetPrimaryKey()
 	return &d, err
 }
 
@@ -482,7 +446,7 @@ 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(info *TableInfo, fb *document.FieldBuffer) ([]byte, error) {
-	if pk := info.GetPrimaryKey(); pk != nil {
+	if pk := t.Info.GetPrimaryKey(); pk != nil {
 
 		v, err := pk.Path.GetValueFromDocument(fb)
 		if err == document.ErrFieldNotFound {
diff --git a/database/transaction.go b/database/transaction.go
index 21d07d88..297a4d2d 100644
--- a/database/transaction.go
+++ b/database/transaction.go
@@ -2,13 +2,6 @@ package database
 
 import (
 	"github.com/genjidb/genji/engine"
-	"github.com/genjidb/genji/stringutil"
-)
-
-var (
-	internalPrefix     = "__genji_"
-	tableInfoStoreName = internalPrefix + "tables"
-	indexStoreName     = internalPrefix + "indexes"
 )
 
 // Transaction represents a database transaction. It provides methods for managing the
@@ -16,48 +9,43 @@ 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
+	DB       *Database
+	Tx       engine.Transaction
+	Catalog  *Catalog
+	Writable bool
 	// if set to true, this transaction is attached to the database
 	attached bool
 
-	// these functions are run after a successful rollback or commit.
-	onRollbackHooks []func()
-	onCommitHooks   []func()
-}
-
-// DB returns the underlying database that created the transaction.
-func (tx *Transaction) DB() *Database {
-	return tx.db
+	// these functions are run after a successful rollback.
+	OnRollbackHooks []func()
 }
 
 // Rollback the transaction. Can be used safely after commit.
 func (tx *Transaction) Rollback() error {
-	err := tx.tx.Rollback()
+	err := tx.Tx.Rollback()
 	if err != nil {
 		return err
 	}
 
 	defer func() {
-		if tx.writable {
-			tx.db.txmu.Unlock()
+		if tx.Writable {
+			tx.DB.txmu.Unlock()
 		} else {
-			tx.db.txmu.RUnlock()
+			tx.DB.txmu.RUnlock()
 		}
 	}()
 
 	if tx.attached {
-		tx.db.attachedTxMu.Lock()
-		defer tx.db.attachedTxMu.Unlock()
+		tx.DB.attachedTxMu.Lock()
+		defer tx.DB.attachedTxMu.Unlock()
 
-		if tx.db.attachedTransaction != nil {
-			tx.db.attachedTransaction = nil
+		if tx.DB.attachedTransaction != nil {
+			tx.DB.attachedTransaction = nil
 		}
 	}
 
-	for i := len(tx.onRollbackHooks) - 1; i >= 0; i-- {
-		tx.onRollbackHooks[i]()
+	for i := len(tx.OnRollbackHooks) - 1; i >= 0; i-- {
+		tx.OnRollbackHooks[i]()
 	}
 
 	return nil
@@ -66,124 +54,27 @@ func (tx *Transaction) Rollback() error {
 // Commit the transaction. Calling this method on read-only transactions
 // will return an error.
 func (tx *Transaction) Commit() error {
-	err := tx.tx.Commit()
+	err := tx.Tx.Commit()
 	if err != nil {
 		return err
 	}
 
 	defer func() {
-		if tx.writable {
-			tx.db.txmu.Unlock()
+		if tx.Writable {
+			tx.DB.txmu.Unlock()
 		} else {
-			tx.db.txmu.RUnlock()
+			tx.DB.txmu.RUnlock()
 		}
 	}()
 
 	if tx.attached {
-		tx.db.attachedTxMu.Lock()
-		defer tx.db.attachedTxMu.Unlock()
+		tx.DB.attachedTxMu.Lock()
+		defer tx.DB.attachedTxMu.Unlock()
 
-		if tx.db.attachedTransaction != nil {
-			tx.db.attachedTransaction = nil
+		if tx.DB.attachedTransaction != nil {
+			tx.DB.attachedTransaction = nil
 		}
 	}
 
-	for i := len(tx.onCommitHooks) - 1; i >= 0; i-- {
-		tx.onCommitHooks[i]()
-	}
-
 	return nil
-
-}
-
-// Writable indicates if the transaction is writable or not.
-func (tx *Transaction) Writable() bool {
-	return tx.writable
-}
-
-// CreateTable creates a table with the given name.
-// If it already exists, returns ErrTableAlreadyExists.
-func (tx *Transaction) CreateTable(name string, info *TableInfo) error {
-	return tx.db.catalog.CreateTable(tx, name, info)
-}
-
-// 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) {
-	return tx.db.catalog.GetTable(tx, name)
-}
-
-// AddFieldConstraint adds a field constraint to a table.
-func (tx *Transaction) AddFieldConstraint(tableName string, fc FieldConstraint) error {
-	return tx.db.catalog.AddFieldConstraint(tx, tableName, fc)
-}
-
-// RenameTable renames a table.
-// If it doesn't exist, it returns ErrTableNotFound.
-func (tx *Transaction) RenameTable(oldName, newName string) error {
-	return tx.db.catalog.RenameTable(tx, oldName, newName)
-}
-
-// DropTable deletes a table from the database.
-func (tx *Transaction) DropTable(name string) error {
-	return tx.db.catalog.DropTable(tx, name)
-}
-
-// CreateIndex creates an index with the given name.
-// If it already exists, returns ErrIndexAlreadyExists.
-func (tx *Transaction) CreateIndex(opts *IndexInfo) error {
-	err := tx.db.catalog.CreateIndex(tx, opts)
-	if err != nil {
-		return err
-	}
-
-	return tx.ReIndex(opts.IndexName)
-}
-
-// GetIndex returns an index by name.
-func (tx *Transaction) GetIndex(name string) (*Index, error) {
-	return tx.db.catalog.GetIndex(tx, name)
-}
-
-// DropIndex deletes an index from the database.
-func (tx *Transaction) DropIndex(name string) error {
-	return tx.db.catalog.DropIndex(tx, name)
-}
-
-// ListIndexes lists all indexes.
-func (tx *Transaction) ListIndexes() []string {
-	return tx.db.catalog.ListIndexes("")
-}
-
-// ReIndex truncates and recreates selected index from scratch.
-func (tx *Transaction) ReIndex(indexName string) error {
-	return tx.db.catalog.ReIndex(tx, indexName)
-}
-
-// ReIndexAll truncates and recreates all indexes of the database from scratch.
-func (tx *Transaction) ReIndexAll() error {
-	return tx.db.catalog.ReIndexAll(tx)
-}
-
-func (tx *Transaction) getTableStore() *tableStore {
-	st, err := tx.tx.GetStore([]byte(tableInfoStoreName))
-	if err != nil {
-		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", tableInfoStoreName, err))
-	}
-
-	return &tableStore{
-		st: st,
-		db: tx.db,
-	}
-}
-
-func (tx *Transaction) getIndexStore() *indexStore {
-	st, err := tx.tx.GetStore([]byte(indexStoreName))
-	if err != nil {
-		panic(stringutil.Sprintf("database incorrectly setup: missing %q table: %v", indexStoreName, err))
-	}
-
-	return &indexStore{
-		st: st,
-		db: tx.db,
-	}
 }
diff --git a/db.go b/db.go
index e9a7cded..01adbe38 100644
--- a/db.go
+++ b/db.go
@@ -12,8 +12,7 @@ import (
 
 // DB represents a collection of tables stored in the underlying engine.
 type DB struct {
-	DB *database.Database
-
+	DB  *database.Database
 	ctx context.Context
 }
 
@@ -72,64 +71,49 @@ func (db *DB) Update(fn func(tx *Tx) error) error {
 	return tx.Commit()
 }
 
-// Exec a query against the database without returning the result.
-func (db *DB) Exec(q string, args ...interface{}) error {
-	res, err := db.Query(q, args...)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		er := res.Close()
-		if err == nil {
-			err = er
-		}
-	}()
-
-	return res.Iterate(func(d document.Document) error {
-		return nil
-	})
-}
-
 // Query the database and return the result.
 // The returned result must always be closed after usage.
 func (db *DB) Query(q string, args ...interface{}) (*query.Result, error) {
-	pq, err := parser.ParseQuery(q)
+	stmt, err := db.Prepare(q)
 	if err != nil {
 		return nil, err
 	}
 
-	return pq.Run(db.ctx, db.DB, argsToParams(args))
+	return stmt.Query(args...)
 }
 
 // QueryDocument runs the query and returns the first document.
 // If the query returns no error, QueryDocument returns database.ErrDocumentNotFound.
 func (db *DB) QueryDocument(q string, args ...interface{}) (document.Document, error) {
-	res, err := db.Query(q, args...)
+	stmt, err := db.Prepare(q)
 	if err != nil {
 		return nil, err
 	}
-	defer res.Close()
 
-	return scanDocument(res)
+	return stmt.QueryDocument(args...)
 }
 
-func scanDocument(res *query.Result) (document.Document, error) {
-	var d document.Document
-	err := res.Iterate(func(doc document.Document) error {
-		d = doc
-		return stream.ErrStreamClosed
-	})
+// Exec a query against the database without returning the result.
+func (db *DB) Exec(q string, args ...interface{}) error {
+	stmt, err := db.Prepare(q)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	if d == nil {
-		return nil, database.ErrDocumentNotFound
+	return stmt.Exec(args...)
+}
+
+// Prepare parses the query and returns a prepared statement.
+func (db *DB) Prepare(q string) (*Statement, error) {
+	pq, err := parser.ParseQuery(q)
+	if err != nil {
+		return nil, err
 	}
 
-	fb := document.NewFieldBuffer()
-	err = fb.Copy(d)
-	return fb, err
+	return &Statement{
+		pq: pq,
+		db: db,
+	}, nil
 }
 
 // Tx represents a database transaction. It provides methods for managing the
@@ -143,29 +127,107 @@ type Tx struct {
 // Query the database withing the transaction and returns the result.
 // Closing the returned result after usage is not mandatory.
 func (tx *Tx) Query(q string, args ...interface{}) (*query.Result, error) {
-	pq, err := parser.ParseQuery(q)
+	stmt, err := tx.Prepare(q)
 	if err != nil {
 		return nil, err
 	}
 
-	return pq.Exec(tx.Transaction, argsToParams(args))
+	return stmt.Query(args...)
 }
 
 // QueryDocument runs the query and returns the first document.
 // If the query returns no error, QueryDocument returns database.ErrDocumentNotFound.
 func (tx *Tx) QueryDocument(q string, args ...interface{}) (document.Document, error) {
-	res, err := tx.Query(q, args...)
+	stmt, err := tx.Prepare(q)
 	if err != nil {
 		return nil, err
 	}
-	defer res.Close()
 
-	return scanDocument(res)
+	return stmt.QueryDocument(args...)
 }
 
 // Exec a query against the database within tx and without returning the result.
 func (tx *Tx) Exec(q string, args ...interface{}) (err error) {
-	res, err := tx.Query(q, args...)
+	stmt, err := tx.Prepare(q)
+	if err != nil {
+		return err
+	}
+
+	return stmt.Exec(args...)
+}
+
+// Prepare parses the query and returns a prepared statement.
+func (tx *Tx) Prepare(q string) (*Statement, error) {
+	pq, err := parser.ParseQuery(q)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Statement{
+		pq: pq,
+		tx: tx,
+	}, nil
+}
+
+// Statement is a prepared statement. If Statement has been created on a Tx,
+// it will only be valid until Tx closes. If it has been created on a DB, it
+// is valid until the DB closes.
+// It's safe for concurrent use by multiple goroutines.
+type Statement struct {
+	pq query.Query
+	db *DB
+	tx *Tx
+}
+
+// Query the database and return the result.
+// The returned result must always be closed after usage.
+func (s *Statement) Query(args ...interface{}) (*query.Result, error) {
+	if s.tx != nil {
+		return s.pq.Exec(s.tx.Transaction, argsToParams(args))
+	}
+
+	return s.pq.Run(s.db.ctx, s.db.DB, argsToParams(args))
+}
+
+// QueryDocument runs the query and returns the first document.
+// If the query returns no error, QueryDocument returns database.ErrDocumentNotFound.
+func (s *Statement) QueryDocument(args ...interface{}) (d document.Document, err error) {
+	res, err := s.Query(args...)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		er := res.Close()
+		if err == nil {
+			err = er
+		}
+	}()
+
+	return scanDocument(res)
+}
+
+func scanDocument(res *query.Result) (document.Document, error) {
+	var d document.Document
+	err := res.Iterate(func(doc document.Document) error {
+		d = doc
+		return stream.ErrStreamClosed
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	if d == nil {
+		return nil, database.ErrDocumentNotFound
+	}
+
+	fb := document.NewFieldBuffer()
+	err = fb.Copy(d)
+	return fb, err
+}
+
+// Exec a query against the database without returning the result.
+func (s *Statement) Exec(args ...interface{}) (err error) {
+	res, err := s.Query(args...)
 	if err != nil {
 		return err
 	}
diff --git a/engine/badgerengine/go.sum b/engine/badgerengine/go.sum
index 53644204..1f20516e 100644
--- a/engine/badgerengine/go.sum
+++ b/engine/badgerengine/go.sum
@@ -38,8 +38,8 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 github.com/google/flatbuffers v1.12.0 h1:/PtAHvnBY4Kqnx/xCQ3OIV9uYcSFGScBsWI3Oogeh6w=
 github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -88,12 +88,13 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/vmihailenco/msgpack/v5 v5.1.4 h1:6K44/cU6dMNGkVTGGuu7ef2NdSRFMhAFGGLfE3cqtHM=
-github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
-github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
-github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/vmihailenco/msgpack/v5 v5.3.2 h1:MsXyN2rqdM8NM0lLiIpTn610e8Zcoj8ZuHxsMOi9qhI=
+github.com/vmihailenco/msgpack/v5 v5.3.2/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
@@ -110,20 +111,23 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -133,8 +137,11 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -144,10 +151,11 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/go.mod b/go.mod
index 96913b9b..8b2dcb52 100644
--- a/go.mod
+++ b/go.mod
@@ -4,10 +4,10 @@ go 1.16
 
 require (
 	github.com/buger/jsonparser v1.1.1
-	github.com/google/btree v1.0.0
+	github.com/google/btree v1.0.1
 	github.com/google/go-cmp v0.5.5
 	github.com/stretchr/testify v1.7.0
-	github.com/vmihailenco/msgpack/v5 v5.1.4
+	github.com/vmihailenco/msgpack/v5 v5.3.2
 	go.etcd.io/bbolt v1.3.5
-	golang.org/x/tools v0.1.0
+	golang.org/x/tools v0.1.1
 )
diff --git a/go.sum b/go.sum
index 503cf490..cd1f7301 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -12,35 +12,36 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/vmihailenco/msgpack/v5 v5.1.4 h1:6K44/cU6dMNGkVTGGuu7ef2NdSRFMhAFGGLfE3cqtHM=
-github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI=
-github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
-github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/vmihailenco/msgpack/v5 v5.3.2 h1:MsXyN2rqdM8NM0lLiIpTn610e8Zcoj8ZuHxsMOi9qhI=
+github.com/vmihailenco/msgpack/v5 v5.3.2/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/new.go b/new.go
index a80be632..1fce1366 100644
--- a/new.go
+++ b/new.go
@@ -12,13 +12,5 @@ import (
 
 // New initializes the DB using the given engine.
 func New(ctx context.Context, ng engine.Engine) (*DB, error) {
-	db, err := database.New(ctx, ng, database.Options{Codec: msgpack.NewCodec()})
-	if err != nil {
-		return nil, err
-	}
-
-	return &DB{
-		DB:  db,
-		ctx: context.Background(),
-	}, nil
+	return newDatabase(ctx, ng, database.Options{Codec: msgpack.NewCodec()})
 }
diff --git a/new_wasm.go b/new_wasm.go
index 44c05aff..9ca65365 100644
--- a/new_wasm.go
+++ b/new_wasm.go
@@ -12,13 +12,5 @@ import (
 
 // New initializes the DB using the given engine.
 func New(ctx context.Context, ng engine.Engine) (*DB, error) {
-	db, err := database.New(ctx, ng, database.Options{Codec: custom.NewCodec()})
-	if err != nil {
-		return nil, err
-	}
-
-	return &DB{
-		DB:  db,
-		ctx: context.Background(),
-	}, nil
+	return newDatabase(ctx, ng, database.Options{Codec: custom.NewCodec()})
 }
diff --git a/open.go b/open.go
index 055766b9..2e98b35d 100644
--- a/open.go
+++ b/open.go
@@ -4,10 +4,15 @@ package genji
 
 import (
 	"context"
+	"strings"
 
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/engine/boltengine"
 	"github.com/genjidb/genji/engine/memoryengine"
+	"github.com/genjidb/genji/query"
+	"github.com/genjidb/genji/sql/parser"
 )
 
 // Open creates a Genji database at the given path.
@@ -30,3 +35,96 @@ func Open(path string) (*DB, error) {
 	ctx := context.Background()
 	return New(ctx, ng)
 }
+
+func newDatabase(ctx context.Context, ng engine.Engine, opts database.Options) (*DB, error) {
+	db, err := database.New(ctx, ng, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	tx, err := db.Begin(true)
+	if err != nil {
+		return nil, err
+	}
+	defer tx.Rollback()
+
+	err = loadCatalog(tx)
+	if err != nil {
+		return nil, err
+	}
+
+	return &DB{
+		DB:  db,
+		ctx: context.Background(),
+	}, nil
+}
+
+func loadCatalog(tx *database.Transaction) error {
+	tables, err := loadCatalogTables(tx)
+	if err != nil {
+		return err
+	}
+
+	indexes, err := loadCatalogIndexes(tx)
+	if err != nil {
+		return err
+	}
+
+	tx.Catalog.Load(tables, indexes)
+	return nil
+}
+
+func loadCatalogTables(tx *database.Transaction) ([]database.TableInfo, error) {
+	tb := database.GetTableStore(tx)
+
+	var tables []database.TableInfo
+	err := tb.AscendGreaterOrEqual(document.Value{}, func(d document.Document) error {
+		s, err := d.GetByField("sql")
+		if err != nil {
+			return err
+		}
+
+		stmt, err := parser.NewParser(strings.NewReader(s.V.(string))).ParseStatement()
+		if err != nil {
+			return err
+		}
+
+		ti := stmt.(query.CreateTableStmt).Info
+
+		v, err := d.GetByField("store_name")
+		if err != nil {
+			return err
+		}
+		ti.StoreName = v.V.([]byte)
+
+		tables = append(tables, ti)
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return tables, nil
+}
+
+func loadCatalogIndexes(tx *database.Transaction) ([]database.IndexInfo, error) {
+	tb := database.GetIndexStore(tx)
+
+	var indexes []database.IndexInfo
+	err := tb.AscendGreaterOrEqual(document.Value{}, func(d document.Document) error {
+		s, err := d.GetByField("sql")
+		if err != nil {
+			return err
+		}
+
+		stmt, err := parser.NewParser(strings.NewReader(s.V.(string))).ParseStatement()
+		if err != nil {
+			return err
+		}
+
+		indexes = append(indexes, stmt.(query.CreateIndexStmt).Info)
+		return nil
+	})
+
+	return indexes, err
+}
diff --git a/planner/optimizer.go b/planner/optimizer.go
index cd11daea..6f951d4d 100644
--- a/planner/optimizer.go
+++ b/planner/optimizer.go
@@ -326,12 +326,10 @@ func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transactio
 		return s, nil
 	}
 
-	t, err := tx.GetTable(st.TableName)
+	t, err := tx.Catalog.GetTable(tx, st.TableName)
 	if err != nil {
 		return nil, err
 	}
-	info := t.Info()
-	indexes := t.Indexes()
 
 	// this optimization applies to project operators that immediately follow distinct
 	for n != nil {
@@ -342,7 +340,7 @@ func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transactio
 				if ok {
 
 					// if the projection is unique, we remove the node from the tree
-					if isProjectionUnique(indexes, pn, info.GetPrimaryKey()) {
+					if isProjectionUnique(t.Indexes, pn, t.Info.GetPrimaryKey()) {
 						s.Remove(n)
 						n = prev
 						continue
@@ -414,14 +412,11 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, p
 	if !ok {
 		return s, nil
 	}
-	t, err := tx.GetTable(st.TableName)
+	t, err := tx.Catalog.GetTable(tx, st.TableName)
 	if err != nil {
 		return nil, err
 	}
 
-	info := t.Info()
-	indexes := t.Indexes()
-
 	var candidates []*candidate
 	var filterNodes []filterNode
 
@@ -458,9 +453,9 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, p
 			filterNodes = append(filterNodes, filterNode{path: path, v: v, f: f})
 
 			// check for primary keys scan while iterating on the filter nodes
-			if pk := info.GetPrimaryKey(); pk != nil && pk.Path.IsEqual(path) {
+			if pk := t.Info.GetPrimaryKey(); pk != nil && pk.Path.IsEqual(path) {
 				// if both types are different, don't select this scanner
-				v, ok, err := operandCanUseIndex(pk.Type, pk.Path, info.FieldConstraints, v)
+				v, ok, err := operandCanUseIndex(pk.Type, pk.Path, t.Info.FieldConstraints, v)
 				if err != nil {
 					return nil, err
 				}
@@ -510,7 +505,7 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, p
 	// iterate on all indexes for that table, checking for each of them if its paths are matching
 	// the filter nodes of the given query. The resulting nodes are ordered like the index paths.
 outer:
-	for _, idx := range indexes {
+	for _, idx := range t.Indexes {
 		// order filter nodes by how the index paths order them; if absent, nil in still inserted
 		found := make([]*filterNode, len(idx.Info.Paths))
 		for i, path := range idx.Info.Paths {
@@ -565,7 +560,7 @@ outer:
 				// what the index says this node type must be
 				typ := idx.Info.Types[i]
 
-				fno.v, ok, err = operandCanUseIndex(typ, fno.path, info.FieldConstraints, fno.v)
+				fno.v, ok, err = operandCanUseIndex(typ, fno.path, t.Info.FieldConstraints, fno.v)
 				if err != nil {
 					return nil, err
 				}
diff --git a/query/alter.go b/query/alter.go
index 20d84445..94892b9e 100644
--- a/query/alter.go
+++ b/query/alter.go
@@ -35,7 +35,7 @@ func (stmt AlterStmt) Run(tx *database.Transaction, _ []expr.Param) (Result, err
 		return res, database.ErrTableAlreadyExists
 	}
 
-	err := tx.RenameTable(stmt.TableName, stmt.NewTableName)
+	err := tx.Catalog.RenameTable(tx, stmt.TableName, stmt.NewTableName)
 	return res, err
 }
 
@@ -62,6 +62,6 @@ func (stmt AlterTableAddField) Run(tx *database.Transaction, _ []expr.Param) (Re
 		return res, errors.New("missing field name")
 	}
 
-	err := tx.AddFieldConstraint(stmt.TableName, stmt.Constraint)
+	err := tx.Catalog.AddFieldConstraint(tx, stmt.TableName, stmt.Constraint)
 	return res, err
 }
diff --git a/query/create.go b/query/create.go
index 812e610d..15b2c8bc 100644
--- a/query/create.go
+++ b/query/create.go
@@ -8,7 +8,6 @@ import (
 
 // CreateTableStmt is a DSL that allows creating a full CREATE TABLE statement.
 type CreateTableStmt struct {
-	TableName   string
 	IfNotExists bool
 	Info        database.TableInfo
 }
@@ -23,15 +22,15 @@ func (stmt CreateTableStmt) IsReadOnly() bool {
 func (stmt CreateTableStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
 	var res Result
 
-	err := tx.CreateTable(stmt.TableName, &stmt.Info)
+	err := tx.Catalog.CreateTable(tx, stmt.Info.TableName, &stmt.Info)
 	if stmt.IfNotExists && err == database.ErrTableAlreadyExists {
-		err = nil
+		return res, nil
 	}
 
 	for _, fc := range stmt.Info.FieldConstraints {
 		if fc.IsUnique {
-			err = tx.CreateIndex(&database.IndexInfo{
-				TableName: stmt.TableName,
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				TableName: stmt.Info.TableName,
 				Paths:     []document.Path{fc.Path},
 				Unique:    true,
 				Types:     []document.ValueType{fc.Type},
@@ -48,11 +47,8 @@ func (stmt CreateTableStmt) Run(tx *database.Transaction, args []expr.Param) (Re
 // CreateIndexStmt is a DSL that allows creating a full CREATE INDEX statement.
 // It is typically created using the CreateIndex function.
 type CreateIndexStmt struct {
-	IndexName   string
-	TableName   string
-	Paths       []document.Path
 	IfNotExists bool
-	Unique      bool
+	Info        database.IndexInfo
 }
 
 // IsReadOnly always returns false. It implements the Statement interface.
@@ -65,15 +61,11 @@ func (stmt CreateIndexStmt) IsReadOnly() bool {
 func (stmt CreateIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
 	var res Result
 
-	err := tx.CreateIndex(&database.IndexInfo{
-		Unique:    stmt.Unique,
-		IndexName: stmt.IndexName,
-		TableName: stmt.TableName,
-		Paths:     stmt.Paths,
-	})
+	err := tx.Catalog.CreateIndex(tx, &stmt.Info)
 	if stmt.IfNotExists && err == database.ErrIndexAlreadyExists {
-		err = nil
+		return res, nil
 	}
 
+	err = tx.Catalog.ReIndex(tx, stmt.Info.IndexName)
 	return res, err
 }
diff --git a/query/drop.go b/query/drop.go
index b3aedd56..db7865a3 100644
--- a/query/drop.go
+++ b/query/drop.go
@@ -27,7 +27,7 @@ func (stmt DropTableStmt) Run(tx *database.Transaction, args []expr.Param) (Resu
 		return res, errors.New("missing table name")
 	}
 
-	err := tx.DropTable(stmt.TableName)
+	err := tx.Catalog.DropTable(tx, stmt.TableName)
 	if errors.Is(err, database.ErrTableNotFound) && stmt.IfExists {
 		err = nil
 	}
@@ -55,7 +55,7 @@ func (stmt DropIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Resu
 		return res, errors.New("missing index name")
 	}
 
-	err := tx.DropIndex(stmt.IndexName)
+	err := tx.Catalog.DropIndex(tx, stmt.IndexName)
 	if err == database.ErrIndexNotFound && stmt.IfExists {
 		err = nil
 	}
diff --git a/planner/explain.go b/query/explain.go
similarity index 75%
rename from planner/explain.go
rename to query/explain.go
index 64fbbdf7..be800611 100644
--- a/planner/explain.go
+++ b/query/explain.go
@@ -1,4 +1,4 @@
-package planner
+package query
 
 import (
 	"errors"
@@ -6,27 +6,27 @@ import (
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/query"
+	"github.com/genjidb/genji/planner"
 	"github.com/genjidb/genji/stream"
 )
 
-// ExplainStmt is a query.Statement that
+// ExplainStmt is a Statement that
 // displays information about how a statement
 // is going to be executed, without executing it.
 type ExplainStmt struct {
-	Statement query.Statement
+	Statement Statement
 }
 
 // Run analyses the inner statement and displays its execution plan.
 // If the statement is a stream, Optimize will be called prior to
 // displaying all the operations.
 // Explain currently only works on SELECT, UPDATE, INSERT and DELETE statements.
-func (s *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (query.Result, error) {
+func (s *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
 	switch t := s.Statement.(type) {
-	case *Statement:
-		s, err := Optimize(t.Stream, tx, params)
+	case *StreamStmt:
+		s, err := planner.Optimize(t.Stream, tx, params)
 		if err != nil {
-			return query.Result{}, err
+			return Result{}, err
 		}
 
 		var plan string
@@ -36,7 +36,7 @@ func (s *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (query.
 			plan = "<no exec>"
 		}
 
-		newStatement := Statement{
+		newStatement := StreamStmt{
 			Stream: &stream.Stream{
 				Op: stream.Project(
 					&expr.NamedExpr{
@@ -49,7 +49,7 @@ func (s *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (query.
 		return newStatement.Run(tx, params)
 	}
 
-	return query.Result{}, errors.New("EXPLAIN only works on INSERT, SELECT, UPDATE AND DELETE statements")
+	return Result{}, errors.New("EXPLAIN only works on INSERT, SELECT, UPDATE AND DELETE statements")
 }
 
 // IsReadOnly indicates that this statement doesn't write anything into
diff --git a/query/query.go b/query/query.go
index 8d28f349..d93ba0f7 100644
--- a/query/query.go
+++ b/query/query.go
@@ -7,6 +7,8 @@ import (
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/expr"
+	"github.com/genjidb/genji/stream"
+	"github.com/genjidb/genji/stringutil"
 )
 
 // ErrResultClosed is returned when trying to close an already closed result.
@@ -42,6 +44,9 @@ func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param
 		default:
 		}
 
+		// reinitialize the result
+		res = Result{}
+
 		if qa, ok := stmt.(queryAlterer); ok {
 			err = qa.alterQuery(ctx, db, &q)
 			if err != nil {
@@ -85,7 +90,7 @@ func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param
 		// it there is an opened transaction but there are still statements
 		// to be executed, close the current transaction.
 		if q.tx != nil && q.autoCommit && i+1 < len(q.Statements) {
-			if q.tx.Writable() {
+			if q.tx.Writable {
 				err := q.tx.Commit()
 				if err != nil {
 					return nil, err
@@ -106,6 +111,13 @@ func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param
 		res.Tx = q.tx
 	}
 
+	// if autoCommit is false and q.tx is nil it means
+	// the query contains a BEGIN statement but no ROLLBACK or COMMIT.
+	// Ensure the stat
+	if !q.autoCommit && q.tx == nil {
+
+	}
+
 	return &res, nil
 }
 
@@ -160,6 +172,37 @@ func (r *Result) Iterate(fn func(d document.Document) error) error {
 	return r.Iterator.Iterate(fn)
 }
 
+func (r *Result) Fields() []string {
+	if r.Iterator == nil {
+		return nil
+	}
+
+	stmt, ok := r.Iterator.(*streamStmtIterator)
+	if !ok || stmt.Stream.Op == nil {
+		return nil
+	}
+
+	// Search for the ProjectOperator. If found, extract the projected expression list
+	for op := stmt.Stream.First(); op != nil; op = op.GetNext() {
+		if po, ok := op.(*stream.ProjectOperator); ok {
+			// if there are no projected expression, it's a wildcard
+			if len(po.Exprs) == 0 {
+				break
+			}
+
+			fields := make([]string, len(po.Exprs))
+			for i := range po.Exprs {
+				fields[i] = stringutil.Sprintf("%s", po.Exprs[i])
+			}
+
+			return fields
+		}
+	}
+
+	// the stream will output documents in a single field
+	return []string{"*"}
+}
+
 // Close the result stream.
 // After closing the result, Stream is not supposed to be used.
 // If the result stream was already closed, it returns
@@ -176,7 +219,7 @@ func (r *Result) Close() (err error) {
 	r.closed = true
 
 	if r.Tx != nil {
-		if r.Tx.Writable() {
+		if r.Tx.Writable {
 			err = r.Tx.Commit()
 		} else {
 			err = r.Tx.Rollback()
diff --git a/query/reindex.go b/query/reindex.go
index e687d238..e59f1bf6 100644
--- a/query/reindex.go
+++ b/query/reindex.go
@@ -23,14 +23,13 @@ func (stmt ReIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result
 	var res Result
 
 	if stmt.TableOrIndexName == "" {
-		return res, tx.ReIndexAll()
+		return res, tx.Catalog.ReIndexAll(tx)
 	}
 
-	catalog := tx.DB().Catalog()
-	_, err := tx.GetTable(stmt.TableOrIndexName)
+	_, err := tx.Catalog.GetTable(tx, stmt.TableOrIndexName)
 	if err == nil {
-		for _, idxName := range catalog.ListIndexes(stmt.TableOrIndexName) {
-			err = catalog.ReIndex(tx, idxName)
+		for _, idxName := range tx.Catalog.ListIndexes(stmt.TableOrIndexName) {
+			err = tx.Catalog.ReIndex(tx, idxName)
 			if err != nil {
 				return res, err
 			}
@@ -42,6 +41,6 @@ func (stmt ReIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result
 		return res, err
 	}
 
-	err = tx.ReIndex(stmt.TableOrIndexName)
+	err = tx.Catalog.ReIndex(tx, stmt.TableOrIndexName)
 	return res, err
 }
diff --git a/planner/statement.go b/query/stream.go
similarity index 65%
rename from planner/statement.go
rename to query/stream.go
index 2cd3ed9f..eb3bb13d 100644
--- a/planner/statement.go
+++ b/query/stream.go
@@ -1,29 +1,29 @@
-package planner
+package query
 
 import (
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/query"
+	"github.com/genjidb/genji/planner"
 	"github.com/genjidb/genji/stream"
 )
 
-// Statement is a query.Statement using a Stream.
-type Statement struct {
+// StreamStmt is a StreamStmt using a Stream.
+type StreamStmt struct {
 	Stream   *stream.Stream
 	ReadOnly bool
 }
 
 // Run returns a result containing the stream. The stream will be executed by calling the Iterate method of
 // the result.
-func (s *Statement) Run(tx *database.Transaction, params []expr.Param) (query.Result, error) {
-	st, err := Optimize(s.Stream, tx, params)
+func (s *StreamStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
+	st, err := planner.Optimize(s.Stream.Clone(), tx, params)
 	if err != nil || st == nil {
-		return query.Result{}, err
+		return Result{}, err
 	}
 
-	return query.Result{
-		Iterator: &statementIterator{
+	return Result{
+		Iterator: &streamStmtIterator{
 			Stream: st,
 			Tx:     tx,
 			Params: params,
@@ -32,21 +32,21 @@ func (s *Statement) Run(tx *database.Transaction, params []expr.Param) (query.Re
 }
 
 // IsReadOnly reports whether the stream will modify the database or only read it.
-func (s *Statement) IsReadOnly() bool {
+func (s *StreamStmt) IsReadOnly() bool {
 	return s.ReadOnly
 }
 
-func (s *Statement) String() string {
+func (s *StreamStmt) String() string {
 	return s.Stream.String()
 }
 
-type statementIterator struct {
+type streamStmtIterator struct {
 	Stream *stream.Stream
 	Tx     *database.Transaction
 	Params []expr.Param
 }
 
-func (s *statementIterator) Iterate(fn func(d document.Document) error) error {
+func (s *streamStmtIterator) Iterate(fn func(d document.Document) error) error {
 	env := expr.Environment{
 		Tx:     s.Tx,
 		Params: s.Params,
diff --git a/query/transaction.go b/query/transaction.go
index e51aa0f7..d37a4e70 100644
--- a/query/transaction.go
+++ b/query/transaction.go
@@ -48,6 +48,7 @@ func (stmt RollbackStmt) alterQuery(ctx context.Context, db *database.Database,
 		return err
 	}
 	q.tx = nil
+	q.autoCommit = true
 	return nil
 }
 
@@ -72,6 +73,7 @@ func (stmt CommitStmt) alterQuery(ctx context.Context, db *database.Database, q
 		return err
 	}
 	q.tx = nil
+	q.autoCommit = true
 	return nil
 }
 
diff --git a/sql/driver/driver.go b/sql/driver/driver.go
index 27c8cfb6..a1d72086 100644
--- a/sql/driver/driver.go
+++ b/sql/driver/driver.go
@@ -12,11 +12,7 @@ import (
 	"github.com/genjidb/genji"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
 	"github.com/genjidb/genji/query"
-	"github.com/genjidb/genji/sql/parser"
-	"github.com/genjidb/genji/stream"
-	"github.com/genjidb/genji/stringutil"
 )
 
 func init() {
@@ -94,15 +90,20 @@ func (c *conn) Prepare(q string) (driver.Stmt, error) {
 
 // PrepareContext returns a prepared statement, bound to this connection.
 func (c *conn) PrepareContext(ctx context.Context, q string) (driver.Stmt, error) {
-	pq, err := parser.ParseQuery(q)
+	var s *genji.Statement
+	var err error
+
+	if c.tx != nil {
+		s, err = c.tx.Prepare(q)
+	} else {
+		s, err = c.db.Prepare(q)
+	}
 	if err != nil {
 		return nil, err
 	}
 
 	return stmt{
-		db: c.db,
-		tx: c.tx,
-		q:  pq,
+		stmt: s,
 	}, nil
 }
 
@@ -153,9 +154,7 @@ func (c *conn) Rollback() error {
 // Stmt is a prepared statement. It is bound to a Conn and not
 // used by multiple goroutines concurrently.
 type stmt struct {
-	db *genji.DB
-	tx *genji.Tx
-	q  query.Query
+	stmt *genji.Statement
 }
 
 // NumInput returns the number of placeholder parameters.
@@ -198,34 +197,10 @@ func (s stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver
 	default:
 	}
 
-	var res *query.Result
-	var err error
-
-	// if calling ExecContext within a transaction, use it,
-	// otherwise use DB.
-	if s.tx != nil {
-		res, err = s.q.Exec(s.tx.Transaction, driverNamedValueToParams(args))
-	} else {
-		res, err = s.q.Run(ctx, s.db.DB, driverNamedValueToParams(args))
-	}
-
-	if err != nil {
-		return nil, err
-	}
-
-	err = res.Iterate(func(d document.Document) error { return nil })
-	if err != nil {
-		return nil, err
-	}
-
-	// s.q.Run might return a stream if the last Statement is a Select,
-	// make sure the result is closed before returning so any transaction
-	// created by s.q.Run is closed.
-	return result{}, res.Close()
+	return result{}, s.stmt.Exec(driverNamedValueToParams(args)...)
 }
 
-type result struct {
-}
+type result struct{}
 
 // LastInsertId is not supported and returns an error.
 func (r result) LastInsertId() (int64, error) {
@@ -250,62 +225,23 @@ func (s stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (drive
 	default:
 	}
 
-	var res *query.Result
-	var err error
-
-	// if calling QueryContext within a transaction, use it,
-	// otherwise use DB.
-	if s.tx != nil {
-		res, err = s.q.Exec(s.tx.Transaction, driverNamedValueToParams(args))
-	} else {
-		res, err = s.q.Run(ctx, s.db.DB, driverNamedValueToParams(args))
-	}
-
+	res, err := s.stmt.Query(driverNamedValueToParams(args)...)
 	if err != nil {
 		return nil, err
 	}
 
 	rs := newRecordStream(res)
-	if len(s.q.Statements) == 0 {
-		return rs, nil
-	}
-
-	lastStmt := s.q.Statements[len(s.q.Statements)-1]
-
-	stmt, ok := lastStmt.(*planner.Statement)
-	if !ok || stmt.Stream.Op == nil {
-		return rs, nil
-	}
-
-	// Search the ProjectOperator.
-	for op := stmt.Stream.First(); op != nil; op = op.GetNext() {
-		if po, ok := op.(*stream.ProjectOperator); ok {
-			if len(po.Exprs) == 0 {
-				break
-			}
-
-			rs.fields = make([]string, len(po.Exprs))
-			for i := range po.Exprs {
-				rs.fields[i] = stringutil.Sprintf("%s", po.Exprs[i])
-			}
-
-			return rs, nil
-		}
-	}
-
-	// if fields is empty, the stream will output documents in a single field
-	if len(rs.fields) == 0 {
-		rs.fields = []string{"*"}
-	}
-
+	rs.fields = res.Fields()
 	return rs, nil
 }
 
-func driverNamedValueToParams(args []driver.NamedValue) []expr.Param {
-	params := make([]expr.Param, len(args))
+func driverNamedValueToParams(args []driver.NamedValue) []interface{} {
+	params := make([]interface{}, len(args))
 	for i, arg := range args {
-		params[i].Name = arg.Name
-		params[i].Value = arg.Value
+		var p expr.Param
+		p.Name = arg.Name
+		p.Value = arg.Value
+		params[i] = p
 	}
 
 	return params
@@ -386,7 +322,7 @@ func (rs *documentStream) iterate(ctx context.Context) {
 
 // Columns returns the fields selected by the SELECT statement.
 func (rs *documentStream) Columns() []string {
-	return rs.fields
+	return rs.res.Fields()
 }
 
 // Close closes the rows iterator.
diff --git a/sql/parser/create.go b/sql/parser/create.go
index 8ba2ba84..8c8271b8 100644
--- a/sql/parser/create.go
+++ b/sql/parser/create.go
@@ -41,18 +41,14 @@ func (p *Parser) parseCreateTableStatement() (query.CreateTableStmt, error) {
 	}
 
 	// Parse table name
-	stmt.TableName, err = p.parseIdent()
+	stmt.Info.TableName, err = p.parseIdent()
 	if err != nil {
 		return stmt, err
 	}
 
 	// parse field constraints
 	err = p.parseConstraints(&stmt)
-	if err != nil {
-		return stmt, err
-	}
-
-	return stmt, nil
+	return stmt, err
 }
 
 func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint) (err error) {
@@ -138,7 +134,7 @@ func (p *Parser) parseConstraints(stmt *query.CreateTableStmt) error {
 	for _, fc := range stmt.Info.FieldConstraints {
 		if fc.IsPrimaryKey {
 			if pkFound {
-				return stringutil.Errorf("table %q has more than one primary key", stmt.TableName)
+				return stringutil.Errorf("table %q has more than one primary key", stmt.Info.TableName)
 			}
 
 			pkFound = true
@@ -232,7 +228,7 @@ func (p *Parser) parseTableConstraint(stmt *query.CreateTableStmt) (bool, error)
 		}
 
 		if pk := stmt.Info.GetPrimaryKey(); pk != nil {
-			return false, stringutil.Errorf("table %q has more than one primary key", stmt.TableName)
+			return false, stringutil.Errorf("table %q has more than one primary key", stmt.Info.TableName)
 		}
 		fc := stmt.Info.FieldConstraints.Get(primaryKeyPath)
 		if fc == nil {
@@ -290,9 +286,8 @@ func (p *Parser) parseTableConstraint(stmt *query.CreateTableStmt) (bool, error)
 // This function assumes the CREATE INDEX or CREATE UNIQUE INDEX tokens have already been consumed.
 func (p *Parser) parseCreateIndexStatement(unique bool) (query.CreateIndexStmt, error) {
 	var err error
-	stmt := query.CreateIndexStmt{
-		Unique: unique,
-	}
+	var stmt query.CreateIndexStmt
+	stmt.Info.Unique = unique
 
 	// Parse IF NOT EXISTS
 	stmt.IfNotExists, err = p.parseOptional(scanner.IF, scanner.NOT, scanner.EXISTS)
@@ -301,7 +296,7 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (query.CreateIndexStmt,
 	}
 
 	// Parse optional index name
-	stmt.IndexName, err = p.parseIdent()
+	stmt.Info.IndexName, err = p.parseIdent()
 	if err != nil {
 		// if IF NOT EXISTS is set, index name is mandatory
 		if stmt.IfNotExists {
@@ -317,7 +312,7 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (query.CreateIndexStmt,
 	}
 
 	// Parse table name
-	stmt.TableName, err = p.parseIdent()
+	stmt.Info.TableName, err = p.parseIdent()
 	if err != nil {
 		return stmt, err
 	}
@@ -331,7 +326,7 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (query.CreateIndexStmt,
 		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"("}, pos)
 	}
 
-	stmt.Paths = paths
+	stmt.Info.Paths = paths
 
 	return stmt, nil
 }
diff --git a/sql/parser/delete.go b/sql/parser/delete.go
index f24abec0..54463707 100644
--- a/sql/parser/delete.go
+++ b/sql/parser/delete.go
@@ -2,7 +2,7 @@ package parser
 
 import (
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/scanner"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/stringutil"
@@ -10,7 +10,7 @@ import (
 
 // parseDeleteStatement parses a delete string and returns a Statement AST object.
 // This function assumes the DELETE token has already been consumed.
-func (p *Parser) parseDeleteStatement() (*planner.Statement, error) {
+func (p *Parser) parseDeleteStatement() (*query.StreamStmt, error) {
 	var cfg deleteConfig
 	var err error
 
@@ -64,7 +64,7 @@ type deleteConfig struct {
 	OrderByDirection scanner.Token
 }
 
-func (cfg deleteConfig) ToStream() (*planner.Statement, error) {
+func (cfg deleteConfig) ToStream() (*query.StreamStmt, error) {
 	s := stream.New(stream.SeqScan(cfg.TableName))
 
 	if cfg.WhereExpr != nil {
@@ -117,7 +117,7 @@ func (cfg deleteConfig) ToStream() (*planner.Statement, error) {
 
 	s = s.Pipe(stream.TableDelete(cfg.TableName))
 
-	return &planner.Statement{
+	return &query.StreamStmt{
 		Stream:   s,
 		ReadOnly: false,
 	}, nil
diff --git a/sql/parser/explain.go b/sql/parser/explain.go
index 9cec54bf..41dba058 100644
--- a/sql/parser/explain.go
+++ b/sql/parser/explain.go
@@ -1,7 +1,6 @@
 package parser
 
 import (
-	"github.com/genjidb/genji/planner"
 	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/scanner"
 )
@@ -21,5 +20,5 @@ func (p *Parser) parseExplainStatement() (query.Statement, error) {
 		return nil, err
 	}
 
-	return &planner.ExplainStmt{Statement: innerStmt}, nil
+	return &query.ExplainStmt{Statement: innerStmt}, nil
 }
diff --git a/sql/parser/expr.go b/sql/parser/expr.go
index cee96853..51d8229c 100644
--- a/sql/parser/expr.go
+++ b/sql/parser/expr.go
@@ -35,7 +35,9 @@ func (p *Parser) parseExprWithMinPrecedence(precedence int) (e expr.Expr, lit st
 	// of the parsed expression
 	if p.buf == nil {
 		p.buf = new(bytes.Buffer)
-		defer func() { p.buf = nil }()
+		defer func() {
+			p.buf = nil
+		}()
 	}
 
 	// Dummy root node.
diff --git a/sql/parser/insert.go b/sql/parser/insert.go
index 2fbd7fdc..d00d65ee 100644
--- a/sql/parser/insert.go
+++ b/sql/parser/insert.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/scanner"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/stringutil"
@@ -12,7 +12,7 @@ import (
 
 // parseInsertStatement parses an insert string and returns a Statement AST object.
 // This function assumes the INSERT token has already been consumed.
-func (p *Parser) parseInsertStatement() (*planner.Statement, error) {
+func (p *Parser) parseInsertStatement() (*query.StreamStmt, error) {
 	var cfg insertConfig
 	var err error
 
@@ -209,11 +209,11 @@ type insertConfig struct {
 	TableName  string
 	Values     []expr.Expr
 	Fields     []string
-	SelectStmt *planner.Statement
+	SelectStmt *query.StreamStmt
 	Returning  []expr.Expr
 }
 
-func (cfg *insertConfig) ToStream() (*planner.Statement, error) {
+func (cfg *insertConfig) ToStream() (*query.StreamStmt, error) {
 	var s *stream.Stream
 	if cfg.Values != nil {
 		s = stream.New(stream.Expressions(cfg.Values...))
@@ -238,7 +238,7 @@ func (cfg *insertConfig) ToStream() (*planner.Statement, error) {
 		s = s.Pipe(stream.Project(cfg.Returning...))
 	}
 
-	return &planner.Statement{
+	return &query.StreamStmt{
 		Stream:   s,
 		ReadOnly: false,
 	}, nil
diff --git a/sql/parser/select.go b/sql/parser/select.go
index ac78c5a7..041a1d0a 100644
--- a/sql/parser/select.go
+++ b/sql/parser/select.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/scanner"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/stringutil"
@@ -12,7 +12,7 @@ import (
 
 // parseSelectStatement parses a select string and returns a Statement AST object.
 // This function assumes the SELECT token has already been consumed.
-func (p *Parser) parseSelectStatement() (*planner.Statement, error) {
+func (p *Parser) parseSelectStatement() (*query.StreamStmt, error) {
 	var cfg selectConfig
 	var err error
 
@@ -179,7 +179,7 @@ type selectConfig struct {
 	ProjectionExprs  []expr.Expr
 }
 
-func (cfg selectConfig) ToStream() (*planner.Statement, error) {
+func (cfg selectConfig) ToStream() (*query.StreamStmt, error) {
 	var s *stream.Stream
 
 	if cfg.TableName != "" {
@@ -322,7 +322,7 @@ func (cfg selectConfig) ToStream() (*planner.Statement, error) {
 		s = s.Pipe(stream.Take(v.V.(int64)))
 	}
 
-	return &planner.Statement{
+	return &query.StreamStmt{
 		Stream:   s,
 		ReadOnly: true,
 	}, nil
diff --git a/sql/parser/update.go b/sql/parser/update.go
index 7445b417..169b706e 100644
--- a/sql/parser/update.go
+++ b/sql/parser/update.go
@@ -3,14 +3,14 @@ package parser
 import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/scanner"
 	"github.com/genjidb/genji/stream"
 )
 
 // parseUpdateStatement parses a update string and returns a Statement AST object.
 // This function assumes the UPDATE token has already been consumed.
-func (p *Parser) parseUpdateStatement() (*planner.Statement, error) {
+func (p *Parser) parseUpdateStatement() (*query.StreamStmt, error) {
 	var cfg updateConfig
 	var err error
 
@@ -134,7 +134,7 @@ type updateSetPair struct {
 }
 
 // ToTree turns the statement into a stream.
-func (cfg updateConfig) ToStream() *planner.Statement {
+func (cfg updateConfig) ToStream() *query.StreamStmt {
 	s := stream.New(stream.SeqScan(cfg.TableName))
 
 	if cfg.WhereExpr != nil {
@@ -153,7 +153,7 @@ func (cfg updateConfig) ToStream() *planner.Statement {
 
 	s = s.Pipe(stream.TableReplace(cfg.TableName))
 
-	return &planner.Statement{
+	return &query.StreamStmt{
 		Stream:   s,
 		ReadOnly: false,
 	}
diff --git a/stream/operator.go b/stream/operator.go
index 81323f4f..c97b2895 100644
--- a/stream/operator.go
+++ b/stream/operator.go
@@ -3,7 +3,6 @@ package stream
 import (
 	"bytes"
 	"container/heap"
-	"container/list"
 	"errors"
 	"strings"
 
@@ -89,7 +88,6 @@ func Map(e expr.Expr) *MapOperator {
 func (op *MapOperator) Iterate(in *expr.Environment, f func(out *expr.Environment) error) error {
 	var newEnv expr.Environment
 
-	list.New()
 	return op.Prev.Iterate(in, func(out *expr.Environment) error {
 		v, err := op.E.Eval(out)
 		if err != nil {
@@ -403,7 +401,7 @@ func (op *TableInsertOperator) Iterate(in *expr.Environment, f func(out *expr.En
 
 		var err error
 		if table == nil {
-			table, err = env.GetTx().GetTable(op.Name)
+			table, err = env.GetTx().Catalog.GetTable(env.GetTx(), op.Name)
 			if err != nil {
 				return err
 			}
@@ -447,7 +445,7 @@ func (op *TableReplaceOperator) Iterate(in *expr.Environment, f func(out *expr.E
 
 		if table == nil {
 			var err error
-			table, err = out.GetTx().GetTable(op.Name)
+			table, err = out.GetTx().Catalog.GetTable(out.GetTx(), op.Name)
 			if err != nil {
 				return err
 			}
@@ -501,7 +499,7 @@ func (op *TableDeleteOperator) Iterate(in *expr.Environment, f func(out *expr.En
 
 		if table == nil {
 			var err error
-			table, err = out.GetTx().GetTable(op.Name)
+			table, err = out.GetTx().Catalog.GetTable(out.GetTx(), op.Name)
 			if err != nil {
 				return err
 			}
diff --git a/stream/scan.go b/stream/scan.go
index 7dbf124b..9c35773a 100644
--- a/stream/scan.go
+++ b/stream/scan.go
@@ -120,7 +120,7 @@ func SeqScanReverse(tableName string) *SeqScanOperator {
 }
 
 func (it *SeqScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Environment) error) error {
-	table, err := in.GetTx().GetTable(it.TableName)
+	table, err := in.GetTx().Catalog.GetTable(in.GetTx(), it.TableName)
 	if err != nil {
 		return err
 	}
@@ -205,7 +205,7 @@ func (it *PkScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Enviro
 	var newEnv expr.Environment
 	newEnv.Outer = in
 
-	table, err := in.GetTx().GetTable(it.TableName)
+	table, err := in.GetTx().Catalog.GetTable(in.GetTx(), it.TableName)
 	if err != nil {
 		return err
 	}
@@ -322,12 +322,12 @@ func (it *IndexScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Env
 	var newEnv expr.Environment
 	newEnv.Outer = in
 
-	index, err := in.GetTx().GetIndex(it.IndexName)
+	index, err := in.GetTx().Catalog.GetIndex(in.GetTx(), it.IndexName)
 	if err != nil {
 		return err
 	}
 
-	table, err := in.GetTx().GetTable(index.Info.TableName)
+	table, err := in.GetTx().Catalog.GetTable(in.GetTx(), index.Info.TableName)
 	if err != nil {
 		return err
 	}
@@ -413,8 +413,6 @@ func (it *IndexScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Env
 		if err != nil {
 			return err
 		}
-
-		// }
 	}
 
 	return nil
diff --git a/stream/stream.go b/stream/stream.go
index d6397a46..fa11f868 100644
--- a/stream/stream.go
+++ b/stream/stream.go
@@ -82,6 +82,17 @@ func (s *Stream) String() string {
 	return sb.String()
 }
 
+func (s *Stream) Clone() *Stream {
+	var clone *Stream
+
+	for n := s.First(); n != nil; n = n.GetNext() {
+		clone = clone.Pipe(n)
+
+	}
+
+	return clone
+}
+
 func InsertBefore(op, newOp Operator) Operator {
 	if op != nil {
 		prev := op.GetPrev()
diff --git a/stringutil/custom.go b/stringutil/custom.go
index ffcc6862..eafe8777 100644
--- a/stringutil/custom.go
+++ b/stringutil/custom.go
@@ -2,6 +2,7 @@ package stringutil
 
 import (
 	"errors"
+	"io"
 	"strconv"
 	"strings"
 )
@@ -10,6 +11,11 @@ type Stringer interface {
 	String() string
 }
 
+func fprintf(w io.Writer, msg string, args ...interface{}) (int, error) {
+	s := sprintf(msg, args...)
+	return w.Write([]byte(s))
+}
+
 func sprintf(msg string, args ...interface{}) string {
 	var count int
 	var b strings.Builder
diff --git a/stringutil/stringutil.go b/stringutil/stringutil.go
index d1f6f399..7dfa5c1f 100644
--- a/stringutil/stringutil.go
+++ b/stringutil/stringutil.go
@@ -2,7 +2,10 @@
 
 package stringutil
 
-import "fmt"
+import (
+	"fmt"
+	"io"
+)
 
 // Sprintf calls fmt.Sprintf.
 // During wasm builds it is replaced by a custom version.
@@ -10,6 +13,12 @@ func Sprintf(format string, a ...interface{}) string {
 	return fmt.Sprintf(format, a...)
 }
 
+// Fprintf calls fmt.Fprintf.
+// During wasm builds it is replaced by a custom version.
+func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
+	return fmt.Fprintf(w, format, a...)
+}
+
 // Errorf calls fmt.Errorf.
 // During wasm builds it is replaced by a custom version.
 func Errorf(format string, a ...interface{}) error {
diff --git a/stringutil/stringutil_wasm.go b/stringutil/stringutil_wasm.go
index 117452ae..6be73c0b 100644
--- a/stringutil/stringutil_wasm.go
+++ b/stringutil/stringutil_wasm.go
@@ -5,6 +5,11 @@ func Sprintf(format string, a ...interface{}) string {
 	return sprintf(format, a...)
 }
 
+// Fprintf calls a custom version of fprintf for wasm builds.
+func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
+	return fprintf(w, format, a...)
+}
+
 // Errorf calls a custom version of ErrorF for wasm builds.
 func Errorf(format string, a ...interface{}) error {
 	return errorf(format, a...)


Test patch:
diff --git a/cmd/genji/dbutil/dump_test.go b/cmd/genji/dbutil/dump_test.go
index a9e0184f..12854d2c 100644
--- a/cmd/genji/dbutil/dump_test.go
+++ b/cmd/genji/dbutil/dump_test.go
@@ -53,7 +53,7 @@ func TestDump(t *testing.T) {
 					writeToBuf("\n")
 				}
 
-				q := fmt.Sprintf("CREATE TABLE %s (\n a INTEGER\n);", table)
+				q := fmt.Sprintf("CREATE TABLE %s (a INTEGER);", table)
 				err = db.Exec(q)
 				require.NoError(t, err)
 				writeToBuf(q + "\n")
@@ -136,7 +136,7 @@ func TestDumpSchema(t *testing.T) {
 					writeToBuf("\n")
 				}
 
-				q := fmt.Sprintf("CREATE TABLE %s (\n a INTEGER\n);", table)
+				q := fmt.Sprintf("CREATE TABLE %s (a INTEGER);", table)
 				err = db.Exec(q)
 				require.NoError(t, err)
 				writeToBuf(q + "\n")
diff --git a/cmd/genji/shell/command_test.go b/cmd/genji/shell/command_test.go
index 4f8880eb..cdd521a5 100644
--- a/cmd/genji/shell/command_test.go
+++ b/cmd/genji/shell/command_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/dgraph-io/badger/v3"
 	"github.com/genjidb/genji"
+	"github.com/genjidb/genji/cmd/genji/dbutil"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine/badgerengine"
 	"github.com/stretchr/testify/require"
@@ -60,8 +61,8 @@ func TestIndexesCmd(t *testing.T) {
 		want      string
 		fails     bool
 	}{
-		{"All", "", "idx_bar_a_b ON bar (a, b)\nidx_foo_a ON foo (a)\nidx_foo_b ON foo (b)\n", false},
-		{"With table name", "foo", "idx_foo_a ON foo (a)\nidx_foo_b ON foo (b)\n", false},
+		{"All", "", "idx_bar_a_b\nidx_foo_a\nidx_foo_b\n", false},
+		{"With table name", "foo", "idx_foo_a\nidx_foo_b\n", false},
 		{"With nonexistent table name", "baz", "", true},
 	}
 
@@ -157,37 +158,10 @@ func TestSaveCommand(t *testing.T) {
 			require.Equal(t, 2, res.B)
 
 			// ensure that the index has been created
-			err = db.View(func(tx *genji.Tx) error {
-				indexes := tx.ListIndexes()
-				require.Len(t, indexes, 1)
-				require.Equal(t, "idx_a_b", indexes[0])
-
-				index, err := tx.GetIndex("idx_a_b")
-				require.NoError(t, err)
-				require.Equal(t, []document.ValueType{document.DoubleValue, 0}, index.Info.Types)
-
-				return nil
-			})
-			require.NoError(t, err)
-
-			// ensure that the data has been reindexed
-			tx, err := db.Begin(false)
-			require.NoError(t, err)
-
-			defer tx.Rollback()
-
-			idx, err := tx.GetIndex("idx_a_b")
-			require.NoError(t, err)
-
-			// check that by iterating through the index and finding the previously inserted values
-			var i int
-			err = idx.AscendGreaterOrEqual([]document.Value{document.NewDoubleValue(0)}, func(v, k []byte) error {
-				i++
-				return nil
-			})
-
-			require.Equal(t, 3, i)
+			indexes, err := dbutil.ListIndexes(context.Background(), db, "")
 			require.NoError(t, err)
+			require.Len(t, indexes, 1)
+			require.Equal(t, "idx_a_b", indexes[0])
 		})
 	}
 }
diff --git a/database/catalog_test.go b/database/catalog_test.go
index 3b870032..25b709ec 100644
--- a/database/catalog_test.go
+++ b/database/catalog_test.go
@@ -40,22 +40,20 @@ func update(t testing.TB, db *database.Database, fn func(tx *database.Transactio
 // - AddFieldConstraint
 func TestCatalogTable(t *testing.T) {
 	t.Run("Get", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		update(t, db, func(tx *database.Transaction) error {
-			return catalog.CreateTable(tx, "test", nil)
+			return tx.Catalog.CreateTable(tx, "test", nil)
 		})
 
 		update(t, db, func(tx *database.Transaction) error {
-			table, err := tx.GetTable("test")
+			table, err := tx.Catalog.GetTable(tx, "test")
 			require.NoError(t, err)
-			require.Equal(t, "test", table.Name())
+			require.Equal(t, "test", table.Name)
 
 			// Getting a table that doesn't exist should fail.
-			_, err = tx.GetTable("unknown")
+			_, err = tx.Catalog.GetTable(tx, "unknown")
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
@@ -65,29 +63,27 @@ func TestCatalogTable(t *testing.T) {
 	})
 
 	t.Run("Drop", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		update(t, db, func(tx *database.Transaction) error {
-			return catalog.CreateTable(tx, "test", nil)
+			return tx.Catalog.CreateTable(tx, "test", nil)
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := tx.DropTable("test")
+			err := tx.Catalog.DropTable(tx, "test")
 			require.NoError(t, err)
 
 			// Getting a table that has been dropped should fail.
-			_, err = tx.GetTable("test")
+			_, err = tx.Catalog.GetTable(tx, "test")
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
 
 			// Dropping a table that doesn't exist should fail.
-			err = tx.DropTable("test")
+			err = tx.Catalog.DropTable(tx, "test")
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
@@ -95,63 +91,61 @@ func TestCatalogTable(t *testing.T) {
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Rename", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		ti := &database.TableInfo{FieldConstraints: []*database.FieldConstraint{
-			{Path: parsePath(t, "name"), Type: document.TextValue, IsNotNull: true},
-			{Path: parsePath(t, "age"), Type: document.IntegerValue, IsPrimaryKey: true},
-			{Path: parsePath(t, "gender"), Type: document.TextValue},
-			{Path: parsePath(t, "city"), Type: document.TextValue},
+			{Path: testutil.ParseDocumentPath(t, "name"), Type: document.TextValue, IsNotNull: true},
+			{Path: testutil.ParseDocumentPath(t, "age"), Type: document.IntegerValue, IsPrimaryKey: true},
+			{Path: testutil.ParseDocumentPath(t, "gender"), Type: document.TextValue},
+			{Path: testutil.ParseDocumentPath(t, "city"), Type: document.TextValue},
 		}}
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "foo", ti)
+			err := tx.Catalog.CreateTable(tx, "foo", ti)
 			require.NoError(t, err)
 
-			err = catalog.CreateIndex(tx, &database.IndexInfo{Paths: []document.Path{parsePath(t, "gender")}, IndexName: "idx_gender", TableName: "foo"})
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{Paths: []document.Path{testutil.ParseDocumentPath(t, "gender")}, IndexName: "idx_gender", TableName: "foo"})
 			require.NoError(t, err)
-			err = catalog.CreateIndex(tx, &database.IndexInfo{Paths: []document.Path{parsePath(t, "city")}, IndexName: "idx_city", TableName: "foo", Unique: true})
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{Paths: []document.Path{testutil.ParseDocumentPath(t, "city")}, IndexName: "idx_city", TableName: "foo", Unique: true})
 			require.NoError(t, err)
 
 			return nil
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := tx.RenameTable("foo", "zoo")
+			err := tx.Catalog.RenameTable(tx, "foo", "zoo")
 			require.NoError(t, err)
 
 			// Getting the old table should return an error.
-			_, err = tx.GetTable("foo")
+			_, err = tx.Catalog.GetTable(tx, "foo")
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
 
-			tb, err := tx.GetTable("zoo")
+			tb, err := tx.Catalog.GetTable(tx, "zoo")
 			require.NoError(t, err)
 			// The field constraints should be the same.
-			info := tb.Info()
-			require.Equal(t, ti.FieldConstraints, info.FieldConstraints)
+
+			require.Equal(t, ti.FieldConstraints, tb.Info.FieldConstraints)
 
 			// Check that the indexes have been updated as well.
-			idxs := tx.ListIndexes()
+			idxs := tx.Catalog.ListIndexes(tb.Name)
 			require.Len(t, idxs, 2)
 			for _, name := range idxs {
-				idx, err := tx.GetIndex(name)
+				idx, err := tx.Catalog.GetIndex(tx, name)
 				require.NoError(t, err)
 				require.Equal(t, "zoo", idx.Info.TableName)
 			}
 
 			// Renaming a non existing table should return an error
-			err = tx.RenameTable("foo", "")
+			err = tx.Catalog.RenameTable(tx, "foo", "")
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
@@ -159,102 +153,98 @@ func TestCatalogTable(t *testing.T) {
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Add field constraint", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		ti := &database.TableInfo{FieldConstraints: []*database.FieldConstraint{
-			{Path: parsePath(t, "name"), Type: document.TextValue, IsNotNull: true},
-			{Path: parsePath(t, "age"), Type: document.IntegerValue, IsPrimaryKey: true},
-			{Path: parsePath(t, "gender"), Type: document.TextValue},
-			{Path: parsePath(t, "city"), Type: document.TextValue},
+			{Path: testutil.ParseDocumentPath(t, "name"), Type: document.TextValue, IsNotNull: true},
+			{Path: testutil.ParseDocumentPath(t, "age"), Type: document.IntegerValue, IsPrimaryKey: true},
+			{Path: testutil.ParseDocumentPath(t, "gender"), Type: document.TextValue},
+			{Path: testutil.ParseDocumentPath(t, "city"), Type: document.TextValue},
 		}}
 
 		update(t, db, func(tx *database.Transaction) error {
-			return catalog.CreateTable(tx, "foo", ti)
+			return tx.Catalog.CreateTable(tx, "foo", ti)
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
 
 			// Add field constraint
 			fieldToAdd := database.FieldConstraint{
-				Path: parsePath(t, "last_name"), Type: document.TextValue,
+				Path: testutil.ParseDocumentPath(t, "last_name"), Type: document.TextValue,
 			}
-			err := tx.AddFieldConstraint("foo", fieldToAdd)
+			err := tx.Catalog.AddFieldConstraint(tx, "foo", fieldToAdd)
 			require.NoError(t, err)
 
-			tb, err := tx.GetTable("foo")
+			tb, err := tx.Catalog.GetTable(tx, "foo")
 			require.NoError(t, err)
 
 			// The field constraints should not be the same.
-			info := tb.Info()
-			require.Contains(t, info.FieldConstraints, &fieldToAdd)
+
+			require.Contains(t, tb.Info.FieldConstraints, &fieldToAdd)
 
 			// Renaming a non existing table should return an error
-			err = tx.AddFieldConstraint("bar", fieldToAdd)
+			err = tx.Catalog.AddFieldConstraint(tx, "bar", fieldToAdd)
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
 			}
 
 			// Adding a existing field should return an error
-			err = tx.AddFieldConstraint("foo", *ti.FieldConstraints[0])
+			err = tx.Catalog.AddFieldConstraint(tx, "foo", *ti.FieldConstraints[0])
 			require.Error(t, err)
 
 			// Adding a second primary key should return an error
 			fieldToAdd = database.FieldConstraint{
-				Path: parsePath(t, "foobar"), Type: document.IntegerValue, IsPrimaryKey: true,
+				Path: testutil.ParseDocumentPath(t, "foobar"), Type: document.IntegerValue, IsPrimaryKey: true,
 			}
-			err = tx.AddFieldConstraint("foo", fieldToAdd)
+			err = tx.Catalog.AddFieldConstraint(tx, "foo", fieldToAdd)
 			require.Error(t, err)
 
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 }
 
 func TestCatalogCreate(t *testing.T) {
 	t.Run("Same table name", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", nil)
+			err := tx.Catalog.CreateTable(tx, "test", nil)
 			require.NoError(t, err)
 
 			// Creating a table that already exists should fail.
-			err = catalog.CreateTable(tx, "test", nil)
+			err = tx.Catalog.CreateTable(tx, "test", nil)
 			require.EqualError(t, err, database.ErrTableAlreadyExists.Error())
 
 			// Creating a table that starts with __genji_ should fail.
-			err = catalog.CreateTable(tx, "__genji_foo", nil)
+			err = tx.Catalog.CreateTable(tx, "__genji_foo", nil)
 			require.Error(t, err)
 
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Create and rollback", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
 		check := func() {
 			update(t, db, func(tx *database.Transaction) error {
-				err := db.Catalog().CreateTable(tx, "test", nil)
+				err := tx.Catalog.CreateTable(tx, "test", nil)
 				require.NoError(t, err)
 
 				return errDontCommit
@@ -266,15 +256,13 @@ func TestCatalogCreate(t *testing.T) {
 	})
 
 	t.Run("Invalid constraints", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", &database.TableInfo{
+			err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 				FieldConstraints: []*database.FieldConstraint{
 					{Path: document.NewPath("a", "b"), Type: document.IntegerValue},
 					{Path: document.NewPath("a"), Type: document.IntegerValue},
@@ -284,23 +272,22 @@ func TestCatalogCreate(t *testing.T) {
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 }
 
 func TestTxCreateIndex(t *testing.T) {
 	t.Run("Should create an index, build it and return it", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
-		catalog := db.Catalog()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", nil)
+			err := tx.Catalog.CreateTable(tx, "test", nil)
 			if err != nil {
 				return err
 			}
 
-			tb, err := tx.GetTable("test")
+			tb, err := tx.Catalog.GetTable(tx, "test")
 			require.NoError(t, err)
 
 			for i := int64(0); i < 10; i++ {
@@ -314,14 +301,14 @@ func TestTxCreateIndex(t *testing.T) {
 			return nil
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idx_a", TableName: "test", Paths: []document.Path{parsePath(t, "a")},
+			err := tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idx_a", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "a")},
 			})
 			require.NoError(t, err)
-			idx, err := tx.GetIndex("idx_a")
+			idx, err := tx.Catalog.GetIndex(tx, "idx_a")
 			require.NoError(t, err)
 			require.NotNil(t, idx)
 
@@ -341,26 +328,25 @@ func TestTxCreateIndex(t *testing.T) {
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Should fail if it already exists", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
 		update(t, db, func(tx *database.Transaction) error {
-			return catalog.CreateTable(tx, "test", nil)
+			return tx.Catalog.CreateTable(tx, "test", nil)
 		})
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err := tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			require.NoError(t, err)
 
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			require.Equal(t, database.ErrIndexAlreadyExists, err)
 			return nil
@@ -368,12 +354,11 @@ func TestTxCreateIndex(t *testing.T) {
 	})
 
 	t.Run("Should fail if table doesn't exist", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
-		catalog := db.Catalog()
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err := tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			if !errors.Is(err, database.ErrTableNotFound) {
 				require.Equal(t, err, database.ErrTableNotFound)
@@ -384,30 +369,29 @@ func TestTxCreateIndex(t *testing.T) {
 	})
 
 	t.Run("Should generate a name if not provided", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
-		catalog := db.Catalog()
 
 		update(t, db, func(tx *database.Transaction) error {
-			return catalog.CreateTable(tx, "test", nil)
+			return tx.Catalog.CreateTable(tx, "test", nil)
 		})
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateIndex(tx, &database.IndexInfo{
-				TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err := tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			require.NoError(t, err)
 
-			_, err = catalog.GetIndex(tx, "__genji_autoindex_test_1")
+			_, err = tx.Catalog.GetIndex(tx, "__genji_autoindex_test_1")
 			require.NoError(t, err)
 
 			// create another one
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
-				TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			require.NoError(t, err)
 
-			_, err = catalog.GetIndex(tx, "__genji_autoindex_test_2")
+			_, err = tx.Catalog.GetIndex(tx, "__genji_autoindex_test_2")
 			require.NoError(t, err)
 			return nil
 		})
@@ -416,52 +400,50 @@ func TestTxCreateIndex(t *testing.T) {
 
 func TestTxDropIndex(t *testing.T) {
 	t.Run("Should drop an index", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
-		catalog := db.Catalog()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", nil)
+			err := tx.Catalog.CreateTable(tx, "test", nil)
 			require.NoError(t, err)
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 			})
 			require.NoError(t, err)
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
-				IndexName: "idxBar", TableName: "test", Paths: []document.Path{parsePath(t, "bar")},
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+				IndexName: "idxBar", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "bar")},
 			})
 			require.NoError(t, err)
 			return nil
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.DropIndex(tx, "idxFoo")
+			err := tx.Catalog.DropIndex(tx, "idxFoo")
 			require.NoError(t, err)
 
-			_, err = tx.GetIndex("idxFoo")
+			_, err = tx.Catalog.GetIndex(tx, "idxFoo")
 			require.Error(t, err)
 
-			_, err = tx.GetIndex("idxBar")
+			_, err = tx.Catalog.GetIndex(tx, "idxBar")
 			require.NoError(t, err)
 
 			// cf: https://github.com/genjidb/genji/issues/360
-			_, err = tx.GetTable("test")
+			_, err = tx.Catalog.GetTable(tx, "test")
 			require.NoError(t, err)
 
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Should fail if it doesn't exist", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
-		catalog := db.Catalog()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.DropIndex(tx, "idxFoo")
+			err := tx.Catalog.DropIndex(tx, "idxFoo")
 			require.Equal(t, database.ErrIndexNotFound, err)
 			return nil
 		})
@@ -470,12 +452,11 @@ func TestTxDropIndex(t *testing.T) {
 
 func TestCatalogReIndex(t *testing.T) {
 	prepareTableFn := func(t *testing.T, db *database.Database) {
-		catalog := db.Catalog()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", nil)
+			err := tx.Catalog.CreateTable(tx, "test", nil)
 			require.NoError(t, err)
-			tb, err := tx.GetTable("test")
+			tb, err := tx.Catalog.GetTable(tx, "test")
 			require.NoError(t, err)
 
 			for i := int64(0); i < 10; i++ {
@@ -486,16 +467,16 @@ func TestCatalogReIndex(t *testing.T) {
 				require.NoError(t, err)
 			}
 
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 				IndexName: "a",
 				TableName: "test",
-				Paths:     []document.Path{parsePath(t, "a")},
+				Paths:     []document.Path{testutil.ParseDocumentPath(t, "a")},
 			})
 			require.NoError(t, err)
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 				IndexName: "b",
 				TableName: "test",
-				Paths:     []document.Path{parsePath(t, "b")},
+				Paths:     []document.Path{testutil.ParseDocumentPath(t, "b")},
 			})
 			require.NoError(t, err)
 
@@ -504,29 +485,27 @@ func TestCatalogReIndex(t *testing.T) {
 	}
 
 	t.Run("Should fail if not found", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
 		prepareTableFn(t, db)
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := tx.ReIndex("foo")
+			err := tx.Catalog.ReIndex(tx, "foo")
 			require.Equal(t, database.ErrIndexNotFound, err)
 			return nil
 		})
 	})
 
 	t.Run("Should not fail if field not found", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test", nil)
+			err := tx.Catalog.CreateTable(tx, "test", nil)
 			require.NoError(t, err)
 
-			tb, err := tx.GetTable("test")
+			tb, err := tx.Catalog.GetTable(tx, "test")
 			require.NoError(t, err)
 
 			_, err = tb.Insert(document.NewFieldBuffer().
@@ -534,43 +513,42 @@ func TestCatalogReIndex(t *testing.T) {
 			)
 			require.NoError(t, err)
 
-			return catalog.CreateIndex(tx, &database.IndexInfo{
+			return tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 				IndexName: "b",
 				TableName: "test",
-				Paths:     []document.Path{parsePath(t, "b")},
+				Paths:     []document.Path{testutil.ParseDocumentPath(t, "b")},
 			})
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.ReIndex(tx, "b")
+			err := tx.Catalog.ReIndex(tx, "b")
 			require.NoError(t, err)
 
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 
 	t.Run("Should reindex the index", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
 		prepareTableFn(t, db)
 
-		catalog := db.Catalog()
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := tx.ReIndex("a")
+			err := tx.Catalog.ReIndex(tx, "a")
 			require.NoError(t, err)
 
-			idx, err := tx.GetIndex("a")
+			idx, err := tx.Catalog.GetIndex(tx, "a")
 			require.NoError(t, err)
 
 			var i int
-			err = idx.AscendGreaterOrEqual([]document.Value{document.Value{Type: document.DoubleValue}}, func(v, k []byte) error {
+			err = idx.AscendGreaterOrEqual([]document.Value{{Type: document.DoubleValue}}, func(v, k []byte) error {
 				var buf bytes.Buffer
 				err = document.NewValueEncoder(&buf).Encode(document.NewDoubleValue(float64(i)))
 				require.NoError(t, err)
@@ -582,44 +560,41 @@ func TestCatalogReIndex(t *testing.T) {
 			require.Equal(t, 10, i)
 			require.NoError(t, err)
 
-			_, err = tx.GetIndex("b")
+			_, err = tx.Catalog.GetIndex(tx, "b")
 			require.NoError(t, err)
 
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 }
 
 func TestReIndexAll(t *testing.T) {
 	t.Run("Should succeed if no indexes", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
 		update(t, db, func(tx *database.Transaction) error {
-			catalog := db.Catalog()
-			err := catalog.ReIndexAll(tx)
+			err := tx.Catalog.ReIndexAll(tx)
 			require.NoError(t, err)
 			return nil
 		})
 	})
 
 	t.Run("Should reindex all indexes", func(t *testing.T) {
-		db, cleanup := newTestDB(t)
+		db, cleanup := testutil.NewTestDB(t)
 		defer cleanup()
 
-		catalog := db.Catalog()
-
 		update(t, db, func(tx *database.Transaction) error {
-			err := catalog.CreateTable(tx, "test1", nil)
+			err := tx.Catalog.CreateTable(tx, "test1", nil)
 			require.NoError(t, err)
-			tb1, err := tx.GetTable("test1")
+			tb1, err := tx.Catalog.GetTable(tx, "test1")
 			require.NoError(t, err)
 
-			err = catalog.CreateTable(tx, "test2", nil)
+			err = tx.Catalog.CreateTable(tx, "test2", nil)
 			require.NoError(t, err)
-			tb2, err := tx.GetTable("test2")
+			tb2, err := tx.Catalog.GetTable(tx, "test2")
 			require.NoError(t, err)
 
 			for i := int64(0); i < 10; i++ {
@@ -635,32 +610,32 @@ func TestReIndexAll(t *testing.T) {
 				require.NoError(t, err)
 			}
 
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 				IndexName: "t1a",
 				TableName: "test1",
-				Paths:     []document.Path{parsePath(t, "a")},
+				Paths:     []document.Path{testutil.ParseDocumentPath(t, "a")},
 			})
 			require.NoError(t, err)
-			err = catalog.CreateIndex(tx, &database.IndexInfo{
+			err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 				IndexName: "t2a",
 				TableName: "test2",
-				Paths:     []document.Path{parsePath(t, "a")},
+				Paths:     []document.Path{testutil.ParseDocumentPath(t, "a")},
 			})
 			require.NoError(t, err)
 
 			return nil
 		})
 
-		clone := catalog.Clone()
+		clone := db.Catalog.Clone()
 
 		update(t, db, func(tx *database.Transaction) error {
-			err := tx.ReIndexAll()
+			err := tx.Catalog.ReIndexAll(tx)
 			require.NoError(t, err)
-			idx, err := tx.GetIndex("t1a")
+			idx, err := tx.Catalog.GetIndex(tx, "t1a")
 			require.NoError(t, err)
 
 			var i int
-			err = idx.AscendGreaterOrEqual([]document.Value{document.Value{Type: document.DoubleValue}}, func(v, k []byte) error {
+			err = idx.AscendGreaterOrEqual([]document.Value{{Type: document.DoubleValue}}, func(v, k []byte) error {
 				var buf bytes.Buffer
 				err = document.NewValueEncoder(&buf).Encode(document.NewDoubleValue(float64(i)))
 				require.NoError(t, err)
@@ -672,11 +647,11 @@ func TestReIndexAll(t *testing.T) {
 			require.Equal(t, 10, i)
 			require.NoError(t, err)
 
-			idx, err = tx.GetIndex("t2a")
+			idx, err = tx.Catalog.GetIndex(tx, "t2a")
 			require.NoError(t, err)
 
 			i = 0
-			err = idx.AscendGreaterOrEqual([]document.Value{document.Value{Type: document.DoubleValue}}, func(v, k []byte) error {
+			err = idx.AscendGreaterOrEqual([]document.Value{{Type: document.DoubleValue}}, func(v, k []byte) error {
 				var buf bytes.Buffer
 				err = document.NewValueEncoder(&buf).Encode(document.NewDoubleValue(float64(i)))
 				require.NoError(t, err)
@@ -691,7 +666,7 @@ func TestReIndexAll(t *testing.T) {
 			return errDontCommit
 		})
 
-		require.Equal(t, clone, catalog)
+		require.Equal(t, clone, db.Catalog)
 	})
 }
 
@@ -700,13 +675,25 @@ func TestReadOnlyTables(t *testing.T) {
 	require.NoError(t, err)
 	defer db.Close()
 
-	doc, err := db.QueryDocument(`CREATE TABLE foo; SELECT * FROM __genji_tables`)
+	doc, err := db.QueryDocument(`CREATE TABLE foo (a int, b[3].c double unique); SELECT * FROM __genji_tables`)
 	require.NoError(t, err)
 
-	testutil.RequireDocJSONEq(t, doc, `{"field_constraints": [], "read_only":false, "store_name":"dAE=", "table_name":"foo"}`)
+	testutil.RequireDocJSONEq(t, doc, `{"sql":"CREATE TABLE foo (a INTEGER, b[3].c DOUBLE)", "store_name":"dAE=", "table_name":"foo"}`)
 
-	doc, err = db.QueryDocument(`CREATE INDEX idx_foo_a ON foo(a); SELECT * FROM __genji_indexes`)
+	res, err := db.Query(`CREATE INDEX idx_foo_a ON foo(a); SELECT * FROM __genji_indexes`)
 	require.NoError(t, err)
+	defer res.Close()
 
-	testutil.RequireDocJSONEq(t, doc, `{"index_name":"idx_foo_a", "paths":[["a"]], "table_name":"foo", "types":[0], "unique":false}`)
+	testutil.RequireStreamEq(t, `
+		{
+			"sql": "CREATE INDEX __genji_autoindex_foo_1 ON foo (b[3].c)",
+			"index_name": "__genji_autoindex_foo_1",
+			"table_name": "foo"
+		}
+		{
+			"sql": "CREATE INDEX idx_foo_a ON foo (a)",
+			"index_name": "idx_foo_a",
+			"table_name": "foo"
+		}
+	`, res)
 }
diff --git a/database/config_test.go b/database/config_test.go
deleted file mode 100644
index 0561ef54..00000000
--- a/database/config_test.go
+++ /dev/null
@@ -1,220 +0,0 @@
-package database
-
-import (
-	"context"
-	"errors"
-	"testing"
-
-	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding/msgpack"
-	"github.com/genjidb/genji/engine"
-	"github.com/genjidb/genji/engine/memoryengine"
-	"github.com/stretchr/testify/require"
-)
-
-func newPath(s string) document.Path {
-	return document.Path{
-		document.PathFragment{
-			FieldName: "k",
-		},
-	}
-}
-
-func TestTableInfo(t *testing.T) {
-	info := &TableInfo{
-		FieldConstraints: []*FieldConstraint{
-			{Path: newPath("k"), Type: document.DoubleValue, IsPrimaryKey: true},
-		},
-	}
-
-	doc := info.ToDocument()
-
-	var res TableInfo
-	err := res.ScanDocument(doc)
-	require.NoError(t, err)
-}
-
-func TestTableInfoStore(t *testing.T) {
-	t.Run("ok", func(t *testing.T) {
-		ng := memoryengine.NewEngine()
-		defer ng.Close()
-
-		db, err := New(context.Background(), ng, Options{
-			Codec: msgpack.NewCodec(),
-		})
-		require.NoError(t, err)
-		defer db.Close()
-
-		tx, err := db.Begin(true)
-		require.NoError(t, err)
-		defer tx.Rollback()
-
-		info := &TableInfo{
-			FieldConstraints: []*FieldConstraint{
-				{Path: newPath("k"), Type: document.DoubleValue, IsPrimaryKey: true},
-			},
-		}
-
-		// Inserting one TableInfo should work.
-		err = tx.getTableStore().Insert(tx, "foo1", info)
-		require.NoError(t, err)
-
-		// Inserting an existing TableInfo should not work.
-		err = tx.getTableStore().Insert(tx, "foo1", info)
-		require.Equal(t, err, ErrTableAlreadyExists)
-
-		// Getting an the list of TableInfo should work.
-		list, err := tx.getTableStore().ListAll()
-		require.NoError(t, err)
-		require.Len(t, list, 1)
-
-		// Deleting an existing TableInfo should work.
-		err = tx.getTableStore().Delete(tx, "foo1")
-		require.NoError(t, err)
-
-		// Deleting a non-existing TableInfo should not work.
-		err = tx.getTableStore().Delete(tx, "foo1")
-		if !errors.Is(err, ErrTableNotFound) {
-			require.Equal(t, err, ErrTableNotFound)
-		}
-	})
-
-	t.Run("on rollback", func(t *testing.T) {
-		ng := memoryengine.NewEngine()
-		defer ng.Close()
-
-		db, err := New(context.Background(), ng, Options{
-			Codec: msgpack.NewCodec(),
-		})
-		require.NoError(t, err)
-		defer db.Close()
-
-		info := &TableInfo{
-			FieldConstraints: []*FieldConstraint{
-				{Path: newPath("k"), Type: document.DoubleValue, IsPrimaryKey: true},
-			},
-		}
-
-		insertAndRollback := func() {
-			tx, err := db.Begin(true)
-			require.NoError(t, err)
-			err = tx.getTableStore().Insert(tx, "foo", info)
-			require.NoError(t, err)
-			err = tx.Rollback()
-			require.NoError(t, err)
-		}
-
-		insertAndRollback()
-		insertAndRollback()
-	})
-
-	t.Run("on commit", func(t *testing.T) {
-		ng := memoryengine.NewEngine()
-		defer ng.Close()
-
-		db, err := New(context.Background(), ng, Options{
-			Codec: msgpack.NewCodec(),
-		})
-		require.NoError(t, err)
-		defer db.Close()
-
-		info := &TableInfo{
-			FieldConstraints: []*FieldConstraint{
-				{Path: newPath("k"), Type: document.DoubleValue, IsPrimaryKey: true},
-			},
-		}
-
-		insertAndCommit := func() error {
-			tx, err := db.Begin(true)
-			require.NoError(t, err)
-			defer tx.Rollback()
-
-			err = tx.getTableStore().Insert(tx, "foo", info)
-			if err != nil {
-				return err
-			}
-			return tx.Commit()
-		}
-
-		require.NoError(t, insertAndCommit())
-		require.Error(t, insertAndCommit())
-	})
-}
-
-func TestIndexStore(t *testing.T) {
-	ng := memoryengine.NewEngine()
-	defer ng.Close()
-
-	tx, err := ng.Begin(context.Background(), engine.TxOptions{
-		Writable: true,
-	})
-	require.NoError(t, err)
-	defer tx.Rollback()
-
-	err = tx.CreateStore([]byte("test"))
-	require.NoError(t, err)
-	st, err := tx.GetStore([]byte("test"))
-	require.NoError(t, err)
-
-	idxs := indexStore{db: &Database{Codec: msgpack.NewCodec()}, st: st}
-
-	t.Run("Basic operations", func(t *testing.T) {
-		cfg := IndexInfo{
-			TableName: "test",
-			IndexName: "idx_test",
-			Unique:    true,
-			Types:     []document.ValueType{document.BoolValue},
-		}
-
-		err = idxs.Insert(&cfg)
-		require.NoError(t, err)
-
-		// Inserting the same index should fail.
-		err = idxs.Insert(&cfg)
-		require.EqualError(t, err, ErrIndexAlreadyExists.Error())
-
-		idxcfg, err := idxs.Get("idx_test")
-		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)
-
-		// Getting a non existing index should fail.
-		_, err = idxs.Get("idx_test")
-		require.EqualError(t, err, ErrIndexNotFound.Error())
-	})
-
-	t.Run("List all indexes", func(t *testing.T) {
-		idxcfgs := []*IndexInfo{
-			{TableName: "test1", IndexName: "idx_test1", Unique: true},
-			{TableName: "test2", IndexName: "idx_test2", Unique: true},
-			{TableName: "test3", IndexName: "idx_test3", Unique: true},
-		}
-		for _, v := range idxcfgs {
-			err = idxs.Insert(v)
-			require.NoError(t, err)
-		}
-
-		list, err := idxs.ListAll()
-		require.NoError(t, err)
-		require.Len(t, list, len(idxcfgs))
-		require.EqualValues(t, idxcfgs, list)
-
-		// Removing one index should remove only one index.
-		err = idxs.Delete("idx_test1")
-		require.NoError(t, err)
-
-		list, err = idxs.ListAll()
-		require.NoError(t, err)
-		require.Len(t, list, len(idxcfgs)-1)
-	})
-}
diff --git a/database/table_test.go b/database/table_test.go
index 7438c99e..3014e4f2 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -12,23 +12,16 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/document/encoding/msgpack"
 	"github.com/genjidb/genji/engine/memoryengine"
-	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/testutil"
 	"github.com/stretchr/testify/require"
 )
 
-func parsePath(t testing.TB, str string) document.Path {
-	vp, err := parser.ParsePath(str)
-	require.NoError(t, err)
-	return vp
-}
-
 func newTestTable(t testing.TB) (*database.Table, func()) {
-	_, tx, fn := newTestTx(t)
+	_, tx, fn := testutil.NewTestTx(t)
 
-	err := tx.CreateTable("test", nil)
+	err := tx.Catalog.CreateTable(tx, "test", nil)
 	require.NoError(t, err)
-	tb, err := tx.GetTable("test")
+	tb, err := tx.Catalog.GetTable(tx, "test")
 	require.NoError(t, err)
 
 	return tb, fn
@@ -162,9 +155,9 @@ func TestTableInsert(t *testing.T) {
 		insertDoc := func(db *database.Database) (rawKey []byte) {
 			update(t, db, func(tx *database.Transaction) error {
 				// create table if not exists
-				_ = tx.CreateTable("test", nil)
+				_ = tx.Catalog.CreateTable(tx, "test", nil)
 
-				tb, err := tx.GetTable("test")
+				tb, err := tx.Catalog.GetTable(tx, "test")
 				require.NoError(t, err)
 
 				doc := newDocument()
@@ -179,10 +172,13 @@ func TestTableInsert(t *testing.T) {
 
 		key1 := insertDoc(db)
 
+		catalog := db.Catalog
 		// create new database object
 		db, err = database.New(context.Background(), ng, database.Options{
 			Codec: msgpack.NewCodec(),
 		})
+		db.Catalog = catalog
+
 		require.NoError(t, err)
 
 		key2 := insertDoc(db)
@@ -200,13 +196,13 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{Path: parsePath(t, "foo.a[1]"), Type: document.IntegerValue, IsPrimaryKey: true},
+				{Path: testutil.ParseDocumentPath(t, "foo.a[1]"), Type: document.IntegerValue, IsPrimaryKey: true},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		var doc document.FieldBuffer
@@ -231,14 +227,14 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{Path: parsePath(t, "foo"), Type: document.ArrayValue},
-				{Path: parsePath(t, "foo[0]"), Type: document.IntegerValue},
+				{Path: testutil.ParseDocumentPath(t, "foo"), Type: document.ArrayValue},
+				{Path: testutil.ParseDocumentPath(t, "foo[0]"), Type: document.IntegerValue},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		var doc document.FieldBuffer
@@ -252,7 +248,7 @@ func TestTableInsert(t *testing.T) {
 		d, err = tb.GetDocument(d.(document.Keyer).RawKey())
 		require.NoError(t, err)
 
-		v, err := parsePath(t, "foo[0]").GetValueFromDocument(d)
+		v, err := testutil.ParseDocumentPath(t, "foo[0]").GetValueFromDocument(d)
 		require.NoError(t, err)
 		require.Equal(t, document.NewIntegerValue(100), v)
 	})
@@ -261,13 +257,13 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{Path: parsePath(t, "foo"), Type: document.IntegerValue, IsPrimaryKey: true},
+				{Path: testutil.ParseDocumentPath(t, "foo"), Type: document.IntegerValue, IsPrimaryKey: true},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		tests := [][]byte{
@@ -291,17 +287,17 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", nil)
+		err := tx.Catalog.CreateTable(tx, "test", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateIndex(&database.IndexInfo{
-			IndexName: "idxFoo", TableName: "test", Paths: []document.Path{parsePath(t, "foo")},
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
+			IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
 		})
 		require.NoError(t, err)
-		idx, err := tx.GetIndex("idxFoo")
+		idx, err := tx.Catalog.GetIndex(tx, "idxFoo")
 		require.NoError(t, err)
 
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		// create one document with the foo field
@@ -318,7 +314,7 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 
 		var count int
-		err = idx.AscendGreaterOrEqual([]document.Value{document.Value{}}, func(val, k []byte) error {
+		err = idx.AscendGreaterOrEqual([]document.Value{{}}, func(val, k []byte) error {
 			switch count {
 			case 0:
 				// key2, which doesn't countain the field must appear first in the next,
@@ -338,14 +334,14 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), document.DocumentValue, false, false, false, document.Value{}, true, []document.Path{parsePath(t, "foo.bar")}},
-				{parsePath(t, "foo.bar"), document.IntegerValue, false, false, false, document.Value{}, true, []document.Path{parsePath(t, "foo")}},
+				{testutil.ParseDocumentPath(t, "foo"), document.DocumentValue, false, false, false, document.Value{}, true, []document.Path{testutil.ParseDocumentPath(t, "foo.bar")}},
+				{testutil.ParseDocumentPath(t, "foo.bar"), document.IntegerValue, false, false, false, document.Value{}, true, []document.Path{testutil.ParseDocumentPath(t, "foo")}},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		doc := document.NewFieldBuffer().
@@ -383,13 +379,13 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), document.DoubleValue, false, false, false, document.Value{}, false, nil},
+				{testutil.ParseDocumentPath(t, "foo"), document.DoubleValue, false, false, false, document.Value{}, false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test")
+		tb, err := tx.Catalog.GetTable(tx, "test")
 		require.NoError(t, err)
 
 		doc := document.NewFieldBuffer().
@@ -405,23 +401,23 @@ func TestTableInsert(t *testing.T) {
 		defer cleanup()
 
 		// no enforced type, not null
-		err := tx.CreateTable("test1", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test1", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), 0, false, true, false, document.Value{}, false, nil},
+				{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, document.Value{}, false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb1, err := tx.GetTable("test1")
+		tb1, err := tx.Catalog.GetTable(tx, "test1")
 		require.NoError(t, err)
 
 		// enforced type, not null
-		err = tx.CreateTable("test2", &database.TableInfo{
+		err = tx.Catalog.CreateTable(tx, "test2", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, true, false, document.Value{}, false, nil},
+				{testutil.ParseDocumentPath(t, "foo"), document.IntegerValue, false, true, false, document.Value{}, false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb2, err := tx.GetTable("test2")
+		tb2, err := tx.Catalog.GetTable(tx, "test2")
 		require.NoError(t, err)
 
 		// insert with empty foo field should fail
@@ -460,23 +456,23 @@ func TestTableInsert(t *testing.T) {
 		defer cleanup()
 
 		// no enforced type, not null
-		err := tx.CreateTable("test1", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test1", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), 0, false, true, false, document.NewIntegerValue(42), false, nil},
+				{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, document.NewIntegerValue(42), false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb1, err := tx.GetTable("test1")
+		tb1, err := tx.Catalog.GetTable(tx, "test1")
 		require.NoError(t, err)
 
 		// enforced type, not null
-		err = tx.CreateTable("test2", &database.TableInfo{
+		err = tx.Catalog.CreateTable(tx, "test2", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, true, false, document.NewIntegerValue(42), false, nil},
+				{testutil.ParseDocumentPath(t, "foo"), document.IntegerValue, false, true, false, document.NewIntegerValue(42), false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb2, err := tx.GetTable("test2")
+		tb2, err := tx.Catalog.GetTable(tx, "test2")
 		require.NoError(t, err)
 
 		// insert with empty foo field shouldn't fail
@@ -526,13 +522,13 @@ func TestTableInsert(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test1", &database.TableInfo{
+		err := tx.Catalog.CreateTable(tx, "test1", &database.TableInfo{
 			FieldConstraints: []*database.FieldConstraint{
-				{parsePath(t, "foo[1]"), 0, false, true, false, document.Value{}, false, nil},
+				{testutil.ParseDocumentPath(t, "foo[1]"), 0, false, true, false, document.Value{}, false, nil},
 			},
 		})
 		require.NoError(t, err)
-		tb, err := tx.GetTable("test1")
+		tb, err := tx.Catalog.GetTable(tx, "test1")
 		require.NoError(t, err)
 
 		// insert table with only one value
@@ -639,14 +635,14 @@ func TestTableReplace(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test1", nil)
+		err := tx.Catalog.CreateTable(tx, "test1", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateTable("test2", nil)
+		err = tx.Catalog.CreateTable(tx, "test2", nil)
 		require.NoError(t, err)
 
 		// simple indexes
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Paths:     []document.Path{document.NewPath("a")},
 			Unique:    true,
 			TableName: "test1",
@@ -655,7 +651,7 @@ func TestTableReplace(t *testing.T) {
 		require.NoError(t, err)
 
 		// composite indexes
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Paths:     []document.Path{document.NewPath("x"), document.NewPath("y")},
 			Unique:    true,
 			TableName: "test2",
@@ -663,7 +659,7 @@ func TestTableReplace(t *testing.T) {
 		})
 		require.NoError(t, err)
 
-		tb, err := tx.GetTable("test1")
+		tb, err := tx.Catalog.GetTable(tx, "test1")
 		require.NoError(t, err)
 
 		// insert two different documents
@@ -698,7 +694,7 @@ func TestTableReplace(t *testing.T) {
 		require.Equal(t, database.ErrDuplicateDocument, err)
 
 		// --- x, y
-		tb, err = tx.GetTable("test2")
+		tb, err = tx.Catalog.GetTable(tx, "test2")
 		require.NoError(t, err)
 		// insert two different documents
 		dc1, err := tb.Insert(testutil.MakeDocument(t, `{"x": 1, "y": 1, "z": 1}`))
@@ -771,7 +767,7 @@ func TestTableIndexes(t *testing.T) {
 		tb, cleanup := newTestTable(t)
 		defer cleanup()
 
-		m := tb.Indexes()
+		m := tb.Indexes
 		require.Empty(t, m)
 	})
 
@@ -779,45 +775,45 @@ func TestTableIndexes(t *testing.T) {
 		_, tx, cleanup := newTestTx(t)
 		defer cleanup()
 
-		err := tx.CreateTable("test1", nil)
+		err := tx.Catalog.CreateTable(tx, "test1", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateTable("test2", nil)
+		err = tx.Catalog.CreateTable(tx, "test2", nil)
 		require.NoError(t, err)
 
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Unique:    true,
 			IndexName: "idx1a",
 			TableName: "test1",
-			Paths:     []document.Path{parsePath(t, "a")},
+			Paths:     []document.Path{testutil.ParseDocumentPath(t, "a")},
 		})
 		require.NoError(t, err)
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Unique:    false,
 			IndexName: "idx1b",
 			TableName: "test1",
-			Paths:     []document.Path{parsePath(t, "b")},
+			Paths:     []document.Path{testutil.ParseDocumentPath(t, "b")},
 		})
 		require.NoError(t, err)
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Unique:    false,
 			IndexName: "idx1ab",
 			TableName: "test1",
-			Paths:     []document.Path{parsePath(t, "a"), parsePath(t, "b")},
+			Paths:     []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 		})
 		require.NoError(t, err)
-		err = tx.CreateIndex(&database.IndexInfo{
+		err = tx.Catalog.CreateIndex(tx, &database.IndexInfo{
 			Unique:    false,
 			IndexName: "idx2a",
 			TableName: "test2",
-			Paths:     []document.Path{parsePath(t, "a")},
+			Paths:     []document.Path{testutil.ParseDocumentPath(t, "a")},
 		})
 		require.NoError(t, err)
 
-		tb, err := tx.GetTable("test1")
+		tb, err := tx.Catalog.GetTable(tx, "test1")
 		require.NoError(t, err)
 
-		m := tb.Indexes()
+		m := tb.Indexes
 		require.NoError(t, err)
 		require.Len(t, m, 3)
 	})
diff --git a/open_test.go b/open_test.go
new file mode 100644
index 00000000..9adea98e
--- /dev/null
+++ b/open_test.go
@@ -0,0 +1,110 @@
+package genji_test
+
+import (
+	"errors"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/genjidb/genji"
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/testutil"
+	"github.com/stretchr/testify/require"
+)
+
+func TestOpen(t *testing.T) {
+	dir, err := ioutil.TempDir("", "genji")
+	require.NoError(t, err)
+	defer os.RemoveAll(dir)
+
+	db, err := genji.Open(filepath.Join(dir, "test.db"))
+	require.NoError(t, err)
+
+	err = db.Exec(`
+		CREATE TABLE tableA (a INTEGER NOT NULL, b.c[0].d DOUBLE UNIQUE PRIMARY KEY);
+		CREATE TABLE tableB (a TEXT NOT NULL DEFAULT 'hello', PRIMARY KEY (a));
+		CREATE TABLE tableC;
+
+		INSERT INTO tableB (a) VALUES (1)
+	`)
+	require.NoError(t, err)
+
+	err = db.Close()
+	require.NoError(t, err)
+
+	// ensure tables are loaded properly
+	db, err = genji.Open(filepath.Join(dir, "test.db"))
+	require.NoError(t, err)
+	defer db.Close()
+
+	res1, err := db.Query("SELECT * FROM __genji_tables")
+	require.NoError(t, err)
+	defer res1.Close()
+
+	var count int
+	err = res1.Iterate(func(d document.Document) error {
+		count++
+		if count == 1 {
+			fb := testutil.MakeDocument(t, `{
+				"sql": "CREATE TABLE tableA (a INTEGER NOT NULL, b.c[0].d DOUBLE PRIMARY KEY)",
+				"table_name": "tableA"
+			}`).(*document.FieldBuffer)
+
+			fb.Add("store_name", document.NewBlobValue([]byte{116, 1}))
+			testutil.RequireDocEqual(t, fb, d)
+			return nil
+		}
+
+		if count == 2 {
+			fb := testutil.MakeDocument(t, `{
+				"sql": "CREATE TABLE tableB (a TEXT NOT NULL DEFAULT \"hello\" PRIMARY KEY)",
+				"table_name": "tableB"
+			}`).(*document.FieldBuffer)
+
+			fb.Add("store_name", document.NewBlobValue([]byte{116, 2}))
+			testutil.RequireDocEqual(t, fb, d)
+			return nil
+		}
+
+		if count == 3 {
+			fb := testutil.MakeDocument(t, `{
+				"sql": "CREATE TABLE tableC",
+				"table_name": "tableC"
+			}`).(*document.FieldBuffer)
+
+			fb.Add("store_name", document.NewBlobValue([]byte{116, 3}))
+			testutil.RequireDocEqual(t, fb, d)
+			return nil
+		}
+		return errors.New("more than 3 tables")
+	})
+	require.NoError(t, err)
+
+	res2, err := db.Query("SELECT * FROM __genji_indexes")
+	require.NoError(t, err)
+	defer res2.Close()
+
+	count = 0
+	err = res2.Iterate(func(d document.Document) error {
+		count++
+		if count == 1 {
+			fb := testutil.MakeDocument(t, `{
+				"sql": "CREATE INDEX __genji_autoindex_tableA_1 ON tableA (b.c[0].d)",
+				"index_name": "__genji_autoindex_tableA_1",
+				"table_name": "tableA"
+			}`).(*document.FieldBuffer)
+
+			testutil.RequireDocEqual(t, fb, d)
+			return nil
+		}
+
+		return errors.New("more than 1 index")
+	})
+	require.NoError(t, err)
+
+	d, err := db.QueryDocument("SELECT * FROM tableB")
+	require.NoError(t, err)
+
+	testutil.RequireDocJSONEq(t, d, `{"a": "1"}`)
+}
diff --git a/query/create_test.go b/query/create_test.go
index 6e522ae5..e31b6725 100644
--- a/query/create_test.go
+++ b/query/create_test.go
@@ -55,7 +55,7 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				_, err := tx.GetTable("test")
+				_, err := tx.Catalog.GetTable(tx.Transaction, "test")
 				return err
 			})
 			require.NoError(t, err)
@@ -72,17 +72,15 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				tb, err := tx.GetTable("test")
+				tb, err := tx.Catalog.GetTable(tx.Transaction, "test")
 				if err != nil {
 					return err
 				}
 
-				info := tb.Info()
-
 				require.Equal(t, database.FieldConstraints{
 					{Path: parsePath(t, "d"), Type: document.DoubleValue},
 					{Path: parsePath(t, "b"), Type: document.BoolValue},
-				}, info.FieldConstraints)
+				}, tb.Info.FieldConstraints)
 				return nil
 			})
 			require.NoError(t, err)
@@ -98,11 +96,10 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				tb, err := tx.GetTable("test1")
+				tb, err := tx.Catalog.GetTable(tx.Transaction, "test1")
 				if err != nil {
 					return err
 				}
-				info := tb.Info()
 
 				require.Equal(t, database.FieldConstraints{
 					{Path: parsePath(t, "foo"), Type: document.DocumentValue, IsInferred: true,
@@ -145,7 +142,7 @@ func TestCreateTable(t *testing.T) {
 					{Path: parsePath(t, "t"), Type: document.TextValue},
 					{Path: parsePath(t, "a"), Type: document.ArrayValue},
 					{Path: parsePath(t, "d"), Type: document.DocumentValue},
-				}, info.FieldConstraints)
+				}, tb.Info.FieldConstraints)
 				return nil
 			})
 			require.NoError(t, err)
@@ -162,11 +159,10 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				tb, err := tx.GetTable("test2")
+				tb, err := tx.Catalog.GetTable(tx.Transaction, "test2")
 				if err != nil {
 					return err
 				}
-				info := tb.Info()
 
 				require.Equal(t, database.FieldConstraints{{Path: parsePath(t, "foo"), Type: document.DocumentValue, IsInferred: true,
 					InferredBy: []document.Path{
@@ -211,7 +207,7 @@ func TestCreateTable(t *testing.T) {
 					{Path: parsePath(t, "eight"), Type: document.IntegerValue},
 					{Path: parsePath(t, "ii"), Type: document.IntegerValue},
 					{Path: parsePath(t, "c"), Type: document.TextValue},
-				}, info.FieldConstraints)
+				}, tb.Info.FieldConstraints)
 				return nil
 			})
 			require.NoError(t, err)
@@ -244,10 +240,9 @@ func TestCreateTable(t *testing.T) {
 					require.NoError(t, err)
 
 					err = db.View(func(tx *genji.Tx) error {
-						tb, err := tx.GetTable("test")
-						info := tb.Info()
+						tb, err := tx.Catalog.GetTable(tx.Transaction, "test")
 
-						require.Equal(t, test.constraints, info.FieldConstraints)
+						require.Equal(t, test.constraints, tb.Info.FieldConstraints)
 						return err
 					})
 					require.NoError(t, err)
@@ -264,39 +259,38 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				tb, err := tx.GetTable("test")
+				tb, err := tx.Catalog.GetTable(tx.Transaction, "test")
 				require.NoError(t, err)
-				info := tb.Info()
-				require.Len(t, info.FieldConstraints, 3)
+				require.Len(t, tb.Info.FieldConstraints, 3)
 
 				require.Equal(t, &database.FieldConstraint{
 					Path:     parsePath(t, "a"),
 					Type:     document.IntegerValue,
 					IsUnique: true,
-				}, info.FieldConstraints[0])
+				}, tb.Info.FieldConstraints[0])
 
 				require.Equal(t, &database.FieldConstraint{
 					Path:     parsePath(t, "b"),
 					Type:     document.DoubleValue,
 					IsUnique: true,
-				}, info.FieldConstraints[1])
+				}, tb.Info.FieldConstraints[1])
 
 				require.Equal(t, &database.FieldConstraint{
 					Path:     parsePath(t, "c"),
 					IsUnique: true,
-				}, info.FieldConstraints[2])
+				}, tb.Info.FieldConstraints[2])
 
-				idx, err := tx.GetIndex("__genji_autoindex_test_1")
+				idx, err := tx.Catalog.GetIndex(tx.Transaction, "__genji_autoindex_test_1")
 				require.NoError(t, err)
 				require.Equal(t, document.IntegerValue, idx.Info.Types[0])
 				require.True(t, idx.Info.Unique)
 
-				idx, err = tx.GetIndex("__genji_autoindex_test_2")
+				idx, err = tx.Catalog.GetIndex(tx.Transaction, "__genji_autoindex_test_2")
 				require.NoError(t, err)
 				require.Equal(t, document.DoubleValue, idx.Info.Types[0])
 				require.True(t, idx.Info.Unique)
 
-				idx, err = tx.GetIndex("__genji_autoindex_test_3")
+				idx, err = tx.Catalog.GetIndex(tx.Transaction, "__genji_autoindex_test_3")
 				require.NoError(t, err)
 				require.Zero(t, idx.Info.Types[0])
 				require.True(t, idx.Info.Unique)
diff --git a/query/drop_test.go b/query/drop_test.go
index a5b3b7bb..6db6a02b 100644
--- a/query/drop_test.go
+++ b/query/drop_test.go
@@ -66,7 +66,7 @@ func TestDropIndex(t *testing.T) {
 	// Assert that the good index has been dropped.
 	var indexes []string
 	err = db.View(func(tx *genji.Tx) error {
-		indexes = tx.ListIndexes()
+		indexes = tx.Catalog.ListIndexes("")
 		return nil
 	})
 	require.NoError(t, err)
diff --git a/planner/explain_test.go b/query/explain_test.go
similarity index 99%
rename from planner/explain_test.go
rename to query/explain_test.go
index 32c839ef..0e2cf86f 100644
--- a/planner/explain_test.go
+++ b/query/explain_test.go
@@ -1,4 +1,4 @@
-package planner_test
+package query_test
 
 import (
 	"testing"
diff --git a/query/reindex_test.go b/query/reindex_test.go
index a4a0721e..2ed29b4e 100644
--- a/query/reindex_test.go
+++ b/query/reindex_test.go
@@ -43,7 +43,7 @@ func TestReIndex(t *testing.T) {
 
 			// truncate all indexes
 			err = db.Update(func(tx *genji.Tx) error {
-				c := tx.DB().Catalog()
+				c := tx.Catalog
 				for _, idxName := range c.ListIndexes("") {
 					idx, err := c.GetIndex(tx.Transaction, idxName)
 					if err != nil {
@@ -68,8 +68,8 @@ func TestReIndex(t *testing.T) {
 			require.NoError(t, err)
 
 			err = db.View(func(tx *genji.Tx) error {
-				for _, idxName := range tx.ListIndexes() {
-					idx, err := tx.GetIndex(idxName)
+				for _, idxName := range tx.Catalog.ListIndexes("") {
+					idx, err := tx.Catalog.GetIndex(tx.Transaction, idxName)
 					require.NoError(t, err)
 
 					shouldBeIndexed := false
diff --git a/sql/parser/create_test.go b/sql/parser/create_test.go
index f40ef8dd..10fc31f6 100644
--- a/sql/parser/create_test.go
+++ b/sql/parser/create_test.go
@@ -18,13 +18,13 @@ func TestParserCreateTable(t *testing.T) {
 		expected query.Statement
 		errored  bool
 	}{
-		{"Basic", "CREATE TABLE test", query.CreateTableStmt{TableName: "test"}, false},
-		{"If not exists", "CREATE TABLE IF NOT EXISTS test", query.CreateTableStmt{TableName: "test", IfNotExists: true}, false},
+		{"Basic", "CREATE TABLE test", query.CreateTableStmt{Info: database.TableInfo{TableName: "test"}}, false},
+		{"If not exists", "CREATE TABLE IF NOT EXISTS test", query.CreateTableStmt{Info: database.TableInfo{TableName: "test"}, IfNotExists: true}, false},
 		{"Path only", "CREATE TABLE test(a)", query.CreateTableStmt{}, true},
 		{"With primary key", "CREATE TABLE test(foo INTEGER PRIMARY KEY)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsPrimaryKey: true},
 					},
@@ -34,8 +34,8 @@ func TestParserCreateTable(t *testing.T) {
 			query.CreateTableStmt{}, true},
 		{"With type", "CREATE TABLE test(foo INTEGER)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue},
 					},
@@ -43,8 +43,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With not null", "CREATE TABLE test(foo NOT NULL)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), IsNotNull: true},
 					},
@@ -52,8 +52,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With default", "CREATE TABLE test(foo DEFAULT \"10\")",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), DefaultValue: document.NewTextValue("10")},
 					},
@@ -61,8 +61,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With unique", "CREATE TABLE test(foo UNIQUE)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), IsUnique: true},
 					},
@@ -76,8 +76,8 @@ func TestParserCreateTable(t *testing.T) {
 			query.CreateTableStmt{}, true},
 		{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsNotNull: true},
 					},
@@ -85,8 +85,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With not null and primary key", "CREATE TABLE test(foo INTEGER NOT NULL PRIMARY KEY)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsPrimaryKey: true, IsNotNull: true},
 					},
@@ -94,8 +94,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With primary key and not null", "CREATE TABLE test(foo INTEGER PRIMARY KEY NOT NULL)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsPrimaryKey: true, IsNotNull: true},
 					},
@@ -103,8 +103,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With multiple constraints", "CREATE TABLE test(foo INTEGER PRIMARY KEY, bar INTEGER NOT NULL, baz[4][1].bat TEXT)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsPrimaryKey: true},
 						{Path: document.Path(testutil.ParsePath(t, "bar")), Type: document.IntegerValue, IsNotNull: true},
@@ -114,8 +114,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With table constraints / PK on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, PRIMARY KEY (foo))",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsPrimaryKey: true},
 						{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
@@ -124,8 +124,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With table constraints / PK on undefined field", "CREATE TABLE test(foo INTEGER, PRIMARY KEY (bar))",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue},
 						{Path: document.Path(testutil.ParsePath(t, "bar")), IsPrimaryKey: true},
@@ -137,8 +137,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With table constraints / duplicate pk on same path", "CREATE TABLE test(foo INTEGER PRIMARY KEY, PRIMARY KEY (foo))", nil, true},
 		{"With table constraints / UNIQUE on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, UNIQUE (foo))",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsUnique: true},
 						{Path: document.Path(testutil.ParsePath(t, "bar")), IsNotNull: true},
@@ -147,8 +147,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With table constraints / UNIQUE on undefined field", "CREATE TABLE test(foo INTEGER, UNIQUE (bar))",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue},
 						{Path: document.Path(testutil.ParsePath(t, "bar")), IsUnique: true},
@@ -157,8 +157,8 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With table constraints / UNIQUE twice", "CREATE TABLE test(foo INTEGER UNIQUE, UNIQUE (foo))",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "foo")), Type: document.IntegerValue, IsUnique: true},
 					},
@@ -170,8 +170,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With all supported fixed size data types",
 			"CREATE TABLE test(d double, b bool)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "d")), Type: document.DoubleValue},
 						{Path: document.Path(testutil.ParsePath(t, "b")), Type: document.BoolValue},
@@ -181,8 +181,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With all supported variable size data types",
 			"CREATE TABLE test(i integer, b blob, byt bytes, t text, a array, d document)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "i")), Type: document.IntegerValue},
 						{Path: document.Path(testutil.ParsePath(t, "b")), Type: document.BlobValue},
@@ -196,8 +196,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With integer aliases types",
 			"CREATE TABLE test(i int, ii int2, ei int8, m mediumint, s smallint, b bigint, t tinyint)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "i")), Type: document.IntegerValue},
 						{Path: document.Path(testutil.ParsePath(t, "ii")), Type: document.IntegerValue},
@@ -212,8 +212,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With double aliases types",
 			"CREATE TABLE test(dp DOUBLE PRECISION, r real, d double)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "dp")), Type: document.DoubleValue},
 						{Path: document.Path(testutil.ParsePath(t, "r")), Type: document.DoubleValue},
@@ -224,8 +224,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With text aliases types",
 			"CREATE TABLE test(v VARCHAR(255), c CHARACTER(64), t TEXT)",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "v")), Type: document.TextValue},
 						{Path: document.Path(testutil.ParsePath(t, "c")), Type: document.TextValue},
@@ -236,8 +236,8 @@ func TestParserCreateTable(t *testing.T) {
 		{"With errored text aliases types",
 			"CREATE TABLE test(v VARCHAR(1 IN [1, 2, 3] AND foo > 4) )",
 			query.CreateTableStmt{
-				TableName: "test",
 				Info: database.TableInfo{
+					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
 						{Path: document.Path(testutil.ParsePath(t, "v")), Type: document.TextValue},
 					},
@@ -266,18 +266,30 @@ func TestParserCreateIndex(t *testing.T) {
 		expected query.Statement
 		errored  bool
 	}{
-		{"Basic", "CREATE INDEX idx ON test (foo)", query.CreateIndexStmt{IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo"))}}, false},
-		{"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo.bar[1])", query.CreateIndexStmt{IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo.bar[1]"))}, IfNotExists: true}, false},
-		{"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo[3].baz)", query.CreateIndexStmt{IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, IfNotExists: true, Unique: true}, false},
-		{"No name", "CREATE UNIQUE INDEX ON test (foo[3].baz)", query.CreateIndexStmt{TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, Unique: true}, false},
+		{"Basic", "CREATE INDEX idx ON test (foo)", query.CreateIndexStmt{
+			Info: database.IndexInfo{
+				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo"))},
+			}}, false},
+		{"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo.bar[1])", query.CreateIndexStmt{
+			Info: database.IndexInfo{
+				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo.bar[1]"))},
+			}, IfNotExists: true}, false},
+		{"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo[3].baz)", query.CreateIndexStmt{
+			Info: database.IndexInfo{
+				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, Unique: true,
+			}, IfNotExists: true}, false},
+		{"No name", "CREATE UNIQUE INDEX ON test (foo[3].baz)", query.CreateIndexStmt{
+			Info: database.IndexInfo{TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, Unique: true}}, false},
 		{"No name with IF NOT EXISTS", "CREATE UNIQUE INDEX IF NOT EXISTS ON test (foo[3].baz)", nil, true},
 		{"More than 1 path", "CREATE INDEX idx ON test (foo, bar)",
 			query.CreateIndexStmt(query.CreateIndexStmt{
-				IndexName: "idx",
-				TableName: "test",
-				Paths: []document.Path{
-					document.Path(testutil.ParsePath(t, "foo")),
-					document.Path(testutil.ParsePath(t, "bar")),
+				Info: database.IndexInfo{
+					IndexName: "idx",
+					TableName: "test",
+					Paths: []document.Path{
+						document.Path(testutil.ParsePath(t, "foo")),
+						document.Path(testutil.ParsePath(t, "bar")),
+					},
 				},
 			}),
 			false},
diff --git a/sql/parser/delete_test.go b/sql/parser/delete_test.go
index ff3c6031..70a18b8f 100644
--- a/sql/parser/delete_test.go
+++ b/sql/parser/delete_test.go
@@ -3,7 +3,7 @@ package parser_test
 import (
 	"testing"
 
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/stream"
 	"github.com/stretchr/testify/require"
@@ -54,7 +54,7 @@ func TestParserDelete(t *testing.T) {
 			q, err := parser.ParseQuery(test.s)
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			require.EqualValues(t, &planner.Statement{Stream: test.expected}, q.Statements[0])
+			require.EqualValues(t, &query.StreamStmt{Stream: test.expected}, q.Statements[0])
 		})
 	}
 }
diff --git a/sql/parser/explain_test.go b/sql/parser/explain_test.go
index 06cbd8b5..acace92e 100644
--- a/sql/parser/explain_test.go
+++ b/sql/parser/explain_test.go
@@ -3,7 +3,7 @@ package parser_test
 import (
 	"testing"
 
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/stretchr/testify/require"
@@ -16,7 +16,7 @@ func TestParserExplain(t *testing.T) {
 		expected query.Statement
 		errored  bool
 	}{
-		{"Explain create table", "EXPLAIN CREATE TABLE test", &planner.ExplainStmt{Statement: query.CreateTableStmt{TableName: "test"}}, false},
+		{"Explain create table", "EXPLAIN CREATE TABLE test", &query.ExplainStmt{Statement: query.CreateTableStmt{Info: database.TableInfo{TableName: "test"}}}, false},
 		{"Multiple Explains", "EXPLAIN EXPLAIN CREATE TABLE test", nil, true},
 	}
 
diff --git a/sql/parser/insert_test.go b/sql/parser/insert_test.go
index 9f1674df..7574c66f 100644
--- a/sql/parser/insert_test.go
+++ b/sql/parser/insert_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/testutil"
@@ -135,7 +135,7 @@ func TestParserInsert(t *testing.T) {
 			}
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			stmt := q.Statements[0].(*planner.Statement)
+			stmt := q.Statements[0].(*query.StreamStmt)
 			require.False(t, stmt.ReadOnly)
 			require.EqualValues(t, test.expected.String(), stmt.Stream.String())
 		})
diff --git a/sql/parser/parser_test.go b/sql/parser/parser_test.go
index 024185fe..e2d459ee 100644
--- a/sql/parser/parser_test.go
+++ b/sql/parser/parser_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
 	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/stream"
@@ -20,11 +19,11 @@ func TestParserMultiStatement(t *testing.T) {
 	}{
 		{"OnlyCommas", ";;;", nil},
 		{"TrailingComma", "SELECT * FROM foo;;;DELETE FROM foo;", []query.Statement{
-			&planner.Statement{
+			&query.StreamStmt{
 				Stream:   stream.New(stream.SeqScan("foo")).Pipe(stream.Project(expr.Wildcard{})),
 				ReadOnly: true,
 			},
-			&planner.Statement{
+			&query.StreamStmt{
 				Stream: stream.New(stream.SeqScan("foo")).Pipe(stream.TableDelete("foo")),
 			},
 		}},
diff --git a/sql/parser/select_test.go b/sql/parser/select_test.go
index 6499d2db..3fb070ff 100644
--- a/sql/parser/select_test.go
+++ b/sql/parser/select_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/genjidb/genji/expr"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/testutil"
@@ -148,7 +148,7 @@ func TestParserSelect(t *testing.T) {
 			if !test.mustFail {
 				require.NoError(t, err)
 				require.Len(t, q.Statements, 1)
-				require.EqualValues(t, &planner.Statement{Stream: test.expected, ReadOnly: true}, q.Statements[0])
+				require.EqualValues(t, &query.StreamStmt{Stream: test.expected, ReadOnly: true}, q.Statements[0])
 			} else {
 				require.Error(t, err)
 			}
diff --git a/sql/parser/update_test.go b/sql/parser/update_test.go
index eed5c6fc..a2104929 100644
--- a/sql/parser/update_test.go
+++ b/sql/parser/update_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/planner"
+	"github.com/genjidb/genji/query"
 	"github.com/genjidb/genji/sql/parser"
 	"github.com/genjidb/genji/stream"
 	"github.com/genjidb/genji/testutil"
@@ -75,7 +75,7 @@ func TestParserUpdate(t *testing.T) {
 
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			stmt := q.Statements[0].(*planner.Statement)
+			stmt := q.Statements[0].(*query.StreamStmt)
 			require.False(t, stmt.ReadOnly)
 			require.EqualValues(t, test.expected, stmt.Stream)
 		})
diff --git a/stream/operator_test.go b/stream/operator_test.go
index c12def4a..039a16ce 100644
--- a/stream/operator_test.go
+++ b/stream/operator_test.go
@@ -436,7 +436,7 @@ func TestTableReplace(t *testing.T) {
 			require.NoError(t, err)
 			defer tx.Rollback()
 
-			tb, err := tx.GetTable("test")
+			tb, err := tx.Catalog.GetTable(tx.Transaction, "test")
 			require.NoError(t, err)
 
 			for i, doc := range test.docsInTable {
@@ -531,7 +531,7 @@ func TestTableDelete(t *testing.T) {
 			var env expr.Environment
 			env.Tx = tx.Transaction
 
-			tb, err := tx.GetTable("test")
+			tb, err := tx.Catalog.GetTable(tx.Transaction, "test")
 			require.NoError(t, err)
 			kk, err := test.in.GetByField("a")
 			require.NoError(t, err)
diff --git a/stream/stream_test.go b/stream/stream_test.go
index e91358f1..c4c98abd 100644
--- a/stream/stream_test.go
+++ b/stream/stream_test.go
@@ -31,4 +31,7 @@ func TestStream(t *testing.T) {
 	})
 	require.NoError(t, err)
 	require.Equal(t, int64(1), count)
+
+	clone := s.Clone()
+	require.Equal(t, s, clone)
 }
diff --git a/testutil/db.go b/testutil/db.go
new file mode 100644
index 00000000..cc6dab6b
--- /dev/null
+++ b/testutil/db.go
@@ -0,0 +1,38 @@
+package testutil
+
+import (
+	"context"
+	"testing"
+
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document/encoding/msgpack"
+	"github.com/genjidb/genji/engine/memoryengine"
+	"github.com/stretchr/testify/require"
+)
+
+func NewTestDB(t testing.TB) (*database.Database, func()) {
+	t.Helper()
+
+	db, err := database.New(context.Background(), memoryengine.NewEngine(), database.Options{
+		Codec: msgpack.NewCodec(),
+	})
+	require.NoError(t, err)
+
+	return db, func() {
+		db.Close()
+	}
+}
+
+func NewTestTx(t testing.TB) (*database.Database, *database.Transaction, func()) {
+	t.Helper()
+
+	db, cleanup := NewTestDB(t)
+
+	tx, err := db.Begin(true)
+	require.NoError(t, err)
+
+	return db, tx, func() {
+		tx.Rollback()
+		cleanup()
+	}
+}
diff --git a/testutil/document.go b/testutil/document.go
index ba7cb853..21cfe459 100644
--- a/testutil/document.go
+++ b/testutil/document.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/stringutil"
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/require"
 )
 
@@ -145,6 +146,6 @@ func RequireDocEqual(t testing.TB, d1, d2 document.Document) {
 	ok, err := l.IsEqual(r)
 	require.NoError(t, err)
 	if !ok {
-		require.Equal(t, l, r)
+		t.Fatal(cmp.Diff(transformDoc(d1), transformDoc(d2)))
 	}
 }
diff --git a/testutil/expr.go b/testutil/expr.go
index a5b4d652..f269e37a 100644
--- a/testutil/expr.go
+++ b/testutil/expr.go
@@ -56,9 +56,15 @@ func ArrayValue(a document.Array) expr.LiteralValue {
 func ParsePath(t testing.TB, p string) expr.Path {
 	t.Helper()
 
+	return expr.Path(ParseDocumentPath(t, p))
+}
+
+func ParseDocumentPath(t testing.TB, p string) document.Path {
+	t.Helper()
+
 	vp, err := parser.ParsePath(p)
 	require.NoError(t, err)
-	return expr.Path(vp)
+	return vp
 }
 
 func ParseNamedExpr(t testing.TB, s string, name ...string) expr.Expr {
diff --git a/testutil/index.go b/testutil/index.go
index 85578979..55d78028 100644
--- a/testutil/index.go
+++ b/testutil/index.go
@@ -12,7 +12,7 @@ import (
 func GetIndexContent(t testing.TB, tx *database.Transaction, indexName string) []KV {
 	t.Helper()
 
-	idx, err := tx.GetIndex(indexName)
+	idx, err := tx.Catalog.GetIndex(tx, indexName)
 	require.NoError(t, err)
 
 	var content []KV