Repository: genjidb/genji
Pull request #146 (2020-08-12 21:14:45)

Improved comparison rules
This PR changes the way values are compared together.
- Comparison is more strict, only values of the same type can be compared together, with the following exceptions:
  -  Integers are cast to double before doing a comparison
  -  Comparison operators no longer work for arrays and documents. Comparison operators will always return false when used against these types
- Indexes now contain one sub-index per type, with the following exceptions:
  - Integers are cast to double before being indexed
  - Arrays and documents are no longer indexed

This PR also introduces a new package `key`, which contains most of the code that used to be in `document/encoding`.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 364fd0d9..fb387dea 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,5 +16,5 @@ script:
   - go mod vendor
   - make
   - make gen
-  - go test -v -mod vendor -race -cover -timeout=2m ./...
-  - go test -v -mod vendor -race -cover -timeout=2m -tags=tinygo ./...
+  - go test -mod vendor -race -cover -timeout=2m ./...
+  - go test -mod vendor -race -cover -timeout=2m -tags=tinygo ./...
diff --git a/Makefile b/Makefile
index e80741e5..08bfe9bb 100644
--- a/Makefile
+++ b/Makefile
@@ -13,10 +13,10 @@ gen: $(NAME)
 	go generate ./...
 
 test:
-	go test -v -cover -timeout=1m ./...
+	go test -cover -timeout=1m ./...
 
 testtinygo:
-	go test -tags=tinygo -v -cover -timeout=1m ./...
+	go test -tags=tinygo -cover -timeout=1m ./...
 
 bench:
 	go test -v -run=^\$$ -benchmem -bench=. ./...
diff --git a/cmd/genji/go.sum b/cmd/genji/go.sum
index 4d2fa01e..ecc68800 100644
--- a/cmd/genji/go.sum
+++ b/cmd/genji/go.sum
@@ -96,6 +96,8 @@ github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oT
 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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
+go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
diff --git a/database/config.go b/database/config.go
index e4d4e554..bd6058e2 100644
--- a/database/config.go
+++ b/database/config.go
@@ -262,6 +262,19 @@ func (t *tableInfoStore) ListTables() []string {
 	return names
 }
 
+// GetTableInfo returns a copy of all the table information.
+func (t *tableInfoStore) GetTableInfo() map[string]TableInfo {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+
+	ti := make(map[string]TableInfo, len(t.tableInfos))
+	for k, v := range t.tableInfos {
+		ti[k] = v
+	}
+
+	return ti
+}
+
 func (t *tableInfoStore) generateStoreID() [6]byte {
 	t.mu.RLock()
 	defer t.mu.RUnlock()
@@ -321,13 +334,13 @@ func (i *IndexConfig) ScanDocument(d document.Document) error {
 	if err != nil {
 		return err
 	}
-	i.IndexName = string(v.V.([]byte))
+	i.IndexName = string(v.V.(string))
 
 	v, err = d.GetByField("tablename")
 	if err != nil {
 		return err
 	}
-	i.TableName = string(v.V.([]byte))
+	i.TableName = string(v.V.(string))
 
 	v, err = d.GetByField("path")
 	if err != nil {
@@ -438,7 +451,7 @@ func arrayToValuePath(v document.Value) (document.ValuePath, error) {
 	var path document.ValuePath
 
 	err := v.V.(document.Array).Iterate(func(_ int, value document.Value) error {
-		path = append(path, string(value.V.([]byte)))
+		path = append(path, value.V.(string))
 		return nil
 	})
 
diff --git a/database/database.go b/database/database.go
index 54f77c8e..7263fc83 100644
--- a/database/database.go
+++ b/database/database.go
@@ -97,3 +97,8 @@ func (db *Database) Begin(writable bool) (*Transaction, error) {
 
 	return &tx, nil
 }
+
+// GetTableInfo returns a map containing information about all the tables.
+func (db *Database) GetTableInfo() map[string]TableInfo {
+	return db.tableInfoStore.GetTableInfo()
+}
diff --git a/database/table.go b/database/table.go
index b20897ba..5ab9e9bc 100644
--- a/database/table.go
+++ b/database/table.go
@@ -8,10 +8,10 @@ import (
 	"strconv"
 
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding"
 	"github.com/genjidb/genji/document/encoding/msgpack"
 	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/index"
+	"github.com/genjidb/genji/key"
 )
 
 // A Table represents a collection of documents.
@@ -83,6 +83,11 @@ func (t *Table) Insert(d document.Document) ([]byte, error) {
 			v = document.NewNullValue()
 		}
 
+		// arrays and documents are not indexed.
+		if v.Type == document.ArrayValue || v.Type == document.DocumentValue {
+			continue
+		}
+
 		err = idx.Set(v, key)
 		if err != nil {
 			if err == index.ErrDuplicate {
@@ -202,7 +207,6 @@ func (t *Table) Indexes() (map[string]Index, error) {
 		name:  indexStoreName,
 	}
 
-	tableName := []byte(t.name)
 	indexes := make(map[string]Index)
 
 	err = document.NewStream(&tb).
@@ -212,7 +216,7 @@ func (t *Table) Indexes() (map[string]Index, error) {
 				return false, err
 			}
 
-			return bytes.Equal(v.V.([]byte), tableName), nil
+			return v.V.(string) == t.name, nil
 		}).
 		Iterate(func(d document.Document) error {
 			var opts IndexConfig
@@ -358,7 +362,7 @@ func (t *Table) generateKey(d document.Document) ([]byte, error) {
 			return nil, err
 		}
 
-		return encoding.EncodeValue(v)
+		return key.AppendValue(nil, v), nil
 	}
 
 	docid, err := t.generateDocid()
@@ -366,7 +370,7 @@ func (t *Table) generateKey(d document.Document) ([]byte, error) {
 		return nil, err
 	}
 
-	return encoding.EncodeInt64(docid), nil
+	return key.AppendInt64(nil, docid), nil
 }
 
 // this function looks up for the highest key in the table,
@@ -389,7 +393,7 @@ func (t *Table) generateDocid() (int64, error) {
 		it := t.Store.NewIterator(engine.IteratorConfig{Reverse: true})
 		it.Seek(nil)
 		if it.Valid() {
-			t.tx.db.tableDocids[t.name], err = encoding.DecodeInt64(it.Item().Key())
+			t.tx.db.tableDocids[t.name], err = key.DecodeInt64(it.Item().Key())
 			if err != nil {
 				it.Close()
 				return 0, err
@@ -427,8 +431,9 @@ func (t *Table) getSmallestAvailableDocid() (int64, error) {
 
 	var i int64 = 1
 
+	var buf [8]byte
 	for it.Seek(nil); it.Valid(); it.Next() {
-		if !bytes.Equal(it.Item().Key(), encoding.EncodeInt64(i)) {
+		if !bytes.Equal(it.Item().Key(), key.AppendInt64(buf[:0], i)) {
 			return i, nil
 		}
 		i++
diff --git a/document/array.go b/document/array.go
index 48225a78..9fa5abdc 100644
--- a/document/array.go
+++ b/document/array.go
@@ -32,6 +32,8 @@ func ArrayLength(a Array) (int, error) {
 	return len, err
 }
 
+var errStop = errors.New("stop")
+
 // ArrayContains iterates over a and returns whether v is equal to one of its values.
 func ArrayContains(a Array, v Value) (bool, error) {
 	var found bool
diff --git a/document/cast.go b/document/cast.go
index ba843fe6..45dbe23d 100644
--- a/document/cast.go
+++ b/document/cast.go
@@ -52,7 +52,7 @@ func (v Value) CastAsBool() (Value, error) {
 	case IntegerValue:
 		return NewBoolValue(v.V.(int64) != 0), nil
 	case TextValue:
-		b, err := strconv.ParseBool(string(v.V.([]byte)))
+		b, err := strconv.ParseBool(v.V.(string))
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as bool: %w`, v.V, err)
 		}
@@ -66,9 +66,10 @@ func (v Value) CastAsBool() (Value, error) {
 // Bool: returns 1 if true, 0 if false.
 // Double: cuts off the decimal and remaining numbers.
 // Duration: returns the number of nanoseconds in the duration.
-// Text: uses strconv.ParseFloat to determine the double value,
-// then casts it to an integer. It fails if the text doesn't
-// contain a valid float value.
+// Text: uses strconv.ParseInt to determine the integer value,
+// then casts it to an integer. If it fails uses strconv.ParseFloat
+// to determine the double value, then casts it to an integer
+// It fails if the text doesn't contain a valid float value.
 // Any other type is considered an invalid cast.
 func (v Value) CastAsInteger() (Value, error) {
 	switch v.Type {
@@ -84,11 +85,16 @@ func (v Value) CastAsInteger() (Value, error) {
 	case DurationValue:
 		return NewIntegerValue(int64(v.V.(time.Duration))), nil
 	case TextValue:
-		f, err := strconv.ParseFloat(string(v.V.([]byte)), 64)
+		i, err := strconv.ParseInt(v.V.(string), 10, 64)
 		if err != nil {
-			return Value{}, fmt.Errorf(`cannot cast %q as integer: %w`, v.V, err)
+			intErr := err
+			f, err := strconv.ParseFloat(v.V.(string), 64)
+			if err != nil {
+				return Value{}, fmt.Errorf(`cannot cast %q as integer: %w`, v.V, intErr)
+			}
+			i = int64(f)
 		}
-		return NewIntegerValue(int64(f)), nil
+		return NewIntegerValue(i), nil
 	}
 
 	return Value{}, fmt.Errorf("cannot cast %s as integer", v.Type)
@@ -106,7 +112,7 @@ func (v Value) CastAsDouble() (Value, error) {
 	case IntegerValue:
 		return NewDoubleValue(float64(v.V.(int64))), nil
 	case TextValue:
-		f, err := strconv.ParseFloat(string(v.V.([]byte)), 64)
+		f, err := strconv.ParseFloat(v.V.(string), 64)
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as double: %w`, v.V, err)
 		}
@@ -125,7 +131,7 @@ func (v Value) CastAsDuration() (Value, error) {
 	}
 
 	if v.Type == TextValue {
-		d, err := time.ParseDuration(string(v.V.([]byte)))
+		d, err := time.ParseDuration(v.V.(string))
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as duration: %w`, v.V, err)
 		}
@@ -169,7 +175,7 @@ func (v Value) CastAsBlob() (Value, error) {
 	}
 
 	if v.Type == TextValue {
-		b, err := base64.StdEncoding.DecodeString(string(v.V.([]byte)))
+		b, err := base64.StdEncoding.DecodeString(v.V.(string))
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as blob: %w`, v.V, err)
 		}
@@ -190,7 +196,7 @@ func (v Value) CastAsArray() (Value, error) {
 
 	if v.Type == TextValue {
 		var vb ValueBuffer
-		err := vb.UnmarshalJSON([]byte(string(v.V.([]byte))))
+		err := vb.UnmarshalJSON([]byte(v.V.(string)))
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as array: %w`, v.V, err)
 		}
@@ -211,7 +217,7 @@ func (v Value) CastAsDocument() (Value, error) {
 
 	if v.Type == TextValue {
 		var fb FieldBuffer
-		err := fb.UnmarshalJSON([]byte(string(v.V.([]byte))))
+		err := fb.UnmarshalJSON([]byte(v.V.(string)))
 		if err != nil {
 			return Value{}, fmt.Errorf(`cannot cast %q as document: %w`, v.V, err)
 		}
diff --git a/document/compare.go b/document/compare.go
index 882dd357..c2cddd24 100644
--- a/document/compare.go
+++ b/document/compare.go
@@ -2,8 +2,8 @@ package document
 
 import (
 	"bytes"
-	"errors"
-	"fmt"
+	"strings"
+	"time"
 )
 
 type operator uint8
@@ -74,35 +74,25 @@ func compare(op operator, l, r Value) (bool, error) {
 	case l.Type == NullValue || r.Type == NullValue:
 		return compareWithNull(op, l, r)
 
-	// compare documents together
-	case l.Type == DocumentValue && r.Type == DocumentValue:
-		return compareDocuments(op, l, r)
+	// compare booleans together
+	case l.Type == BoolValue && r.Type == BoolValue:
+		return compareBooleans(op, l.V.(bool), r.V.(bool)), nil
 
-	// compare arrays together
-	case l.Type == ArrayValue && r.Type == ArrayValue:
-		return compareArrays(op, l, r)
+	// compare texts together
+	case l.Type == TextValue && r.Type == TextValue:
+		return compareTexts(op, l.V.(string), r.V.(string)), nil
 
-	// compare boolean and another value
-	case l.Type == BoolValue || r.Type == BoolValue:
-		return compareWithBool(op, l, r)
+	// compare blobs together
+	case r.Type == BlobValue && l.Type == BlobValue:
+		return compareBlobs(op, l.V.([]byte), r.V.([]byte)), nil
 
-	// compare strings and bytes together
-	case l.Type == TextValue && r.Type == TextValue:
-		fallthrough
-	case l.Type == BlobValue && r.Type == BlobValue:
-		fallthrough
-	case l.Type == TextValue && r.Type == BlobValue:
-		fallthrough
-	case r.Type == TextValue && l.Type == BlobValue:
-		return compareBytes(op, l, r)
-
-	// integer OP integer
-	case l.Type.IsInteger() && r.Type.IsInteger():
-		return compareIntegers(op, l, r)
-
-	// number OP number
+	// compare numbers together
 	case l.Type.IsNumber() && r.Type.IsNumber():
 		return compareNumbers(op, l, r)
+
+	// compare durations together
+	case l.Type == DurationValue && r.Type == DurationValue:
+		return compareIntegers(op, int64(l.V.(time.Duration)), int64(r.V.(time.Duration))), nil
 	}
 
 	return false, nil
@@ -116,97 +106,81 @@ func compareWithNull(op operator, l, r Value) (bool, error) {
 		return false, nil
 	}
 
-	return false, fmt.Errorf("unknown operator %v", op)
+	return false, nil
 }
 
-// when comparing booleans with numbers, true equals 1 and false 0
-// when comparing booleans with other types, the boolean is always smaller.
-func compareWithBool(op operator, l, r Value) (bool, error) {
-	// if comparing a boolean with something other than a number or another bool, always return false.
-	if (!l.Type.IsNumber() && l.Type != BoolValue) || (!r.Type.IsNumber() && r.Type != BoolValue) {
-		return false, nil
-	}
-
-	var a, b bool
-
-	a, err := l.IsTruthy()
-	if err != nil {
-		return false, err
-	}
-	b, err = r.IsTruthy()
-	if err != nil {
-		return false, err
-	}
-
+func compareBooleans(op operator, a, b bool) bool {
 	switch op {
 	case operatorEq:
-		return a == b, nil
+		return a == b
 	case operatorGt:
-		return a == true && b == false, nil
+		return a == true && b == false
 	case operatorGte:
-		return a == b || a == true, nil
+		return a == b || a == true
 	case operatorLt:
-		return a == false && b == true, nil
+		return a == false && b == true
 	case operatorLte:
-		return a == b || a == false, nil
+		return a == b || a == false
 	}
 
-	return false, fmt.Errorf("unknown operator %v", op)
+	return false
 }
 
-func compareBytes(op operator, l, r Value) (bool, error) {
-	var ok bool
-
+func compareTexts(op operator, l, r string) bool {
 	switch op {
 	case operatorEq:
-		ok = bytes.Equal(l.V.([]byte), r.V.([]byte))
+		return l == r
 	case operatorGt:
-		ok = bytes.Compare(l.V.([]byte), r.V.([]byte)) > 0
+		return strings.Compare(l, r) > 0
 	case operatorGte:
-		ok = bytes.Compare(l.V.([]byte), r.V.([]byte)) >= 0
+		return strings.Compare(l, r) >= 0
 	case operatorLt:
-		ok = bytes.Compare(l.V.([]byte), r.V.([]byte)) < 0
+		return strings.Compare(l, r) < 0
 	case operatorLte:
-		ok = bytes.Compare(l.V.([]byte), r.V.([]byte)) <= 0
+		return strings.Compare(l, r) <= 0
 	}
 
-	return ok, nil
+	return false
 }
 
-func compareIntegers(op operator, l, r Value) (bool, error) {
-	l, err := l.CastAsInteger()
-	if err != nil {
-		return false, err
-	}
-	r, err = r.CastAsInteger()
-	if err != nil {
-		return false, err
+func compareBlobs(op operator, l, r []byte) bool {
+	switch op {
+	case operatorEq:
+		return bytes.Equal(l, r)
+	case operatorGt:
+		return bytes.Compare(l, r) > 0
+	case operatorGte:
+		return bytes.Compare(l, r) >= 0
+	case operatorLt:
+		return bytes.Compare(l, r) < 0
+	case operatorLte:
+		return bytes.Compare(l, r) <= 0
 	}
 
-	// integer OP integer
-	ai := l.V.(int64)
-	bi := r.V.(int64)
-
-	var ok bool
+	return false
+}
 
+func compareIntegers(op operator, l, r int64) bool {
 	switch op {
 	case operatorEq:
-		ok = ai == bi
+		return l == r
 	case operatorGt:
-		ok = ai > bi
+		return l > r
 	case operatorGte:
-		ok = ai >= bi
+		return l >= r
 	case operatorLt:
-		ok = ai < bi
+		return l < r
 	case operatorLte:
-		ok = ai <= bi
+		return l <= r
 	}
 
-	return ok, nil
+	return false
 }
 
 func compareNumbers(op operator, l, r Value) (bool, error) {
-	l, err := l.CastAsDouble()
+	var err error
+
+	l, err = l.CastAsDouble()
 	if err != nil {
 		return false, err
 	}
@@ -235,125 +209,3 @@ func compareNumbers(op operator, l, r Value) (bool, error) {
 
 	return ok, nil
 }
-
-var errStop = errors.New("stop")
-
-func compareDocuments(op operator, l, r Value) (bool, error) {
-	if op != operatorEq {
-		return false, nil
-	}
-
-	ld := l.V.(Document)
-	rd := r.V.(Document)
-
-	var lsize, rsize int
-	err := ld.Iterate(func(field string, lv Value) error {
-		lsize++
-		return nil
-	})
-	if err != nil {
-		return false, err
-	}
-	err = rd.Iterate(func(field string, lv Value) error {
-		rsize++
-		return nil
-	})
-	if err != nil {
-		return false, err
-	}
-
-	if lsize != rsize {
-		return false, nil
-	}
-
-	// if both empty documents
-	if lsize == 0 {
-		return true, nil
-	}
-
-	var ok bool
-
-	err = ld.Iterate(func(field string, lv Value) error {
-		rv, err := rd.GetByField(field)
-		if err != nil {
-			return err
-		}
-
-		ok, err = compare(op, lv, rv)
-		if err != nil {
-			return err
-		}
-
-		if !ok {
-			return errStop
-		}
-
-		return nil
-	})
-
-	if err != nil && err != errStop {
-		return false, err
-	}
-
-	return ok, nil
-}
-
-func compareArrays(op operator, l, r Value) (bool, error) {
-	la := l.V.(Array)
-	ra := r.V.(Array)
-
-	var i, j int
-
-	for {
-		lv, lerr := la.GetByIndex(i)
-		rv, rerr := ra.GetByIndex(j)
-
-		if lerr == nil {
-			i++
-		}
-		if rerr == nil {
-			j++
-		}
-
-		if lerr != nil || rerr != nil {
-			break
-		}
-
-		isEq, err := compare(operatorEq, lv, rv)
-		if err != nil {
-			return false, err
-		}
-
-		if !isEq && op != operatorEq {
-			return compare(op, lv, rv)
-		}
-
-		if !isEq {
-			return false, nil
-		}
-	}
-
-	switch {
-	case i > j:
-		switch op {
-		case operatorEq, operatorLt, operatorLte:
-			return false, nil
-		default:
-			return true, nil
-		}
-	case i < j:
-		switch op {
-		case operatorEq, operatorGt, operatorGte:
-			return false, nil
-		default:
-			return true, nil
-		}
-	default:
-		switch op {
-		case operatorEq, operatorGte, operatorLte:
-			return true, nil
-		default:
-			return false, nil
-		}
-	}
-}
diff --git a/document/encoding/encoding.go b/document/encoding/encoding.go
deleted file mode 100644
index ad684dbe..00000000
--- a/document/encoding/encoding.go
+++ /dev/null
@@ -1,277 +0,0 @@
-// Package encoding provides types and functions to encode and decode documents and values.
-//
-// Encoding values
-//
-// Each type is encoded in a way that allows ordering to be preserved. That way, if vA < vB,
-// where vA and vB are two unencoded values of the same type, then eA < eB, where eA and eB
-// are the respective encoded values of vA and vB.
-package encoding
-
-import (
-	"encoding/binary"
-	"errors"
-	"math"
-	"time"
-
-	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding/msgpack"
-)
-
-// EncodeString takes a string and returns its binary representation.
-func EncodeString(x string) []byte {
-	return []byte(x)
-}
-
-// DecodeString takes a byte slice and decodes it into a string.
-func DecodeString(buf []byte) (string, error) {
-	return string(buf), nil
-}
-
-// EncodeBool takes a bool and returns its binary representation.
-func EncodeBool(x bool) []byte {
-	if x {
-		return []byte{1}
-	}
-	return []byte{0}
-}
-
-// DecodeBool takes a byte slice and decodes it into a boolean.
-func DecodeBool(buf []byte) (bool, error) {
-	if len(buf) != 1 {
-		return false, errors.New("cannot decode buffer to bool")
-	}
-	return buf[0] == 1, nil
-}
-
-// EncodeUint takes an uint and returns its binary representation.
-func EncodeUint(x uint) []byte {
-	return EncodeUint64(uint64(x))
-}
-
-// DecodeUint takes a byte slice and decodes it into a uint.
-func DecodeUint(buf []byte) (uint, error) {
-	x, err := DecodeUint64(buf)
-	return uint(x), err
-}
-
-// EncodeUint8 takes an uint8 and returns its binary representation.
-func EncodeUint8(x uint8) []byte {
-	return []byte{x}
-}
-
-// DecodeUint8 takes a byte slice and decodes it into a uint8.
-func DecodeUint8(buf []byte) (uint8, error) {
-	if len(buf) == 0 {
-		return 0, errors.New("cannot decode buffer to uint8")
-	}
-
-	return buf[0], nil
-}
-
-// EncodeUint16 takes an uint16 and returns its binary representation.
-func EncodeUint16(x uint16) []byte {
-	var buf [2]byte
-	binary.BigEndian.PutUint16(buf[:], x)
-	return buf[:]
-}
-
-// DecodeUint16 takes a byte slice and decodes it into a uint16.
-func DecodeUint16(buf []byte) (uint16, error) {
-	if len(buf) < 2 {
-		return 0, errors.New("cannot decode buffer to uint16")
-	}
-
-	return binary.BigEndian.Uint16(buf), nil
-}
-
-// EncodeUint32 takes an uint32 and returns its binary representation.
-func EncodeUint32(x uint32) []byte {
-	var buf [4]byte
-	binary.BigEndian.PutUint32(buf[:], x)
-	return buf[:]
-}
-
-// DecodeUint32 takes a byte slice and decodes it into a uint32.
-func DecodeUint32(buf []byte) (uint32, error) {
-	if len(buf) < 4 {
-		return 0, errors.New("cannot decode buffer to uint32")
-	}
-
-	return binary.BigEndian.Uint32(buf), nil
-}
-
-// EncodeUint64 takes an uint64 and returns its binary representation.
-func EncodeUint64(x uint64) []byte {
-	var buf [8]byte
-	binary.BigEndian.PutUint64(buf[:], x)
-	return buf[:]
-}
-
-// DecodeUint64 takes a byte slice and decodes it into a uint64.
-func DecodeUint64(buf []byte) (uint64, error) {
-	if len(buf) < 8 {
-		return 0, errors.New("cannot decode buffer to uint64")
-	}
-
-	return binary.BigEndian.Uint64(buf), nil
-}
-
-// EncodeInt takes an int and returns its binary representation.
-func EncodeInt(x int) []byte {
-	return EncodeInt64(int64(x))
-}
-
-// DecodeInt takes a byte slice and decodes it into an int.
-func DecodeInt(buf []byte) (int, error) {
-	x, err := DecodeInt64(buf)
-	return int(x), err
-}
-
-// EncodeInt8 takes an int8 and returns its binary representation.
-func EncodeInt8(x int8) []byte {
-	return []byte{uint8(x + math.MaxInt8 + 1)}
-}
-
-// DecodeInt8 takes a byte slice and decodes it into an int8.
-func DecodeInt8(buf []byte) (int8, error) {
-	return int8(buf[0] - math.MaxInt8 - 1), nil
-}
-
-// EncodeInt16 takes an int16 and returns its binary representation.
-func EncodeInt16(x int16) []byte {
-	var buf [2]byte
-
-	binary.BigEndian.PutUint16(buf[:], uint16(x)+math.MaxInt16+1)
-	return buf[:]
-}
-
-// DecodeInt16 takes a byte slice and decodes it into an int16.
-func DecodeInt16(buf []byte) (int16, error) {
-	x, err := DecodeUint16(buf)
-	x -= math.MaxInt16 + 1
-	return int16(x), err
-}
-
-// EncodeInt32 takes an int32 and returns its binary representation.
-func EncodeInt32(x int32) []byte {
-	var buf [4]byte
-
-	binary.BigEndian.PutUint32(buf[:], uint32(x)+math.MaxInt32+1)
-	return buf[:]
-}
-
-// DecodeInt32 takes a byte slice and decodes it into an int32.
-func DecodeInt32(buf []byte) (int32, error) {
-	x, err := DecodeUint32(buf)
-	x -= math.MaxInt32 + 1
-	return int32(x), err
-}
-
-// EncodeInt64 takes an int64 and returns its binary representation.
-func EncodeInt64(x int64) []byte {
-	var buf [8]byte
-
-	binary.BigEndian.PutUint64(buf[:], uint64(x)+math.MaxInt64+1)
-	return buf[:]
-}
-
-// DecodeInt64 takes a byte slice and decodes it into an int64.
-func DecodeInt64(buf []byte) (int64, error) {
-	x, err := DecodeUint64(buf)
-	x -= math.MaxInt64 + 1
-	return int64(x), err
-}
-
-// EncodeFloat64 takes an float64 and returns its binary representation.
-func EncodeFloat64(x float64) []byte {
-	fb := math.Float64bits(x)
-	if x >= 0 {
-		fb ^= 1 << 63
-	} else {
-		fb ^= 1<<64 - 1
-	}
-	return EncodeUint64(fb)
-}
-
-// DecodeFloat64 takes a byte slice and decodes it into an float64.
-func DecodeFloat64(buf []byte) (float64, error) {
-	x := binary.BigEndian.Uint64(buf)
-
-	if (x & (1 << 63)) != 0 {
-		x ^= 1 << 63
-	} else {
-		x ^= 1<<64 - 1
-	}
-	return math.Float64frombits(x), nil
-}
-
-func EncodeValue(v document.Value) ([]byte, error) {
-	switch v.Type {
-	case document.DocumentValue:
-		return msgpack.EncodeDocument(v.V.(document.Document))
-	case document.ArrayValue:
-		return msgpack.EncodeArray(v.V.(document.Array))
-	case document.BlobValue:
-		return v.V.([]byte), nil
-	case document.TextValue:
-		return EncodeString(string(v.V.([]byte))), nil
-	case document.BoolValue:
-		return EncodeBool(v.V.(bool)), nil
-	case document.IntegerValue:
-		return EncodeInt64(v.V.(int64)), nil
-	case document.DoubleValue:
-		return EncodeFloat64(v.V.(float64)), nil
-	case document.DurationValue:
-		return EncodeInt64(int64(v.V.(time.Duration))), nil
-	case document.NullValue:
-		return nil, nil
-	}
-
-	return nil, errors.New("unknown type")
-}
-
-// DecodeValue takes some encoded data and decodes it to the target type t.
-func DecodeValue(t document.ValueType, data []byte) (document.Value, error) {
-	switch t {
-	case document.DocumentValue:
-		return document.NewDocumentValue(msgpack.EncodedDocument(data)), nil
-	case document.ArrayValue:
-		return document.NewArrayValue(msgpack.EncodedArray(data)), nil
-	case document.BlobValue:
-		return document.NewBlobValue(data), nil
-	case document.TextValue:
-		x, err := DecodeString(data)
-		if err != nil {
-			return document.Value{}, err
-		}
-		return document.NewTextValue(x), nil
-	case document.BoolValue:
-		x, err := DecodeBool(data)
-		if err != nil {
-			return document.Value{}, err
-		}
-		return document.NewBoolValue(x), nil
-	case document.IntegerValue:
-		x, err := DecodeInt64(data)
-		if err != nil {
-			return document.Value{}, err
-		}
-		return document.NewIntegerValue(x), nil
-	case document.DoubleValue:
-		x, err := DecodeFloat64(data)
-		if err != nil {
-			return document.Value{}, err
-		}
-		return document.NewDoubleValue(x), nil
-	case document.DurationValue:
-		x, err := DecodeInt64(data)
-		if err != nil {
-			return document.Value{}, err
-		}
-		return document.NewDurationValue(time.Duration(x)), nil
-	case document.NullValue:
-		return document.NewNullValue(), nil
-	}
-
-	return document.Value{}, errors.New("unknown type")
-}
diff --git a/document/encoding/msgpack/codec.go b/document/encoding/msgpack/codec.go
index c070158c..1f0e741d 100644
--- a/document/encoding/msgpack/codec.go
+++ b/document/encoding/msgpack/codec.go
@@ -106,7 +106,7 @@ func (e *Encoder) EncodeValue(v document.Value) error {
 	case document.NullValue:
 		return e.enc.EncodeNil()
 	case document.TextValue:
-		return e.enc.EncodeString(string(v.V.([]byte)))
+		return e.enc.EncodeString(v.V.(string))
 	case document.BlobValue:
 		return e.enc.EncodeBytes(v.V.([]byte))
 	case document.BoolValue:
@@ -205,8 +205,7 @@ func (d *Decoder) DecodeValue() (v document.Value, err error) {
 		if err != nil {
 			return
 		}
-		v.V = []byte(s)
-		v.Type = document.TextValue
+		v = document.NewTextValue(s)
 		return
 	}
 
diff --git a/document/json.go b/document/json.go
index 464bf4da..55827050 100644
--- a/document/json.go
+++ b/document/json.go
@@ -32,7 +32,7 @@ func (v Value) MarshalJSON() ([]byte, error) {
 	case ArrayValue:
 		return jsonArray{v.V.(Array)}.MarshalJSON()
 	case TextValue:
-		x = string(v.V.([]byte))
+		x = string(v.V.(string))
 	case DurationValue:
 		x = v.V.(time.Duration).String()
 	default:
@@ -63,7 +63,7 @@ func (v Value) String() string {
 	case NullValue:
 		return "NULL"
 	case TextValue:
-		return strconv.Quote(string(v.V.([]byte)))
+		return strconv.Quote(v.V.(string))
 	case BlobValue, DurationValue:
 		return fmt.Sprintf("%v", v.V)
 	}
diff --git a/document/scan.go b/document/scan.go
index 30856dbb..7358dae1 100644
--- a/document/scan.go
+++ b/document/scan.go
@@ -249,7 +249,7 @@ func scanValue(v Value, ref reflect.Value) error {
 		if err != nil {
 			return err
 		}
-		ref.SetString(string(v.V.([]byte)))
+		ref.SetString(string(v.V.(string)))
 		return nil
 	case reflect.Bool:
 		v, err := v.CastAsBool()
@@ -295,7 +295,11 @@ func scanValue(v Value, ref reflect.Value) error {
 			if v.Type != TextValue && v.Type != BlobValue {
 				return fmt.Errorf("cannot scan value of type %s to byte slice", v.Type)
 			}
-			ref.SetBytes(v.V.([]byte))
+			if v.Type == TextValue {
+				ref.SetBytes([]byte(v.V.(string)))
+			} else {
+				ref.SetBytes(v.V.([]byte))
+			}
 			return nil
 		}
 		v, err := v.CastAsArray()
diff --git a/document/value.go b/document/value.go
index 6455d57f..e66e4940 100644
--- a/document/value.go
+++ b/document/value.go
@@ -38,26 +38,28 @@ type ValueType uint8
 const (
 	NullValue ValueType = 0x1
 
-	BoolValue = 0x5
+	BoolValue ValueType = 0x5
 
 	// integer family: 0x10 to 0x1F
-	IntegerValue = 0x10
+	IntegerValue ValueType = 0x10
 
 	// double family: 0x20 to 0x2F
-	DoubleValue = 0x20
+	DoubleValue ValueType = 0x20
 
 	// time family: 0x30 to 0x3F
-	DurationValue = 0x30
+	DurationValue ValueType = 0x30
 
 	// string family: 0x40 to 0x4F
-	BlobValue = 0x40
-	TextValue = 0x41
+	TextValue ValueType = 0x40
 
-	// array family: 0x40 to 0x5F
-	ArrayValue = 0x50
+	// blob family: 0x50 to 0x5F
+	BlobValue ValueType = 0x50
 
-	// document family: 0x60 to 0x6F
-	DocumentValue = 0x60
+	// array family: 0x60 to 0x6F
+	ArrayValue ValueType = 0x60
+
+	// document family: 0x70 to 0x7F
+	DocumentValue ValueType = 0x70
 )
 
 func (t ValueType) String() string {
@@ -87,17 +89,7 @@ func (t ValueType) String() string {
 
 // IsNumber returns true if t is either an integer of a float.
 func (t ValueType) IsNumber() bool {
-	return t.IsInteger() || t.IsFloat() || t == DurationValue
-}
-
-// IsInteger returns true if t is a signed or unsigned integer of any size.
-func (t ValueType) IsInteger() bool {
-	return t == IntegerValue || t == DurationValue
-}
-
-// IsFloat returns true if t is a Double.
-func (t ValueType) IsFloat() bool {
-	return t == DoubleValue
+	return t == IntegerValue || t == DoubleValue
 }
 
 // A Value stores encoded data alongside its type.
@@ -158,7 +150,7 @@ func NewBlobValue(x []byte) Value {
 func NewTextValue(x string) Value {
 	return Value{
 		Type: TextValue,
-		V:    []byte(x),
+		V:    x,
 	}
 }
 
@@ -227,8 +219,10 @@ func (v Value) IsZeroValue() (bool, error) {
 		return v.V == doubleZeroValue.V, nil
 	case DurationValue:
 		return v.V == durationZeroValue.V, nil
-	case BlobValue, TextValue:
+	case BlobValue:
 		return bytes.Compare(v.V.([]byte), blobZeroValue.V.([]byte)) == 0, nil
+	case TextValue:
+		return v.V == textZeroValue.V, nil
 	case ArrayValue:
 		// The zero value of an array is an empty array.
 		// Thus, if GetByIndex(0) returns the ErrValueNotFound
@@ -318,18 +312,8 @@ func calculateValues(a, b Value, operator byte) (res Value, err error) {
 		return NewNullValue(), nil
 	}
 
-	if a.Type == BoolValue {
-		a, err = a.CastAs(IntegerValue)
-		if err != nil {
-			return
-		}
-	}
-
-	if b.Type == BoolValue {
-		b, err = b.CastAs(IntegerValue)
-		if err != nil {
-			return
-		}
+	if a.Type == BoolValue || b.Type == BoolValue {
+		return NewNullValue(), nil
 	}
 
 	if a.Type == DurationValue && b.Type == DurationValue {
@@ -345,11 +329,11 @@ func calculateValues(a, b Value, operator byte) (res Value, err error) {
 	}
 
 	if a.Type.IsNumber() && b.Type.IsNumber() {
-		if a.Type.IsFloat() || b.Type.IsFloat() {
+		if a.Type == DoubleValue || b.Type == DoubleValue {
 			return calculateFloats(a, b, operator)
 		}
 
-		if a.Type.IsInteger() || b.Type.IsInteger() {
+		if a.Type == IntegerValue || b.Type == IntegerValue {
 			return calculateIntegers(a, b, operator)
 		}
 	}
diff --git a/go.mod b/go.mod
index fd76fc94..59c1fe33 100644
--- a/go.mod
+++ b/go.mod
@@ -4,8 +4,7 @@ go 1.15
 
 require (
 	github.com/google/btree v1.0.0
-	github.com/stretchr/testify v1.5.1
+	github.com/stretchr/testify v1.6.1
 	github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1
-	go.etcd.io/bbolt v1.3.4
-	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	go.etcd.io/bbolt v1.3.5
 )
diff --git a/go.sum b/go.sum
index b0209b73..13ffbf9e 100644
--- a/go.sum
+++ b/go.sum
@@ -14,9 +14,12 @@ 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 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=
@@ -44,3 +47,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 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/index/index.go b/index/index.go
index 1ed2f95d..37392384 100644
--- a/index/index.go
+++ b/index/index.go
@@ -3,11 +3,8 @@ package index
 import (
 	"bytes"
 	"errors"
-	"fmt"
 
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding"
-	"github.com/genjidb/genji/document/encoding/msgpack"
 	"github.com/genjidb/genji/engine"
 )
 
@@ -18,40 +15,15 @@ const (
 	separator byte = 0x1E
 )
 
-// Type of the index. Values are stored in different sub indexes depending on their types.
-// They are automatically converted to one of the following types:
-//
-// Text and Blob values are stored in Bytes indexes.
-// Signed, unsigned integers, and floats are stored in Float indexes.
-// Booleans are stores in Bool indexes.
-type Type byte
-
-// index value types
-const (
-	Null Type = iota + 1
-	Bool
-	Float
-	Bytes
-	Array
-	Document
-)
-
-// NewTypeFromValueType returns the right index type associated with t.
-func NewTypeFromValueType(t document.ValueType) Type {
-	switch {
-	case t.IsNumber():
-		return Float
-	case t == document.TextValue || t == document.BlobValue:
-		return Bytes
-	case t == document.BoolValue:
-		return Bool
-	case t == document.ArrayValue:
-		return Array
-	case t == document.DocumentValue:
-		return Document
-	}
-
-	return Null
+var valueTypes = []document.ValueType{
+	document.NullValue,
+	document.BoolValue,
+	document.DoubleValue,
+	document.DurationValue,
+	document.TextValue,
+	document.BlobValue,
+	document.ArrayValue,
+	document.DocumentValue,
 }
 
 var (
@@ -71,12 +43,14 @@ type Index interface {
 	// AscendGreaterOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in increasing order and calls the given function for each pair.
 	// If the given function returns an error, the iteration stops and returns that error.
 	// If the pivot is nil, starts from the beginning.
-	AscendGreaterOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error
+	// If val is equal to the pivot, isEqual is set to true.
+	AscendGreaterOrEqual(pivot document.Value, fn func(val []byte, key []byte, isEqual bool) error) error
 
 	// DescendLessOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in descreasing order and calls the given function for each pair.
 	// If the given function returns an error, the iteration stops and returns that error.
 	// If the pivot is nil, starts from the end.
-	DescendLessOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error
+	// If val is equal to the pivot, isEqual is set to true.
+	DescendLessOrEqual(pivot document.Value, fn func(val []byte, key []byte, isEqual bool) error) error
 
 	// Truncate deletes all the index data.
 	Truncate() error
@@ -98,7 +72,7 @@ func NewUniqueIndex(tx engine.Transaction, idxName string) *UniqueIndex {
 	}
 }
 
-func buildIndexName(name []byte, t Type) []byte {
+func buildIndexName(name []byte, t document.ValueType) []byte {
 	var buf bytes.Buffer
 
 	// We can deterministically set the size of the buffer.
@@ -113,60 +87,8 @@ func buildIndexName(name []byte, t Type) []byte {
 	return buf.Bytes()
 }
 
-// A Pivot is a value that is used to seek for a particular value in an index.
-// A Pivot is typed and can only be used to seek for values of the same type.
-type Pivot struct {
-	Value document.Value
-	empty bool
-}
-
-// EmptyPivot returns a pivot that starts at the beginning of any indexed values compatible with the given type.
-func EmptyPivot(t document.ValueType) *Pivot {
-	v := document.NewZeroValue(t)
-	return &Pivot{
-		Value: v,
-		empty: true,
-	}
-}
-
-// EncodeFieldToIndexValue returns a byte array that represents the value in such
-// a way that can be compared for ordering and indexing
-func EncodeFieldToIndexValue(val document.Value) ([]byte, error) {
-	if val.V != nil && val.Type.IsNumber() && val.Type != document.DoubleValue {
-		v, err := val.CastAsDouble()
-		if err != nil {
-			return nil, err
-		}
-
-		return encoding.EncodeFloat64(v.V.(float64)), nil
-	}
-
-	return encoding.EncodeValue(val)
-}
-
-func decodeIndexValueToField(t Type, data []byte) (document.Value, error) {
-	switch t {
-	case Null:
-		return document.NewNullValue(), nil
-	case Bytes:
-		return document.NewBlobValue(data), nil
-	case Float:
-		f, err := encoding.DecodeFloat64(data)
-		return document.NewDoubleValue(f), err
-	case Bool:
-		b, err := encoding.DecodeBool(data)
-		return document.NewBoolValue(b), err
-	case Array:
-		return document.NewArrayValue(msgpack.DecodeArray(data)), nil
-	case Document:
-		return document.NewDocumentValue(msgpack.DecodeDocument(data)), nil
-	}
-
-	return document.Value{}, fmt.Errorf("unknown index type %d", t)
-}
-
 func getOrCreateStore(tx engine.Transaction, t document.ValueType, name string) (engine.Store, error) {
-	idxName := buildIndexName([]byte(name), NewTypeFromValueType(t))
+	idxName := buildIndexName([]byte(name), t)
 	st, err := tx.GetStore(idxName)
 	if err == nil {
 		return st, nil
@@ -184,7 +106,7 @@ func getOrCreateStore(tx engine.Transaction, t document.ValueType, name string)
 	return tx.GetStore(idxName)
 }
 
-func getStore(tx engine.Transaction, t Type, name string) (engine.Store, error) {
+func getStore(tx engine.Transaction, t document.ValueType, name string) (engine.Store, error) {
 	idxName := buildIndexName([]byte(name), t)
 	st, err := tx.GetStore(idxName)
 	if err == nil || err == engine.ErrStoreNotFound {
@@ -194,7 +116,7 @@ func getStore(tx engine.Transaction, t Type, name string) (engine.Store, error)
 	return nil, err
 }
 
-func dropStore(tx engine.Transaction, t Type, name string) error {
+func dropStore(tx engine.Transaction, t document.ValueType, name string) error {
 	idxName := buildIndexName([]byte(name), t)
 	_, err := tx.GetStore(idxName)
 	if err != nil && err != engine.ErrStoreNotFound {
diff --git a/index/list.go b/index/list.go
index 1879352d..2e32d171 100644
--- a/index/list.go
+++ b/index/list.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine"
+	"github.com/genjidb/genji/key"
 )
 
 // ListIndex is an implementation that associates a value with a list of keys.
@@ -15,41 +16,53 @@ type ListIndex struct {
 
 // Set associates a value with a key. It is possible to associate multiple keys for the same value
 // but a key can be associated to only one value.
-func (i *ListIndex) Set(val document.Value, key []byte) error {
-	st, err := getOrCreateStore(i.tx, val.Type, i.name)
-	if err != nil {
-		return err
+func (idx *ListIndex) Set(v document.Value, k []byte) error {
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
+		if err != nil {
+			return err
+		}
 	}
 
-	v, err := EncodeFieldToIndexValue(val)
+	st, err := getOrCreateStore(idx.tx, v.Type, idx.name)
 	if err != nil {
 		return err
 	}
 
-	buf := make([]byte, 0, len(v)+len(key)+1)
-	buf = append(buf, v...)
+	enc := key.AppendValue(nil, v)
+
+	buf := make([]byte, 0, len(enc)+len(k)+1)
+	buf = append(buf, enc...)
 	buf = append(buf, separator)
-	buf = append(buf, key...)
+	buf = append(buf, k...)
 
 	return st.Put(buf, nil)
 }
 
 // Delete all the references to the key from the index.
-func (i *ListIndex) Delete(val document.Value, key []byte) error {
-	v, err := EncodeFieldToIndexValue(val)
-	if err != nil {
-		return err
+func (idx *ListIndex) Delete(v document.Value, k []byte) error {
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
+		if err != nil {
+			return err
+		}
 	}
 
-	st, err := getOrCreateStore(i.tx, val.Type, i.name)
+	enc := key.AppendValue(nil, v)
+
+	st, err := getOrCreateStore(idx.tx, v.Type, idx.name)
 	if err != nil {
 		return err
 	}
 
-	buf := make([]byte, 0, len(v)+len(key)+1)
-	buf = append(buf, v...)
+	buf := make([]byte, 0, len(enc)+len(k)+1)
+	buf = append(buf, enc...)
 	buf = append(buf, separator)
-	buf = append(buf, key...)
+	buf = append(buf, k...)
 
 	return st.Delete(buf)
 }
@@ -57,36 +70,11 @@ func (i *ListIndex) Delete(val document.Value, key []byte) error {
 // AscendGreaterOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in increasing order and calls the given function for each pair.
 // If the given function returns an error, the iteration stops and returns that error.
 // If the pivot is nil, starts from the beginning.
-func (i *ListIndex) AscendGreaterOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error {
+func (idx *ListIndex) AscendGreaterOrEqual(pivot document.Value, fn func(val, key []byte, isEqual bool) error) error {
 	// iterate over all stores in order
-	if pivot == nil {
-		for t := Null; t <= Bytes; t++ {
-			st, err := getStore(i.tx, t, i.name)
-			if err != nil {
-				return err
-			}
-			if st == nil {
-				continue
-			}
-
-			it := st.NewIterator(engine.IteratorConfig{})
-			for it.Seek(nil); it.Valid(); it.Next() {
-				item := it.Item()
-				k := item.Key()
-				idx := bytes.LastIndexByte(k, separator)
-				f, err := decodeIndexValueToField(t, k[:idx])
-				if err != nil {
-					it.Close()
-					return err
-				}
-
-				err = fn(f, k[idx+1:])
-				if err != nil {
-					it.Close()
-					return err
-				}
-			}
-			err = it.Close()
+	if pivot.Type == 0 {
+		for i := 0; i < len(valueTypes); i++ {
+			err := idx.iterateOnStore(document.Value{Type: valueTypes[i]}, false, fn)
 			if err != nil {
 				return err
 			}
@@ -95,93 +83,43 @@ func (i *ListIndex) AscendGreaterOrEqual(pivot *Pivot, fn func(val document.Valu
 		return nil
 	}
 
-	st, err := getStore(i.tx, NewTypeFromValueType(pivot.Value.Type), i.name)
-	if err != nil {
-		return err
-	}
-	if st == nil {
-		return nil
-	}
-
-	var data []byte
-	if !pivot.empty {
-		data, err = EncodeFieldToIndexValue(pivot.Value)
-		if err != nil {
-			return err
-		}
-	}
-
-	it := st.NewIterator(engine.IteratorConfig{})
-	// https://github.com/tinygo-org/tinygo/issues/1033
-	defer func() {
-		it.Close()
-	}()
-
-	for it.Seek(data); it.Valid(); it.Next() {
-		item := it.Item()
-		k := item.Key()
-
-		idx := bytes.LastIndexByte(k, separator)
-		f, err := decodeIndexValueToField(NewTypeFromValueType(pivot.Value.Type), k[:idx])
-		if err != nil {
-			return err
-		}
-
-		err = fn(f, k[idx+1:])
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
+	return idx.iterateOnStore(pivot, false, fn)
 }
 
 // DescendLessOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in descreasing order and calls the given function for each pair.
 // If the given function returns an error, the iteration stops and returns that error.
 // If the pivot is nil, starts from the end.
-func (i *ListIndex) DescendLessOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error {
+func (idx *ListIndex) DescendLessOrEqual(pivot document.Value, fn func(val, key []byte, isEqual bool) error) error {
 	// iterate over all stores in order
-	if pivot == nil {
-		for t := Bytes; t >= Null; t-- {
-			st, err := getStore(i.tx, t, i.name)
+	if pivot.Type == 0 {
+		for i := len(valueTypes) - 1; i >= 0; i-- {
+			err := idx.iterateOnStore(document.Value{Type: valueTypes[i]}, true, fn)
 			if err != nil {
 				return err
 			}
-			if st == nil {
-				continue
-			}
-
-			it := st.NewIterator(engine.IteratorConfig{Reverse: true})
+		}
 
-			for it.Seek(nil); it.Valid(); it.Next() {
-				item := it.Item()
-				k := item.Key()
+		return nil
+	}
 
-				idx := bytes.LastIndexByte(k, separator)
-				f, err := decodeIndexValueToField(t, k[:idx])
-				if err != nil {
-					it.Close()
-					return err
-				}
+	return idx.iterateOnStore(pivot, true, fn)
+}
 
-				err = fn(f, k[idx+1:])
-				if err != nil {
-					it.Close()
-					return err
-				}
-			}
+func (idx *ListIndex) iterateOnStore(pivot document.Value, reverse bool, fn func(val, key []byte, isEqual bool) error) error {
+	var err error
 
-			err = it.Close()
+	if pivot.Type == document.IntegerValue {
+		if pivot.V != nil {
+			pivot, err = pivot.CastAsDouble()
 			if err != nil {
 				return err
 			}
-
+		} else {
+			pivot.Type = document.DoubleValue
 		}
-
-		return nil
 	}
 
-	st, err := getStore(i.tx, NewTypeFromValueType(pivot.Value.Type), i.name)
+	st, err := getStore(idx.tx, pivot.Type, idx.name)
 	if err != nil {
 		return err
 	}
@@ -189,36 +127,25 @@ func (i *ListIndex) DescendLessOrEqual(pivot *Pivot, fn func(val document.Value,
 		return nil
 	}
 
-	var data []byte
-	if !pivot.empty {
-		data, err = EncodeFieldToIndexValue(pivot.Value)
-		if err != nil {
-			return err
-		}
-	}
+	var seek, enc []byte
+
+	if pivot.V != nil {
+		enc = key.AppendValue(nil, pivot)
+		seek = enc
 
-	if len(data) > 0 {
-		// ensure the pivot is bigger than the requested value so it doesn't get skipped.
-		data = append(data, separator, 0xFF)
+		if reverse {
+			seek = append(seek, separator, 0xFF)
+		}
 	}
 
-	it := st.NewIterator(engine.IteratorConfig{Reverse: true})
-	// https://github.com/tinygo-org/tinygo/issues/1033
-	defer func() {
-		it.Close()
-	}()
+	it := st.NewIterator(engine.IteratorConfig{Reverse: reverse})
+	defer it.Close()
 
-	for it.Seek(data); it.Valid(); it.Next() {
+	for it.Seek(seek); it.Valid(); it.Next() {
 		item := it.Item()
 		k := item.Key()
-
 		idx := bytes.LastIndexByte(k, separator)
-		f, err := decodeIndexValueToField(NewTypeFromValueType(pivot.Value.Type), k[:idx])
-		if err != nil {
-			return err
-		}
-
-		err = fn(f, k[idx+1:])
+		err = fn(k[:idx], k[idx+1:], bytes.Equal(k[:idx], enc))
 		if err != nil {
 			return err
 		}
@@ -228,16 +155,13 @@ func (i *ListIndex) DescendLessOrEqual(pivot *Pivot, fn func(val document.Value,
 }
 
 // Truncate deletes all the index data.
-func (i *ListIndex) Truncate() error {
-	err := dropStore(i.tx, Float, i.name)
-	if err != nil {
-		return err
-	}
-
-	err = dropStore(i.tx, Bytes, i.name)
-	if err != nil {
-		return err
+func (idx *ListIndex) Truncate() error {
+	for t := document.NullValue; t <= document.BlobValue; t++ {
+		err := dropStore(idx.tx, t, idx.name)
+		if err != nil {
+			return err
+		}
 	}
 
-	return dropStore(i.tx, Bool, i.name)
+	return nil
 }
diff --git a/index/unique.go b/index/unique.go
index 24dd279a..93a410ee 100644
--- a/index/unique.go
+++ b/index/unique.go
@@ -1,8 +1,11 @@
 package index
 
 import (
+	"bytes"
+
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine"
+	"github.com/genjidb/genji/key"
 )
 
 // UniqueIndex is an implementation that associates a value with a exactly one key.
@@ -13,23 +16,25 @@ type UniqueIndex struct {
 
 // Set associates a value with exactly one key.
 // If the association already exists, it returns an error.
-func (i *UniqueIndex) Set(val document.Value, key []byte) error {
-	v, err := EncodeFieldToIndexValue(val)
-	if err != nil {
-		return err
+// It stores integers as doubles.
+func (idx *UniqueIndex) Set(v document.Value, k []byte) error {
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
+		if err != nil {
+			return err
+		}
 	}
 
-	st, err := getOrCreateStore(i.tx, val.Type, i.name)
+	st, err := getOrCreateStore(idx.tx, v.Type, idx.name)
 	if err != nil {
 		return err
 	}
 
-	buf := make([]byte, 0, len(v)+2)
-	buf = append(buf, uint8(NewTypeFromValueType(val.Type)))
-	buf = append(buf, separator)
-	buf = append(buf, v...)
+	enc := key.AppendValue(nil, v)
 
-	_, err = st.Get(buf)
+	_, err = st.Get(enc)
 	if err == nil {
 		return ErrDuplicate
 	}
@@ -37,67 +42,38 @@ func (i *UniqueIndex) Set(val document.Value, key []byte) error {
 		return err
 	}
 
-	return st.Put(buf, key)
+	return st.Put(enc, k)
 }
 
 // Delete all the references to the key from the index.
-func (i *UniqueIndex) Delete(val document.Value, key []byte) error {
-	v, err := EncodeFieldToIndexValue(val)
-	if err != nil {
-		return err
+func (idx *UniqueIndex) Delete(v document.Value, k []byte) error {
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
+		if err != nil {
+			return err
+		}
 	}
 
-	st, err := getOrCreateStore(i.tx, val.Type, i.name)
+	st, err := getOrCreateStore(idx.tx, v.Type, idx.name)
 	if err != nil {
 		return err
 	}
 
-	buf := make([]byte, 0, len(v)+2)
-	buf = append(buf, uint8(NewTypeFromValueType(val.Type)))
-	buf = append(buf, separator)
-	buf = append(buf, v...)
+	enc := key.AppendValue(nil, v)
 
-	return st.Delete(buf)
+	return st.Delete(enc)
 }
 
 // AscendGreaterOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in increasing order and calls the given function for each pair.
 // If the given function returns an error, the iteration stops and returns that error.
 // If the pivot is nil, starts from the beginning.
-func (i *UniqueIndex) AscendGreaterOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error {
+func (idx *UniqueIndex) AscendGreaterOrEqual(pivot document.Value, fn func(val, key []byte, isEqual bool) error) error {
 	// iterate over all stores in order
-	if pivot == nil {
-		for t := Null; t <= Bytes; t++ {
-			st, err := getStore(i.tx, t, i.name)
-			if err != nil {
-				return err
-			}
-			if st == nil {
-				continue
-			}
-
-			it := st.NewIterator(engine.IteratorConfig{})
-
-			var v []byte
-			for it.Seek(nil); it.Valid(); it.Next() {
-				item := it.Item()
-				f, err := decodeIndexValueToField(t, item.Key()[2:])
-				if err != nil {
-					it.Close()
-					return err
-				}
-
-				v, err = item.ValueCopy(v[:0])
-				if err != nil {
-					it.Close()
-					return err
-				}
-				err = fn(f, v)
-				if err != nil {
-					it.Close()
-					return err
-				}
-			}
-			err = it.Close()
+	if pivot.Type == 0 {
+		for i := 0; i < len(valueTypes); i++ {
+			err := idx.iterateOnStore(document.Value{Type: valueTypes[i]}, false, fn)
 			if err != nil {
 				return err
 			}
@@ -106,147 +82,75 @@ func (i *UniqueIndex) AscendGreaterOrEqual(pivot *Pivot, fn func(val document.Va
 		return nil
 	}
 
-	st, err := getStore(i.tx, NewTypeFromValueType(pivot.Value.Type), i.name)
-	if err != nil {
-		return err
-	}
-	if st == nil {
-		return nil
-	}
-
-	var data []byte
-	if !pivot.empty {
-		data, err = EncodeFieldToIndexValue(pivot.Value)
-		if err != nil {
-			return err
-		}
-	}
-
-	seek := make([]byte, 0, len(data)+2)
-	seek = append(seek, uint8(NewTypeFromValueType(pivot.Value.Type)))
-	seek = append(seek, separator)
-	seek = append(seek, data...)
-
-	it := st.NewIterator(engine.IteratorConfig{})
-	// https://github.com/tinygo-org/tinygo/issues/1033
-	defer func() {
-		it.Close()
-	}()
-
-	var pk []byte
-	for it.Seek(seek); it.Valid(); it.Next() {
-		item := it.Item()
-
-		pk, err = item.ValueCopy(pk[:0])
-		if err != nil {
-			return err
-		}
-
-		f, err := decodeIndexValueToField(NewTypeFromValueType(pivot.Value.Type), item.Key()[2:])
-		if err != nil {
-			return err
-		}
-
-		err = fn(f, pk)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
+	return idx.iterateOnStore(pivot, false, fn)
 }
 
 // DescendLessOrEqual seeks for the pivot and then goes through all the subsequent key value pairs in descreasing order and calls the given function for each pair.
 // If the given function returns an error, the iteration stops and returns that error.
 // If the pivot is nil, starts from the end.
-func (i *UniqueIndex) DescendLessOrEqual(pivot *Pivot, fn func(val document.Value, key []byte) error) error {
+func (idx *UniqueIndex) DescendLessOrEqual(pivot document.Value, fn func(val, key []byte, isEqual bool) error) error {
 	// iterate over all stores in order
-	if pivot == nil {
-		for t := Bytes; t >= Null; t-- {
-			st, err := getStore(i.tx, t, i.name)
+	if pivot.Type == 0 {
+		for i := len(valueTypes) - 1; i >= 0; i-- {
+			err := idx.iterateOnStore(document.Value{Type: valueTypes[i]}, true, fn)
 			if err != nil {
 				return err
 			}
-			if st == nil {
-				continue
-			}
+		}
 
-			it := st.NewIterator(engine.IteratorConfig{Reverse: true})
-
-			var v []byte
-			for it.Seek(nil); it.Valid(); it.Next() {
-				item := it.Item()
-
-				f, err := decodeIndexValueToField(t, item.Key()[2:])
-				if err != nil {
-					it.Close()
-					return err
-				}
-
-				v, err = item.ValueCopy(v[:0])
-				if err != nil {
-					it.Close()
-					return err
-				}
-
-				err = fn(f, v)
-				if err != nil {
-					it.Close()
-					return err
-				}
-			}
-			err = it.Close()
+		return nil
+	}
+
+	return idx.iterateOnStore(pivot, true, fn)
+}
+
+func (idx *UniqueIndex) iterateOnStore(pivot document.Value, reverse bool, fn func(val, key []byte, isEqual bool) error) error {
+	var err error
+
+	if pivot.Type == document.IntegerValue {
+		if pivot.V != nil {
+			pivot, err = pivot.CastAsDouble()
 			if err != nil {
 				return err
 			}
+		} else {
+			pivot.Type = document.DoubleValue
 		}
-
-		return nil
 	}
 
-	st, err := getStore(i.tx, NewTypeFromValueType(pivot.Value.Type), i.name)
+	st, err := getStore(idx.tx, pivot.Type, idx.name)
 	if err != nil {
 		return err
 	}
 	if st == nil {
 		return nil
 	}
+	var v []byte
 
-	var data []byte
-	if !pivot.empty {
-		data, err = EncodeFieldToIndexValue(pivot.Value)
-		if err != nil {
-			return err
+	var seek, enc []byte
+
+	if pivot.V != nil {
+		enc = key.AppendValue(nil, pivot)
+		seek = enc
+
+		if reverse {
+			seek = append(seek, 0xFF)
 		}
 	}
 
-	seek := make([]byte, 0, len(data)+3)
-	seek = append(seek, uint8(NewTypeFromValueType(pivot.Value.Type)))
-	seek = append(seek, separator)
-	seek = append(seek, data...)
-	seek = append(seek, 0xFF)
+	it := st.NewIterator(engine.IteratorConfig{Reverse: reverse})
+	defer it.Close()
 
-	it := st.NewIterator(engine.IteratorConfig{Reverse: true})
-	// https://github.com/tinygo-org/tinygo/issues/1033
-	defer func() {
-		it.Close()
-	}()
-
-	var pk []byte
 	for it.Seek(seek); it.Valid(); it.Next() {
 		item := it.Item()
 
-		pk, err = item.ValueCopy(pk[:0])
-		if err != nil {
-			return err
-		}
-
-		f, err := decodeIndexValueToField(NewTypeFromValueType(pivot.Value.Type), item.Key()[2:])
+		v, err = item.ValueCopy(v[:0])
 		if err != nil {
 			return err
 		}
 
-		err = fn(f, pk)
+		k := item.Key()
+		err = fn(k, v, bytes.Equal(k, enc))
 		if err != nil {
 			return err
 		}
@@ -256,16 +160,13 @@ func (i *UniqueIndex) DescendLessOrEqual(pivot *Pivot, fn func(val document.Valu
 }
 
 // Truncate deletes all the index data.
-func (i *UniqueIndex) Truncate() error {
-	err := dropStore(i.tx, Float, i.name)
-	if err != nil {
-		return err
-	}
-
-	err = dropStore(i.tx, Bytes, i.name)
-	if err != nil {
-		return err
+func (idx *UniqueIndex) Truncate() error {
+	for t := document.NullValue; t <= document.BlobValue; t++ {
+		err := dropStore(idx.tx, t, idx.name)
+		if err != nil {
+			return err
+		}
 	}
 
-	return dropStore(i.tx, Bool, i.name)
+	return nil
 }
diff --git a/key/encoding.go b/key/encoding.go
new file mode 100644
index 00000000..5aeb0b98
--- /dev/null
+++ b/key/encoding.go
@@ -0,0 +1,146 @@
+// Package key provides types and functions to encode and decode keys.
+//
+// Encoding keys
+//
+// Each type is encoded in a way that allows ordering to be preserved. That way, if vA < vB,
+// where vA and vB are two unencoded values of the same type, then eA < eB, where eA and eB
+// are the respective encoded values of vA and vB.
+package key
+
+import (
+	"encoding/binary"
+	"errors"
+	"math"
+	"time"
+
+	"github.com/genjidb/genji/document"
+)
+
+// EncodeString takes a string and returns its binary representation.
+func EncodeString(buf []byte, x string) {
+	copy(buf, x)
+}
+
+// AppendBool takes a bool and returns its binary representation.
+func AppendBool(buf []byte, x bool) []byte {
+	if x {
+		return append(buf, 1)
+	}
+	return append(buf, 0)
+}
+
+// DecodeBool takes a byte slice and decodes it into a boolean.
+func DecodeBool(buf []byte) bool {
+	return buf[0] == 1
+}
+
+// AppendUint64 takes an uint64 and returns its binary representation.
+func AppendUint64(buf []byte, x uint64) []byte {
+	var b [8]byte
+	binary.BigEndian.PutUint64(b[:], x)
+	return append(buf, b[:]...)
+}
+
+// DecodeUint64 takes a byte slice and decodes it into a uint64.
+func DecodeUint64(buf []byte) (uint64, error) {
+	if len(buf) < 8 {
+		return 0, errors.New("cannot decode buffer to uint64")
+	}
+
+	return binary.BigEndian.Uint64(buf), nil
+}
+
+// AppendInt64 takes an int64 and returns its binary representation.
+func AppendInt64(buf []byte, x int64) []byte {
+	var b [8]byte
+
+	binary.BigEndian.PutUint64(b[:], uint64(x)+math.MaxInt64+1)
+	return append(buf, b[:]...)
+}
+
+// DecodeInt64 takes a byte slice and decodes it into an int64.
+func DecodeInt64(buf []byte) (int64, error) {
+	x, err := DecodeUint64(buf)
+	x -= math.MaxInt64 + 1
+	return int64(x), err
+}
+
+// AppendFloat64 takes an float64 and returns its binary representation.
+func AppendFloat64(buf []byte, x float64) []byte {
+	fb := math.Float64bits(x)
+	if x >= 0 {
+		fb ^= 1 << 63
+	} else {
+		fb ^= 1<<64 - 1
+	}
+	return AppendUint64(buf, fb)
+}
+
+// DecodeFloat64 takes a byte slice and decodes it into an float64.
+func DecodeFloat64(buf []byte) (float64, error) {
+	x := binary.BigEndian.Uint64(buf)
+
+	if (x & (1 << 63)) != 0 {
+		x ^= 1 << 63
+	} else {
+		x ^= 1<<64 - 1
+	}
+	return math.Float64frombits(x), nil
+}
+
+// AppendValue encodes a value as a key.
+// It only works with scalars and doesn't support documents and arrays.
+func AppendValue(buf []byte, v document.Value) []byte {
+	switch v.Type {
+	case document.BlobValue:
+		return append(buf, v.V.([]byte)...)
+	case document.TextValue:
+		return append(buf, v.V.(string)...)
+	case document.BoolValue:
+		return AppendBool(buf, v.V.(bool))
+	case document.IntegerValue:
+		return AppendInt64(buf, v.V.(int64))
+	case document.DoubleValue:
+		return AppendFloat64(buf, v.V.(float64))
+	case document.DurationValue:
+		return AppendInt64(buf, int64(v.V.(time.Duration)))
+	case document.NullValue:
+		return buf
+	}
+
+	panic("cannot encode type " + v.Type.String() + " as key")
+}
+
+// DecodeValue takes some encoded data and decodes it to the target type t.
+func DecodeValue(t document.ValueType, data []byte) (document.Value, error) {
+	switch t {
+	case document.BlobValue:
+		return document.NewBlobValue(data), nil
+	case document.TextValue:
+		return document.NewTextValue(string(data)), nil
+	case document.BoolValue:
+		return document.NewBoolValue(DecodeBool(data)), nil
+	case document.IntegerValue:
+		x, err := DecodeInt64(data)
+		if err != nil {
+			return document.Value{}, err
+		}
+		return document.NewIntegerValue(x), nil
+	case document.DoubleValue:
+		x, err := DecodeFloat64(data)
+		if err != nil {
+			return document.Value{}, err
+		}
+		return document.NewDoubleValue(x), nil
+	case document.DurationValue:
+		x, err := DecodeInt64(data)
+		if err != nil {
+			return document.Value{}, err
+		}
+		return document.NewDurationValue(time.Duration(x)), nil
+	case document.NullValue:
+		return document.NewNullValue(), nil
+	}
+
+	return document.Value{}, errors.New("unknown type")
+}
diff --git a/sql/planner/input.go b/sql/planner/input.go
index 9173789a..9c4fbe8a 100644
--- a/sql/planner/input.go
+++ b/sql/planner/input.go
@@ -137,22 +137,22 @@ func (it indexIterator) Iterate(fn func(d document.Document) error) error {
 		var err error
 
 		if it.orderByDirection == scanner.DESC {
-			err = it.index.DescendLessOrEqual(nil, func(val document.Value, key []byte) error {
-				r, err := it.tb.GetDocument(key)
+			err = it.index.DescendLessOrEqual(document.Value{}, func(val, key []byte, isEqual bool) error {
+				d, err := it.tb.GetDocument(key)
 				if err != nil {
 					return err
 				}
 
-				return fn(r)
+				return fn(d)
 			})
 		} else {
-			err = it.index.AscendGreaterOrEqual(nil, func(val document.Value, key []byte) error {
-				r, err := it.tb.GetDocument(key)
+			err = it.index.AscendGreaterOrEqual(document.Value{}, func(val, key []byte, isEqual bool) error {
+				d, err := it.tb.GetDocument(key)
 				if err != nil {
 					return err
 				}
 
-				return fn(r)
+				return fn(d)
 			})
 		}
 
@@ -167,12 +167,5 @@ func (it indexIterator) Iterate(fn func(d document.Document) error) error {
 		return err
 	}
 
-	if v.Type.IsNumber() {
-		v, err = v.CastAsDouble()
-		if err != nil {
-			return err
-		}
-	}
-
 	return it.iop.IterateIndex(it.index, it.tb, v, fn)
 }
diff --git a/sql/planner/sort.go b/sql/planner/sort.go
index 0b9351b6..94bcddc7 100644
--- a/sql/planner/sort.go
+++ b/sql/planner/sort.go
@@ -7,8 +7,7 @@ import (
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding/msgpack"
-	"github.com/genjidb/genji/index"
+	"github.com/genjidb/genji/key"
 	"github.com/genjidb/genji/sql/query/expr"
 	"github.com/genjidb/genji/sql/scanner"
 )
@@ -73,7 +72,8 @@ func (it *sortIterator) Iterate(fn func(d document.Document) error) error {
 	}
 
 	for h.Len() > 0 {
-		err := fn(msgpack.EncodedDocument(heap.Pop(h).(heapNode).data))
+		node := heap.Pop(h).(heapNode)
+		err := fn(&(node.data))
 		if err != nil {
 			return err
 		}
@@ -90,7 +90,7 @@ func (it *sortIterator) Iterate(fn func(d document.Document) error) error {
 // Once the heap is filled entirely with the content of the table a stream is returned.
 // During iteration, the stream will pop the k-smallest or k-largest elements, depending on
 // the chosen sorting order (ASC or DESC).
-// This function is not memory efficient as it's loading the entire table in memory before
+// This function is not memory efficient as it's loading the entire stream in memory before
 // returning the k-smallest or k-largest elements.
 func (it *sortIterator) sortStream(st document.Stream) (heap.Interface, error) {
 	path := document.ValuePath(it.sortField)
@@ -117,9 +117,16 @@ func (it *sortIterator) sortStream(st document.Stream) (heap.Interface, error) {
 		// if the same with or without indexes.
 		// To achieve that, the value must be encoded using the same method
 		// as what the index package would do.
-		value, err := index.EncodeFieldToIndexValue(v)
-		if err != nil {
-			return err
+		if v.Type == document.IntegerValue {
+			v, err = v.CastAsDouble()
+			if err != nil {
+				return err
+			}
+		}
+
+		var value []byte
+		if v.Type != document.ArrayValue && v.Type != document.DocumentValue {
+			value = key.AppendValue(nil, v)
 		}
 
 		// to ensure ordering of values based on their types
@@ -127,17 +134,18 @@ func (it *sortIterator) sortStream(st document.Stream) (heap.Interface, error) {
 		// see index package for more info)
 		// we will prepend the encoded value with one byte
 		// representing the type of the value.
-		value = append([]byte{byte(index.NewTypeFromValueType(v.Type))}, value...)
+		// integer will be considered as double
+		value = append([]byte{byte(v.Type)}, value...)
 
-		data, err := msgpack.EncodeDocument(d)
+		node := heapNode{
+			value: value,
+		}
+		err = node.data.Copy(d)
 		if err != nil {
 			return err
 		}
 
-		heap.Push(h, heapNode{
-			value: value,
-			data:  data,
-		})
+		heap.Push(h, node)
 
 		return nil
 	})
@@ -145,7 +153,7 @@ func (it *sortIterator) sortStream(st document.Stream) (heap.Interface, error) {
 
 type heapNode struct {
 	value []byte
-	data  []byte
+	data  document.FieldBuffer
 }
 
 type minHeap []heapNode
diff --git a/sql/query/expr/comparison.go b/sql/query/expr/comparison.go
index 67199be7..665909e8 100644
--- a/sql/query/expr/comparison.go
+++ b/sql/query/expr/comparison.go
@@ -7,10 +7,10 @@ import (
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding"
 	"github.com/genjidb/genji/document/encoding/msgpack"
 	"github.com/genjidb/genji/engine"
 	"github.com/genjidb/genji/index"
+	"github.com/genjidb/genji/key"
 	"github.com/genjidb/genji/sql/scanner"
 )
 
@@ -36,19 +36,14 @@ func Eq(a, b Expr) Expr {
 var errStop = errors.New("errStop")
 
 func (op eqOp) IterateIndex(idx index.Index, tb *database.Table, v document.Value, fn func(d document.Document) error) error {
-	err := idx.AscendGreaterOrEqual(&index.Pivot{Value: v}, func(val document.Value, key []byte) error {
-		ok, err := v.IsEqual(val)
-		if err != nil {
-			return err
-		}
-
-		if ok {
-			r, err := tb.GetDocument(key)
+	err := idx.AscendGreaterOrEqual(v, func(val, key []byte, isEqual bool) error {
+		if isEqual {
+			d, err := tb.GetDocument(key)
 			if err != nil {
 				return err
 			}
 
-			return fn(r)
+			return fn(d)
 		}
 
 		return errStop
@@ -67,10 +62,7 @@ func (op eqOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 		return nil
 	}
 
-	data, err := encoding.EncodeValue(v)
-	if err != nil {
-		return err
-	}
+	data := key.AppendValue(nil, v)
 
 	val, err := tb.Store.Get(data)
 	if err != nil {
@@ -110,22 +102,17 @@ func Gt(a, b Expr) Expr {
 }
 
 func (op gtOp) IterateIndex(idx index.Index, tb *database.Table, v document.Value, fn func(d document.Document) error) error {
-	err := idx.AscendGreaterOrEqual(&index.Pivot{Value: v}, func(val document.Value, key []byte) error {
-		ok, err := v.IsEqual(val)
-		if err != nil {
-			return err
-		}
-
-		if ok {
+	err := idx.AscendGreaterOrEqual(v, func(val, key []byte, isEqual bool) error {
+		if isEqual {
 			return nil
 		}
 
-		r, err := tb.GetDocument(key)
+		d, err := tb.GetDocument(key)
 		if err != nil {
 			return err
 		}
 
-		return fn(r)
+		return fn(d)
 	})
 
 	if err != nil && err != errStop {
@@ -143,10 +130,7 @@ func (op gtOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 
 	var d msgpack.EncodedDocument
 
-	data, err := encoding.EncodeValue(v)
-	if err != nil {
-		return err
-	}
+	data := key.AppendValue(nil, v)
 
 	it := tb.Store.NewIterator(engine.IteratorConfig{})
 	defer it.Close()
@@ -183,13 +167,13 @@ func Gte(a, b Expr) Expr {
 }
 
 func (op gteOp) IterateIndex(idx index.Index, tb *database.Table, v document.Value, fn func(d document.Document) error) error {
-	err := idx.AscendGreaterOrEqual(&index.Pivot{Value: v}, func(val document.Value, key []byte) error {
-		r, err := tb.GetDocument(key)
+	err := idx.AscendGreaterOrEqual(v, func(val, key []byte, isEqual bool) error {
+		d, err := tb.GetDocument(key)
 		if err != nil {
 			return err
 		}
 
-		return fn(r)
+		return fn(d)
 	})
 
 	if err != nil && err != errStop {
@@ -207,10 +191,7 @@ func (op gteOp) IteratePK(tb *database.Table, v document.Value, pkType document.
 
 	var d msgpack.EncodedDocument
 
-	data, err := encoding.EncodeValue(v)
-	if err != nil {
-		return err
-	}
+	data := key.AppendValue(nil, v)
 
 	it := tb.Store.NewIterator(engine.IteratorConfig{})
 	defer it.Close()
@@ -244,22 +225,27 @@ func Lt(a, b Expr) Expr {
 }
 
 func (op ltOp) IterateIndex(idx index.Index, tb *database.Table, v document.Value, fn func(d document.Document) error) error {
-	err := idx.AscendGreaterOrEqual(index.EmptyPivot(v.Type), func(val document.Value, key []byte) error {
-		ok, err := v.IsLesserThanOrEqual(val)
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
 		if err != nil {
 			return err
 		}
+	}
 
-		if ok {
+	enc := key.AppendValue(nil, v)
+	err = idx.AscendGreaterOrEqual(document.Value{Type: v.Type}, func(val, key []byte, isEqual bool) error {
+		if bytes.Compare(enc, val) <= 0 {
 			return errStop
 		}
 
-		r, err := tb.GetDocument(key)
+		d, err := tb.GetDocument(key)
 		if err != nil {
 			return err
 		}
 
-		return fn(r)
+		return fn(d)
 	})
 
 	if err != nil && err != errStop {
@@ -277,10 +263,7 @@ func (op ltOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 
 	var d msgpack.EncodedDocument
 
-	data, err := encoding.EncodeValue(v)
-	if err != nil {
-		return err
-	}
+	data := key.AppendValue(nil, v)
 
 	it := tb.Store.NewIterator(engine.IteratorConfig{})
 	defer it.Close()
@@ -317,22 +300,27 @@ func Lte(a, b Expr) Expr {
 }
 
 func (op lteOp) IterateIndex(idx index.Index, tb *database.Table, v document.Value, fn func(d document.Document) error) error {
-	err := idx.AscendGreaterOrEqual(index.EmptyPivot(v.Type), func(val document.Value, key []byte) error {
-		ok, err := v.IsLesserThan(val)
+	var err error
+
+	if v.Type == document.IntegerValue {
+		v, err = v.CastAsDouble()
 		if err != nil {
 			return err
 		}
+	}
 
-		if ok {
+	enc := key.AppendValue(nil, v)
+	err = idx.AscendGreaterOrEqual(document.Value{Type: v.Type}, func(val, key []byte, isEqual bool) error {
+		if bytes.Compare(enc, val) < 0 {
 			return errStop
 		}
 
-		r, err := tb.GetDocument(key)
+		d, err := tb.GetDocument(key)
 		if err != nil {
 			return err
 		}
 
-		return fn(r)
+		return fn(d)
 	})
 
 	if err != nil && err != errStop {
@@ -350,10 +338,7 @@ func (op lteOp) IteratePK(tb *database.Table, v document.Value, pkType document.
 
 	var d msgpack.EncodedDocument
 
-	data, err := encoding.EncodeValue(v)
-	if err != nil {
-		return err
-	}
+	data := key.AppendValue(nil, v)
 
 	it := tb.Store.NewIterator(engine.IteratorConfig{})
 	defer it.Close()
@@ -510,10 +495,7 @@ func (op inOp) IteratePK(tb *database.Table, v document.Value, pkType document.V
 			return nil
 		}
 
-		data, err := encoding.EncodeValue(val)
-		if err != nil {
-			return err
-		}
+		data := key.AppendValue(nil, val)
 
 		d, err = tb.Store.Get(data)
 		if err != nil {
diff --git a/sql/query/expr/function.go b/sql/query/expr/function.go
index 4cf3b2a2..caad48c1 100644
--- a/sql/query/expr/function.go
+++ b/sql/query/expr/function.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding"
+	"github.com/genjidb/genji/key"
 )
 
 var functions = map[string]func(args ...Expr) (Expr, error){
@@ -42,7 +42,7 @@ func (k PKFunc) Eval(ctx EvalStack) (document.Value, error) {
 		return pk.Path.GetValue(ctx.Document)
 	}
 
-	return encoding.DecodeValue(document.IntegerValue, ctx.Document.(document.Keyer).Key())
+	return key.DecodeValue(document.IntegerValue, ctx.Document.(document.Keyer).Key())
 }
 
 // IsEqual compares this expression with the other expression and returns


Test patch:
diff --git a/database/table_test.go b/database/table_test.go
index 3b757ee3..4f51a955 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -8,9 +8,9 @@ import (
 
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/document/encoding"
 	"github.com/genjidb/genji/document/encoding/msgpack"
 	"github.com/genjidb/genji/engine/memoryengine"
+	"github.com/genjidb/genji/key"
 	"github.com/stretchr/testify/require"
 )
 
@@ -177,10 +177,10 @@ func TestTableInsert(t *testing.T) {
 
 		key2 := insertDoc(db)
 
-		a, err := encoding.DecodeInt64(key1)
+		a, err := key.DecodeInt64(key1)
 		require.NoError(t, err)
 
-		b, err := encoding.DecodeInt64(key2)
+		b, err := key.DecodeInt64(key2)
 		require.NoError(t, err)
 
 		require.Equal(t, a+1, b)
@@ -197,7 +197,7 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 
 		manualInsert := func(id int64) {
-			docid := encoding.EncodeInt64(id)
+			docid := key.AppendInt64(nil, id)
 			v, err := msgpack.EncodeDocument(newDocument())
 			require.NoError(t, err)
 			err = tb.Store.Put(docid, v)
@@ -210,7 +210,7 @@ func TestTableInsert(t *testing.T) {
 		manualInsert(2)
 
 		expectDocid := func(want int64, got []byte) {
-			newDocid, err := encoding.DecodeInt64(got)
+			newDocid, err := key.DecodeInt64(got)
 			require.NoError(t, err)
 			require.Equal(t, want, newDocid)
 		}
@@ -244,16 +244,16 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 
 		// insert
-		key, err := tb.Insert(doc)
+		k, err := tb.Insert(doc)
 		require.NoError(t, err)
-		require.Equal(t, encoding.EncodeInt64(10), key)
+		require.Equal(t, key.AppendInt64(nil, 10), k)
 
 		// make sure the document is fetchable using the returned key
-		_, err = tb.GetDocument(key)
+		_, err = tb.GetDocument(k)
 		require.NoError(t, err)
 
 		// insert again
-		key, err = tb.Insert(doc)
+		k, err = tb.Insert(doc)
 		require.Equal(t, database.ErrDuplicateDocument, err)
 	})
 
@@ -348,7 +348,7 @@ func TestTableInsert(t *testing.T) {
 		require.NoError(t, err)
 
 		var count int
-		err = idx.AscendGreaterOrEqual(nil, func(val document.Value, k []byte) error {
+		err = idx.AscendGreaterOrEqual(document.Value{}, func(val, k []byte, isEqual bool) error {
 			switch count {
 			case 0:
 				// key2, which doesn't countain the field must appear first in the next,
@@ -559,14 +559,14 @@ func TestTableReplace(t *testing.T) {
 		require.NoError(t, err)
 		f, err := res.GetByField("fielda")
 		require.NoError(t, err)
-		require.Equal(t, "e", string(f.V.([]byte)))
+		require.Equal(t, "e", f.V.(string))
 
 		// make sure it didn't also replace the other one
 		res, err = tb.GetDocument(key2)
 		require.NoError(t, err)
 		f, err = res.GetByField("fielda")
 		require.NoError(t, err)
-		require.Equal(t, "c", string(f.V.([]byte)))
+		require.Equal(t, "c", f.V.(string))
 	})
 }
 
diff --git a/database/transaction_test.go b/database/transaction_test.go
index e733b3f5..d0975b6b 100644
--- a/database/transaction_test.go
+++ b/database/transaction_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/genjidb/genji/database"
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine/memoryengine"
-	"github.com/genjidb/genji/index"
+	"github.com/genjidb/genji/key"
 	"github.com/stretchr/testify/require"
 )
 
@@ -271,8 +271,9 @@ func TestTxReIndex(t *testing.T) {
 		require.NoError(t, err)
 
 		var i int
-		err = idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
-			require.Equal(t, document.NewDoubleValue(float64(i)), val)
+		err = idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(v, k []byte, isEqual bool) error {
+			enc := key.AppendValue(nil, document.NewDoubleValue(float64(i)))
+			require.Equal(t, enc, v)
 			i++
 			return nil
 		})
@@ -283,7 +284,7 @@ func TestTxReIndex(t *testing.T) {
 		require.NoError(t, err)
 
 		i = 0
-		err = idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
+		err = idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(val, key []byte, isEqual bool) error {
 			i++
 			return nil
 		})
@@ -348,8 +349,9 @@ func TestReIndexAll(t *testing.T) {
 		require.NoError(t, err)
 
 		var i int
-		err = idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
-			require.Equal(t, document.NewDoubleValue(float64(i)), val)
+		err = idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(v, k []byte, isEqual bool) error {
+			enc := key.AppendValue(nil, document.NewDoubleValue(float64(i)))
+			require.Equal(t, enc, v)
 			i++
 			return nil
 		})
@@ -360,8 +362,9 @@ func TestReIndexAll(t *testing.T) {
 		require.NoError(t, err)
 
 		i = 0
-		err = idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
-			require.Equal(t, document.NewDoubleValue(float64(i)), val)
+		err = idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(v, k []byte, isEqual bool) error {
+			enc := key.AppendValue(nil, document.NewDoubleValue(float64(i)))
+			require.Equal(t, enc, v)
 			i++
 			return nil
 		})
diff --git a/document/array_test.go b/document/array_test.go
index 74c99952..19f4a724 100644
--- a/document/array_test.go
+++ b/document/array_test.go
@@ -18,11 +18,11 @@ func TestArrayContains(t *testing.T) {
 	require.NoError(t, err)
 	require.True(t, ok)
 
-	ok, err = ArrayContains(arr, NewBlobValue([]byte("foo")))
+	ok, err = ArrayContains(arr, NewTextValue("foo"))
 	require.NoError(t, err)
 	require.True(t, ok)
 
-	ok, err = ArrayContains(arr, NewBlobValue([]byte("bar")))
+	ok, err = ArrayContains(arr, NewTextValue("bar"))
 	require.NoError(t, err)
 	require.False(t, ok)
 }
@@ -36,7 +36,7 @@ func TestSortArray(t *testing.T) {
 		{"empty array", `[]`, `[]`},
 		{"numbers", `[1.4,3,2.1,-5]`, `[-5,1.4,2.1,3]`},
 		{"text", `["foo","bar",""]`, `["","bar","foo"]`},
-		{"arrays", `[[1, 2],[-1,10],[]]`, `[[],[-1,10],[1,2]]`},
+		{"arrays", `[[1, 2],[-1,10],[]]`, `[[1,2],[-1,10],[]]`},
 		{"documents", `[{"z":10},{"a":40},{}]`, `[{"z":10},{"a":40},{}]`},
 		{"mixed", `["foo",["a"],{},null,true,10]`, `[null,true,10,"foo",["a"],{}]`},
 	}
diff --git a/document/compare_test.go b/document/compare_test.go
index 649cdcc3..6e43c8e2 100644
--- a/document/compare_test.go
+++ b/document/compare_test.go
@@ -4,415 +4,221 @@ import (
 	"encoding/json"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/genjidb/genji/document"
 	"github.com/stretchr/testify/require"
 )
 
-var numericFuncs = []struct {
-	name string
-	fn   func(x interface{}) document.Value
-}{
-	{"integer", func(x interface{}) document.Value { return document.NewIntegerValue(int64(x.(int))) }},
-	{"double", func(x interface{}) document.Value { return document.NewDoubleValue(float64(x.(int))) }},
-}
+func jsonToInteger(t testing.TB, x string) document.Value {
+	var i int64
+	err := json.Unmarshal([]byte(x), &i)
+	require.NoError(t, err)
 
-var textFuncs = []struct {
-	name string
-	fn   func(x interface{}) document.Value
-}{
-	{"text", func(x interface{}) document.Value { return document.NewTextValue(x.(string)) }},
-	{"bytes", func(x interface{}) document.Value { return document.NewBlobValue([]byte(x.(string))) }},
+	return document.NewIntegerValue(i)
 }
 
-func TestComparisonNumbers(t *testing.T) {
-	tests := []struct {
-		op   string
-		a, b int
-		ok   bool
-	}{
-		{"=", 2, 1, false},
-		{"=", 2, 2, true},
-		{"!=", 2, 1, true},
-		{"!=", 2, 2, false},
-		{">", 2, 1, true},
-		{">", 1, 2, false},
-		{">", 2, 2, false},
-		{">=", 2, 1, true},
-		{">=", 1, 2, false},
-		{">=", 2, 2, true},
-		{"<", 2, 1, false},
-		{"<", 1, 2, true},
-		{"<", 2, 2, false},
-		{"<=", 2, 1, false},
-		{"<=", 1, 2, true},
-		{"<=", 2, 2, true},
-	}
+func jsonToDouble(t testing.TB, x string) document.Value {
+	var f float64
+	err := json.Unmarshal([]byte(x), &f)
+	require.NoError(t, err)
 
-	for i := 0; i < len(numericFuncs); i++ {
-		for j := 0; j < len(numericFuncs); j++ {
-			for _, test := range tests {
-				t.Run(fmt.Sprintf("%s(%d)%s%s(%d)", numericFuncs[i].name, test.a, test.op, numericFuncs[j].name, test.b), func(t *testing.T) {
-					var ok bool
-					var err error
-
-					switch test.op {
-					case "=":
-						ok, err = numericFuncs[i].fn(test.a).IsEqual(numericFuncs[j].fn(test.b))
-					case "!=":
-						ok, err = numericFuncs[i].fn(test.a).IsNotEqual(numericFuncs[j].fn(test.b))
-					case ">":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThan(numericFuncs[j].fn(test.b))
-					case ">=":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThanOrEqual(numericFuncs[j].fn(test.b))
-					case "<":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThan(numericFuncs[j].fn(test.b))
-					case "<=":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThanOrEqual(numericFuncs[j].fn(test.b))
-					}
-					require.NoError(t, err)
-					require.Equal(t, test.ok, ok)
-				})
-			}
-		}
-	}
+	return document.NewDoubleValue(f)
 }
 
-func TestComparisonNumbersWithNull(t *testing.T) {
-	tests := []struct {
-		op string
-		a  int
-		ok bool
-	}{
-		{"=", 1, false},
-		{"=", 0, false},
-		{"!=", 0, true},
-		{"!=", 1, true},
-		{">", 1, false},
-		{">", 0, false},
-		{">=", 1, false},
-		{">=", 0, false},
-		{"<", 1, false},
-		{"<", 0, false},
-		{"<=", 1, false},
-		{"<=", 0, false},
-	}
-
-	for i := 0; i < len(numericFuncs); i++ {
-		for j := 0; j < len(numericFuncs); j++ {
-			for _, test := range tests {
-				t.Run(fmt.Sprintf("%s(%q)%sNULL", numericFuncs[i].name, test.a, test.op), func(t *testing.T) {
-					var ok bool
-					var err error
+func jsonToBoolean(t testing.TB, x string) document.Value {
+	var b bool
+	err := json.Unmarshal([]byte(x), &b)
+	require.NoError(t, err)
 
-					switch test.op {
-					case "=":
-						ok, err = numericFuncs[i].fn(test.a).IsEqual(document.NewNullValue())
-					case "!=":
-						ok, err = numericFuncs[i].fn(test.a).IsNotEqual(document.NewNullValue())
-					case ">":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThan(document.NewNullValue())
-					case ">=":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThanOrEqual(document.NewNullValue())
-					case "<":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThan(document.NewNullValue())
-					case "<=":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThanOrEqual(document.NewNullValue())
-					}
-					require.NoError(t, err)
-					require.Equal(t, test.ok, ok)
-				})
-			}
-		}
-	}
+	return document.NewBoolValue(b)
 }
 
-func TestComparisonText(t *testing.T) {
-	tests := []struct {
-		op   string
-		a, b string
-		ok   bool
-	}{
-		{"=", "b", "a", false},
-		{"=", "b", "b", true},
-		{"!=", "b", "a", true},
-		{"!=", "b", "b", false},
-		{">", "b", "a", true},
-		{">", "a", "b", false},
-		{">", "b", "b", false},
-		{">=", "b", "a", true},
-		{">=", "a", "b", false},
-		{">=", "b", "b", true},
-		{"<", "b", "a", false},
-		{"<", "a", "b", true},
-		{"<", "b", "b", false},
-		{"<=", "b", "a", false},
-		{"<=", "a", "b", true},
-		{"<=", "b", "b", true},
-	}
-
-	for i := 0; i < len(textFuncs); i++ {
-		for j := 0; j < len(textFuncs); j++ {
-			for _, test := range tests {
-				t.Run(fmt.Sprintf("%s(%q)%s%s(%q)", textFuncs[i].name, test.a, test.op, textFuncs[j].name, test.b), func(t *testing.T) {
-					var ok bool
-					var err error
-
-					switch test.op {
-					case "=":
-						ok, err = textFuncs[i].fn(test.a).IsEqual(textFuncs[j].fn(test.b))
-					case "!=":
-						ok, err = textFuncs[i].fn(test.a).IsNotEqual(textFuncs[j].fn(test.b))
-					case ">":
-						ok, err = textFuncs[i].fn(test.a).IsGreaterThan(textFuncs[j].fn(test.b))
-					case ">=":
-						ok, err = textFuncs[i].fn(test.a).IsGreaterThanOrEqual(textFuncs[j].fn(test.b))
-					case "<":
-						ok, err = textFuncs[i].fn(test.a).IsLesserThan(textFuncs[j].fn(test.b))
-					case "<=":
-						ok, err = textFuncs[i].fn(test.a).IsLesserThanOrEqual(textFuncs[j].fn(test.b))
-					}
-					require.NoError(t, err)
-					require.Equal(t, test.ok, ok)
-				})
-			}
-		}
-	}
+func jsonToDuration(t testing.TB, x string) document.Value {
+	var d time.Duration
+	err := json.Unmarshal([]byte(x), &d)
+	require.NoError(t, err)
 
+	return document.NewDurationValue(d)
 }
 
-func TestComparisonTextWithNull(t *testing.T) {
-	nullTextTests := []struct {
-		op string
-		a  string
-		ok bool
-	}{
-		{"=", "a", false},
-		{"=", "", false},
-		{"!=", "a", true},
-		{"!=", "", true},
-		{">", "a", false},
-		{">", "", false},
-		{">=", "a", false},
-		{">=", "", false},
-		{"<", "a", false},
-		{"<", "", false},
-		{"<=", "a", false},
-		{"<=", "", false},
-	}
-
-	for i := 0; i < len(textFuncs); i++ {
-		for j := 0; j < len(textFuncs); j++ {
-			for _, test := range nullTextTests {
-				t.Run(fmt.Sprintf("%s(%q)%sNULL", textFuncs[i].name, test.a, test.op), func(t *testing.T) {
-					var ok bool
-					var err error
+func toText(t testing.TB, x string) document.Value {
+	return document.NewTextValue(x)
+}
 
-					switch test.op {
-					case "=":
-						ok, err = textFuncs[i].fn(test.a).IsEqual(document.NewNullValue())
-					case "!=":
-						ok, err = textFuncs[i].fn(test.a).IsNotEqual(document.NewNullValue())
-					case ">":
-						ok, err = textFuncs[i].fn(test.a).IsGreaterThan(document.NewNullValue())
-					case ">=":
-						ok, err = textFuncs[i].fn(test.a).IsGreaterThanOrEqual(document.NewNullValue())
-					case "<":
-						ok, err = textFuncs[i].fn(test.a).IsLesserThan(document.NewNullValue())
-					case "<=":
-						ok, err = textFuncs[i].fn(test.a).IsLesserThanOrEqual(document.NewNullValue())
-					}
-					require.NoError(t, err)
-					require.Equal(t, test.ok, ok)
-				})
-			}
-		}
-	}
+func toBlob(t testing.TB, x string) document.Value {
+	return document.NewBlobValue([]byte(x))
 }
 
-func TestComparisonDocuments(t *testing.T) {
-	tests := []struct {
-		op string
-		a  string
-		b  string
-	}{
-		{"=", `{}`, `{}`},
-		{"=", `{"a": 1}`, `{"a": 1}`},
-		{
-			"=",
-			`{"a": 1, "b": 2}`,
-			`{"b": 2, "a": 1}`,
-		},
-		{">", `{"a": 2}`, `{"a": 1}`},
-		{"<", `{"a": 1}`, `{"a": 2}`},
-	}
+func jsonToArray(t testing.TB, x string) document.Value {
+	var vb document.ValueBuffer
+	err := json.Unmarshal([]byte(x), &vb)
+	require.NoError(t, err)
 
-	for _, test := range tests {
-		t.Run(fmt.Sprintf("%v %s %v", test.a, test.op, test.b), func(t *testing.T) {
-			var ok bool
-			var err error
+	return document.NewArrayValue(vb)
+}
 
-			var d1, d2 document.FieldBuffer
-			require.NoError(t, json.Unmarshal([]byte(test.a), &d1))
-			require.NoError(t, json.Unmarshal([]byte(test.b), &d2))
+func jsonToDocument(t testing.TB, x string) document.Value {
+	var fb document.FieldBuffer
+	err := json.Unmarshal([]byte(x), &fb)
+	require.NoError(t, err)
 
-			switch test.op {
-			case "=":
-				ok, err = document.NewDocumentValue(d1).IsEqual(document.NewDocumentValue(d2))
-				require.NoError(t, err)
-				require.True(t, ok)
-			case ">":
-				ok, err = document.NewDocumentValue(d1).IsGreaterThan(document.NewDocumentValue(d2))
-				require.NoError(t, err)
-				require.False(t, ok)
-			case ">=":
-				ok, err = document.NewDocumentValue(d1).IsGreaterThanOrEqual(document.NewDocumentValue(d2))
-				require.NoError(t, err)
-				require.False(t, ok)
-			case "<":
-				ok, err = document.NewDocumentValue(d1).IsLesserThan(document.NewDocumentValue(d2))
-				require.NoError(t, err)
-				require.False(t, ok)
-			case "<=":
-				ok, err = document.NewDocumentValue(d1).IsLesserThanOrEqual(document.NewDocumentValue(d2))
-				require.NoError(t, err)
-				require.False(t, ok)
-			}
-		})
-	}
+	return document.NewDocumentValue(fb)
 }
 
-func TestComparisonArrays(t *testing.T) {
+func TestCompare(t *testing.T) {
 	tests := []struct {
-		op       string
-		a        string
-		b        string
-		expected bool
+		op        string
+		a, b      string
+		ok        bool
+		converter func(testing.TB, string) document.Value
 	}{
-		{"=", `[]`, `[]`, true},
-		{"=", `[1]`, `[1]`, true},
-		{"=", `[1]`, `[]`, false},
-		{"=", `[1.0, 2]`, `[1, 2]`, true},
-		{"=", `[1,2,3]`, `[1,2,3]`, true},
-		{"!=", `[1]`, `[5]`, true},
-		{"!=", `[1]`, `[1, 1]`, true},
-		{"!=", `[1,2,3]`, `[1,2,3]`, false},
-		{"!=", `[1]`, `[]`, true},
-		{">", `[2]`, `[1]`, true},
-		{">", `[2]`, `[1, 1000]`, true},
-		{">", `[1]`, `[1, 1000]`, false},
-		{">", `[1, 2]`, `[1, 1000]`, false},
-		{">", `[2, 1000]`, `[1]`, true},
-		{">", `[2, 1000]`, `[2]`, true},
-		{">", `[1,2,3]`, `[1,2,3]`, false},
-		{">", `[1,2,3]`, `[]`, true},
-		{">=", `[2]`, `[1]`, true},
-		{">=", `[2]`, `[2]`, true},
-		{">=", `[2]`, `[1, 1000]`, true},
-		{">=", `[1]`, `[1, 1000]`, false},
-		{">=", `[1, 2]`, `[1, 2]`, true},
-		{">=", `[1, 2]`, `[1, 1000]`, false},
-		{">=", `[2, 1000]`, `[1]`, true},
-		{">=", `[2, 1000]`, `[2]`, true},
-		{">=", `[1,2,3]`, `[1,2,3]`, true},
-		{">=", `[1,2,3]`, `[]`, true},
-		{"<", `[1]`, `[2]`, true},
-		{"<", `[1,2,3]`, `[1,2]`, false},
-		{"<", `[1,2,3]`, `[1,2,3]`, false},
-		{"<", `[1,2]`, `[1,2,3]`, true},
-		{"<", `[1, 1000]`, `[2]`, true},
-		{"<", `[2]`, `[2, 1000]`, true},
-		{"<", `[1,2,3]`, `[]`, false},
-		{"<", `[]`, `[1,2,3]`, true},
-		{"<=", `[1]`, `[2]`, true},
-		{"<=", `[1, 1000]`, `[2]`, true},
-		{"<=", `[1,2,3]`, `[1,2]`, false},
-		{">=", `[2]`, `[1]`, true},
-		{">=", `[2]`, `[2]`, true},
-		{">=", `[2]`, `[1, 1000]`, true},
-		{">=", `[2, 1000]`, `[1]`, true},
-		{"<=", `[1,2,3]`, `[1,2,3]`, true},
-		{"<=", `[]`, `[]`, true},
-		{"<=", `[]`, `[1,2,3]`, true},
+		// bool
+		{"=", "true", "false", false, jsonToBoolean},
+		{"=", "true", "true", true, jsonToBoolean},
+		{"!=", "true", "false", true, jsonToBoolean},
+		{"!=", "true", "true", false, jsonToBoolean},
+		{">", "true", "false", true, jsonToBoolean},
+		{">", "false", "true", false, jsonToBoolean},
+		{">", "true", "true", false, jsonToBoolean},
+		{">=", "true", "false", true, jsonToBoolean},
+		{">=", "false", "true", false, jsonToBoolean},
+		{">=", "true", "true", true, jsonToBoolean},
+		{"<", "true", "false", false, jsonToBoolean},
+		{"<", "false", "true", true, jsonToBoolean},
+		{"<", "true", "true", false, jsonToBoolean},
+		{"<=", "true", "false", false, jsonToBoolean},
+		{"<=", "false", "true", true, jsonToBoolean},
+		{"<=", "true", "true", true, jsonToBoolean},
+
+		// integer
+		{"=", "2", "1", false, jsonToInteger},
+		{"=", "2", "2", true, jsonToInteger},
+		{"!=", "2", "1", true, jsonToInteger},
+		{"!=", "2", "2", false, jsonToInteger},
+		{">", "2", "1", true, jsonToInteger},
+		{">", "1", "2", false, jsonToInteger},
+		{">", "2", "2", false, jsonToInteger},
+		{">=", "2", "1", true, jsonToInteger},
+		{">=", "1", "2", false, jsonToInteger},
+		{">=", "2", "2", true, jsonToInteger},
+		{"<", "2", "1", false, jsonToInteger},
+		{"<", "1", "2", true, jsonToInteger},
+		{"<", "2", "2", false, jsonToInteger},
+		{"<=", "2", "1", false, jsonToInteger},
+		{"<=", "1", "2", true, jsonToInteger},
+		{"<=", "2", "2", true, jsonToInteger},
+
+		// double
+		{"=", "2", "1", false, jsonToDouble},
+		{"=", "2", "2", true, jsonToDouble},
+		{"!=", "2", "1", true, jsonToDouble},
+		{"!=", "2", "2", false, jsonToDouble},
+		{">", "2", "1", true, jsonToDouble},
+		{">", "1", "2", false, jsonToDouble},
+		{">", "2", "2", false, jsonToDouble},
+		{">=", "2", "1", true, jsonToDouble},
+		{">=", "1", "2", false, jsonToDouble},
+		{">=", "2", "2", true, jsonToDouble},
+		{"<", "2", "1", false, jsonToDouble},
+		{"<", "1", "2", true, jsonToDouble},
+		{"<", "2", "2", false, jsonToDouble},
+		{"<=", "2", "1", false, jsonToDouble},
+		{"<=", "1", "2", true, jsonToDouble},
+		{"<=", "2", "2", true, jsonToDouble},
+
+		// duration
+		{"=", "2", "1", false, jsonToDuration},
+		{"=", "2", "2", true, jsonToDuration},
+		{"!=", "2", "1", true, jsonToDuration},
+		{"!=", "2", "2", false, jsonToDuration},
+		{">", "2", "1", true, jsonToDuration},
+		{">", "1", "2", false, jsonToDuration},
+		{">", "2", "2", false, jsonToDuration},
+		{">=", "2", "1", true, jsonToDuration},
+		{">=", "1", "2", false, jsonToDuration},
+		{">=", "2", "2", true, jsonToDuration},
+		{"<", "2", "1", false, jsonToDuration},
+		{"<", "1", "2", true, jsonToDuration},
+		{"<", "2", "2", false, jsonToDuration},
+		{"<=", "2", "1", false, jsonToDuration},
+		{"<=", "1", "2", true, jsonToDuration},
+		{"<=", "2", "2", true, jsonToDuration},
+
+		// text
+		{"=", "b", "a", false, toText},
+		{"=", "b", "b", true, toText},
+		{"!=", "b", "a", true, toText},
+		{"!=", "b", "b", false, toText},
+		{">", "b", "a", true, toText},
+		{">", "a", "b", false, toText},
+		{">", "b", "b", false, toText},
+		{">=", "b", "a", true, toText},
+		{">=", "a", "b", false, toText},
+		{">=", "b", "b", true, toText},
+		{"<", "b", "a", false, toText},
+		{"<", "a", "b", true, toText},
+		{"<", "b", "b", false, toText},
+		{"<=", "b", "a", false, toText},
+		{"<=", "a", "b", true, toText},
+		{"<=", "b", "b", true, toText},
+
+		// blob
+		{"=", "b", "a", false, toBlob},
+		{"=", "b", "b", true, toBlob},
+		{"!=", "b", "a", true, toBlob},
+		{"!=", "b", "b", false, toBlob},
+		{">", "b", "a", true, toBlob},
+		{">", "a", "b", false, toBlob},
+		{">", "b", "b", false, toBlob},
+		{">=", "b", "a", true, toBlob},
+		{">=", "a", "b", false, toBlob},
+		{">=", "b", "b", true, toBlob},
+		{"<", "b", "a", false, toBlob},
+		{"<", "a", "b", true, toBlob},
+		{"<", "b", "b", false, toBlob},
+		{"<=", "b", "a", false, toBlob},
+		{"<=", "a", "b", true, toBlob},
+		{"<=", "b", "b", true, toBlob},
+
+		// array
+		{"=", `[1]`, `[1]`, false, jsonToArray},
+		{"!=", `[1]`, `[1]`, false, toBlob},
+		{">", `[1]`, `[1]`, false, jsonToArray},
+		{">=", `[1]`, `[1]`, false, jsonToArray},
+		{"<", `[1]`, `[1]`, false, jsonToArray},
+		{"<=", `[1]`, `[1]`, false, jsonToArray},
+
+		// document
+		{"=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
+		{">", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
+		{">=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
+		{"<", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
+		{"<=", `{"a": 1}`, `{"a": 1}`, false, jsonToDocument},
 	}
 
 	for _, test := range tests {
-		t.Run(fmt.Sprintf("%v %s %v", test.a, test.op, test.b), func(t *testing.T) {
+		a, b := test.converter(t, test.a), test.converter(t, test.b)
+		t.Run(fmt.Sprintf("%s/%v%v%v", a.Type.String(), a, test.op, b), func(t *testing.T) {
 			var ok bool
 			var err error
 
-			var a1, a2 document.ValueBuffer
-			require.NoError(t, json.Unmarshal([]byte(test.a), &a1))
-			require.NoError(t, json.Unmarshal([]byte(test.b), &a2))
-
 			switch test.op {
 			case "=":
-				ok, err = document.NewArrayValue(a1).IsEqual(document.NewArrayValue(a2))
+				ok, err = a.IsEqual(b)
+			case "!=":
+				ok, err = a.IsNotEqual(b)
 			case ">":
-				ok, err = document.NewArrayValue(a1).IsGreaterThan(document.NewArrayValue(a2))
+				ok, err = a.IsGreaterThan(b)
 			case ">=":
-				ok, err = document.NewArrayValue(a1).IsGreaterThanOrEqual(document.NewArrayValue(a2))
+				ok, err = a.IsGreaterThanOrEqual(b)
 			case "<":
-				ok, err = document.NewArrayValue(a1).IsLesserThan(document.NewArrayValue(a2))
+				ok, err = a.IsLesserThan(b)
 			case "<=":
-				ok, err = document.NewArrayValue(a1).IsLesserThanOrEqual(document.NewArrayValue(a2))
-			case "!=":
-				ok, err = document.NewArrayValue(a1).IsNotEqual(document.NewArrayValue(a2))
+				ok, err = a.IsLesserThanOrEqual(b)
 			}
 			require.NoError(t, err)
-			require.Equal(t, test.expected, ok)
+			require.Equal(t, test.ok, ok)
 		})
 	}
 }
-
-func TestComparisonDifferentTypes(t *testing.T) {
-	tests := []struct {
-		op string
-		a  int
-		b  string
-	}{
-		{"=", 1, "1"},
-		{"=", 0, ""},
-		{"=", 0, ""},
-		{">", 2, "1"},
-		{">", 1, ""},
-		{">=", 1, "1"},
-		{">=", 0, ""},
-		{"<", 0, "1"},
-		{"<", -1, ""},
-		{"<=", 1, "1"},
-		{"<=", 0, ""},
-	}
-
-	for i := 0; i < len(numericFuncs); i++ {
-		for j := 0; j < len(textFuncs); j++ {
-			for _, test := range tests {
-				t.Run(fmt.Sprintf("%s(%d)%s%s(%q)", numericFuncs[i].name, test.a, test.op, textFuncs[j].name, test.b), func(t *testing.T) {
-					var ok bool
-					var err error
-
-					switch test.op {
-					case "=":
-						ok, err = numericFuncs[i].fn(test.a).IsEqual(textFuncs[j].fn(test.b))
-					case ">":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThan(textFuncs[j].fn(test.b))
-					case ">=":
-						ok, err = numericFuncs[i].fn(test.a).IsGreaterThanOrEqual(textFuncs[j].fn(test.b))
-					case "<":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThan(textFuncs[j].fn(test.b))
-					case "<=":
-						ok, err = numericFuncs[i].fn(test.a).IsLesserThanOrEqual(textFuncs[j].fn(test.b))
-					}
-					require.NoError(t, err)
-					require.False(t, ok)
-				})
-			}
-		}
-	}
-
-	t.Run("not equal with different types", func(t *testing.T) {
-		ok, err := document.NewIntegerValue(1).IsNotEqual(document.NewTextValue("foo"))
-		require.NoError(t, err)
-		require.True(t, ok)
-	})
-}
diff --git a/document/document_test.go b/document/document_test.go
index f7abbada..3b9e2712 100644
--- a/document/document_test.go
+++ b/document/document_test.go
@@ -150,16 +150,10 @@ func TestFieldBuffer(t *testing.T) {
 					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},
-			{"+int8 values", `{"a": 1}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1)), false},
-			{"-int8 values", `{"a": -1}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(-1)), false},
-			{"+int16 values", `{"a": 1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000)), false},
-			{"-int16 values", `{"a": 1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000)), false},
-			{"+int32 values", `{"a": 1000000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000000)), false},
-			{"-int32 values", `{"a": 1000000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000000)), false},
-			{"+int64 values", `{"a": 10000000000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(10000000000)), false},
-			{"-int64 values", `{"a": -10000000000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(-10000000000)), false},
-			{"+float64 values", `{"a": 10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(10000000000)), false},
-			{"-float64 values", `{"a": -10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(-10000000000)), 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().
@@ -206,7 +200,7 @@ func TestNewFromMap(t *testing.T) {
 			counter[f]++
 			switch f {
 			case "name":
-				require.Equal(t, m[f], string(v.V.([]byte)))
+				require.Equal(t, m[f], v.V.(string))
 			default:
 				require.EqualValues(t, m[f], v.V)
 			}
@@ -335,7 +329,7 @@ func TestNewFromStruct(t *testing.T) {
 			case 0:
 				require.Equal(t, u.A, v.V.([]byte))
 			case 1:
-				require.Equal(t, u.B, string(v.V.([]byte)))
+				require.Equal(t, u.B, v.V.(string))
 			case 2:
 				require.Equal(t, u.C, v.V.(bool))
 			case 3:
@@ -408,7 +402,7 @@ func TestNewFromStruct(t *testing.T) {
 		require.Equal(t, u.A, v.V.([]byte))
 		v, err = doc.GetByField("b")
 		require.NoError(t, err)
-		require.Equal(t, u.B, string(v.V.([]byte)))
+		require.Equal(t, u.B, v.V.(string))
 		v, err = doc.GetByField("c")
 		require.NoError(t, err)
 		require.Equal(t, u.C, v.V.(bool))
diff --git a/document/encoding/encoding_test.go b/document/encoding/encoding_test.go
deleted file mode 100644
index 57fb3446..00000000
--- a/document/encoding/encoding_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package encoding
-
-import (
-	"bytes"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestValueEncodeDecode(t *testing.T) {
-	tests := []struct {
-		name     string
-		expected interface{}
-		enc      func() []byte
-		dec      func([]byte) (interface{}, error)
-	}{
-		{"string", "bar", func() []byte { return EncodeString("bar") }, func(buf []byte) (interface{}, error) { return DecodeString(buf) }},
-		{"bool", true, func() []byte { return EncodeBool(true) }, func(buf []byte) (interface{}, error) { return DecodeBool(buf) }},
-		{"uint", uint(10), func() []byte { return EncodeUint(10) }, func(buf []byte) (interface{}, error) { return DecodeUint(buf) }},
-		{"uint8", uint8(10), func() []byte { return EncodeUint8(10) }, func(buf []byte) (interface{}, error) { return DecodeUint8(buf) }},
-		{"uint16", uint16(10), func() []byte { return EncodeUint16(10) }, func(buf []byte) (interface{}, error) { return DecodeUint16(buf) }},
-		{"uint32", uint32(10), func() []byte { return EncodeUint32(10) }, func(buf []byte) (interface{}, error) { return DecodeUint32(buf) }},
-		{"uint64", uint64(10), func() []byte { return EncodeUint64(10) }, func(buf []byte) (interface{}, error) { return DecodeUint64(buf) }},
-		{"int", int(-10), func() []byte { return EncodeInt(-10) }, func(buf []byte) (interface{}, error) { return DecodeInt(buf) }},
-		{"int8", int8(-10), func() []byte { return EncodeInt8(-10) }, func(buf []byte) (interface{}, error) { return DecodeInt8(buf) }},
-		{"int16", int16(-10), func() []byte { return EncodeInt16(-10) }, func(buf []byte) (interface{}, error) { return DecodeInt16(buf) }},
-		{"int32", int32(-10), func() []byte { return EncodeInt32(-10) }, func(buf []byte) (interface{}, error) { return DecodeInt32(buf) }},
-		{"int64", int64(-10), func() []byte { return EncodeInt64(-10) }, func(buf []byte) (interface{}, error) { return DecodeInt64(buf) }},
-		{"float64", float64(-3.14), func() []byte { return EncodeFloat64(-3.14) }, func(buf []byte) (interface{}, error) { return DecodeFloat64(buf) }},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			buf := test.enc()
-			actual, err := test.dec(buf)
-			require.NoError(t, err)
-			require.Equal(t, test.expected, actual)
-		})
-	}
-}
-
-const Rng = 1000
-
-func TestOrdering(t *testing.T) {
-	tests := []struct {
-		name     string
-		min, max int
-		enc      func(int) []byte
-	}{
-		{"uint", 0, 1000, func(i int) []byte { return EncodeUint(uint(i)) }},
-		{"uint8", 0, 255, func(i int) []byte { return EncodeUint8(uint8(i)) }},
-		{"uint16", 0, 1000, func(i int) []byte { return EncodeUint16(uint16(i)) }},
-		{"uint32", 0, 1000, func(i int) []byte { return EncodeUint32(uint32(i)) }},
-		{"uint64", 0, 1000, func(i int) []byte { return EncodeUint64(uint64(i)) }},
-		{"int", -1000, 1000, func(i int) []byte { return EncodeInt(i) }},
-		{"int8", -100, 100, func(i int) []byte { return EncodeInt8(int8(i)) }},
-		{"int16", -1000, 1000, func(i int) []byte { return EncodeInt16(int16(i)) }},
-		{"int32", -1000, 1000, func(i int) []byte { return EncodeInt32(int32(i)) }},
-		{"int64", -1000, 1000, func(i int) []byte { return EncodeInt64(int64(i)) }},
-		{"float64", -1000, 1000, func(i int) []byte { return EncodeFloat64(float64(i)) }},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			var prev []byte
-			for i := test.min; i < test.max; i++ {
-				cur := test.enc(i)
-				if prev == nil {
-					prev = cur
-					continue
-				}
-
-				require.Equal(t, -1, bytes.Compare(prev, cur))
-				prev = cur
-			}
-		})
-	}
-}
diff --git a/document/value_test.go b/document/value_test.go
index dbcfde07..a345c31a 100644
--- a/document/value_test.go
+++ b/document/value_test.go
@@ -56,7 +56,7 @@ func TestNewValue(t *testing.T) {
 		value, expected interface{}
 	}{
 		{"bytes", []byte("bar"), []byte("bar")},
-		{"string", "bar", []byte("bar")},
+		{"string", "bar", "bar"},
 		{"bool", true, true},
 		{"uint", uint(10), int64(10)},
 		{"uint8", uint8(10), int64(10)},
@@ -74,7 +74,7 @@ func TestNewValue(t *testing.T) {
 		{"array", document.NewValueBuffer(document.NewIntegerValue(10)), document.NewValueBuffer(document.NewIntegerValue(10))},
 		{"duration", 10 * time.Nanosecond, 10 * time.Nanosecond},
 		{"bytes", myBytes("bar"), []byte("bar")},
-		{"string", myString("bar"), []byte("bar")},
+		{"string", myString("bar"), "bar"},
 		{"myUint", myUint(10), int64(10)},
 		{"myUint16", myUint16(500), int64(500)},
 		{"myUint32", myUint32(90000), int64(90000)},
@@ -103,9 +103,9 @@ func TestValueAdd(t *testing.T) {
 	}{
 		{"null+null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null+integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)+bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(2), false},
-		{"bool(true)+bool(false)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(2), false},
-		{"bool(true)+integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewIntegerValue(-9), false},
+		{"bool(true)+bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)+bool(false)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)+integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewNullValue(), false},
 		{"integer(-10)+integer(10)", document.NewIntegerValue(-10), document.NewIntegerValue(10), document.NewIntegerValue(0), false},
 		{"integer(120)+integer(120)", document.NewIntegerValue(120), document.NewIntegerValue(120), document.NewIntegerValue(240), false},
 		{"integer(120)+float64(120)", document.NewIntegerValue(120), document.NewDoubleValue(120), document.NewDoubleValue(240), false},
@@ -140,9 +140,9 @@ func TestValueSub(t *testing.T) {
 	}{
 		{"null-null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null-integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)-bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(0), false},
-		{"bool(true)-bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewIntegerValue(1), false},
-		{"bool(true)-integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewIntegerValue(11), false},
+		{"bool(true)-bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)-bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
+		{"bool(true)-integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewNullValue(), false},
 		{"integer(10)-integer(10)", document.NewIntegerValue(10), document.NewIntegerValue(10), document.NewIntegerValue(0), false},
 		{"int16(250)-int16(220)", document.NewIntegerValue(250), document.NewIntegerValue(220), document.NewIntegerValue(30), false},
 		{"integer(120)-float64(620)", document.NewIntegerValue(120), document.NewDoubleValue(620), document.NewDoubleValue(-500), false},
@@ -177,9 +177,9 @@ func TestValueMult(t *testing.T) {
 	}{
 		{"null*null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null*integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)*bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(1), false},
-		{"bool(true)*bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewIntegerValue(0), false},
-		{"bool(true)*integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewIntegerValue(-10), false},
+		{"bool(true)*bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)*bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
+		{"bool(true)*integer(-10)", document.NewBoolValue(true), document.NewIntegerValue(-10), document.NewNullValue(), false},
 		{"integer(10)*integer(10)", document.NewIntegerValue(10), document.NewIntegerValue(10), document.NewIntegerValue(100), false},
 		{"integer(10)*integer(80)", document.NewIntegerValue(10), document.NewIntegerValue(80), document.NewIntegerValue(800), false},
 		{"integer(10)*float64(80)", document.NewIntegerValue(10), document.NewDoubleValue(80), document.NewDoubleValue(800), false},
@@ -212,7 +212,7 @@ func TestValueDiv(t *testing.T) {
 	}{
 		{"null/null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null/integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)/bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(1), false},
+		{"bool(true)/bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
 		{"bool(true)/bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
 		{"integer(10)/integer(0)", document.NewIntegerValue(10), document.NewIntegerValue(0), document.NewNullValue(), false},
 		{"integer(10)/float64(0)", document.NewIntegerValue(10), document.NewDoubleValue(0), document.NewNullValue(), false},
@@ -248,7 +248,7 @@ func TestValueMod(t *testing.T) {
 	}{
 		{"null%null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null%integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)%bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(0), false},
+		{"bool(true)%bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
 		{"bool(true)%bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
 		{"integer(10)%integer(0)", document.NewIntegerValue(10), document.NewIntegerValue(0), document.NewNullValue(), false},
 		{"integer(10)%float64(0)", document.NewIntegerValue(10), document.NewDoubleValue(0), document.NewNullValue(), false},
@@ -286,8 +286,8 @@ func TestValueBitwiseAnd(t *testing.T) {
 	}{
 		{"null&null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null&integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)&bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(1), false},
-		{"bool(true)&bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewIntegerValue(0), false},
+		{"bool(true)&bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)&bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
 		{"integer(10)&integer(0)", document.NewIntegerValue(10), document.NewIntegerValue(0), document.NewIntegerValue(0), false},
 		{"double(10.5)&float64(3.2)", document.NewDoubleValue(10.5), document.NewDoubleValue(3.2), document.NewIntegerValue(2), false},
 		{"integer(10)&float64(0)", document.NewIntegerValue(10), document.NewDoubleValue(0), document.NewIntegerValue(0), false},
@@ -321,8 +321,8 @@ func TestValueBitwiseOr(t *testing.T) {
 	}{
 		{"null|null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null|integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)|bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(1), false},
-		{"bool(true)|bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewIntegerValue(1), false},
+		{"bool(true)|bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)|bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
 		{"integer(10)|integer(0)", document.NewIntegerValue(10), document.NewIntegerValue(0), document.NewIntegerValue(10), false},
 		{"double(10.5)|float64(3.2)", document.NewDoubleValue(10.5), document.NewDoubleValue(3.2), document.NewIntegerValue(11), false},
 		{"integer(10)|float64(0)", document.NewIntegerValue(10), document.NewDoubleValue(0), document.NewIntegerValue(10), false},
@@ -355,8 +355,8 @@ func TestValueBitwiseXor(t *testing.T) {
 	}{
 		{"null^null", document.NewNullValue(), document.NewNullValue(), document.NewNullValue(), false},
 		{"null^integer(10)", document.NewNullValue(), document.NewIntegerValue(10), document.NewNullValue(), false},
-		{"bool(true)^bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewIntegerValue(0), false},
-		{"bool(true)^bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewIntegerValue(1), false},
+		{"bool(true)^bool(true)", document.NewBoolValue(true), document.NewBoolValue(true), document.NewNullValue(), false},
+		{"bool(true)^bool(false)", document.NewBoolValue(true), document.NewBoolValue(false), document.NewNullValue(), false},
 		{"integer(10)^integer(0)", document.NewIntegerValue(10), document.NewIntegerValue(0), document.NewIntegerValue(10), false},
 		{"double(10.5)^double(3.2)", document.NewDoubleValue(10.5), document.NewDoubleValue(3.2), document.NewIntegerValue(9), false},
 		{"integer(10)^double(0)", document.NewIntegerValue(10), document.NewDoubleValue(0), document.NewIntegerValue(10), false},
diff --git a/index/index_test.go b/index/index_test.go
index d4fcd4d5..47c14549 100644
--- a/index/index_test.go
+++ b/index/index_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/engine/memoryengine"
 	"github.com/genjidb/genji/index"
+	"github.com/genjidb/genji/key"
 	"github.com/stretchr/testify/require"
 )
 
@@ -70,13 +71,15 @@ func TestIndexDelete(t *testing.T) {
 
 		pivot := document.NewIntegerValue(10)
 		i := 0
-		err := idx.AscendGreaterOrEqual(&index.Pivot{Value: pivot}, func(val document.Value, key []byte) error {
+		err := idx.AscendGreaterOrEqual(pivot, func(v, k []byte, isEqual bool) error {
 			if i == 0 {
-				require.Equal(t, document.NewDoubleValue(10), val)
-				require.Equal(t, "other-key", string(key))
+				require.True(t, isEqual)
+				requireEqualEncoded(t, document.NewDoubleValue(10), v)
+				require.Equal(t, "other-key", string(k))
 			} else if i == 1 {
-				require.Equal(t, document.NewDoubleValue(11), val)
-				require.Equal(t, "yet-another-key", string(key))
+				require.False(t, isEqual)
+				requireEqualEncoded(t, document.NewDoubleValue(11), v)
+				require.Equal(t, "yet-another-key", string(k))
 			} else {
 				return errors.New("should not reach this point")
 			}
@@ -98,14 +101,16 @@ func TestIndexDelete(t *testing.T) {
 		require.NoError(t, idx.Delete(document.NewIntegerValue(11), []byte("key2")))
 
 		i := 0
-		err := idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
+		err := idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(v, k []byte, isEqual bool) error {
 			switch i {
 			case 0:
-				require.Equal(t, document.NewDoubleValue(10), val)
-				require.Equal(t, "key1", string(key))
+				require.False(t, isEqual)
+				requireEqualEncoded(t, document.NewDoubleValue(10), v)
+				require.Equal(t, "key1", string(k))
 			case 1:
-				require.Equal(t, document.NewDoubleValue(12), val)
-				require.Equal(t, "key3", string(key))
+				require.False(t, isEqual)
+				requireEqualEncoded(t, document.NewDoubleValue(12), v)
+				require.Equal(t, "key3", string(k))
 			default:
 				return errors.New("should not reach this point")
 			}
@@ -129,6 +134,13 @@ func TestIndexDelete(t *testing.T) {
 	}
 }
 
+func requireEqualEncoded(t *testing.T, expected document.Value, actual []byte) {
+	t.Helper()
+
+	enc := key.AppendValue(nil, expected)
+	require.Equal(t, enc, actual)
+}
+
 func TestIndexAscendGreaterThan(t *testing.T) {
 	for _, unique := range []bool{true, false} {
 		text := fmt.Sprintf("Unique: %v, ", unique)
@@ -138,7 +150,7 @@ func TestIndexAscendGreaterThan(t *testing.T) {
 			defer cleanup()
 
 			i := 0
-			err := idx.AscendGreaterOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
+			err := idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(val, key []byte, isEqual bool) error {
 				i++
 				return errors.New("should not iterate")
 			})
@@ -150,11 +162,11 @@ func TestIndexAscendGreaterThan(t *testing.T) {
 			tests := []struct {
 				name  string
 				val   func(i int) document.Value
-				t     index.Type
-				pivot *index.Pivot
+				t     document.ValueType
+				pivot document.Value
 			}{
-				{"floats", func(i int) document.Value { return document.NewIntegerValue(int64(i)) }, index.Float, index.EmptyPivot(document.IntegerValue)},
-				{"bytes", func(i int) document.Value { return document.NewTextValue(string([]byte{byte(i)})) }, index.Bytes, index.EmptyPivot(document.TextValue)},
+				{"integers", func(i int) document.Value { return document.NewIntegerValue(int64(i)) }, document.IntegerValue, document.Value{Type: document.IntegerValue}},
+				{"texts", func(i int) document.Value { return document.NewTextValue(strconv.Itoa(i)) }, document.TextValue, document.Value{Type: document.TextValue}},
 			}
 
 			for _, test := range tests {
@@ -168,14 +180,14 @@ func TestIndexAscendGreaterThan(t *testing.T) {
 
 					var i uint8
 					var count int
-					err := idx.AscendGreaterOrEqual(test.pivot, func(val document.Value, rid []byte) error {
+					err := idx.AscendGreaterOrEqual(test.pivot, func(val, rid []byte, isEqual bool) error {
 						switch test.t {
-						case index.Float:
-							require.Equal(t, document.NewDoubleValue(float64(i)), val)
-						case index.Bytes:
-							require.Equal(t, document.NewBlobValue([]byte{i}), val)
-						case index.Bool:
-							require.Equal(t, document.NewBoolValue(i > 0), val)
+						case document.IntegerValue:
+							requireEqualEncoded(t, document.NewDoubleValue(float64(i)), val)
+						case document.TextValue:
+							requireEqualEncoded(t, document.NewTextValue(strconv.Itoa(int(i))), val)
+						case document.BoolValue:
+							requireEqualEncoded(t, document.NewBoolValue(i > 0), val)
 						}
 						require.Equal(t, []byte{'a' + i}, rid)
 
@@ -200,8 +212,8 @@ func TestIndexAscendGreaterThan(t *testing.T) {
 			var i uint8
 			var count int
 			pivot := document.NewTextValue("C")
-			err := idx.AscendGreaterOrEqual(&index.Pivot{Value: pivot}, func(val document.Value, rid []byte) error {
-				require.Equal(t, document.NewBlobValue([]byte{'C' + i}), val)
+			err := idx.AscendGreaterOrEqual(pivot, func(val, rid []byte, isEqual bool) error {
+				requireEqualEncoded(t, document.NewTextValue(string([]byte{'C' + i})), val)
 				require.Equal(t, []byte{'c' + i}, rid)
 
 				i += 2
@@ -221,29 +233,25 @@ func TestIndexAscendGreaterThan(t *testing.T) {
 				require.NoError(t, idx.Set(document.NewTextValue(strconv.Itoa(int(i))), []byte{'s', 'a' + byte(i)}))
 			}
 
-			var floats, bytes int
+			var ints, texts int
 			var count int
-			err := idx.AscendGreaterOrEqual(nil, func(val document.Value, rid []byte) error {
-				switch val.Type {
-				case document.DoubleValue:
-					require.Equal(t, document.NewDoubleValue(float64(floats)), val)
-					require.Equal(t, []byte{'i', 'a' + byte(floats)}, rid)
-					floats++
-				case document.BlobValue:
-					require.Equal(t, document.NewBlobValue([]byte(strconv.Itoa(int(bytes)))), val)
-					require.Equal(t, []byte{'s', 'a' + byte(bytes)}, rid)
-					bytes++
-				default:
-					t.FailNow()
+			err := idx.AscendGreaterOrEqual(document.Value{}, func(val, rid []byte, isEqual bool) error {
+				if count < 10 {
+					requireEqualEncoded(t, document.NewDoubleValue(float64(ints)), val)
+					require.Equal(t, []byte{'i', 'a' + byte(ints)}, rid)
+					ints++
+				} else {
+					requireEqualEncoded(t, document.NewTextValue(strconv.Itoa(int(texts))), val)
+					require.Equal(t, []byte{'s', 'a' + byte(texts)}, rid)
+					texts++
 				}
-
 				count++
 				return nil
 			})
 			require.NoError(t, err)
 			require.Equal(t, 20, count)
-			require.Equal(t, 10, floats)
-			require.Equal(t, 10, bytes)
+			require.Equal(t, 10, ints)
+			require.Equal(t, 10, texts)
 		})
 	}
 }
@@ -257,7 +265,7 @@ func TestIndexDescendLessOrEqual(t *testing.T) {
 			defer cleanup()
 
 			i := 0
-			err := idx.DescendLessOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
+			err := idx.DescendLessOrEqual(document.Value{Type: document.IntegerValue}, func(val, key []byte, isEqual bool) error {
 				i++
 				return errors.New("should not iterate")
 			})
@@ -275,8 +283,8 @@ func TestIndexDescendLessOrEqual(t *testing.T) {
 
 			var i uint8 = 8
 			var count int
-			err := idx.DescendLessOrEqual(index.EmptyPivot(document.IntegerValue), func(val document.Value, key []byte) error {
-				require.Equal(t, document.NewDoubleValue(float64(i)), val)
+			err := idx.DescendLessOrEqual(document.Value{Type: document.IntegerValue}, func(val, key []byte, isEqual bool) error {
+				requireEqualEncoded(t, document.NewDoubleValue(float64(i)), val)
 				require.Equal(t, []byte{'a' + i}, key)
 
 				i -= 2
@@ -298,8 +306,8 @@ func TestIndexDescendLessOrEqual(t *testing.T) {
 			var i byte = 0
 			var count int
 			pivot := document.NewTextValue("F")
-			err := idx.DescendLessOrEqual(&index.Pivot{Value: pivot}, func(val document.Value, rid []byte) error {
-				require.Equal(t, document.NewBlobValue([]byte{'F' - i}), val)
+			err := idx.DescendLessOrEqual(pivot, func(val, rid []byte, isEqual bool) error {
+				requireEqualEncoded(t, document.NewTextValue(string([]byte{'F' - i})), val)
 				require.Equal(t, []byte{'f' - i}, rid)
 
 				i++
@@ -319,20 +327,17 @@ func TestIndexDescendLessOrEqual(t *testing.T) {
 				require.NoError(t, idx.Set(document.NewTextValue(strconv.Itoa(i)), []byte{'s', 'a' + byte(i)}))
 			}
 
-			var floats, bytes int = 9, 9
+			var ints, texts int = 9, 9
 			var count int = 20
-			err := idx.DescendLessOrEqual(nil, func(val document.Value, rid []byte) error {
-				switch val.Type {
-				case document.DoubleValue:
-					require.Equal(t, document.NewDoubleValue(float64(floats)), val)
-					require.Equal(t, []byte{'i', 'a' + byte(floats)}, rid)
-					floats--
-				case document.BlobValue:
-					require.Equal(t, document.NewBlobValue([]byte(strconv.Itoa(int(bytes)))), val)
-					require.Equal(t, []byte{'s', 'a' + byte(bytes)}, rid)
-					bytes--
-				default:
-					t.FailNow()
+			err := idx.DescendLessOrEqual(document.Value{}, func(val, rid []byte, isEqual bool) error {
+				if count > 10 {
+					requireEqualEncoded(t, document.NewTextValue(strconv.Itoa(int(texts))), val)
+					require.Equal(t, []byte{'s', 'a' + byte(texts)}, rid)
+					texts--
+				} else {
+					requireEqualEncoded(t, document.NewDoubleValue(float64(ints)), val)
+					require.Equal(t, []byte{'i', 'a' + byte(ints)}, rid)
+					ints--
 				}
 
 				count--
@@ -340,8 +345,8 @@ func TestIndexDescendLessOrEqual(t *testing.T) {
 			})
 			require.NoError(t, err)
 			require.Equal(t, 0, count)
-			require.Equal(t, -1, floats)
-			require.Equal(t, -1, bytes)
+			require.Equal(t, -1, ints)
+			require.Equal(t, -1, texts)
 		})
 	}
 }
@@ -357,8 +362,8 @@ func BenchmarkIndexSet(b *testing.B) {
 
 				b.StartTimer()
 				for j := 0; j < size; j++ {
-					k := []byte(fmt.Sprintf("name-%d", j))
-					idx.Set(document.NewBlobValue(k), k)
+					k := fmt.Sprintf("name-%d", j)
+					idx.Set(document.NewTextValue(k), []byte(k))
 				}
 				b.StopTimer()
 				cleanup()
@@ -381,7 +386,7 @@ func BenchmarkIndexIteration(b *testing.B) {
 
 			b.ResetTimer()
 			for i := 0; i < b.N; i++ {
-				idx.AscendGreaterOrEqual(index.EmptyPivot(document.TextValue), func(_ document.Value, _ []byte) error {
+				idx.AscendGreaterOrEqual(document.Value{Type: document.TextValue}, func(_, _ []byte, isEqual bool) error {
 					return nil
 				})
 			}
diff --git a/key/encoding_test.go b/key/encoding_test.go
new file mode 100644
index 00000000..c2dabac3
--- /dev/null
+++ b/key/encoding_test.go
@@ -0,0 +1,62 @@
+package key
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestValueEncodeDecode(t *testing.T) {
+	tests := []struct {
+		name     string
+		expected interface{}
+		enc      func([]byte) []byte
+		dec      func([]byte) (interface{}, error)
+	}{
+		{"bool", true, func(buf []byte) []byte { return AppendBool(buf, true) }, func(buf []byte) (interface{}, error) { return DecodeBool(buf), nil }},
+		{"uint64", uint64(10), func(buf []byte) []byte { return AppendUint64(buf, 10) }, func(buf []byte) (interface{}, error) { return DecodeUint64(buf) }},
+		{"int64", int64(-10), func(buf []byte) []byte { return AppendInt64(buf, -10) }, func(buf []byte) (interface{}, error) { return DecodeInt64(buf) }},
+		{"float64", float64(-3.14), func(buf []byte) []byte { return AppendFloat64(buf, -3.14) }, func(buf []byte) (interface{}, error) { return DecodeFloat64(buf) }},
+	}
+
+	for _, test := range tests {
+		var buf []byte
+		t.Run(test.name, func(t *testing.T) {
+			buf = test.enc(buf[:0])
+			actual, err := test.dec(buf)
+			require.NoError(t, err)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+const Rng = 1000
+
+func TestOrdering(t *testing.T) {
+	tests := []struct {
+		name     string
+		min, max int
+		enc      func([]byte, int) []byte
+	}{
+		{"uint64", 0, 1000, func(buf []byte, i int) []byte { return AppendUint64(buf, uint64(i)) }},
+		{"int64", -1000, 1000, func(buf []byte, i int) []byte { return AppendInt64(buf, int64(i)) }},
+		{"float64", -1000, 1000, func(buf []byte, i int) []byte { return AppendFloat64(buf, float64(i)) }},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var prev, cur []byte
+			for i := test.min; i < test.max; i++ {
+				cur = test.enc(cur[:0], i)
+				if prev == nil {
+					prev = append(prev[:0], cur...)
+					continue
+				}
+
+				require.Equal(t, -1, bytes.Compare(prev, cur))
+				prev = append(prev[:0], cur...)
+			}
+		})
+	}
+}
diff --git a/sql/driver/driver_test.go b/sql/driver/driver_test.go
index b7d12a05..33ff65ce 100644
--- a/sql/driver/driver_test.go
+++ b/sql/driver/driver_test.go
@@ -99,7 +99,7 @@ func TestDriver(t *testing.T) {
 	})
 
 	t.Run("Params", func(t *testing.T) {
-		rows, err := db.Query("SELECT a FROM test WHERE a = ? AND b = ?", 5, []int{6, 7, 8})
+		rows, err := db.Query("SELECT a FROM test WHERE a = ?", 5)
 		require.NoError(t, err)
 		defer rows.Close()
 
diff --git a/sql/planner/optimizer_test.go b/sql/planner/optimizer_test.go
index 63b37af3..9e79e1d6 100644
--- a/sql/planner/optimizer_test.go
+++ b/sql/planner/optimizer_test.go
@@ -102,25 +102,25 @@ func TestPrecalculateExprRule(t *testing.T) {
 			expr.IntegerValue(3),
 		},
 		{
-			"operator with two constant operands: 3 + true -> 4",
-			expr.Add(expr.IntegerValue(3), expr.BoolValue(true)),
-			expr.IntegerValue(4),
+			"operator with two constant operands: 3 + 2.4 -> 5.4",
+			expr.Add(expr.IntegerValue(3), expr.DoubleValue(2.4)),
+			expr.DoubleValue(5.4),
 		},
 		{
-			"operator with constant nested operands: 3 > true - 40 -> true",
-			expr.Gt(expr.IntegerValue(3), expr.Sub(expr.BoolValue(true), expr.DoubleValue(40))),
+			"operator with constant nested operands: 3 > 1 - 40 -> true",
+			expr.Gt(expr.DoubleValue(3), expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40))),
 			expr.BoolValue(true),
 		},
 		{
-			"constant sub-expr: a > true - 40 -> a > -39",
-			expr.Gt(expr.FieldSelector{"a"}, expr.Sub(expr.BoolValue(true), expr.DoubleValue(40))),
+			"constant sub-expr: a > 1 - 40 -> a > -39",
+			expr.Gt(expr.FieldSelector{"a"}, expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40))),
 			expr.Gt(expr.FieldSelector{"a"}, expr.DoubleValue(-39)),
 		},
 		{
-			"non-constant expr list: [a, true - 40] -> [a, -39]",
+			"non-constant expr list: [a, 1 - 40] -> [a, -39]",
 			expr.LiteralExprList{
 				expr.FieldSelector([]string{"a"}),
-				expr.Sub(expr.BoolValue(true), expr.DoubleValue(40)),
+				expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40)),
 			},
 			expr.LiteralExprList{
 				expr.FieldSelector([]string{"a"}),
@@ -128,10 +128,10 @@ func TestPrecalculateExprRule(t *testing.T) {
 			},
 		},
 		{
-			"constant expr list: [3, true - 40] -> array([3, 40])",
+			"constant expr list: [3, 1 - 40] -> array([3, -39])",
 			expr.LiteralExprList{
 				expr.IntegerValue(3),
-				expr.Sub(expr.BoolValue(true), expr.DoubleValue(40)),
+				expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40)),
 			},
 			expr.LiteralValue(document.NewArrayValue(document.NewValueBuffer().
 				Append(document.NewIntegerValue(3)).
@@ -141,7 +141,7 @@ func TestPrecalculateExprRule(t *testing.T) {
 			`non-constant kvpair: {"a": d, "b": 1 - 40} -> {"a": 3, "b": -39}`,
 			expr.KVPairs{
 				{K: "a", V: expr.FieldSelector{"d"}},
-				{K: "b", V: expr.Sub(expr.BoolValue(true), expr.DoubleValue(40))},
+				{K: "b", V: expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40))},
 			},
 			expr.KVPairs{
 				{K: "a", V: expr.FieldSelector{"d"}},
@@ -152,7 +152,7 @@ func TestPrecalculateExprRule(t *testing.T) {
 			`constant kvpair: {"a": 3, "b": 1 - 40} -> document({"a": 3, "b": -39})`,
 			expr.KVPairs{
 				{K: "a", V: expr.IntegerValue(3)},
-				{K: "b", V: expr.Sub(expr.BoolValue(true), expr.DoubleValue(40))},
+				{K: "b", V: expr.Sub(expr.IntegerValue(1), expr.DoubleValue(40))},
 			},
 			expr.LiteralValue(document.NewDocumentValue(document.NewFieldBuffer().
 				Add("a", document.NewIntegerValue(3)).
@@ -165,7 +165,7 @@ func TestPrecalculateExprRule(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			res, err := planner.PrecalculateExprRule(planner.NewTree(planner.NewSelectionNode(planner.NewTableInputNode("foo"), test.e)))
 			require.NoError(t, err)
-			require.Equal(t, res.String(), planner.NewTree(planner.NewSelectionNode(planner.NewTableInputNode("foo"), test.expected)).String())
+			require.Equal(t, planner.NewTree(planner.NewSelectionNode(planner.NewTableInputNode("foo"), test.expected)).String(), res.String())
 		})
 	}
 }
diff --git a/sql/query/expr/comparison_test.go b/sql/query/expr/comparison_test.go
index eb222bd6..94ed618f 100644
--- a/sql/query/expr/comparison_test.go
+++ b/sql/query/expr/comparison_test.go
@@ -51,7 +51,7 @@ func TestComparisonINExpr(t *testing.T) {
 		{"2 IN [2.1, 2.2, 2.0]", document.NewBoolValue(true), false},
 		{"1 IN [2, 3]", document.NewBoolValue(false), false},
 		{"[1] IN [1, 2, 3]", document.NewBoolValue(false), false},
-		{"[1] IN [[1], [2], [3]]", document.NewBoolValue(true), false},
+		{"[1] IN [[1], [2], [3]]", document.NewBoolValue(false), false},
 		{"1 IN {}", document.NewBoolValue(false), false},
 		{"[1, 2] IN 1", document.NewBoolValue(false), false},
 		{"1 IN NULL", nullLitteral, false},
@@ -75,7 +75,7 @@ func TestComparisonNOTINExpr(t *testing.T) {
 		{"1 NOT IN [1, 2, 3]", document.NewBoolValue(false), false},
 		{"1 NOT IN [2, 3]", document.NewBoolValue(true), false},
 		{"[1] NOT IN [1, 2, 3]", document.NewBoolValue(true), false},
-		{"[1] NOT IN [[1], [2], [3]]", document.NewBoolValue(false), false},
+		{"[1] NOT IN [[1], [2], [3]]", document.NewBoolValue(true), false},
 		{"1 NOT IN {}", document.NewBoolValue(true), false},
 		{"[1, 2] NOT IN 1", document.NewBoolValue(true), false},
 		{"1 NOT IN NULL", nullLitteral, false},
diff --git a/sql/query/insert_test.go b/sql/query/insert_test.go
index 62575d52..5736ca99 100644
--- a/sql/query/insert_test.go
+++ b/sql/query/insert_test.go
@@ -177,40 +177,39 @@ func TestInsertStmt(t *testing.T) {
 			name            string
 			fieldConstraint string
 			value           string
-			expectedErr     string
 		}{
-			{"not null without type constraint", "NOT NULL", `{}`, `field "a" is required and must be not null`},
+			{"not null without type constraint", "NOT NULL", `{}`},
 
-			{"array / not null with type constraint", "ARRAY NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"array / not null with non-respected type constraint ", "ARRAY NOT NULL", `{a: 42}`, `cannot cast integer as array`},
+			{"array / not null with type constraint", "ARRAY NOT NULL", `{}`},
+			{"array / not null with non-respected type constraint ", "ARRAY NOT NULL", `{a: 42}`},
 
-			{"blob", "BLOB", `{a: true}`, `cannot cast bool as blob`},
-			{"blob / not null with type constraint", "BLOB NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"blob / not null with non-respected type constraint ", "BLOB NOT NULL", `{a: 42}`, `cannot cast integer as blob`},
+			{"blob", "BLOB", `{a: true}`},
+			{"blob / not null with type constraint", "BLOB NOT NULL", `{}`},
+			{"blob / not null with non-respected type constraint ", "BLOB NOT NULL", `{a: 42}`},
 
-			{"bool / not null with type constraint", "BOOL NOT NULL", `{}`, `field "a" is required and must be not null`},
+			{"bool / not null with type constraint", "BOOL NOT NULL", `{}`},
 
-			{"bytes", "BYTES", `{a: [1,2,3]}`, `cannot cast array as blob`},
-			{"bytes / not null with type constraint", "BYTES NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"bytes / not null with non-respected type constraint ", "BYTES NOT NULL", `{a: 42}`, `cannot cast integer as blob`},
+			{"bytes", "BYTES", `{a: [1,2,3]}`},
+			{"bytes / not null with type constraint", "BYTES NOT NULL", `{}`},
+			{"bytes / not null with non-respected type constraint ", "BYTES NOT NULL", `{a: 42}`},
 
-			{"document", "DOCUMENT", `{"a": "foo"}`, `cannot cast "foo" as document: found "\x00", expected '{'`},
-			{"document / not null with type constraint", "DOCUMENT NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"document / not null with non-respected type constraint ", "DOCUMENT NOT NULL", `{a: false}`, `cannot cast bool as document`},
+			{"document", "DOCUMENT", `{"a": "foo"}`},
+			{"document / not null with type constraint", "DOCUMENT NOT NULL", `{}`},
+			{"document / not null with non-respected type constraint ", "DOCUMENT NOT NULL", `{a: false}`},
 
-			{"duration", "DURATION", `{a: "foo"}`, `cannot cast "foo" as duration: time: invalid duration foo`},
-			{"duration / not null with type constraint", "DURATION NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"duration / not null with non-respected type constraint ", "DURATION NOT NULL", `{a: [1,2,3]}`, `cannot cast array as duration`},
+			{"duration", "DURATION", `{a: "foo"}`},
+			{"duration / not null with type constraint", "DURATION NOT NULL", `{}`},
+			{"duration / not null with non-respected type constraint ", "DURATION NOT NULL", `{a: [1,2,3]}`},
 
-			{"double", "DOUBLE", `{a: "foo"}`, `cannot cast "foo" as double: strconv.ParseFloat: parsing "foo": invalid syntax`},
-			{"double / not null with type constraint", "DOUBLE NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"double / not null with non-respected type constraint ", "DOUBLE NOT NULL", `{a: [1,2,3]}`, `cannot cast array as double`},
+			{"double", "DOUBLE", `{a: "foo"}`},
+			{"double / not null with type constraint", "DOUBLE NOT NULL", `{}`},
+			{"double / not null with non-respected type constraint ", "DOUBLE NOT NULL", `{a: [1,2,3]}`},
 
-			{"integer", "INTEGER", `{a: "foo"}`, `cannot cast "foo" as integer: strconv.ParseFloat: parsing "foo": invalid syntax`},
-			{"integer / not null with type constraint", "INTEGER NOT NULL", `{}`, `field "a" is required and must be not null`},
-			{"integer / not null with non-respected type constraint ", "INTEGER NOT NULL", `{a: [1,2,3]}`, `cannot cast array as integer`},
+			{"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]}`},
 
-			{"text / not null with type constraint", "TEXT NOT NULL", `{}`, `field "a" is required and must be not null`},
+			{"text / not null with type constraint", "TEXT NOT NULL", `{}`},
 		}
 
 		db, err := genji.Open(":memory:")
@@ -225,7 +224,7 @@ func TestInsertStmt(t *testing.T) {
 
 				q = fmt.Sprintf("INSERT INTO test%d VALUES %s", i, test.value)
 				err = db.Exec(q)
-				require.EqualError(t, err, test.expectedErr)
+				require.Error(t, err)
 			})
 		}
 	})