Repository: genjidb/genji
Pull request #241 (2020-10-08 14:44:46)

Add LIKE operator support
Adds LIKE operator support.
Implementation is based on CockroachDB implementation. 

Updates #52.

codecov-io: # [Codecov](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=h1) Report
> Merging [#241](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=desc) into [master](https://codecov.io/gh/genjidb/genji/commit/dfeafd1f5354bfe2a587ea276c1a61a9c4eaa61b?el=desc) will **decrease** coverage by `0.02%`.
> The diff coverage is `59.34%`.

[![Impacted file tree graph](https://codecov.io/gh/genjidb/genji/pull/241/graphs/tree.svg?width=650&height=150&src=pr&token=nwR1GpvtyH)](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #241      +/-   ##
==========================================
- Coverage   61.34%   61.31%   -0.03%     
==========================================
  Files          68       69       +1     
  Lines        6327     6405      +78     
==========================================
+ Hits         3881     3927      +46     
- Misses       1933     1964      +31     
- Partials      513      514       +1     
```


| [Impacted Files](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sql/parser/expr.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-c3FsL3BhcnNlci9leHByLmdv) | `77.13% <0.00%> (-1.44%)` | :arrow_down: |
| [sql/scanner/token.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-c3FsL3NjYW5uZXIvdG9rZW4uZ28=) | `22.22% <0.00%> (ø)` | |
| [sql/query/expr/comparison.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-c3FsL3F1ZXJ5L2V4cHIvY29tcGFyaXNvbi5nbw==) | `25.19% <50.00%> (-1.24%)` | :arrow_down: |
| [sql/query/expr/like.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-c3FsL3F1ZXJ5L2V4cHIvbGlrZS5nbw==) | `65.71% <65.71%> (ø)` | |
| [sql/query/expr/expr.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-c3FsL3F1ZXJ5L2V4cHIvZXhwci5nbw==) | `33.33% <77.77%> (+14.81%)` | :arrow_up: |
| [engine/memoryengine/store.go](https://codecov.io/gh/genjidb/genji/pull/241/diff?src=pr&el=tree#diff-ZW5naW5lL21lbW9yeWVuZ2luZS9zdG9yZS5nbw==) | `91.96% <0.00%> (-1.79%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=footer). Last update [dfeafd1...95e675d](https://codecov.io/gh/genjidb/genji/pull/241?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.travis.yml b/.travis.yml
index 40872885..c6ce1360 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,8 +18,8 @@ script:
   - make gen
   - go test -mod vendor -race -cover -coverprofile=coverage.txt -covermode=atomic -timeout=2m ./...
   - go test -mod vendor -race -cover -timeout=2m -tags=tinygo ./...
-  - cd ./cmd/genji && go test -race && cd -
-  - cd ./engine/badgerengine && go test -race && cd -
+  - cd ./cmd/genji && go test -race ./... && cd -
+  - cd ./engine/badgerengine && go test -race ./... && cd -
 
 
 after_success:
diff --git a/README.md b/README.md
index 8c39d0cc..4805bf96 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,8 @@
 [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/genjidb/genji)
 [![Slack channel](https://img.shields.io/badge/slack-join%20chat-green.svg)](https://gophers.slack.com/messages/CKPCYQFE0)
 
+![Fuzz](https://github.com/genjidb/genji/workflows/Fuzz/badge.svg)
+
 Genji is a schemaless database that allows running SQL queries on documents.
 
 Checkout the [SQL documentation](https://genji.dev/docs/genji-sql), the [Go doc](https://pkg.go.dev/github.com/genjidb/genji) and the [usage example](#usage) in the README to get started quickly.
diff --git a/cmd/genji/go.mod b/cmd/genji/go.mod
index 73fdfa77..72dd9068 100644
--- a/cmd/genji/go.mod
+++ b/cmd/genji/go.mod
@@ -10,6 +10,8 @@ require (
 	github.com/genjidb/genji/engine/badgerengine v0.9.0
 	github.com/stretchr/testify v1.6.1
 	github.com/urfave/cli/v2 v2.2.0
+	go.uber.org/multierr v1.6.0
+	golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520
 )
 
 replace (
diff --git a/cmd/genji/go.sum b/cmd/genji/go.sum
index 9984a0db..c66df2f7 100644
--- a/cmd/genji/go.sum
+++ b/cmd/genji/go.sum
@@ -24,10 +24,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/badger/v2 v2.0.3/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM=
 github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k=
 github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
-github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA=
 github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
@@ -37,10 +35,6 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8
 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/genjidb/genji v0.8.0 h1:yDxDF8rfwDMVSI1OPZUQzUl8+9QflTym87sUXr9hw3Y=
-github.com/genjidb/genji v0.8.0/go.mod h1:7MhLPBD74B2Z9+L0xwoKbfkS6Dba/PHcZp+vpjoZiyY=
-github.com/genjidb/genji/engine/badgerengine v0.8.0 h1:cYNTxOUTRQo5cRpLVrA8yS/OLv958ahsQatXdvjG9dk=
-github.com/genjidb/genji/engine/badgerengine v0.8.0/go.mod h1:1Rs9U2ahauFJu2ByWDlR34Pa+urEElGzT1hbVo8FEaA=
 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/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
@@ -95,6 +89,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
@@ -110,13 +105,20 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 h1:Bx6FllMpG4NWDOfhMBz1VR2QYNp/SAOHPIAsaVmxfPo=
+golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/cmd/genji/main.go b/cmd/genji/main.go
index 53342b18..c251ead8 100644
--- a/cmd/genji/main.go
+++ b/cmd/genji/main.go
@@ -3,6 +3,7 @@ package main
 import (
 	"fmt"
 	"os"
+	"runtime/debug"
 
 	"github.com/genjidb/genji/cmd/genji/shell"
 	"github.com/urfave/cli/v2"
@@ -86,6 +87,30 @@ $ curl https://api.github.com/repos/genjidb/genji/issues | genji insert --db my.
 				return runInsertCommand(c.Context, engine, dbPath, table, c.Bool("auto"), args)
 			},
 		},
+		{
+			Name:  "version",
+			Usage: "Shows Genji and Genji CLI version",
+			Action: func(c *cli.Context) error {
+				var cliVersion, genjiVersion string
+				info, ok := debug.ReadBuildInfo()
+
+				if !ok {
+					fmt.Println(`version not available in GOPATH mode; use "go get" with Go modules enabled`)
+					return nil
+				}
+
+				cliVersion = info.Main.Version
+				for _, mod := range info.Deps {
+					if mod.Path != "github.com/genjidb/genji" {
+						continue
+					}
+					genjiVersion = mod.Version
+					break
+				}
+				fmt.Printf("Genji %v\nGenji CLI %v\n", genjiVersion, cliVersion)
+				return nil
+			},
+		},
 	}
 
 	// Root command
@@ -112,7 +137,7 @@ $ curl https://api.github.com/repos/genjidb/genji/issues | genji insert --db my.
 			engine = "badger"
 		}
 
-		return shell.Run(&shell.Options{
+		return shell.Run(c.Context, &shell.Options{
 			Engine: engine,
 			DBPath: dbpath,
 		})
diff --git a/cmd/genji/shell/command.go b/cmd/genji/shell/command.go
index 3334b716..cce41386 100644
--- a/cmd/genji/shell/command.go
+++ b/cmd/genji/shell/command.go
@@ -3,6 +3,7 @@ package shell
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -16,13 +17,39 @@ import (
 var commands = []struct {
 	Name        string
 	Options     string
+	DisplayName string
 	Description string
+	Aliases     []string
 }{
-	{".exit", ``, "Exit this program."},
-	{".help", ``, "List all commands."},
-	{".tables", ``, "List names of tables."},
-	{".indexes", `[table_name]`, "Display all indexes or the indexes of the given table name."},
-	{".dump", `[table_name]`, "Dump database content or table content as SQL statements"},
+	{
+		Name:        ".exit",
+		DisplayName: ".exit or exit",
+		Description: "Exit this program.",
+		Aliases:     []string{"exit"},
+	},
+	{
+		Name:        ".help",
+		DisplayName: ".help or help",
+		Description: "List all commands.",
+		Aliases:     []string{"help"},
+	},
+	{
+		Name:        ".tables",
+		DisplayName: ".tables",
+		Description: "List names of tables.",
+	},
+	{
+		Name:        ".indexes",
+		Options:     "[table_name]",
+		DisplayName: ".indexes",
+		Description: "Display all indexes or the indexes of the given table name.",
+	},
+	{
+		Name:        ".dump",
+		Options:     "[table_name]",
+		DisplayName: ".dump",
+		Description: "Dump database content or table content as SQL statements.",
+	},
 }
 
 // runTablesCmd shows all tables.
@@ -52,24 +79,34 @@ func runTablesCmd(db *genji.DB, cmd []string) error {
 
 // displayTableIndex prints all indexes that the given table contains.
 func displayTableIndex(db *genji.DB, tableName string) error {
-	ctx := context.Background()
-
-	res, err := db.Query(ctx, "SELECT * FROM __genji_indexes WHERE table_name = ?", tableName)
-	if err != nil {
-		return err
-	}
-	defer res.Close()
+	return db.View(func(tx *genji.Tx) error {
+		ctx := context.Background()
+		_, err := tx.QueryDocument(ctx, "SELECT table_name FROM __genji_tables WHERE table_name = ?", tableName)
+		if err != nil {
+			if err == database.ErrDocumentNotFound {
+				return fmt.Errorf("%w: %q", database.ErrTableNotFound, tableName)
+			}
 
-	return res.Iterate(func(d document.Document) error {
-		var index database.IndexConfig
+			return err
+		}
 
-		if err := index.ScanDocument(d); err != nil {
+		res, err := tx.Query(ctx, "SELECT * FROM __genji_indexes WHERE table_name = ?", tableName)
+		if err != nil {
 			return err
 		}
+		defer res.Close()
 
-		fmt.Printf("%s ON %s (%s)\n", index.IndexName, index.TableName, index.Path)
+		return res.Iterate(func(d document.Document) error {
+			var index database.IndexConfig
 
-		return nil
+			if err := index.ScanDocument(d); err != nil {
+				return err
+			}
+
+			fmt.Printf("%s ON %s (%s)\n", index.IndexName, index.TableName, index.Path)
+
+			return nil
+		})
 	})
 }
 
@@ -116,22 +153,32 @@ func runHelpCmd() error {
 	for _, c := range commands {
 		// spaces indentation for readability.
 		spaces := 25
-		indent := spaces - len(c.Name) - len(c.Options)
-		fmt.Printf("%s %s %*s %s\n", c.Name, c.Options, indent, "", c.Description)
+		indent := spaces - len(c.DisplayName) - len(c.Options)
+		fmt.Printf("%s %s %*s %s\n", c.DisplayName, c.Options, indent, "", c.Description)
 	}
 
 	return nil
 }
 
+func shouldDisplaySuggestion(name, in string) bool {
+	// input should be at least half the command size to get a suggestion.
+	d := levenshtein.ComputeDistance(name, in)
+	return d < (len(name) / 2)
+}
+
 // displaySuggestions shows suggestions.
 func displaySuggestions(in string) error {
 	var suggestions []string
 	for _, c := range commands {
-		d := levenshtein.ComputeDistance(c.Name, in)
-		// input should be at least half the command size to get a suggestion.
-		if d < (len(c.Name) / 2) {
+		if shouldDisplaySuggestion(c.Name, in) {
 			suggestions = append(suggestions, c.Name)
 		}
+
+		for _, alias := range c.Aliases {
+			if shouldDisplaySuggestion(alias, in) {
+				suggestions = append(suggestions, alias)
+			}
+		}
 	}
 
 	if len(suggestions) == 0 {
@@ -267,21 +314,21 @@ func runDumpCmd(db *genji.DB, tables []string, w io.Writer) error {
 
 	for i, table := range tables {
 		err = dumpTable(tx, table, w)
-		switch err {
-		case nil:
-			// Blank separation between tables.
-			if i > 0 {
-				if _, err := fmt.Fprintln(w, ""); err != nil {
-					return err
-				}
+		if err != nil {
+			// If table doesn’t exist we skip it.
+			if errors.Is(err, database.ErrTableNotFound) {
+				continue
 			}
-
-		case database.ErrTableNotFound: // If table doesn't exist we skip it.
-			continue
-		default:
 			_, err = fmt.Fprintln(w, "COMMIT;")
 			return err
 		}
+		
+		// Blank separation between tables.
+		if i > 0 {
+			if _, err := fmt.Fprintln(w, ""); err != nil {
+				return err
+			}
+		}
 	}
 
 	// tables slice argument is not empty, all args tables has been displayed.
@@ -316,11 +363,7 @@ func runDumpCmd(db *genji.DB, tables []string, w io.Writer) error {
 			return err
 		}
 
-		if err := dumpTable(tx, tableName, w); err != nil {
-			return err
-		}
-
-		return nil
+		return dumpTable(tx, tableName, w)
 	})
 	if err != nil {
 		_, err = fmt.Fprintln(w, "ROLLBACK;")
diff --git a/cmd/genji/shell/shell.go b/cmd/genji/shell/shell.go
index f10bce76..6bce276f 100644
--- a/cmd/genji/shell/shell.go
+++ b/cmd/genji/shell/shell.go
@@ -4,11 +4,15 @@ import (
 	"bufio"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"os"
+	"os/signal"
 	"path/filepath"
 	"strings"
+	"sync"
+	"syscall"
 
 	"github.com/c-bata/go-prompt"
 	"github.com/dgraph-io/badger/v2"
@@ -19,12 +23,23 @@ import (
 	"github.com/genjidb/genji/engine/boltengine"
 	"github.com/genjidb/genji/engine/memoryengine"
 	"github.com/genjidb/genji/sql/parser"
+	"go.uber.org/multierr"
+	"golang.org/x/sync/errgroup"
 )
 
 const (
 	historyFilename = ".genji_history"
 )
 
+var (
+	// error returned when the exit command is executed
+	errExitCommand = errors.New("exit command")
+	// error returned when the program received a termination signal
+	errExitSignal = errors.New("termination signal received")
+	// error returned when the prompt reads a ctrl d input
+	errExitCtrlD = errors.New("ctrl-d")
+)
+
 // A Shell manages a command line shell program for manipulating a Genji database.
 type Shell struct {
 	db   *genji.DB
@@ -37,6 +52,13 @@ type Shell struct {
 	history []string
 
 	cmdSuggestions []prompt.Suggest
+
+	// context used for execution cancelation,
+	// these must not be used manually.
+	// Use getExecContext and cancelExecContext instead.
+	execContext  context.Context
+	execCancelFn func()
+	mu           sync.Mutex
 }
 
 // Options of the shell.
@@ -77,12 +99,12 @@ func stdinFromTerminal() bool {
 }
 
 // Run a shell.
-func Run(opts *Options) error {
+func Run(ctx context.Context, opts *Options) (err error) {
 	if opts == nil {
 		opts = new(Options)
 	}
 
-	err := opts.validate()
+	err = opts.validate()
 	if err != nil {
 		return err
 	}
@@ -103,25 +125,144 @@ func Run(opts *Options) error {
 		fmt.Println("Enter \".help\" for usage hints.")
 	}
 
-	sh.loadCommandSuggestions()
-	history, err := sh.loadHistory()
+	ran, err := sh.runPipedInput(ctx)
 	if err != nil {
 		return err
 	}
+	if ran {
+		return nil
+	}
+
+	defer func() {
+		if sh.db != nil {
+			closeErr := sh.db.Close()
+			if closeErr != nil {
+				err = multierr.Append(err, closeErr)
+			}
+		}
+	}()
+
+	defer func() {
+		dumpErr := sh.dumpHistory()
+		if dumpErr != nil {
+			err = multierr.Append(err, dumpErr)
+		}
+	}()
+
+	promptExecCh := make(chan string)
+
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	g, ctx := errgroup.WithContext(ctx)
+
+	g.Go(func() error {
+		return sh.runSignalHandlers(ctx)
+	})
+
+	g.Go(func() error {
+		return sh.runExecutor(ctx, promptExecCh)
+	})
+
+	// Because go-prompt doesn't handle cancelation
+	// it is impossible to ask it to stop when the prompt.Input function
+	// is running.
+	// We run it in a non-managed goroutine with no graceful shutdown
+	// so that if the prompt.Input function is running when we want to quit the program
+	// we simply don't wait for this goroutine to end.
+	// This goroutine must not manage any resource.
+	go func() {
+		defer cancel()
+
+		err := sh.runPrompt(ctx, promptExecCh)
+		if err != nil && err != errExitCtrlD {
+			fmt.Fprintf(os.Stderr, err.Error())
+		}
+	}()
+
+	err = g.Wait()
+	if err == errExitCommand || err == errExitSignal || err == context.Canceled {
+		return nil
+	}
+
+	return err
+}
 
-	ran, err := sh.runPipedInput()
+// runSignalHandlers handles two different signals.
+// On SIGINT, it cancels any query execution using sh.cancelExecution.
+// On SIGTERM, it triggers graceful shutdown by returning errExitSignal.
+func (sh *Shell) runSignalHandlers(ctx context.Context) error {
+	interruptC := make(chan os.Signal, 1)
+	termC := make(chan os.Signal, 1)
+	signal.Notify(interruptC, os.Interrupt)
+	signal.Notify(termC, syscall.SIGTERM)
+
+	for {
+		select {
+		case <-interruptC:
+			sh.cancelExecution()
+		case <-termC:
+			fmt.Fprintf(os.Stderr, "\nTermination signal received. Quitting...\n")
+			return errExitSignal
+		case <-ctx.Done():
+			return ctx.Err()
+		}
+	}
+}
+
+// runExecutor manages execution. It reads user input from promptExecCh, executes any
+// command or query and writes back an empty string to that channel once it's done.
+func (sh *Shell) runExecutor(ctx context.Context, promptExecCh chan string) error {
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case input := <-promptExecCh:
+			err := sh.executeInput(sh.getExecContext(ctx), input)
+			// if the context has been canceled
+			// there is no way to tell at this point
+			// if this is because of a user interruption
+			// or a termination signal.
+			// If it's the latter, it will be detected by the Select statement.
+			if err == context.Canceled {
+				// Print a newline for cleanliness
+				fmt.Println()
+				continue
+			}
+			if err == errExitCommand {
+				return err
+			}
+			if err != nil {
+				fmt.Println(err)
+			}
+		case promptExecCh <- "":
+		}
+	}
+}
+
+// runPrompt is a stateless function that displays a prompt to the user.
+// User input is sent to the execCh channel which must deal with parsing and error handling.
+// Once the execution of the user input is done by the reader of the channel, it must
+// send a string back to execCh so that this function will display another prompt.
+func (sh *Shell) runPrompt(ctx context.Context, execCh chan (string)) error {
+	sh.loadCommandSuggestions()
+	history, err := sh.loadHistory()
 	if err != nil {
 		return err
 	}
-	if ran {
-		return nil
-	}
+
+	// we store the last key stroke to
+	// determine if ctrl D was pressed by the user.
+	var lastKeyStroke prompt.Key
 
 	promptOpts := []prompt.Option{
 		prompt.OptionPrefix("genji> "),
-		prompt.OptionTitle("genji"),
+		prompt.OptionTitle("Genji"),
 		prompt.OptionLivePrefix(sh.changelivePrefix),
 		prompt.OptionHistory(history),
+		prompt.OptionBreakLineCallback(func(d *prompt.Document) {
+			lastKeyStroke = d.LastKeyStroke()
+		}),
 	}
 
 	// If NO_COLOR env var is present, disable color. See https://no-color.org
@@ -147,28 +288,74 @@ func Run(opts *Options) error {
 		}
 	}
 
-	e := prompt.New(
-		sh.execute,
-		sh.completer,
-		promptOpts...,
-	)
+	pt := prompt.New(func(in string) {}, sh.completer, append(promptOpts, prompt.OptionHistory(history))...)
 
-	e.Run()
+	for {
+		// Input() captures ctrl D and ctrl C.
+		// It never returns when ctrl C is pressed but does on CTRL D
+		// under specific conditions.
+		input := pt.Input()
 
-	if sh.db != nil {
-		err = sh.db.Close()
-		if err != nil {
-			return err
+		// go-prompt ignores ctrl D it if it was pressed while the line is not empty.
+		// However, it returns if the line is empty and sets lastKeyStroke to prompt.ControlD.
+		// if so, we must stop the program.
+		if lastKeyStroke == prompt.ControlD {
+			return errExitCtrlD
 		}
+
+		input = strings.TrimSpace(input)
+
+		if len(input) == 0 {
+			continue
+		}
+
+		// deleguate execution to the sh.runExecutor goroutine
+		execCh <- input
+		// and wait for it to finish to display another prompt.
+		<-execCh
 	}
+}
 
-	return sh.dumpHistory()
+// cancelExecution must be called to cancel any ongoing execution without
+// stopping the program.
+// Calling this function when there is no ongoing execution is a no-op.
+func (sh *Shell) cancelExecution() {
+	sh.mu.Lock()
+	defer sh.mu.Unlock()
+
+	if sh.execCancelFn != nil {
+		sh.execCancelFn()
+		sh.execContext = nil
+		sh.execCancelFn = nil
+	}
+}
+
+// getExecContext returns the current cancelable execution context
+// or creates one if needed.
+func (sh *Shell) getExecContext(ctx context.Context) context.Context {
+	sh.mu.Lock()
+	defer sh.mu.Unlock()
+
+	if sh.execContext != nil {
+		return sh.execContext
+	}
+
+	sh.execContext, sh.execCancelFn = context.WithCancel(ctx)
+	return sh.execContext
 }
 
 func (sh *Shell) loadCommandSuggestions() {
-	suggestions := make([]prompt.Suggest, len(commands))
-	for i, c := range commands {
-		suggestions[i].Text = c.Name
+	suggestions := make([]prompt.Suggest, 0, len(commands))
+	for _, c := range commands {
+		suggestions = append(suggestions, prompt.Suggest{
+			Text: c.Name,
+		})
+
+		for _, alias := range c.Aliases {
+			suggestions = append(suggestions, prompt.Suggest{
+				Text: alias,
+			})
+		}
 	}
 	sh.cmdSuggestions = suggestions
 }
@@ -232,28 +419,23 @@ func (sh *Shell) dumpHistory() error {
 	return w.Flush()
 }
 
-func (sh *Shell) execute(in string) {
+// executeInput stores user input in the history and executes it.
+func (sh *Shell) executeInput(ctx context.Context, in string) error {
 	sh.history = append(sh.history, in)
 
-	err := sh.executeInput(in)
-	if err != nil {
-		fmt.Println(err)
-	}
-}
-
-func (sh *Shell) executeInput(in string) error {
-	in = strings.TrimSpace(in)
 	switch {
 	// if it starts with a "." it's a command
+	// if the input is "help" or "exit", then it's a command.
 	// it must not be in the middle of a multi line query though
-	case strings.HasPrefix(in, "."):
-		return sh.runCommand(in)
+	case !sh.multiLine && strings.HasPrefix(in, "."), in == "help", in == "exit":
+		return sh.runCommand(ctx, in)
+
 	// If it ends with a ";" we can run a query
 	case strings.HasSuffix(in, ";"):
 		sh.query = sh.query + in
 		sh.multiLine = false
 		sh.livePrefix = in
-		err := sh.runQuery(sh.query)
+		err := sh.runQuery(ctx, sh.query)
 		sh.query = ""
 		return err
 	// If the input is empty we ignore it
@@ -271,11 +453,11 @@ func (sh *Shell) executeInput(in string) error {
 	return nil
 }
 
-func (sh *Shell) runCommand(in string) error {
+func (sh *Shell) runCommand(ctx context.Context, in string) error {
 	in = strings.TrimSuffix(in, ";")
 	cmd := strings.Fields(in)
 	switch cmd[0] {
-	case ".help":
+	case ".help", "help":
 		return runHelpCmd()
 	case ".tables":
 		db, err := sh.getDB()
@@ -284,12 +466,12 @@ func (sh *Shell) runCommand(in string) error {
 		}
 
 		return runTablesCmd(db, cmd)
-	case ".exit":
+	case ".exit", "exit":
 		if len(cmd) > 1 {
 			return fmt.Errorf("usage: .exit")
 		}
 
-		sh.exit()
+		return errExitCommand
 	case ".indexes":
 		db, err := sh.getDB()
 		if err != nil {
@@ -306,17 +488,15 @@ func (sh *Shell) runCommand(in string) error {
 	default:
 		return displaySuggestions(in)
 	}
-
-	return fmt.Errorf("unknown command %q", cmd)
 }
 
-func (sh *Shell) runQuery(q string) error {
+func (sh *Shell) runQuery(ctx context.Context, q string) error {
 	db, err := sh.getDB()
 	if err != nil {
 		return err
 	}
 
-	res, err := db.Query(context.Background(), q)
+	res, err := db.Query(ctx, q)
 	if err != nil {
 		return err
 	}
@@ -327,24 +507,14 @@ func (sh *Shell) runQuery(q string) error {
 	enc.SetEscapeHTML(false)
 	enc.SetIndent("", "  ")
 	return res.Iterate(func(d document.Document) error {
-		return enc.Encode(d)
-	})
-}
-
-func (sh *Shell) exit() {
-	if sh.db != nil {
-		err := sh.db.Close()
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+		select {
+		case <-ctx.Done():
+			return errors.New("interrupted")
+		default:
 		}
-	}
 
-	err := sh.dumpHistory()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
-	}
-
-	os.Exit(0)
+		return enc.Encode(d)
+	})
 }
 
 func (sh *Shell) getDB() (*genji.DB, error) {
@@ -364,20 +534,18 @@ func (sh *Shell) getDB() (*genji.DB, error) {
 		ng, err = badgerengine.NewEngine(badger.DefaultOptions(sh.opts.DBPath).WithLogger(nil))
 	}
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v\n", err)
-		os.Exit(2)
+		return nil, err
 	}
 
 	sh.db, err = genji.New(ng)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "%v\n", err)
-		os.Exit(2)
+		return nil, err
 	}
 
 	return sh.db, nil
 }
 
-func (sh *Shell) runPipedInput() (ran bool, err error) {
+func (sh *Shell) runPipedInput(ctx context.Context) (ran bool, err error) {
 	// Check if there is any input being piped in from the terminal
 	stat, _ := os.Stdin.Stat()
 	m := stat.Mode()
@@ -389,7 +557,7 @@ func (sh *Shell) runPipedInput() (ran bool, err error) {
 	if err != nil {
 		return true, fmt.Errorf("Unable to read piped input: %w", err)
 	}
-	err = sh.runQuery(string(data))
+	err = sh.runQuery(ctx, string(data))
 	if err != nil {
 		return true, fmt.Errorf("Unable to execute provided sql statements: %w", err)
 	}
@@ -457,33 +625,31 @@ func (sh *Shell) getAllTables() ([]string, error) {
 }
 
 func (sh *Shell) completer(in prompt.Document) []prompt.Suggest {
-	if strings.HasPrefix(in.Text, ".") {
-		return prompt.FilterHasPrefix(sh.cmdSuggestions, in.Text, true)
-	}
+	suggestions := prompt.FilterHasPrefix(sh.cmdSuggestions, in.Text, true)
 
 	_, err := parser.NewParser(strings.NewReader(in.Text)).ParseQuery(context.Background())
 	if err != nil {
 		e, ok := err.(*parser.ParseError)
-		if !ok {
-			return []prompt.Suggest{}
+		if !ok || len(e.Expected) < 1 {
+			return suggestions
 		}
 		expected := e.Expected
 		switch expected[0] {
 		case "table_name":
 			expected, err = sh.getAllTables()
 			if err != nil {
-				return []prompt.Suggest{}
+				return suggestions
 			}
 		case "index_name":
 			expected, err = sh.getAllIndexes()
 			if err != nil {
-				return []prompt.Suggest{}
+				return suggestions
 			}
 		}
-
-		suggestions := make([]prompt.Suggest, len(expected))
-		for i, e := range expected {
-			suggestions[i].Text = e
+		for _, e := range expected {
+			suggestions = append(suggestions, prompt.Suggest{
+				Text: e,
+			})
 		}
 
 		w := in.GetWordBeforeCursor()
diff --git a/database/config.go b/database/config.go
index 636e0627..54fb025c 100644
--- a/database/config.go
+++ b/database/config.go
@@ -3,6 +3,7 @@ package database
 import (
 	"bytes"
 	"encoding/binary"
+	"fmt"
 	"sync"
 
 	"github.com/genjidb/genji/document"
@@ -18,6 +19,11 @@ type FieldConstraint struct {
 	Type         document.ValueType
 	IsPrimaryKey bool
 	IsNotNull    bool
+	DefaultValue document.Value
+}
+
+func (f *FieldConstraint) HasDefaultValue() bool {
+	return f.DefaultValue.Type != 0
 }
 
 // ToDocument returns a document from f.
@@ -28,6 +34,9 @@ func (f *FieldConstraint) ToDocument() document.Document {
 	buf.Add("type", document.NewIntegerValue(int64(f.Type)))
 	buf.Add("is_primary_key", document.NewBoolValue(f.IsPrimaryKey))
 	buf.Add("is_not_null", document.NewBoolValue(f.IsNotNull))
+	if f.HasDefaultValue() {
+		buf.Add("default_value", f.DefaultValue)
+	}
 	return buf
 }
 
@@ -60,6 +69,15 @@ func (f *FieldConstraint) ScanDocument(d document.Document) error {
 		return err
 	}
 	f.IsNotNull = v.V.(bool)
+
+	v, err = d.GetByField("default_value")
+	if err != nil && err != document.ErrFieldNotFound {
+		return err
+	}
+	if err == nil {
+		f.DefaultValue = v
+	}
+
 	return nil
 }
 
@@ -230,11 +248,11 @@ func (t *tableInfoStore) Get(tx *Transaction, tableName string) (*TableInfo, err
 
 	info, ok := t.tableInfos[tableName]
 	if !ok {
-		return nil, ErrTableNotFound
+		return nil, fmt.Errorf("%w: %q", ErrTableNotFound, tableName)
 	}
 
 	if info.transactionID != 0 && info.transactionID != tx.id {
-		return nil, ErrTableNotFound
+		return nil, fmt.Errorf("%w: %q", ErrTableNotFound, tableName)
 	}
 
 	return &info, nil
@@ -246,11 +264,11 @@ func (t *tableInfoStore) Delete(tx *Transaction, tableName string) error {
 
 	info, ok := t.tableInfos[tableName]
 	if !ok {
-		return ErrTableNotFound
+		return fmt.Errorf("%w: %q", ErrTableNotFound, tableName)
 	}
 
 	if info.transactionID != 0 && info.transactionID != tx.id {
-		return ErrTableNotFound
+		return fmt.Errorf("%w: %q", ErrTableNotFound, tableName)
 	}
 
 	st, err := tx.tx.GetStore([]byte(tableInfoStoreName))
@@ -261,7 +279,7 @@ func (t *tableInfoStore) Delete(tx *Transaction, tableName string) error {
 	key := []byte(tableName)
 	err = st.Delete(key)
 	if err == engine.ErrKeyNotFound {
-		return ErrTableNotFound
+		return fmt.Errorf("%w: %q", ErrTableNotFound, tableName)
 	}
 	if err != nil {
 		return err
@@ -272,6 +290,47 @@ func (t *tableInfoStore) Delete(tx *Transaction, tableName string) error {
 	return nil
 }
 
+// modifyTable modifies TableInfo using given callback.
+func (t *tableInfoStore) modifyTable(tx *Transaction, tableName string, f func(*TableInfo) error) error {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+
+	info, ok := t.tableInfos[tableName]
+	if !ok {
+		return ErrTableNotFound
+	}
+
+	if info.transactionID != 0 && info.transactionID != tx.id {
+		return ErrTableNotFound
+	}
+
+	err := f(&info)
+	if err != nil {
+		return err
+	}
+
+	st, err := tx.tx.GetStore([]byte(tableInfoStoreName))
+	if err != nil {
+		return err
+	}
+
+	var buf bytes.Buffer
+	err = t.db.Codec.NewEncoder(&buf).EncodeDocument(info.ToDocument())
+	if err != nil {
+		return err
+	}
+
+	err = st.Put([]byte(tableName), buf.Bytes())
+	if err != nil {
+		return err
+	}
+
+	info.transactionID = tx.id
+	t.tableInfos[tableName] = info
+
+	return nil
+}
+
 func (t *tableInfoStore) loadAllTableInfo(tx engine.Transaction) error {
 	t.mu.Lock()
 	defer t.mu.Unlock()
@@ -319,7 +378,7 @@ func (t *tableInfoStore) loadAllTableInfo(tx engine.Transaction) error {
 
 	t.tableInfos[indexStoreName] = TableInfo{
 		storeName: []byte(indexStoreName),
-		readOnly: true,
+		readOnly:  true,
 		FieldConstraints: []FieldConstraint{
 			{
 				Path: document.ValuePath{
diff --git a/database/table.go b/database/table.go
index 6c7924d5..2552d0a9 100644
--- a/database/table.go
+++ b/database/table.go
@@ -466,7 +466,11 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if field.FieldName == "" {
 			// if the field is not found we make sure it is not required
 			if c.IsNotNull {
-				return fmt.Errorf("field %q is required and must be not null", c.Path)
+				if !c.HasDefaultValue() {
+					return fmt.Errorf("field %q is required and must be not null", c.Path)
+				}
+
+				return buf.Set(c.Path, c.DefaultValue)
 			}
 			return nil
 		}
@@ -476,9 +480,12 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if err != nil {
 			if err == document.ErrFieldNotFound {
 				if c.IsNotNull {
-					return fmt.Errorf("field %q is required and must be not null", c.Path)
-				}
+					if !c.HasDefaultValue() {
+						return fmt.Errorf("field %q is required and must be not null", c.Path)
+					}
 
+					return buf.Set(c.Path, c.DefaultValue)
+				}
 				return nil
 			}
 
@@ -507,7 +514,7 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		}
 	case document.ArrayValue:
 		// if it's an array, we can assume it's a ValueBuffer
-		buf := parent.V.(*document.ValueBuffer)
+		buf := parent.V.(document.ValueBuffer)
 
 		frag := c.Path[len(c.Path)-1]
 		if frag.FieldName != "" {
@@ -521,9 +528,12 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
 		if err != nil {
 			if err == document.ErrValueNotFound {
 				if c.IsNotNull {
-					return fmt.Errorf("value %q is required and must be not null", c.Path)
-				}
+					if !c.HasDefaultValue() {
+						return fmt.Errorf("field %q is required and must be not null", c.Path)
+					}
 
+					return buf.Copy(c.DefaultValue.V.(document.Array))
+				}
 				return nil
 			}
 
diff --git a/database/transaction.go b/database/transaction.go
index a4c851cb..e77bb79f 100644
--- a/database/transaction.go
+++ b/database/transaction.go
@@ -128,6 +128,25 @@ func (tx *Transaction) GetTable(name string) (*Table, error) {
 	}, nil
 }
 
+func (tx *Transaction) AddField(name string, fc FieldConstraint) error {
+	return tx.tableInfoStore.modifyTable(tx, name, func(info *TableInfo) error {
+		for _, field := range info.FieldConstraints {
+			if field.Path.IsEqual(fc.Path) {
+				return fmt.Errorf("field %q already exists", fc.Path.String())
+			}
+			if field.IsPrimaryKey && fc.IsPrimaryKey {
+				return fmt.Errorf(
+					"multiple primary keys are not allowed (%q is primary key)",
+					field.Path.String(),
+				)
+			}
+		}
+
+		info.FieldConstraints = append(info.FieldConstraints, fc)
+		return nil
+	})
+}
+
 // RenameTable renames a table.
 // If it doesn't exist, it returns ErrTableNotFound.
 func (tx *Transaction) RenameTable(oldName, newName string) error {
diff --git a/document/array.go b/document/array.go
index 58bf0683..b1ee610b 100644
--- a/document/array.go
+++ b/document/array.go
@@ -68,6 +68,11 @@ type ValueBuffer []Value
 
 // NewValueBuffer creates a buffer of values.
 func NewValueBuffer(values ...Value) ValueBuffer {
+	if len(values) == 0 {
+		// If called with no values return a non-nil slice.
+		return ValueBuffer{}
+	}
+
 	return ValueBuffer(values)
 }
 
@@ -141,7 +146,7 @@ func (vb *ValueBuffer) Copy(a Array) error {
 				return err
 			}
 
-			err = vb.Replace(i, NewArrayValue(&buf))
+			err = vb.Replace(i, NewArrayValue(buf))
 			if err != nil {
 				return err
 			}
diff --git a/document/create.go b/document/create.go
index ce4014b9..07a3e1e5 100644
--- a/document/create.go
+++ b/document/create.go
@@ -9,16 +9,43 @@ import (
 	"reflect"
 	"strings"
 	"time"
+
+	"github.com/buger/jsonparser"
 )
 
-// NewFromJSON creates a document from a JSON object.
-func NewFromJSON(data []byte) (Document, error) {
-	var fb FieldBuffer
-	err := fb.UnmarshalJSON(data)
+// NewFromJSON creates a document from raw JSON data.
+// The returned document will lazily decode the data.
+// If data is not a valid json object, calls to Iterate or GetByField will
+// return an error.
+func NewFromJSON(data []byte) Document {
+	return &jsonEncodedDocument{data}
+}
+
+type jsonEncodedDocument struct {
+	data []byte
+}
+
+func (j jsonEncodedDocument) Iterate(fn func(field string, value Value) error) error {
+	return jsonparser.ObjectEach(j.data, func(key, value []byte, dataType jsonparser.ValueType, offset int) error {
+		v, err := parseJSONValue(dataType, value)
+		if err != nil {
+			return err
+		}
+
+		return fn(string(key), v)
+	})
+}
+
+func (j jsonEncodedDocument) GetByField(field string) (Value, error) {
+	v, dt, _, err := jsonparser.Get(j.data, field)
+	if dt == jsonparser.NotExist {
+		return Value{}, ErrFieldNotFound
+	}
 	if err != nil {
-		return nil, err
+		return Value{}, err
 	}
-	return &fb, nil
+
+	return parseJSONValue(dt, v)
 }
 
 // NewFromMap creates a document from a map.
diff --git a/document/document.go b/document/document.go
index 648a32d4..7b9c930e 100644
--- a/document/document.go
+++ b/document/document.go
@@ -290,7 +290,7 @@ func (fb *FieldBuffer) Copy(d Document) error {
 				return err
 			}
 
-			fb.fields[i].Value = NewArrayValue(&buf)
+			fb.fields[i].Value = NewArrayValue(buf)
 		}
 	}
 
diff --git a/document/value.go b/document/value.go
index 47e9aefb..6d920468 100644
--- a/document/value.go
+++ b/document/value.go
@@ -258,7 +258,17 @@ func (v Value) MarshalJSON() ([]byte, error) {
 			}
 		}
 
-		return strconv.AppendFloat(nil, v.V.(float64), fmt, -1, 64), nil
+		// By default the precision is -1 to use the smallest number of digits.
+		// See https://pkg.go.dev/strconv#FormatFloat
+		prec := -1
+
+		// If fractional floating-point is 0, the precision should be 1 to add a trailing .0
+		_, frac := math.Modf(f)
+		if frac == 0 {
+			prec = 1
+		}
+
+		return strconv.AppendFloat(nil, v.V.(float64), fmt, prec, 64), nil
 	case TextValue:
 		return []byte(strconv.Quote(v.V.(string))), nil
 	case BlobValue:
@@ -483,12 +493,13 @@ func calculateFloats(a, b Value, operator byte) (res Value, err error) {
 
 		return NewDoubleValue(xa / xb), nil
 	case '%':
-		if xb == 0 {
+		mod := math.Mod(xa, xb)
+
+		if math.IsNaN(mod) {
 			return NewNullValue(), nil
 		}
 
-		ia, ib := int64(xa), int64(xb)
-		return NewDoubleValue(float64(ia % ib)), nil
+		return NewDoubleValue(mod), nil
 	case '&':
 		ia, ib := int64(xa), int64(xb)
 		return NewIntegerValue(ia & ib), nil
diff --git a/fuzz/README.md b/fuzz/README.md
new file mode 100644
index 00000000..abb6ad8a
--- /dev/null
+++ b/fuzz/README.md
@@ -0,0 +1,38 @@
+# Fuzz Tests
+
+Fuzzing Genji with [go-fuzz](https://github.com/dvyukov/go-fuzz).
+
+## Quick Start
+
+1. Install [go-fuzz](https://github.com/dvyukov/go-fuzz)
+   ```
+   go install github.com/dvyukov/go-fuzz/go-fuzz-build github.com/dvyukov/go-fuzz/go-fuzz
+   ```
+
+2. Download the initial corpus
+   ```
+   git clone https://github.com/genjidb/go-fuzz-corpus testdata/fuzz
+   ```
+
+3. Build the test program with necessary instrumentation
+   ```
+   go-fuzz-build -func FuzzParseQuery
+   ```
+   This will produce fuzz-fuzz.zip archive.
+
+4. Run the fuzzer.
+   ```
+   go-fuzz -workdir=testdata/fuzz/FuzzParseQuery
+   ```
+   The results will be written to `testdata/fuzz/FuzzParseQuery`.
+   Note that go-fuzz runs forever until manually stopped.
+
+   Example output:
+   ```
+   …
+   2020/10/14 12:03:58 workers: 2, corpus: 166 (13s ago), crashers: 2, restarts: 1/9663, execs: 1526845 (16418/sec), cover: 924, uptime: 1m33s
+   …
+   ```
+   Here, `corpus` is number of interesting inputs the fuzzer has discovered, and `crashers` is number of discovered bugs (check out `testdata/fuzz/FuzzParseQuery/crashers` dir).
+
+See [README file from go-fuzz](https://github.com/dvyukov/go-fuzz/blob/master/README.md) for more information.
diff --git a/fuzz/go.mod b/fuzz/go.mod
new file mode 100644
index 00000000..142824e3
--- /dev/null
+++ b/fuzz/go.mod
@@ -0,0 +1,13 @@
+module github.com/genjidb/genji/fuzz
+
+go 1.15
+
+require (
+	github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b
+	github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
+	github.com/genjidb/genji v0.9.0
+	github.com/stephens2424/writerset v1.0.2 // indirect
+	golang.org/x/tools v0.0.0-20201013053347-2db1cd791039 // indirect
+)
+
+replace github.com/genjidb/genji v0.9.0 => ../
diff --git a/fuzz/go.sum b/fuzz/go.sum
new file mode 100644
index 00000000..f0fc14ce
--- /dev/null
+++ b/fuzz/go.sum
@@ -0,0 +1,77 @@
+github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
+github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A=
+github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b h1:CXfDl9Y3NKuhOSxF9kXhiLmuYCdufQDrLY2fO1BzqBU=
+github.com/dvyukov/go-fuzz v0.0.0-20201003075337-90825f39c90b/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
+github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
+github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
+github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8=
+github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
+github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk=
+github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI=
+github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
+github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
+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.0.0-20201013053347-2db1cd791039 h1:kLBxO4OPBgPwjg8Vvu+/0DCHIfDwYIGNFcD66NU9kpo=
+golang.org/x/tools v0.0.0-20201013053347-2db1cd791039/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
+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-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.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/fuzz/sqlparser.go b/fuzz/sqlparser.go
new file mode 100644
index 00000000..989907dd
--- /dev/null
+++ b/fuzz/sqlparser.go
@@ -0,0 +1,21 @@
+package fuzz
+
+import (
+	"context"
+	"strings"
+
+	"github.com/genjidb/genji/sql/parser"
+)
+
+func FuzzParseQuery(data []byte) int {
+	var b strings.Builder
+	b.Write(data)
+	q, err := parser.ParseQuery(context.Background(), b.String())
+	if err != nil {
+		return 0
+	}
+	if len(q.Statements) != 0 {
+		return 1
+	}
+	return 0
+}
diff --git a/fuzz/tools.go b/fuzz/tools.go
new file mode 100644
index 00000000..a3675daf
--- /dev/null
+++ b/fuzz/tools.go
@@ -0,0 +1,10 @@
+// +build tools
+
+package fuzz
+
+// Makes `go mod tidy` keep tool dependencies in go.mod and go.sum.
+
+import (
+	_ "github.com/dvyukov/go-fuzz/go-fuzz"
+	_ "github.com/dvyukov/go-fuzz/go-fuzz-build"
+)
diff --git a/rfcs/001_context.md b/rfcs/001_context.md
new file mode 100644
index 00000000..2695ad91
--- /dev/null
+++ b/rfcs/001_context.md
@@ -0,0 +1,123 @@
+- Feature name: `Cancelable operations and use of Context`
+- Date: 2020-10-10
+- Author: Asdine El Hrychy
+- Related discussions: [Issue #224](https://github.com/genjidb/genji/issues/224), [PR #231](https://github.com/genjidb/genji/pull/231)
+
+## General approach
+
+I would like to avoid context pollution as much as possible, but without restricting user flexibility. It is important to be able to control and cancel operations but I also believe that there are times where we don't care about that.
+I want Genji to be simple to use and intuitive, and for that, I prefer to expand a bit more the surface API, rather than making the existing ones more complete and complex.
+The purpose of this RFC is not to describe in detail how each operation should be canceled but rather which APIs should expect a context parameter.
+
+## Genji package and database lifecycle
+
+### Open and New
+
+Opening a database relies on IO and should be cancelable, but after that operation is completed the database handle itself is long-lived and should not be cancelable, but closable with the `Close()` method.
+However, most of the time, we don't care about how long it takes to open a database.
+
+We currently have two ways of opening a database:
+
+- `genji.Open`
+- `genji.New`
+
+`Open` is just a specialized way of opening a database which calls `New`. It is by nature incomplete:
+
+- It only allows opening an in-memory database or a Bolt database
+- It doesn't take any option
+
+```go
+db, err := genji.Open("foo.db")
+if err != nil {
+    return err
+}
+defer db.Close()
+```
+
+It is very simple to write and to use, it must remain as-is.
+
+If users want more power, they must use the `genji.New` function.
+
+Opening a database can take time, even for Bolt or Badger, because accessing disks is unbounded (slow disks, NFS, etc.). It makes sense to have this a cancelable operation.
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
+...
+
+// ctx here is only used to control how long the engine creation can take
+// as engines might wait for IO
+ng := someengine.New(ctx, ...)
+// but not all of them
+ng := memoryengine.New()
+
+// at startup, Genji performs some operations on the engine, this should be cancelable
+db, err := genji.New(ctx, ng)
+```
+
+### genji.DB methods
+
+By default, database methods must not expect a context. This avoids context pollution when cancelation is not necessary.
+However, when it is necessary, a new handle can be created to use a specific context, using a new method named `WithContext`.
+
+```go
+// non-cancelable queries
+db.Exec("INSERT INTO foo(a) VALUES (1)")
+db.Query("SELECT 1")
+db.QueryDocument("SELECT 1")
+
+ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
+
+// WithContext returns a cancelable database handle
+db.WithContext(ctx).Exec("INSERT INTO foo (a) VALUES (1)")
+
+dbx := db.WithContext(ctx)
+
+// any called dbx method must return an error if the context is canceled.
+dbx.Query("SELECT 1")
+dbx.Begin(true)
+```
+
+## Engines
+
+Since almost every engine action is done from within a transaction, cancelation must be controlled by the context passed to the `Begin` method. Any action done on a canceled transaction must return an error.
+
+In addition, context cancellation or some unrecoverable I/O error may occur when iterating over store with `engine.Iterator`. In such case iterator is invalidated, `Valid()` method returns false and the new `Err()` will return non-nil error.
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
+
+// only the Begin method takes the context.
+tx, err := ng.Begin(ctx, true)
+
+// Transaction methods don't need to take a context.
+st, err := tx.GetStore([]byte("foo"))
+err = st.Put([]byte("a"), []byte("b"))
+
+it := st.NewIterator(engine.IteratorConfig{})
+defer it.Close()
+for it.Seek(nil); it.Valid(); it.Next() {
+	// …
+}
+if err := it.Err(); err != nil {
+	return err
+}
+```
+
+### Packages that use an engine
+
+Since the context is passed only to `engine.Engine#Begin`, only the functions that explicitly need to open a transaction must expect a context themselves.
+
+```go
+package database
+
+func (db *Database) Begin(ctx context.Context, writable bool) (*Transaction, error)
+func (db *Database) BeginTx(ctx context.Context, writable bool) (*Transaction, error)
+```
+
+Any function using the returned transaction must not expect a context in their signature.
+
+However, there might be some exceptions: If a function performs a long-running operation or relies on IO itself, it must expect a context.
+
+### Other packages
+
+If a function doesn't rely on IO, it must not expect a context.
diff --git a/sql/parser/alter.go b/sql/parser/alter.go
index 7ec67b18..b48fe6d8 100644
--- a/sql/parser/alter.go
+++ b/sql/parser/alter.go
@@ -5,40 +5,71 @@ import (
 	"github.com/genjidb/genji/sql/scanner"
 )
 
+func (p *Parser) parseAlterTableRenameStatement(tableName string) (_ query.AlterStmt, err error) {
+	var stmt query.AlterStmt
+	stmt.TableName = tableName
+
+	// Parse "TO".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TO {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TO"}, pos)
+	}
+
+	// Parse new table name.
+	stmt.NewTableName, err = p.parseIdent()
+	if err != nil {
+		return stmt, err
+	}
+
+	return stmt, nil
+}
+
+func (p *Parser) parseAlterTableAddFieldStatement(tableName string) (_ query.AlterTableAddField, err error) {
+	var stmt query.AlterTableAddField
+	stmt.TableName = tableName
+
+	// Parse "FIELD".
+	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.FIELD {
+		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"FIELD"}, pos)
+	}
+
+	// Parse new field definition.
+	err = p.parseFieldDefinition(&stmt.Constraint)
+	if err != nil {
+		return stmt, err
+	}
+
+	if stmt.Constraint.IsPrimaryKey {
+		return stmt, &ParseError{Message: "cannot add a PRIMARY KEY constraint"}
+	}
+
+	return stmt, nil
+}
+
 // parseAlterStatement parses a Alter query string and returns a Statement AST object.
 // This function assumes the ALTER token has already been consumed.
-func (p *Parser) parseAlterStatement() (query.AlterStmt, error) {
-	var stmt query.AlterStmt
+func (p *Parser) parseAlterStatement() (query.Statement, error) {
 	var err error
 
 	// Parse "TABLE".
 	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TABLE {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE"}, pos)
+		return nil, newParseError(scanner.Tokstr(tok, lit), []string{"TABLE"}, pos)
 	}
 
 	// Parse table name.
-	stmt.TableName, err = p.parseIdent()
+	tableName, err := p.parseIdent()
 	if err != nil {
 		pErr := err.(*ParseError)
 		pErr.Expected = []string{"table_name"}
-		return stmt, pErr
-	}
-
-	// Parse "RENAME".
-	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.RENAME {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"RENAME"}, pos)
+		return nil, pErr
 	}
 
-	// Parse "TO".
-	if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.TO {
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"TO"}, pos)
-	}
-
-	// Parse new table name.
-	stmt.NewTableName, err = p.parseIdent()
-	if err != nil {
-		return stmt, err
+	tok, pos, lit := p.ScanIgnoreWhitespace()
+	switch tok {
+	case scanner.RENAME:
+		return p.parseAlterTableRenameStatement(tableName)
+	case scanner.ADD_KEYWORD:
+		return p.parseAlterTableAddFieldStatement(tableName)
 	}
 
-	return stmt, nil
+	return nil, newParseError(scanner.Tokstr(tok, lit), []string{"ADD", "RENAME"}, pos)
 }
diff --git a/sql/parser/create.go b/sql/parser/create.go
index 792e1339..be213f66 100644
--- a/sql/parser/create.go
+++ b/sql/parser/create.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/query/expr"
 	"github.com/genjidb/genji/sql/scanner"
 )
 
@@ -75,6 +76,20 @@ func (p *Parser) parseIfNotExists() (bool, error) {
 	return true, nil
 }
 
+func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint) (err error) {
+	fc.Path, err = p.parsePath()
+	if err != nil {
+		return err
+	}
+
+	fc.Type, err = p.parseType()
+	if err != nil {
+		return err
+	}
+
+	return p.parseFieldConstraint(fc)
+}
+
 func (p *Parser) parseFieldConstraints(info *database.TableInfo) error {
 	// Parse ( token.
 	if tok, _, _ := p.ScanIgnoreWhitespace(); tok != scanner.LPAREN {
@@ -88,15 +103,7 @@ func (p *Parser) parseFieldConstraints(info *database.TableInfo) error {
 	for {
 		var fc database.FieldConstraint
 
-		fc.Path, err = p.parsePath()
-		if err != nil {
-			p.Unscan()
-			break
-		}
-
-		fc.Type = p.parseType()
-
-		err = p.parseFieldConstraint(&fc)
+		err = p.parseFieldDefinition(&fc)
 		if err != nil {
 			return err
 		}
@@ -156,6 +163,28 @@ func (p *Parser) parseFieldConstraint(fc *database.FieldConstraint) error {
 			}
 
 			fc.IsNotNull = true
+		case scanner.DEFAULT:
+			// Parse default value expression.
+			e, err := p.parseUnaryExpr()
+			if err != nil {
+				return err
+			}
+
+			d, err := e.Eval(expr.EvalStack{})
+			if err != nil {
+				return err
+			}
+
+			if fc.Type != 0 && fc.Type != d.Type {
+				return &ParseError{Message: fmt.Sprintf("default value must have type %s, got %s", fc.Type, d.Type)}
+			}
+
+			// if it's already default value we return an error
+			if fc.HasDefaultValue() {
+				return newParseError(scanner.Tokstr(tok, lit), []string{"CONSTRAINT", ")"}, pos)
+			}
+
+			fc.DefaultValue = d
 		default:
 			p.Unscan()
 			return nil
diff --git a/sql/parser/expr.go b/sql/parser/expr.go
index 0b676235..39cbad44 100644
--- a/sql/parser/expr.go
+++ b/sql/parser/expr.go
@@ -84,6 +84,12 @@ func (p *Parser) parseOperator() (func(lhs, rhs expr.Expr) expr.Expr, scanner.To
 		return nil, 0, nil
 	}
 
+	// Ignore currently unused operators.
+	if op == scanner.EQREGEX || op == scanner.NEQREGEX {
+		p.Unscan()
+		return nil, 0, nil
+	}
+
 	switch op {
 	case scanner.EQ:
 		return expr.Eq, op, nil
@@ -126,10 +132,17 @@ func (p *Parser) parseOperator() (func(lhs, rhs expr.Expr) expr.Expr, scanner.To
 		p.Unscan()
 		return expr.Is, op, nil
 	case scanner.NOT:
-		if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.IN {
-			return nil, 0, newParseError(scanner.Tokstr(tok, lit), []string{"IN"}, pos)
+		tok, pos, lit := p.ScanIgnoreWhitespace()
+		switch tok {
+		case scanner.IN:
+			return expr.NotIn, op, nil
+		case scanner.LIKE:
+			return expr.NotLike, op, nil
 		}
-		return expr.NotIn, op, nil
+
+		return nil, 0, newParseError(scanner.Tokstr(tok, lit), []string{"IN, LIKE"}, pos)
+	case scanner.LIKE:
+		return expr.Like, op, nil
 	}
 
 	panic(fmt.Sprintf("unknown operator %q", op))
@@ -286,29 +299,52 @@ func (p *Parser) parseParam() (expr.Expr, error) {
 	}
 }
 
-func (p *Parser) parseType() document.ValueType {
+func (p *Parser) parseType() (document.ValueType, error) {
 	tok, _, _ := p.ScanIgnoreWhitespace()
 	switch tok {
 	case scanner.TYPEARRAY:
-		return document.ArrayValue
+		return document.ArrayValue, nil
 	case scanner.TYPEBLOB:
-		return document.BlobValue
+		return document.BlobValue, nil
 	case scanner.TYPEBOOL:
-		return document.BoolValue
+		return document.BoolValue, nil
 	case scanner.TYPEBYTES:
-		return document.BlobValue
+		return document.BlobValue, nil
 	case scanner.TYPEDOCUMENT:
-		return document.DocumentValue
+		return document.DocumentValue, nil
+	case scanner.TYPEREAL:
+		return document.DoubleValue, nil
 	case scanner.TYPEDOUBLE:
-		return document.DoubleValue
-	case scanner.TYPEINTEGER:
-		return document.IntegerValue
+		tok, _, _ := p.ScanIgnoreWhitespace()
+		if tok == scanner.PRECISION {
+			return document.DoubleValue, nil
+		}
+		p.Unscan()
+		return document.DoubleValue, nil
+	case scanner.TYPEINTEGER, scanner.TYPEINT, scanner.TYPEINT2, scanner.TYPEINT8, scanner.TYPETINYINT,
+		 scanner.TYPEBIGINT, scanner.TYPEMEDIUMINT, scanner.TYPESMALLINT:
+		return document.IntegerValue, nil
 	case scanner.TYPETEXT:
-		return document.TextValue
+		return document.TextValue, nil
+	case scanner.TYPEVARCHAR, scanner.TYPECHARACTER:
+		if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.LPAREN {
+			return 0, newParseError(scanner.Tokstr(tok, lit), []string{"("}, pos)
+		}
+		
+		// The value between parentheses is not used.
+		if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.INTEGER {
+			return 0, newParseError(scanner.Tokstr(tok, lit), []string{"integer"}, pos)
+		}
+
+		if tok, pos, lit := p.ScanIgnoreWhitespace(); tok != scanner.RPAREN {
+			return 0, newParseError(scanner.Tokstr(tok, lit), []string{")"}, pos)
+		}
+
+		return document.TextValue, nil
 	}
 
 	p.Unscan()
-	return 0
+	return 0, nil
 }
 
 // parseDocument parses a document
@@ -409,7 +445,7 @@ LOOP:
 			}
 			idx, err := strconv.Atoi(lit)
 			if err != nil {
-				return nil, err
+				return nil, newParseError(lit, []string{"integer"}, pos)
 			}
 			vPath = append(vPath, document.ValuePathFragment{
 				ArrayIndex: idx,
@@ -545,7 +581,11 @@ func (p *Parser) parseCastExpression() (expr.Expr, error) {
 	}
 
 	// Parse require typename.
-	tp := p.parseType()
+	tp, err := p.parseType()
+	if err != nil {
+		return nil, err
+	}
+
 	if tp == 0 {
 		tok, pos, lit := p.ScanIgnoreWhitespace()
 		p.Unscan()
diff --git a/sql/query/alter.go b/sql/query/alter.go
index 29657f0e..f53ccca2 100644
--- a/sql/query/alter.go
+++ b/sql/query/alter.go
@@ -39,3 +39,30 @@ func (stmt AlterStmt) Run(ctx context.Context, tx *database.Transaction, _ []exp
 	err := tx.RenameTable(stmt.TableName, stmt.NewTableName)
 	return res, err
 }
+
+type AlterTableAddField struct {
+	TableName  string
+	Constraint database.FieldConstraint
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt AlterTableAddField) IsReadOnly() bool {
+	return false
+}
+
+// Run runs the ALTER TABLE ADD FIELD statement in the given transaction.
+// It implements the Statement interface.
+func (stmt AlterTableAddField) Run(ctx context.Context, tx *database.Transaction, _ []expr.Param) (Result, error) {
+	var res Result
+
+	if stmt.TableName == "" {
+		return res, errors.New("missing table name")
+	}
+
+	if stmt.Constraint.Path == nil {
+		return res, errors.New("missing field name")
+	}
+
+	err := tx.AddField(stmt.TableName, stmt.Constraint)
+	return res, err
+}
diff --git a/sql/query/constraint.go b/sql/query/constraint.go
new file mode 100644
index 00000000..04e1a683
--- /dev/null
+++ b/sql/query/constraint.go
@@ -0,0 +1,142 @@
+package query
+
+import (
+	"fmt"
+	"github.com/genjidb/genji/database"
+
+	"github.com/genjidb/genji/document"
+)
+
+// constraintNode is a tree node which stores a type of document field
+type constraintNode struct {
+	frag   document.ValuePathFragment
+	typ    document.ValueType
+	parent *constraintNode
+	sub    []*constraintNode
+}
+
+func createConstraintNode(parent *constraintNode, path document.ValuePath, typ document.ValueType) *constraintNode {
+	node := &constraintNode{
+		frag:   path[0],
+		parent: parent,
+	}
+
+	if len(path) == 1 {
+		node.typ = typ
+		return node
+	}
+
+	// Here we check next path fragment
+	// If next is index, current node is array
+	// Otherwise it is a document
+	if path[1].FieldName != "" {
+		node.typ = document.DocumentValue
+	} else {
+		node.typ = document.ArrayValue
+	}
+
+	node.sub = append(node.sub, createConstraintNode(node, path[1:], typ))
+	return node
+}
+
+func (n *constraintNode) getPath() document.ValuePath {
+	if n.parent == nil {
+		return document.ValuePath{n.frag}
+	}
+
+	return append(n.parent.getPath(), n.frag)
+}
+
+func (n *constraintNode) search(path document.ValuePath) *constraintNode {
+	if n.frag != path[0] {
+		return nil
+	}
+
+	if len(path) == 1 {
+		return n
+	}
+
+	for _, sub := range n.sub {
+		t := sub.search(path[1:])
+		if t != nil {
+			return t
+		}
+	}
+
+	return nil
+}
+
+func (n *constraintNode) insert(path document.ValuePath, typ document.ValueType) error {
+	switch {
+	case len(path) == 1:
+		return fmt.Errorf("%q already exists as type %s", n.getPath().String(), n.typ.String())
+	// when type is explicitly set and does not have array or document type
+	case n.typ != 0 && n.typ != document.ArrayValue && n.typ != document.DocumentValue:
+		p := append(n.getPath(), path[1]).String()
+		return fmt.Errorf("%q already exists as type %s, but trying add %q constraint", n.getPath().String(), n.typ.String(), p)
+	// when constraint tries to set document constraint for path, but there is already a array constraint
+	case path[1].FieldName != "" && n.typ == document.ArrayValue:
+		p := append(n.getPath(), path[1]).String()
+		return fmt.Errorf("%q already exists as array, but trying add %q constraint", n.getPath().String(), p)
+	// when constraint tries to set array constraint for path, but there is already a document constraint
+	case path[1].FieldName == "" && n.typ == document.DocumentValue:
+		p := append(n.getPath(), path[1]).String()
+		return fmt.Errorf("%q already exists as document, but trying add %q constraint", n.getPath().String(), p)
+	}
+
+	for _, sub := range n.sub {
+		if sub.frag == path[1] {
+			return sub.insert(path[1:], typ)
+		}
+	}
+
+	n.sub = append(n.sub, createConstraintNode(n, path[1:], typ))
+	return nil
+}
+
+// constraintTree is a tree of document field types.
+// Field type can be set explicitly or derived from path.
+// Example query:
+// 	CREATE TABLE foo(a.b TEXT, a.d[1])
+// as a tree
+// 	document(a)
+// 	├── text(b)
+// 	├── array(d)
+// 	│   ├──[1] any
+// 	│   └──...
+//
+type constraintTree struct {
+	roots []*constraintNode
+}
+
+func (tree *constraintTree) insert(path document.ValuePath, typ document.ValueType) error {
+	for _, sub := range tree.roots {
+		if sub.frag == path[0] {
+			return sub.insert(path, typ)
+		}
+	}
+
+	tree.roots = append(tree.roots, createConstraintNode(nil, path, typ))
+	return nil
+}
+
+func (tree *constraintTree) search(path document.ValuePath) *constraintNode {
+	for _, sub := range tree.roots {
+		if sub.frag == path[0] {
+			return sub.search(path)
+		}
+	}
+
+	return nil
+}
+
+func checkConstraints(constraints []database.FieldConstraint) error {
+	tree := constraintTree{}
+	for _, fc := range constraints {
+		if err := tree.insert(fc.Path, fc.Type); err != nil {
+			return fmt.Errorf("incoherent field constraint: %w", err)
+		}
+	}
+
+	return nil
+}
diff --git a/sql/query/create.go b/sql/query/create.go
index 764fae35..9110a605 100644
--- a/sql/query/create.go
+++ b/sql/query/create.go
@@ -30,7 +30,12 @@ func (stmt CreateTableStmt) Run(ctx context.Context, tx *database.Transaction, a
 		return res, errors.New("missing table name")
 	}
 
-	err := tx.CreateTable(stmt.TableName, &stmt.Info)
+	err := checkConstraints(stmt.Info.FieldConstraints)
+	if err != nil {
+		return res, err
+	}
+
+	err = tx.CreateTable(stmt.TableName, &stmt.Info)
 	if stmt.IfNotExists && err == database.ErrTableAlreadyExists {
 		err = nil
 	}
diff --git a/sql/query/drop.go b/sql/query/drop.go
index 0e16c534..536d1393 100644
--- a/sql/query/drop.go
+++ b/sql/query/drop.go
@@ -29,7 +29,7 @@ func (stmt DropTableStmt) Run(ctx context.Context, tx *database.Transaction, arg
 	}
 
 	err := tx.DropTable(stmt.TableName)
-	if err == database.ErrTableNotFound && stmt.IfExists {
+	if errors.Is(err, database.ErrTableNotFound) && stmt.IfExists {
 		err = nil
 	}
 
diff --git a/sql/query/expr/comparison.go b/sql/query/expr/comparison.go
index 5415d605..92423ac8 100644
--- a/sql/query/expr/comparison.go
+++ b/sql/query/expr/comparison.go
@@ -426,7 +426,7 @@ func (op cmpOp) compare(l, r document.Value) (bool, error) {
 func IsComparisonOperator(op Operator) bool {
 	switch op.(type) {
 	case eqOp, neqOp, gtOp, gteOp, ltOp, lteOp,
-		isOp, isNotOp, inOp, notInOp:
+		isOp, isNotOp, inOp, notInOp, likeOp, notLikeOp:
 		return true
 	}
 
@@ -540,17 +540,7 @@ func NotIn(a, b Expr) Expr {
 }
 
 func (op notInOp) Eval(ctx EvalStack) (document.Value, error) {
-	v, err := op.inOp.Eval(ctx)
-	if err != nil {
-		return v, err
-	}
-	if v == trueLitteral {
-		return falseLitteral, nil
-	}
-	if v == falseLitteral {
-		return trueLitteral, nil
-	}
-	return v, nil
+	return invertBoolResult(op.inOp.Eval)(ctx)
 }
 
 func (op notInOp) String() string {
diff --git a/sql/query/expr/expr.go b/sql/query/expr/expr.go
index d22de393..fd949199 100644
--- a/sql/query/expr/expr.go
+++ b/sql/query/expr/expr.go
@@ -132,3 +132,20 @@ type Parentheses struct {
 func (p Parentheses) Eval(es EvalStack) (document.Value, error) {
 	return p.E.Eval(es)
 }
+
+func invertBoolResult(f func(ctx EvalStack) (document.Value, error)) func(ctx EvalStack) (document.Value, error) {
+	return func(ctx EvalStack) (document.Value, error) {
+		v, err := f(ctx)
+
+		if err != nil {
+			return v, err
+		}
+		if v == trueLitteral {
+			return falseLitteral, nil
+		}
+		if v == falseLitteral {
+			return trueLitteral, nil
+		}
+		return v, nil
+	}
+}
diff --git a/sql/query/expr/function.go b/sql/query/expr/function.go
index d005c0b1..89b22b56 100644
--- a/sql/query/expr/function.go
+++ b/sql/query/expr/function.go
@@ -159,6 +159,9 @@ type CountFunc struct {
 }
 
 func (c *CountFunc) Eval(ctx EvalStack) (document.Value, error) {
+	if ctx.Document == nil {
+		return document.Value{}, errors.New("misuse of aggregation function COUNT()")
+	}
 	return ctx.Document.GetByField(c.String())
 }
 
@@ -239,6 +242,9 @@ type MinFunc struct {
 
 // Eval extracts the min value from the given document and returns it.
 func (m *MinFunc) Eval(ctx EvalStack) (document.Value, error) {
+	if ctx.Document == nil {
+		return document.Value{}, errors.New("misuse of aggregation function MIN()")
+	}
 	return ctx.Document.GetByField(m.String())
 }
 
@@ -336,6 +342,9 @@ type MaxFunc struct {
 
 // Eval extracts the max value from the given document and returns it.
 func (m *MaxFunc) Eval(ctx EvalStack) (document.Value, error) {
+	if ctx.Document == nil {
+		return document.Value{}, errors.New("misuse of aggregation function MAX()")
+	}
 	return ctx.Document.GetByField(m.String())
 }
 
@@ -433,6 +442,9 @@ type SumFunc struct {
 
 // Eval extracts the sum value from the given document and returns it.
 func (s *SumFunc) Eval(ctx EvalStack) (document.Value, error) {
+	if ctx.Document == nil {
+		return document.Value{}, errors.New("misuse of aggregation function SUM()")
+	}
 	return ctx.Document.GetByField(s.String())
 }
 
@@ -545,6 +557,9 @@ type AvgFunc struct {
 
 // Eval extracts the average value from the given document and returns it.
 func (s *AvgFunc) Eval(ctx EvalStack) (document.Value, error) {
+	if ctx.Document == nil {
+		return document.Value{}, errors.New("misuse of aggregation function AVG()")
+	}
 	return ctx.Document.GetByField(s.String())
 }
 
diff --git a/sql/query/expr/like.go b/sql/query/expr/like.go
new file mode 100644
index 00000000..6bbc2c77
--- /dev/null
+++ b/sql/query/expr/like.go
@@ -0,0 +1,60 @@
+package expr
+
+import (
+	"errors"
+	"fmt"
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/sql/query/glob"
+	"github.com/genjidb/genji/sql/scanner"
+)
+
+func like(pattern, text string) bool {
+	return glob.MatchLike(pattern, text)
+}
+
+type likeOp struct {
+	*simpleOperator
+}
+
+// Like creates an expression that evaluates to the result of a LIKE b.
+func Like(a, b Expr) Expr {
+	return &likeOp{&simpleOperator{a, b, scanner.LIKE}}
+}
+
+func (op likeOp) Eval(ctx EvalStack) (document.Value, error) {
+	a, b, err := op.simpleOperator.eval(ctx)
+	if err != nil {
+		return nullLitteral, err
+	}
+
+	if a.Type != document.TextValue || b.Type != document.TextValue {
+		return nullLitteral, errors.New("LIKE operator takes a text")
+	}
+
+	if like(b.V.(string), a.V.(string)) {
+		return trueLitteral, nil
+	}
+
+	return falseLitteral, nil
+}
+
+func (op likeOp) String() string {
+	return fmt.Sprintf("%v LIKE %v", op.a, op.b)
+}
+
+type notLikeOp struct {
+	likeOp
+}
+
+// NotLike creates an expression that evaluates to the result of a NOT LIKE b.
+func NotLike(a, b Expr) Expr {
+	return &notLikeOp{likeOp{&simpleOperator{a, b, scanner.LIKE}}}
+}
+
+func (op notLikeOp) Eval(ctx EvalStack) (document.Value, error) {
+	return invertBoolResult(op.likeOp.Eval)(ctx)
+}
+
+func (op notLikeOp) String() string {
+	return fmt.Sprintf("%v NOT LIKE %v", op.a, op.b)
+}
diff --git a/sql/query/expr/path.go b/sql/query/expr/path.go
index a0d7289e..1c3c5b16 100644
--- a/sql/query/expr/path.go
+++ b/sql/query/expr/path.go
@@ -24,7 +24,7 @@ func (f FieldSelector) Eval(stack EvalStack) (document.Value, error) {
 		return nullLitteral, nil
 	}
 
-	return v, nil
+	return v, err
 }
 
 // IsEqual compares this expression with the other expression and returns
diff --git a/sql/query/glob/doc.go b/sql/query/glob/doc.go
new file mode 100644
index 00000000..12dfb74b
--- /dev/null
+++ b/sql/query/glob/doc.go
@@ -0,0 +1,3 @@
+// Package glob implements wildcard pattern matching algorithms for strings.
+//
+package glob
diff --git a/sql/query/glob/like.go b/sql/query/glob/like.go
new file mode 100644
index 00000000..9454f0c6
--- /dev/null
+++ b/sql/query/glob/like.go
@@ -0,0 +1,194 @@
+// The author disclaims copyright to this source code.  In place of
+// a legal notice, here is a blessing:
+//
+//    May you do good and not evil.
+//    May you find forgiveness for yourself and forgive others.
+//    May you share freely, never taking more than you give.
+//
+// This is an optimized Go port of the SQLite’s icuLikeCompare routine using backtracking.
+// See https://sqlite.org/src/file?name=ext%2Ficu%2Ficu.c&ln=117-195&ci=54b54f02c66c5aea
+
+package glob
+
+import (
+	"unicode"
+	"unicode/utf8"
+)
+
+const (
+	matchOne = '_'
+	matchAll = '%'
+	matchEsc = '\\'
+)
+
+// readRune is like skipRune, but also returns the removed Unicode code point.
+func readRune(s string) (rune, string) {
+	r, size := utf8.DecodeRuneInString(s)
+	if r == utf8.RuneError && size == 1 {
+		return rune(s[0]), s[1:]
+	}
+	return r, s[size:]
+}
+
+// skipRune returns a slice of the string s with the first Unicode code point removed.
+func skipRune(s string) string {
+	_, size := utf8.DecodeRuneInString(s)
+	return s[size:]
+}
+
+// equalFold is strings.EqualFold for individual runes.
+func equalFold(sr, tr rune) bool {
+	// Easy case.
+	if tr == sr {
+		return true
+	}
+
+	// Make sr < tr to simplify what follows.
+	if tr < sr {
+		tr, sr = sr, tr
+	}
+	// Fast check for ASCII.
+	if tr < utf8.RuneSelf {
+		// ASCII only, sr/tr must be upper/lower case
+		return 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A'
+	}
+
+	// General case. SimpleFold(x) returns the next equivalent rune > x
+	// or wraps around to smaller values.
+	r := unicode.SimpleFold(sr)
+	for r != sr && r < tr {
+		r = unicode.SimpleFold(r)
+	}
+	return r == tr
+}
+
+// MatchLike reports whether string s matches the SQL LIKE-style glob pattern.
+// Supported wildcards are '_' (match any one character) and '%' (match zero
+// or more characters). They can be escaped by '\' (escape character).
+//
+// MatchLike requires pattern to match whole string, not just a substring.
+func MatchLike(pattern, s string) bool {
+	var prevEscape bool
+
+	var w, t string // backtracking state
+
+	for len(s) != 0 {
+		// Read (and consume) the next character from the input pattern.
+		var p rune
+		if len(pattern) == 0 {
+			goto backtrack
+		}
+		p, pattern = readRune(pattern)
+
+	loop:
+		// There are now 4 possibilities:
+		//
+		// 1. p is an unescaped matchAll character “%”,
+		// 2. p is an unescaped matchOne character “_”,
+		// 3. p is an unescaped matchEsc character, or
+		// 4. p is to be handled as an ordinary character
+		//
+		if p == matchAll && !prevEscape {
+			// Case 1.
+			var c byte
+
+			// Skip any matchAll or matchOne characters that follow a
+			// matchAll. For each matchOne, skip one character in the
+			// test string.
+			//
+			for len(pattern) != 0 {
+				c = pattern[0]
+				if c != matchAll && c != matchOne {
+					break
+				}
+				pattern = pattern[1:]
+
+				if c != matchOne {
+					continue
+				}
+				if len(s) == 0 {
+					return false
+				}
+				s = skipRune(s)
+			}
+
+			if len(pattern) == 0 {
+				return true
+			}
+
+			// Save state and match next character.
+			//
+			// Since we save t = s and then continue to loop for len(s) ≠ 0,
+			// the condition len(t) ≠ 0 is always true when we need to backtrack.
+			//
+			w, t = pattern, s
+		} else if p == matchOne && !prevEscape {
+			// Case 2.
+			//
+			// We can either enter loop on normal iteration where len(s) ≠ 0,
+			// or from backtracking. But we consume all matchOne characters
+			// before saving backtracking state, so this case is reachable on
+			// normal iteration only.
+			//
+			// That is, we are guaranteed to have input at this point.
+			//
+			s = skipRune(s)
+		} else if p == matchEsc && !prevEscape {
+			// Case 3.
+			//
+			// We can’t reach this case from backtracking to matchAll.
+			// That implies len(s) ≠ 0 and normal iteration on continue.
+			// We would either have an escaped character in the pattern,
+			// or we’ve consumed whole pattern and attempt to backtrack.
+			// If we can’t backtrack then we are not at the end of input
+			// since len(s) ≠ 0, and false is returned. That said, it’s
+			// impossible to exit the loop with truthy prevEscape.
+			//
+			prevEscape = true
+		} else {
+			// Case 4.
+			prevEscape = false
+
+			var r rune
+			r, s = readRune(s)
+			if !equalFold(p, r) {
+				goto backtrack
+			}
+		}
+		continue
+
+	backtrack:
+		// If we can’t backtrack return prevEscape
+		// to allow escaping end of input.
+		//
+		if len(w) == 0 {
+			return prevEscape && len(s) == 0
+		}
+
+		// Keep the pattern and skip rune in input.
+		// Note that we only backtrack to matchAll.
+		//
+		p, pattern = matchAll, w
+		prevEscape = false
+		s = skipRune(t)
+
+		goto loop
+	}
+
+	// Check that the rest of the pattern is matchAll.
+	for i := 0; i < len(pattern); i++ {
+		if pattern[i] == matchAll {
+			continue
+		}
+
+		// Allow escaping end of string.
+		if i+1 == len(pattern) {
+			if pattern[i] == matchEsc {
+				return true
+			}
+		}
+
+		return false
+	}
+	return true
+}
diff --git a/sql/query/reindex.go b/sql/query/reindex.go
index f3ab6718..aeee50ce 100644
--- a/sql/query/reindex.go
+++ b/sql/query/reindex.go
@@ -2,7 +2,8 @@ package query
 
 import (
 	"context"
-
+	"errors"
+	
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/sql/query/expr"
 )
@@ -30,7 +31,7 @@ func (stmt ReIndexStmt) Run(ctx context.Context, tx *database.Transaction, args
 	if err == nil {
 		return res, t.ReIndex()
 	}
-	if err != database.ErrTableNotFound {
+	if !errors.Is(err, database.ErrTableNotFound) {
 		return res, err
 	}
 
diff --git a/sql/scanner/token.go b/sql/scanner/token.go
index fab98a8e..695733d1 100644
--- a/sql/scanner/token.go
+++ b/sql/scanner/token.go
@@ -56,6 +56,7 @@ const (
 	GTE      // >=
 	IN       // IN
 	IS       // IS
+	LIKE     // LIKE
 	operatorEnd
 
 	LPAREN      // (
@@ -72,6 +73,7 @@ const (
 
 	keywordBeg
 	// ALL and the following are Genji SQL Keywords
+	ADD_KEYWORD
 	ALTER
 	AS
 	ASC
@@ -80,11 +82,13 @@ const (
 	CAST
 	COMMIT
 	CREATE
+	DEFAULT
 	DELETE
 	DESC
 	DROP
 	EXISTS
 	EXPLAIN
+	FIELD
 	FROM
 	GROUP
 	IF
@@ -98,6 +102,7 @@ const (
 	ON
 	ONLY
 	ORDER
+	PRECISION
 	PRIMARY
 	READ
 	REINDEX
@@ -115,14 +120,25 @@ const (
 	WHERE
 	WRITE
 
+	// Aliases
 	TYPEARRAY
+	TYPEBIGINT
 	TYPEBLOB
 	TYPEBOOL
 	TYPEBYTES
+	TYPECHARACTER
 	TYPEDOCUMENT
 	TYPEDOUBLE
+	TYPEINT
+	TYPEINT2
+	TYPEINT8
 	TYPEINTEGER
+	TYPEMEDIUMINT
+	TYPESMALLINT
 	TYPETEXT
+	TYPETINYINT
+	TYPEREAL
+	TYPEVARCHAR
 
 	keywordEnd
 )
@@ -165,6 +181,7 @@ var tokens = [...]string{
 	GTE:      ">=",
 	IN:       "IN",
 	IS:       "IS",
+	LIKE:     "LIKE",
 
 	LPAREN:      "(",
 	RPAREN:      ")",
@@ -178,6 +195,7 @@ var tokens = [...]string{
 	SEMICOLON:   ";",
 	DOT:         ".",
 
+	ADD_KEYWORD: "ADD",
 	ALTER:       "ALTER",
 	AS:          "AS",
 	ASC:         "ASC",
@@ -187,12 +205,14 @@ var tokens = [...]string{
 	BY:          "BY",
 	CREATE:      "CREATE",
 	CAST:        "CAST",
+	DEFAULT:     "DEFAULT",
 	DELETE:      "DELETE",
 	DESC:        "DESC",
 	DROP:        "DROP",
 	EXISTS:      "EXISTS",
 	EXPLAIN:     "EXPLAIN",
 	KEY:         "KEY",
+	FIELD:       "FIELD",
 	FROM:        "FROM",
 	IF:          "IF",
 	INDEX:       "INDEX",
@@ -204,6 +224,7 @@ var tokens = [...]string{
 	ON:          "ON",
 	ONLY:        "ONLY",
 	ORDER:       "ORDER",
+	PRECISION:   "PRECISION",
 	PRIMARY:     "PRIMARY",
 	READ:        "READ",
 	REINDEX:     "REINDEX",
@@ -221,14 +242,24 @@ var tokens = [...]string{
 	WHERE:       "WHERE",
 	WRITE:       "WRITE",
 
-	TYPEARRAY:    "ARRAY",
-	TYPEBLOB:     "BLOB",
-	TYPEBOOL:     "BOOL",
-	TYPEBYTES:    "BYTES",
-	TYPEDOCUMENT: "DOCUMENT",
-	TYPEDOUBLE:   "DOUBLE",
-	TYPEINTEGER:  "INTEGER",
-	TYPETEXT:     "TEXT",
+	TYPEARRAY:     "ARRAY",
+	TYPEBIGINT:    "BIGINT",
+	TYPEBLOB:      "BLOB",
+	TYPEBOOL:      "BOOL",
+	TYPEBYTES:     "BYTES",
+	TYPECHARACTER: "CHARACTER",
+	TYPEDOCUMENT:  "DOCUMENT",
+	TYPEDOUBLE:    "DOUBLE",
+	TYPEINT:       "INT",
+	TYPEINT2:      "INT2",
+	TYPEINT8:      "INT8",
+	TYPEINTEGER:   "INTEGER",
+	TYPEMEDIUMINT: "MEDIUMINT",
+	TYPESMALLINT:  "SMALLINT",
+	TYPETEXT:      "TEXT",
+	TYPETINYINT:   "TINYINT",
+	TYPEREAL:      "REAL",
+	TYPEVARCHAR:   "VARCHAR",
 }
 
 var keywords map[string]Token
@@ -238,7 +269,7 @@ func initKeywords() {
 	for tok := keywordBeg + 1; tok < keywordEnd; tok++ {
 		keywords[strings.ToLower(tokens[tok])] = tok
 	}
-	for _, tok := range []Token{AND, OR, TRUE, FALSE, NULL, IN, IS} {
+	for _, tok := range []Token{AND, OR, TRUE, FALSE, NULL, IN, IS, LIKE} {
 		keywords[strings.ToLower(tokens[tok])] = tok
 	}
 }
@@ -260,7 +291,7 @@ func (tok Token) Precedence() int {
 		return 2
 	case IN:
 		return 3
-	case EQ, NEQ, EQREGEX, NEQREGEX, LT, LTE, GT, GTE, IS:
+	case EQ, NEQ, EQREGEX, NEQREGEX, LT, LTE, GT, GTE, IS, LIKE:
 		return 4
 	case ADD, SUB, BITWISEOR, BITWISEXOR:
 		return 5


Test patch:
diff --git a/.github/workflows/test-fuzz.yaml b/.github/workflows/test-fuzz.yaml
new file mode 100644
index 00000000..bac331ac
--- /dev/null
+++ b/.github/workflows/test-fuzz.yaml
@@ -0,0 +1,125 @@
+name: Fuzz
+on:
+  push:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  run:
+    name: Run
+    runs-on: ubuntu-latest
+    env:
+      GOOS: ${{ matrix.goos }}
+      GOARCH: ${{ matrix.goarch }}
+      GOFLAGS: '-trimpath -mod=readonly'
+      GO111MODULE: 'on'
+    strategy:
+      fail-fast: false
+      matrix:
+        go:
+          # Misplaced compiler directive error with Go 1.15.x.
+          # https://github.com/dvyukov/go-fuzz/issues/294
+          # Keep this version fixed so that we can analyze
+          # coverage profiles locally.
+          - 1.14.9
+        goos:
+          - linux
+        goarch:
+          - amd64
+        func:
+          - FuzzParseQuery
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+        with:
+          path: genji
+
+      - name: Checkout corpus
+        uses: actions/checkout@v2
+        with:
+          repository: genjidb/go-fuzz-corpus
+          path: genji/fuzz/testdata/fuzz
+
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go }}
+
+      - name: Get cache path
+        # FIXME use GOMODCACHE instead of GOPATH once
+        # https://github.com/dvyukov/go-fuzz/issues/294
+        # is fixed and we can bump Go to 1.15 or greater.
+        run: echo "::set-output name=dir::$(go env GOPATH)"
+        id: modcache
+
+      - name: Set up cache
+        uses: actions/cache@v2
+        with:
+          # FIXME remove -fuzz- once we can use GOMODCACHE
+          path: ${{ steps.modcache.outputs.dir }}
+          key: modcache-fuzz-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
+          restore-keys: modcache-fuzz-
+
+      - name: Download dependencies
+        run: go mod download
+        working-directory: genji/fuzz
+
+      # Using libFuzzer
+      #
+      # - https://github.com/mdempsky/go114-fuzz-build
+      #   https://google.github.io/oss-fuzz/getting-started/new-project-guide/go-lang/
+      #
+      #   go114-fuzz-build -o fuzz.a .
+      #   clang -fsanitize=fuzzer -o fuzz fuzz.a
+      #
+      # - https://github.com/dvyukov/go-fuzz#libfuzzer-support
+      #
+      #   go-fuzz-build -libfuzzer -o fuzz.a
+      #   clang -fsanitize=fuzzer -o fuzz fuzz.a
+      #
+      # ./fuzz -max_len=1024 -max_total_time=300 corpus
+      #
+      # I haven’t tried go114-fuzz-build which uses builtin Go libFuzzer support,
+      # but I’ve had a better experience with go-fuzz vs libFuzzer without corpus.
+
+      - name: Run tests
+        run: |
+          go run github.com/dvyukov/go-fuzz/go-fuzz-build -func ${{ matrix.func }} && timeout -s INT -k 30 300 \
+          go run github.com/dvyukov/go-fuzz/go-fuzz -workdir=testdata/fuzz/${{ matrix.func }} -dumpcover || true
+          if [ -d crashers ]; then
+            exit 1
+          fi
+        working-directory: genji/fuzz
+
+      - name: Prepare report
+        if: ${{ success() || failure() }}
+        id: report
+        run: |
+          mv testdata/fuzz/${{ matrix.func }}/coverprofile coverprofile-coverage.out
+          mv testdata/fuzz/${{ matrix.func }}/sonarprofile sonarprofile-coverage.out
+
+          # go-fuzz dumps coverage profiles that are not reproducible.
+          # We fix that by trimming absolute paths.
+          perl -pi -e '
+            s:^/opt/.*/src/::;
+            s:^/home/.*/mod/(.*)@[^/]*:\1:;
+            s:^/home/.*(?!/cmd)/genji/:github.com/genjidb/genji/:;
+          ' coverprofile-coverage.out sonarprofile-coverage.out
+
+          # https://github.com/dvyukov/go-fuzz/issues/170#issuecomment-462297614
+          perl -ni -e 'print unless /0.0,1.1/' coverprofile-coverage.out
+        working-directory: genji/fuzz
+
+      - name: Upload coverage
+        if: ${{ success() || failure() }}
+        uses: actions/upload-artifact@v2
+        with:
+          name: ${{ matrix.func }}_cover_go${{ matrix.go }}
+          path: genji/fuzz/*coverage.*
+
+      - name: Upload corpus
+        if: ${{ success() || failure() }}
+        uses: actions/upload-artifact@v2
+        with:
+          name: ${{ matrix.func }}_corpus
+          path: genji/fuzz/testdata/fuzz/${{ matrix.func }}
diff --git a/cmd/genji/shell/command_test.go b/cmd/genji/shell/command_test.go
index eb138396..b69279e6 100644
--- a/cmd/genji/shell/command_test.go
+++ b/cmd/genji/shell/command_test.go
@@ -14,17 +14,17 @@ import (
 func TestRunTablesCmd(t *testing.T) {
 	tests := []struct {
 		name    string
-		in      string
+		in      []string
 		wantErr bool
 	}{
 		{
 			"Table",
-			".tables",
+			strings.Fields(".tables"),
 			false,
 		},
 		{
 			"Table with options",
-			".tables test",
+			strings.Fields(".tables test"),
 			true,
 		},
 	}
@@ -34,7 +34,7 @@ func TestRunTablesCmd(t *testing.T) {
 			require.NoError(t, err)
 			defer db.Close()
 
-			if err := displayTableIndex(db, test.in); (err != nil) != test.wantErr {
+			if err := runTablesCmd(db, test.in); (err != nil) != test.wantErr {
 				require.Errorf(t, err, "", test.wantErr)
 			}
 		})
@@ -60,7 +60,7 @@ func TestIndexesCmd(t *testing.T) {
 		{
 			"Indexes with nonexistent table name",
 			strings.Fields(".indexes foo"),
-			false,
+			true,
 		},
 	}
 
diff --git a/database/config_test.go b/database/config_test.go
index 6a7a5a5a..b0319fc6 100644
--- a/database/config_test.go
+++ b/database/config_test.go
@@ -1,6 +1,7 @@
 package database
 
 import (
+	"errors"
 	"testing"
 
 	"github.com/genjidb/genji/document"
@@ -64,7 +65,9 @@ func TestTableInfoStore(t *testing.T) {
 
 		// Getting a non-existing TableInfo should not work.
 		_, err = tx.tableInfoStore.Get(tx, "unknown")
-		require.Equal(t, ErrTableNotFound, err)
+		if !errors.Is(err, ErrTableNotFound) {
+			require.Equal(t, err, ErrTableNotFound)
+		}
 
 		// Deleting an existing TableInfo should work.
 		err = tx.tableInfoStore.Delete(tx, "foo1")
@@ -72,7 +75,9 @@ func TestTableInfoStore(t *testing.T) {
 
 		// Deleting a non-existing TableInfo should not work.
 		err = tx.tableInfoStore.Delete(tx, "foo1")
-		require.Equal(t, ErrTableNotFound, err)
+		if !errors.Is(err, ErrTableNotFound) {
+			require.Equal(t, err, ErrTableNotFound)
+		}
 	})
 
 	t.Run("on rollback", func(t *testing.T) {
diff --git a/database/table_test.go b/database/table_test.go
index 2260f4cd..2e17c66e 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -337,8 +337,8 @@ func TestTableInsert(t *testing.T) {
 
 		err := tx.CreateTable("test", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, false},
-				{parsePath(t, "bar"), document.IntegerValue, false, false},
+				{parsePath(t, "foo"), document.IntegerValue, false, false, document.Value{}},
+				{parsePath(t, "bar"), document.IntegerValue, false, false, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -375,7 +375,7 @@ func TestTableInsert(t *testing.T) {
 		// no enforced type, not null
 		err := tx.CreateTable("test1", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), 0, false, true},
+				{parsePath(t, "foo"), 0, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -385,7 +385,7 @@ func TestTableInsert(t *testing.T) {
 		// enforced type, not null
 		err = tx.CreateTable("test2", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo"), document.IntegerValue, false, true},
+				{parsePath(t, "foo"), document.IntegerValue, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
@@ -423,13 +423,80 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 	})
 
+	t.Run("Shouldn't fail if there is a not null field and default constraint on a document field and the field is null or missing", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		// no enforced type, not null
+		err := tx.CreateTable("test1", &database.TableInfo{
+			FieldConstraints: []database.FieldConstraint{
+				{parsePath(t, "foo"), 0, false, true, document.NewIntegerValue(42)},
+			},
+		})
+		require.NoError(t, err)
+		tb1, err := tx.GetTable("test1")
+		require.NoError(t, err)
+
+		// enforced type, not null
+		err = tx.CreateTable("test2", &database.TableInfo{
+			FieldConstraints: []database.FieldConstraint{
+				{parsePath(t, "foo"), document.IntegerValue, false, true, document.NewIntegerValue(42)},
+			},
+		})
+		require.NoError(t, err)
+		tb2, err := tx.GetTable("test2")
+		require.NoError(t, err)
+
+		// insert with empty foo field shouldn't fail
+		k, err := tb1.Insert(document.NewFieldBuffer().
+			Add("bar", document.NewDoubleValue(1)))
+		require.NoError(t, err)
+
+		d, err := tb1.GetDocument(k)
+		require.NoError(t, err)
+		v, err := d.GetByField("foo")
+		require.NoError(t, err)
+		require.Equal(t, v.V.(int64), int64(42))
+
+		// insert with explicit null foo field should fail
+		_, err = tb1.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewNullValue()))
+		require.Error(t, err)
+
+		// otherwise it should work
+		_, err = tb1.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewIntegerValue(1)))
+		require.NoError(t, err)
+
+		// insert with empty foo field shouldn't fail
+		k, err = tb2.Insert(document.NewFieldBuffer().
+			Add("bar", document.NewIntegerValue(1)))
+		require.NoError(t, err)
+
+		d, err = tb2.GetDocument(k)
+		require.NoError(t, err)
+		v, err = d.GetByField("foo")
+		require.NoError(t, err)
+		require.Equal(t, v.V.(int64), int64(42))
+
+		// insert with explicit null foo field should fail
+		_, err = tb2.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewNullValue()))
+		require.Error(t, err)
+
+		// otherwise it should work
+		_, err = tb2.Insert(document.NewFieldBuffer().
+			Add("foo", document.NewDoubleValue(1)))
+		require.NoError(t, err)
+	})
+
 	t.Run("Should fail if there is a not null field constraint on an array value and the value is null", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
 		defer cleanup()
 
 		err := tx.CreateTable("test1", &database.TableInfo{
 			FieldConstraints: []database.FieldConstraint{
-				{parsePath(t, "foo[1]"), 0, false, true},
+				{parsePath(t, "foo[1]"), 0, false, true, document.Value{}},
 			},
 		})
 		require.NoError(t, err)
diff --git a/database/transaction_test.go b/database/transaction_test.go
index 3a99ebbf..a8b6294f 100644
--- a/database/transaction_test.go
+++ b/database/transaction_test.go
@@ -1,8 +1,8 @@
 package database_test
 
 import (
+	"errors"
 	"testing"
-
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/document/encoding/msgpack"
@@ -28,6 +28,7 @@ func newTestDB(t testing.TB) (*database.Transaction, func()) {
 // - GetTable
 // - DropTable
 // - RenameTable
+// - AddField
 func TestTxTable(t *testing.T) {
 	t.Run("Create", func(t *testing.T) {
 		tx, cleanup := newTestDB(t)
@@ -79,7 +80,9 @@ func TestTxTable(t *testing.T) {
 
 		// Getting a table that doesn't exist should fail.
 		_, err = tx.GetTable("unknown")
-		require.EqualError(t, err, database.ErrTableNotFound.Error())
+		if !errors.Is(err, database.ErrTableNotFound) {
+			require.Equal(t, err, database.ErrTableNotFound)
+		}
 	})
 
 	t.Run("Drop", func(t *testing.T) {
@@ -94,11 +97,15 @@ func TestTxTable(t *testing.T) {
 
 		// Getting a table that has been dropped should fail.
 		_, err = tx.GetTable("test")
-		require.EqualError(t, err, database.ErrTableNotFound.Error())
+		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")
-		require.EqualError(t, err, database.ErrTableNotFound.Error())
+		if !errors.Is(err, database.ErrTableNotFound) {
+			require.Equal(t, err, database.ErrTableNotFound)
+		}
 	})
 
 	t.Run("Rename", func(t *testing.T) {
@@ -124,7 +131,9 @@ func TestTxTable(t *testing.T) {
 
 		// Getting the old table should return an error.
 		_, err = tx.GetTable("foo")
-		require.EqualError(t, database.ErrTableNotFound, err.Error())
+		if !errors.Is(err, database.ErrTableNotFound) {
+			require.Equal(t, err, database.ErrTableNotFound)
+		}
 
 		tb, err := tx.GetTable("zoo")
 		require.NoError(t, err)
@@ -144,7 +153,53 @@ func TestTxTable(t *testing.T) {
 
 		// Renaming a non existing table should return an error
 		err = tx.RenameTable("foo", "")
+		if !errors.Is(err, database.ErrTableNotFound) {
+			require.Equal(t, err, database.ErrTableNotFound)
+		}
+	})
+
+	t.Run("Add field", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		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},
+		}}
+		err := tx.CreateTable("foo", ti)
+		require.NoError(t, err)
+
+		// Add field
+		fieldToAdd := database.FieldConstraint{
+			Path: parsePath(t, "last_name"), Type: document.TextValue,
+		}
+		err = tx.AddField("foo", fieldToAdd)
+		require.NoError(t, err)
+
+		tb, err := tx.GetTable("foo")
+		require.NoError(t, err)
+
+		// The field constraints should not be the same.
+		info, err := tb.Info()
+		require.NoError(t, err)
+		require.Contains(t, info.FieldConstraints, fieldToAdd)
+
+		// Renaming a non existing table should return an error
+		err = tx.AddField("bar", fieldToAdd)
 		require.EqualError(t, database.ErrTableNotFound, err.Error())
+
+		// Adding a existing field should return an error
+		err = tx.AddField("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,
+		}
+		err = tx.AddField("foo", fieldToAdd)
+		require.Error(t, err)
 	})
 }
 
@@ -190,7 +245,9 @@ func TestTxCreateIndex(t *testing.T) {
 		err := tx.CreateIndex(database.IndexConfig{
 			IndexName: "idxFoo", TableName: "test", Path: parsePath(t, "foo"),
 		})
-		require.Equal(t, database.ErrTableNotFound, err)
+		if !errors.Is(err, database.ErrTableNotFound) {
+			require.Equal(t, err, database.ErrTableNotFound)
+		}
 	})
 }
 
diff --git a/document/create_test.go b/document/create_test.go
new file mode 100644
index 00000000..b85ae5d0
--- /dev/null
+++ b/document/create_test.go
@@ -0,0 +1,143 @@
+package document_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/document"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewFromJSON(t *testing.T) {
+	tests := []struct {
+		name     string
+		data     string
+		expected *document.FieldBuffer
+		fails    bool
+	}{
+		{"empty object", "{}", document.NewFieldBuffer(), false},
+		{"empty object, missing closing bracket", "{", nil, true},
+		{"classic object", `{"a": 1, "b": true, "c": "hello", "d": [1, 2, 3], "e": {"f": "g"}}`,
+			document.NewFieldBuffer().
+				Add("a", document.NewIntegerValue(1)).
+				Add("b", document.NewBoolValue(true)).
+				Add("c", document.NewTextValue("hello")).
+				Add("d", document.NewArrayValue(document.NewValueBuffer().
+					Append(document.NewIntegerValue(1)).
+					Append(document.NewIntegerValue(2)).
+					Append(document.NewIntegerValue(3)))).
+				Add("e", document.NewDocumentValue(document.NewFieldBuffer().Add("f", document.NewTextValue("g")))),
+			false},
+		{"string values", `{"a": "hello ciao"}`, document.NewFieldBuffer().Add("a", document.NewTextValue("hello ciao")), false},
+		{"+integer values", `{"a": 1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000)), false},
+		{"-integer values", `{"a": -1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(-1000)), false},
+		{"+float values", `{"a": 10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(10000000000)), false},
+		{"-float values", `{"a": -10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(-10000000000)), false},
+		{"bool values", `{"a": true, "b": false}`, document.NewFieldBuffer().Add("a", document.NewBoolValue(true)).Add("b", document.NewBoolValue(false)), false},
+		{"empty arrays", `{"a": []}`, document.NewFieldBuffer().Add("a", document.NewArrayValue(document.NewValueBuffer())), false},
+		{"nested arrays", `{"a": [[1,  2]]}`, document.NewFieldBuffer().
+			Add("a", document.NewArrayValue(
+				document.NewValueBuffer().
+					Append(document.NewArrayValue(
+						document.NewValueBuffer().
+							Append(document.NewIntegerValue(1)).
+							Append(document.NewIntegerValue(2)))))), false},
+		{"missing comma", `{"a": 1 "b": 2}`, nil, true},
+		{"missing closing brackets", `{"a": 1, "b": 2`, nil, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			d := document.NewFromJSON([]byte(test.data))
+
+			fb := document.NewFieldBuffer()
+			err := fb.Copy(d)
+
+			if test.fails {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, *test.expected, *fb)
+			}
+		})
+	}
+
+	t.Run("GetByField", func(t *testing.T) {
+		d := document.NewFromJSON([]byte(`{"a": 1000}`))
+
+		v, err := d.GetByField("a")
+		require.NoError(t, err)
+		require.Equal(t, document.NewIntegerValue(1000), v)
+
+		v, err = d.GetByField("b")
+		require.Equal(t, document.ErrFieldNotFound, err)
+	})
+}
+
+func TestNewFromMap(t *testing.T) {
+	m := map[string]interface{}{
+		"name":     "foo",
+		"age":      10,
+		"nilField": nil,
+	}
+
+	doc, err := document.NewFromMap(m)
+	require.NoError(t, err)
+
+	t.Run("Iterate", func(t *testing.T) {
+		counter := make(map[string]int)
+
+		err := doc.Iterate(func(f string, v document.Value) error {
+			counter[f]++
+			switch f {
+			case "name":
+				require.Equal(t, m[f], v.V.(string))
+			default:
+				require.EqualValues(t, m[f], v.V)
+			}
+			return nil
+		})
+		require.NoError(t, err)
+		require.Len(t, counter, 3)
+		require.Equal(t, counter["name"], 1)
+		require.Equal(t, counter["age"], 1)
+		require.Equal(t, counter["nilField"], 1)
+	})
+
+	t.Run("GetByField", func(t *testing.T) {
+		v, err := doc.GetByField("name")
+		require.NoError(t, err)
+		require.Equal(t, document.NewTextValue("foo"), v)
+
+		v, err = doc.GetByField("age")
+		require.NoError(t, err)
+		require.Equal(t, document.NewIntegerValue(10), v)
+
+		v, err = doc.GetByField("nilField")
+		require.NoError(t, err)
+		require.Equal(t, document.NewNullValue(), v)
+
+		_, err = doc.GetByField("bar")
+		require.Equal(t, document.ErrFieldNotFound, err)
+	})
+
+	t.Run("Invalid types", func(t *testing.T) {
+
+		// test NewFromMap rejects invalid types
+		_, err = document.NewFromMap(8)
+		require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map")
+		_, err = document.NewFromMap(map[int]float64{2: 4.3})
+		require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map with a string key type")
+	})
+}
+
+func BenchmarkJSONToDocument(b *testing.B) {
+	data := []byte(`{"_id":"5f8aefb8e443c6c13afdb305","index":0,"guid":"42c2719e-3371-4b2f-b855-d302a8b7eab0","isActive":true,"balance":"$1,064.79","picture":"http://placehold.it/32x32","age":40,"eyeColor":"blue","name":"Adele Webb","gender":"female","company":"EXTRAGEN","email":"adelewebb@extragen.com","phone":"+1 (964) 409-2397","address":"970 Charles Place, Watrous, Texas, 2522","about":"Amet non do ullamco duis velit sunt esse et cillum nisi mollit ea magna. Tempor ut occaecat proident laborum velit nisi et excepteur exercitation non est labore. Laboris pariatur enim proident et. Qui minim enim et incididunt incididunt adipisicing tempor. Occaecat adipisicing sint ex ut exercitation exercitation voluptate. Laboris adipisicing ut cillum eu cillum est sunt amet Lorem quis pariatur.\r\n","registered":"2016-05-25T10:36:44 -04:00","latitude":64.57112,"longitude":176.136138,"tags":["velit","minim","eiusmod","est","eu","voluptate","deserunt"],"friends":[{"id":0,"name":"Mathis Robertson"},{"id":1,"name":"Cecilia Donaldson"},{"id":2,"name":"Joann Goodwin"}],"greeting":"Hello, Adele Webb! You have 2 unread messages.","favoriteFruit":"apple"}`)
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		d := document.NewFromJSON(data)
+		d.Iterate(func(string, document.Value) error {
+			return nil
+		})
+	}
+}
diff --git a/document/document_test.go b/document/document_test.go
index ec5784cd..a95c3a9b 100644
--- a/document/document_test.go
+++ b/document/document_test.go
@@ -83,7 +83,6 @@ func TestFieldBuffer(t *testing.T) {
 	})
 
 	t.Run("Set", func(t *testing.T) {
-
 		tests := []struct {
 			name  string
 			data  string
@@ -118,9 +117,8 @@ func TestFieldBuffer(t *testing.T) {
 			t.Run(tt.name, func(t *testing.T) {
 				var fb document.FieldBuffer
 
-				d, err := document.NewFromJSON([]byte(tt.data))
-				require.NoError(t, err)
-				err = fb.Copy(d)
+				d := document.NewFromJSON([]byte(tt.data))
+				err := fb.Copy(d)
 				require.NoError(t, err)
 				p, err := parser.ParsePath(tt.path)
 				require.NoError(t, err)
@@ -227,63 +225,6 @@ func TestFieldBuffer(t *testing.T) {
 	})
 }
 
-func TestNewFromMap(t *testing.T) {
-	m := map[string]interface{}{
-		"name":     "foo",
-		"age":      10,
-		"nilField": nil,
-	}
-
-	doc, err := document.NewFromMap(m)
-	require.NoError(t, err)
-
-	t.Run("Iterate", func(t *testing.T) {
-		counter := make(map[string]int)
-
-		err := doc.Iterate(func(f string, v document.Value) error {
-			counter[f]++
-			switch f {
-			case "name":
-				require.Equal(t, m[f], v.V.(string))
-			default:
-				require.EqualValues(t, m[f], v.V)
-			}
-			return nil
-		})
-		require.NoError(t, err)
-		require.Len(t, counter, 3)
-		require.Equal(t, counter["name"], 1)
-		require.Equal(t, counter["age"], 1)
-		require.Equal(t, counter["nilField"], 1)
-	})
-
-	t.Run("GetByField", func(t *testing.T) {
-		v, err := doc.GetByField("name")
-		require.NoError(t, err)
-		require.Equal(t, document.NewTextValue("foo"), v)
-
-		v, err = doc.GetByField("age")
-		require.NoError(t, err)
-		require.Equal(t, document.NewIntegerValue(10), v)
-
-		v, err = doc.GetByField("nilField")
-		require.NoError(t, err)
-		require.Equal(t, document.NewNullValue(), v)
-
-		_, err = doc.GetByField("bar")
-		require.Equal(t, document.ErrFieldNotFound, err)
-	})
-
-	t.Run("Invalid types", func(t *testing.T) {
-
-		// test NewFromMap rejects invalid types
-		_, err = document.NewFromMap(8)
-		require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map")
-		_, err = document.NewFromMap(map[int]float64{2: 4.3})
-		require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map with a string key type")
-	})
-}
-
 func TestNewFromStruct(t *testing.T) {
 	type group struct {
 		A int
diff --git a/document/value_test.go b/document/value_test.go
index c0a08a3b..4b9a502f 100644
--- a/document/value_test.go
+++ b/document/value_test.go
@@ -21,6 +21,8 @@ func TestValueString(t *testing.T) {
 		{"bool", document.NewBoolValue(true), "true"},
 		{"int", document.NewIntegerValue(10), "10"},
 		{"double", document.NewDoubleValue(10.1), "10.1"},
+		{"double with trailing .0", document.NewDoubleValue(10), "10.0"},
+		{"big double with trailing .0", document.NewDoubleValue(1e21), "1.0e+21"},
 		{"document", document.NewDocumentValue(document.NewFieldBuffer().Add("a", document.NewIntegerValue(10))), "{\"a\": 10}"},
 		{"array", document.NewArrayValue(document.NewValueBuffer(document.NewIntegerValue(10))), "[10]"},
 	}
@@ -253,7 +255,7 @@ func TestValueMod(t *testing.T) {
 		{"integer(10)%integer(8)", document.NewIntegerValue(10), document.NewIntegerValue(8), document.NewIntegerValue(2), false},
 		{"integer(10)%float64(8)", document.NewIntegerValue(10), document.NewDoubleValue(8), document.NewDoubleValue(2), false},
 		{"int64(maxint)%float64(maxint)", document.NewIntegerValue(math.MaxInt64), document.NewDoubleValue(math.MaxInt64), document.NewDoubleValue(0), false},
-		{"double(> maxint)%int64(100)", document.NewDoubleValue(math.MaxInt64 + 1000), document.NewIntegerValue(100), document.NewDoubleValue(-8), false},
+		{"double(> maxint)%int64(100)", document.NewDoubleValue(math.MaxInt64 + 1000), document.NewIntegerValue(100), document.NewDoubleValue(8), false},
 		{"int64(100)%float64(> maxint)", document.NewIntegerValue(100), document.NewDoubleValue(math.MaxInt64 + 1000), document.NewDoubleValue(100), false},
 		{"integer(120)%text('120')", document.NewIntegerValue(120), document.NewTextValue("120"), document.NewNullValue(), false},
 		{"text('120')%text('120')", document.NewTextValue("120"), document.NewTextValue("120"), document.NewNullValue(), false},
diff --git a/engine/badgerengine/engine_test.go b/engine/badgerengine/engine_test.go
index 2016cacd..43f22702 100644
--- a/engine/badgerengine/engine_test.go
+++ b/engine/badgerengine/engine_test.go
@@ -3,7 +3,7 @@ package badgerengine_test
 import (
 	"io/ioutil"
 	"os"
-	"path"
+	"path/filepath"
 	"testing"
 
 	"github.com/dgraph-io/badger/v2"
@@ -16,7 +16,7 @@ import (
 func builder(t testing.TB) func() (engine.Engine, func()) {
 	return func() (engine.Engine, func()) {
 		dir, cleanup := tempDir(t)
-		opts := badger.DefaultOptions(path.Join(dir, "badger"))
+		opts := badger.DefaultOptions(filepath.Join(dir, "badger"))
 		opts.Logger = nil
 
 		ng, err := badgerengine.NewEngine(opts)
diff --git a/engine/badgerengine/example_test.go b/engine/badgerengine/example_test.go
index 37171a41..e639bf73 100644
--- a/engine/badgerengine/example_test.go
+++ b/engine/badgerengine/example_test.go
@@ -4,7 +4,7 @@ import (
 	"io/ioutil"
 	"log"
 	"os"
-	"path"
+	"path/filepath"
 
 	"github.com/dgraph-io/badger/v2"
 	"github.com/genjidb/genji"
@@ -18,7 +18,7 @@ func Example() {
 	}
 	defer os.RemoveAll(dir)
 
-	ng, err := badgerengine.NewEngine(badger.DefaultOptions(path.Join(dir, "badger")))
+	ng, err := badgerengine.NewEngine(badger.DefaultOptions(filepath.Join(dir, "badger")))
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/engine/boltengine/engine_test.go b/engine/boltengine/engine_test.go
index 9f89ef6d..d31e520c 100644
--- a/engine/boltengine/engine_test.go
+++ b/engine/boltengine/engine_test.go
@@ -3,7 +3,7 @@ package boltengine_test
 import (
 	"io/ioutil"
 	"os"
-	"path"
+	"path/filepath"
 	"testing"
 
 	"github.com/genjidb/genji/engine"
@@ -15,7 +15,7 @@ import (
 func builder(t testing.TB) func() (engine.Engine, func()) {
 	return func() (engine.Engine, func()) {
 		dir, cleanup := tempDir(t)
-		ng, err := boltengine.NewEngine(path.Join(dir, "test.db"), 0600, nil)
+		ng, err := boltengine.NewEngine(filepath.Join(dir, "test.db"), 0o600, nil)
 		require.NoError(t, err)
 		return ng, cleanup
 	}
diff --git a/engine/boltengine/example_test.go b/engine/boltengine/example_test.go
index c4fa2731..fe9d4d70 100644
--- a/engine/boltengine/example_test.go
+++ b/engine/boltengine/example_test.go
@@ -4,7 +4,7 @@ import (
 	"io/ioutil"
 	"log"
 	"os"
-	"path"
+	"path/filepath"
 
 	"github.com/genjidb/genji"
 	"github.com/genjidb/genji/engine/boltengine"
@@ -17,7 +17,7 @@ func Example() {
 	}
 	defer os.RemoveAll(dir)
 
-	db, err := genji.Open(path.Join(dir, "my.db"))
+	db, err := genji.Open(filepath.Join(dir, "my.db"))
 	defer db.Close()
 	if err != nil {
 		log.Fatal(err)
@@ -31,7 +31,7 @@ func ExampleNewEngine() {
 	}
 	defer os.RemoveAll(dir)
 
-	ng, err := boltengine.NewEngine(path.Join(dir, "genji.db"), 0600, nil)
+	ng, err := boltengine.NewEngine(filepath.Join(dir, "genji.db"), 0o600, nil)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/engine/enginetest/benchmark.go b/engine/enginetest/benchmark.go
index 8b354aef..7bd661a6 100644
--- a/engine/enginetest/benchmark.go
+++ b/engine/enginetest/benchmark.go
@@ -17,13 +17,14 @@ func BenchmarkStorePut(b *testing.B, builder Builder) {
 		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				st, cleanup := storeBuilder(b, builder)
+				defer cleanup()
 
+				b.ResetTimer()
 				for j := 0; j < size; j++ {
 					k := []byte(fmt.Sprintf("k%d", j))
-
 					st.Put(k, v)
 				}
-				cleanup()
+				b.StopTimer()
 			}
 		})
 	}
diff --git a/engine/enginetest/testing.go b/engine/enginetest/testing.go
index e2fdc468..474cef44 100644
--- a/engine/enginetest/testing.go
+++ b/engine/enginetest/testing.go
@@ -14,7 +14,8 @@ import (
 )
 
 // Builder is a function that can create an engine on demand and that provides
-// a function to cleanup up and remove any created state.
+// a function to cleanup up and remove any created state. Note that the engine
+// is not closed on cleanup.
 // Tests will use the builder like this:
 //     ng, cleanup := builder()
 //     defer cleanup()
@@ -65,6 +66,9 @@ func TestEngine(t *testing.T, builder Builder) {
 func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 	ng, cleanup := builder()
 	defer cleanup()
+	defer func() {
+		require.NoError(t, ng.Close())
+	}()
 
 	t.Run("Commit on read-only transaction should fail", func(t *testing.T) {
 		tx, err := ng.Begin(false)
@@ -204,6 +208,9 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 			t.Run(test.name+"/rollback", func(t *testing.T) {
 				ng, cleanup := builder()
 				defer cleanup()
+				defer func() {
+					require.NoError(t, ng.Close())
+				}()
 
 				if test.initFn != nil {
 					func() {
@@ -240,6 +247,9 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 		for _, test := range tests {
 			ng, cleanup := builder()
 			defer cleanup()
+			defer func() {
+				require.NoError(t, ng.Close())
+			}()
 
 			t.Run(test.name+"/commit", func(t *testing.T) {
 				if test.initFn != nil {
@@ -292,6 +302,9 @@ func TestTransactionCommitRollback(t *testing.T, builder Builder) {
 			t.Run(test.name, func(t *testing.T) {
 				ng, cleanup := builder()
 				defer cleanup()
+				defer func() {
+					require.NoError(t, ng.Close())
+				}()
 
 				tx, err := ng.Begin(true)
 				require.NoError(t, err)
@@ -312,6 +325,9 @@ func TestTransactionCreateStore(t *testing.T, builder Builder) {
 	t.Run("Should create a store", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
@@ -328,6 +344,9 @@ func TestTransactionCreateStore(t *testing.T, builder Builder) {
 	t.Run("Should fail if store already exists", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
@@ -345,6 +364,9 @@ func TestTransactionStore(t *testing.T, builder Builder) {
 	t.Run("Should fail if store not found", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(false)
 		require.NoError(t, err)
@@ -357,6 +379,9 @@ func TestTransactionStore(t *testing.T, builder Builder) {
 	t.Run("Should return the right store", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
@@ -397,6 +422,9 @@ func TestTransactionDropStore(t *testing.T, builder Builder) {
 	t.Run("Should drop a store", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
@@ -415,6 +443,9 @@ func TestTransactionDropStore(t *testing.T, builder Builder) {
 	t.Run("Should fail if store not found", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
@@ -434,8 +465,11 @@ func storeBuilder(t testing.TB, builder Builder) (engine.Store, func()) {
 	st, err := tx.GetStore([]byte("test"))
 	require.NoError(t, err)
 	return st, func() {
-		tx.Rollback()
-		cleanup()
+		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
+		defer tx.Rollback()
 	}
 }
 
@@ -791,6 +825,10 @@ func TestStoreNextSequence(t *testing.T, builder Builder) {
 	t.Run("Should fail if tx not writable", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
+
 		tx, err := ng.Begin(true)
 		require.NoError(t, err)
 		err = tx.CreateStore([]byte("test"))
@@ -823,7 +861,11 @@ func TestStoreNextSequence(t *testing.T, builder Builder) {
 	t.Run("Should store the last sequence", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 		tx, err := ng.Begin(true)
+
 		require.NoError(t, err)
 		err = tx.CreateStore([]byte("test"))
 		require.NoError(t, err)
@@ -855,10 +897,12 @@ func TestQueries(t *testing.T, builder Builder) {
 	t.Run("SELECT", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		ctx := context.Background()
 
@@ -895,10 +939,12 @@ func TestQueries(t *testing.T, builder Builder) {
 	t.Run("INSERT", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Exec(ctx, `
 			CREATE TABLE test;
@@ -910,10 +956,12 @@ func TestQueries(t *testing.T, builder Builder) {
 	t.Run("UPDATE", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		st, err := db.Query(ctx, `
 				CREATE TABLE test;
@@ -932,10 +980,12 @@ func TestQueries(t *testing.T, builder Builder) {
 	t.Run("DELETE", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Exec(ctx, "CREATE TABLE test")
 		require.NoError(t, err)
@@ -968,10 +1018,12 @@ func TestQueriesSameTransaction(t *testing.T, builder Builder) {
 	t.Run("SELECT", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Update(func(tx *genji.Tx) error {
 			st, err := tx.Query(ctx, `
@@ -992,10 +1044,12 @@ func TestQueriesSameTransaction(t *testing.T, builder Builder) {
 	t.Run("INSERT", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Update(func(tx *genji.Tx) error {
 			err = tx.Exec(ctx, `
@@ -1011,10 +1065,12 @@ func TestQueriesSameTransaction(t *testing.T, builder Builder) {
 	t.Run("UPDATE", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Update(func(tx *genji.Tx) error {
 			st, err := tx.Query(ctx, `
@@ -1037,10 +1093,12 @@ func TestQueriesSameTransaction(t *testing.T, builder Builder) {
 	t.Run("DELETE", func(t *testing.T) {
 		ng, cleanup := builder()
 		defer cleanup()
+		defer func() {
+			require.NoError(t, ng.Close())
+		}()
 
 		db, err := genji.New(ng)
 		require.NoError(t, err)
-		defer db.Close()
 
 		err = db.Update(func(tx *genji.Tx) error {
 			st, err := tx.Query(ctx, `
diff --git a/fuzz/fuzz_test.go b/fuzz/fuzz_test.go
new file mode 100644
index 00000000..f72bed51
--- /dev/null
+++ b/fuzz/fuzz_test.go
@@ -0,0 +1,96 @@
+package fuzz
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"strings"
+	"testing"
+)
+
+func TestFuzzParseQuery(t *testing.T) { testFuzz(t, FuzzParseQuery) }
+
+func TestFuncName(t *testing.T) {
+	got := funcName(funcName)
+	expected := "funcName"
+	if got != expected {
+		t.Errorf("funcName(%s): expected %q, got %q", expected, expected, got)
+	}
+}
+
+// funcName returns the name of a function f.
+// It is used to infer the workdir of a fuzz function.
+func funcName(f interface{}) string {
+	v := reflect.ValueOf(f)
+	pc := v.Pointer()
+	fn := runtime.FuncForPC(pc)
+	name := fn.Name()
+	return name[strings.LastIndex(name, ".")+1:]
+}
+
+// testFuzz runs fuzz function once for each input in corpus.
+// It assumes the directory structure from github.com/thepudds/fzgo.
+// That is, testdata/fuzz/<FuzzName>/corpus stores the corpus.
+func testFuzz(t *testing.T, fuzz func([]byte) int) {
+	t.Helper()
+
+	name := funcName(fuzz)
+
+	workdir := filepath.Join("testdata", "fuzz", name)
+	corpus := filepath.Join(workdir, "corpus")
+	crashers := filepath.Join(workdir, "crashers")
+
+	walkFn := func(path string, info os.FileInfo, err error) error {
+		t.Helper()
+
+		if err != nil {
+			// Do nothing if the root directory does not exist.
+			// Note that we expect testdata to be immutable
+			// while running tests (but not fuzzing).
+			if os.IsNotExist(err) {
+				return nil
+			}
+			return err
+		}
+
+		// Don’t descend into subdirectories.
+		if info.IsDir() && path != corpus && path != crashers {
+			return filepath.SkipDir
+		}
+
+		if !info.Mode().IsRegular() {
+			return nil
+		}
+
+		// Skip debug info in crashers directory.
+		if path == crashers {
+			if strings.HasSuffix(path, ".output") || strings.HasSuffix(path, ".quoted") {
+				return nil
+			}
+		}
+
+		t.Run(info.Name(), func(t *testing.T) {
+			t.Helper()
+
+			data, err := ioutil.ReadFile(path)
+			if err != nil {
+				t.Errorf("read test data: %v", err)
+				return
+			}
+
+			fuzz(data)
+		})
+
+		return nil
+	}
+
+	roots := []string{corpus, crashers}
+	for _, root := range roots {
+		err := filepath.Walk(root, walkFn)
+		if err != nil {
+			t.Errorf("walk %q: %v", root, err)
+		}
+	}
+}
diff --git a/sql/parser/alter_test.go b/sql/parser/alter_test.go
index 294fdde0..6be2f6de 100644
--- a/sql/parser/alter_test.go
+++ b/sql/parser/alter_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/sql/query"
 	"github.com/stretchr/testify/require"
 )
@@ -34,3 +36,54 @@ func TestParserAlterTable(t *testing.T) {
 		})
 	}
 }
+
+func TestParserAlterTableAddColumn(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected query.Statement
+		errored  bool
+	}{
+		{"Basic", "ALTER TABLE foo ADD FIELD bar", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path: parsePath(t, "bar"),
+			},
+		}, false},
+		{"With type", "ALTER TABLE foo ADD FIELD bar integer", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path: parsePath(t, "bar"),
+				Type: document.IntegerValue,
+			},
+		}, false},
+		{"With not null", "ALTER TABLE foo ADD FIELD bar NOT NULL", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path:      parsePath(t, "bar"),
+				IsNotNull: true,
+			},
+		}, false},
+		{"With primary key", "ALTER TABLE foo ADD FIELD bar PRIMARY KEY", query.AlterTableAddField{}, true},
+		{"With multiple constraints", "ALTER TABLE foo ADD FIELD bar integer NOT NULL DEFAULT 0", query.AlterTableAddField{TableName: "foo",
+			Constraint: database.FieldConstraint{
+				Path:         parsePath(t, "bar"),
+				Type:         document.IntegerValue,
+				IsNotNull:    true,
+				DefaultValue: document.NewIntegerValue(0),
+			},
+		}, false},
+		{"With error / missing FIELD keyword", "ALTER TABLE foo ADD bar", nil, true},
+		{"With error / missing field name", "ALTER TABLE foo ADD FIELD", nil, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := ParseQuery(context.Background(), test.s)
+			if test.errored {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Len(t, q.Statements, 1)
+			require.EqualValues(t, test.expected, q.Statements[0])
+		})
+	}
+}
diff --git a/sql/parser/create_test.go b/sql/parser/create_test.go
index 5983ece5..ebc8ab20 100644
--- a/sql/parser/create_test.go
+++ b/sql/parser/create_test.go
@@ -48,6 +48,17 @@ func TestParserCreateTable(t *testing.T) {
 					},
 				},
 			}, false},
+		{"With default", "CREATE TABLE test(foo DEFAULT \"10\")",
+			query.CreateTableStmt{
+				TableName: "test",
+				Info: database.TableInfo{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "foo"), DefaultValue: document.NewTextValue("10")},
+					},
+				},
+			}, false},
+		{"With default twice", "CREATE TABLE test(foo DEFAULT 10 DEFAULT 10)",
+			query.CreateTableStmt{}, true},
 		{"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)",
 			query.CreateTableStmt{}, true},
 		{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
@@ -116,6 +127,58 @@ func TestParserCreateTable(t *testing.T) {
 					},
 				},
 			}, false},
+		{"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{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "i"), Type: document.IntegerValue},
+						{Path: parsePath(t, "ii"), Type: document.IntegerValue},
+						{Path: parsePath(t, "ei"), Type: document.IntegerValue},
+						{Path: parsePath(t, "m"), Type: document.IntegerValue},
+						{Path: parsePath(t, "s"), Type: document.IntegerValue},
+						{Path: parsePath(t, "b"), Type: document.IntegerValue},
+						{Path: parsePath(t, "t"), Type: document.IntegerValue},
+					},
+				},
+			}, false},
+		{"With double aliases types",
+			"CREATE TABLE test(dp DOUBLE PRECISION, r real, d double)",
+			query.CreateTableStmt{
+				TableName: "test",
+				Info: database.TableInfo{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "dp"), Type: document.DoubleValue},
+						{Path: parsePath(t, "r"), Type: document.DoubleValue},
+						{Path: parsePath(t, "d"), Type: document.DoubleValue},
+					},
+				},
+			}, false},
+
+		{"With text aliases types",
+			"CREATE TABLE test(v VARCHAR(255), c CHARACTER(64), t TEXT)",
+			query.CreateTableStmt{
+				TableName: "test",
+				Info: database.TableInfo{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "v"), Type: document.TextValue},
+						{Path: parsePath(t, "c"), Type: document.TextValue},
+						{Path: parsePath(t, "t"), Type: document.TextValue},
+					},
+				},
+			}, false},
+
+		{"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{
+					FieldConstraints: []database.FieldConstraint{
+						{Path: parsePath(t, "v"), Type: document.TextValue},
+					},
+				},
+			}, true},
 	}
 
 	for _, test := range tests {
diff --git a/sql/parser/parser_test.go b/sql/parser/parser_test.go
index 0c287762..e7a151e7 100644
--- a/sql/parser/parser_test.go
+++ b/sql/parser/parser_test.go
@@ -4,9 +4,10 @@ import (
 	"context"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/genjidb/genji/sql/planner"
 	"github.com/genjidb/genji/sql/query"
-	"github.com/stretchr/testify/require"
 )
 
 func TestParserMultiStatement(t *testing.T) {
@@ -43,3 +44,10 @@ func TestParserMultiStatement(t *testing.T) {
 		})
 	}
 }
+
+func TestParserDivideByZero(t *testing.T) {
+	// See https://github.com/genjidb/genji/issues/268
+	require.NotPanics(t, func() {
+		_, _ = ParseQuery(context.Background(), "SELECT * FROM t LIMIT 0 % .5")
+	})
+}
diff --git a/sql/parser/select_test.go b/sql/parser/select_test.go
index f3a54f98..619595d6 100644
--- a/sql/parser/select_test.go
+++ b/sql/parser/select_test.go
@@ -222,6 +222,11 @@ func TestParserSelect(t *testing.T) {
 				)),
 			false},
 		{"WithOffsetThenLimit", "SELECT * FROM test WHERE age = 10 OFFSET 20 LIMIT 10", nil, true},
+		{"Invalid use of MIN() aggregator", "SELECT * FROM test LIMIT min(0)", nil, true},
+		{"Invalid use of COUNT() aggregator", "SELECT * FROM test OFFSET x(*)", nil, true},
+		{"Invalid use of MAX() aggregator", "SELECT * FROM test LIMIT max(0)", nil, true},
+		{"Invalid use of SUM() aggregator", "SELECT * FROM test LIMIT sum(0)", nil, true},
+		{"Invalid use of AVG() aggregator", "SELECT * FROM test LIMIT avg(0)", nil, true},
 	}
 
 	for _, test := range tests {
diff --git a/sql/query/alter_test.go b/sql/query/alter_test.go
index ba2953b0..d3d85be9 100644
--- a/sql/query/alter_test.go
+++ b/sql/query/alter_test.go
@@ -2,6 +2,7 @@ package query_test
 
 import (
 	"context"
+	"errors"
 	"testing"
 
 	"github.com/genjidb/genji"
@@ -33,7 +34,9 @@ func TestAlterTable(t *testing.T) {
 
 	// Selecting from the old name should fail.
 	err = db.Exec(ctx, "SELECT * FROM foo")
-	require.EqualError(t, err, database.ErrTableNotFound.Error())
+	if !errors.Is(err, database.ErrTableNotFound) {
+		require.Equal(t, err, database.ErrTableNotFound)
+	}
 
 	d, err := db.QueryDocument(ctx, "SELECT * FROM bar")
 	data, err := document.MarshalJSON(d)
diff --git a/sql/query/create_test.go b/sql/query/create_test.go
index eee8028d..c605b763 100644
--- a/sql/query/create_test.go
+++ b/sql/query/create_test.go
@@ -32,6 +32,16 @@ func TestCreateTable(t *testing.T) {
 		{"With primary key", "CREATE TABLE test(foo TEXT PRIMARY KEY)", false},
 		{"With field constraints", "CREATE TABLE test(foo.a[1][2] TEXT primary key, bar[4][0].bat INTEGER not null, baz not null)", false},
 		{"With no constraints", "CREATE TABLE test(a, b)", false},
+		{"With coherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b ARRAY, a.b[0] TEXT);", false},
+		{"With coherent constraint(any)", "CREATE TABLE test(a, a.b[0] TEXT);", false},
+		{"With coherent constraint(document)", "CREATE TABLE test(a DOCUMENT, a.b TEXT);", false},
+		{"With coherent constraint(array)", "CREATE TABLE test(a ARRAY, a[0] TEXT);", false},
+		{"With incoherent constraint(common)", "CREATE TABLE test(a INTEGER, a.b[0] TEXT);", true},
+		{"With incoherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b[0] TEXT, a.b.c TEXT);", true},
+		{"With incoherent constraint(common)", "CREATE TABLE test(a DOCUMENT, a.b.c TEXT, a.b[0] TEXT);", true},
+		{"With incoherent constraint(document)", "CREATE TABLE test(a INTEGER, a.b TEXT);", true},
+		{"With incoherent constraint(array)", "CREATE TABLE test(a INTEGER, a[0] TEXT);", true},
+		{"With duplicate constraints", "CREATE TABLE test(a INTEGER, a TEXT);", true},
 	}
 
 	for _, test := range tests {
@@ -117,6 +127,45 @@ func TestCreateTable(t *testing.T) {
 			require.NoError(t, err)
 
 		})
+
+		t.Run("with variable aliases data types", func(t *testing.T) {
+			ctx := context.Background()
+
+			err = db.Exec(ctx, `
+				CREATE TABLE test2(
+					foo.bar[1].hello bytes PRIMARY KEY, foo.a[1][2] VARCHAR(255) NOT NULL, bar[4][0].bat tinyint,
+				 	dp double precision, r real, b bigint, m mediumint, eight int8, ii int2, c character(64)
+				)
+			`)
+			require.NoError(t, err)
+
+			err = db.View(func(tx *genji.Tx) error {
+				tb, err := tx.GetTable("test2")
+				if err != nil {
+					return err
+				}
+				info, err := tb.Info()
+				if err != nil {
+					return err
+				}
+
+				require.Equal(t, []database.FieldConstraint{
+					{Path: parsePath(t, "foo.bar[1].hello"), Type: document.BlobValue, IsPrimaryKey: true},
+					{Path: parsePath(t, "foo.a[1][2]"), Type: document.TextValue, IsNotNull: true},
+					{Path: parsePath(t, "bar[4][0].bat"), Type: document.IntegerValue},
+					{Path: parsePath(t, "dp"), Type: document.DoubleValue},
+					{Path: parsePath(t, "r"), Type: document.DoubleValue},
+					{Path: parsePath(t, "b"), Type: document.IntegerValue},
+					{Path: parsePath(t, "m"), Type: document.IntegerValue},
+					{Path: parsePath(t, "eight"), Type: document.IntegerValue},
+					{Path: parsePath(t, "ii"), Type: document.IntegerValue},
+					{Path: parsePath(t, "c"), Type: document.TextValue},
+				}, info.FieldConstraints)
+				return nil
+			})
+			require.NoError(t, err)
+		})
+
 	})
 }
 
diff --git a/sql/query/expr/expr_test.go b/sql/query/expr/expr_test.go
index cbd4da65..bf12df81 100644
--- a/sql/query/expr/expr_test.go
+++ b/sql/query/expr/expr_test.go
@@ -13,13 +13,11 @@ import (
 )
 
 var doc document.Document = func() document.Document {
-	d, _ := document.NewFromJSON([]byte(`{
+	return document.NewFromJSON([]byte(`{
 		"a": 1,
 		"b": {"foo bar": [1, 2]},
 		"c": [1, {"foo": "bar"}, [1, 2]]
 	}`))
-
-	return d
 }()
 
 var stackWithDoc = expr.EvalStack{
diff --git a/sql/query/expr/path_test.go b/sql/query/expr/path_test.go
index a52dfd4b..c8f3a267 100644
--- a/sql/query/expr/path_test.go
+++ b/sql/query/expr/path_test.go
@@ -1,6 +1,7 @@
 package expr_test
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/genjidb/genji/document"
@@ -16,8 +17,10 @@ func TestPathExpr(t *testing.T) {
 	}{
 		{"a", document.NewIntegerValue(1), false},
 		{"b", func() document.Value {
-			d, _ := document.NewFromJSON([]byte(`{"foo bar": [1, 2]}`))
-			return document.NewDocumentValue(d)
+			fb := document.NewFieldBuffer()
+			err := json.Unmarshal([]byte(`{"foo bar": [1, 2]}`), fb)
+			require.NoError(t, err)
+			return document.NewDocumentValue(fb)
 		}(),
 			false},
 		{"b.`foo bar`[0]", document.NewIntegerValue(1), false},
@@ -30,12 +33,11 @@ func TestPathExpr(t *testing.T) {
 		{"d", nullLitteral, false},
 	}
 
-	d, err := document.NewFromJSON([]byte(`{
+	d := document.NewFromJSON([]byte(`{
 		"a": 1,
 		"b": {"foo bar": [1, 2]},
 		"c": [1, {"foo": "bar"}, [1, 2]]
 	}`))
-	require.NoError(t, err)
 
 	for _, test := range tests {
 		t.Run(test.expr, func(t *testing.T) {
diff --git a/sql/query/glob/like_test.go b/sql/query/glob/like_test.go
new file mode 100644
index 00000000..5c7cf4ae
--- /dev/null
+++ b/sql/query/glob/like_test.go
@@ -0,0 +1,202 @@
+package glob
+
+import (
+	"testing"
+)
+
+func TestMatchLike(t *testing.T) {
+	tests := []struct {
+		s, pattern string
+		want       bool
+	}{
+		// Empty
+		{"", "", true},
+		{"", "x", false},
+		{"x", "", false},
+
+		// One
+		{"", "_", false},
+		{"x", "_", true},
+		{"x", "__", false},
+		{"xx", "_", false},
+		{"bLah", "bL_h", true},
+		{"bLaaa", "bLa_", false},
+		{"bLah", "bLa_", true},
+		{"bLaH", "_Lah", true},
+		{"bLaH", "_LaH", true},
+
+		// All
+		{"", "%", true},
+		{"abc", "%", true},
+		{"", "%", true},
+		{"abc", "%%", true},
+
+		// Any and one
+		{"x", "%_", true},
+		{"", "%_", false},
+
+		// Escape
+		{"", "\\", true},
+		{"", "%\\", true},
+		{"", "\\%", false},
+		{"x", "%\\", true},
+		{"x", "\\%", false},
+		{"x", "_\\", true},
+		{"x", "_\\x", false},
+		{"%", "\\%", true},
+		{"_", "\\_", true},
+		{"x", "\\%", false},
+		{"x", "\\_", false},
+		{"x", "\\x", true},
+		{"ab", "a\\", false},
+		{"ab", "\\b", false},
+
+		// Escaping escape
+		{"", "\\\\", false},
+		{"x", "\\\\", false},
+		{"\\", "\\\\", true},
+		{"\\", "%\\\\", true},
+		{"\\", "\\\\%", true},
+		{"\\", "_\\\\", false},
+		{"\\", "\\\\_", false},
+		{"x\\", "\\x\\", false},
+		{"\\x", "\\\\x", true},
+
+		// Exact
+		{"abc", "abc", true},
+		{"aBc", "AbC", true},
+		{"abc", "def", false},
+
+		// Case folding
+		{"K", "\u212A", true}, // K → k → U+212A
+		{"\u212A", "k", true},
+
+		// Invalid UTF-8
+		{"\xFF", "\xFF", true},
+		{"\xFA", "\xFB", false},
+		{"\xFF", "_", true},
+		{"\xFF", "\xFF_", false},
+		{"\xFF", "%", true},
+		{"\xFF", "%\xFF%", true},
+		{"\xFF", "x", false},
+
+		// Prefix
+		{"abc", "abc%", true},
+		{"abcdef", "abc%", true},
+		{"abcdef", "def%", false},
+
+		// Suffix
+		{"abc", "%abc", true},
+		{"defabc", "%abc", true},
+		{"defabc", "%def", false},
+
+		// Contains
+		{"defabcdef", "%abc%", true},
+		{"abcd", "%def%", false},
+		{"abc", "b", false},
+
+		// Complex
+		{"abc", "ab%d", false},
+		{"ABCD", "%B%C%", true},
+		{"ABxCxxD", "a%b%c%d", true},
+		{"a", "__", false},
+		{"ab", "__", true},
+		{"abc", "___", true},
+		{"abcd", "____", true},
+		{"abc", "____", false},
+		{"abcd", "_b__", true},
+		{"abcd", "_a__", false},
+		{"abcd", "__c_", true},
+		{"abcd", "__d_", false},
+
+		// Mixed
+		{"", "%_", false},
+		{"", "_%", false},
+		{"a", "%_", true},
+		{"a", "%__", false},
+		{"ab", "%_", true},
+		{"abc", "%_", true},
+		{"ab", "_%_", true},
+		{"ab", "%_%_%", true},
+		{"aab", "%b_", false},
+		{"aaaa", "_aa%", true},
+		{"aaaa", "%aa_", true},
+		{"abc", "_%%_%_", true},
+		{"abc", "_%%_%&_", false},
+		{"abcd", "_b%__", true},
+		{"abcd", "_a%__", false},
+		{"abcd", "_%%_c_", true},
+		{"abcd", "_%%_d_", false},
+		{"abcde", "_b_d%_", true},
+		{"abcde", "_%b%_%d%_", true},
+		{"abcd", "_%b%c%_", true},
+		{"ABxCxxD", "%__B", false},
+		{"abBbc", "%b_c", true},
+
+		// Longer strings
+		{
+			"%abc%",
+			"%%\\%a%b%c\\%%%",
+			true,
+		},
+		{
+			"aaabbaabbaab",
+			"%aabbaa%a%",
+			true,
+		},
+		{
+			"abacaaadabacababacaaabadagabacaba",
+			"%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%",
+			true,
+		},
+		{
+			"aaaaaaaaaaaaaaaa",
+			"%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%",
+			false,
+		},
+		{
+			"%a%b%c%",
+			"%%%%%%%%a%%%%\\%%%%b%%%%\\%%%%c%%%%%%%%",
+			true,
+		},
+		{
+			"a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%",
+			"a%a\\%a%a\\%a%a\\%a%a\\%a%a\\%a%a\\%a%a\\%a%a\\%a%",
+			true,
+		},
+		{
+			"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
+			"a%a%a%a%a%a%aa%aaa%a%a%b",
+			true,
+		},
+		{
+			"abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",
+			"%a%b%ba%ca%a%aa%aaa%fa%ga%b%",
+			true,
+		},
+		{
+			"abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",
+			"%a%b%ba%ca%a%x%aaa%fa%ga%b%",
+			false,
+		},
+		{
+			"abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",
+			"%a%b%ba%ca%aaaa%fa%ga%gggg%b%",
+			false,
+		},
+		{
+			"abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab",
+			"%a%b%ba%ca%aaaa%fa%ga%ggg%b%",
+			true,
+		},
+	}
+
+	for _, test := range tests {
+		if got := MatchLike(test.pattern, test.s); got != test.want {
+			t.Errorf(
+				"MatchLike(%#v, %#v): expected %#v, got %#v",
+				test.pattern, test.s, test.want, got,
+			)
+		}
+	}
+}
diff --git a/sql/query/insert_test.go b/sql/query/insert_test.go
index 895a3178..3a196334 100644
--- a/sql/query/insert_test.go
+++ b/sql/query/insert_test.go
@@ -208,11 +208,45 @@ func TestInsertStmt(t *testing.T) {
 			{"double / not null with type constraint", "DOUBLE NOT NULL", `{}`},
 			{"double / not null with non-respected type constraint ", "DOUBLE NOT NULL", `{a: [1,2,3]}`},
 
+			{"double precision", "DOUBLE PRECISION", `{a: "foo"}`},
+			{"double precision / not null with type constraint", "DOUBLE PRECISION NOT NULL", `{}`},
+			{"double precision / not null with non-respected type constraint ", "DOUBLE PRECISION NOT NULL", `{a: [1,2,3]}`},
+
+			{"real", "REAL", `{a: "foo"}`},
+			{"real / not null with type constraint", "REAL NOT NULL", `{}`},
+			{"real / not null with non-respected type constraint ", "REAL NOT NULL", `{a: [1,2,3]}`},
+
 			{"integer", "INTEGER", `{a: "foo"}`},
 			{"integer / not null with type constraint", "INTEGER NOT NULL", `{}`},
 			{"integer / not null with non-respected type constraint ", "INTEGER NOT NULL", `{a: [1,2,3]}`},
 
+			{"int2", "INT2", `{a: "foo"}`},
+			{"int2 / not null with type constraint", "INT2 NOT NULL", `{}`},
+			{"int2 / not null with non-respected type constraint ", "INT NOT NULL", `{a: [1,2,3]}`},
+
+			{"int8", "INT8", `{a: "foo"}`},
+			{"int8 / not null with type constraint", "INT8 NOT NULL", `{}`},
+			{"int8 / not null with non-respected type constraint ", "INT8 NOT NULL", `{a: [1,2,3]}`},
+
+			{"tinyint", "TINYINT", `{a: "foo"}`},
+			{"tinyint / not null with type constraint", "TINYINT NOT NULL", `{}`},
+			{"tinyint / not null with non-respected type constraint ", "TINYINT NOT NULL", `{a: [1,2,3]}`},
+
+			{"bigint", "BIGINT", `{a: "foo"}`},
+			{"bigint / not null with type constraint", "BIGINT NOT NULL", `{}`},
+			{"bigint / not null with non-respected type constraint ", "BIGINT NOT NULL", `{a: [1,2,3]}`},
+
+			{"smallint", "SMALLINT", `{a: "foo"}`},
+			{"smallint / not null with type constraint", "SMALLINT NOT NULL", `{}`},
+			{"smallint / not null with non-respected type constraint ", "SMALLINT NOT NULL", `{a: [1,2,3]}`},
+
+			{"mediumint", "MEDIUMINT", `{a: "foo"}`},
+			{"mediumint / not null with type constraint", "MEDIUMINT NOT NULL", `{}`},
+			{"mediumint / not null with non-respected type constraint ", "MEDIUMINT NOT NULL", `{a: [1,2,3]}`},
+
 			{"text / not null with type constraint", "TEXT NOT NULL", `{}`},
+			{"varchar / not null with type constraint", "VARCHAR(255) NOT NULL", `{}`},
+			{"character / not null with type constraint", "CHARACTER(64) NOT NULL", `{}`},
 		}
 
 		db, err := genji.Open(":memory:")
diff --git a/sql/scanner/scanner_test.go b/sql/scanner/scanner_test.go
index c194ac8d..5e661cfe 100644
--- a/sql/scanner/scanner_test.go
+++ b/sql/scanner/scanner_test.go
@@ -54,6 +54,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `>=`, tok: scanner.GTE, raw: `>=`},
 		{s: `IN`, tok: scanner.IN, raw: `IN`},
 		{s: `IS`, tok: scanner.IS, raw: `IS`},
+		{s: `LIKE`, tok: scanner.LIKE, raw: `LIKE`},
 
 		// Misc tokens
 		{s: `(`, tok: scanner.LPAREN, raw: `(`},
@@ -116,6 +117,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `-10.3`, tok: scanner.NUMBER, lit: `-10.3`, raw: `-10.3`},
 
 		// Keywords
+		{s: `ADD`, tok: scanner.ADD_KEYWORD, raw: `ADD`},
 		{s: `ALTER`, tok: scanner.ALTER, raw: `ALTER`},
 		{s: `AS`, tok: scanner.AS, raw: `AS`},
 		{s: `ASC`, tok: scanner.ASC, raw: `ASC`},
@@ -125,9 +127,11 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `COMMIT`, tok: scanner.COMMIT, raw: `COMMIT`},
 		{s: `CREATE`, tok: scanner.CREATE, raw: `CREATE`},
 		{s: `EXPLAIN`, tok: scanner.EXPLAIN, raw: `EXPLAIN`},
+		{s: `DEFAULT`, tok: scanner.DEFAULT, raw: `DEFAULT`},
 		{s: `DELETE`, tok: scanner.DELETE, raw: `DELETE`},
 		{s: `DESC`, tok: scanner.DESC, raw: `DESC`},
 		{s: `DROP`, tok: scanner.DROP, raw: `DROP`},
+		{s: `FIELD`, tok: scanner.FIELD, raw: `FIELD`},
 		{s: `FROM`, tok: scanner.FROM, raw: `FROM`},
 		{s: `GROUP`, tok: scanner.GROUP, raw: `GROUP`},
 		{s: `INSERT`, tok: scanner.INSERT, raw: `INSERT`},