Java: syncbase server implementation and client-server tests

Change-Id: I0a45e357864ac3bbaba54bb598f5352fdf127416
diff --git a/lib/build.gradle b/lib/build.gradle
index ccb6d77..02aed5f 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -1,14 +1,3 @@
-buildscript {
-    repositories {
-        mavenCentral()
-    }
-
-    dependencies {
-        classpath 'com.android.tools.build:gradle:1.2.3'
-        classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
-    }
-}
-
 apply plugin: 'java'
 
 repositories {
@@ -26,6 +15,30 @@
     testCompile group: 'com.google.truth', name: 'truth', version: '0.25'
 }
 
+clean {
+    delete 'generated-src'
+}
+
+public static isDarwin() {
+    return getOS().contains("os x")
+}
+
+public static isLinux() {
+    return getOS().contains("linux")
+}
+
+public static isAmd64() {
+    return getArch().contains("x86_64") || getArch().contains("amd64")
+}
+
+public static getArch() {
+    return System.properties['os.arch'].toLowerCase()
+}
+
+public static getOS() {
+    return System.properties['os.name'].toLowerCase()
+}
+
 class VanadiumEnvironment {
     File v23Root;
     File v23Bin;
@@ -59,6 +72,25 @@
 
 sourceSets.main.java.srcDirs += 'generated-src/vdl'
 
+task checkVanadiumEnvironment {
+    VanadiumEnvironment.getVanadiumEnvironment()
+
+    if (System.getenv('JAVA_HOME') == null) {
+        throw new InvalidUserDataException("The JAVA_HOME environment variable is not set. "
+                + "Please set it to the root of a JDK installation directory.  If JDK isn't "
+                + "installed, you probably didn't install the java profile: try running\n\n"
+                + "v23 profile install java\n\nand then try building again.")
+    }
+    if (!isAmd64()) {
+        throw new InvalidUserDataException("Java Vanadium builds only enabled on amd64 "
+                + "architectures, not: " + getArch())
+    }
+    if (!isLinux() && !isDarwin()) {
+        throw new InvalidUserDataException("Java Vanadium builds only enabled on "
+                + "linux/darwin systems, not: " + getOS())
+    }
+}
+
 task buildVdlTool(type: Exec) {
     commandLine v23Bin, 'go', 'install', 'v.io/x/ref/cmd/vdl'
 }
@@ -68,4 +100,46 @@
     commandLine vdlBin, 'generate', '--lang=java', '--java-out-dir=generated-src/vdl', 'all'
 }
 
-tasks.'compileJava'.dependsOn(generateVdl)
\ No newline at end of file
+tasks.'compileJava'.dependsOn(generateVdl)
+
+task goBuildVanadiumLib(type: Exec, dependsOn: checkVanadiumEnvironment) {
+    def env = VanadiumEnvironment.getVanadiumEnvironment()
+    def existingPath = System.getenv('PATH')
+    if (existingPath == null) {
+        existingPath = ""
+    }
+
+    environment 'V23_PROFILE': 'java'
+    commandLine env.v23Bin.getAbsolutePath(), 'go', 'install',
+            '-buildmode=c-shared', '-v', '-tags', 'java', 'v.io/syncbase/jni/main'
+}
+
+// Copy the shared library to its ultimate destination.
+if (isLinux()) {
+    task copyVanadiumLib(type: Copy, dependsOn: goBuildVanadiumLib) {
+        from v23Root + '/roadmap/go/pkg/linux_amd64_shared/v.io/syncbase/jni'
+        into 'build/libs'
+        include 'main.a'
+        rename 'main.a', 'libv23.so'
+    }
+} else {  // darwin
+    task copyVanadiumLib(type: Copy, dependsOn: goBuildVanadiumLib) {
+        from v23Root + '/roadmap/go/pkg/darwin_amd64/v.io/syncbase/jni'
+        into 'build/libs'
+        include 'main.a'
+        rename 'main.a', 'libv23.dylib'
+    }
+}
+
+// Add shared library dependency to our tests.
+tasks.withType(Test) {
+    if (isDarwin()) {
+        // TODO(sjr): remove these when
+        // https://github.com/vanadium/issues/issues/567 is resolved.
+        jvmArgs '-XX:+UnlockDiagnosticVMOptions'
+        jvmArgs '-XX:-LogEvents'
+    }
+    systemProperty "java.library.path", "build/libs"
+}
+
+tasks.'processTestResources'.dependsOn(copyVanadiumLib)
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/impl/google/services/syncbase/syncbased/SyncbaseServer.java b/lib/src/main/java/io/v/impl/google/services/syncbase/syncbased/SyncbaseServer.java
new file mode 100644
index 0000000..c156afc
--- /dev/null
+++ b/lib/src/main/java/io/v/impl/google/services/syncbase/syncbased/SyncbaseServer.java
@@ -0,0 +1,53 @@
+package io.v.impl.google.services.syncbase.syncbased;
+
+import io.v.v23.V;
+import io.v.v23.context.VContext;
+import io.v.syncbase.v23.services.syncbase.SyncbaseServerParams;
+import io.v.syncbase.v23.services.syncbase.SyncbaseServerStartException;
+import io.v.v23.rpc.Server;
+import io.v.v23.verror.VException;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * An implementation of a syncbase server.
+ */
+public class SyncbaseServer {
+    private static boolean initOnceDone = false;
+
+    private static native void nativeInit() throws VException;
+    private static native Server nativeStart(VContext ctx, SyncbaseServerParams params)
+            throws VException;
+
+    private static synchronized void initOnce() {
+        if (initOnceDone) {
+            return;
+        }
+        V.init();
+        try {
+            nativeInit();
+        } catch (VException e) {
+            throw new RuntimeException("Couldn't initialize syncbase native code.", e);
+        }
+        initOnceDone = true;
+    }
+
+    /**
+     * Starts the syncbase server with the given parameters.
+     * <p>
+     * This is a non-blocking call.
+     *
+     * @param params                         syncbase starting parameters
+     * @throws SyncbaseServerStartException  if there was an error starting the syncbase service
+     * @return                               vanadium server
+     */
+    public static Server start(SyncbaseServerParams params) throws SyncbaseServerStartException {
+        initOnce();
+        VContext ctx = V.init();
+        try {
+            return nativeStart(ctx, params);
+        } catch (VException e) {
+            throw new SyncbaseServerStartException(e.getMessage());
+        }
+    }
+}
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/Syncbase.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/Syncbase.java
index 80af359..230cf7d 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/Syncbase.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/Syncbase.java
@@ -4,12 +4,15 @@
 
 package io.v.syncbase.v23.services.syncbase;
 
+import io.v.impl.google.services.syncbase.syncbased.SyncbaseServer;
+import io.v.v23.rpc.Server;
+
 /**
  * Various syncbase utility methods.
  */
 public class Syncbase {
     /**
-     * Returns a new handle to a syncbase service running at the given name.
+     * Returns a new client handle to a syncbase service running at the given name.
      *
      * @param  fullName full (i.e., object) name of the syncbase service
      */
@@ -17,5 +20,20 @@
         return new SyncbaseServiceImpl(fullName);
     }
 
+    /**
+     * Starts the syncbase server with the given parameters.
+     * <p>
+     * This is a non-blocking call.
+     *
+     * @param params                         syncbase starting parameters
+     * @throws SyncbaseServerStartException  if there was an error starting the syncbase service
+     * @return                               vanadium server
+     */
+    public static Server startServer(SyncbaseServerParams params)
+            throws SyncbaseServerStartException {
+        // TODO(spetrovic): allow clients to pass in their own Server implementations.
+        return SyncbaseServer.start(params);
+    }
+
     private Syncbase() {}
 }
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseApp.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseApp.java
index 4d0be37..84fa6c1 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseApp.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseApp.java
@@ -50,7 +50,7 @@
     Database getNoSqlDatabase(String relativeName, Schema schema);
 
     /**
-     * Returns a list of all database names.
+     * Returns a list of all (relative) database names.
      *
      * @param  ctx        Vanadium context
      * @throws VException if the list of database names couldn't be retrieved
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseAppImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseAppImpl.java
index 50f428e..b69fca4 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseAppImpl.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseAppImpl.java
@@ -15,7 +15,6 @@
 import io.v.syncbase.v23.services.syncbase.util.Util;
 import io.v.v23.context.VContext;
 import io.v.v23.security.access.Permissions;
-import io.v.v23.services.permissions.ObjectClient.GetPermissionsOut;
 import io.v.v23.verror.VException;
 
 class SyncbaseAppImpl implements SyncbaseApp {
@@ -63,7 +62,7 @@
     }
     @Override
     public Map<String, Permissions> getPermissions(VContext ctx) throws VException {
-        GetPermissionsOut perms = this.client.getPermissions(ctx);
+        ServiceClient.GetPermissionsOut perms = this.client.getPermissions(ctx);
         return ImmutableMap.of(perms.version, perms.perms);
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerParams.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerParams.java
new file mode 100644
index 0000000..47d97e4
--- /dev/null
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerParams.java
@@ -0,0 +1,129 @@
+package io.v.syncbase.v23.services.syncbase;
+
+import io.v.v23.rpc.ListenSpec;
+import io.v.v23.security.access.Permissions;
+
+/**
+ * Parameters used when starting a syncbase service.  Here is an example of a simple
+ * parameter creation:
+ * <p><blockquote><pre>
+ *     SyncbaseServerParams params = new SyncbaseServerParams()
+ *           .withListenSpec(V.getListenSpec(ctx))
+ *           .withName("test")
+ *           .withStorageEngine(SyncbaseStorageEngine.LEVELDB);
+ *     Syncbase.startServer(params);
+ * </pre></blockquote><p>
+ * {@link SyncbaseServerParams} form a tree where derived params are children of the params from
+ * which they were derived.  Children inherit all the properties of their parent except for the
+ * property being replaced (the listenSpec/name/storageEngine in the example above).
+ */
+public class SyncbaseServerParams {
+    private SyncbaseServerParams parent = null;
+
+    private Permissions permissions;
+    private ListenSpec listenSpec;
+    private String name;
+    private String storageRootDir;
+    private SyncbaseStorageEngine storageEngine;
+
+    /**
+     * Creates a new (and empty) {@link SyncbaseServerParams} object.
+     */
+    public SyncbaseServerParams() {
+    }
+
+    private SyncbaseServerParams(SyncbaseServerParams parent) {
+        this.parent = parent;
+    }
+
+    /**
+     * Returns a child of the current params with the given permissions.
+     */
+    public SyncbaseServerParams withPermissions(Permissions permissions) {
+        SyncbaseServerParams ret = new SyncbaseServerParams(this);
+        ret.permissions = permissions;
+        return ret;
+    }
+
+    /**
+     * Returns a child of the current params with the given {@link ListenSpec}.
+     */
+    public SyncbaseServerParams withListenSpec(ListenSpec spec) {
+        SyncbaseServerParams ret = new SyncbaseServerParams(this);
+        ret.listenSpec = spec;
+        return ret;
+    }
+
+    /**
+     * Returns a child of the current params with the given mount name.
+     */
+    public SyncbaseServerParams withName(String name) {
+        SyncbaseServerParams ret = new SyncbaseServerParams(this);
+        ret.name = name;
+        return ret;
+    }
+
+    /**
+     * Returns a child of the current params with the given storage root directory.
+     */
+    public SyncbaseServerParams withStorageRootDir(String rootDir) {
+        SyncbaseServerParams ret = new SyncbaseServerParams(this);
+        ret.storageRootDir = rootDir;
+        return ret;
+    }
+
+    /**
+     * Returns a child of the current params with the given storage engine.
+     */
+    public SyncbaseServerParams withStorageEngine(SyncbaseStorageEngine engine) {
+        SyncbaseServerParams ret = new SyncbaseServerParams(this);
+        ret.storageEngine = engine;
+        return ret;
+    }
+
+    /**
+     * Returns permissions that the syncbase service will be started with.
+     */
+    public Permissions getPermissions() {
+        if (this.permissions != null) return this.permissions;
+        if (this.parent != null) return this.parent.getPermissions();
+        return null;
+    }
+
+    /**
+     * Returns a {@ListenSpec} that the service will listen on.
+     */
+
+    public ListenSpec getListenSpec() {
+        if (this.listenSpec != null) return this.listenSpec;
+        if (this.parent != null) return this.parent.getListenSpec();
+        return null;
+    }
+
+    /**
+     * Returns a name that the service will mount itself on.
+     */
+    public String getName() {
+        if (this.name != null) return this.name;
+        if (this.parent != null) return this.parent.getName();
+        return null;
+    }
+
+    /**
+     * Returns a root directory for all of the service's storage files.
+     */
+    public String getStorageRootDir() {
+        if (this.storageRootDir != null) return this.storageRootDir;
+        if (this.parent != null) return this.parent.getStorageRootDir();
+        return null;
+    }
+
+    /**
+     * Returns a storage engine for the service.
+     */
+    public SyncbaseStorageEngine getStorageEngine() {
+        if (this.storageEngine != null) return this.storageEngine;
+        if (this.parent != null) return this.parent.getStorageEngine();
+        return null;
+    }
+}
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerStartException.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerStartException.java
new file mode 100644
index 0000000..ef31e95
--- /dev/null
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServerStartException.java
@@ -0,0 +1,10 @@
+package io.v.syncbase.v23.services.syncbase;
+
+/**
+ * Exception thrown if the syncbase server couldn't be started.
+ */
+public class SyncbaseServerStartException extends Exception {
+    public SyncbaseServerStartException(String msg) {
+        super(msg);
+    }
+}
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseService.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseService.java
index 62d2bb5..4dcad34 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseService.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseService.java
@@ -28,7 +28,7 @@
     SyncbaseApp getApp(String relativeName);
 
     /**
-     * Returns a list of all app names.
+     * Returns a list of all relative app names.
      *
      * @param  ctx        Vanadium context
      * @throws VException if the list of app names couldn't be retrieved
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServiceImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServiceImpl.java
index 32bfb44..e250040 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServiceImpl.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseServiceImpl.java
@@ -11,7 +11,6 @@
 import io.v.syncbase.v23.services.syncbase.util.Util;
 import io.v.v23.context.VContext;
 import io.v.v23.security.access.Permissions;
-import io.v.v23.services.permissions.ObjectClient.GetPermissionsOut;
 import io.v.v23.verror.VException;
 
 class SyncbaseServiceImpl implements SyncbaseService {
@@ -41,7 +40,7 @@
     }
     @Override
     public Map<String, Permissions> getPermissions(VContext ctx) throws VException {
-        GetPermissionsOut perms = this.client.getPermissions(ctx);
+        ServiceClient.GetPermissionsOut perms = this.client.getPermissions(ctx);
         return ImmutableMap.of(perms.version, perms.perms);
     }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseStorageEngine.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseStorageEngine.java
new file mode 100644
index 0000000..0c5359c
--- /dev/null
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/SyncbaseStorageEngine.java
@@ -0,0 +1,22 @@
+package io.v.syncbase.v23.services.syncbase;
+
+/**
+ * Storage engine used for storing the syncbase data.
+ */
+public enum SyncbaseStorageEngine {
+    LEVELDB   ("leveldb"),
+    MEMSTORE  ("memstore");
+
+    private final String value;
+
+    SyncbaseStorageEngine(String value) {
+        this.value = value;
+    }
+
+    /**
+     * Returns the {@link String} value corresponding to this {@link SyncbaseStorageEngine}.
+     */
+    public String getValue() {
+        return this.value;
+    }
+}
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Database.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Database.java
index ec2aa98..774ac48 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Database.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Database.java
@@ -96,7 +96,7 @@
     BatchDatabase beginBatch(VContext ctx, BatchOptions opts) throws VException;
 
     /**
-     * Returns a handle to a database {@link SyncGroup} with the given name.
+     * Returns a handle to a database {@link SyncGroup} with the given full (i.e., object) name.
      *
      * @param  name name of the synchronization group
      */
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/DatabaseImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/DatabaseImpl.java
index 9e185de..99d3c76 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/DatabaseImpl.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/DatabaseImpl.java
@@ -6,18 +6,19 @@
 
 import java.io.EOFException;
 import java.io.Serializable;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+
 import com.google.common.collect.ImmutableMap;
 
 import io.v.impl.google.naming.NamingUtil;
-import io.v.syncbase.v23.services.syncbase.nosql.Schema;
 import io.v.syncbase.v23.services.syncbase.util.Util;
 import io.v.v23.context.CancelableVContext;
 import io.v.v23.context.VContext;
 import io.v.v23.security.access.Permissions;
-import io.v.v23.services.permissions.ObjectClient.GetPermissionsOut;
 import io.v.v23.vdl.TypedClientStream;
+import io.v.v23.vdl.Types;
 import io.v.v23.vdl.VdlAny;
 import io.v.v23.vdl.VdlOptional;
 import io.v.v23.verror.VException;
@@ -77,7 +78,7 @@
                         "names (of type String), got type: " + elem.getClass());
             }
         }
-        return new ResultStreamImpl(ctxC, stream, columnNames);
+        return new ResultStreamImpl(ctxC, stream, Arrays.asList(columnNames));
     }
 
     // Implements AccessController interface.
@@ -87,7 +88,7 @@
     }
     @Override
     public Map<String, Permissions> getPermissions(VContext ctx) throws VException {
-        GetPermissionsOut perms = this.client.getPermissions(ctx);
+        DatabaseClient.GetPermissionsOut perms = this.client.getPermissions(ctx);
         return ImmutableMap.of(perms.version, perms.perms);
     }
 
@@ -98,8 +99,10 @@
     }
     @Override
     public void create(VContext ctx, Permissions perms) throws VException {
-        SchemaMetadata metadata = this.schema != null ? this.schema.getMetadata() : null;
-        this.client.create(ctx, VdlOptional.of(metadata), perms);
+        VdlOptional metadataOpt = this.schema != null
+                ? VdlOptional.of(this.schema.getMetadata())
+                : new VdlOptional<SchemaMetadata>(Types.optionalOf(SchemaMetadata.VDL_TYPE));
+        this.client.create(ctx, metadataOpt, perms);
     }
     @Override
     public void delete(VContext ctx) throws VException {
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/NoSql.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/NoSql.java
index 4540823..cb7c21a 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/NoSql.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/NoSql.java
@@ -16,27 +16,6 @@
     }
 
     /**
-     * Creates a {@link RowRange} representing a single row.
-     */
-    public static RowRange newSingleRowRange(String row) {
-        return new RowRangeImpl(row);
-    }
-
-    /**
-     * Creates a {@link RowRange} represented by the provided {@code [start, limit)} parameters.
-     */
-    public static RowRange newRowRange(String start, String limit) {
-        return new RowRangeImpl(start, limit);
-    }
-
-    /**
-     * Creates {@link PrefixRange} with the provided prefix.
-     */
-    public static PrefixRange newPrefixRange(String prefix) {
-        return new PrefixRangeImpl(prefix);
-    }
-
-    /**
      * Interface for a batch operation that is executed as part of {@link #runInBatch runInBatch()}.
      */
     public static interface BatchOperation {
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRange.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRange.java
index 4745827..04e5793 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRange.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRange.java
@@ -4,12 +4,26 @@
 
 package io.v.syncbase.v23.services.syncbase.nosql;
 
+import io.v.syncbase.v23.services.syncbase.util.Util;
+
 /**
  * Represents all rows with keys that have some prefix.
  */
-public interface PrefixRange extends RowRange {
+public class PrefixRange extends RowRange {
+    private final String prefix;
+
+    /**
+     * Creates a new prefix range that includes all rows with the given prefix.
+     */
+    public PrefixRange(String prefix) {
+        super(Util.prefixRangeStart(prefix), Util.prefixRangeLimit(prefix));
+        this.prefix = prefix;
+    }
+
     /**
      * Returns the prefix shared by all the keys in the range.
      */
-    String getPrefix();
+    public String getPrefix() {
+        return this.prefix;
+    }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeImpl.java
deleted file mode 100644
index 80b9546..0000000
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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 io.v.syncbase.v23.services.syncbase.nosql;
-
-import io.v.syncbase.v23.services.syncbase.util.Util;
-
-class PrefixRangeImpl implements PrefixRange {
-    private final String prefix;
-
-    PrefixRangeImpl(String prefix) {
-        this.prefix = prefix;
-    }
-
-    @Override
-    public String getStart() {
-        return Util.prefixRangeStart(prefix);
-    }
-
-    @Override
-    public String getLimit() {
-        return Util.prefixRangeLimit(prefix);
-    }
-
-    @Override
-    public String getPrefix() {
-        return this.prefix;
-    }
-}
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStream.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStream.java
index e106e36..b87fae0 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStream.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStream.java
@@ -7,6 +7,8 @@
 import io.v.v23.vdl.VdlAny;
 import io.v.v23.verror.VException;
 
+import java.util.List;
+
 /**
  * An interface for iterating through rows resulting from a
  * {@link DatabaseCore#exec DatabaseCore.exec()}.
@@ -20,12 +22,12 @@
  *         {@link RuntimeException} will be the said {@link VException}.</li>
  * </ul>
  */
-public interface ResultStream extends Iterable<VdlAny[]> {
+public interface ResultStream extends Iterable<List<VdlAny>> {
     /**
      * Returns an array of column names that matched the query.  The size of the {@link VdlAny}
-     * array returned in every iteration will match the size of this array.
+     * list returned in every iteration will match the size of this array.
      */
-    String[] columnNames();
+    List<String> columnNames();
 
     /**
      * Notifies the stream provider that it can stop producing elements.  The client must call
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStreamImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStreamImpl.java
index bb33b9f..4179c89 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStreamImpl.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/ResultStreamImpl.java
@@ -18,12 +18,12 @@
 class ResultStreamImpl implements ResultStream {
     private final CancelableVContext ctxC;
     private final TypedClientStream<Void, List<VdlAny>, Void> stream;
-    private final String[] columnNames;
+    private final List<String> columnNames;
     private volatile boolean isCanceled;
     private volatile boolean isCreated;
 
     ResultStreamImpl(CancelableVContext ctxC, TypedClientStream<Void, List<VdlAny>, Void> stream,
-            String[] columnNames) {
+            List<String> columnNames) {
         this.ctxC = ctxC;
         this.stream = stream;
         this.columnNames = columnNames;
@@ -31,21 +31,20 @@
     }
     // Implements Iterable.
     @Override
-    public synchronized Iterator<VdlAny[]> iterator() {
+    public synchronized Iterator<List<VdlAny>> iterator() {
         if (this.isCreated) {
             throw new RuntimeException("Can only create one ResultStream iterator.");
         }
         this.isCreated = true;
-        return new AbstractIterator<VdlAny[]>() {
+        return new AbstractIterator<List<VdlAny>>() {
             @Override
-            protected VdlAny[] computeNext() {
+            protected List<VdlAny> computeNext() {
                 synchronized (ResultStreamImpl.this) {
                     if (ResultStreamImpl.this.isCanceled) {  // client canceled the stream
                         return endOfData();
                     }
                     try {
-                        List<VdlAny> result = ResultStreamImpl.this.stream.recv();
-                        return result.toArray(new VdlAny[result.size()]);
+                        return ResultStreamImpl.this.stream.recv();
                     } catch (EOFException e) {  // legitimate end of stream
                         return endOfData();
                     } catch (VException e) {
@@ -58,7 +57,7 @@
 
     // Implements ResultStream.
     @Override
-    public String[] columnNames() {
+    public List<String> columnNames() {
         return this.columnNames;
     }
     @Override
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Row.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Row.java
index 4ba462f..cddb2ad 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Row.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Row.java
@@ -41,9 +41,12 @@
 
     /**
      * Returns the value for this row.
+     * <p>
+     * Throws a {@link VException} if the row doesn't exist.
      *
      * @param  ctx        Vanadium context
-     * @throws VException if the value couldn't be retrieved
+     * @throws VException if the value couldn't be retrieved or if its type doesn't match the
+     *                    provided type
      */
     Object get(VContext ctx, Type type) throws VException;
 
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowImpl.java
index 0451dd8..4e8cfa3 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowImpl.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowImpl.java
@@ -7,7 +7,6 @@
 import java.lang.reflect.Type;
 
 import io.v.impl.google.naming.NamingUtil;
-import io.v.syncbase.v23.services.syncbase.util.Util;
 import io.v.v23.context.VContext;
 import io.v.v23.verror.VException;
 import io.v.v23.vom.VomUtil;
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRange.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRange.java
index 7de840d..194bdb4 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRange.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRange.java
@@ -4,18 +4,46 @@
 
 package io.v.syncbase.v23.services.syncbase.nosql;
 
+import io.v.syncbase.v23.services.syncbase.util.Util;
+
 /**
  * Represents all rows with keys in {@code [start, limit)}.  If limit is {@code ""}, all rows with
  * keys &ge; {@code start} are included.
  */
-public interface RowRange {
+public class RowRange {
+    private final String start, limit;
+
+    /**
+     * Creates a new row range with keys in {@code [start, limit)}.
+     */
+    public RowRange(String start, String limit) {
+        this.start = start;
+        this.limit = limit;
+    }
+
+    /**
+     * Creates a row range containing a single {@code row}.
+     */
+    public RowRange(String row) {
+        this.start = row;
+        this.limit = row + "\u0000";
+    }
+
+    /**
+     * Returns {@code true} iff the provided row is inside this range.
+     */
+    public boolean isWithin(String row) {
+        return this.start.compareTo(row) <= 0 &&
+                (this.limit.isEmpty() || this.limit.compareTo(row) > 0);
+    }
+
     /**
      * Returns the key that marks the start of the row range.
      */
-    public String getStart();
+    public String getStart() { return this.start; }
 
     /**
      * Returns the key that marks the limit of the row range.
      */
-    public String getLimit();
+    public String getLimit() { return this.limit; }
 }
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeImpl.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeImpl.java
deleted file mode 100644
index 3359cf3..0000000
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeImpl.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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 io.v.syncbase.v23.services.syncbase.nosql;
-
-class RowRangeImpl implements RowRange {
-    private final String start, limit;
-
-    RowRangeImpl(String start, String limit) {
-        this.start = start;
-        this.limit = limit;
-    }
-
-    RowRangeImpl(String row) {
-        this.start = this.limit = row;
-    }
-
-    @Override
-    public String getStart() { return this.start; }
-
-    @Override
-    public String getLimit() { return this.limit; }
-}
\ No newline at end of file
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Table.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Table.java
index ee598a6..d281284 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Table.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/nosql/Table.java
@@ -42,12 +42,14 @@
 
     /**
      * Returns the value for the given primary key.
+     * <p>
+     * Throws a {@link VException} if the row doesn't exist.
      *
      * @param  ctx        Vanadium context
      * @param  key        the primary key for a row
      * @param  type       type of the value to be returned (needed for de-serialization)
-     * @throws VException if the value couldn't be retrieved or if its class doesn't match the
-     *                    provided class
+     * @throws VException if the value couldn't be retrieved or if its type doesn't match the
+     *                    provided type
      */
     Object get(VContext ctx, String key, Type type) throws VException;
 
@@ -80,10 +82,6 @@
      * reads from a consistent snapshot taken at the time of the method and will not reflect
      * subsequent writes to keys not yet reached by the stream.
      *
-     * See helpers {@link NoSql#newPrefixRange NoSql.newPrefixRange()},
-     * {@link NoSql#newRowRange NoSql.newRowRange()},
-     * and {@link NoSql#newSingleRowRange NoSql.newSingleRowRange()}.
-     *
      * @param  ctx         Vanadium context
      * @param  range       range of rows to be read
      * @return             a {@link ScanStream} used for iterating over the snapshot of the
diff --git a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/util/Util.java b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/util/Util.java
index 9bb34f2..ed4d988 100644
--- a/lib/src/main/java/io/v/syncbase/v23/services/syncbase/util/Util.java
+++ b/lib/src/main/java/io/v/syncbase/v23/services/syncbase/util/Util.java
@@ -6,15 +6,12 @@
 
 import com.google.common.base.Charsets;
 
-import java.io.EOFException;
 import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
 
 import io.v.impl.google.naming.NamingUtil;
-import io.v.v23.InputChannel;
 import io.v.v23.V;
 import io.v.v23.context.VContext;
 import io.v.v23.namespace.Namespace;
@@ -66,12 +63,11 @@
      */
     public static String[] list(VContext ctx, String globName) throws VException {
         Namespace n = V.getNamespace(ctx);
-        InputChannel<GlobReply> chan = n.glob(ctx, NamingUtil.join(globName, "*"));
         ArrayList<String> names = new ArrayList<String>();
         try {
-            for (GlobReply reply : chan) {
+            for (GlobReply reply : n.glob(ctx, NamingUtil.join(globName, "*"))) {
                 if (reply instanceof GlobReply.Entry) {
-                    String fullName = ((GlobReply.Entry) reply).getName();
+                    String fullName = ((GlobReply.Entry) reply).getElem().getName();
                     // NOTE(nlacasse): The names that come back from Glob are all
                     // rooted.  We only want the last part of the name, so we must chop
                     // off everything before the final '/'.  Since endpoints can
diff --git a/lib/src/test/java/io/v/syncbase/v23/services/syncbase/SyncbaseTest.java b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/SyncbaseTest.java
new file mode 100644
index 0000000..b65aef0
--- /dev/null
+++ b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/SyncbaseTest.java
@@ -0,0 +1,373 @@
+// 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 io.v.syncbase.v23.services.syncbase;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Files;
+import io.v.impl.google.naming.NamingUtil;
+import io.v.syncbase.v23.services.syncbase.nosql.BatchDatabase;
+import io.v.syncbase.v23.services.syncbase.nosql.Database;
+import io.v.syncbase.v23.services.syncbase.nosql.KeyValue;
+import io.v.syncbase.v23.services.syncbase.nosql.PrefixRange;
+import io.v.syncbase.v23.services.syncbase.nosql.ResultStream;
+import io.v.syncbase.v23.services.syncbase.nosql.Row;
+import io.v.syncbase.v23.services.syncbase.nosql.RowRange;
+import io.v.syncbase.v23.services.syncbase.nosql.SyncGroup;
+import io.v.syncbase.v23.services.syncbase.nosql.SyncGroupMemberInfo;
+import io.v.syncbase.v23.services.syncbase.nosql.SyncGroupSpec;
+import io.v.syncbase.v23.services.syncbase.nosql.Table;
+import io.v.v23.V;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.access.AccessList;
+import io.v.v23.security.access.Constants;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.vdl.VdlAny;
+import io.v.v23.verror.VException;
+import io.v.v23.vom.VomUtil;
+import junit.framework.TestCase;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Client-server syncbase tests.
+ */
+public class SyncbaseTest extends TestCase {
+    private static final String APP_NAME = "app";
+    private static final String DB_NAME = "db";
+    private static final String TABLE_NAME = "table";
+    private static final String ROW_NAME = "row";
+
+    private VContext ctx;
+    private Permissions allowAll;
+    private Server server;
+    private String serverName;
+
+    @Override
+    protected void setUp() throws Exception {
+        ctx = V.init();
+        AccessList acl = new AccessList(
+                ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
+        allowAll = new Permissions(ImmutableMap.of(
+                Constants.READ.getValue(), acl,
+                Constants.WRITE.getValue(), acl,
+                Constants.ADMIN.getValue(), acl));
+        String tmpDir = Files.createTempDir().getAbsolutePath();
+        server = Syncbase.startServer(new SyncbaseServerParams()
+                .withPermissions(allowAll)
+                .withStorageRootDir(tmpDir));
+        String[] endpoints = server.getStatus().getEndpoints();
+        assertThat(endpoints).isNotEmpty();
+        serverName = "/" + endpoints[0];
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (server != null) {
+            server.stop();
+        }
+        V.shutdown();
+    }
+
+    public void testService() throws Exception {
+        SyncbaseService service = createService();
+        assertThat(service.fullName()).isEqualTo(serverName);
+        assertThat(service.listApps(ctx)).isEmpty();
+    }
+
+    public void testApp() throws Exception {
+        SyncbaseService service = createService();
+        SyncbaseApp app = service.getApp(APP_NAME);
+        assertThat(app).isNotNull();
+        assertThat(app.name()).isEqualTo(APP_NAME);
+        assertThat(app.fullName()).is(NamingUtil.join(serverName, APP_NAME));
+        assertThat(app.exists(ctx)).isFalse();
+        assertThat(service.listApps(ctx)).isEmpty();
+        app.create(ctx, allowAll);
+        assertThat(app.exists(ctx)).isTrue();
+        assertThat(Arrays.asList(service.listApps(ctx))).containsExactly(app.name());
+        assertThat(app.listDatabases(ctx)).isEmpty();
+        app.delete(ctx);
+        assertThat(app.exists(ctx)).isFalse();
+        assertThat(service.listApps(ctx)).isEmpty();
+    }
+
+    public void testDatabase() throws Exception {
+        SyncbaseApp app = createApp(createService());
+        assertThat(app).isNotNull();
+        Database db = app.getNoSqlDatabase("db", null);
+        assertThat(db).isNotNull();
+        assertThat(db.name()).isEqualTo(DB_NAME);
+        assertThat(db.fullName()).isEqualTo(NamingUtil.join(serverName, APP_NAME, DB_NAME));
+        assertThat(db.exists(ctx)).isFalse();
+        assertThat(app.listDatabases(ctx)).isEmpty();
+        db.create(ctx, allowAll);
+        assertThat(db.exists(ctx)).isTrue();
+        assertThat(Arrays.asList(app.listDatabases(ctx))).containsExactly(db.name());
+        assertThat(db.listTables(ctx)).isEmpty();
+        db.delete(ctx);
+        assertThat(db.exists(ctx)).isFalse();
+        assertThat(app.listDatabases(ctx)).isEmpty();
+    }
+
+    public void testTable() throws Exception {
+        Database db = createDatabase(createApp(createService()));
+        assertThat(db).isNotNull();
+        Table table = db.getTable(TABLE_NAME);
+        assertThat(table).isNotNull();
+        assertThat(table.name()).isEqualTo(TABLE_NAME);
+        assertThat(table.fullName()).isEqualTo(
+                NamingUtil.join(serverName, APP_NAME, DB_NAME, TABLE_NAME));
+        assertThat(table.exists(ctx)).isFalse();
+        assertThat(db.listTables(ctx)).isEmpty();
+        db.createTable(ctx, TABLE_NAME, allowAll);
+        assertThat(table.exists(ctx)).isTrue();
+        assertThat(Arrays.asList(db.listTables(ctx))).containsExactly(TABLE_NAME);
+
+        assertThat(table.getRow("row1").exists(ctx)).isFalse();
+        table.put(ctx, "row1", "value1", String.class);
+        assertThat(table.getRow("row1").exists(ctx)).isTrue();
+        assertThat(table.get(ctx, "row1", String.class)).isEqualTo("value1");
+        table.delete(ctx, new RowRange("row1"));
+        assertThat(table.getRow("row1").exists(ctx)).isFalse();
+        table.put(ctx, "row1", "value1", String.class);
+        table.put(ctx, "row2", "value2", String.class);
+        assertThat(table.getRow("row1").exists(ctx)).isTrue();
+        assertThat(table.getRow("row2").exists(ctx)).isTrue();
+        assertThat(table.get(ctx, "row1", String.class)).isEqualTo("value1");
+        assertThat(table.get(ctx, "row2", String.class)).isEqualTo("value2");
+        assertThat(table.scan(ctx, new RowRange("row1", "row3"))).containsExactly(
+                new KeyValue("row1", VomUtil.encode("value1", String.class)),
+                new KeyValue("row2", VomUtil.encode("value2", String.class)));
+        table.delete(ctx, new RowRange("row1", "row3"));
+        assertThat(table.getRow("row1").exists(ctx)).isFalse();
+        assertThat(table.getRow("row2").exists(ctx)).isFalse();
+
+        db.deleteTable(ctx, TABLE_NAME);
+        assertThat(table.exists(ctx)).isFalse();
+        assertThat(db.listTables(ctx)).isEmpty();
+    }
+
+    public void testRow() throws Exception {
+        Table table = createTable(createDatabase(createApp(createService())));
+        Row row = table.getRow(ROW_NAME);
+        assertThat(row).isNotNull();
+        assertThat(row.key()).isEqualTo(ROW_NAME);
+        assertThat(row.fullName()).isEqualTo(
+                NamingUtil.join(serverName, APP_NAME, DB_NAME, TABLE_NAME, ROW_NAME));
+        assertThat(row.exists(ctx)).isFalse();
+        row.put(ctx, "value", String.class);
+        assertThat(row.exists(ctx)).isTrue();
+        assertThat(row.get(ctx, String.class)).isEqualTo("value");
+        assertThat(table.get(ctx, ROW_NAME, String.class)).isEqualTo("value");
+        row.delete(ctx);
+        assertThat(row.exists(ctx)).isFalse();
+        table.put(ctx, ROW_NAME, "value", String.class);
+        assertThat(row.exists(ctx)).isTrue();
+        assertThat(row.get(ctx, String.class)).isEqualTo("value");
+        assertThat(table.get(ctx, ROW_NAME, String.class)).isEqualTo("value");
+    }
+
+    public void testDatabaseExec() throws Exception {
+        Database db = createDatabase(createApp(createService()));
+        Table table = createTable(db);
+        Foo foo = new Foo(4, "f");
+        Bar bar = new Bar(0.5f, "b");
+        Baz baz = new Baz("John Doe", true);
+
+        table.put(ctx, "foo", foo, Foo.class);
+        table.put(ctx, "bar", bar, Bar.class);
+        table.put(ctx, "baz", baz, Baz.class);
+
+        {
+            ResultStream stream = db.exec(ctx,
+                    "select k, v.Name from " + TABLE_NAME + " where Type(v) like \"%Baz\"");
+            assertThat(stream.columnNames()).containsExactly("k", "v.Name");
+            assertThat(stream).containsExactly(ImmutableList.of(
+                    new VdlAny(String.class, "baz"), new VdlAny(String.class, baz.name)));
+        }
+        {
+            ResultStream stream = db.exec(ctx, "select k, v from " + TABLE_NAME);
+            assertThat(stream.columnNames()).containsExactly("k", "v");
+            assertThat(stream).containsExactly(
+                    ImmutableList.of(new VdlAny(String.class, "bar"), new VdlAny(Bar.class, bar)),
+                    ImmutableList.of(new VdlAny(String.class, "baz"), new VdlAny(Baz.class, baz)),
+                    ImmutableList.of(new VdlAny(String.class, "foo"), new VdlAny(Foo.class, foo))
+            );
+        }
+    }
+
+    public void testBatch() throws Exception {
+        Database db = createDatabase(createApp(createService()));
+        Table table = createTable(db);
+        assertThat(table.scan(ctx, new PrefixRange(""))).isEmpty();
+
+        BatchDatabase batchFoo = db.beginBatch(ctx, null);
+        Table batchFooTable = batchFoo.getTable(TABLE_NAME);
+        assertThat(batchFooTable.exists(ctx)).isTrue();
+        batchFooTable.put(ctx, ROW_NAME, "foo", String.class);
+        // Assert that value is visible inside the batch but not outside.
+        assertThat(batchFooTable.get(ctx, ROW_NAME, String.class)).isEqualTo("foo");
+        assertThat(table.getRow(ROW_NAME).exists(ctx)).isFalse();
+
+        BatchDatabase batchBar = db.beginBatch(ctx, null);
+        Table batchBarTable = batchBar.getTable(TABLE_NAME);
+        assertThat(batchBarTable.exists(ctx)).isTrue();
+        batchBarTable.put(ctx, ROW_NAME, "foo", String.class);
+        // Assert that value is visible inside the batch but not outside.
+        assertThat(batchBarTable.get(ctx, ROW_NAME, String.class)).isEqualTo("foo");
+        assertThat(table.getRow(ROW_NAME).exists(ctx)).isFalse();
+
+        batchFoo.commit(ctx);
+        // Assert that the value is visible outside the batch.
+        assertThat(table.get(ctx, ROW_NAME, String.class)).isEqualTo("foo");
+
+        try {
+            batchBar.commit(ctx);
+            fail("Expected batchBar.commit() to fail");
+        } catch (VException e) {
+            // ok
+        }
+    }
+
+    public void testSyncGroup() throws Exception {
+        Database db = createDatabase(createApp(createService()));
+        String groupName = "test";
+
+        // "A" creates the group.
+        SyncGroupSpec spec = new SyncGroupSpec("test", allowAll,
+                ImmutableList.of(TABLE_NAME + "/"), ImmutableList.<String>of(), false);
+        SyncGroupMemberInfo memberInfo = new SyncGroupMemberInfo((byte) 1);
+        SyncGroup group = db.getSyncGroup(groupName);
+        {
+            group.create(ctx, spec, memberInfo);
+            assertThat(Arrays.asList(db.listSyncGroupNames(ctx))).containsExactly(groupName);
+            assertThat(group.getSpec(ctx).values()).containsExactly(spec);
+            assertThat(group.getMembers(ctx).values()).containsExactly(memberInfo);
+            assertThat(group.join(ctx, memberInfo)).isEqualTo(spec);
+        }
+        // TODO(spetrovic): test leave() and destroy().
+
+        SyncGroupSpec specRMW = new SyncGroupSpec("testRMW", allowAll,
+                ImmutableList.of(TABLE_NAME + "/"), ImmutableList.<String>of(), false);
+        assertThat(group.getSpec(ctx).keySet()).isNotEmpty();
+        String version = group.getSpec(ctx).keySet().iterator().next();
+        group.setSpec(ctx, specRMW, version);
+        assertThat(group.getSpec(ctx).values()).containsExactly(specRMW);
+        SyncGroupSpec specOverwrite = new SyncGroupSpec("testOverwrite", allowAll,
+                ImmutableList.of(TABLE_NAME + "/"), ImmutableList.<String>of(), false);
+        group.setSpec(ctx, specOverwrite, "");
+        assertThat(group.getSpec(ctx).values()).containsExactly(specOverwrite);
+    }
+
+    // TODO(spetrovic): Test Database.upgradeIfOutdated().
+
+    private SyncbaseService createService() throws Exception {
+        return Syncbase.newService(serverName);
+    }
+
+    private SyncbaseApp createApp(SyncbaseService service) throws Exception {
+        SyncbaseApp app = service.getApp(APP_NAME);
+        app.create(ctx, allowAll);
+        return app;
+    }
+
+    private Database createDatabase(SyncbaseApp app) throws Exception {
+        Database db = app.getNoSqlDatabase(DB_NAME, null);
+        db.create(ctx, allowAll);
+        return db;
+    }
+
+    private Table createTable(Database db) throws Exception {
+        db.createTable(ctx, TABLE_NAME, allowAll);
+        return db.getTable(TABLE_NAME);
+    }
+
+    private static class Foo implements Serializable {
+        private int i;
+        private String s;
+
+        public Foo() {
+            this.i = 0;
+            this.s = "";
+        }
+
+        public Foo(int i, String s) {
+            this.i = i;
+            this.s = s;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Foo foo = (Foo) o;
+
+            if (i != foo.i) return false;
+            return !(s != null ? !s.equals(foo.s) : foo.s != null);
+
+        }
+    }
+
+    private static class Bar implements Serializable {
+        private float f;
+        private String s;
+
+        public Bar() {
+            this.f = 0f;
+            this.s = "";
+        }
+
+        public Bar(float f, String s) {
+            this.f = f;
+            this.s = s;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Bar bar = (Bar) o;
+
+            if (Float.compare(bar.f, f) != 0) return false;
+            return !(s != null ? !s.equals(bar.s) : bar.s != null);
+
+        }
+    }
+
+    private static class Baz implements Serializable {
+        private String name;
+        private boolean active;
+
+        public Baz() {
+            this.name = "";
+            this.active = false;
+        }
+
+        public Baz(String name, boolean active) {
+            this.name = name;
+            this.active = active;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Baz baz = (Baz) o;
+
+            if (active != baz.active) return false;
+            return !(name != null ? !name.equals(baz.name) : baz.name != null);
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeTest.java b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeTest.java
new file mode 100644
index 0000000..a8af0d8
--- /dev/null
+++ b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/PrefixRangeTest.java
@@ -0,0 +1,55 @@
+// 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 io.v.syncbase.v23.services.syncbase.nosql;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests the {@link PrefixRange} implementation.
+ */
+@RunWith(Parameterized.class)
+public class PrefixRangeTest {
+    private final PrefixRange range;
+    private final String row;
+    private final boolean expectedIsWithin;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                {"", "aaaa", true},
+                {"", "zzzz", true},
+                {"", "", true},
+                {"aaaa", "aaaa", true},
+                {"aaaa", "aaaabb", true},
+                {"aaaa", "aaaa" + '\u0000', true},
+                {"aaaa", "aaab", false},
+                {"aaaa", "aaa", false},
+                {"aaaa", "", false},
+                {"aaaa", "b", false},
+        });
+    }
+
+    public PrefixRangeTest(String prefix, String row, boolean expectedIsWithin) {
+        this.range = new PrefixRange(prefix);
+        this.row = row;
+        this.expectedIsWithin = expectedIsWithin;
+    }
+
+    @Test
+    public void testIsWithin() {
+        if (expectedIsWithin) {
+            assertThat(range.isWithin(row)).isTrue();
+        } else {
+            assertThat(range.isWithin(row)).isFalse();
+        }
+    }
+}
diff --git a/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeMultiRowTest.java b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeMultiRowTest.java
new file mode 100644
index 0000000..d03723d
--- /dev/null
+++ b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeMultiRowTest.java
@@ -0,0 +1,57 @@
+// 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 io.v.syncbase.v23.services.syncbase.nosql;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests the {@link RowRange} multi-row implementation.
+ */
+@RunWith(Parameterized.class)
+public class RowRangeMultiRowTest {
+    private final RowRange range;
+    private final String row;
+    private final boolean expectedIsWithin;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                {"aaaa", "aaab", "aaaa", true},
+                {"aaaa", "aaab", "aaab", false},
+                {"aaaa", "aaaa", "aaaa", false},
+                {"aaaa", "aaab", "aaaaa", true},
+                {"aaaa", "aaaa" + '\u0000', "aaaa", true},
+                {"aaaa", "aaaa" + '\u0000', "aaaaaaaaaaaa", false},
+                {"aaaa", "aaaa" + '\u0000', "aaaa" + '\u0000', false},
+                {"aaab", "aaac", "aaaa", false},
+                {"aaab", "aaac", "aaaaaaab", false},
+                {"aaaa", "", "aaaaa", true},
+                {"aaaa", "", "bbbb", true},
+                {"aaab", "", "aaaa", false},
+        });
+    }
+
+    public RowRangeMultiRowTest(String start, String limit, String row, boolean expectedIsWithin) {
+        this.range = new RowRange(start, limit);
+        this.row = row;
+        this.expectedIsWithin = expectedIsWithin;
+    }
+
+    @Test
+    public void testIsWithin() {
+        if (expectedIsWithin) {
+            assertThat(range.isWithin(row)).isTrue();
+        } else {
+            assertThat(range.isWithin(row)).isFalse();
+        }
+    }
+}
diff --git a/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeSingleRowTest.java b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeSingleRowTest.java
new file mode 100644
index 0000000..4d70062
--- /dev/null
+++ b/lib/src/test/java/io/v/syncbase/v23/services/syncbase/nosql/RowRangeSingleRowTest.java
@@ -0,0 +1,52 @@
+// 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 io.v.syncbase.v23.services.syncbase.nosql;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests the {@link RowRange} single-row implementation.
+ */
+@RunWith(Parameterized.class)
+public class RowRangeSingleRowTest {
+    private final RowRange range;
+    private final String row;
+    private final boolean expectedIsWithin;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                {"aaaa", "aaaa", true},
+                {"aaaa", "aaab", false},
+                {"aaaa", "aaaaaa", false},
+                {"aaaa", "aaaa" + '\u0000', false},
+                {"aaab", "aaab", true},
+                {"aaab", "aaaa", false},
+                {"aaab", "aaaaaaaab", false},
+        });
+    }
+
+    public RowRangeSingleRowTest(String range, String row, boolean expectedIsWithin) {
+        this.range = new RowRange(range);
+        this.row = row;
+        this.expectedIsWithin = expectedIsWithin;
+    }
+
+    @Test
+    public void testIsWithin() {
+        if (expectedIsWithin) {
+            assertThat(range.isWithin(row)).isTrue();
+        } else {
+            assertThat(range.isWithin(row)).isFalse();
+        }
+    }
+}