Merge "TBR: js.syncbase: Replace V23_ROOT with JIRI_ROOT everywhere."
diff --git a/src/app.js b/src/app.js
index ed9a1db..3fdc0f2 100644
--- a/src/app.js
+++ b/src/app.js
@@ -27,7 +27,9 @@
     return new App(parentFullName, relativeName);
   }
 
-  util.addNameProperties(this, parentFullName, relativeName, false);
+  var fullName = vanadium.naming.join(
+    parentFullName, util.escape(relativeName));
+  util.addNameProperties(this, parentFullName, relativeName, fullName);
 
   // TODO(nlacasse): Use the prr module to simplify all the
   // 'Object.defineProperty' calls scattered throughout the project.
@@ -48,12 +50,12 @@
 // must not contain slashes. schema can be null or undefined only if a schema
 // was never set for the database in the first place.
 App.prototype.noSqlDatabase = function(relativeName, schema) {
-  return new Database(this.fullName, relativeName, schema);
+  return new Database(this.fullName, relativeName, '', schema);
 };
 
 // listDatabases returns of all database names.
 App.prototype.listDatabases = function(ctx, cb) {
-  this._wire(ctx).listDatabases(ctx, cb);
+  util.listChildren(ctx, this.fullName, cb);
 };
 
 // create creates this app.  If perms is empty, we inherit (copy) the Service
diff --git a/src/gen-vdl/v.io/v23/services/syncbase/index.js b/src/gen-vdl/v.io/v23/services/syncbase/index.js
index b086ab2..9102a01 100644
--- a/src/gen-vdl/v.io/v23/services/syncbase/index.js
+++ b/src/gen-vdl/v.io/v23/services/syncbase/index.js
@@ -84,7 +84,7 @@
       
     {
     name: 'ListApps',
-    doc: "// ListApps returns a list of all App names.",
+    doc: "// ListApps returns a list of all App names.\n// TODO(sadovsky): Maybe switch to streaming RPC.",
     inArgs: [],
     outArgs: [{
       name: '',
@@ -238,7 +238,7 @@
       
     {
     name: 'ListDatabases',
-    doc: "// ListDatabases returns a list of all Database names.\n// TODO(kash): Include the database type (NoSQL vs. SQL).",
+    doc: "// ListDatabases returns a list of all Database names.\n// TODO(kash): Include the database type (NoSQL vs. SQL).\n// TODO(sadovsky): Maybe switch to streaming RPC.",
     inArgs: [],
     outArgs: [{
       name: '',
diff --git a/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js b/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
index e144175..a334d66 100644
--- a/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
+++ b/src/gen-vdl/v.io/v23/services/syncbase/nosql/index.js
@@ -299,7 +299,7 @@
 DatabaseWatcher.prototype._serviceDescription = {
   name: 'DatabaseWatcher',
   pkgPath: 'v.io/v23/services/syncbase/nosql',
-  doc: "// DatabaseWatcher allows a client to watch for updates to the database. For\n// each watch request, the client will receive a reliable stream of watch events\n// without re-ordering. See watch.GlobWatcher for a detailed explanation of the\n// behavior.\n// TODO(rogulenko): Currently the only supported watch patterns are\n// \"<tableName>/$/<rowPrefix>*\". Consider changing that.\n//\n// The watching is done by starting a streaming RPC. The argument to the RPC\n// contains the ResumeMarker that points to a particular place in the database\n// event log. The result stream consists of a never-ending sequence of Change\n// messages (until the call fails or is canceled). Each Change contains the\n// Name field in the form \"<tableName>/<rowKey>\" and the Value field of the\n// StoreChange type. If the client has no access to a row specified in a change,\n// that change is excluded from the result stream.\n//\n// The DatabaseWatcher is designed to be used in the following way:\n// 1) begin a read-only batch\n// 2) read all information your app needs\n// 3) read the ResumeMarker\n// 4) abort the batch\n// 5) start watching changes to the data using the ResumeMarker\n// In this configuration the client doesn't miss any changes.",
+  doc: "// DatabaseWatcher allows a client to watch for updates to the database. For\n// each watch request, the client will receive a reliable stream of watch events\n// without re-ordering. See watch.GlobWatcher for a detailed explanation of the\n// behavior.\n// TODO(rogulenko): Currently the only supported watch patterns are\n// \"<tableName>/$/<rowPrefix>*\". Consider changing that.\n//\n// The watching is done by starting a streaming RPC. The argument to the RPC\n// contains the ResumeMarker that points to a particular place in the database\n// event log. The result stream consists of a never-ending sequence of Change\n// messages (until the call fails or is canceled). Each Change contains the Name\n// field in the form \"<tableName>/$/<rowKey>\" and the Value field of the\n// StoreChange type. If the client has no access to a row specified in a change,\n// that change is excluded from the result stream.\n//\n// DatabaseWatcher is designed to be used in the following way:\n// 1) begin a read-only batch\n// 2) read all data your app needs\n// 3) read the ResumeMarker\n// 4) abort the batch\n// 5) start watching for changes to the data using the ResumeMarker\n// In this configuration the client will not miss any changes to the data.",
   embeds: [{
       name: 'GlobWatcher',
       pkgPath: 'v.io/v23/services/watch',
@@ -665,7 +665,7 @@
 BlobManager.prototype._serviceDescription = {
   name: 'BlobManager',
   pkgPath: 'v.io/v23/services/syncbase/nosql',
-  doc: "// BlobManager is the interface for blob operations.",
+  doc: "// BlobManager is the interface for blob operations.\n//\n// Description of API for resumable blob creation (append-only):\n// - Up until commit, a BlobRef may be used with PutBlob, GetBlobSize,\n//   DeleteBlob, and CommitBlob. Blob creation may be resumed by obtaining the\n//   current blob size via GetBlobSize and appending to the blob via PutBlob.\n// - After commit, a blob is immutable, at which point PutBlob and CommitBlob\n//   may no longer be used.\n// - All other methods (GetBlob, FetchBlob, PinBlob, etc.) may only be used\n//   after commit.",
   embeds: [],
   methods: [
     
@@ -1156,7 +1156,7 @@
     {
       name: 'DatabaseWatcher',
       pkgPath: 'v.io/v23/services/syncbase/nosql',
-      doc: "// DatabaseWatcher allows a client to watch for updates to the database. For\n// each watch request, the client will receive a reliable stream of watch events\n// without re-ordering. See watch.GlobWatcher for a detailed explanation of the\n// behavior.\n// TODO(rogulenko): Currently the only supported watch patterns are\n// \"<tableName>/$/<rowPrefix>*\". Consider changing that.\n//\n// The watching is done by starting a streaming RPC. The argument to the RPC\n// contains the ResumeMarker that points to a particular place in the database\n// event log. The result stream consists of a never-ending sequence of Change\n// messages (until the call fails or is canceled). Each Change contains the\n// Name field in the form \"<tableName>/<rowKey>\" and the Value field of the\n// StoreChange type. If the client has no access to a row specified in a change,\n// that change is excluded from the result stream.\n//\n// The DatabaseWatcher is designed to be used in the following way:\n// 1) begin a read-only batch\n// 2) read all information your app needs\n// 3) read the ResumeMarker\n// 4) abort the batch\n// 5) start watching changes to the data using the ResumeMarker\n// In this configuration the client doesn't miss any changes."
+      doc: "// DatabaseWatcher allows a client to watch for updates to the database. For\n// each watch request, the client will receive a reliable stream of watch events\n// without re-ordering. See watch.GlobWatcher for a detailed explanation of the\n// behavior.\n// TODO(rogulenko): Currently the only supported watch patterns are\n// \"<tableName>/$/<rowPrefix>*\". Consider changing that.\n//\n// The watching is done by starting a streaming RPC. The argument to the RPC\n// contains the ResumeMarker that points to a particular place in the database\n// event log. The result stream consists of a never-ending sequence of Change\n// messages (until the call fails or is canceled). Each Change contains the Name\n// field in the form \"<tableName>/$/<rowKey>\" and the Value field of the\n// StoreChange type. If the client has no access to a row specified in a change,\n// that change is excluded from the result stream.\n//\n// DatabaseWatcher is designed to be used in the following way:\n// 1) begin a read-only batch\n// 2) read all data your app needs\n// 3) read the ResumeMarker\n// 4) abort the batch\n// 5) start watching for changes to the data using the ResumeMarker\n// In this configuration the client will not miss any changes to the data."
     },
     {
       name: 'SyncGroupManager',
@@ -1166,7 +1166,7 @@
     {
       name: 'BlobManager',
       pkgPath: 'v.io/v23/services/syncbase/nosql',
-      doc: "// BlobManager is the interface for blob operations."
+      doc: "// BlobManager is the interface for blob operations.\n//\n// Description of API for resumable blob creation (append-only):\n// - Up until commit, a BlobRef may be used with PutBlob, GetBlobSize,\n//   DeleteBlob, and CommitBlob. Blob creation may be resumed by obtaining the\n//   current blob size via GetBlobSize and appending to the blob via PutBlob.\n// - After commit, a blob is immutable, at which point PutBlob and CommitBlob\n//   may no longer be used.\n// - All other methods (GetBlob, FetchBlob, PinBlob, etc.) may only be used\n//   after commit."
     },
     {
       name: 'SchemaManager',
@@ -1242,7 +1242,7 @@
       
     {
     name: 'ListTables',
-    doc: "// ListTables returns a list of all Table names.",
+    doc: "// ListTables returns a list of all Table names.\n// TODO(sadovsky): Maybe switch to streaming RPC.",
     inArgs: [],
     outArgs: [{
       name: '',
@@ -1283,7 +1283,7 @@
       
     {
     name: 'BeginBatch',
-    doc: "// BeginBatch creates a new batch. It returns an App-relative name for a\n// Database handle bound to this batch. If this Database is already bound to a\n// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics\n// are documented in model.go.\n// TODO(sadovsky): make BatchOptions optional",
+    doc: "// BeginBatch creates a new batch. It returns an App-relative name for a\n// Database handle bound to this batch. If this Database is already bound to a\n// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics\n// are documented in model.go.\n// TODO(sadovsky): Maybe make BatchOptions optional.",
     inArgs: [{
       name: 'schemaVersion',
       doc: "",
@@ -1878,6 +1878,16 @@
 };
     
       
+Table.prototype.getPermissions = function(ctx, serverCall, schemaVersion) {
+  throw new Error('Method GetPermissions not implemented');
+};
+    
+      
+Table.prototype.setPermissions = function(ctx, serverCall, schemaVersion, perms) {
+  throw new Error('Method SetPermissions not implemented');
+};
+    
+      
 Table.prototype.deleteRange = function(ctx, serverCall, schemaVersion, start, limit) {
   throw new Error('Method DeleteRange not implemented');
 };
@@ -1888,18 +1898,18 @@
 };
     
       
-Table.prototype.getPermissions = function(ctx, serverCall, schemaVersion, key) {
-  throw new Error('Method GetPermissions not implemented');
+Table.prototype.getPrefixPermissions = function(ctx, serverCall, schemaVersion, key) {
+  throw new Error('Method GetPrefixPermissions not implemented');
 };
     
       
-Table.prototype.setPermissions = function(ctx, serverCall, schemaVersion, prefix, perms) {
-  throw new Error('Method SetPermissions not implemented');
+Table.prototype.setPrefixPermissions = function(ctx, serverCall, schemaVersion, prefix, perms) {
+  throw new Error('Method SetPrefixPermissions not implemented');
 };
     
       
-Table.prototype.deletePermissions = function(ctx, serverCall, schemaVersion, prefix) {
-  throw new Error('Method DeletePermissions not implemented');
+Table.prototype.deletePrefixPermissions = function(ctx, serverCall, schemaVersion, prefix) {
+  throw new Error('Method DeletePrefixPermissions not implemented');
 };
      
 
@@ -1971,6 +1981,48 @@
     
       
     {
+    name: 'GetPermissions',
+    doc: "// GetPermissions returns the current Permissions for the Table.",
+    inArgs: [{
+      name: 'schemaVersion',
+      doc: "",
+      type: vdl.types.INT32
+    },
+    ],
+    outArgs: [{
+      name: '',
+      doc: "",
+      type: new access.Permissions()._type
+    },
+    ],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Admin", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
+    name: 'SetPermissions',
+    doc: "// SetPermissions replaces the current Permissions for the Table.",
+    inArgs: [{
+      name: 'schemaVersion',
+      doc: "",
+      type: vdl.types.INT32
+    },
+    {
+      name: 'perms',
+      doc: "",
+      type: new access.Permissions()._type
+    },
+    ],
+    outArgs: [],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Admin", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
     name: 'DeleteRange',
     doc: "// DeleteRange deletes all rows in the given half-open range [start, limit).\n// If limit is \"\", all rows with keys >= start are included.\n// TODO(sadovsky): Maybe add option to delete prefix perms fully covered by\n// the row range.",
     inArgs: [{
@@ -2027,8 +2079,8 @@
     
       
     {
-    name: 'GetPermissions',
-    doc: "// GetPermissions returns an array of (prefix, perms) pairs. The array is\n// sorted from longest prefix to shortest, so element zero is the one that\n// applies to the row with the given key. The last element is always the\n// prefix \"\" which represents the table's permissions -- the array will always\n// have at least one element.",
+    name: 'GetPrefixPermissions',
+    doc: "// GetPrefixPermissions returns an array of (prefix, perms) pairs. The array is\n// sorted from longest prefix to shortest, so element zero is the one that\n// applies to the row with the given key. The last element is always the\n// prefix \"\" which represents the table's permissions -- the array will always\n// have at least one element.",
     inArgs: [{
       name: 'schemaVersion',
       doc: "",
@@ -2053,8 +2105,8 @@
     
       
     {
-    name: 'SetPermissions',
-    doc: "// SetPermissions sets the permissions for all current and future rows with\n// the given prefix. If the prefix overlaps with an existing prefix, the\n// longest prefix that matches a row applies. For example:\n//     SetPermissions(ctx, Prefix(\"a/b\"), perms1)\n//     SetPermissions(ctx, Prefix(\"a/b/c\"), perms2)\n// The permissions for row \"a/b/1\" are perms1, and the permissions for row\n// \"a/b/c/1\" are perms2.",
+    name: 'SetPrefixPermissions',
+    doc: "// SetPrefixPermissions sets the permissions for all current and future rows with\n// the given prefix. If the prefix overlaps with an existing prefix, the\n// longest prefix that matches a row applies. For example:\n//     SetPrefixPermissions(ctx, Prefix(\"a/b\"), perms1)\n//     SetPrefixPermissions(ctx, Prefix(\"a/b/c\"), perms2)\n// The permissions for row \"a/b/1\" are perms1, and the permissions for row\n// \"a/b/c/1\" are perms2.",
     inArgs: [{
       name: 'schemaVersion',
       doc: "",
@@ -2079,8 +2131,8 @@
     
       
     {
-    name: 'DeletePermissions',
-    doc: "// DeletePermissions deletes the permissions for the specified prefix. Any\n// rows covered by this prefix will use the next longest prefix's permissions\n// (see the array returned by GetPermissions).",
+    name: 'DeletePrefixPermissions',
+    doc: "// DeletePrefixPermissions deletes the permissions for the specified prefix. Any\n// rows covered by this prefix will use the next longest prefix's permissions\n// (see the array returned by GetPrefixPermissions).",
     inArgs: [{
       name: 'schemaVersion',
       doc: "",
diff --git a/src/nosql/database.js b/src/nosql/database.js
index 34ca05b..aef4454 100644
--- a/src/nosql/database.js
+++ b/src/nosql/database.js
@@ -36,12 +36,18 @@
  * @inner
  * @memberof {module:syncbase.nosql}
  */
-function Database(parentFullName, relativeName, schema) {
+function Database(parentFullName, relativeName, batchSuffix, schema) {
   if (!(this instanceof Database)) {
     return new Database(parentFullName, relativeName);
   }
 
-  util.addNameProperties(this, parentFullName, relativeName, true);
+  // Escape relativeName so that any forward slashes get dropped, thus ensuring
+  // that the server will interpret fullName as referring to a database object.
+  // Note that the server will still reject this name if util.ValidDatabaseName
+  // returns false.
+  var fullName = vanadium.naming.join(
+    parentFullName, util.escape(relativeName) + batchSuffix);
+  util.addNameProperties(this, parentFullName, relativeName, fullName);
 
   Object.defineProperty(this, 'schema', {
     enumerable: false,
@@ -179,7 +185,7 @@
  * @param {function} cb Callback.
  */
 Database.prototype.listTables = function(ctx, cb) {
-  this._wire(ctx).listTables(ctx, cb);
+  util.listChildren(ctx, this.fullName, cb);
 };
 
 /**
@@ -217,7 +223,7 @@
  */
 Database.prototype.watch = function(ctx, tableName, prefix, resumeMarker, cb) {
   var globReq = new watchVdl.GlobRequest({
-    pattern: vanadium.naming.join(tableName, util.NAME_SEP, prefix + '*'),
+    pattern: vanadium.naming.join(tableName, prefix + '*'),
     resumeMarker: resumeMarker
   });
 
@@ -236,11 +242,8 @@
         return cb(new Error('invalid change state ' + change.state));
     }
 
-    // NOTE(sadovsky): We call stripBasename twice to convert "<table>/$/<row>"
-    // to "<table>".
     var wc = new watch.WatchChange({
-      tableName: vanadium.naming.stripBasename(
-        vanadium.naming.stripBasename(change.name)),
+      tableName: vanadium.naming.stripBasename(change.name),
       rowName: vanadium.naming.basename(change.name),
       changeType: changeType,
       valueBytes: changeType === 'put' ? change.value.value : null,
@@ -253,11 +256,10 @@
 
   var stream = this._wire(ctx).watchGlob(ctx, globReq, cb).stream;
 
-  // TODO(sadovsky): Our JS watch test times out after 20s when pattern is
-  // "<table>/<prefix>*", i.e. when it's missing the "/$/" separator. That's
-  // strange, because the server should immediately return an RPC error (since
-  // util.ParseTableRowPair returns an error). Does the JS watch test not check
-  // for this error?
+  // TODO(sadovsky): Our JS watch test times out after 20s when globReq is
+  // invalid. That's strange, because the server should immediately return an
+  // RPC error (since util.ParseTableRowPair returns an error). Does the JS
+  // watch test not check for this error?
   var watchChangeStream = stream.pipe(watchChangeEncoder);
   stream.on('error', function(err) {
     watchChangeStream.emit('error', err);
@@ -324,16 +326,12 @@
 Database.prototype.beginBatch = function(ctx, opts, cb) {
   var self = this;
   this._wire(ctx).beginBatch(ctx, this.schemaVersion, opts,
-    function(err, relativeName) {
+    function(err, batchSuffix) {
       if (err) {
         return cb(err);
       }
-
-      // The relativeName returned from the beginBatch() call above is different
-      // than the relativeName of the current database. We must create a new
-      // Database with this new relativeName, and then create a BatchDatabase
-      // from that new Database.
-      var db = new Database(self._parentFullName, relativeName);
+      var db = new Database(self._parentFullName, self.name, batchSuffix,
+                            self.schema);
       return cb(null, new BatchDatabase(db));
     });
 };
diff --git a/src/nosql/row.js b/src/nosql/row.js
index ae6f31a..c2702ec 100644
--- a/src/nosql/row.js
+++ b/src/nosql/row.js
@@ -25,7 +25,10 @@
     return new Row(parentFullName, key, schemaVersion);
   }
 
-  util.addNameProperties(this, parentFullName, key, true);
+  // Note, we immediately unescape row keys on the server side. See comment in
+  // server/nosql/dispatcher.go for explanation.
+  var fullName = vanadium.naming.join(parentFullName, util.escape(key));
+  util.addNameProperties(this, parentFullName, key, fullName);
 
   this.schemaVersion = schemaVersion;
 
diff --git a/src/nosql/table.js b/src/nosql/table.js
index 63a870c..ea6190c 100644
--- a/src/nosql/table.js
+++ b/src/nosql/table.js
@@ -30,7 +30,13 @@
     return new Table(parentFullName, relativeName, schemaVersion);
   }
 
-  util.addNameProperties(this, parentFullName, relativeName, true);
+  // Escape relativeName so that any forward slashes get dropped, thus ensuring
+  // that the server will interpret fullName as referring to a table object.
+  // Note that the server will still reject this name if util.ValidTableName
+  // returns false.
+  var fullName = vanadium.naming.join(
+    parentFullName, util.escape(relativeName));
+  util.addNameProperties(this, parentFullName, relativeName, fullName);
 
   this.schemaVersion = schemaVersion;
 
@@ -95,6 +101,25 @@
 };
 
 /**
+ * Returns the current permissions for the table.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+Table.prototype.getPermissions = function(ctx, cb) {
+  this._wire(ctx).getPermissions(ctx, this.schemaVersion, cb);
+};
+
+/**
+ * Replaces the current permissions for the table.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {module:vanadium.security.access.Permissions} perms Permissions
+ * @param {function} cb Callback.
+ */
+Table.prototype.setPermissions = function(ctx, perms, cb) {
+  this._wire(ctx).setPermissions(ctx, this.schemaVersion, perms, cb);
+};
+
+/**
  * Creates a row the given primary key in this table.
  * @param {string} key Primary key for the row.
  * @return {module:syncbase.row.Row} Row object.
@@ -195,29 +220,29 @@
 };
 
 /**
- * SetPermissions sets the permissions for all current and future rows with
- * the given prefix. If the prefix overlaps with an existing prefix, the
+ * SetPrefixPermissions sets the permissions for all current and future rows
+ * with the given prefix. If the prefix overlaps with an existing prefix, the
  * longest prefix that matches a row applies. For example:
- *     setPermissions(ctx, prefix('a/b'), perms1)
- *     setPermissions(ctx, prefix('a/b/c'), perms2)
+ *     setPerfixPermissions(ctx, prefix('a/b'), perms1)
+ *     setPrefixPermissions(ctx, prefix('a/b/c'), perms2)
  * The permissions for row "a/b/1" are perms1, and the permissions for row
  * "a/b/c/1" are perms2.
  *
- * SetPermissions will fail if called with a prefix that does not match any
- * rows.
+ * SetPrefixPermissions will fail if called with a prefix that does not match
+ * any rows.
  * @param {module:vanadium.context.Context} ctx Vanadium context.
  * @param {string} prefix Prefix.
- * @param @param {module:vanadium.security.access.Permissions} perms Permissions
+ * @param {module:vanadium.security.access.Permissions} perms Permissions
  * for the rows matching the prefix.
  * @param {function} cb Callback.
  */
-Table.prototype.setPermissions = function(ctx, prefix, perms, cb) {
-  this._wire(ctx).setPermissions(
+Table.prototype.setPrefixPermissions = function(ctx, prefix, perms, cb) {
+  this._wire(ctx).setPrefixPermissions(
         ctx, this.schemaVersion, prefix, perms, cb);
 };
 
 /**
- * GetPermissions returns an array of (prefix, perms) pairs. The array is
+ * GetPrefixPermissions returns an array of (prefix, perms) pairs. The array is
  * sorted from longest prefix to shortest, so element zero is the one that
  * applies to the row with the given key. The last element is always the
  * prefix "" which represents the table's permissions -- the array will always
@@ -226,19 +251,19 @@
  * @param {string} key Row key to get permissions for.
  * @param {function} cb Callback.
  */
-Table.prototype.getPermissions = function(ctx, key, cb) {
-  this._wire(ctx).getPermissions(ctx, this.schemaVersion, key, cb);
+Table.prototype.getPrefixPermissions = function(ctx, key, cb) {
+  this._wire(ctx).getPrefixPermissions(ctx, this.schemaVersion, key, cb);
 };
 
 /**
- * DeletePermissions deletes the permissions for the specified prefix. Any
+ * DeletePrefixPermissions deletes the permissions for the specified prefix. Any
  * rows covered by this prefix will use the next longest prefix's permissions.
  * @param {module:vanadium.context.Context} ctx Vanadium context.
  * @param {string} prefix Prefix.
  * @param {function} cb Callback.
  */
-Table.prototype.deletePermissions = function(ctx, prefix, cb) {
-  this._wire(ctx).deletePermissions(
+Table.prototype.deletePrefixPermissions = function(ctx, prefix, cb) {
+  this._wire(ctx).deletePrefixPermissions(
         ctx, this.schemaVersion, prefix, cb);
 };
 
diff --git a/src/service.js b/src/service.js
index 72f7088..48c9469 100644
--- a/src/service.js
+++ b/src/service.js
@@ -5,6 +5,7 @@
 var vanadium = require('vanadium');
 
 var App = require('./app');
+var util = require('./util');
 var vdl = require('./gen-vdl/v.io/v23/services/syncbase');
 
 // TODO(aghassemi): This looks clunky,
@@ -55,7 +56,7 @@
 
 // listApps returns a list of all app names.
 Service.prototype.listApps = function(ctx, cb) {
-  this._wire(ctx).listApps(ctx, cb);
+  util.listChildren(ctx, this.fullName, cb);
 };
 
 Service.prototype.getPermissions = function(ctx, cb) {
diff --git a/src/util.js b/src/util.js
index 06e26c6..0a1e52b 100644
--- a/src/util.js
+++ b/src/util.js
@@ -4,27 +4,21 @@
 
 var vanadium = require('vanadium');
 
-var NAME_SEP = '$';
-
 module.exports = {
   addNameProperties: addNameProperties,
+  listChildren: listChildren,
   prefixRangeLimit: prefixRangeLimit,
   stringToUTF8Bytes: stringToUTF8Bytes,
-  NAME_SEP: NAME_SEP
+  escape: escape,
+  unescape: unescape
 };
 
 /**
- * Creates the 'name' and 'fullName' properties on an object.
+ * Creates public 'name' and 'fullName' properties on an object, as well as a
+ * private '_parentFullName' property.
  * @private
  */
-function addNameProperties(self, parentFullName, relativeName, addNameSep) {
-  var fullName;
-  if (addNameSep) {
-    fullName = vanadium.naming.join(parentFullName, NAME_SEP, relativeName);
-  } else {
-    fullName = vanadium.naming.join(parentFullName, relativeName);
-  }
-
+function addNameProperties(self, parentFullName, name, fullName) {
   /**
    * @property _parentFullName
    * @private
@@ -41,7 +35,7 @@
    * @type {string}
    */
   Object.defineProperty(self, 'name', {
-    value: relativeName,
+    value: name,
     writable: false,
     enumerable: true
   });
@@ -58,6 +52,42 @@
 }
 
 /**
+ * listChildren returns the relative names of all children of parentFullName.
+ * @private
+ */
+function listChildren(ctx, parentFullName, cb) {
+  var rt = vanadium.runtimeForContext(ctx);
+  var globPattern = vanadium.naming.join(parentFullName, '*');
+
+  var childNames = [];
+  var streamErr = null;
+  var stream = rt.getNamespace().glob(ctx, globPattern, function(err) {
+    if (err) {
+      return cb(err);
+    }
+    if (streamErr) {
+      return cb(streamErr);
+    }
+    cb(null, childNames);
+  }).stream;
+
+  stream.on('data', function(globResult) {
+    var fullName = globResult.name;
+    var escName = vanadium.naming.basename(fullName);
+    // Component names within object names are always escaped. See comment in
+    // server/nosql/dispatcher.go for explanation.
+    // If unescape throws an exception, there's a bug in the Syncbase server.
+    // Glob should return names with escaped components.
+    childNames.push(unescape(escName));
+  });
+
+  stream.on('error', function(err) {
+    console.error('Stream error: ' + JSON.stringify(err));
+    streamErr = streamErr || err.error;
+  });
+}
+
+/**
  * prefixRangeLimit modifies the input bytes to be the limit of the row range
  * for the given prefix.
  * TODO(sadovsky): Why do we modify the input bytes, rather than returning a new
@@ -97,3 +127,27 @@
 
   return bytes;
 }
+
+/**
+ * escape escapes a component name for use in a Syncbase object name. In
+ * particular, it replaces bytes "%" and "/" with the "%" character followed by
+ * the byte's two-digit hex code. Clients using the client library need not
+ * escape names themselves; the client library does so on their behalf.
+ * @param {string} s String to escape.
+ * @return {string} Escaped string.
+ */
+function escape(s) {
+  return s
+    .replace(/%/g, '%25')
+    .replace(/\//g, '%2F');
+}
+
+/**
+ * unescape applies the inverse of escape. Throws exception if the given string
+ * is not a valid escaped string.
+ * @param {string} s String to unescape.
+ * @return {string} Unescaped string.
+ */
+function unescape(s) {
+  return decodeURIComponent(s);
+}
diff --git a/test/integration/test-database.js b/test/integration/test-database.js
index d9258e4..262d453 100644
--- a/test/integration/test-database.js
+++ b/test/integration/test-database.js
@@ -42,7 +42,7 @@
     db.name = 'foo';
     t.equal(db.name, dbName, 'Setting the name has no effect.');
 
-    var expectedFullName = naming.join(o.app.fullName, '$', dbName);
+    var expectedFullName = naming.join(o.app.fullName, dbName);
     t.equal(db.fullName, expectedFullName, 'Database has correct fullName.');
 
     db.fullName = 'bar';
@@ -76,21 +76,6 @@
   });
 });
 
-test('app.noSqlDatabase with slashes in the name', function(t) {
-  setupApp(t, function(err, o) {
-    if (err) {
-      return t.end(err);
-    }
-
-    var dbName = 'bad/name';
-    t.doesNotThrow(function() {
-      o.app.noSqlDatabase(dbName);
-    }, 'should throw');
-
-    o.teardown(t.end);
-  });
-});
-
 test('db.create() creates a database', function(t) {
   setupApp(t, function(err, o) {
     if (err) {
@@ -238,7 +223,7 @@
     t.ok(table, 'table is created.');
     t.ok(table instanceof Table, 'table is a Table object.');
     t.equal(table.name, tableName, 'table has the correct name.');
-    t.equal(table.fullName, vanadium.naming.join(db.fullName, '$', tableName),
+    t.equal(table.fullName, vanadium.naming.join(db.fullName, tableName),
       'table has the correct fullName.');
 
     o.teardown(t.end);