syncbase: Implementation and tests for Glob and Scan.

This CL includes a few smaller changes on the side:
- Implements row-level Get/Put/Delete, needed so we can
  test the behavior of Scan. Includes tests for those
  methods.
- Enables TestTableCreate and TestTableDelete (since Table
  Create/Delete is a prereq for Scan). This required
  extending the utilities in testutil/layer.go a bit, and
  implementing Table.{Get,Set}Permissions for the "empty
  prefix" case.
- Adds FullName() methods to all levels of hierarchy.
  I ended up not needing these, but they seem worth keeping
  nonetheless.
- Adds various util functions (along with tests for them)
  and fixes a few small TODOs.

Change-Id: I55a4b0da9fb8f5551958d09c5ac8afd88dff2ebb
diff --git a/services/syncbase/server/app.go b/services/syncbase/server/app.go
index c1c6dcd..188ef16 100644
--- a/services/syncbase/server/app.go
+++ b/services/syncbase/server/app.go
@@ -12,6 +12,7 @@
 	"v.io/syncbase/x/ref/services/syncbase/server/util"
 	"v.io/syncbase/x/ref/services/syncbase/store"
 	"v.io/v23/context"
+	"v.io/v23/naming"
 	"v.io/v23/rpc"
 	"v.io/v23/security/access"
 	"v.io/v23/verror"
@@ -61,7 +62,15 @@
 	return data.Perms, util.FormatVersion(data.Version), nil
 }
 
-// TODO(sadovsky): Implement Glob.
+func (a *app) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+	// Check perms.
+	sn := a.s.st.NewSnapshot()
+	if err := util.Get(ctx, call, sn, a, &appData{}); err != nil {
+		sn.Close()
+		return nil, err
+	}
+	return util.Glob(ctx, call, pattern, sn, util.JoinKeyParts(util.DbInfoPrefix, a.name))
+}
 
 ////////////////////////////////////////
 // util.App methods
diff --git a/services/syncbase/server/db_info.go b/services/syncbase/server/db_info.go
index 6ccc664..e284fae 100644
--- a/services/syncbase/server/db_info.go
+++ b/services/syncbase/server/db_info.go
@@ -37,7 +37,7 @@
 }
 
 func (d *dbInfoLayer) StKey() string {
-	return util.JoinKeyParts(util.DbInfoPrefix, d.stKeyPart())
+	return util.JoinKeyParts(util.DbInfoPrefix, d.a.name, d.stKeyPart())
 }
 
 ////////////////////////////////////////
diff --git a/services/syncbase/server/dispatcher.go b/services/syncbase/server/dispatcher.go
index a6127cd..c676f97 100644
--- a/services/syncbase/server/dispatcher.go
+++ b/services/syncbase/server/dispatcher.go
@@ -8,8 +8,8 @@
 	"strings"
 
 	wire "v.io/syncbase/v23/services/syncbase"
+	pubutil "v.io/syncbase/v23/syncbase/util"
 	"v.io/syncbase/x/ref/services/syncbase/server/nosql"
-	"v.io/syncbase/x/ref/services/syncbase/server/util"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
 	"v.io/v23/verror"
@@ -37,7 +37,7 @@
 	// Validate all key atoms up front, so that we can avoid doing so in all our
 	// method implementations.
 	appName := parts[0]
-	if !util.ValidKeyAtom(appName) {
+	if !pubutil.ValidName(appName) {
 		return nil, nil, wire.NewErrInvalidName(nil, suffix)
 	}
 
diff --git a/services/syncbase/server/nosql/database.go b/services/syncbase/server/nosql/database.go
index 03377fc..7ca9271 100644
--- a/services/syncbase/server/nosql/database.go
+++ b/services/syncbase/server/nosql/database.go
@@ -10,6 +10,7 @@
 	"v.io/syncbase/x/ref/services/syncbase/store"
 	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
 	"v.io/v23/context"
+	"v.io/v23/naming"
 	"v.io/v23/rpc"
 	"v.io/v23/security/access"
 	"v.io/v23/verror"
@@ -34,7 +35,7 @@
 // Designed for use from within App.CreateNoSQLDatabase.
 func NewDatabase(ctx *context.T, call rpc.ServerCall, a util.App, name string, perms access.Permissions) (*database, error) {
 	if perms == nil {
-		return nil, verror.New(verror.ErrInternal, nil, "perms must be specified")
+		return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
 	}
 	// TODO(sadovsky): Make storage engine pluggable.
 	d := &database{
@@ -90,7 +91,15 @@
 	return data.Perms, util.FormatVersion(data.Version), nil
 }
 
-// TODO(sadovsky): Implement Glob.
+func (d *database) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+	// Check perms.
+	sn := d.st.NewSnapshot()
+	if err := util.Get(ctx, call, sn, d, &databaseData{}); err != nil {
+		sn.Close()
+		return nil, err
+	}
+	return util.Glob(ctx, call, pattern, sn, util.TablePrefix)
+}
 
 ////////////////////////////////////////
 // util.Database methods
diff --git a/services/syncbase/server/nosql/dispatcher.go b/services/syncbase/server/nosql/dispatcher.go
index 50737c5..db18382 100644
--- a/services/syncbase/server/nosql/dispatcher.go
+++ b/services/syncbase/server/nosql/dispatcher.go
@@ -9,6 +9,7 @@
 
 	wire "v.io/syncbase/v23/services/syncbase"
 	nosqlWire "v.io/syncbase/v23/services/syncbase/nosql"
+	pubutil "v.io/syncbase/v23/syncbase/util"
 	"v.io/syncbase/x/ref/services/syncbase/server/util"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
@@ -37,7 +38,7 @@
 	// Validate all key atoms up front, so that we can avoid doing so in all our
 	// method implementations.
 	for _, s := range parts {
-		if !util.ValidKeyAtom(s) {
+		if !pubutil.ValidName(s) {
 			return nil, nil, wire.NewErrInvalidName(nil, suffix)
 		}
 	}
diff --git a/services/syncbase/server/nosql/row.go b/services/syncbase/server/nosql/row.go
index c699e88..c26eb39 100644
--- a/services/syncbase/server/nosql/row.go
+++ b/services/syncbase/server/nosql/row.go
@@ -10,7 +10,6 @@
 	"v.io/syncbase/x/ref/services/syncbase/store"
 	"v.io/v23/context"
 	"v.io/v23/rpc"
-	"v.io/v23/vdl"
 	"v.io/v23/verror"
 )
 
@@ -32,11 +31,11 @@
 ////////////////////////////////////////
 // RPC methods
 
-func (r *row) Get(ctx *context.T, call rpc.ServerCall) (*vdl.Value, error) {
+func (r *row) Get(ctx *context.T, call rpc.ServerCall) ([]byte, error) {
 	return r.get(ctx, call, r.t.d.st)
 }
 
-func (r *row) Put(ctx *context.T, call rpc.ServerCall, value *vdl.Value) error {
+func (r *row) Put(ctx *context.T, call rpc.ServerCall, value []byte) error {
 	return r.put(ctx, call, r.t.d.st, value)
 }
 
@@ -68,18 +67,18 @@
 // an authorization check (currently against the table perms).
 // Returns a VDL-compatible error.
 func (r *row) checkAccess(ctx *context.T, call rpc.ServerCall, st store.StoreReader) error {
-	return util.Get(ctx, call, st, r.t.d, &databaseData{})
+	return util.Get(ctx, call, st, r.t, &tableData{})
 }
 
 // get reads data from the storage engine.
 // Performs authorization check.
 // Returns a VDL-compatible error.
-func (r *row) get(ctx *context.T, call rpc.ServerCall, st store.StoreReader) (*vdl.Value, error) {
+func (r *row) get(ctx *context.T, call rpc.ServerCall, st store.StoreReader) ([]byte, error) {
 	if err := r.checkAccess(ctx, call, st); err != nil {
 		return nil, err
 	}
-	value := &vdl.Value{}
-	if err := util.GetObject(st, r.StKey(), value); err != nil {
+	value, err := st.Get([]byte(r.StKey()), nil)
+	if err != nil {
 		if _, ok := err.(*store.ErrUnknownKey); ok {
 			// We've already done an auth check, so here we can safely return NoExist
 			// rather than NoExistOrNoAccess.
@@ -93,11 +92,14 @@
 // put writes data to the storage engine.
 // Performs authorization check.
 // Returns a VDL-compatible error.
-func (r *row) put(ctx *context.T, call rpc.ServerCall, st store.StoreReadWriter, value *vdl.Value) error {
+func (r *row) put(ctx *context.T, call rpc.ServerCall, st store.StoreReadWriter, value []byte) error {
 	if err := r.checkAccess(ctx, call, st); err != nil {
 		return err
 	}
-	return util.Put(ctx, call, st, r, value)
+	if err := st.Put([]byte(r.StKey()), value); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
 }
 
 // del deletes data from the storage engine.
@@ -107,5 +109,8 @@
 	if err := r.checkAccess(ctx, call, st); err != nil {
 		return err
 	}
-	return util.Delete(ctx, call, st, r)
+	if err := st.Delete([]byte(r.StKey())); err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
 }
diff --git a/services/syncbase/server/nosql/table.go b/services/syncbase/server/nosql/table.go
index 1983ba8..67c79a7 100644
--- a/services/syncbase/server/nosql/table.go
+++ b/services/syncbase/server/nosql/table.go
@@ -9,6 +9,7 @@
 	"v.io/syncbase/x/ref/services/syncbase/server/util"
 	"v.io/syncbase/x/ref/services/syncbase/store"
 	"v.io/v23/context"
+	"v.io/v23/naming"
 	"v.io/v23/rpc"
 	"v.io/v23/security/access"
 	"v.io/v23/verror"
@@ -67,23 +68,67 @@
 	})
 }
 
-func (t *table) DeleteRowRange(ctx *context.T, call rpc.ServerCall, start, limit string) error {
+func (t *table) DeleteRowRange(ctx *context.T, call rpc.ServerCall, start, end string) error {
 	return verror.NewErrNotImplemented(ctx)
 }
 
+func (t *table) Scan(ctx *context.T, call wire.TableScanServerCall, start, end string) error {
+	sn := t.d.st.NewSnapshot()
+	defer sn.Close()
+	it, err := sn.Scan(util.ScanRangeArgs(util.JoinKeyParts(util.RowPrefix, t.name), start, end))
+	if err == nil {
+		sender := call.SendStream()
+		key, value := []byte{}, []byte{}
+		for it.Advance() {
+			key = it.Key(key)
+			parts := util.SplitKeyParts(string(key))
+			sender.Send(wire.KeyValue{Key: parts[len(parts)-1], Value: it.Value(value)})
+		}
+		err = it.Err()
+	}
+	if err != nil {
+		return verror.New(verror.ErrInternal, ctx, err)
+	}
+	return nil
+}
+
 func (t *table) SetPermissions(ctx *context.T, call rpc.ServerCall, prefix string, perms access.Permissions) error {
-	return verror.NewErrNotImplemented(ctx)
+	if prefix != "" {
+		return verror.NewErrNotImplemented(ctx)
+	}
+	return store.RunInTransaction(t.d.st, func(st store.StoreReadWriter) error {
+		data := &tableData{}
+		return util.Update(ctx, call, st, t, data, func() error {
+			data.Perms = perms
+			return nil
+		})
+	})
 }
 
 func (t *table) GetPermissions(ctx *context.T, call rpc.ServerCall, key string) ([]wire.PrefixPermissions, error) {
-	return nil, verror.NewErrNotImplemented(ctx)
+	if key != "" {
+		return nil, verror.NewErrNotImplemented(ctx)
+	}
+	data := &tableData{}
+	if err := util.Get(ctx, call, t.d.st, t, data); err != nil {
+		return nil, err
+	}
+	return []wire.PrefixPermissions{{Prefix: "", Perms: data.Perms}}, nil
 }
 
 func (t *table) DeletePermissions(ctx *context.T, call rpc.ServerCall, prefix string) error {
 	return verror.NewErrNotImplemented(ctx)
 }
 
-// TODO(sadovsky): Implement Glob.
+func (t *table) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+	// Check perms.
+	sn := t.d.st.NewSnapshot()
+	if err := util.Get(ctx, call, sn, t, &tableData{}); err != nil {
+		sn.Close()
+		return nil, err
+	}
+	return util.Glob(ctx, call, pattern, sn, util.JoinKeyParts(util.RowPrefix, t.name))
+}
 
 ////////////////////////////////////////
 // util.Layer methods
diff --git a/services/syncbase/server/service.go b/services/syncbase/server/service.go
index e268787..c8ea107 100644
--- a/services/syncbase/server/service.go
+++ b/services/syncbase/server/service.go
@@ -12,6 +12,7 @@
 	"v.io/syncbase/x/ref/services/syncbase/store"
 	"v.io/syncbase/x/ref/services/syncbase/store/memstore"
 	"v.io/v23/context"
+	"v.io/v23/naming"
 	"v.io/v23/rpc"
 	"v.io/v23/security/access"
 	"v.io/v23/verror"
@@ -34,7 +35,7 @@
 // Returns a VDL-compatible error.
 func NewService(ctx *context.T, call rpc.ServerCall, perms access.Permissions) (*service, error) {
 	if perms == nil {
-		return nil, verror.New(verror.ErrInternal, nil, "perms must be specified")
+		return nil, verror.New(verror.ErrInternal, ctx, "perms must be specified")
 	}
 	// TODO(sadovsky): Make storage engine pluggable.
 	s := &service{
@@ -75,7 +76,15 @@
 	return data.Perms, util.FormatVersion(data.Version), nil
 }
 
-// TODO(sadovsky): Implement Glob.
+func (s *service) Glob__(ctx *context.T, call rpc.ServerCall, pattern string) (<-chan naming.GlobReply, error) {
+	// Check perms.
+	sn := s.st.NewSnapshot()
+	if err := util.Get(ctx, call, sn, s, &serviceData{}); err != nil {
+		sn.Close()
+		return nil, err
+	}
+	return util.Glob(ctx, call, pattern, sn, util.AppPrefix)
+}
 
 ////////////////////////////////////////
 // App management methods
diff --git a/services/syncbase/server/util/glob.go b/services/syncbase/server/util/glob.go
new file mode 100644
index 0000000..750f4db
--- /dev/null
+++ b/services/syncbase/server/util/glob.go
@@ -0,0 +1,82 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+	"strings"
+
+	"v.io/syncbase/x/ref/services/syncbase/store"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/rpc"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/glob"
+)
+
+// globPatternToPrefix takes "foo*" and returns "foo". It returns ErrBadArg for
+// inputs that are not valid glob patterns as well as for inputs that are valid
+// glob patterns but not valid prefixes.
+func globPatternToPrefix(pattern string) (string, error) {
+	if _, err := glob.Parse(pattern); err != nil {
+		return "", verror.NewErrBadArg(nil)
+	}
+	if pattern == "" {
+		return "", verror.NewErrBadArg(nil)
+	}
+	if pattern[len(pattern)-1] != '*' {
+		return "", verror.NewErrBadArg(nil)
+	}
+	res := pattern[:len(pattern)-1]
+	// Disallow chars and char sequences that have special meaning in glob, since
+	// our Glob() does not support these.
+	if strings.ContainsAny(res, "/*?[") {
+		return "", verror.NewErrBadArg(nil)
+	}
+	if strings.Contains(res, "...") {
+		return "", verror.NewErrBadArg(nil)
+	}
+	return res, nil
+}
+
+// TODO(sadovsky): It sucks that Glob must be implemented differently from other
+// streaming RPC handlers. I don't have much confidence that I've implemented
+// both types of streaming correctly.
+func Glob(ctx *context.T, call rpc.ServerCall, pattern string, sn store.Snapshot, stKeyPrefix string) (<-chan naming.GlobReply, error) {
+	// TODO(sadovsky): Support glob with non-prefix pattern.
+	prefix, err := globPatternToPrefix(pattern)
+	if err != nil {
+		sn.Close()
+		if verror.ErrorID(err) == verror.ErrBadArg.ID {
+			return nil, verror.NewErrNotImplemented(ctx)
+		} else {
+			return nil, verror.New(verror.ErrInternal, ctx, err)
+		}
+	}
+	// TODO(sadovsky): Does Scan really need to return an error?
+	it, err := sn.Scan(ScanPrefixArgs(stKeyPrefix, prefix))
+	if err != nil {
+		sn.Close()
+		return nil, verror.New(verror.ErrInternal, ctx, err)
+	}
+	ch := make(chan naming.GlobReply)
+	go func() {
+		defer sn.Close()
+		defer close(ch)
+		key := []byte{}
+		for it.Advance() {
+			key = it.Key(key)
+			parts := SplitKeyParts(string(key))
+			ch <- naming.GlobReplyEntry{naming.MountEntry{Name: parts[len(parts)-1]}}
+		}
+		if err := it.Err(); err != nil {
+			vlog.VI(1).Infof("Glob(%q) failed: %v", pattern, err)
+			ch <- naming.GlobReplyError{naming.GlobError{
+				Error: verror.New(verror.ErrInternal, ctx, err),
+			}}
+		}
+	}()
+	return ch, nil
+}
diff --git a/services/syncbase/server/util/glob_test.go b/services/syncbase/server/util/glob_test.go
new file mode 100644
index 0000000..d9c8b71
--- /dev/null
+++ b/services/syncbase/server/util/glob_test.go
@@ -0,0 +1,54 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Note, we use package util rather than util_test so that we can access the
+// unexported name util.globPatternToPrefix.
+
+package util
+
+import (
+	"testing"
+
+	"v.io/v23/verror"
+)
+
+var (
+	errBadArg = string(verror.ErrBadArg.ID)
+)
+
+func TestGlobPatternToPrefix(t *testing.T) {
+	tests := []struct {
+		pattern, prefix string
+	}{
+		{"", errBadArg},
+		{"*", ""},
+		{"foo", errBadArg},
+		{"foo*", "foo"},
+		{"foo/", errBadArg},
+		{"foo/*", errBadArg},
+		{"foo/bar", errBadArg},
+		{"foo/bar*", errBadArg},
+		{".*", "."},
+		{"..*", ".."},
+		{"...*", errBadArg},
+		{"...foo*", errBadArg},
+		{"foo...foo*", errBadArg},
+		{"..foo*", "..foo"},
+		{"foo..foo*", "foo..foo"},
+		{"...", errBadArg},
+		{"*/...", errBadArg},
+		{"foo/...", errBadArg},
+		{"/", errBadArg},
+		{"/*", errBadArg},
+	}
+	for _, test := range tests {
+		prefix, err := globPatternToPrefix(test.pattern)
+		if err != nil {
+			prefix = string(verror.ErrorID(err))
+		}
+		if prefix != test.prefix {
+			t.Errorf("%q: got %q, want %q", test.pattern, prefix, test.prefix)
+		}
+	}
+}
diff --git a/services/syncbase/server/util/key_util.go b/services/syncbase/server/util/key_util.go
index 542a0ce..d5b6e36 100644
--- a/services/syncbase/server/util/key_util.go
+++ b/services/syncbase/server/util/key_util.go
@@ -4,23 +4,34 @@
 
 package util
 
-// Note, some of the code below is copied from
-// v.io/syncbase/v23/syncbase/util/util.go.
-
 import (
-	"regexp"
 	"strings"
+
+	"v.io/syncbase/v23/syncbase/util"
 )
 
-// TODO(sadovsky): Consider loosening. Perhaps model after MySQL:
-// http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
-var keyAtomRegexp *regexp.Regexp = regexp.MustCompile("^[a-zA-Z0-9_.-]+$")
-
-func ValidKeyAtom(s string) bool {
-	return keyAtomRegexp.MatchString(s)
-}
-
+// JoinKeyParts builds keys for accessing data in the storage engine.
 func JoinKeyParts(parts ...string) string {
 	// TODO(sadovsky): Figure out which delimeter makes the most sense.
 	return strings.Join(parts, ":")
 }
+
+// SplitKeyParts is the inverse of JoinKeyParts.
+func SplitKeyParts(key string) []string {
+	return strings.Split(key, ":")
+}
+
+// ScanPrefixArgs returns args for sn.Scan() for the specified prefix.
+func ScanPrefixArgs(stKeyPrefix, prefix string) ([]byte, []byte) {
+	return ScanRangeArgs(stKeyPrefix, util.PrefixRangeStart(prefix), util.PrefixRangeEnd(prefix))
+}
+
+// ScanRangeArgs returns args for sn.Scan() for the specified range.
+// If end is "", all rows with keys >= start are included.
+func ScanRangeArgs(stKeyPrefix, start, end string) ([]byte, []byte) {
+	fullStart, fullEnd := JoinKeyParts(stKeyPrefix, start), JoinKeyParts(stKeyPrefix, end)
+	if end == "" {
+		fullEnd = util.PrefixRangeEnd(fullEnd)
+	}
+	return []byte(fullStart), []byte(fullEnd)
+}
diff --git a/services/syncbase/server/util/key_util_test.go b/services/syncbase/server/util/key_util_test.go
new file mode 100644
index 0000000..1bc1148
--- /dev/null
+++ b/services/syncbase/server/util/key_util_test.go
@@ -0,0 +1,83 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util_test
+
+import (
+	"reflect"
+	"testing"
+
+	"v.io/syncbase/x/ref/services/syncbase/server/util"
+)
+
+type kpt struct {
+	parts []string
+	key   string
+}
+
+var keyPartTests []kpt = []kpt{
+	{[]string{"a", "b"}, "a:b"},
+	{[]string{"aa", "bb"}, "aa:bb"},
+	{[]string{"a", "b", "c"}, "a:b:c"},
+}
+
+func TestJoinKeyParts(t *testing.T) {
+	for _, test := range keyPartTests {
+		got, want := util.JoinKeyParts(test.parts...), test.key
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("%v: got %q, want %q", test.parts, got, want)
+		}
+	}
+}
+
+func TestSplitKeyParts(t *testing.T) {
+	for _, test := range keyPartTests {
+		got, want := util.SplitKeyParts(test.key), test.parts
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("%q: got %v, want %v", test.key, got, want)
+		}
+	}
+}
+
+func TestScanPrefixArgs(t *testing.T) {
+	tests := []struct {
+		stKeyPrefix, prefix, wantStart, wantEnd string
+	}{
+		{"x", "", "x:", "x;"},
+		{"x", "a", "x:a", "x:b"},
+		{"x", "a\xff", "x:a\xff", "x:b"},
+	}
+	for _, test := range tests {
+		start, end := util.ScanPrefixArgs(test.stKeyPrefix, test.prefix)
+		gotStart, gotEnd := string(start), string(end)
+		if gotStart != test.wantStart {
+			t.Errorf("{%q, %q} start: got %q, want %q", test.stKeyPrefix, test.prefix, gotStart, test.wantStart)
+		}
+		if gotEnd != test.wantEnd {
+			t.Errorf("{%q, %q} end: got %q, want %q", test.stKeyPrefix, test.prefix, gotEnd, test.wantEnd)
+		}
+	}
+}
+
+func TestScanRangeArgs(t *testing.T) {
+	tests := []struct {
+		stKeyPrefix, start, end, wantStart, wantEnd string
+	}{
+		{"x", "", "", "x:", "x;"},   // end "" means "no limit"
+		{"x", "a", "", "x:a", "x;"}, // end "" means "no limit"
+		{"x", "a", "b", "x:a", "x:b"},
+		{"x", "a", "a", "x:a", "x:a"}, // empty range
+		{"x", "b", "a", "x:b", "x:a"}, // empty range
+	}
+	for _, test := range tests {
+		start, end := util.ScanRangeArgs(test.stKeyPrefix, test.start, test.end)
+		gotStart, gotEnd := string(start), string(end)
+		if gotStart != test.wantStart {
+			t.Errorf("{%q, %q, %q} start: got %q, want %q", test.stKeyPrefix, test.start, test.end, gotStart, test.wantStart)
+		}
+		if gotEnd != test.wantEnd {
+			t.Errorf("{%q, %q, %q} end: got %q, want %q", test.stKeyPrefix, test.start, test.end, gotEnd, test.wantEnd)
+		}
+	}
+}