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 ≥ {@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();
+ }
+ }
+}