Repository: genjidb/genji
Pull request #102 (2020-06-24 06:08:22)

Add EXPLAIN statement
This PR adds basic support for the EXPLAIN statement. If the query is a SELECT, UPDATE or DELETE, It displays the query plan tree, otherwise, it returns an error.
This PR also contains some fixes on how values are marshaled into JSON


Patch:
diff --git a/document/encoding/encoding.go b/document/encoding/encoding.go
index f655aeb8..7d29e60b 100644
--- a/document/encoding/encoding.go
+++ b/document/encoding/encoding.go
@@ -360,6 +360,18 @@ func (e EncodedDocument) Iterate(fn func(field string, value document.Value) err
 	return nil
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+func (e EncodedDocument) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+
+	err := document.ToJSON(&buf, e)
+	if err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
 // An EncodedArray implements the document.Array interface on top of an encoded representation of an
 // array.
 // It is useful to avoid decoding the entire array when only a few values are needed.
@@ -398,6 +410,18 @@ func (e EncodedArray) GetByIndex(i int) (document.Value, error) {
 	return decodeValueFromDocument(e, string(EncodeInt64(int64(i))))
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+func (e EncodedArray) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+
+	err := document.ArrayToJSON(&buf, e)
+	if err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
 func decodeValueFromDocument(data []byte, field string) (document.Value, error) {
 	hsize, n := binary.Uvarint(data)
 	if n <= 0 {
diff --git a/document/json.go b/document/json.go
index 00ff2f2c..e918cac7 100644
--- a/document/json.go
+++ b/document/json.go
@@ -18,7 +18,7 @@ import (
 // NewFromJSON creates a document from a JSON object.
 func NewFromJSON(data []byte) (Document, error) {
 	var fb FieldBuffer
-	err := fb.UnmarshalJSON(data)
+	err := json.Unmarshal(data, &fb)
 	if err != nil {
 		return nil, err
 	}
@@ -35,13 +35,13 @@ func (v Value) MarshalJSON() ([]byte, error) {
 		if err != nil {
 			return nil, err
 		}
-		x = &jsonDocument{d}
+		return jsonDocument{d}.MarshalJSON()
 	case ArrayValue:
 		a, err := v.ConvertToArray()
 		if err != nil {
 			return nil, err
 		}
-		x = &jsonArray{a}
+		return jsonArray{a}.MarshalJSON()
 	case TextValue, BlobValue:
 		s, err := v.ConvertToText()
 		if err != nil {
@@ -52,33 +52,37 @@ func (v Value) MarshalJSON() ([]byte, error) {
 		x = v.V
 	}
 
-	return json.Marshal(x)
+	var buf bytes.Buffer
+	// json.Marshal uses HTML escaping by default
+	// which causes characters like > to be transformed
+	// into \u003c.
+	// to disable that we need to use json.Encoder instead
+	// and call SetEscapeHTML with false.
+	enc := json.NewEncoder(&buf)
+	enc.SetEscapeHTML(false)
+	err := enc.Encode(x)
+	if err != nil {
+		return nil, err
+	}
+
+	// json.Encoder always terminates each value with a newline,
+	// we need to remove it.
+	return buf.Bytes()[:buf.Len()-1], nil
 }
 
 // String returns a string representation of the value. It implements the fmt.Stringer interface.
 func (v Value) String() string {
 	switch v.Type {
-	case DocumentValue:
-		var buf bytes.Buffer
-		err := ToJSON(&buf, v.V.(Document))
-		if err != nil {
-			panic(err)
-		}
-		return buf.String()
-	case ArrayValue:
-		var buf bytes.Buffer
-		err := ArrayToJSON(&buf, v.V.(Array))
-		if err != nil {
-			panic(err)
-		}
-		return buf.String()
 	case NullValue:
 		return "NULL"
 	case TextValue:
-		return string(v.V.([]byte))
+		return strconv.Quote(string(v.V.([]byte)))
+	case BlobValue, DurationValue:
+		return fmt.Sprintf("%v", v.V)
 	}
 
-	return fmt.Sprintf("%v", v.V)
+	d, _ := v.MarshalJSON()
+	return string(d)
 }
 
 // MarshalJSON implements the json.Marshaler interface.
@@ -115,14 +119,36 @@ func (vb *ValueBuffer) UnmarshalJSON(data []byte) error {
 	return parseJSONArray(dec, t, vb)
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+func (m mapDocument) MarshalJSON() ([]byte, error) {
+	return json.Marshal(jsonDocument{Document: m})
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+func (m structDocument) MarshalJSON() ([]byte, error) {
+	return json.Marshal(jsonDocument{Document: m})
+}
+
 // ToJSON encodes d to w in JSON.
 func ToJSON(w io.Writer, d Document) error {
-	return json.NewEncoder(w).Encode(jsonDocument{d})
+	buf, err := jsonDocument{d}.MarshalJSON()
+	if err != nil {
+		return err
+	}
+
+	_, err = w.Write(buf)
+	return err
 }
 
 // ArrayToJSON encodes a to w in JSON.
 func ArrayToJSON(w io.Writer, a Array) error {
-	return json.NewEncoder(w).Encode(jsonArray{a})
+	buf, err := jsonArray{a}.MarshalJSON()
+	if err != nil {
+		return err
+	}
+
+	_, err = w.Write(buf)
+	return err
 }
 
 type jsonArray struct {
@@ -136,7 +162,7 @@ func (j jsonArray) MarshalJSON() ([]byte, error) {
 	var notFirst bool
 	err := j.Array.Iterate(func(i int, v Value) error {
 		if notFirst {
-			buf.WriteByte(',')
+			buf.WriteString(", ")
 		}
 		notFirst = true
 
@@ -167,12 +193,12 @@ func (j jsonDocument) MarshalJSON() ([]byte, error) {
 	var notFirst bool
 	err := j.Document.Iterate(func(f string, v Value) error {
 		if notFirst {
-			buf.WriteByte(',')
+			buf.WriteString(", ")
 		}
 		notFirst = true
 
 		buf.WriteString(strconv.Quote(f))
-		buf.WriteRune(':')
+		buf.WriteString(": ")
 
 		data, err := v.MarshalJSON()
 		if err != nil {
@@ -325,6 +351,7 @@ func parseJSONArray(dec *json.Decoder, currToken json.Token, buf *ValueBuffer) e
 // IteratorToJSON encodes all the documents of an iterator to JSON stream.
 func IteratorToJSON(w io.Writer, s Iterator) error {
 	enc := json.NewEncoder(w)
+	enc.SetEscapeHTML(false)
 	enc.SetIndent("", "  ")
 
 	return s.Iterate(func(d Document) error {
@@ -338,13 +365,10 @@ func IteratorToJSONArray(w io.Writer, s Iterator) error {
 
 	buf.WriteByte('[')
 
-	enc := json.NewEncoder(w)
-	enc.SetIndent("", "  ")
-
 	first := true
 	err := s.Iterate(func(d Document) error {
 		if !first {
-			buf.WriteByte(',')
+			buf.WriteString(", ")
 		} else {
 			first = false
 		}
diff --git a/sql/parser/explain.go b/sql/parser/explain.go
new file mode 100644
index 00000000..70cc7959
--- /dev/null
+++ b/sql/parser/explain.go
@@ -0,0 +1,25 @@
+package parser
+
+import (
+	"github.com/genjidb/genji/sql/planner"
+	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/scanner"
+)
+
+// parseExplainStatement parses any statement and returns an ExplainStmt object.
+// This function assumes the EXPLAIN token has already been consumed.
+func (p *Parser) parseExplainStatement() (query.Statement, error) {
+	// ensure we don't have multiple EXPLAIN keywords
+	tok, pos, lit := p.ScanIgnoreWhitespace()
+	if tok == scanner.EXPLAIN {
+		return nil, newParseError(scanner.Tokstr(tok, lit), []string{"SELECT", "UPDATE", "DELETE"}, pos)
+	}
+	p.Unscan()
+
+	innerStmt, err := p.ParseStatement()
+	if err != nil {
+		return nil, err
+	}
+
+	return &planner.ExplainStmt{Statement: innerStmt}, nil
+}
diff --git a/sql/parser/parser.go b/sql/parser/parser.go
index 0d491a17..c8d3bd06 100644
--- a/sql/parser/parser.go
+++ b/sql/parser/parser.go
@@ -69,6 +69,8 @@ func (p *Parser) ParseStatement() (query.Statement, error) {
 		return p.parseCreateStatement()
 	case scanner.DROP:
 		return p.parseDropStatement()
+	case scanner.EXPLAIN:
+		return p.parseExplainStatement()
 	}
 
 	return nil, newParseError(scanner.Tokstr(tok, lit), []string{
diff --git a/sql/planner/deletion.go b/sql/planner/deletion.go
index 44d44cc1..0335bef9 100644
--- a/sql/planner/deletion.go
+++ b/sql/planner/deletion.go
@@ -2,6 +2,7 @@ package planner
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
@@ -92,3 +93,7 @@ func (n *deletionNode) toResult(st document.Stream) (res query.Result, err error
 
 	return res, nil
 }
+
+func (n *deletionNode) String() string {
+	return fmt.Sprintf("Delete(%s)", n.tableName)
+}
diff --git a/sql/planner/explain.go b/sql/planner/explain.go
new file mode 100644
index 00000000..e783370b
--- /dev/null
+++ b/sql/planner/explain.go
@@ -0,0 +1,55 @@
+package planner
+
+import (
+	"errors"
+
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/query/expr"
+)
+
+// ExplainStmt is a query.Statement that
+// displays information about how a statement
+// is going to be executed, without executing it.
+type ExplainStmt struct {
+	Statement query.Statement
+}
+
+// Run analyses the inner statement and displays its execution plan.
+// If the statement is a tree, Bind and Optimize will be called prior to
+// displaying all the operations.
+// Explain currently only works on SELECT, UPDATE and DELETE statements.
+func (s *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (query.Result, error) {
+	switch t := s.Statement.(type) {
+	case *Tree:
+		err := Bind(t, tx, params)
+		if err != nil {
+			return query.Result{}, err
+		}
+
+		t, err = Optimize(t)
+		if err != nil {
+			return query.Result{}, err
+		}
+
+		return s.createResult(t.String())
+	}
+
+	return query.Result{}, errors.New("EXPLAIN only works on SELECT, UPDATE AND DELETE statements")
+}
+
+func (s *ExplainStmt) createResult(text string) (query.Result, error) {
+	return query.Result{
+		Stream: document.NewStream(
+			document.NewIterator(
+				document.NewFieldBuffer().
+					Add("plan", document.NewTextValue(text)))),
+	}, nil
+}
+
+// IsReadOnly indicates that this statement doesn't write anything into
+// the database.
+func (s *ExplainStmt) IsReadOnly() bool {
+	return true
+}
diff --git a/sql/planner/input.go b/sql/planner/input.go
index 5040faf1..971355df 100644
--- a/sql/planner/input.go
+++ b/sql/planner/input.go
@@ -2,6 +2,7 @@ package planner
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
@@ -48,6 +49,10 @@ func (n *tableInputNode) Bind(tx *database.Transaction, params []expr.Param) (er
 	return
 }
 
+func (n *tableInputNode) String() string {
+	return fmt.Sprintf("Table(%s)", n.tableName)
+}
+
 func (n *tableInputNode) buildStream() (document.Stream, error) {
 	return document.NewStream(n.table), nil
 }
@@ -123,6 +128,10 @@ func (n *indexInputNode) buildStream() (document.Stream, error) {
 	}), nil
 }
 
+func (n *indexInputNode) String() string {
+	return fmt.Sprintf("Index(%s)", n.indexName)
+}
+
 // IndexIteratorOperator is an operator that can be used
 // as an input node.
 type IndexIteratorOperator interface {
diff --git a/sql/planner/operation_string.go b/sql/planner/operation_string.go
new file mode 100644
index 00000000..6817c243
--- /dev/null
+++ b/sql/planner/operation_string.go
@@ -0,0 +1,33 @@
+// Code generated by "stringer -type=Operation"; DO NOT EDIT.
+
+package planner
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[Input-0]
+	_ = x[Selection-1]
+	_ = x[Projection-2]
+	_ = x[Rename-3]
+	_ = x[Deletion-4]
+	_ = x[Replacement-5]
+	_ = x[Limit-6]
+	_ = x[Skip-7]
+	_ = x[Sort-8]
+	_ = x[Set-9]
+	_ = x[Unset-10]
+}
+
+const _Operation_name = "InputSelectionProjectionRenameDeletionReplacementLimitSkipSortSetUnset"
+
+var _Operation_index = [...]uint8{0, 5, 14, 24, 30, 38, 49, 54, 58, 62, 65, 70}
+
+func (i Operation) String() string {
+	if i < 0 || i >= Operation(len(_Operation_index)-1) {
+		return "Operation(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _Operation_name[_Operation_index[i]:_Operation_index[i+1]]
+}
diff --git a/sql/planner/projection.go b/sql/planner/projection.go
index 7114a0ed..4ac1c09a 100644
--- a/sql/planner/projection.go
+++ b/sql/planner/projection.go
@@ -1,7 +1,10 @@
 package planner
 
 import (
+	"bytes"
 	"errors"
+	"fmt"
+	"strings"
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
@@ -127,6 +130,19 @@ func (n *ProjectionNode) toResult(st document.Stream) (res query.Result, err err
 	return
 }
 
+func (n *ProjectionNode) String() string {
+	var b strings.Builder
+
+	for i, ex := range n.Expressions {
+		if i > 0 {
+			b.WriteString(", ")
+		}
+		b.WriteString(fmt.Sprintf("%v", ex))
+	}
+
+	return fmt.Sprintf("∏(%s)", b.String())
+}
+
 type documentMask struct {
 	cfg          *database.TableConfig
 	r            document.Document
@@ -161,6 +177,18 @@ func (r documentMask) Iterate(fn func(field string, value document.Value) error)
 	return nil
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+func (r documentMask) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+
+	err := document.ToJSON(&buf, r)
+	if err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
 // A ResultField is a field that will be part of the result document that will be returned at the end of a Select statement.
 type ResultField interface {
 	Iterate(stack expr.EvalStack, fn func(field string, value document.Value) error) error
@@ -189,6 +217,10 @@ func (r ResultFieldExpr) Iterate(stack expr.EvalStack, fn func(field string, val
 	return fn(r.ExprName, v)
 }
 
+func (r ResultFieldExpr) String() string {
+	return fmt.Sprintf("%s", r.Expr)
+}
+
 // A Wildcard is a ResultField that iterates over all the fields of a document.
 type Wildcard struct{}
 
@@ -197,6 +229,10 @@ func (w Wildcard) Name() string {
 	return "*"
 }
 
+func (w Wildcard) String() string {
+	return w.Name()
+}
+
 // Iterate call the document iterate method.
 func (w Wildcard) Iterate(stack expr.EvalStack, fn func(field string, value document.Value) error) error {
 	if stack.Document == nil {
diff --git a/sql/planner/replacement.go b/sql/planner/replacement.go
index 72619cc5..eb9d2079 100644
--- a/sql/planner/replacement.go
+++ b/sql/planner/replacement.go
@@ -2,6 +2,7 @@ package planner
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
@@ -103,6 +104,10 @@ func (n *replacementNode) toResult(st document.Stream) (res query.Result, err er
 	return res, err
 }
 
+func (n *replacementNode) String() string {
+	return fmt.Sprintf("Replace(%s)", n.tableName)
+}
+
 // storeFromKey implements an engine.Store which iterates from a certain key.
 // it is used to resume iteration.
 type resumableIterator struct {
diff --git a/sql/planner/sort.go b/sql/planner/sort.go
index aa01c5a2..6abf1298 100644
--- a/sql/planner/sort.go
+++ b/sql/planner/sort.go
@@ -2,6 +2,7 @@ package planner
 
 import (
 	"container/heap"
+	"fmt"
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
@@ -59,6 +60,15 @@ func (n *sortNode) toStream(st document.Stream) (document.Stream, error) {
 	}), nil
 }
 
+func (n *sortNode) String() string {
+	dir := "ASC"
+	if n.direction == scanner.DESC {
+		dir = "DESC"
+	}
+
+	return fmt.Sprintf("Sort(%s %s)", n.sortField, dir)
+}
+
 type sortIterator struct {
 	st        document.Stream
 	sortField expr.FieldSelector
diff --git a/sql/planner/tree.go b/sql/planner/tree.go
index fdb03222..8447dab7 100644
--- a/sql/planner/tree.go
+++ b/sql/planner/tree.go
@@ -84,6 +84,30 @@ func (t *Tree) execute() (query.Result, error) {
 	return t.Root.(outputNode).toResult(st)
 }
 
+func (t *Tree) String() string {
+	n := t.Root
+
+	if n == nil {
+		return ""
+	}
+
+	return nodeToString(t.Root)
+}
+
+func nodeToString(n Node) string {
+	var s string
+
+	if n.Left() != nil {
+		s = nodeToString(n.Left())
+	}
+
+	if s == "" {
+		return fmt.Sprintf("%v", n)
+	}
+
+	return fmt.Sprintf("%s -> %v", s, n)
+}
+
 // IsReadOnly implements the query.Statement interface.
 func (t *Tree) IsReadOnly() bool {
 	return false
@@ -257,6 +281,10 @@ func (n *selectionNode) toStream(st document.Stream) (document.Stream, error) {
 	}), nil
 }
 
+func (n *selectionNode) String() string {
+	return fmt.Sprintf("σ(cond: %s)", n.cond)
+}
+
 type limitNode struct {
 	node
 
@@ -296,6 +324,10 @@ func (n *limitNode) toStream(st document.Stream) (document.Stream, error) {
 	return st.Limit(n.limit), nil
 }
 
+func (n *limitNode) String() string {
+	return fmt.Sprintf("Limit(%d)", n.limit)
+}
+
 type offsetNode struct {
 	node
 	offset int
@@ -325,6 +357,10 @@ func (n *offsetNode) IsEqual(other Node) bool {
 	return n.offset == other.(*offsetNode).offset
 }
 
+func (n *offsetNode) String() string {
+	return fmt.Sprintf("Offset(%d)", n.offset)
+}
+
 func (n *offsetNode) Bind(tx *database.Transaction, params []expr.Param) (err error) {
 	n.tx = tx
 	n.params = params
@@ -374,6 +410,10 @@ func (n *setNode) Bind(tx *database.Transaction, params []expr.Param) (err error
 	return
 }
 
+func (n *setNode) String() string {
+	return fmt.Sprintf("Set(%s = %s)", n.field, n.e)
+}
+
 func (n *setNode) toStream(st document.Stream) (document.Stream, error) {
 	var fb document.FieldBuffer
 
@@ -473,3 +513,7 @@ func (n *unsetNode) toStream(st document.Stream) (document.Stream, error) {
 		return &fb, nil
 	}), nil
 }
+
+func (n *unsetNode) String() string {
+	return fmt.Sprintf("Unset(%s)", n.field)
+}
diff --git a/sql/query/expr/arithmeric.go b/sql/query/expr/arithmeric.go
index 5c869f15..23f6a98d 100644
--- a/sql/query/expr/arithmeric.go
+++ b/sql/query/expr/arithmeric.go
@@ -1,6 +1,8 @@
 package expr
 
 import (
+	"fmt"
+
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/sql/scanner"
 )
@@ -35,6 +37,10 @@ func (op addOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.Add(b)
 }
 
+func (op addOp) String() string {
+	return fmt.Sprintf("%v + %v", op.a, op.b)
+}
+
 type subOp struct {
 	*simpleOperator
 }
@@ -53,6 +59,10 @@ func (op subOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.Sub(b)
 }
 
+func (op subOp) String() string {
+	return fmt.Sprintf("%v - %v", op.a, op.b)
+}
+
 type mulOp struct {
 	*simpleOperator
 }
@@ -71,6 +81,10 @@ func (op mulOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.Mul(b)
 }
 
+func (op mulOp) String() string {
+	return fmt.Sprintf("%v * %v", op.a, op.b)
+}
+
 type divOp struct {
 	*simpleOperator
 }
@@ -89,6 +103,10 @@ func (op divOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.Div(b)
 }
 
+func (op divOp) String() string {
+	return fmt.Sprintf("%v / %v", op.a, op.b)
+}
+
 type modOp struct {
 	*simpleOperator
 }
@@ -107,6 +125,10 @@ func (op modOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.Mod(b)
 }
 
+func (op modOp) String() string {
+	return fmt.Sprintf("%v %% %v", op.a, op.b)
+}
+
 type bitwiseAndOp struct {
 	*simpleOperator
 }
@@ -125,6 +147,10 @@ func (op bitwiseAndOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.BitwiseAnd(b)
 }
 
+func (op bitwiseAndOp) String() string {
+	return fmt.Sprintf("%v & %v", op.a, op.b)
+}
+
 type bitwiseOrOp struct {
 	*simpleOperator
 }
@@ -143,6 +169,10 @@ func (op bitwiseOrOp) Eval(ctx EvalStack) (document.Value, error) {
 	return a.BitwiseOr(b)
 }
 
+func (op bitwiseOrOp) String() string {
+	return fmt.Sprintf("%v | %v", op.a, op.b)
+}
+
 type bitwiseXorOp struct {
 	*simpleOperator
 }
@@ -160,3 +190,7 @@ func (op bitwiseXorOp) Eval(ctx EvalStack) (document.Value, error) {
 
 	return a.BitwiseXor(b)
 }
+
+func (op bitwiseXorOp) String() string {
+	return fmt.Sprintf("%v ^ %v", op.a, op.b)
+}
diff --git a/sql/query/expr/comparison.go b/sql/query/expr/comparison.go
index ade78b89..586135ba 100644
--- a/sql/query/expr/comparison.go
+++ b/sql/query/expr/comparison.go
@@ -83,6 +83,10 @@ func (op eqOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 	return fn(encoding.EncodedDocument(val))
 }
 
+func (op eqOp) String() string {
+	return fmt.Sprintf("%v = %v", op.a, op.b)
+}
+
 type neqOp struct {
 	*cmpOp
 }
@@ -92,6 +96,10 @@ func Neq(a, b Expr) Expr {
 	return neqOp{newCmpOp(a, b, scanner.NEQ)}
 }
 
+func (op neqOp) String() string {
+	return fmt.Sprintf("%v != %v", op.a, op.b)
+}
+
 type gtOp struct {
 	*cmpOp
 }
@@ -161,6 +169,10 @@ func (op gtOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 	return nil
 }
 
+func (op gtOp) String() string {
+	return fmt.Sprintf("%v > %v", op.a, op.b)
+}
+
 type gteOp struct {
 	*cmpOp
 }
@@ -218,6 +230,10 @@ func (op gteOp) IteratePK(tb *database.Table, v document.Value, pkType document.
 	return nil
 }
 
+func (op gteOp) String() string {
+	return fmt.Sprintf("%v >= %v", op.a, op.b)
+}
+
 type ltOp struct {
 	*cmpOp
 }
@@ -287,6 +303,10 @@ func (op ltOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 	return nil
 }
 
+func (op ltOp) String() string {
+	return fmt.Sprintf("%v < %v", op.a, op.b)
+}
+
 type lteOp struct {
 	*cmpOp
 }
@@ -356,6 +376,10 @@ func (op lteOp) IteratePK(tb *database.Table, v document.Value, pkType document.
 	return nil
 }
 
+func (op lteOp) String() string {
+	return fmt.Sprintf("%v <= %v", op.a, op.b)
+}
+
 // Eval compares a and b together using the operator specified when constructing the CmpOp
 // and returns the result of the comparison.
 // Comparing with NULL always evaluates to NULL.
@@ -517,6 +541,10 @@ func (op inOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 	})
 }
 
+func (op inOp) String() string {
+	return fmt.Sprintf("%v IN %v", op.a, op.b)
+}
+
 type notInOp struct {
 	inOp
 }
@@ -540,6 +568,10 @@ func (op notInOp) Eval(ctx EvalStack) (document.Value, error) {
 	return v, nil
 }
 
+func (op notInOp) String() string {
+	return fmt.Sprintf("%v NOT IN %v", op.a, op.b)
+}
+
 type isOp struct {
 	*simpleOperator
 }
@@ -566,6 +598,10 @@ func (op isOp) Eval(ctx EvalStack) (document.Value, error) {
 	return falseLitteral, nil
 }
 
+func (op isOp) String() string {
+	return fmt.Sprintf("%v IS %v", op.a, op.b)
+}
+
 type isNotOp struct {
 	*simpleOperator
 }
@@ -591,3 +627,7 @@ func (op isNotOp) Eval(ctx EvalStack) (document.Value, error) {
 
 	return falseLitteral, nil
 }
+
+func (op isNotOp) String() string {
+	return fmt.Sprintf("%v IS NOT %v", op.a, op.b)
+}
diff --git a/sql/query/expr/dot_notation.go b/sql/query/expr/dot_notation.go
index 3519304f..61437b45 100644
--- a/sql/query/expr/dot_notation.go
+++ b/sql/query/expr/dot_notation.go
@@ -92,3 +92,7 @@ func (f FieldSelector) IsEqual(other Expr) bool {
 
 	return true
 }
+
+func (f FieldSelector) String() string {
+	return f.Name()
+}
diff --git a/sql/query/expr/function.go b/sql/query/expr/function.go
index 878ff8f9..7a2b09b6 100644
--- a/sql/query/expr/function.go
+++ b/sql/query/expr/function.go
@@ -52,6 +52,10 @@ func (k PKFunc) IsEqual(other Expr) bool {
 	return ok
 }
 
+func (k PKFunc) String() string {
+	return "pk()"
+}
+
 // Cast represents the CAST expression.
 type Cast struct {
 	Expr      Expr
@@ -90,3 +94,7 @@ func (c Cast) IsEqual(other Expr) bool {
 
 	return o.Expr != nil
 }
+
+func (c Cast) String() string {
+	return fmt.Sprintf("CAST(%v AS %v)", c.Expr, c.ConvertTo)
+}
diff --git a/sql/query/expr/literal.go b/sql/query/expr/literal.go
index 6bb3ad80..98f97175 100644
--- a/sql/query/expr/literal.go
+++ b/sql/query/expr/literal.go
@@ -1,6 +1,8 @@
 package expr
 
 import (
+	"fmt"
+	"strings"
 	"time"
 
 	"github.com/genjidb/genji/document"
@@ -20,6 +22,11 @@ func (v LiteralValue) IsEqual(other Expr) bool {
 	return ok && err == nil
 }
 
+// String implements the fmt.Stringer interface.
+func (v LiteralValue) String() string {
+	return document.Value(v).String()
+}
+
 // BlobValue creates a litteral value of type Blob.
 func BlobValue(v []byte) LiteralValue {
 	return LiteralValue(document.NewBlobValue(v))
@@ -93,6 +100,22 @@ func (l LiteralExprList) IsEqual(other Expr) bool {
 	return true
 }
 
+// String implements the fmt.Stringer interface.
+func (l LiteralExprList) String() string {
+	var b strings.Builder
+
+	b.WriteRune('[')
+	for i, e := range l {
+		if i > 0 {
+			b.WriteString(", ")
+		}
+		b.WriteString(fmt.Sprintf("%v", e))
+	}
+	b.WriteRune(']')
+
+	return b.String()
+}
+
 // Eval evaluates all the expressions and returns a litteralValueList. It implements the Expr interface.
 func (l LiteralExprList) Eval(stack EvalStack) (document.Value, error) {
 	var err error
@@ -113,6 +136,11 @@ type KVPair struct {
 	V Expr
 }
 
+// String implements the fmt.Stringer interface.
+func (p KVPair) String() string {
+	return fmt.Sprintf("%q: %v", p.K, p.V)
+}
+
 // KVPairs is a list of KVPair.
 type KVPairs []KVPair
 
@@ -154,3 +182,19 @@ func (kvp KVPairs) Eval(ctx EvalStack) (document.Value, error) {
 
 	return document.NewDocumentValue(&fb), nil
 }
+
+// String implements the fmt.Stringer interface.
+func (kvp KVPairs) String() string {
+	var b strings.Builder
+
+	b.WriteRune('{')
+	for i, p := range kvp {
+		if i > 0 {
+			b.WriteString(", ")
+		}
+		b.WriteString(fmt.Sprintf("%s", p))
+	}
+	b.WriteRune('}')
+
+	return b.String()
+}
diff --git a/sql/query/expr/logical.go b/sql/query/expr/logical.go
index f431eff4..3564f647 100644
--- a/sql/query/expr/logical.go
+++ b/sql/query/expr/logical.go
@@ -1,6 +1,8 @@
 package expr
 
 import (
+	"fmt"
+
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/sql/scanner"
 )
@@ -39,6 +41,11 @@ func (op *AndOp) Eval(ctx EvalStack) (document.Value, error) {
 	return trueLitteral, nil
 }
 
+// String implements the fmt.Stringer interface.
+func (op *AndOp) String() string {
+	return fmt.Sprintf("%v AND %v", op.a, op.b)
+}
+
 // OrOp is the And operator.
 type OrOp struct {
 	*simpleOperator
@@ -78,3 +85,8 @@ func (op *OrOp) Eval(ctx EvalStack) (document.Value, error) {
 
 	return falseLitteral, nil
 }
+
+// String implements the fmt.Stringer interface.
+func (op *OrOp) String() string {
+	return fmt.Sprintf("%v OR %v", op.a, op.b)
+}
diff --git a/sql/query/expr/param.go b/sql/query/expr/param.go
index 44c06e1b..1ce27aeb 100644
--- a/sql/query/expr/param.go
+++ b/sql/query/expr/param.go
@@ -46,6 +46,11 @@ func (p NamedParam) IsEqual(other Expr) bool {
 	return ok && p == o
 }
 
+// String implements the fmt.Stringer interface.
+func (p NamedParam) String() string {
+	return fmt.Sprintf("$%s", string(p))
+}
+
 // PositionalParam is an expression which represents the position of a parameter.
 type PositionalParam int
 
@@ -67,6 +72,11 @@ func (p PositionalParam) IsEqual(other Expr) bool {
 	return ok && p == o
 }
 
+// String implements the fmt.Stringer interface.
+func (p PositionalParam) String() string {
+	return "?"
+}
+
 func (p PositionalParam) extract(params []Param) (interface{}, error) {
 	idx := int(p - 1)
 	if idx >= len(params) {
diff --git a/sql/scanner/token.go b/sql/scanner/token.go
index 6140fa3a..3263d151 100644
--- a/sql/scanner/token.go
+++ b/sql/scanner/token.go
@@ -82,6 +82,7 @@ const (
 	DESC
 	DROP
 	EXISTS
+	EXPLAIN
 	FROM
 	IF
 	INDEX
@@ -185,6 +186,7 @@ var tokens = [...]string{
 	DESC:    "DESC",
 	DROP:    "DROP",
 	EXISTS:  "EXISTS",
+	EXPLAIN: "EXPLAIN",
 	KEY:     "KEY",
 	FROM:    "FROM",
 	IF:      "IF",


Test patch:
diff --git a/document/encoding/encoding_test.go b/document/encoding/encoding_test.go
index 4d8c69d5..1d6d2b5c 100644
--- a/document/encoding/encoding_test.go
+++ b/document/encoding/encoding_test.go
@@ -159,12 +159,12 @@ func TestEncodeArray(t *testing.T) {
 				Append(document.NewTextValue("john")).
 				Append(document.NewDocumentValue(mapDoc)).
 				Append(document.NewArrayValue(document.NewValueBuffer().Append(document.NewInt64Value(11)))),
-			`[10, "john", {"city": "Ajaccio", "country": "France"}, [11]]`,
+			`[10, "john", {"city": "Ajaccio", "country": "France"}, [11]]` + "\n",
 		},
 		{
 			"Empty array",
 			document.NewValueBuffer(),
-			`[]`,
+			"[]\n",
 		},
 	}
 
diff --git a/document/json_test.go b/document/json_test.go
index bbfa37d1..bc61235b 100644
--- a/document/json_test.go
+++ b/document/json_test.go
@@ -22,7 +22,7 @@ func TestToJSON(t *testing.T) {
 				Add("name", document.NewTextValue("John")).
 				Add("age", document.NewInt16Value(10)).
 				Add(`"something with" quotes`, document.NewInt16Value(10)),
-			`{"name":"John","age":10,"\"something with\" quotes":10}` + "\n",
+			`{"name": "John", "age": 10, "\"something with\" quotes": 10}`,
 		},
 		{
 			"Nested",
@@ -38,7 +38,7 @@ func TestToJSON(t *testing.T) {
 						Append(document.NewTextValue("fred")).
 						Append(document.NewTextValue("jamie")),
 				)),
-			`{"name":"John","age":10,"address":{"city":"Ajaccio","country":"France"},"friends":["fred","jamie"]}` + "\n",
+			`{"name": "John", "age": 10, "address": {"city": "Ajaccio", "country": "France"}, "friends": ["fred", "jamie"]}`,
 		},
 	}
 
@@ -66,5 +66,5 @@ func TestIteratorToJSONArray(t *testing.T) {
 	var buf bytes.Buffer
 	err := document.IteratorToJSONArray(&buf, it)
 	require.NoError(t, err)
-	require.JSONEq(t, `[{"a": 0}, {"a": 1}, {"a": 2}]`, buf.String())
+	require.Equal(t, `[{"a": 0}, {"a": 1}, {"a": 2}]`, buf.String())
 }
diff --git a/document/value_test.go b/document/value_test.go
index 9d1b15b0..d21d7cc8 100644
--- a/document/value_test.go
+++ b/document/value_test.go
@@ -18,7 +18,7 @@ func TestValueString(t *testing.T) {
 	}{
 		{"null", document.NewNullValue(), "NULL"},
 		{"bytes", document.NewBlobValue([]byte("bar")), "[98 97 114]"},
-		{"string", document.NewTextValue("bar"), "bar"},
+		{"string", document.NewTextValue("bar"), "\"bar\""},
 		{"bool", document.NewBoolValue(true), "true"},
 		{"int", document.NewIntValue(10), "10"},
 		{"int8", document.NewInt8Value(10), "10"},
@@ -26,8 +26,8 @@ func TestValueString(t *testing.T) {
 		{"int32", document.NewInt32Value(10), "10"},
 		{"int64", document.NewInt64Value(10), "10"},
 		{"float64", document.NewFloat64Value(10.1), "10.1"},
-		{"document", document.NewDocumentValue(document.NewFieldBuffer().Add("a", document.NewIntValue(10))), "{\"a\":10}\n"},
-		{"array", document.NewArrayValue(document.NewValueBuffer(document.NewIntValue(10))), "[10]\n"},
+		{"document", document.NewDocumentValue(document.NewFieldBuffer().Add("a", document.NewIntValue(10))), "{\"a\": 10}"},
+		{"array", document.NewArrayValue(document.NewValueBuffer(document.NewIntValue(10))), "[10]"},
 		{"duration", document.NewDurationValue(10 * time.Nanosecond), "10ns"},
 	}
 
@@ -729,13 +729,12 @@ func TestValueCompare(t *testing.T) {
 	int64s := []document.Value{document.NewInt64Value(0), document.NewInt64Value(1)}
 	float64s := []document.Value{document.NewFloat64Value(0), document.NewFloat64Value(1)}
 	bools := []document.Value{document.NewBoolValue(false), document.NewBoolValue(true)}
-	texts := []document.Value{document.NewTextValue("0"), document.NewTextValue("1")}
 
 	// generate a batch of tests mixing everything with everything
-	cartesian(int8s, int16s, int32s, int64s, float64s, bools, texts)
+	cartesian(int8s, int16s, int32s, int64s, float64s, bools)
 
 	// Sample blob and text values. Values at index [0] are known to be less than values at index [1]
-	texts = []document.Value{document.NewTextValue("ABC"), document.NewTextValue("CDE")}
+	texts := []document.Value{document.NewTextValue("ABC"), document.NewTextValue("CDE")}
 	blobs := []document.Value{document.NewBlobValue([]byte{65, 66, 67}), document.NewBlobValue([]byte{68, 69, 70})}
 
 	// generate another batch of tests mixing everything with everything
diff --git a/example_test.go b/example_test.go
index ebcdbaf6..fdf36b56 100644
--- a/example_test.go
+++ b/example_test.go
@@ -1,6 +1,7 @@
 package genji_test
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 
@@ -100,6 +101,8 @@ func Example() {
 		panic(err)
 	}
 
+	enc := json.NewEncoder(os.Stdout)
+
 	// Apply some manual transformations
 	err = stream.
 		// Filter all even ids
@@ -125,7 +128,7 @@ func Example() {
 		}).
 		// Iterate on them
 		Iterate(func(d document.Document) error {
-			return document.ToJSON(os.Stdout, d)
+			return enc.Encode(d)
 		})
 
 	if err != nil {
diff --git a/sql/parser/explain_test.go b/sql/parser/explain_test.go
new file mode 100644
index 00000000..2efcea51
--- /dev/null
+++ b/sql/parser/explain_test.go
@@ -0,0 +1,34 @@
+package parser
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/sql/planner"
+	"github.com/genjidb/genji/sql/query"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParserExplain(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected query.Statement
+		errored  bool
+	}{
+		{"Explain create table", "EXPLAIN CREATE TABLE test", &planner.ExplainStmt{Statement: query.CreateTableStmt{TableName: "test"}}, false},
+		{"Multiple Explains", "EXPLAIN EXPLAIN CREATE TABLE test", nil, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := ParseQuery(test.s)
+			if test.errored {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Len(t, q.Statements, 1)
+			require.EqualValues(t, test.expected, q.Statements[0])
+		})
+	}
+}
diff --git a/sql/planner/explain_test.go b/sql/planner/explain_test.go
new file mode 100644
index 00000000..db36373e
--- /dev/null
+++ b/sql/planner/explain_test.go
@@ -0,0 +1,62 @@
+package planner_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji"
+	"github.com/stretchr/testify/require"
+)
+
+func TestExplainStmt(t *testing.T) {
+	tests := []struct {
+		query    string
+		fails    bool
+		expected string
+	}{
+		{"EXPLAIN SELECT 1 + 1", false, `"∏(1 + 1)"`},
+		{"EXPLAIN SELECT * FROM noexist", true, ``},
+		{"EXPLAIN SELECT * FROM test", false, `"Table(test) -> ∏(*)"`},
+		{"EXPLAIN SELECT a + 1 FROM test", false, `"Table(test) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE c > 10", false, `"Table(test) -> σ(cond: c > 10) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE c > 10 AND d > 20", false, `"Table(test) -> σ(cond: d > 20) -> σ(cond: c > 10) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE c > 10 OR d > 20", false, `"Table(test) -> σ(cond: c > 10 OR d > 20) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE c IN [1 + 1, 2 + 2]", false, `"Table(test) -> σ(cond: c IN [2, 4]) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE a > 10", false, `"Index(idx_a) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE a > 10 AND b > 20 AND c > 30", false, `"Index(idx_b) -> σ(cond: c > 30) -> σ(cond: a > 10) -> ∏(a + 1)"`},
+		{"EXPLAIN SELECT a + 1 FROM test WHERE c > 30 ORDER BY a DESC LIMIT 10 OFFSET 20", false, `"Table(test) -> σ(cond: c > 30) -> Sort(a DESC) -> Offset(20) -> Limit(10) -> ∏(a + 1)"`},
+		{"EXPLAIN UPDATE test SET a = 10", false, `"Table(test) -> Set(a = 10) -> Replace(test)"`},
+		{"EXPLAIN UPDATE test SET a = 10 WHERE c > 10", false, `"Table(test) -> σ(cond: c > 10) -> Set(a = 10) -> Replace(test)"`},
+		{"EXPLAIN UPDATE test SET a = 10 WHERE a > 10", false, `"Index(idx_a) -> Set(a = 10) -> Replace(test)"`},
+		{"EXPLAIN DELETE FROM test", false, `"Table(test) -> Delete(test)"`},
+		{"EXPLAIN DELETE FROM test WHERE c > 10", false, `"Table(test) -> σ(cond: c > 10) -> Delete(test)"`},
+		{"EXPLAIN DELETE FROM test WHERE a > 10", false, `"Index(idx_a) -> Delete(test)"`},
+	}
+
+	for _, test := range tests {
+		t.Run(test.query, func(t *testing.T) {
+			db, err := genji.Open(":memory:")
+			require.NoError(t, err)
+			defer db.Close()
+
+			err = db.Exec("CREATE TABLE test (k INTEGER PRIMARY KEY)")
+			require.NoError(t, err)
+			err = db.Exec(`
+						CREATE INDEX idx_a ON test (a);
+						CREATE UNIQUE INDEX idx_b ON test (b);
+					`)
+			require.NoError(t, err)
+
+			d, err := db.QueryDocument(test.query)
+			if test.fails {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			v, err := d.GetByField("plan")
+			require.NoError(t, err)
+
+			require.JSONEq(t, test.expected, v.String())
+		})
+	}
+}
diff --git a/sql/query/expr/expr_test.go b/sql/query/expr/expr_test.go
index 00e5ab6b..2e1ef07b 100644
--- a/sql/query/expr/expr_test.go
+++ b/sql/query/expr/expr_test.go
@@ -1,6 +1,7 @@
 package expr_test
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
@@ -49,3 +50,38 @@ func testExpr(t testing.TB, exprStr string, stack expr.EvalStack, want document.
 		require.Equal(t, want, res)
 	}
 }
+
+func TestString(t *testing.T) {
+	var operands = []string{
+		`10.4`,
+		"true",
+		"500",
+		`foo.bar.1`,
+		`"hello"`,
+		`[1, 2, "foo"]`,
+		`{"a": "foo", "b": 10}`,
+		"pk()",
+		"CAST(10 AS int64)",
+	}
+
+	var operators = []string{
+		"=", ">", ">=", "<", "<=",
+		"+", "-", "*", "/", "%", "&", "|", "^",
+		"AND", "OR",
+	}
+
+	testFn := func(s string, want string) {
+		e, _, err := parser.NewParser(strings.NewReader(s)).ParseExpr()
+		require.NoError(t, err)
+		require.Equal(t, want, fmt.Sprintf("%v", e))
+	}
+
+	for _, op := range operands {
+		testFn(op, op)
+	}
+
+	for _, op := range operators {
+		want := fmt.Sprintf("10.4 %s foo.bar.1", op)
+		testFn(want, want)
+	}
+}
diff --git a/sql/scanner/scanner_test.go b/sql/scanner/scanner_test.go
index c378b576..9eb478a7 100644
--- a/sql/scanner/scanner_test.go
+++ b/sql/scanner/scanner_test.go
@@ -132,6 +132,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `BY`, tok: scanner.BY, raw: `BY`},
 		{s: `CAST`, tok: scanner.CAST, raw: `CAST`},
 		{s: `CREATE`, tok: scanner.CREATE, raw: `CREATE`},
+		{s: `EXPLAIN`, tok: scanner.EXPLAIN, raw: `EXPLAIN`},
 		{s: `DELETE`, tok: scanner.DELETE, raw: `DELETE`},
 		{s: `DESC`, tok: scanner.DESC, raw: `DESC`},
 		{s: `DROP`, tok: scanner.DROP, raw: `DROP`},