syncbase.js: Add Exists rpc method to app, db, table, row.

Mirrors v.io/c/13470

Change-Id: Iaa225dbc8146cfe5a02bb4fa803188e69478a450
diff --git a/src/app.js b/src/app.js
index 9452023..3357eae 100644
--- a/src/app.js
+++ b/src/app.js
@@ -52,6 +52,12 @@
   this._wire(ctx).delete(ctx, cb);
 };
 
+// exists returns true only if this app exists. Insufficient permissions cause
+// exists to return false instead of an error.
+App.prototype.exists = function(ctx, cb) {
+  this._wire(ctx).exists(ctx, cb);
+};
+
 App.prototype.getPermissions = function(ctx, cb) {
   this._wire(ctx).getPermissions(ctx, cb);
 };
diff --git a/src/gen-vdl/v.io/syncbase/v23/services/syncbase/index.js b/src/gen-vdl/v.io/syncbase/v23/services/syncbase/index.js
index 812ab36..3777938 100644
--- a/src/gen-vdl/v.io/syncbase/v23/services/syncbase/index.js
+++ b/src/gen-vdl/v.io/syncbase/v23/services/syncbase/index.js
@@ -133,6 +133,11 @@
 };
     
       
+App.prototype.exists = function(ctx, serverCall) {
+  throw new Error('Method Exists not implemented');
+};
+    
+      
 App.prototype.setPermissions = function(ctx, serverCall, perms, version) {
   throw new Error('Method SetPermissions not implemented');
 };
@@ -185,6 +190,22 @@
     
       
     {
+    name: 'Exists',
+    doc: "// Exists returns true only if this App exists. Insufficient permissions\n// cause Exists to return false instead of an error.",
+    inArgs: [],
+    outArgs: [{
+      name: '',
+      doc: "",
+      type: vdl.types.BOOL
+    },
+    ],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Read", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
     name: 'SetPermissions',
     doc: "// SetPermissions replaces the current Permissions for an object.  version\n// allows for optional, optimistic concurrency control.  If non-empty,\n// version's value must come from GetPermissions.  If any client has\n// successfully called SetPermissions in the meantime, the version will be\n// stale and SetPermissions will fail.  If empty, SetPermissions performs an\n// unconditional update.\n//\n// Permissions objects are expected to be small.  It is up to the\n// implementation to define the exact limit, though it should probably be\n// around 100KB.  Large lists of principals can be represented concisely using\n// blessings.\n//\n// There is some ambiguity when calling SetPermissions on a mount point.\n// Does it affect the mount itself or does it affect the service endpoint\n// that the mount points to?  The chosen behavior is that it affects the\n// service endpoint.  To modify the mount point's Permissions, use\n// ResolveToMountTable to get an endpoint and call SetPermissions on that.\n// This means that clients must know when a name refers to a mount point to\n// change its Permissions.",
     inArgs: [{
diff --git a/src/gen-vdl/v.io/syncbase/v23/services/syncbase/nosql/index.js b/src/gen-vdl/v.io/syncbase/v23/services/syncbase/nosql/index.js
index 18bc481..af0ea54 100644
--- a/src/gen-vdl/v.io/syncbase/v23/services/syncbase/nosql/index.js
+++ b/src/gen-vdl/v.io/syncbase/v23/services/syncbase/nosql/index.js
@@ -382,6 +382,11 @@
 };
     
       
+Database.prototype.exists = function(ctx, serverCall) {
+  throw new Error('Method Exists not implemented');
+};
+    
+      
 Database.prototype.beginBatch = function(ctx, serverCall, bo) {
   throw new Error('Method BeginBatch not implemented');
 };
@@ -504,8 +509,24 @@
     
       
     {
+    name: 'Exists',
+    doc: "// Exists returns true only if this Database exists. Insufficient permissions\n// cause Exists to return false instead of an error.\n// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy\n// do not exist.",
+    inArgs: [],
+    outArgs: [{
+      name: '',
+      doc: "",
+      type: vdl.types.BOOL
+    },
+    ],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Read", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
     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.\n//\n// Concurrency semantics are documented in model.go.",
+    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.",
     inArgs: [{
       name: 'bo',
       doc: "",
@@ -537,7 +558,7 @@
       
     {
     name: 'Exec',
-    doc: "// Exec executes a syncQL query and returns all results as specified by\n// in the query's select clause.  The returned stream reads\n// from a consistent snapshot taken at the time of the Exec RPC.",
+    doc: "// Exec executes a syncQL query and returns all results as specified by in the\n// query's select clause. Concurrency semantics are documented in model.go.",
     inArgs: [{
       name: 'query',
       doc: "",
@@ -821,6 +842,11 @@
 };
     
       
+Table.prototype.exists = function(ctx, serverCall) {
+  throw new Error('Method Exists not implemented');
+};
+    
+      
 Table.prototype.deleteRowRange = function(ctx, serverCall, start, limit) {
   throw new Error('Method DeleteRowRange not implemented');
 };
@@ -831,13 +857,13 @@
 };
     
       
-Table.prototype.setPermissions = function(ctx, serverCall, prefix, perms) {
-  throw new Error('Method SetPermissions not implemented');
+Table.prototype.getPermissions = function(ctx, serverCall, key) {
+  throw new Error('Method GetPermissions not implemented');
 };
     
       
-Table.prototype.getPermissions = function(ctx, serverCall, key) {
-  throw new Error('Method GetPermissions not implemented');
+Table.prototype.setPermissions = function(ctx, serverCall, prefix, perms) {
+  throw new Error('Method SetPermissions not implemented');
 };
     
       
@@ -883,8 +909,24 @@
     
       
     {
+    name: 'Exists',
+    doc: "// Exists returns true only if this Table exists. Insufficient permissions\n// cause Exists to return false instead of an error.\n// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy\n// do not exist.",
+    inArgs: [],
+    outArgs: [{
+      name: '',
+      doc: "",
+      type: vdl.types.BOOL
+    },
+    ],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Read", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
     name: 'DeleteRowRange',
-    doc: "// Delete deletes all rows in the given half-open range [start, limit). If\n// limit is \"\", all rows with keys >= start are included. If the last row that\n// is covered by a prefix from SetPermissions is deleted, that (prefix, perms)\n// pair is removed.\n// TODO(sadovsky): Automatic GC interacts poorly with sync. Revisit this API.",
+    doc: "// Delete deletes all rows in the given half-open range [start, limit). If\n// limit is \"\", all rows with keys >= start are included.",
     inArgs: [{
       name: 'start',
       doc: "",
@@ -905,7 +947,7 @@
       
     {
     name: 'Scan',
-    doc: "// Scan returns all rows in the given half-open range [start, limit). If limit\n// is \"\", all rows with keys >= start are included. The returned stream reads\n// from a consistent snapshot taken at the time of the Scan RPC.",
+    doc: "// Scan returns all rows in the given half-open range [start, limit). If limit\n// is \"\", all rows with keys >= start are included. Concurrency semantics are\n// documented in model.go.",
     inArgs: [{
       name: 'start',
       doc: "",
@@ -929,27 +971,6 @@
     
       
     {
-    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.\n//\n// SetPermissions will fail if called with a prefix that does not match any\n// rows.",
-    inArgs: [{
-      name: 'prefix',
-      doc: "",
-      type: vdl.types.STRING
-    },
-    {
-      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: '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.",
     inArgs: [{
@@ -971,6 +992,27 @@
     
       
     {
+    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.",
+    inArgs: [{
+      name: 'prefix',
+      doc: "",
+      type: vdl.types.STRING
+    },
+    {
+      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: '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).",
     inArgs: [{
@@ -995,6 +1037,11 @@
 
     
       
+Row.prototype.exists = function(ctx, serverCall) {
+  throw new Error('Method Exists not implemented');
+};
+    
+      
 Row.prototype.get = function(ctx, serverCall) {
   throw new Error('Method Get not implemented');
 };
@@ -1020,6 +1067,22 @@
     
       
     {
+    name: 'Exists',
+    doc: "// Exists returns true only if this Row exists. Insufficient permissions\n// cause Exists to return false instead of an error.\n// TODO(ivanpi): Exists may fail with an error if higher levels of hierarchy\n// do not exist.",
+    inArgs: [],
+    outArgs: [{
+      name: '',
+      doc: "",
+      type: vdl.types.BOOL
+    },
+    ],
+    inStream: null,
+    outStream: null,
+    tags: [canonicalize.reduce(new access.Tag("Read", true), new access.Tag()._type), ]
+  },
+    
+      
+    {
     name: 'Get',
     doc: "// Get returns the value for this Row.",
     inArgs: [],
diff --git a/src/nosql/database.js b/src/nosql/database.js
index 76386a7..1e01c67 100644
--- a/src/nosql/database.js
+++ b/src/nosql/database.js
@@ -78,6 +78,18 @@
 };
 
 /**
+ * Returns true only if this Database exists.
+ * Insufficient permissions cause exists to return false instead of an error.
+ * TODO(ivanpi): exists may fail with an error if higher levels of hierarchy
+ * do not exist.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+Database.prototype.exists = function(ctx, cb) {
+  this._wire(ctx).exists(ctx, cb);
+};
+
+/**
  * Executes a syncQL query.
  *
  * Returns a stream of rows.  The first row contains an array of headers (i.e.
diff --git a/src/nosql/row.js b/src/nosql/row.js
index a748c7d..ba9f633 100644
--- a/src/nosql/row.js
+++ b/src/nosql/row.js
@@ -74,6 +74,18 @@
 };
 
 /**
+ * Returns true only if this Row exists.
+ * Insufficient permissions cause exists to return false instead of an error.
+ * TODO(ivanpi): exists may fail with an error if higher levels of hierarchy
+ * do not exist.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+Row.prototype.exists = function(ctx, cb) {
+  this._wire(ctx).exists(ctx, cb);
+};
+
+/**
  * Returns the value for this Row.
  * @param {module:vanadium.context.Context} ctx Vanadium context.
  * @param {function} cb Callback.
diff --git a/src/nosql/table.js b/src/nosql/table.js
index 384a1f7..824f816 100644
--- a/src/nosql/table.js
+++ b/src/nosql/table.js
@@ -58,6 +58,18 @@
 };
 
 /**
+ * Returns true only if this Table exists.
+ * Insufficient permissions cause exists to return false instead of an error.
+ * TODO(ivanpi): exists may fail with an error if higher levels of hierarchy
+ * do not exist.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+Table.prototype.exists = function(ctx, cb) {
+  this._wire(ctx).exists(ctx, 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.
diff --git a/test/integration/test-app.js b/test/integration/test-app.js
index 73838c2..597a569 100644
--- a/test/integration/test-app.js
+++ b/test/integration/test-app.js
@@ -9,7 +9,6 @@
 var syncbase = require('../..');
 
 var testUtil = require('./util');
-var appExists = testUtil.appExists;
 var setupApp = testUtil.setupApp;
 var setupService = testUtil.setupService;
 var uniqueName = testUtil.uniqueName;
@@ -52,22 +51,51 @@
       uniqueName('app'),
       uniqueName('app')
     ];
-    async.forEach(appNames, function(appName, cb) {
-      o.service.app(appName).create(o.ctx, {}, cb);
-    }, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
 
-      // Verify each app exists.
-      async.map(appNames, function(appName, cb) {
-        appExists(o.ctx, o.service, appName, cb);
-      }, function(err, existsArray) {
-        t.error(err);
-        t.deepEqual(existsArray, [true, true, true], 'all apps exist');
-        o.teardown(t.end);
-      });
+    async.waterfall([
+      // Verify none of the apps exist using exists().
+      async.apply(async.map, appNames, function(appName, cb) {
+        o.service.app(appName).exists(o.ctx, cb);
+      }),
+      function(existsArray, cb) {
+        t.deepEqual(existsArray, [false, false, false],
+          'exists: no apps exist');
+        cb(null);
+      },
+
+      // Verify none of the apps exist using listApps().
+      o.service.listApps.bind(o.service, o.ctx),
+      function(appList, cb) {
+        t.deepEqual(appList, [],
+          'listApps: no apps exist');
+        cb(null);
+      },
+
+      // Create all apps.
+      async.apply(async.forEach, appNames, function(appName, cb) {
+        o.service.app(appName).create(o.ctx, {}, cb);
+      }),
+
+      // Verify each app exists using exists().
+      async.apply(async.map, appNames, function(appName, cb) {
+        o.service.app(appName).exists(o.ctx, cb);
+      }),
+      function(existsArray, cb) {
+        t.deepEqual(existsArray, [true, true, true],
+          'exists: all apps exist');
+        cb(null);
+      },
+
+      // Verify all the apps exist using listApps().
+      o.service.listApps.bind(o.service, o.ctx),
+      function(appList, cb) {
+        t.deepEqual(appList.sort(), appNames.sort(),
+          'listApps: all apps exist');
+        cb(null);
+      }
+    ], function(err) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
@@ -78,17 +106,26 @@
       return t.end(err);
     }
 
-    o.app.delete(o.ctx, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
+    async.waterfall([
+      // Verify app exists.
+      o.app.exists.bind(o.app, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'app exists');
+        cb(null);
+      },
 
-      appExists(o.ctx, o.service, o.app.name, function(err, exists) {
-        t.error(err);
+      // Delete app.
+      o.app.delete.bind(o.app, o.ctx),
+
+      // Verify app no longer exists.
+      o.app.exists.bind(o.app, o.ctx),
+      function(exists, cb) {
         t.notok(exists, 'app no longer exists');
-        o.teardown(t.end);
-      });
+        cb(null);
+      }
+    ], function(err, arg) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
diff --git a/test/integration/test-database.js b/test/integration/test-database.js
index 9bba2ee..af5a0f1 100644
--- a/test/integration/test-database.js
+++ b/test/integration/test-database.js
@@ -16,8 +16,6 @@
 var Table = require('../../src/nosql/table');
 
 var testUtil = require('./util');
-var databaseExists = testUtil.databaseExists;
-var tableExists = testUtil.tableExists;
 var setupApp = testUtil.setupApp;
 var setupDatabase = testUtil.setupDatabase;
 var setupTable = testUtil.setupTable;
@@ -71,18 +69,55 @@
     }
 
     var db = o.app.noSqlDatabase(uniqueName('db'));
+    var db2 = o.app.noSqlDatabase(uniqueName('db'));
 
-    db.create(o.ctx, {}, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
+    async.waterfall([
+      // Verify database does not exist yet.
+      db.exists.bind(db, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'exists: database doesn\'t exist yet');
+        cb(null);
+      },
 
-      databaseExists(o.ctx, o.app, db.name, function(err, exists) {
-        t.error(err);
-        t.ok(exists, 'database exists');
-        o.teardown(t.end);
-      });
+      // Verify database list is empty.
+      o.app.listDatabases.bind(o.app, o.ctx),
+      function(dbList, cb) {
+        t.deepEqual(dbList, [],
+          'listDatabases: no databases exist');
+        cb(null);
+      },
+
+      // Create database.
+      db.create.bind(db, o.ctx, {}),
+
+      // Verify database exists.
+      db.exists.bind(db, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'exists: database exists');
+        cb(null);
+      },
+
+      // Verify database list contains the database.
+      o.app.listDatabases.bind(o.app, o.ctx),
+      function(dbList, cb) {
+        t.deepEqual(dbList, [db.name],
+          'listDatabases: database exists');
+        cb(null);
+      },
+
+      // Create another database.
+      db2.create.bind(db2, o.ctx, {}),
+
+      // Verify database list contains both databases.
+      o.app.listDatabases.bind(o.app, o.ctx),
+      function(dbList, cb) {
+        t.deepEqual(dbList.sort(), [db.name, db2.name].sort(),
+          'listDatabases: both databases exist');
+        cb(null);
+      },
+    ], function(err, arg) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
@@ -119,24 +154,29 @@
 
     var db = o.app.noSqlDatabase(uniqueName('db'));
 
-    db.create(o.ctx, {}, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
+    async.waterfall([
+      // Create database.
+      db.create.bind(db, o.ctx, {}),
 
-      db.delete(o.ctx, function(err) {
-        if (err) {
-          t.error(err);
-          return o.teardown(t.end);
-        }
+      // Verify database exists.
+      db.exists.bind(db, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'database exists');
+        cb(null);
+      },
 
-        databaseExists(o.ctx, o.app, db.name, function(err, exists) {
-          t.error(err);
-          t.notok(exists, 'database does not exist');
-          o.teardown(t.end);
-        });
-      });
+      // Delete database.
+      db.delete.bind(db, o.ctx),
+
+      // Verify database no longer exists.
+      db.exists.bind(db, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'database no longer exists');
+        cb(null);
+      },
+    ], function(err, arg) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
@@ -183,19 +223,56 @@
     }
 
     var db = o.database;
+    var table = db.table(uniqueName('table'));
+    var table2 = db.table(uniqueName('table'));
 
-    var tableName = uniqueName('table');
-    db.createTable(o.ctx, tableName, {}, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
+    async.waterfall([
+      // Verify table does not exist yet.
+      table.exists.bind(table, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'exists: table doesn\'t exist yet');
+        cb(null);
+      },
 
-      tableExists(o.ctx, db, tableName, function(err, exists) {
-        t.error(err);
-        t.ok(exists, 'table exists');
-        o.teardown(t.end);
-      });
+      // Verify table list is empty.
+      db.listTables.bind(db, o.ctx),
+      function(tableList, cb) {
+        t.deepEqual(tableList, [],
+          'listTables: no tables exist');
+        cb(null);
+      },
+
+      // Create table.
+      db.createTable.bind(db, o.ctx, table.name, {}),
+
+      // Verify table exists.
+      table.exists.bind(table, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'exists: table exists');
+        cb(null);
+      },
+
+      // Verify table list contains the table.
+      db.listTables.bind(db, o.ctx),
+      function(tableList, cb) {
+        t.deepEqual(tableList, [table.name],
+          'listTables: table exists');
+        cb(null);
+      },
+
+      // Create another table.
+      db.createTable.bind(db, o.ctx, table2.name, {}),
+
+      // Verify table list contains both tables.
+      db.listTables.bind(db, o.ctx),
+      function(tableList, cb) {
+        t.deepEqual(tableList.sort(), [table.name, table2.name].sort(),
+          'listTables: both tables exist');
+        cb(null);
+      },
+    ], function(err, arg) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
@@ -207,26 +284,31 @@
     }
 
     var db = o.database;
+    var table = db.table(uniqueName('table'));
 
-    var tableName = uniqueName('table');
-    db.createTable(o.ctx, tableName, {}, function(err) {
-      if (err) {
-        t.error(err);
-        return o.teardown(t.end);
-      }
+    async.waterfall([
+      // Create table.
+      db.createTable.bind(db, o.ctx, table.name, {}),
 
-      db.deleteTable(o.ctx, tableName, function(err) {
-        if (err) {
-          t.error(err);
-          return o.teardown(t.end);
-        }
+      // Verify table exists.
+      table.exists.bind(table, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'table exists');
+        cb(null);
+      },
 
-        tableExists(o.ctx, db, tableName, function(err, exists) {
-          t.error(err);
-          t.notok(exists, 'table does not exist');
-          o.teardown(t.end);
-        });
-      });
+      // Delete table.
+      db.deleteTable.bind(db, o.ctx, table.name),
+
+      // Verify table no longer exists.
+      table.exists.bind(table, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'table no longer exists');
+        cb(null);
+      },
+    ], function(err, arg) {
+      t.error(err);
+      o.teardown(t.end);
     });
   });
 });
diff --git a/test/integration/test-table.js b/test/integration/test-table.js
index 3714eba..7e924f8 100644
--- a/test/integration/test-table.js
+++ b/test/integration/test-table.js
@@ -53,16 +53,46 @@
     var value = uniqueName(ROW_VAL);
 
     var table = o.table;
-    table.put(o.ctx, key, value, deleteRow);
+    var row = o.table.row(key);
 
-    function deleteRow() {
-      table.row(key).delete(o.ctx, function() {
-        table.get(o.ctx, key, function(err, val) {
-          t.ok(err, 'get should error after row is deleted');
-          o.teardown(t.end);
-        });
+    async.waterfall([
+      // Verify row doesn't exist yet.
+      row.exists.bind(row, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'row doesn\'t exist yet');
+        cb(null);
+      },
+
+      // Put row.
+      table.put.bind(table, o.ctx, key, value),
+
+      // Verify row exists.
+      row.exists.bind(row, o.ctx),
+      function(exists, cb) {
+        t.ok(exists, 'row exists');
+        cb(null);
+      },
+
+      // Delete row.
+      row.delete.bind(row, o.ctx),
+
+      // Verify row no longer exists.
+      row.exists.bind(row, o.ctx),
+      function(exists, cb) {
+        t.notok(exists, 'row no longer exists');
+        cb(null);
+      },
+    ], function(err, arg) {
+      if (err) {
+        t.error(err);
+        return o.teardown(t.end);
+      }
+
+      table.get(o.ctx, key, function(err, val) {
+        t.ok(err, 'get should error after row is deleted');
+        o.teardown(t.end);
       });
-    }
+    });
   });
 });
 
diff --git a/test/integration/util.js b/test/integration/util.js
index 50b6563..7242f07 100644
--- a/test/integration/util.js
+++ b/test/integration/util.js
@@ -3,10 +3,6 @@
 // license that can be found in the LICENSE file.
 
 module.exports = {
-  appExists: appExists,
-  databaseExists: databaseExists,
-  tableExists: tableExists,
-
   setupApp: setupApp,
   setupDatabase: setupDatabase,
   setupService: setupService,
@@ -223,36 +219,6 @@
   });
 }
 
-function appExists(ctx, service, name, cb) {
-  service.listApps(ctx, function(err, names) {
-    if (err) {
-      return cb(err);
-    }
-
-    cb(null, names.indexOf(name) >= 0);
-  });
-}
-
-function databaseExists(ctx, app, name, cb) {
-  app.listDatabases(ctx, function(err, names) {
-    if (err) {
-      return cb(err);
-    }
-
-    cb(null, names.indexOf(name) >= 0);
-  });
-}
-
-function tableExists(ctx, db, name, cb) {
-  db.listTables(ctx, function(err, names) {
-    if (err) {
-      return cb(err);
-    }
-
-    cb(null, names.indexOf(name) >= 0);
-  });
-}
-
 function compareRows(r1, r2) {
   if (r1.key > r2.key) {
     return 1;