Repository: genjidb/genji
Pull request #406 (2021-06-04 06:23:02)

Thread safe statements
Previously, expressions and params were evaluated
during the planning phase. This change builds the query
plan without evaluating params and expressions
which are then evaluated only during the execution phase.



Patch:
diff --git a/db.go b/db.go
index e2f71c6e..7bab4786 100644
--- a/db.go
+++ b/db.go
@@ -115,6 +115,11 @@ func (db *DB) Prepare(q string) (*Statement, error) {
 		return nil, err
 	}
 
+	err = pq.Prepare(db.ctx, db.db)
+	if err != nil {
+		return nil, err
+	}
+
 	return &Statement{
 		pq: pq,
 		db: db,
@@ -179,6 +184,11 @@ func (tx *Tx) Prepare(q string) (*Statement, error) {
 		return nil, err
 	}
 
+	err = pq.PrepareTx(tx.tx)
+	if err != nil {
+		return nil, err
+	}
+
 	return &Statement{
 		pq: pq,
 		tx: tx,
@@ -370,7 +380,7 @@ func loadCatalogTables(tx *database.Transaction) ([]database.TableInfo, error) {
 			return err
 		}
 
-		ti := stmt.(statement.CreateTableStmt).Info
+		ti := stmt.(*statement.CreateTableStmt).Info
 
 		v, err := d.GetByField("store_name")
 		if err != nil {
@@ -403,7 +413,7 @@ func loadCatalogIndexes(tx *database.Transaction) ([]database.IndexInfo, error)
 			return err
 		}
 
-		indexes = append(indexes, stmt.(statement.CreateIndexStmt).Info)
+		indexes = append(indexes, stmt.(*statement.CreateIndexStmt).Info)
 		return nil
 	})
 
diff --git a/go.mod b/go.mod
index 8b2dcb52..306f85db 100644
--- a/go.mod
+++ b/go.mod
@@ -9,5 +9,6 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/vmihailenco/msgpack/v5 v5.3.2
 	go.etcd.io/bbolt v1.3.5
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/tools v0.1.1
 )
diff --git a/go.sum b/go.sum
index cd1f7301..16e1b4b9 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/internal/database/constraint.go b/internal/database/constraint.go
index 56f6af3c..00a9b34b 100644
--- a/internal/database/constraint.go
+++ b/internal/database/constraint.go
@@ -320,22 +320,6 @@ func CastConversion(v document.Value, path document.Path, targetType document.Va
 	return newV, nil
 }
 
-// LosslessConversion is a ConversionFunc that only converts numbers if there is no precision loss in the process.
-func LosslessNumbersConversion(v document.Value, path document.Path, targetType document.ValueType) (document.Value, error) {
-	if v.Type == document.IntegerValue && targetType == document.DoubleValue {
-		return v.CastAsDouble()
-	}
-
-	if v.Type == document.DoubleValue && targetType == document.IntegerValue {
-		f := v.V.(float64)
-		if float64(int64(f)) == f {
-			return v.CastAsInteger()
-		}
-	}
-
-	return v, nil
-}
-
 // ConvertValueAtPath converts the value using the field constraints that are applicable
 // at the given path.
 func (f FieldConstraints) ConvertValueAtPath(path document.Path, v document.Value, conversionFn ConversionFunc) (document.Value, error) {
diff --git a/internal/expr/expr.go b/internal/expr/expr.go
index 9847b6ac..f548d5ba 100644
--- a/internal/expr/expr.go
+++ b/internal/expr/expr.go
@@ -94,11 +94,11 @@ type NamedExpr struct {
 }
 
 // Name returns ExprName.
-func (e NamedExpr) Name() string {
+func (e *NamedExpr) Name() string {
 	return e.ExprName
 }
 
-func (e NamedExpr) String() string {
+func (e *NamedExpr) String() string {
 	return stringutil.Sprintf("%s", e.Expr)
 }
 
diff --git a/internal/expr/function.go b/internal/expr/function.go
index 139a0f1d..1cbd51c2 100644
--- a/internal/expr/function.go
+++ b/internal/expr/function.go
@@ -93,6 +93,8 @@ type Aggregator interface {
 
 // An AggregatorBuilder is a type that can create aggregators.
 type AggregatorBuilder interface {
+	Expr
+
 	Aggregator() Aggregator
 }
 
diff --git a/internal/expr/literal.go b/internal/expr/literal.go
index 57f74d4a..12ee1a3c 100644
--- a/internal/expr/literal.go
+++ b/internal/expr/literal.go
@@ -132,7 +132,7 @@ func (kvp *KVPairs) IsEqual(other Expr) bool {
 }
 
 // Eval turns a list of KVPairs into a document.
-func (kvp KVPairs) Eval(env *Environment) (document.Value, error) {
+func (kvp *KVPairs) Eval(env *Environment) (document.Value, error) {
 	var fb document.FieldBuffer
 	if kvp.SelfReferenced {
 		if _, ok := env.GetDocument(); !ok {
@@ -153,7 +153,7 @@ func (kvp KVPairs) Eval(env *Environment) (document.Value, error) {
 }
 
 // String implements the stringutil.Stringer interface.
-func (kvp KVPairs) String() string {
+func (kvp *KVPairs) String() string {
 	var b strings.Builder
 
 	b.WriteRune('{')
diff --git a/internal/planner/optimizer.go b/internal/planner/optimizer.go
index b1294d79..e0d53de3 100644
--- a/internal/planner/optimizer.go
+++ b/internal/planner/optimizer.go
@@ -9,24 +9,24 @@ import (
 	"github.com/genjidb/genji/internal/stringutil"
 )
 
-var optimizerRules = []func(s *stream.Stream, tx *database.Transaction, params []expr.Param) (*stream.Stream, error){
+var optimizerRules = []func(s *stream.Stream, tx *database.Transaction) (*stream.Stream, error){
 	SplitANDConditionRule,
-	PrecalculateExprRule,
-	RemoveUnnecessaryFilterNodesRule,
-	RemoveUnnecessaryDistinctNodeRule,
 	RemoveUnnecessaryProjection,
+	RemoveUnnecessaryDistinctNodeRule,
+	RemoveUnnecessaryFilterNodesRule,
 	UseIndexBasedOnFilterNodeRule,
+	PrecalculateExprRule,
 }
 
 // Optimize takes a tree, applies a list of optimization rules
 // and returns an optimized tree.
 // Depending on the rule, the tree may be modified in place or
 // replaced by a new one.
-func Optimize(s *stream.Stream, tx *database.Transaction, params []expr.Param) (*stream.Stream, error) {
+func Optimize(s *stream.Stream, tx *database.Transaction) (*stream.Stream, error) {
 	var err error
 
 	for _, rule := range optimizerRules {
-		s, err = rule(s, tx, params)
+		s, err = rule(s, tx)
 		if err != nil {
 			return nil, err
 		}
@@ -49,7 +49,7 @@ func Optimize(s *stream.Stream, tx *database.Transaction, params []expr.Param) (
 //     filter(a > 2)
 //     filter(b != 3)
 //     filter(c < 2)
-func SplitANDConditionRule(s *stream.Stream, _ *database.Transaction, _ []expr.Param) (*stream.Stream, error) {
+func SplitANDConditionRule(s *stream.Stream, _ *database.Transaction) (*stream.Stream, error) {
 	n := s.Op
 
 	for n != nil {
@@ -103,20 +103,20 @@ func splitANDExpr(cond expr.Expr) (exprs []expr.Expr) {
 // Examples:
 //   3 + 4 --> 7
 //   3 + 1 > 10 - a --> 4 > 10 - a
-func PrecalculateExprRule(s *stream.Stream, _ *database.Transaction, params []expr.Param) (*stream.Stream, error) {
+func PrecalculateExprRule(s *stream.Stream, _ *database.Transaction) (*stream.Stream, error) {
 	n := s.Op
 
 	var err error
 	for n != nil {
 		switch t := n.(type) {
 		case *stream.FilterOperator:
-			t.E, err = precalculateExpr(t.E, params)
+			t.E, err = precalculateExpr(t.E)
 			if err != nil {
 				return nil, err
 			}
 		case *stream.ProjectOperator:
 			for i, e := range t.Exprs {
-				t.Exprs[i], err = precalculateExpr(e, params)
+				t.Exprs[i], err = precalculateExpr(e)
 				if err != nil {
 					return nil, err
 				}
@@ -133,34 +133,18 @@ func PrecalculateExprRule(s *stream.Stream, _ *database.Transaction, params []ex
 // expression nodes when possible.
 // it returns a new expression with simplified nodes.
 // if no simplification is possible it returns the same expression.
-func precalculateExpr(e expr.Expr, params []expr.Param) (expr.Expr, error) {
+func precalculateExpr(e expr.Expr) (expr.Expr, error) {
 	switch t := e.(type) {
 	case expr.LiteralExprList:
 		// we assume that the list of expressions contains only literals
 		// until proven wrong.
-		literalsOnly := true
 		for i, te := range t {
-			newExpr, err := precalculateExpr(te, params)
+			newExpr, err := precalculateExpr(te)
 			if err != nil {
 				return nil, err
 			}
-			if _, ok := newExpr.(expr.LiteralValue); !ok {
-				literalsOnly = false
-			}
 			t[i] = newExpr
 		}
-
-		// if literalsOnly is still true, it means we have a list of constant expressions
-		// (ex: [1, 4, true]). We can transform that into a document.Array.
-		if literalsOnly {
-			values := make([]document.Value, len(t))
-			for i := range t {
-				values[i] = document.Value(t[i].(expr.LiteralValue))
-			}
-
-			return expr.LiteralValue(document.NewArrayValue(document.NewValueBuffer(values...))), nil
-		}
-
 	case *expr.KVPairs:
 		// we assume that the list of kvpairs contains only literals
 		// until proven wrong.
@@ -168,7 +152,7 @@ func precalculateExpr(e expr.Expr, params []expr.Param) (expr.Expr, error) {
 
 		var err error
 		for i, kv := range t.Pairs {
-			kv.V, err = precalculateExpr(kv.V, params)
+			kv.V, err = precalculateExpr(kv.V)
 			if err != nil {
 				return nil, err
 			}
@@ -201,11 +185,11 @@ func precalculateExpr(e expr.Expr, params []expr.Param) (expr.Expr, error) {
 			return e, nil
 		}
 
-		lh, err := precalculateExpr(t.LeftHand(), params)
+		lh, err := precalculateExpr(t.LeftHand())
 		if err != nil {
 			return nil, err
 		}
-		rh, err := precalculateExpr(t.RightHand(), params)
+		rh, err := precalculateExpr(t.RightHand())
 		if err != nil {
 			return nil, err
 		}
@@ -224,12 +208,6 @@ func precalculateExpr(e expr.Expr, params []expr.Param) (expr.Expr, error) {
 			// we replace this expression with the result of its evaluation
 			return expr.LiteralValue(v), nil
 		}
-	case expr.PositionalParam, expr.NamedParam:
-		v, err := e.Eval(&expr.Environment{Params: params})
-		if err != nil {
-			return nil, err
-		}
-		return expr.LiteralValue(v), nil
 	}
 
 	return e, nil
@@ -239,7 +217,7 @@ func precalculateExpr(e expr.Expr, params []expr.Param) (expr.Expr, error) {
 // condition is a constant expression that evaluates to a truthy value.
 // if it evaluates to a falsy value, it considers that the tree
 // will not stream any document, so it returns an empty tree.
-func RemoveUnnecessaryFilterNodesRule(s *stream.Stream, _ *database.Transaction, _ []expr.Param) (*stream.Stream, error) {
+func RemoveUnnecessaryFilterNodesRule(s *stream.Stream, _ *database.Transaction) (*stream.Stream, error) {
 	n := s.Op
 
 	for n != nil {
@@ -290,7 +268,7 @@ func RemoveUnnecessaryFilterNodesRule(s *stream.Stream, _ *database.Transaction,
 
 // RemoveUnnecessaryProjection removes any project node whose
 // expression is a wildcard only.
-func RemoveUnnecessaryProjection(s *stream.Stream, _ *database.Transaction, _ []expr.Param) (*stream.Stream, error) {
+func RemoveUnnecessaryProjection(s *stream.Stream, _ *database.Transaction) (*stream.Stream, error) {
 	n := s.Op
 
 	for n != nil {
@@ -312,7 +290,7 @@ func RemoveUnnecessaryProjection(s *stream.Stream, _ *database.Transaction, _ []
 
 // RemoveUnnecessaryDistinctNodeRule removes any Dedup nodes
 // where projection is already unique.
-func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transaction, _ []expr.Param) (*stream.Stream, error) {
+func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transaction) (*stream.Stream, error) {
 	n := s.Op
 
 	// we assume that if we are reading from a table, the first
@@ -338,7 +316,6 @@ func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transactio
 			if prev != nil {
 				pn, ok := prev.(*stream.ProjectOperator)
 				if ok {
-
 					// if the projection is unique, we remove the node from the tree
 					if isProjectionUnique(t.Indexes, pn, t.Info.GetPrimaryKey()) {
 						s.Remove(n)
@@ -357,9 +334,8 @@ func RemoveUnnecessaryDistinctNodeRule(s *stream.Stream, tx *database.Transactio
 
 func isProjectionUnique(indexes database.Indexes, po *stream.ProjectOperator, pk *database.FieldConstraint) bool {
 	for _, field := range po.Exprs {
-		e, ok := field.(*expr.NamedExpr)
+		_, ok := field.(*expr.NamedExpr)
 		if ok {
-			field = e.Expr
 			return false
 		}
 
@@ -384,7 +360,7 @@ func isProjectionUnique(indexes database.Indexes, po *stream.ProjectOperator, pk
 
 type filterNode struct {
 	path document.Path
-	v    document.Value
+	e    expr.Expr
 	f    *stream.FilterOperator
 }
 
@@ -399,7 +375,7 @@ type filterNode struct {
 //
 // TODO(asdine): add support for ORDER BY
 // TODO(jh): clarify cost code in composite indexes case
-func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, params []expr.Param) (*stream.Stream, error) {
+func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction) (*stream.Stream, error) {
 	// first we lookup for the seq scan node.
 	// Here we will assume that at this point
 	// if there is one it has to be the
@@ -443,22 +419,15 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, p
 				continue
 			}
 
-			ev, ok := e.(expr.LiteralValue)
-			if !ok {
-				continue
-			}
-
-			v := document.Value(ev)
-
-			filterNodes = append(filterNodes, filterNode{path: path, v: v, f: f})
+			filterNodes = append(filterNodes, filterNode{path: path, e: e, f: f})
 
 			// check for primary keys scan while iterating on the filter nodes
 			if pk := t.Info.GetPrimaryKey(); pk != nil && pk.Path.IsEqual(path) {
-				// if both types are different, don't select this scanner
-				v, ok, err := operandCanUseIndex(pk.Type, pk.Path, t.Info.FieldConstraints, v)
-				if err != nil {
-					return nil, err
-				}
+				// // if both types are different, don't select this scanner
+				// v, ok, err := operandCanUseIndex(pk.Type, pk.Path, t.Info.FieldConstraints, v)
+				// if err != nil {
+				// 	return nil, err
+				// }
 
 				if !ok {
 					continue
@@ -469,7 +438,7 @@ func UseIndexBasedOnFilterNodeRule(s *stream.Stream, tx *database.Transaction, p
 						priority:  3,
 					}
 
-					ranges, err := getRangesFromOp(op, v)
+					ranges, err := getRangesFromOp(op, e)
 					if err != nil {
 						return nil, err
 					}
@@ -556,17 +525,6 @@ outer:
 						continue outer
 					}
 				}
-
-				// what the index says this node type must be
-				typ := idx.Info.Types[i]
-
-				fno.v, ok, err = operandCanUseIndex(typ, fno.path, t.Info.FieldConstraints, fno.v)
-				if err != nil {
-					return nil, err
-				}
-				if !ok {
-					continue outer
-				}
 			} else {
 				// if on the index idx_abc(a,b,c), a is found, b isn't but c is
 				// then idx_abc is valid but just with a, c will use a filter node instead
@@ -679,23 +637,19 @@ type candidate struct {
 }
 
 func operatorCanUseIndex(op expr.Operator) (bool, document.Path, expr.Expr) {
-	lf, leftIsField := op.LeftHand().(expr.Path)
-	rf, rightIsField := op.RightHand().(expr.Path)
+	lf, leftIsPath := op.LeftHand().(expr.Path)
+	rf, rightIsPath := op.RightHand().(expr.Path)
 
 	// Special case for IN operator: only left operand is valid for index usage
 	// valid:   a IN [1, 2, 3]
 	// invalid: 1 IN a
 	if op.Token() == scanner.IN {
-		if leftIsField && !rightIsField {
+		if leftIsPath && !rightIsPath {
 			rh := op.RightHand()
-			// The IN operator can use indexes only if the right hand side is an array with constants.
-			// At this point, we know that PrecalculateExprRule has converted any constant expression into
-			// actual values, so we can check if the right hand side is an array.
-			lv, ok := rh.(expr.LiteralValue)
-			if !ok || lv.Type != document.ArrayValue {
+			// The IN operator can use indexes only if the right hand side is an expression list.
+			if _, ok := rh.(expr.LiteralExprList); !ok {
 				return false, nil, nil
 			}
-
 			return true, document.Path(lf), rh
 		}
 
@@ -703,57 +657,39 @@ func operatorCanUseIndex(op expr.Operator) (bool, document.Path, expr.Expr) {
 	}
 
 	// path OP expr
-	if leftIsField && !rightIsField {
+	if leftIsPath && !rightIsPath {
 		return true, document.Path(lf), op.RightHand()
 	}
 
 	// expr OP path
-	if rightIsField && !leftIsField {
+	if rightIsPath && !leftIsPath {
 		return true, document.Path(rf), op.LeftHand()
 	}
 
 	return false, nil, nil
 }
 
-func operandCanUseIndex(indexType document.ValueType, path document.Path, fc database.FieldConstraints, v document.Value) (document.Value, bool, error) {
-	// ensure the operand satisfies all the constraints, index can work only on exact types.
-	// if a number is encountered, try to convert it to the right type if and only if the conversion
-	// is lossless.
-	converted, err := fc.ConvertValueAtPath(path, v, database.LosslessNumbersConversion)
-	if err != nil {
-		return v, false, err
-	}
-
-	// if the index is not typed, any operand can work
-	if indexType.IsAny() {
-		return converted, true, nil
-	}
-
-	// if the index is typed, it must be of the same type as the converted value
-	return converted, indexType == converted.Type, nil
-}
-
 func getRangesFromFilterNodes(fnodes []*filterNode) (stream.IndexRanges, error) {
 	var ranges stream.IndexRanges
-	vb := document.NewValueBuffer()
+	var el expr.LiteralExprList
 	// store IN operands with their position (in the index paths) as a key
-	inOperands := make(map[int]document.Array)
+	inOperands := make(map[int]expr.LiteralExprList)
 
 	for i, fno := range fnodes {
 		op := fno.f.E.(expr.Operator)
-		v := fno.v
+		e := fno.e
 
 		switch {
 		case op.Token() == scanner.IN:
 			// mark where the IN operator values are supposed to go is in the buffer
 			// and what are the value needed to generate the ranges.
 			// operatorCanUseIndex made sure v is an array.
-			inOperands[i] = v.V.(document.Array)
+			inOperands[i] = e.(expr.LiteralExprList)
 
 			// placeholder for when we'll explode the IN operands in multiple ranges
-			vb = vb.Append(document.Value{})
+			el = append(el, expr.LiteralValue{})
 		case expr.IsComparisonOperator(op):
-			vb = vb.Append(v)
+			el = append(el, e)
 		default:
 			panic(stringutil.Sprintf("unknown operator %#v", op))
 		}
@@ -765,23 +701,29 @@ func getRangesFromFilterNodes(fnodes []*filterNode) (stream.IndexRanges, error)
 	}
 
 	// a small helper func to create a range based on an operator type
-	buildRange := func(op expr.Operator, vb *document.ValueBuffer) stream.IndexRange {
-		var rng stream.IndexRange
+	buildRange := func(op expr.Operator, el expr.LiteralExprList) stream.IndexRange {
+		var paths []document.Path
+		for i := range el {
+			paths = append(paths, fnodes[i].path)
+		}
+		rng := stream.IndexRange{
+			Paths: paths,
+		}
 
 		switch op.Token() {
 		case scanner.EQ, scanner.IN:
 			rng.Exact = true
-			rng.Min = vb
+			rng.Min = el
 		case scanner.GT:
 			rng.Exclusive = true
-			rng.Min = vb
+			rng.Min = el
 		case scanner.GTE:
-			rng.Min = vb
+			rng.Min = el
 		case scanner.LT:
 			rng.Exclusive = true
-			rng.Max = vb
+			rng.Max = el
 		case scanner.LTE:
-			rng.Max = vb
+			rng.Max = el
 		}
 
 		return rng
@@ -789,15 +731,12 @@ func getRangesFromFilterNodes(fnodes []*filterNode) (stream.IndexRanges, error)
 
 	// explode the IN operator values in multiple ranges
 	for pos, operands := range inOperands {
-		err := operands.Iterate(func(j int, value document.Value) error {
-			newVB := document.NewValueBuffer()
-			err := newVB.Copy(vb)
-			if err != nil {
-				return err
-			}
+		for i := range operands {
+			newVB := make(expr.LiteralExprList, len(el))
+			copy(newVB, el)
 
 			// insert IN operand at the right position, replacing the placeholder value
-			newVB.Values[pos] = value
+			newVB[pos] = operands[i]
 
 			// the last node is the only one that can be a comparison operator, so
 			// it's the one setting the range behaviour
@@ -807,11 +746,6 @@ func getRangesFromFilterNodes(fnodes []*filterNode) (stream.IndexRanges, error)
 			rng := buildRange(op, newVB)
 
 			ranges = ranges.Append(rng)
-			return nil
-		})
-
-		if err != nil {
-			return nil, err
 		}
 	}
 
@@ -825,50 +759,46 @@ func getRangesFromFilterNodes(fnodes []*filterNode) (stream.IndexRanges, error)
 	// it's the one setting the range behaviour
 	last := fnodes[len(fnodes)-1]
 	op := last.f.E.(expr.Operator)
-	rng := buildRange(op, vb)
+	rng := buildRange(op, el)
 
 	return stream.IndexRanges{rng}, nil
 }
 
-func getRangesFromOp(op expr.Operator, v document.Value) (stream.ValueRanges, error) {
+func getRangesFromOp(op expr.Operator, e expr.Expr) (stream.ValueRanges, error) {
 	var ranges stream.ValueRanges
 
 	switch op.Token() {
 	case scanner.EQ:
 		ranges = ranges.Append(stream.ValueRange{
-			Min:   v,
+			Min:   e,
 			Exact: true,
 		})
 	case scanner.GT:
 		ranges = ranges.Append(stream.ValueRange{
-			Min:       v,
+			Min:       e,
 			Exclusive: true,
 		})
 	case scanner.GTE:
 		ranges = ranges.Append(stream.ValueRange{
-			Min: v,
+			Min: e,
 		})
 	case scanner.LT:
 		ranges = ranges.Append(stream.ValueRange{
-			Max:       v,
+			Max:       e,
 			Exclusive: true,
 		})
 	case scanner.LTE:
 		ranges = ranges.Append(stream.ValueRange{
-			Max: v,
+			Max: e,
 		})
 	case scanner.IN:
-		// operatorCanUseIndex made sure e is an array.
-		a := v.V.(document.Array)
-		err := a.Iterate(func(i int, value document.Value) error {
+		// operatorCanUseIndex made sure e is a expression list.
+		el := e.(expr.LiteralExprList)
+		for i := range el {
 			ranges = ranges.Append(stream.ValueRange{
-				Min:   value,
+				Min:   el[i],
 				Exact: true,
 			})
-			return nil
-		})
-		if err != nil {
-			return nil, err
 		}
 	default:
 		panic(stringutil.Sprintf("unknown operator %#v", op))
diff --git a/internal/query/query.go b/internal/query/query.go
index 750df2d4..ef5d2ee1 100644
--- a/internal/query/query.go
+++ b/internal/query/query.go
@@ -18,6 +18,11 @@ type Query struct {
 	autoCommit bool
 }
 
+// New creates a new query with the given statements.
+func New(statements ...statement.Statement) Query {
+	return Query{Statements: statements}
+}
+
 // Run executes all the statements in their own transaction and returns the last result.
 func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param) (*statement.Result, error) {
 	var res statement.Result
@@ -28,10 +33,6 @@ func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param
 		q.autoCommit = true
 	}
 
-	type queryAlterer interface {
-		alterQuery(ctx context.Context, db *database.Database, q *Query) error
-	}
-
 	for i, stmt := range q.Statements {
 		select {
 		case <-ctx.Done():
@@ -113,6 +114,66 @@ func (q Query) Run(ctx context.Context, db *database.Database, args []expr.Param
 	return &res, nil
 }
 
+type queryAlterer interface {
+	alterQuery(ctx context.Context, db *database.Database, q *Query) error
+}
+
+// Prepare the statements by calling their Prepare methods.
+// It stops at the first statement that doesn't implement the statement.Preparer interface.
+func (q Query) Prepare(ctx context.Context, db *database.Database) error {
+	var err error
+	var tx *database.Transaction
+
+	for _, stmt := range q.Statements {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+
+		p, ok := stmt.(statement.Preparer)
+		if !ok {
+			break
+		}
+
+		if tx == nil {
+			tx = db.GetAttachedTx()
+			if tx == nil {
+				tx, err = db.BeginTx(ctx, &database.TxOptions{
+					ReadOnly: true,
+				})
+				if err != nil {
+					return err
+				}
+				defer tx.Rollback()
+			}
+		}
+
+		err = p.Prepare(tx)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (q Query) PrepareTx(tx *database.Transaction) error {
+	for _, stmt := range q.Statements {
+		p, ok := stmt.(statement.Preparer)
+		if !ok {
+			break
+		}
+
+		err := p.Prepare(tx)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // Exec the query within the given transaction.
 func (q Query) Exec(tx *database.Transaction, args []expr.Param) (*statement.Result, error) {
 	var res statement.Result
@@ -137,8 +198,3 @@ func (q Query) Exec(tx *database.Transaction, args []expr.Param) (*statement.Res
 
 	return &res, nil
 }
-
-// New creates a new query with the given statements.
-func New(statements ...statement.Statement) Query {
-	return Query{Statements: statements}
-}
diff --git a/internal/query/statement/create.go b/internal/query/statement/create.go
index e743cb1d..9be63b85 100644
--- a/internal/query/statement/create.go
+++ b/internal/query/statement/create.go
@@ -14,13 +14,13 @@ type CreateTableStmt struct {
 }
 
 // IsReadOnly always returns false. It implements the Statement interface.
-func (stmt CreateTableStmt) IsReadOnly() bool {
+func (stmt *CreateTableStmt) IsReadOnly() bool {
 	return false
 }
 
 // Run runs the Create table statement in the given transaction.
 // It implements the Statement interface.
-func (stmt CreateTableStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
+func (stmt *CreateTableStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
 	var res Result
 
 	err := tx.Catalog.CreateTable(tx, stmt.Info.TableName, &stmt.Info)
@@ -53,13 +53,13 @@ type CreateIndexStmt struct {
 }
 
 // IsReadOnly always returns false. It implements the Statement interface.
-func (stmt CreateIndexStmt) IsReadOnly() bool {
+func (stmt *CreateIndexStmt) IsReadOnly() bool {
 	return false
 }
 
 // Run runs the Create index statement in the given transaction.
 // It implements the Statement interface.
-func (stmt CreateIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
+func (stmt *CreateIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
 	var res Result
 
 	err := tx.Catalog.CreateIndex(tx, &stmt.Info)
diff --git a/internal/query/statement/delete.go b/internal/query/statement/delete.go
index 2f17fb12..1cc139bf 100644
--- a/internal/query/statement/delete.go
+++ b/internal/query/statement/delete.go
@@ -1,7 +1,6 @@
 package statement
 
 import (
-	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/sql/scanner"
 	"github.com/genjidb/genji/internal/stream"
@@ -18,21 +17,6 @@ type DeleteStmt struct {
 	OrderByDirection scanner.Token
 }
 
-func (stmt *DeleteStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	var res Result
-
-	s, err := stmt.ToStream()
-	if err != nil {
-		return res, err
-	}
-
-	return s.Run(tx, params)
-}
-
-func (stmt *DeleteStmt) IsReadOnly() bool {
-	return false
-}
-
 func (stmt *DeleteStmt) ToStream() (*StreamStmt, error) {
 	s := stream.New(stream.SeqScan(stmt.TableName))
 
diff --git a/internal/query/statement/explain.go b/internal/query/statement/explain.go
index 45a7b14a..ee0c9c96 100644
--- a/internal/query/statement/explain.go
+++ b/internal/query/statement/explain.go
@@ -6,7 +6,6 @@ import (
 	"github.com/genjidb/genji/document"
 	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
-	"github.com/genjidb/genji/internal/planner"
 	"github.com/genjidb/genji/internal/stream"
 )
 
@@ -22,40 +21,25 @@ type ExplainStmt struct {
 // displaying all the operations.
 // Explain currently only works on SELECT, UPDATE, INSERT and DELETE statements.
 func (stmt *ExplainStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	var ss *StreamStmt
-	var err error
-	var res Result
-
-	switch t := stmt.Statement.(type) {
-	case *SelectStmt:
-		ss, err = t.ToStream()
-	case *UpdateStmt:
-		ss = t.ToStream()
-	case *InsertStmt:
-		ss, err = t.ToStream()
-	case *DeleteStmt:
-		ss, err = t.ToStream()
-	default:
+	st, ok := stmt.Statement.(*StreamStmt)
+	if !ok {
 		return Result{}, errors.New("EXPLAIN only works on INSERT, SELECT, UPDATE AND DELETE statements")
 	}
-	if err != nil {
-		return res, err
-	}
 
-	s, err := planner.Optimize(ss.Stream, tx, params)
+	err := st.Prepare(tx)
 	if err != nil {
 		return Result{}, err
 	}
 
 	var plan string
-	if s != nil {
-		plan = s.String()
+	if st.PreparedStream != nil {
+		plan = st.PreparedStream.String()
 	} else {
 		plan = "<no exec>"
 	}
 
 	newStatement := StreamStmt{
-		Stream: &stream.Stream{
+		PreparedStream: &stream.Stream{
 			Op: stream.Project(
 				&expr.NamedExpr{
 					ExprName: "plan",
diff --git a/internal/query/statement/insert.go b/internal/query/statement/insert.go
index 57a263b9..52a1d65f 100644
--- a/internal/query/statement/insert.go
+++ b/internal/query/statement/insert.go
@@ -13,37 +13,18 @@ type InsertStmt struct {
 	TableName  string
 	Values     []expr.Expr
 	Fields     []string
-	SelectStmt *SelectStmt
+	SelectStmt *StreamStmt
 	Returning  []expr.Expr
 	OnConflict database.OnInsertConflictAction
 }
 
-func (stmt *InsertStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	var res Result
-
-	s, err := stmt.ToStream()
-	if err != nil {
-		return res, err
-	}
-
-	return s.Run(tx, params)
-}
-
-func (stmt *InsertStmt) IsReadOnly() bool {
-	return false
-}
-
 func (stmt *InsertStmt) ToStream() (*StreamStmt, error) {
 	var s *stream.Stream
 
 	if stmt.Values != nil {
 		s = stream.New(stream.Expressions(stmt.Values...))
 	} else {
-		st, err := stmt.SelectStmt.ToStream()
-		if err != nil {
-			return nil, err
-		}
-		s = st.Stream
+		s = stmt.SelectStmt.Stream
 
 		// ensure we are not reading and writing to the same table.
 		if s.First().(*stream.SeqScanOperator).TableName == stmt.TableName {
diff --git a/internal/query/statement/select.go b/internal/query/statement/select.go
index 2caaf439..83f94ab7 100644
--- a/internal/query/statement/select.go
+++ b/internal/query/statement/select.go
@@ -3,7 +3,6 @@ package statement
 import (
 	"errors"
 
-	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/sql/scanner"
 	"github.com/genjidb/genji/internal/stream"
@@ -23,21 +22,6 @@ type SelectStmt struct {
 	ProjectionExprs  []expr.Expr
 }
 
-func (stmt *SelectStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	var res Result
-
-	s, err := stmt.ToStream()
-	if err != nil {
-		return res, err
-	}
-
-	return s.Run(tx, params)
-}
-
-func (stmt *SelectStmt) IsReadOnly() bool {
-	return true
-}
-
 func (stmt *SelectStmt) ToStream() (*StreamStmt, error) {
 	var s *stream.Stream
 
diff --git a/internal/query/statement/statement.go b/internal/query/statement/statement.go
index c9016715..411a0111 100644
--- a/internal/query/statement/statement.go
+++ b/internal/query/statement/statement.go
@@ -14,6 +14,10 @@ type Statement interface {
 	IsReadOnly() bool
 }
 
+type Preparer interface {
+	Prepare(tx *database.Transaction) error
+}
+
 // Result of a query.
 type Result struct {
 	Iterator document.Iterator
diff --git a/internal/query/statement/stream.go b/internal/query/statement/stream.go
index bf05bb30..bc2a773e 100644
--- a/internal/query/statement/stream.go
+++ b/internal/query/statement/stream.go
@@ -12,19 +12,30 @@ import (
 type StreamStmt struct {
 	Stream   *stream.Stream
 	ReadOnly bool
+
+	PreparedStream *stream.Stream
+}
+
+// Prepare optimizes the stream and stores it in s.
+func (s *StreamStmt) Prepare(tx *database.Transaction) error {
+	var err error
+	s.PreparedStream, err = planner.Optimize(s.Stream, tx)
+	return err
 }
 
 // Run returns a result containing the stream. The stream will be executed by calling the Iterate method of
 // the result.
 func (s *StreamStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	st, err := planner.Optimize(s.Stream.Clone(), tx, params)
-	if err != nil || st == nil {
-		return Result{}, err
+	if s.PreparedStream == nil {
+		err := s.Prepare(tx)
+		if err != nil {
+			return Result{}, err
+		}
 	}
 
 	return Result{
 		Iterator: &StreamStmtIterator{
-			Stream: st,
+			Stream: s.PreparedStream,
 			Tx:     tx,
 			Params: params,
 		},
diff --git a/internal/query/statement/update.go b/internal/query/statement/update.go
index 7a512711..c7b218ff 100644
--- a/internal/query/statement/update.go
+++ b/internal/query/statement/update.go
@@ -2,7 +2,6 @@ package statement
 
 import (
 	"github.com/genjidb/genji/document"
-	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/stream"
 )
@@ -28,16 +27,6 @@ type UpdateSetPair struct {
 	E    expr.Expr
 }
 
-func (stmt *UpdateStmt) Run(tx *database.Transaction, params []expr.Param) (Result, error) {
-	s := stmt.ToStream()
-
-	return s.Run(tx, params)
-}
-
-func (stmt *UpdateStmt) IsReadOnly() bool {
-	return false
-}
-
 // ToTree turns the statement into a stream.
 func (stmt *UpdateStmt) ToStream() *StreamStmt {
 	s := stream.New(stream.SeqScan(stmt.TableName))
diff --git a/internal/sql/parser/create.go b/internal/sql/parser/create.go
index 9bd05ac3..f4c0551f 100644
--- a/internal/sql/parser/create.go
+++ b/internal/sql/parser/create.go
@@ -30,25 +30,25 @@ func (p *Parser) parseCreateStatement() (statement.Statement, error) {
 
 // parseCreateTableStatement parses a create table string and returns a Statement AST object.
 // This function assumes the CREATE TABLE tokens have already been consumed.
-func (p *Parser) parseCreateTableStatement() (statement.CreateTableStmt, error) {
+func (p *Parser) parseCreateTableStatement() (*statement.CreateTableStmt, error) {
 	var stmt statement.CreateTableStmt
 	var err error
 
 	// Parse IF NOT EXISTS
 	stmt.IfNotExists, err = p.parseOptional(scanner.IF, scanner.NOT, scanner.EXISTS)
 	if err != nil {
-		return stmt, err
+		return nil, err
 	}
 
 	// Parse table name
 	stmt.Info.TableName, err = p.parseIdent()
 	if err != nil {
-		return stmt, err
+		return nil, err
 	}
 
 	// parse field constraints
 	err = p.parseConstraints(&stmt)
-	return stmt, err
+	return &stmt, err
 }
 
 func (p *Parser) parseFieldDefinition(fc *database.FieldConstraint) (err error) {
@@ -283,7 +283,7 @@ func (p *Parser) parseTableConstraint(stmt *statement.CreateTableStmt) (bool, er
 
 // parseCreateIndexStatement parses a create index string and returns a Statement AST object.
 // This function assumes the CREATE INDEX or CREATE UNIQUE INDEX tokens have already been consumed.
-func (p *Parser) parseCreateIndexStatement(unique bool) (statement.CreateIndexStmt, error) {
+func (p *Parser) parseCreateIndexStatement(unique bool) (*statement.CreateIndexStmt, error) {
 	var err error
 	var stmt statement.CreateIndexStmt
 	stmt.Info.Unique = unique
@@ -291,7 +291,7 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (statement.CreateIndexSt
 	// Parse IF NOT EXISTS
 	stmt.IfNotExists, err = p.parseOptional(scanner.IF, scanner.NOT, scanner.EXISTS)
 	if err != nil {
-		return stmt, err
+		return nil, err
 	}
 
 	// Parse optional index name
@@ -299,7 +299,7 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (statement.CreateIndexSt
 	if err != nil {
 		// if IF NOT EXISTS is set, index name is mandatory
 		if stmt.IfNotExists {
-			return stmt, err
+			return nil, err
 		}
 
 		p.Unscan()
@@ -307,25 +307,25 @@ func (p *Parser) parseCreateIndexStatement(unique bool) (statement.CreateIndexSt
 
 	// Parse "ON"
 	if err := p.parseTokens(scanner.ON); err != nil {
-		return stmt, err
+		return nil, err
 	}
 
 	// Parse table name
 	stmt.Info.TableName, err = p.parseIdent()
 	if err != nil {
-		return stmt, err
+		return nil, err
 	}
 
 	paths, err := p.parsePathList()
 	if err != nil {
-		return stmt, err
+		return nil, err
 	}
 	if len(paths) == 0 {
 		tok, pos, lit := p.ScanIgnoreWhitespace()
-		return stmt, newParseError(scanner.Tokstr(tok, lit), []string{"("}, pos)
+		return nil, newParseError(scanner.Tokstr(tok, lit), []string{"("}, pos)
 	}
 
 	stmt.Info.Paths = paths
 
-	return stmt, nil
+	return &stmt, nil
 }
diff --git a/internal/sql/parser/delete.go b/internal/sql/parser/delete.go
index 7b362628..1e8d8bc0 100644
--- a/internal/sql/parser/delete.go
+++ b/internal/sql/parser/delete.go
@@ -7,7 +7,7 @@ import (
 
 // parseDeleteStatement parses a delete string and returns a Statement AST object.
 // This function assumes the DELETE token has already been consumed.
-func (p *Parser) parseDeleteStatement() (*statement.DeleteStmt, error) {
+func (p *Parser) parseDeleteStatement() (*statement.StreamStmt, error) {
 	var stmt statement.DeleteStmt
 	var err error
 
@@ -48,5 +48,5 @@ func (p *Parser) parseDeleteStatement() (*statement.DeleteStmt, error) {
 		return nil, err
 	}
 
-	return &stmt, nil
+	return stmt.ToStream()
 }
diff --git a/internal/sql/parser/insert.go b/internal/sql/parser/insert.go
index cfb6170d..5f0a65e0 100644
--- a/internal/sql/parser/insert.go
+++ b/internal/sql/parser/insert.go
@@ -10,7 +10,7 @@ import (
 
 // parseInsertStatement parses an insert string and returns a Statement AST object.
 // This function assumes the INSERT token has already been consumed.
-func (p *Parser) parseInsertStatement() (*statement.InsertStmt, error) {
+func (p *Parser) parseInsertStatement() (*statement.StreamStmt, error) {
 	var stmt statement.InsertStmt
 	var err error
 
@@ -62,7 +62,7 @@ func (p *Parser) parseInsertStatement() (*statement.InsertStmt, error) {
 		return nil, err
 	}
 
-	return &stmt, nil
+	return stmt.ToStream()
 }
 
 // parseFieldList parses a list of fields in the form: (path, path, ...), if exists.
diff --git a/internal/sql/parser/select.go b/internal/sql/parser/select.go
index 76688633..120f76fe 100644
--- a/internal/sql/parser/select.go
+++ b/internal/sql/parser/select.go
@@ -8,7 +8,7 @@ import (
 
 // parseSelectStatement parses a select string and returns a Statement AST object.
 // This function assumes the SELECT token has already been consumed.
-func (p *Parser) parseSelectStatement() (*statement.SelectStmt, error) {
+func (p *Parser) parseSelectStatement() (*statement.StreamStmt, error) {
 	var stmt statement.SelectStmt
 	var err error
 
@@ -30,7 +30,7 @@ func (p *Parser) parseSelectStatement() (*statement.SelectStmt, error) {
 		return nil, err
 	}
 	if !found {
-		return &stmt, nil
+		return stmt.ToStream()
 	}
 
 	// Parse condition: "WHERE expr".
@@ -63,7 +63,7 @@ func (p *Parser) parseSelectStatement() (*statement.SelectStmt, error) {
 		return nil, err
 	}
 
-	return &stmt, nil
+	return stmt.ToStream()
 }
 
 // parseProjectedExprs parses the list of projected fields.
diff --git a/internal/sql/parser/update.go b/internal/sql/parser/update.go
index dfc42b1b..33edaae9 100644
--- a/internal/sql/parser/update.go
+++ b/internal/sql/parser/update.go
@@ -7,7 +7,7 @@ import (
 
 // parseUpdateStatement parses a update string and returns a Statement AST object.
 // This function assumes the UPDATE token has already been consumed.
-func (p *Parser) parseUpdateStatement() (*statement.UpdateStmt, error) {
+func (p *Parser) parseUpdateStatement() (*statement.StreamStmt, error) {
 	var stmt statement.UpdateStmt
 	var err error
 
@@ -39,7 +39,7 @@ func (p *Parser) parseUpdateStatement() (*statement.UpdateStmt, error) {
 		return nil, err
 	}
 
-	return &stmt, nil
+	return stmt.ToStream(), nil
 }
 
 // parseSetClause parses the "SET" clause of the query.
diff --git a/internal/stream/operator.go b/internal/stream/operator.go
index fe73ca23..d7cff302 100644
--- a/internal/stream/operator.go
+++ b/internal/stream/operator.go
@@ -108,49 +108,6 @@ func (op *MapOperator) String() string {
 	return stringutil.Sprintf("map(%s)", op.E)
 }
 
-// IfEmpty is a conditional operator. If the source stream is empty, i.e. outputs no documents, it runs the sIf stream.
-// Otherwise, it pipes the incoming documents to sElse.
-type IfEmptyOperator struct {
-	baseOperator
-	sIf, sElse *Stream
-}
-
-// IfEmpty is a conditional operator. If the source stream is empty, i.e. outputs no documents, it runs the sIf stream.
-// Otherwise, it pipes the incoming documents to sElse.
-// If sElse is nil and the source stream is not empty, it iterates on the source stream.
-func IfEmpty(sIf, sElse *Stream) *IfEmptyOperator {
-	return &IfEmptyOperator{sIf: sIf, sElse: sElse}
-}
-
-// Iterate implements the Operator interface.
-// If sElse is nil and the source stream is not empty, it iterates on the source stream.
-func (op *IfEmptyOperator) Iterate(in *expr.Environment, f func(out *expr.Environment) error) error {
-	var called bool
-
-	s := New(op.Prev)
-
-	if op.sElse != nil {
-		s = op.sElse.Clone()
-
-		s.First().SetPrev(op.Prev)
-	}
-
-	err := s.Iterate(in, func(out *expr.Environment) error {
-		called = true
-
-		return f(out)
-	})
-	if err != nil || called {
-		return err
-	}
-
-	return op.sIf.Iterate(in, f)
-}
-
-func (op *IfEmptyOperator) String() string {
-	return stringutil.Sprintf("ifEmpty(\n\t%s,\n\t%s\n)", op.sIf, op.sElse)
-}
-
 // A FilterOperator filters values based on a given expression.
 type FilterOperator struct {
 	baseOperator
diff --git a/internal/stream/range.go b/internal/stream/range.go
index dcb8328a..3be5274c 100644
--- a/internal/stream/range.go
+++ b/internal/stream/range.go
@@ -2,9 +2,11 @@ package stream
 
 import (
 	"bytes"
+	"math"
 	"strings"
 
 	"github.com/genjidb/genji/document"
+	"github.com/genjidb/genji/internal/database"
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/stringutil"
 )
@@ -14,7 +16,7 @@ type Costable interface {
 }
 
 type ValueRange struct {
-	Min, Max document.Value
+	Min, Max expr.Expr
 	// Exclude Min and Max from the results.
 	// By default, min and max are inclusive.
 	// Exclusive and Exact cannot be set to true at the same time.
@@ -23,47 +25,89 @@ type ValueRange struct {
 	// If set to true, Max will be ignored for comparison
 	// and for determining the global upper bound.
 	Exact bool
-
-	encodedMin, encodedMax []byte
-	rangeType              document.ValueType
 }
 
-func (r *ValueRange) encode(encoder ValueEncoder, env *expr.Environment) error {
+func (r *ValueRange) evalRange(table *database.Table, env *expr.Environment) (*encodedValueRange, bool, error) {
 	var err error
 
-	// first we evaluate Min and Max
-	if !r.Min.Type.IsAny() {
-		r.encodedMin, err = encoder.EncodeValue(r.Min)
+	pk := table.Info.GetPrimaryKey()
+
+	rng := encodedValueRange{
+		pkType:      pk.Type,
+		path:        pk.Path,
+		constraints: table.Info.FieldConstraints,
+
+		Exclusive: r.Exclusive,
+		Exact:     r.Exact,
+	}
+
+	if r.Min != nil {
+		rng.Min, err = r.Min.Eval(env)
 		if err != nil {
-			return err
+			return nil, false, err
+		}
+
+		var ok bool
+		rng.Min, ok, err = rng.Convert(rng.Min, true)
+		if err != nil || !ok {
+			return nil, ok, err
 		}
-		r.rangeType = r.Min.Type
 	}
-	if !r.Max.Type.IsAny() {
-		r.encodedMax, err = encoder.EncodeValue(r.Max)
+
+	if r.Max != nil {
+		rng.Max, err = r.Max.Eval(env)
 		if err != nil {
-			return err
+			return nil, false, err
 		}
-		if !r.rangeType.IsAny() && r.rangeType != r.Max.Type {
+
+		var ok bool
+		rng.Max, ok, err = rng.Convert(rng.Max, false)
+		if err != nil || !ok {
+			return nil, ok, err
+		}
+	}
+
+	return &rng, true, nil
+}
+
+func (r *ValueRange) encode(table *database.Table, env *expr.Environment) (*encodedValueRange, error) {
+	rng, ok, err := r.evalRange(table, env)
+	if err != nil || !ok {
+		return nil, err
+	}
+
+	if !rng.Min.Type.IsAny() {
+		rng.EncodedMin, err = table.EncodeValue(rng.Min)
+		if err != nil {
+			return nil, err
+		}
+		rng.RangeType = rng.Min.Type
+	}
+	if !rng.Max.Type.IsAny() {
+		rng.EncodedMax, err = table.EncodeValue(rng.Max)
+		if err != nil {
+			return nil, err
+		}
+		if !rng.RangeType.IsAny() && rng.RangeType != rng.Max.Type {
 			panic("range contain values of different types")
 		}
 
-		r.rangeType = r.Max.Type
+		rng.RangeType = rng.Max.Type
 	}
 
 	// ensure boundaries are typed
-	if r.Min.Type.IsAny() {
-		r.Min.Type = r.rangeType
+	if rng.Min.Type.IsAny() {
+		rng.Min.Type = rng.RangeType
 	}
-	if r.Max.Type.IsAny() {
-		r.Max.Type = r.rangeType
+	if rng.Max.Type.IsAny() {
+		rng.Max.Type = rng.RangeType
 	}
 
 	if r.Exclusive && r.Exact {
 		panic("exclusive and exact cannot both be true")
 	}
 
-	return nil
+	return rng, nil
 }
 
 func (r *ValueRange) String() string {
@@ -71,18 +115,20 @@ func (r *ValueRange) String() string {
 		return stringutil.Sprintf("%v", r.Min)
 	}
 
-	if r.Min.Type.IsAny() {
-		r.Min = document.NewIntegerValue(-1)
+	var min, max string = "-1", "-1"
+
+	if r.Min != nil {
+		min = r.Min.String()
 	}
-	if r.Max.Type.IsAny() {
-		r.Max = document.NewIntegerValue(-1)
+	if r.Max != nil {
+		max = r.Max.String()
 	}
 
 	if r.Exclusive {
-		return stringutil.Sprintf("[%v, %v, true]", r.Min, r.Max)
+		return stringutil.Sprintf("[%v, %v, true]", min, max)
 	}
 
-	return stringutil.Sprintf("[%v, %v]", r.Min, r.Max)
+	return stringutil.Sprintf("[%v, %v]", min, max)
 }
 
 func (r *ValueRange) IsEqual(other *ValueRange) bool {
@@ -90,31 +136,129 @@ func (r *ValueRange) IsEqual(other *ValueRange) bool {
 		return false
 	}
 
-	if r.rangeType != other.rangeType {
+	if r.Exclusive != other.Exclusive {
 		return false
 	}
 
-	if r.Exclusive != other.Exclusive {
+	if expr.Equal(r.Min, other.Min) {
 		return false
 	}
 
-	if r.Min.Type != other.Min.Type {
+	if expr.Equal(r.Max, other.Max) {
 		return false
 	}
-	ok, err := r.Min.IsEqual(other.Min)
-	if err != nil || !ok {
-		return false
+
+	return true
+}
+
+func (r ValueRange) Clone() ValueRange {
+	return r
+}
+
+type encodedValueRange struct {
+	pkType      document.ValueType
+	path        document.Path
+	constraints database.FieldConstraints
+
+	Min, Max  document.Value
+	Exclusive bool
+	Exact     bool
+
+	EncodedMin, EncodedMax []byte
+	RangeType              document.ValueType
+}
+
+func (r *encodedValueRange) Convert(v document.Value, isMin bool) (document.Value, bool, error) {
+	// ensure the operand satisfies all the constraints, index can work only on exact types.
+	// if a number is encountered, try to convert it to the right type if and only if the conversion
+	// is lossless.
+	v, err := r.constraints.ConvertValueAtPath(r.path, v, func(v document.Value, path document.Path, targetType document.ValueType) (document.Value, error) {
+		if v.Type == document.IntegerValue && targetType == document.DoubleValue {
+			return v.CastAsDouble()
+		}
+
+		if v.Type == document.DoubleValue && targetType == document.IntegerValue {
+			f := v.V.(float64)
+			if float64(int64(f)) == f {
+				return v.CastAsInteger()
+			}
+
+			if r.Exact {
+				return v, nil
+			}
+
+			// we want to convert a non rounded double to int in a way that preserves
+			// comparison logic with the index. ex:
+			// a > 1.1  -> a >= 2; exclusive -> false
+			// a >= 1.1 -> a >= 2; exclusive -> false
+			// a < 1.1  -> a < 2;  exclusive -> true
+			// a <= 1.1 -> a < 2;  exclusive -> true
+			// a BETWEEN 1.1 AND 2.2 -> a >= 2 AND a <= 3; exclusive -> false
+
+			// First, we need to ceil the number. Ex: 1.1 -> 2
+			v = document.NewIntegerValue(int64(math.Ceil(f)))
+
+			// Next, we need to convert the boundaries
+			if isMin {
+				// (a > 1.1) or (a >= 1.1) must be transformed to (a >= 2)
+				r.Exclusive = false
+			} else {
+				// (a < 1.1) or (a <= 1.1) must be transformed to (a < 2)
+				// But there is an exception: if we are dealing with both min
+				// and max boundaries, we are operating a BETWEEN operation,
+				// meaning that we need to convert a BETWEEN 1.1 AND 2.2 to a >= 2 AND a <= 3,
+				// and thus have to set exclusive to false.
+				r.Exclusive = r.Min.Type.IsAny()
+			}
+		}
+
+		return v, nil
+	})
+	if err != nil {
+		return v, false, err
+	}
+
+	// if the index is not typed, any operand can work
+	if r.pkType.IsAny() {
+		return v, true, nil
+	}
+
+	// if the index is typed, it must be of the same type as the converted value
+	return v, r.pkType == v.Type, nil
+}
+
+func (r *encodedValueRange) IsInRange(value []byte) bool {
+	// by default, we consider the value within range
+	cmpMin, cmpMax := 1, -1
+
+	// we compare with the lower bound and see if it matches
+	if r.EncodedMin != nil {
+		cmpMin = bytes.Compare(value, r.EncodedMin)
 	}
 
-	if r.Max.Type != other.Max.Type {
+	// if exact is true the value has to be equal to the lower bound.
+	if r.Exact {
+		return cmpMin == 0
+	}
+
+	// if exclusive and the value is equal to the lower bound
+	// we can ignore it
+	if r.Exclusive && cmpMin == 0 {
 		return false
 	}
-	ok, err = r.Max.IsEqual(other.Max)
-	if err != nil || !ok {
+
+	// the value is bigger than the lower bound,
+	// see if it matches the upper bound.
+	if r.EncodedMax != nil {
+		cmpMax = bytes.Compare(value, r.EncodedMax)
+	}
+
+	// if boundaries are strict, ignore values equal to the max
+	if r.Exclusive && cmpMax == 0 {
 		return false
 	}
 
-	return true
+	return cmpMax <= 0
 }
 
 type ValueRanges []ValueRange
@@ -138,20 +282,21 @@ func (r ValueRanges) Append(rng ValueRange) ValueRanges {
 	return append(r, rng)
 }
 
-type ValueEncoder interface {
-	EncodeValue(v document.Value) ([]byte, error)
-}
-
 // Encode each range using the given value encoder.
-func (r ValueRanges) Encode(encoder ValueEncoder, env *expr.Environment) error {
+func (r ValueRanges) Encode(table *database.Table, env *expr.Environment) ([]*encodedValueRange, error) {
+	ranges := make([]*encodedValueRange, 0, len(r))
+
 	for i := range r {
-		err := r[i].encode(encoder, env)
+		rng, err := r[i].encode(table, env)
 		if err != nil {
-			return err
+			return nil, err
+		}
+		if rng != nil {
+			ranges = append(ranges, rng)
 		}
 	}
 
-	return nil
+	return ranges, nil
 }
 
 func (r ValueRanges) String() string {
@@ -182,12 +327,12 @@ func (r ValueRanges) Cost() int {
 		}
 
 		// if there are two boundaries, increment by 50
-		if !rng.Min.Type.IsAny() && !rng.Max.Type.IsAny() {
+		if rng.Min != nil && rng.Max != nil {
 			cost += 50
 		}
 
 		// if there is only one boundary, increment by 100
-		if (!rng.Min.Type.IsAny() && rng.Max.Type.IsAny()) || (rng.Min.Type.IsAny() && !rng.Max.Type.IsAny()) {
+		if (rng.Min != nil && rng.Max == nil) || (rng.Min == nil && rng.Max != nil) {
 			cost += 100
 			continue
 		}
@@ -199,46 +344,12 @@ func (r ValueRanges) Cost() int {
 	return cost
 }
 
-func (r *ValueRange) IsInRange(value []byte) bool {
-	// by default, we consider the value within range
-	cmpMin, cmpMax := 1, -1
-
-	// we compare with the lower bound and see if it matches
-	if r.encodedMin != nil {
-		cmpMin = bytes.Compare(value, r.encodedMin)
-	}
-
-	// if exact is true the value has to be equal to the lower bound.
-	if r.Exact {
-		return cmpMin == 0
-	}
-
-	// if exclusive and the value is equal to the lower bound
-	// we can ignore it
-	if r.Exclusive && cmpMin == 0 {
-		return false
-	}
-
-	// the value is bigger than the lower bound,
-	// see if it matches the upper bound.
-	if r.encodedMax != nil {
-		cmpMax = bytes.Compare(value, r.encodedMax)
-	}
-
-	// if boundaries are strict, ignore values equal to the max
-	if r.Exclusive && cmpMax == 0 {
-		return false
-	}
-
-	return cmpMax <= 0
-}
-
 // IndexRange represents a range to select indexed values after or before
 // a given boundary. Because indexes can be composites, IndexRange boundaries
 // are composite as well.
 type IndexRange struct {
-	Min, Max *document.ValueBuffer
-
+	Min, Max expr.LiteralExprList
+	Paths    []document.Path
 	// Exclude Min and Max from the results.
 	// By default, min and max are inclusive.
 	// Exclusive and Exact cannot be set to true at the same time.
@@ -251,86 +362,124 @@ type IndexRange struct {
 	// IndexArity is the underlying index arity, which can be greater
 	// than the boundaries of this range.
 	IndexArity int
+}
+
+func (r *IndexRange) evalRange(index *database.Index, table *database.Table, env *expr.Environment) (*encodedIndexRange, bool, error) {
+	rng := encodedIndexRange{
+		constraints: table.Info.FieldConstraints,
+
+		Exclusive:  r.Exclusive,
+		Exact:      r.Exact,
+		IndexArity: r.IndexArity,
+	}
+
+	if r.Min != nil {
+		lv, err := r.Min.Eval(env)
+		if err != nil {
+			return nil, false, err
+		}
+		rng.Min = lv.V.(*document.ValueBuffer)
+
+		var ok bool
+		for i := range rng.Min.Values {
+			rng.Min.Values[i], ok, err = rng.Convert(rng.Min.Values[i], index.Info.Paths[i], index.Info.Types[i], true)
+			if err != nil || !ok {
+				return nil, ok, err
+			}
+		}
+	}
+
+	if r.Max != nil {
+		lv, err := r.Max.Eval(env)
+		if err != nil {
+			return nil, false, err
+		}
+		rng.Max = lv.V.(*document.ValueBuffer)
+
+		var ok bool
+		for i := range rng.Max.Values {
+			rng.Max.Values[i], ok, err = rng.Convert(rng.Max.Values[i], index.Info.Paths[i], index.Info.Types[i], false)
+			if err != nil || !ok {
+				return nil, ok, err
+			}
+		}
+	}
 
-	encodedMin, encodedMax []byte
-	rangeTypes             []document.ValueType
+	return &rng, true, nil
 }
 
-func (r *IndexRange) encode(encoder ValueBufferEncoder, env *expr.Environment) error {
-	var err error
+func (r *IndexRange) encode(index *database.Index, table *database.Table, env *expr.Environment) (*encodedIndexRange, error) {
+	rng, ok, err := r.evalRange(index, table, env)
+	if err != nil || !ok {
+		return nil, err
+	}
 
-	// first we evaluate Min and Max
-	if r.Min.Len() > 0 {
-		r.encodedMin, err = encoder.EncodeValueBuffer(r.Min)
+	if len(r.Min) > 0 {
+		rng.EncodedMin, err = index.EncodeValueBuffer(rng.Min)
 		if err != nil {
-			return err
+			return nil, err
 		}
-		r.rangeTypes = r.Min.Types()
+		rng.RangeTypes = rng.Min.Types()
 	}
 
-	if r.Max.Len() > 0 {
-		r.encodedMax, err = encoder.EncodeValueBuffer(r.Max)
+	if len(r.Max) > 0 {
+		rng.EncodedMax, err = index.EncodeValueBuffer(rng.Max)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
-		if len(r.rangeTypes) > 0 {
-			maxTypes := r.Max.Types()
+		if len(rng.RangeTypes) > 0 {
+			maxTypes := rng.Max.Types()
 
-			if len(maxTypes) != len(r.rangeTypes) {
+			if len(maxTypes) != len(rng.RangeTypes) {
 				panic("range types for max and min differ in size")
 			}
 
 			for i, typ := range maxTypes {
-				if typ != r.rangeTypes[i] {
+				if typ != rng.RangeTypes[i] {
 					panic("range contain values of different types")
 				}
 			}
 		}
 
-		r.rangeTypes = r.Max.Types()
+		rng.RangeTypes = rng.Max.Types()
 	}
 
 	// Ensure boundaries are typed, at least with the first type
-	if r.Max.Len() == 0 && r.Min.Len() > 0 {
-		v, err := r.Min.GetByIndex(0)
+	if len(r.Max) == 0 && len(r.Min) > 0 {
+		v, err := rng.Min.GetByIndex(0)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
-		r.Max = document.NewValueBuffer(document.Value{Type: v.Type})
+		rng.Max = document.NewValueBuffer(document.Value{Type: v.Type})
 	}
 
-	if r.Min.Len() == 0 && r.Max.Len() > 0 {
-		v, err := r.Max.GetByIndex(0)
+	if len(r.Min) == 0 && len(r.Max) > 0 {
+		v, err := rng.Max.GetByIndex(0)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
-		r.Min = document.NewValueBuffer(document.Value{Type: v.Type})
+		rng.Min = document.NewValueBuffer(document.Value{Type: v.Type})
 	}
 
 	if r.Exclusive && r.Exact {
 		panic("exclusive and exact cannot both be true")
 	}
 
-	return nil
+	return rng, nil
 }
 
 func (r *IndexRange) String() string {
-	format := func(vb *document.ValueBuffer) string {
-		switch vb.Len() {
+	format := func(el expr.LiteralExprList) string {
+		switch len(el) {
 		case 0:
 			return "-1"
 		case 1:
-			return vb.Values[0].String()
+			return el[0].String()
 		default:
-			b, err := vb.MarshalJSON()
-			if err != nil {
-				return "err"
-			}
-
-			return string(b)
+			return el.String()
 		}
 	}
 
@@ -350,21 +499,15 @@ func (r *IndexRange) IsEqual(other *IndexRange) bool {
 		return false
 	}
 
-	for i, typ := range r.rangeTypes {
-		if typ != other.rangeTypes[i] {
-			return false
-		}
-	}
-
 	if r.Exclusive != other.Exclusive {
 		return false
 	}
 
-	if r.Min.Len() != other.Min.Len() {
+	if len(r.Min) != len(other.Min) {
 		return false
 	}
 
-	if r.Max.Len() != other.Max.Len() {
+	if len(r.Max) != len(other.Max) {
 		return false
 	}
 
@@ -379,6 +522,120 @@ func (r *IndexRange) IsEqual(other *IndexRange) bool {
 	return true
 }
 
+func (r IndexRange) Clone() IndexRange {
+	return r
+}
+
+type encodedIndexRange struct {
+	constraints database.FieldConstraints
+
+	Min, Max *document.ValueBuffer
+
+	Exclusive  bool
+	Exact      bool
+	IndexArity int
+
+	EncodedMin, EncodedMax []byte
+	RangeTypes             []document.ValueType
+}
+
+func (r *encodedIndexRange) Convert(v document.Value, p document.Path, t document.ValueType, isMin bool) (document.Value, bool, error) {
+	// ensure the operand satisfies all the constraints, index can work only on exact types.
+	// if a number is encountered, try to convert it to the right type if and only if the conversion
+	// is lossless.
+	v, err := r.constraints.ConvertValueAtPath(p, v, func(v document.Value, path document.Path, targetType document.ValueType) (document.Value, error) {
+		if v.Type == document.IntegerValue && targetType == document.DoubleValue {
+			return v.CastAsDouble()
+		}
+
+		if v.Type == document.DoubleValue && targetType == document.IntegerValue {
+			f := v.V.(float64)
+			if float64(int64(f)) == f {
+				return v.CastAsInteger()
+			}
+
+			if r.Exact {
+				return v, nil
+			}
+
+			// we want to convert a non rounded double to int in a way that preserves
+			// comparison logic with the index. ex:
+			// a > 1.1  -> a >= 2; exclusive -> false
+			// a >= 1.1 -> a >= 2; exclusive -> false
+			// a < 1.1  -> a < 2;  exclusive -> true
+			// a <= 1.1 -> a < 2;  exclusive -> true
+			// a BETWEEN 1.1 AND 2.2 -> a >= 2 AND a <= 3; exclusive -> false
+
+			// First, we need to ceil the number. Ex: 1.1 -> 2
+			v = document.NewIntegerValue(int64(math.Ceil(f)))
+
+			// Next, we need to convert the boundaries
+			if isMin {
+				// (a > 1.1) or (a >= 1.1) must be transformed to (a >= 2)
+				r.Exclusive = false
+			} else {
+				// (a < 1.1) or (a <= 1.1) must be transformed to (a < 2)
+				// But there is an exception: if we are dealing with both min
+				// and max boundaries, we are operating a BETWEEN operation,
+				// meaning that we need to convert a BETWEEN 1.1 AND 2.2 to a >= 2 AND a <= 3,
+				// and thus have to set exclusive to false.
+				r.Exclusive = r.Min == nil || r.Min.Len() == 0
+			}
+		}
+
+		return v, nil
+	})
+	if err != nil {
+		return v, false, err
+	}
+
+	// if the index is not typed, any operand can work
+	if t.IsAny() {
+		return v, true, nil
+	}
+
+	// if the index is typed, it must be of the same type as the converted value
+	return v, t == v.Type, nil
+}
+
+func (r *encodedIndexRange) IsInRange(value []byte) bool {
+	// by default, we consider the value within range
+	cmpMin, cmpMax := 1, -1
+
+	// we compare with the lower bound and see if it matches
+	if r.EncodedMin != nil {
+		cmpMin = bytes.Compare(value, r.EncodedMin)
+	}
+
+	// if exact is true the value has to be equal to the lower bound.
+	if r.Exact {
+		return cmpMin == 0
+	}
+
+	// if exclusive and the value is equal to the lower bound
+	// we can ignore it
+	if r.Exclusive && cmpMin == 0 {
+		return false
+	}
+
+	// the value is bigger than the lower bound,
+	// see if it matches the upper bound.
+	if r.EncodedMax != nil {
+		if r.Max.Len() < r.IndexArity {
+			cmpMax = bytes.Compare(value[:len(r.EncodedMax)], r.EncodedMax)
+		} else {
+			cmpMax = bytes.Compare(value, r.EncodedMax)
+		}
+	}
+
+	// if boundaries are strict, ignore values equal to the max
+	if r.Exclusive && cmpMax == 0 {
+		return false
+	}
+
+	return cmpMin >= 0 && cmpMax <= 0
+}
+
 type IndexRanges []IndexRange
 
 // Append rng to r and return the new slice.
@@ -405,15 +662,20 @@ type ValueBufferEncoder interface {
 }
 
 // Encode each range using the given value encoder.
-func (r IndexRanges) EncodeBuffer(encoder ValueBufferEncoder, env *expr.Environment) error {
+func (r IndexRanges) EncodeBuffer(index *database.Index, table *database.Table, env *expr.Environment) ([]*encodedIndexRange, error) {
+	ranges := make([]*encodedIndexRange, 0, len(r))
+
 	for i := range r {
-		err := r[i].encode(encoder, env)
+		enc, err := r[i].encode(index, table, env)
 		if err != nil {
-			return err
+			return nil, err
+		}
+		if enc != nil {
+			ranges = append(ranges, enc)
 		}
 	}
 
-	return nil
+	return ranges, nil
 }
 
 func (r IndexRanges) String() string {
@@ -444,13 +706,13 @@ func (r IndexRanges) Cost() int {
 		}
 
 		// if there are two boundaries, increment by 50
-		if rng.Min.Len() > 0 && rng.Max.Len() > 0 {
+		if len(rng.Min) > 0 && len(rng.Max) > 0 {
 			cost += 50
 			continue
 		}
 
 		// if there is only one boundary, increment by 100
-		if rng.Min.Len() > 0 || rng.Max.Len() > 0 {
+		if len(rng.Min) > 0 || len(rng.Max) > 0 {
 			cost += 100
 			continue
 		}
@@ -461,41 +723,3 @@ func (r IndexRanges) Cost() int {
 
 	return cost
 }
-
-func (r *IndexRange) IsInRange(value []byte) bool {
-	// by default, we consider the value within range
-	cmpMin, cmpMax := 1, -1
-
-	// we compare with the lower bound and see if it matches
-	if r.encodedMin != nil {
-		cmpMin = bytes.Compare(value, r.encodedMin)
-	}
-
-	// if exact is true the value has to be equal to the lower bound.
-	if r.Exact {
-		return cmpMin == 0
-	}
-
-	// if exclusive and the value is equal to the lower bound
-	// we can ignore it
-	if r.Exclusive && cmpMin == 0 {
-		return false
-	}
-
-	// the value is bigger than the lower bound,
-	// see if it matches the upper bound.
-	if r.encodedMax != nil {
-		if r.Max.Len() < r.IndexArity {
-			cmpMax = bytes.Compare(value[:len(r.encodedMax)], r.encodedMax)
-		} else {
-			cmpMax = bytes.Compare(value, r.encodedMax)
-		}
-	}
-
-	// if boundaries are strict, ignore values equal to the max
-	if r.Exclusive && cmpMax == 0 {
-		return false
-	}
-
-	return cmpMin >= 0 && cmpMax <= 0
-}
diff --git a/internal/stream/scan.go b/internal/stream/scan.go
index 2a6e403c..a84e9f08 100644
--- a/internal/stream/scan.go
+++ b/internal/stream/scan.go
@@ -210,7 +210,7 @@ func (it *PkScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Enviro
 		return err
 	}
 
-	err = it.Ranges.Encode(table, in)
+	ranges, err := it.Ranges.Encode(table, in)
 	if err != nil {
 		return err
 	}
@@ -223,7 +223,7 @@ func (it *PkScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Enviro
 		iterator = table.DescendLessOrEqual
 	}
 
-	for _, rng := range it.Ranges {
+	for _, rng := range ranges {
 		var start, end document.Value
 		if !it.Reverse {
 			start = rng.Min
@@ -332,8 +332,8 @@ func (it *IndexScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Env
 		return err
 	}
 
-	err = it.Ranges.EncodeBuffer(index, in)
-	if err != nil {
+	ranges, err := it.Ranges.EncodeBuffer(index, table, in)
+	if err != nil || len(ranges) != len(it.Ranges) {
 		return err
 	}
 
@@ -346,7 +346,7 @@ func (it *IndexScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Env
 	}
 
 	// if there are no ranges use a simpler and faster iteration function
-	if len(it.Ranges) == 0 {
+	if len(ranges) == 0 {
 		return iterator(nil, func(val, key []byte) error {
 			d, err := table.GetDocument(key)
 			if err != nil {
@@ -358,7 +358,7 @@ func (it *IndexScanOperator) Iterate(in *expr.Environment, fn func(out *expr.Env
 		})
 	}
 
-	for _, rng := range it.Ranges {
+	for _, rng := range ranges {
 		var start, end *document.ValueBuffer
 		if !it.Reverse {
 			start = rng.Min
diff --git a/internal/stream/stream.go b/internal/stream/stream.go
index 35eae3d1..aac43e97 100644
--- a/internal/stream/stream.go
+++ b/internal/stream/stream.go
@@ -82,17 +82,6 @@ func (s *Stream) String() string {
 	return sb.String()
 }
 
-func (s *Stream) Clone() *Stream {
-	var clone *Stream
-
-	for n := s.First(); n != nil; n = n.GetNext() {
-		clone = clone.Pipe(n)
-
-	}
-
-	return clone
-}
-
 func InsertBefore(op, newOp Operator) Operator {
 	if op != nil {
 		prev := op.GetPrev()


Test patch:
diff --git a/db_test.go b/db_test.go
index 60da8133..b805d262 100644
--- a/db_test.go
+++ b/db_test.go
@@ -1,6 +1,7 @@
 package genji_test
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"testing"
@@ -9,6 +10,7 @@ import (
 	"github.com/genjidb/genji/document"
 	errs "github.com/genjidb/genji/errors"
 	"github.com/stretchr/testify/require"
+	"golang.org/x/sync/errgroup"
 )
 
 func ExampleTx() {
@@ -118,6 +120,38 @@ func TestQueryDocument(t *testing.T) {
 	})
 }
 
+func TestPrepareThreadSafe(t *testing.T) {
+	db, err := genji.Open(":memory:")
+	require.NoError(t, err)
+	defer db.Close()
+
+	err = db.Exec("CREATE TABLE test(a int unique, b text); INSERT INTO test(a, b) VALUES (1, 'a'), (2, 'a')")
+	require.NoError(t, err)
+
+	stmt, err := db.Prepare("SELECT COUNT(a) FROM test WHERE a < ? GROUP BY b ORDER BY a DESC LIMIT 5")
+	require.NoError(t, err)
+
+	g, _ := errgroup.WithContext(context.Background())
+
+	for i := 1; i <= 3; i++ {
+		arg := i
+		g.Go(func() error {
+			res, err := stmt.Query(arg)
+			if err != nil {
+				return err
+			}
+			defer res.Close()
+
+			return res.Iterate(func(d document.Document) error {
+				return nil
+			})
+		})
+	}
+
+	err = g.Wait()
+	require.NoError(t, err)
+}
+
 func BenchmarkSelect(b *testing.B) {
 	for size := 1; size <= 10000; size *= 10 {
 		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
@@ -164,6 +198,30 @@ func BenchmarkSelectWhere(b *testing.B) {
 	}
 }
 
+func BenchmarkPreparedSelectWhere(b *testing.B) {
+	for size := 1; size <= 10000; size *= 10 {
+		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
+			db, err := genji.Open(":memory:")
+			require.NoError(b, err)
+
+			err = db.Exec("CREATE TABLE foo")
+			require.NoError(b, err)
+
+			for i := 0; i < size; i++ {
+				err = db.Exec("INSERT INTO foo(a, b) VALUES (1, 2);")
+				require.NoError(b, err)
+			}
+
+			p, _ := db.Prepare("SELECT b FROM foo WHERE a > 0")
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				res, _ := p.Query()
+				res.Iterate(func(d document.Document) error { return nil })
+			}
+		})
+	}
+}
+
 func BenchmarkSelectPk(b *testing.B) {
 	for size := 1; size <= 10000; size *= 10 {
 		b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
diff --git a/internal/planner/optimizer_test.go b/internal/planner/optimizer_test.go
index a2c23359..3dc46203 100644
--- a/internal/planner/optimizer_test.go
+++ b/internal/planner/optimizer_test.go
@@ -76,7 +76,7 @@ func TestSplitANDConditionRule(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			res, err := planner.SplitANDConditionRule(test.in, nil, nil)
+			res, err := planner.SplitANDConditionRule(test.in, nil)
 			require.NoError(t, err)
 			require.Equal(t, res.String(), test.expected.String())
 		})
@@ -87,31 +87,26 @@ func TestPrecalculateExprRule(t *testing.T) {
 	tests := []struct {
 		name        string
 		e, expected expr.Expr
-		params      []expr.Param
 	}{
 		{
 			"constant expr: 3 -> 3",
 			testutil.IntegerValue(3),
 			testutil.IntegerValue(3),
-			nil,
 		},
 		{
 			"operator with two constant operands: 3 + 2.4 -> 5.4",
-			expr.Add(testutil.IntegerValue(3), expr.PositionalParam(1)),
+			expr.Add(testutil.IntegerValue(3), testutil.DoubleValue(2.4)),
 			testutil.DoubleValue(5.4),
-			[]expr.Param{{Value: 2.4}},
 		},
 		{
 			"operator with constant nested operands: 3 > 1 - 40 -> true",
 			expr.Gt(testutil.DoubleValue(3), expr.Sub(testutil.IntegerValue(1), testutil.DoubleValue(40))),
 			testutil.BoolValue(true),
-			nil,
 		},
 		{
 			"constant sub-expr: a > 1 - 40 -> a > -39",
 			expr.Gt(expr.Path{document.PathFragment{FieldName: "a"}}, expr.Sub(testutil.IntegerValue(1), testutil.DoubleValue(40))),
 			expr.Gt(expr.Path{document.PathFragment{FieldName: "a"}}, testutil.DoubleValue(-39)),
-			nil,
 		},
 		{
 			"constant sub-expr: a IN [1, 2] -> a IN array([1, 2])",
@@ -119,7 +114,6 @@ func TestPrecalculateExprRule(t *testing.T) {
 			expr.In(expr.Path{document.PathFragment{FieldName: "a"}}, expr.LiteralValue(document.NewArrayValue(document.NewValueBuffer().
 				Append(document.NewIntegerValue(1)).
 				Append(document.NewIntegerValue(2))))),
-			nil,
 		},
 		{
 			"non-constant expr list: [a, 1 - 40] -> [a, -39]",
@@ -131,7 +125,6 @@ func TestPrecalculateExprRule(t *testing.T) {
 				expr.Path{document.PathFragment{FieldName: "a"}},
 				testutil.DoubleValue(-39),
 			},
-			nil,
 		},
 		{
 			"constant expr list: [3, 1 - 40] -> array([3, -39])",
@@ -142,7 +135,6 @@ func TestPrecalculateExprRule(t *testing.T) {
 			expr.LiteralValue(document.NewArrayValue(document.NewValueBuffer().
 				Append(document.NewIntegerValue(3)).
 				Append(document.NewDoubleValue(-39)))),
-			nil,
 		},
 		{
 			`non-constant kvpair: {"a": d, "b": 1 - 40} -> {"a": 3, "b": -39}`,
@@ -154,7 +146,6 @@ func TestPrecalculateExprRule(t *testing.T) {
 				{K: "a", V: expr.Path{document.PathFragment{FieldName: "d"}}},
 				{K: "b", V: testutil.DoubleValue(-39)},
 			}},
-			nil,
 		},
 		{
 			`constant kvpair: {"a": 3, "b": 1 - 40} -> document({"a": 3, "b": -39})`,
@@ -166,7 +157,6 @@ func TestPrecalculateExprRule(t *testing.T) {
 				Add("a", document.NewIntegerValue(3)).
 				Add("b", document.NewDoubleValue(-39)),
 			)),
-			nil,
 		},
 	}
 
@@ -174,7 +164,7 @@ func TestPrecalculateExprRule(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			s := st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(test.e))
-			res, err := planner.PrecalculateExprRule(s, nil, test.params)
+			res, err := planner.PrecalculateExprRule(s, nil)
 			require.NoError(t, err)
 			require.Equal(t, st.New(st.SeqScan("foo")).Pipe(st.Filter(test.expected)).String(), res.String())
 		})
@@ -213,7 +203,7 @@ func TestRemoveUnnecessarySelectionNodesRule(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			res, err := planner.RemoveUnnecessaryFilterNodesRule(test.root, nil, nil)
+			res, err := planner.RemoveUnnecessaryFilterNodesRule(test.root, nil)
 			require.NoError(t, err)
 			if test.expected != nil {
 				require.Equal(t, test.expected.String(), res.String())
@@ -286,15 +276,18 @@ func TestRemoveUnnecessaryDedupNodeRule(t *testing.T) {
 					(3, 3, 3)
 			`)
 
-			res, err := planner.RemoveUnnecessaryDistinctNodeRule(test.root, tx, nil)
+			res, err := planner.RemoveUnnecessaryDistinctNodeRule(test.root, tx)
 			require.NoError(t, err)
 			require.Equal(t, test.expected.String(), res.String())
 		})
 	}
 }
 
+func exprList(list ...expr.Expr) expr.LiteralExprList {
+	return expr.LiteralExprList(list)
+}
+
 func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
-	newVB := document.NewValueBuffer
 	tests := []struct {
 		name           string
 		root, expected *st.Stream
@@ -307,14 +300,14 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 		{
 			"FROM foo WHERE a = 1",
 			st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("a = 1"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})),
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: exprList(testutil.IntegerValue(1)), Exact: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND b = 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: exprList(testutil.IntegerValue(1)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
 		},
 		{
@@ -322,7 +315,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("c = 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
-			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: newVB(document.NewIntegerValue(3)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.IntegerValue(3)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
 		},
 		{
@@ -330,7 +323,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("c > 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
-			st.New(st.IndexScan("idx_foo_b", st.IndexRange{Min: newVB(document.NewIntegerValue(2)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_b", st.IndexRange{Min: exprList(testutil.IntegerValue(2)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("c > 3"))),
 		},
 		{
@@ -339,7 +332,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("c = 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
-			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: newVB(document.NewIntegerValue(3)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.IntegerValue(3)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
 		},
@@ -349,8 +342,8 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("c = 'hello'"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
-			st.New(st.IndexScan("idx_foo_b", st.IndexRange{Min: newVB(document.NewIntegerValue(2)), Exact: true})).
-				Pipe(st.Filter(parser.MustParseExpr("c = 'hello'"))).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.TextValue("hello")), Exact: true})).
+				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
 		},
 		{
@@ -359,8 +352,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("c = 'hello'"))).
 				Pipe(st.Filter(parser.MustParseExpr("d = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
-			st.New(st.SeqScan("foo")).
-				Pipe(st.Filter(parser.MustParseExpr("c = 'hello'"))).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.TextValue("hello")), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("d = 2"))).
 				Pipe(st.Project(parser.MustParseExpr("a"))),
 		},
@@ -369,10 +361,10 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).Pipe(st.Filter(
 				expr.In(
 					parser.MustParseExpr("a"),
-					testutil.ArrayValue(document.NewValueBuffer(document.NewIntegerValue(1), document.NewIntegerValue(2))),
+					testutil.ExprList(t, `[1, 2]`),
 				),
 			)),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true}, st.IndexRange{Min: newVB(document.NewIntegerValue(2)), Exact: true})),
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: exprList(testutil.IntegerValue(1)), Exact: true}, st.IndexRange{Min: exprList(testutil.IntegerValue(2)), Exact: true})),
 		},
 		{
 			"FROM foo WHERE 1 IN a",
@@ -382,19 +374,19 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 		{
 			"FROM foo WHERE a >= 10",
 			st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("a >= 10"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(10))})),
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: exprList(testutil.IntegerValue(10))})),
 		},
 		{
 			"FROM foo WHERE k = 1",
 			st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("k = 1"))),
-			st.New(st.PkScan("foo", st.ValueRange{Min: document.NewIntegerValue(1), Exact: true})),
+			st.New(st.PkScan("foo", st.ValueRange{Min: testutil.IntegerValue(1), Exact: true})),
 		},
 		{
 			"FROM foo WHERE k = 1 AND b = 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("k = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
-			st.New(st.PkScan("foo", st.ValueRange{Min: document.NewIntegerValue(1), Exact: true})).
+			st.New(st.PkScan("foo", st.ValueRange{Min: testutil.IntegerValue(1), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
 		},
 		{
@@ -402,7 +394,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("2 = k"))),
-			st.New(st.PkScan("foo", st.ValueRange{Min: document.NewIntegerValue(2), Exact: true})).
+			st.New(st.PkScan("foo", st.ValueRange{Min: testutil.IntegerValue(2), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))),
 		},
 		{
@@ -410,7 +402,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("k < 2"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: exprList(testutil.IntegerValue(1)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("k < 2"))),
 		},
 		{
@@ -418,13 +410,13 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("k = 'hello'"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})).
-				Pipe(st.Filter(parser.MustParseExpr("k = 'hello'"))),
+			st.New(st.PkScan("foo", st.ValueRange{Min: testutil.TextValue("hello"), Exact: true})).
+				Pipe(st.Filter(parser.MustParseExpr("a = 1"))),
 		},
 		{ // c is an INT, 1.1 cannot be converted to int without precision loss, don't use the index
 			"FROM foo WHERE c < 1.1",
 			st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("c < 1.1"))),
-			st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("c < 1.1"))),
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Max: exprList(testutil.DoubleValue(1.1)), Exclusive: true})),
 		},
 	}
 
@@ -444,7 +436,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 					(3, 3, 3, 3, 3)
 			`)
 
-			res, err := planner.UseIndexBasedOnFilterNodeRule(test.root, tx, nil)
+			res, err := planner.UseIndexBasedOnFilterNodeRule(test.root, tx)
 			require.NoError(t, err)
 			require.Equal(t, test.expected.String(), res.String())
 		})
@@ -463,22 +455,22 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 			{
 				"FROM foo WHERE k = [1, 1]",
 				st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("k = [1, 1]"))),
-				st.New(st.PkScan("foo", st.ValueRange{Min: document.NewArrayValue(testutil.MakeArray(t, `[1, 1]`)), Exact: true})),
+				st.New(st.PkScan("foo", st.ValueRange{Min: testutil.ExprList(t, `[1, 1]`), Exact: true})),
 			},
 			{ // constraint on k[0] INT should not modify the operand
 				"FROM foo WHERE k = [1.5, 1.5]",
 				st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("k = [1.5, 1.5]"))),
-				st.New(st.PkScan("foo", st.ValueRange{Min: document.NewArrayValue(testutil.MakeArray(t, `[1.5, 1.5]`)), Exact: true})),
+				st.New(st.PkScan("foo", st.ValueRange{Min: testutil.ExprList(t, `[1.5, 1.5]`), Exact: true})),
 			},
 			{
 				"FROM foo WHERE a = [1, 1]",
 				st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("a = [1, 1]"))),
-				st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewArrayValue(testutil.MakeArray(t, `[1, 1]`))), Exact: true})),
+				st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: testutil.ExprList(t, `[1, 1]`), Exact: true})),
 			},
 			{ // constraint on a[0] DOUBLE should modify the operand because it's lossless
 				"FROM foo WHERE a = [1, 1.5]",
 				st.New(st.SeqScan("foo")).Pipe(st.Filter(parser.MustParseExpr("a = [1, 1.5]"))),
-				st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewArrayValue(testutil.MakeArray(t, `[1.0, 1.5]`))), Exact: true})),
+				st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: testutil.ExprList(t, `[1.0, 1.5]`), Exact: true})),
 			},
 		}
 
@@ -502,10 +494,10 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 						([3, 3], [3, 3], [3, 3])
 				`)
 
-				res, err := planner.PrecalculateExprRule(test.root, tx, nil)
+				res, err := planner.PrecalculateExprRule(test.root, tx)
 				require.NoError(t, err)
 
-				res, err = planner.UseIndexBasedOnFilterNodeRule(res, tx, nil)
+				res, err = planner.UseIndexBasedOnFilterNodeRule(res, tx)
 				require.NoError(t, err)
 				require.Equal(t, test.expected.String(), res.String())
 			})
@@ -514,7 +506,6 @@ func TestUseIndexBasedOnSelectionNodeRule_Simple(t *testing.T) {
 }
 
 func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
-	newVB := document.NewValueBuffer
 	tests := []struct {
 		name           string
 		root, expected *st.Stream
@@ -524,72 +515,80 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d = 2"))),
-			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`), Exact: true})),
+			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`), Exact: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND d > 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d > 2"))),
-			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`), Exclusive: true})),
+			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`), Exclusive: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND d < 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d < 2"))),
-			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 2]`), Exclusive: true})),
+			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Max: testutil.ExprList(t, `[1, 2]`), Exclusive: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND d <= 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d <= 2"))),
-			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 2]`)})),
+			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Max: testutil.ExprList(t, `[1, 2]`)})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND d >= 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d >= 2"))),
-			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`)})),
+			st.New(st.IndexScan("idx_foo_a_d", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`)})),
 		},
 		{
 			"FROM foo WHERE a > 1 AND d > 2",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a > 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("d > 2"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1]`), Exclusive: true})).
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: testutil.ExprList(t, `[1]`), Exclusive: true})).
 				Pipe(st.Filter(parser.MustParseExpr("d > 2"))),
 		},
+		{
+			"FROM foo WHERE a > ? AND d > ?",
+			st.New(st.SeqScan("foo")).
+				Pipe(st.Filter(parser.MustParseExpr("a > ?"))).
+				Pipe(st.Filter(parser.MustParseExpr("d > ?"))),
+			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: testutil.ExprList(t, `[?]`), Exclusive: true})).
+				Pipe(st.Filter(parser.MustParseExpr("d > ?"))),
+		},
 		{
 			"FROM foo WHERE a = 1 AND b = 2 AND c = 3",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Filter(parser.MustParseExpr("c = 3"))),
-			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2, 3]`), Exact: true})),
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.ExprList(t, `[1, 2, 3]`), Exact: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND b = 2", // c is omitted, but it can still use idx_foo_a_b_c
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))),
-			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`), Exact: true})),
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`), Exact: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND b > 2", // c is omitted, but it can still use idx_foo_a_b_c, with > b
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b > 2"))),
-			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`), Exclusive: true})),
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`), Exclusive: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND b < 2", // c is omitted, but it can still use idx_foo_a_b_c, with > b
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b < 2"))),
-			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 2]`), Exclusive: true})),
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Max: testutil.ExprList(t, `[1, 2]`), Exclusive: true})),
 		},
 		{
 			"FROM foo WHERE a = 1 AND b = 2 and k = 3", // c is omitted, but it can still use idx_foo_a_b_c
@@ -597,7 +596,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Filter(parser.MustParseExpr("k = 3"))),
-			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 2]`), Exact: true})).
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: testutil.ExprList(t, `[1, 2]`), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("k = 3"))),
 		},
 		// If a path is missing from the query, we can still the index, with paths after the missing one are
@@ -607,7 +606,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("x = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("z = 2"))),
-			st.New(st.IndexScan("idx_foo_x_y_z", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_x_y_z", st.IndexRange{Min: exprList(testutil.IntegerValue(1)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("z = 2"))),
 		},
 		{
@@ -616,7 +615,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("c = 2"))),
 			// c will be picked because it's a unique index and thus has a lower cost
-			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: newVB(document.NewIntegerValue(2)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.IntegerValue(2)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))),
 		},
 		{
@@ -625,18 +624,16 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(parser.MustParseExpr("b = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("c = 2"))),
 			// c will be picked because it's a unique index and thus has a lower cost
-			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: newVB(document.NewIntegerValue(2)), Exact: true})).
+			st.New(st.IndexScan("idx_foo_c", st.IndexRange{Min: exprList(testutil.IntegerValue(2)), Exact: true})).
 				Pipe(st.Filter(parser.MustParseExpr("b = 1"))),
 		},
 		{
-			"FROM foo WHERE a = 1 AND b = 2 AND c = 'a'", // c is from the wrong type and will prevent the index to be picked
+			"FROM foo WHERE a = 1 AND b = 2 AND c = 'a'",
 			st.New(st.SeqScan("foo")).
 				Pipe(st.Filter(parser.MustParseExpr("a = 1"))).
 				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
 				Pipe(st.Filter(parser.MustParseExpr("c = 'a'"))),
-			st.New(st.IndexScan("idx_foo_a", st.IndexRange{Min: newVB(document.NewIntegerValue(1)), Exact: true})).
-				Pipe(st.Filter(parser.MustParseExpr("b = 2"))).
-				Pipe(st.Filter(parser.MustParseExpr("c = 'a'"))),
+			st.New(st.IndexScan("idx_foo_a_b_c", st.IndexRange{Min: exprList(testutil.IntegerValue(1), testutil.IntegerValue(2), testutil.TextValue("a")), Exact: true})),
 		},
 
 		{
@@ -645,13 +642,13 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(
 					expr.In(
 						parser.MustParseExpr("a"),
-						testutil.ArrayValue(document.NewValueBuffer(document.NewIntegerValue(1), document.NewIntegerValue(2))),
+						testutil.ExprList(t, `[1, 2]`),
 					),
 				)).
 				Pipe(st.Filter(parser.MustParseExpr("d = 4"))),
 			st.New(st.IndexScan("idx_foo_a_d",
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 4]`), Exact: true},
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[2, 4]`), Exact: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[1, 4]`), Exact: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[2, 4]`), Exact: true},
 			)),
 		},
 		{
@@ -660,14 +657,14 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(
 					expr.In(
 						parser.MustParseExpr("a"),
-						testutil.ArrayValue(document.NewValueBuffer(document.NewIntegerValue(1), document.NewIntegerValue(2))),
+						testutil.ExprList(t, `[1, 2]`),
 					),
 				)).
 				Pipe(st.Filter(parser.MustParseExpr("b = 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("c = 4"))),
 			st.New(st.IndexScan("idx_foo_a_b_c",
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 3, 4]`), Exact: true},
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[2, 3, 4]`), Exact: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[1, 3, 4]`), Exact: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[2, 3, 4]`), Exact: true},
 			)),
 		},
 		{
@@ -676,14 +673,14 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(
 					expr.In(
 						parser.MustParseExpr("a"),
-						testutil.ArrayValue(document.NewValueBuffer(document.NewIntegerValue(1), document.NewIntegerValue(2))),
+						testutil.ExprList(t, `[1, 2]`),
 					),
 				)).
 				Pipe(st.Filter(parser.MustParseExpr("b = 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("c > 4"))),
 			st.New(st.IndexScan("idx_foo_a_b_c",
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[1, 3, 4]`), Exclusive: true},
-				st.IndexRange{Min: testutil.MakeValueBuffer(t, `[2, 3, 4]`), Exclusive: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[1, 3, 4]`), Exclusive: true},
+				st.IndexRange{Min: testutil.ExprList(t, `[2, 3, 4]`), Exclusive: true},
 			)),
 		},
 		{
@@ -692,14 +689,14 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 				Pipe(st.Filter(
 					expr.In(
 						parser.MustParseExpr("a"),
-						testutil.ArrayValue(document.NewValueBuffer(document.NewIntegerValue(1), document.NewIntegerValue(2))),
+						testutil.ExprList(t, `[1, 2]`),
 					),
 				)).
 				Pipe(st.Filter(parser.MustParseExpr("b = 3"))).
 				Pipe(st.Filter(parser.MustParseExpr("c < 4"))),
 			st.New(st.IndexScan("idx_foo_a_b_c",
-				st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 3, 4]`), Exclusive: true},
-				st.IndexRange{Max: testutil.MakeValueBuffer(t, `[2, 3, 4]`), Exclusive: true},
+				st.IndexRange{Max: testutil.ExprList(t, `[1, 3, 4]`), Exclusive: true},
+				st.IndexRange{Max: testutil.ExprList(t, `[2, 3, 4]`), Exclusive: true},
 			)),
 		},
 		// {
@@ -719,10 +716,10 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 		// 		)).
 		// 		Pipe(st.Filter(parser.MustParseExpr("c < 5"))),
 		// 	st.New(st.IndexScan("idx_foo_a_b_c",
-		// 		st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 3, 5]`), Exclusive: true},
-		// 		st.IndexRange{Max: testutil.MakeValueBuffer(t, `[2, 3, 5]`), Exclusive: true},
-		// 		st.IndexRange{Max: testutil.MakeValueBuffer(t, `[1, 4, 5]`), Exclusive: true},
-		// 		st.IndexRange{Max: testutil.MakeValueBuffer(t, `[2, 4, 5]`), Exclusive: true},
+		// 		st.IndexRange{Max: testutil.ExprList(t, `[1, 3, 5]`), Exclusive: true},
+		// 		st.IndexRange{Max: testutil.ExprList(t, `[2, 3, 5]`), Exclusive: true},
+		// 		st.IndexRange{Max: testutil.ExprList(t, `[1, 4, 5]`), Exclusive: true},
+		// 		st.IndexRange{Max: testutil.ExprList(t, `[2, 4, 5]`), Exclusive: true},
 		// 	)),
 		// },
 		{
@@ -755,7 +752,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 					(3, 3, 3, 3, 3)
 			`)
 
-			res, err := planner.UseIndexBasedOnFilterNodeRule(test.root, tx, nil)
+			res, err := planner.UseIndexBasedOnFilterNodeRule(test.root, tx)
 			require.NoError(t, err)
 			require.Equal(t, test.expected.String(), res.String())
 		})
@@ -772,7 +769,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 					Pipe(st.Filter(parser.MustParseExpr("a = [1, 1]"))).
 					Pipe(st.Filter(parser.MustParseExpr("b = [2, 2]"))),
 				st.New(st.IndexScan("idx_foo_a_b", st.IndexRange{
-					Min:   testutil.MakeValueBuffer(t, `[[1, 1], [2, 2]]`),
+					Min:   testutil.ExprList(t, `[[1, 1], [2, 2]]`),
 					Exact: true})),
 			},
 			{
@@ -781,7 +778,7 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 					Pipe(st.Filter(parser.MustParseExpr("a = [1, 1]"))).
 					Pipe(st.Filter(parser.MustParseExpr("b > [2, 2]"))),
 				st.New(st.IndexScan("idx_foo_a_b", st.IndexRange{
-					Min:       testutil.MakeValueBuffer(t, `[[1, 1], [2, 2]]`),
+					Min:       testutil.ExprList(t, `[[1, 1], [2, 2]]`),
 					Exclusive: true})),
 			},
 		}
@@ -804,10 +801,10 @@ func TestUseIndexBasedOnSelectionNodeRule_Composite(t *testing.T) {
 							([3, 3], [3, 3], [3, 3])
 	`)
 
-				res, err := planner.PrecalculateExprRule(test.root, tx, nil)
+				res, err := planner.PrecalculateExprRule(test.root, tx)
 				require.NoError(t, err)
 
-				res, err = planner.UseIndexBasedOnFilterNodeRule(res, tx, nil)
+				res, err = planner.UseIndexBasedOnFilterNodeRule(res, tx)
 				require.NoError(t, err)
 				require.Equal(t, test.expected.String(), res.String())
 			})
diff --git a/internal/query/statement/alter_test.go b/internal/query/statement/alter_test.go
index 67967e9a..062b3b79 100644
--- a/internal/query/statement/alter_test.go
+++ b/internal/query/statement/alter_test.go
@@ -32,7 +32,7 @@ func TestAlterTable(t *testing.T) {
 	// Selecting from the old name should fail.
 	err = db.Exec("SELECT * FROM foo")
 	if !errors.Is(err, errs.ErrTableNotFound) {
-		require.Equal(t, err, errs.ErrTableNotFound)
+		require.Equal(t, errs.ErrTableNotFound, err)
 	}
 
 	d, err := db.QueryDocument("SELECT * FROM bar")
diff --git a/internal/sql/parser/create_test.go b/internal/sql/parser/create_test.go
index bb47b1fb..600af67c 100644
--- a/internal/sql/parser/create_test.go
+++ b/internal/sql/parser/create_test.go
@@ -18,11 +18,11 @@ func TestParserCreateTable(t *testing.T) {
 		expected statement.Statement
 		errored  bool
 	}{
-		{"Basic", "CREATE TABLE test", statement.CreateTableStmt{Info: database.TableInfo{TableName: "test"}}, false},
-		{"If not exists", "CREATE TABLE IF NOT EXISTS test", statement.CreateTableStmt{Info: database.TableInfo{TableName: "test"}, IfNotExists: true}, false},
-		{"Path only", "CREATE TABLE test(a)", statement.CreateTableStmt{}, true},
+		{"Basic", "CREATE TABLE test", &statement.CreateTableStmt{Info: database.TableInfo{TableName: "test"}}, false},
+		{"If not exists", "CREATE TABLE IF NOT EXISTS test", &statement.CreateTableStmt{Info: database.TableInfo{TableName: "test"}, IfNotExists: true}, false},
+		{"Path only", "CREATE TABLE test(a)", &statement.CreateTableStmt{}, true},
 		{"With primary key", "CREATE TABLE test(foo INTEGER PRIMARY KEY)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -31,9 +31,9 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With primary key twice", "CREATE TABLE test(foo PRIMARY KEY PRIMARY KEY)",
-			statement.CreateTableStmt{}, true},
+			&statement.CreateTableStmt{}, true},
 		{"With type", "CREATE TABLE test(foo INTEGER)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -42,7 +42,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With not null", "CREATE TABLE test(foo NOT NULL)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -51,7 +51,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With default", "CREATE TABLE test(foo DEFAULT \"10\")",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -60,7 +60,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With unique", "CREATE TABLE test(foo UNIQUE)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -69,13 +69,13 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With default twice", "CREATE TABLE test(foo DEFAULT 10 DEFAULT 10)",
-			statement.CreateTableStmt{}, true},
+			&statement.CreateTableStmt{}, true},
 		{"With not null twice", "CREATE TABLE test(foo NOT NULL NOT NULL)",
-			statement.CreateTableStmt{}, true},
+			&statement.CreateTableStmt{}, true},
 		{"With unique twice", "CREATE TABLE test(foo UNIQUE UNIQUE)",
-			statement.CreateTableStmt{}, true},
+			&statement.CreateTableStmt{}, true},
 		{"With type and not null", "CREATE TABLE test(foo INTEGER NOT NULL)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -84,7 +84,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With not null and primary key", "CREATE TABLE test(foo INTEGER NOT NULL PRIMARY KEY)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -93,7 +93,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With primary key and not null", "CREATE TABLE test(foo INTEGER PRIMARY KEY NOT NULL)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -102,7 +102,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With multiple constraints", "CREATE TABLE test(foo INTEGER PRIMARY KEY, bar INTEGER NOT NULL, baz[4][1].bat TEXT)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -113,7 +113,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With table constraints / PK on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, PRIMARY KEY (foo))",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -123,7 +123,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With table constraints / PK on undefined field", "CREATE TABLE test(foo INTEGER, PRIMARY KEY (bar))",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -136,7 +136,7 @@ func TestParserCreateTable(t *testing.T) {
 		{"With table constraints / duplicate pk", "CREATE TABLE test(foo INTEGER PRIMARY KEY, PRIMARY KEY (bar))", nil, true},
 		{"With table constraints / duplicate pk on same path", "CREATE TABLE test(foo INTEGER PRIMARY KEY, PRIMARY KEY (foo))", nil, true},
 		{"With table constraints / UNIQUE on defined field", "CREATE TABLE test(foo INTEGER, bar NOT NULL, UNIQUE (foo))",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -146,7 +146,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With table constraints / UNIQUE on undefined field", "CREATE TABLE test(foo INTEGER, UNIQUE (bar))",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -156,7 +156,7 @@ func TestParserCreateTable(t *testing.T) {
 				},
 			}, false},
 		{"With table constraints / UNIQUE twice", "CREATE TABLE test(foo INTEGER UNIQUE, UNIQUE (foo))",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -166,10 +166,10 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With table constraints / duplicate pk on same path", "CREATE TABLE test(foo INTEGER PRIMARY KEY, PRIMARY KEY (foo))", nil, true},
 		{"With multiple primary keys", "CREATE TABLE test(foo PRIMARY KEY, bar PRIMARY KEY)",
-			statement.CreateTableStmt{}, true},
+			&statement.CreateTableStmt{}, true},
 		{"With all supported fixed size data types",
 			"CREATE TABLE test(d double, b bool)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -180,7 +180,7 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With all supported variable size data types",
 			"CREATE TABLE test(i integer, b blob, byt bytes, t text, a array, d document)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -195,7 +195,7 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With integer aliases types",
 			"CREATE TABLE test(i int, ii int2, ei int8, m mediumint, s smallint, b bigint, t tinyint)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -211,7 +211,7 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With double aliases types",
 			"CREATE TABLE test(dp DOUBLE PRECISION, r real, d double)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -223,7 +223,7 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With text aliases types",
 			"CREATE TABLE test(v VARCHAR(255), c CHARACTER(64), t TEXT)",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -235,7 +235,7 @@ func TestParserCreateTable(t *testing.T) {
 			}, false},
 		{"With errored text aliases types",
 			"CREATE TABLE test(v VARCHAR(1 IN [1, 2, 3] AND foo > 4) )",
-			statement.CreateTableStmt{
+			&statement.CreateTableStmt{
 				Info: database.TableInfo{
 					TableName: "test",
 					FieldConstraints: []*database.FieldConstraint{
@@ -266,23 +266,23 @@ func TestParserCreateIndex(t *testing.T) {
 		expected statement.Statement
 		errored  bool
 	}{
-		{"Basic", "CREATE INDEX idx ON test (foo)", statement.CreateIndexStmt{
+		{"Basic", "CREATE INDEX idx ON test (foo)", &statement.CreateIndexStmt{
 			Info: database.IndexInfo{
 				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo"))},
 			}}, false},
-		{"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo.bar[1])", statement.CreateIndexStmt{
+		{"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo.bar[1])", &statement.CreateIndexStmt{
 			Info: database.IndexInfo{
 				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo.bar[1]"))},
 			}, IfNotExists: true}, false},
-		{"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo[3].baz)", statement.CreateIndexStmt{
+		{"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo[3].baz)", &statement.CreateIndexStmt{
 			Info: database.IndexInfo{
 				IndexName: "idx", TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, Unique: true,
 			}, IfNotExists: true}, false},
-		{"No name", "CREATE UNIQUE INDEX ON test (foo[3].baz)", statement.CreateIndexStmt{
+		{"No name", "CREATE UNIQUE INDEX ON test (foo[3].baz)", &statement.CreateIndexStmt{
 			Info: database.IndexInfo{TableName: "test", Paths: []document.Path{document.Path(testutil.ParsePath(t, "foo[3].baz"))}, Unique: true}}, false},
 		{"No name with IF NOT EXISTS", "CREATE UNIQUE INDEX IF NOT EXISTS ON test (foo[3].baz)", nil, true},
 		{"More than 1 path", "CREATE INDEX idx ON test (foo, bar)",
-			statement.CreateIndexStmt(statement.CreateIndexStmt{
+			&statement.CreateIndexStmt{
 				Info: database.IndexInfo{
 					IndexName: "idx",
 					TableName: "test",
@@ -291,7 +291,7 @@ func TestParserCreateIndex(t *testing.T) {
 						document.Path(testutil.ParsePath(t, "bar")),
 					},
 				},
-			}),
+			},
 			false},
 		{"No fields", "CREATE INDEX idx ON test", nil, true},
 	}
diff --git a/internal/sql/parser/delete_test.go b/internal/sql/parser/delete_test.go
index a7c37c64..df7f5c99 100644
--- a/internal/sql/parser/delete_test.go
+++ b/internal/sql/parser/delete_test.go
@@ -54,9 +54,7 @@ func TestParserDelete(t *testing.T) {
 			q, err := parser.ParseQuery(test.s)
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			stmt, err := q.Statements[0].(*statement.DeleteStmt).ToStream()
-			require.NoError(t, err)
-			require.EqualValues(t, &statement.StreamStmt{Stream: test.expected}, stmt)
+			require.EqualValues(t, &statement.StreamStmt{Stream: test.expected}, q.Statements[0].(*statement.StreamStmt))
 		})
 	}
 }
diff --git a/internal/sql/parser/explain_test.go b/internal/sql/parser/explain_test.go
index 5b078784..064a4137 100644
--- a/internal/sql/parser/explain_test.go
+++ b/internal/sql/parser/explain_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/query/statement"
 	"github.com/genjidb/genji/internal/sql/parser"
+	"github.com/genjidb/genji/internal/stream"
 	"github.com/stretchr/testify/require"
 )
 
@@ -16,9 +17,9 @@ func TestParserExplain(t *testing.T) {
 		expected statement.Statement
 		errored  bool
 	}{
-		{"Explain create table", "EXPLAIN SELECT * FROM test", &statement.ExplainStmt{Statement: &statement.SelectStmt{
-			TableName:       "test",
-			ProjectionExprs: []expr.Expr{expr.Wildcard{}},
+		{"Explain create table", "EXPLAIN SELECT * FROM test", &statement.ExplainStmt{Statement: &statement.StreamStmt{
+			ReadOnly: true,
+			Stream:   stream.New(stream.SeqScan("test")).Pipe(stream.Project(expr.Wildcard{})),
 		}}, false},
 		{"Multiple Explains", "EXPLAIN EXPLAIN CREATE TABLE test", nil, true},
 	}
diff --git a/internal/sql/parser/insert_test.go b/internal/sql/parser/insert_test.go
index b14f38c8..60c7962e 100644
--- a/internal/sql/parser/insert_test.go
+++ b/internal/sql/parser/insert_test.go
@@ -181,11 +181,7 @@ func TestParserInsert(t *testing.T) {
 			}
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			stmt := q.Statements[0].(*statement.InsertStmt)
-			require.False(t, stmt.IsReadOnly())
-			ss, err := stmt.ToStream()
-			require.NoError(t, err)
-			require.EqualValues(t, test.expected.String(), ss.String())
+			require.EqualValues(t, test.expected.String(), q.Statements[0].(*statement.StreamStmt).Stream.String())
 		})
 	}
 }
diff --git a/internal/sql/parser/parser_test.go b/internal/sql/parser/parser_test.go
index 93311c97..7e7d1d0a 100644
--- a/internal/sql/parser/parser_test.go
+++ b/internal/sql/parser/parser_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/genjidb/genji/internal/expr"
 	"github.com/genjidb/genji/internal/query/statement"
 	"github.com/genjidb/genji/internal/sql/parser"
+	"github.com/genjidb/genji/internal/stream"
 )
 
 func TestParserMultiStatement(t *testing.T) {
@@ -18,12 +19,12 @@ func TestParserMultiStatement(t *testing.T) {
 	}{
 		{"OnlyCommas", ";;;", nil},
 		{"TrailingComma", "SELECT * FROM foo;;;DELETE FROM foo;", []statement.Statement{
-			&statement.SelectStmt{
-				TableName:       "foo",
-				ProjectionExprs: []expr.Expr{expr.Wildcard{}},
+			&statement.StreamStmt{
+				Stream:   stream.New(stream.SeqScan("foo")).Pipe(stream.Project(expr.Wildcard{})),
+				ReadOnly: true,
 			},
-			&statement.DeleteStmt{
-				TableName: "foo",
+			&statement.StreamStmt{
+				Stream: stream.New(stream.SeqScan("foo")).Pipe(stream.TableDelete("foo")),
 			},
 		}},
 	}
diff --git a/internal/sql/parser/select_test.go b/internal/sql/parser/select_test.go
index d5b835c3..644f8085 100644
--- a/internal/sql/parser/select_test.go
+++ b/internal/sql/parser/select_test.go
@@ -132,9 +132,7 @@ func TestParserSelect(t *testing.T) {
 			if !test.mustFail {
 				require.NoError(t, err)
 				require.Len(t, q.Statements, 1)
-				st, err := q.Statements[0].(*statement.SelectStmt).ToStream()
-				require.NoError(t, err)
-				require.EqualValues(t, &statement.StreamStmt{Stream: test.expected, ReadOnly: true}, st)
+				require.EqualValues(t, &statement.StreamStmt{Stream: test.expected, ReadOnly: true}, q.Statements[0].(*statement.StreamStmt))
 			} else {
 				require.Error(t, err)
 			}
diff --git a/internal/sql/parser/update_test.go b/internal/sql/parser/update_test.go
index e7c08b4e..ef5b16dd 100644
--- a/internal/sql/parser/update_test.go
+++ b/internal/sql/parser/update_test.go
@@ -75,9 +75,9 @@ func TestParserUpdate(t *testing.T) {
 
 			require.NoError(t, err)
 			require.Len(t, q.Statements, 1)
-			stmt := q.Statements[0].(*statement.UpdateStmt)
+			stmt := q.Statements[0].(*statement.StreamStmt)
 			require.False(t, stmt.IsReadOnly())
-			require.EqualValues(t, test.expected, stmt.ToStream().Stream)
+			require.EqualValues(t, test.expected, q.Statements[0].(*statement.StreamStmt).Stream)
 		})
 	}
 }
diff --git a/internal/stream/aggregate_test.go b/internal/stream/aggregate_test.go
index d3074eea..61faa6a8 100644
--- a/internal/stream/aggregate_test.go
+++ b/internal/stream/aggregate_test.go
@@ -109,6 +109,7 @@ func (f *fakeAggregator) String() string {
 }
 
 type fakeAggretatorBuilder struct {
+	expr.Expr
 	name string
 }
 
diff --git a/internal/stream/operator_test.go b/internal/stream/operator_test.go
index 555497b6..b4963a57 100644
--- a/internal/stream/operator_test.go
+++ b/internal/stream/operator_test.go
@@ -66,81 +66,6 @@ func TestMap(t *testing.T) {
 	})
 }
 
-func TestIfEmpty(t *testing.T) {
-	tests := []struct {
-		in         []document.Document
-		ifEmpty    []document.Document
-		ifNotEmpty expr.Expr
-		out        []document.Document
-		fails      bool
-	}{
-		{
-			testutil.MakeDocuments(t, `{"a": 1}`),
-			testutil.MakeDocuments(t, `{"nope": "nope"}`),
-			parser.MustParseExpr(`{a: a + 1}`),
-			testutil.MakeDocuments(t, `{"a": 2}`),
-			false,
-		},
-		{
-			nil,
-			testutil.MakeDocuments(t, `{"a": 1}`),
-			parser.MustParseExpr(`{nope: "nope"}`),
-			testutil.MakeDocuments(t, `{"a": 1}`),
-			false,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run("", func(t *testing.T) {
-			s := stream.New(stream.Documents(test.in...)).Pipe(stream.IfEmpty(
-				stream.New(stream.Documents(test.ifEmpty...)),
-				stream.New(stream.Map(test.ifNotEmpty)),
-			))
-			var docs []document.Document
-			err := s.Iterate(&expr.Environment{}, func(out *expr.Environment) error {
-				d, _ := out.GetDocument()
-				docs = append(docs, d)
-				return nil
-			})
-			if test.fails {
-				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, test.out, docs)
-			}
-		})
-	}
-
-	t.Run("Empty else", func(t *testing.T) {
-		s := stream.New(stream.Documents(testutil.MakeDocuments(t, `{"a": 1}`)...)).Pipe(stream.IfEmpty(
-			stream.New(stream.Documents(testutil.MakeDocuments(t, `{"a": 2}`)...)),
-			nil,
-		))
-
-		var docs []document.Document
-		err := s.Iterate(&expr.Environment{}, func(out *expr.Environment) error {
-			d, _ := out.GetDocument()
-			docs = append(docs, d)
-			return nil
-		})
-		require.NoError(t, err)
-		require.Len(t, docs, 1)
-		require.Equal(t, testutil.MakeDocument(t, `{"a": 1}`), docs[0])
-	})
-
-	t.Run("String", func(t *testing.T) {
-		s := stream.New(stream.Documents(testutil.MakeDocuments(t, `{"a": 1}`)...)).Pipe(stream.IfEmpty(
-			stream.New(stream.Documents(testutil.MakeDocuments(t, `{"a": 3}`)...)),
-			stream.New(stream.Map(parser.MustParseExpr(`{a: a + 1}`))),
-		))
-
-		require.Equal(t, `docs({"a": 1}) | ifEmpty(
-	docs({"a": 3}),
-	map({a: a + 1})
-)`, s.String())
-	})
-}
-
 func TestFilter(t *testing.T) {
 	tests := []struct {
 		e     expr.Expr
diff --git a/internal/stream/scan_test.go b/internal/stream/scan_test.go
index 38e347bb..4c9c52cc 100644
--- a/internal/stream/scan_test.go
+++ b/internal/stream/scan_test.go
@@ -131,7 +131,7 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.ValueRanges{
-				{Max: document.NewIntegerValue(2)},
+				{Max: testutil.IntegerValue(2)},
 			},
 			false, false,
 		},
@@ -140,7 +140,16 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`),
 			stream.ValueRanges{
-				{Max: document.NewIntegerValue(1)},
+				{Max: testutil.IntegerValue(1)},
+			},
+			false, false,
+		},
+		{
+			"max:1.1",
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1}`),
+			stream.ValueRanges{
+				{Max: testutil.DoubleValue(1.1)},
 			},
 			false, false,
 		},
@@ -149,7 +158,16 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.ValueRanges{
-				{Min: document.NewIntegerValue(1)},
+				{Min: testutil.IntegerValue(1)},
+			},
+			false, false,
+		},
+		{
+			"min:0.5",
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			stream.ValueRanges{
+				{Min: testutil.DoubleValue(0.5)},
 			},
 			false, false,
 		},
@@ -158,7 +176,16 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.ValueRanges{
-				{Min: document.NewIntegerValue(1), Max: document.NewIntegerValue(2)},
+				{Min: testutil.IntegerValue(1), Max: testutil.IntegerValue(2)},
+			},
+			false, false,
+		},
+		{
+			"min/max:0.5/1.5",
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			stream.ValueRanges{
+				{Min: testutil.DoubleValue(0.5), Max: testutil.DoubleValue(1.5)},
 			},
 			false, false,
 		},
@@ -173,7 +200,7 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.ValueRanges{
-				{Max: document.NewIntegerValue(2)},
+				{Max: testutil.IntegerValue(2)},
 			},
 			true, false,
 		},
@@ -182,7 +209,7 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.ValueRanges{
-				{Min: document.NewIntegerValue(1)},
+				{Min: testutil.IntegerValue(1)},
 			},
 			true, false,
 		},
@@ -191,7 +218,7 @@ func TestPkScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.ValueRanges{
-				{Min: document.NewIntegerValue(1), Max: document.NewIntegerValue(2)},
+				{Min: testutil.IntegerValue(1), Max: testutil.IntegerValue(2)},
 			},
 			true, false,
 		},
@@ -243,13 +270,13 @@ func TestPkScan(t *testing.T) {
 
 	t.Run("String", func(t *testing.T) {
 		require.Equal(t, `pkScan("test", [1, 2])`, stream.PkScan("test", stream.ValueRange{
-			Min: document.NewIntegerValue(1), Max: document.NewIntegerValue(2),
+			Min: testutil.IntegerValue(1), Max: testutil.IntegerValue(2),
 		}).String())
 
 		op := stream.PkScan("test",
-			stream.ValueRange{Min: document.NewIntegerValue(1), Max: document.NewIntegerValue(2), Exclusive: true},
-			stream.ValueRange{Min: document.NewIntegerValue(10), Exact: true},
-			stream.ValueRange{Min: document.NewIntegerValue(100)},
+			stream.ValueRange{Min: testutil.IntegerValue(1), Max: testutil.IntegerValue(2), Exclusive: true},
+			stream.ValueRange{Min: testutil.IntegerValue(10), Exact: true},
+			stream.ValueRange{Min: testutil.IntegerValue(100)},
 		)
 		op.Reverse = true
 
@@ -258,7 +285,6 @@ func TestPkScan(t *testing.T) {
 }
 
 func TestIndexScan(t *testing.T) {
-	newVB := document.NewValueBuffer
 	tests := []struct {
 		name                  string
 		indexOn               string
@@ -285,7 +311,16 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.IndexRanges{
-				{Max: newVB(document.NewIntegerValue(2))},
+				{Max: testutil.ExprList(t, `[2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
+			},
+			false, false,
+		},
+		{
+			"max:1.2", "a",
+			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1}`),
+			stream.IndexRanges{
+				{Max: testutil.ExprList(t, `[1.2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			false, false,
 		},
@@ -294,10 +329,16 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
 			stream.IndexRanges{
-				{Max: newVB(
-					document.NewIntegerValue(2),
-					document.NewIntegerValue(2),
-				)},
+				{Max: testutil.ExprList(t, `[2, 2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
+			},
+			false, false,
+		},
+		{
+			"max:[2, 2.2]", "a, b",
+			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
+			stream.IndexRanges{
+				{Max: testutil.ExprList(t, `[2, 2.2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
 			},
 			false, false,
 		},
@@ -306,7 +347,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`),
 			stream.IndexRanges{
-				{Max: newVB(document.NewIntegerValue(1))},
+				{Max: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			false, false,
 		},
@@ -315,10 +356,16 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`),
 			stream.IndexRanges{
-				{Max: newVB(
-					document.NewIntegerValue(1),
-					document.NewIntegerValue(2),
-				)},
+				{Max: testutil.ExprList(t, `[1, 2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
+			},
+			false, false,
+		},
+		{
+			"max:[1.1, 2]", "a, b",
+			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
+			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`),
+			stream.IndexRanges{
+				{Max: testutil.ExprList(t, `[1.1, 2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
 			},
 			false, false,
 		},
@@ -327,7 +374,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			false, false,
 		},
@@ -337,7 +384,20 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 2, "b": 2}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(2), document.NewIntegerValue(1)),
+					Min:   testutil.ExprList(t, `[2, 1]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
+				},
+			},
+			false, false,
+		},
+		{
+			"min:[2, 1.5]", "a, b",
+			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
+			testutil.MakeDocuments(t, `{"a": 2, "b": 2}`),
+			stream.IndexRanges{
+				{
+					Min:   testutil.ExprList(t, `[2, 1.5]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			false, false,
@@ -348,8 +408,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1)),
-					Max: newVB(document.NewIntegerValue(2)),
+					Min:   testutil.ExprList(t, `[1]`),
+					Max:   testutil.ExprList(t, `[2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a")},
 				},
 			},
 			false, false,
@@ -360,8 +421,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2}`, `{"a": 2, "b": 2}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
-					Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+					Min:   testutil.ExprList(t, `[1, 1]`),
+					Max:   testutil.ExprList(t, `[2, 2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			false, false,
@@ -372,8 +434,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 3}`, `{"a": 2, "b": 2}`), // [1, 3] < [2, 2]
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
-					Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+					Min:   testutil.ExprList(t, `[1, 1]`),
+					Max:   testutil.ExprList(t, `[2, 2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			false, false,
@@ -389,7 +452,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.IndexRanges{
-				{Max: newVB(document.NewIntegerValue(2))},
+				{Max: testutil.ExprList(t, `[2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			true, false,
 		},
@@ -399,7 +462,8 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 2, "b": 2}`, `{"a": 1, "b": 1}`),
 			stream.IndexRanges{
 				{
-					Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+					Max:   testutil.ExprList(t, `[2, 2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			true, false,
@@ -409,7 +473,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": 2}`),
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			true, false,
 		},
@@ -418,7 +482,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1}`, `{"a": -2}`),
 			testutil.MakeDocuments(t, `{"a": 1}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a")}},
 			},
 			true, false,
 		},
@@ -428,7 +492,8 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 2, "b": 2}`, `{"a": 1, "b": 1}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
+					Min:   testutil.ExprList(t, `[1, 1]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			true, false,
@@ -439,8 +504,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 2}`, `{"a": 1}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1)),
-					Max: newVB(document.NewIntegerValue(2)),
+					Min:   testutil.ExprList(t, `[1]`),
+					Max:   testutil.ExprList(t, `[2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a")},
 				},
 			},
 			true, false,
@@ -451,8 +517,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 2, "b": 2}`, `{"a": 1, "b": 1}`),
 			stream.IndexRanges{
 				{
-					Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
-					Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+					Min:   testutil.ExprList(t, `[1, 1]`),
+					Max:   testutil.ExprList(t, `[2, 2]`),
+					Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			true, false,
@@ -464,7 +531,8 @@ func TestIndexScan(t *testing.T) {
 			stream.IndexRanges{
 				{
 					IndexArity: 2,
-					Max:        newVB(document.NewIntegerValue(1)),
+					Max:        testutil.ExprList(t, `[1]`),
+					Paths:      []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			false, false,
@@ -475,10 +543,11 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 9223372036854775807}`, `{"a": 1, "b": 1}`),
 			stream.IndexRanges{
 				{
-					Max:        newVB(document.NewIntegerValue(1)),
+					Max:        testutil.ExprList(t, `[1]`),
 					Exclusive:  false,
 					Exact:      false,
 					IndexArity: 2,
+					Paths:      []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			true, false,
@@ -488,7 +557,9 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2, "c": 1}`, `{"a": 2, "b": 2, "c":  2}`, `{"a": 1, "b": 2, "c": 9223372036854775807}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": 2, "c": 1}`, `{"a": 1, "b": 2, "c": 9223372036854775807}`),
 			stream.IndexRanges{
-				{IndexArity: 3, Max: newVB(document.NewIntegerValue(1), document.NewIntegerValue(2))},
+				{
+					IndexArity: 3, Max: testutil.ExprList(t, `[1, 2]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b"), testutil.ParseDocumentPath(t, "c")},
+				},
 			},
 			false, false,
 		},
@@ -497,7 +568,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": -2}`, `{"a": -2, "b": 2}`, `{"a": 1, "b": 1}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": -2}`, `{"a": 1, "b": 1}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
 			},
 			false, false,
 		},
@@ -506,7 +577,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": -2, "c": 0}`, `{"a": -2, "b": 2, "c": 1}`, `{"a": 1, "b": 1, "c": 2}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": -2, "c": 0}`, `{"a": 1, "b": 1, "c": 2}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b"), testutil.ParseDocumentPath(t, "c")}},
 			},
 			false, false,
 		},
@@ -515,7 +586,7 @@ func TestIndexScan(t *testing.T) {
 			testutil.MakeDocuments(t, `{"a": 1, "b": -2}`, `{"a": -2, "b": 2}`, `{"a": 1, "b": 1}`),
 			testutil.MakeDocuments(t, `{"a": 1, "b": 1}`, `{"a": 1, "b": -2}`),
 			stream.IndexRanges{
-				{Min: newVB(document.NewIntegerValue(1))},
+				{Min: testutil.ExprList(t, `[1]`), Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")}},
 			},
 			true, false,
 		},
@@ -526,8 +597,9 @@ func TestIndexScan(t *testing.T) {
 			stream.IndexRanges{
 				{
 					IndexArity: 2,
-					Min:        newVB(document.NewIntegerValue(1)),
-					Max:        newVB(document.NewIntegerValue(2)),
+					Min:        testutil.ExprList(t, `[1]`),
+					Max:        testutil.ExprList(t, `[2]`),
+					Paths:      []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			false, false,
@@ -539,8 +611,9 @@ func TestIndexScan(t *testing.T) {
 			stream.IndexRanges{
 				{
 					IndexArity: 2,
-					Min:        newVB(document.NewIntegerValue(1)),
-					Max:        newVB(document.NewIntegerValue(2)),
+					Min:        testutil.ExprList(t, `[1]`),
+					Max:        testutil.ExprList(t, `[2]`),
+					Paths:      []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
 				},
 			},
 			true, false,
@@ -548,7 +621,7 @@ func TestIndexScan(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		t.Run(test.name+"index on "+test.indexOn, func(t *testing.T) {
+		t.Run(test.name+":index on "+test.indexOn, func(t *testing.T) {
 			_, tx, cleanup := testutil.NewTestTx(t)
 			defer cleanup()
 
@@ -594,11 +667,11 @@ func TestIndexScan(t *testing.T) {
 	t.Run("String", func(t *testing.T) {
 		t.Run("idx_test_a", func(t *testing.T) {
 			require.Equal(t, `indexScan("idx_test_a", [1, 2])`, stream.IndexScan("idx_test_a", stream.IndexRange{
-				Min: newVB(document.NewIntegerValue(1)), Max: newVB(document.NewIntegerValue(2)),
+				Min: testutil.ExprList(t, `[1]`), Max: testutil.ExprList(t, `[2]`),
 			}).String())
 
 			op := stream.IndexScan("idx_test_a", stream.IndexRange{
-				Min: newVB(document.NewIntegerValue(1)), Max: newVB(document.NewIntegerValue(2)),
+				Min: testutil.ExprList(t, `[1]`), Max: testutil.ExprList(t, `[2]`),
 			})
 			op.Reverse = true
 
@@ -607,13 +680,13 @@ func TestIndexScan(t *testing.T) {
 
 		t.Run("idx_test_a_b", func(t *testing.T) {
 			require.Equal(t, `indexScan("idx_test_a_b", [[1, 1], [2, 2]])`, stream.IndexScan("idx_test_a_b", stream.IndexRange{
-				Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
-				Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+				Min: testutil.ExprList(t, `[1, 1]`),
+				Max: testutil.ExprList(t, `[2, 2]`),
 			}).String())
 
 			op := stream.IndexScan("idx_test_a_b", stream.IndexRange{
-				Min: newVB(document.NewIntegerValue(1), document.NewIntegerValue(1)),
-				Max: newVB(document.NewIntegerValue(2), document.NewIntegerValue(2)),
+				Min: testutil.ExprList(t, `[1, 1]`),
+				Max: testutil.ExprList(t, `[2, 2]`),
 			})
 			op.Reverse = true
 
diff --git a/internal/stream/stream_test.go b/internal/stream/stream_test.go
index 73044711..d437de82 100644
--- a/internal/stream/stream_test.go
+++ b/internal/stream/stream_test.go
@@ -31,7 +31,4 @@ func TestStream(t *testing.T) {
 	})
 	require.NoError(t, err)
 	require.Equal(t, int64(1), count)
-
-	clone := s.Clone()
-	require.Equal(t, s, clone)
 }
diff --git a/internal/testutil/db.go b/internal/testutil/db.go
index 9df47f8b..689a4cef 100644
--- a/internal/testutil/db.go
+++ b/internal/testutil/db.go
@@ -60,6 +60,11 @@ func Query(tx *database.Transaction, q string, params ...expr.Param) (*statement
 		return nil, err
 	}
 
+	err = pq.PrepareTx(tx)
+	if err != nil {
+		return nil, err
+	}
+
 	return pq.Exec(tx, params)
 }
 
diff --git a/internal/testutil/expr.go b/internal/testutil/expr.go
index 2fc7b723..e7914be4 100644
--- a/internal/testutil/expr.go
+++ b/internal/testutil/expr.go
@@ -53,6 +53,16 @@ func ArrayValue(a document.Array) expr.LiteralValue {
 	return expr.LiteralValue(document.NewArrayValue(a))
 }
 
+func ExprList(t testing.TB, s string) expr.LiteralExprList {
+	t.Helper()
+
+	e, err := parser.ParseExpr(s)
+	require.NoError(t, err)
+	require.IsType(t, e, expr.LiteralExprList{})
+
+	return e.(expr.LiteralExprList)
+}
+
 func ParsePath(t testing.TB, p string) expr.Path {
 	t.Helper()