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

Exists returns true if the object exists (has been created).

If the caller doesn't have Read permission on the object, Exists will
return false instead of an error because access errors imply existence
in the current implementation. We leak existence information in other
rpcs, we may want to clean it up.

Change-Id: I7f4300512dff16369932854f0c19e735e3518aa7
diff --git a/v23/services/syncbase/nosql/service.vdl b/v23/services/syncbase/nosql/service.vdl
index ad6b9f5..70424b4 100644
--- a/v23/services/syncbase/nosql/service.vdl
+++ b/v23/services/syncbase/nosql/service.vdl
@@ -24,6 +24,12 @@
 	// Delete deletes this Database.
 	Delete() error {access.Write}
 
+	// Exists 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.
+	Exists() (bool | error) {access.Read}
+
 	// BeginBatch creates a new batch. It returns an App-relative name for a
 	// Database handle bound to this batch. If this Database is already bound to a
 	// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics
@@ -64,6 +70,12 @@
 	// Delete deletes this Table.
 	Delete() error {access.Write}
 
+	// Exists 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.
+	Exists() (bool | error) {access.Read}
+
 	// Delete deletes all rows in the given half-open range [start, limit). If
 	// limit is "", all rows with keys >= start are included.
 	DeleteRowRange(start, limit []byte) error {access.Write}
@@ -102,6 +114,12 @@
 // and Scan. If there's a way to avoid encoding/decoding on the server side, we
 // can use vdl.Value everywhere without sacrificing performance.
 type Row interface {
+	// Exists 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.
+	Exists() (bool | error) {access.Read}
+
 	// Get returns the value for this Row.
 	Get() ([]byte | error) {access.Read}
 
diff --git a/v23/services/syncbase/nosql/service.vdl.go b/v23/services/syncbase/nosql/service.vdl.go
index 503415e..d093c61 100644
--- a/v23/services/syncbase/nosql/service.vdl.go
+++ b/v23/services/syncbase/nosql/service.vdl.go
@@ -468,6 +468,11 @@
 	Create(ctx *context.T, perms access.Permissions, opts ...rpc.CallOpt) error
 	// Delete deletes this Database.
 	Delete(*context.T, ...rpc.CallOpt) error
+	// Exists 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.
+	Exists(*context.T, ...rpc.CallOpt) (bool, error)
 	// BeginBatch creates a new batch. It returns an App-relative name for a
 	// Database handle bound to this batch. If this Database is already bound to a
 	// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics
@@ -516,6 +521,11 @@
 	return
 }
 
+func (c implDatabaseClientStub) Exists(ctx *context.T, opts ...rpc.CallOpt) (o0 bool, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "Exists", nil, []interface{}{&o0}, opts...)
+	return
+}
+
 func (c implDatabaseClientStub) BeginBatch(ctx *context.T, i0 BatchOptions, opts ...rpc.CallOpt) (o0 string, err error) {
 	err = v23.GetClient(ctx).Call(ctx, c.name, "BeginBatch", []interface{}{i0}, []interface{}{&o0}, opts...)
 	return
@@ -671,6 +681,11 @@
 	Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error
 	// Delete deletes this Database.
 	Delete(*context.T, rpc.ServerCall) error
+	// Exists 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.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 	// BeginBatch creates a new batch. It returns an App-relative name for a
 	// Database handle bound to this batch. If this Database is already bound to a
 	// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics
@@ -750,6 +765,11 @@
 	Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error
 	// Delete deletes this Database.
 	Delete(*context.T, rpc.ServerCall) error
+	// Exists 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.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 	// BeginBatch creates a new batch. It returns an App-relative name for a
 	// Database handle bound to this batch. If this Database is already bound to a
 	// batch, BeginBatch() will fail with ErrBoundToBatch. Concurrency semantics
@@ -811,6 +831,10 @@
 	return s.impl.Delete(ctx, call)
 }
 
+func (s implDatabaseServerStub) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	return s.impl.Exists(ctx, call)
+}
+
 func (s implDatabaseServerStub) BeginBatch(ctx *context.T, call rpc.ServerCall, i0 BatchOptions) (string, error) {
 	return s.impl.BeginBatch(ctx, call, i0)
 }
@@ -862,6 +886,14 @@
 			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
 		},
 		{
+			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.",
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // bool
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
+		{
 			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.",
 			InArgs: []rpc.ArgDesc{
@@ -947,6 +979,11 @@
 	Create(ctx *context.T, perms access.Permissions, opts ...rpc.CallOpt) error
 	// Delete deletes this Table.
 	Delete(*context.T, ...rpc.CallOpt) error
+	// Exists 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.
+	Exists(*context.T, ...rpc.CallOpt) (bool, error)
 	// Delete deletes all rows in the given half-open range [start, limit). If
 	// limit is "", all rows with keys >= start are included.
 	DeleteRowRange(ctx *context.T, start []byte, limit []byte, opts ...rpc.CallOpt) error
@@ -999,6 +1036,11 @@
 	return
 }
 
+func (c implTableClientStub) Exists(ctx *context.T, opts ...rpc.CallOpt) (o0 bool, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "Exists", nil, []interface{}{&o0}, opts...)
+	return
+}
+
 func (c implTableClientStub) DeleteRowRange(ctx *context.T, i0 []byte, i1 []byte, opts ...rpc.CallOpt) (err error) {
 	err = v23.GetClient(ctx).Call(ctx, c.name, "DeleteRowRange", []interface{}{i0, i1}, nil, opts...)
 	return
@@ -1108,6 +1150,11 @@
 	Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error
 	// Delete deletes this Table.
 	Delete(*context.T, rpc.ServerCall) error
+	// Exists 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.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 	// Delete deletes all rows in the given half-open range [start, limit). If
 	// limit is "", all rows with keys >= start are included.
 	DeleteRowRange(ctx *context.T, call rpc.ServerCall, start []byte, limit []byte) error
@@ -1145,6 +1192,11 @@
 	Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error
 	// Delete deletes this Table.
 	Delete(*context.T, rpc.ServerCall) error
+	// Exists 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.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 	// Delete deletes all rows in the given half-open range [start, limit). If
 	// limit is "", all rows with keys >= start are included.
 	DeleteRowRange(ctx *context.T, call rpc.ServerCall, start []byte, limit []byte) error
@@ -1209,6 +1261,10 @@
 	return s.impl.Delete(ctx, call)
 }
 
+func (s implTableServerStub) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	return s.impl.Exists(ctx, call)
+}
+
 func (s implTableServerStub) DeleteRowRange(ctx *context.T, call rpc.ServerCall, i0 []byte, i1 []byte) error {
 	return s.impl.DeleteRowRange(ctx, call, i0, i1)
 }
@@ -1260,6 +1316,14 @@
 			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
 		},
 		{
+			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.",
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // bool
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
+		{
 			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.",
 			InArgs: []rpc.ArgDesc{
@@ -1361,6 +1425,11 @@
 // and Scan. If there's a way to avoid encoding/decoding on the server side, we
 // can use vdl.Value everywhere without sacrificing performance.
 type RowClientMethods interface {
+	// Exists 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.
+	Exists(*context.T, ...rpc.CallOpt) (bool, error)
 	// Get returns the value for this Row.
 	Get(*context.T, ...rpc.CallOpt) ([]byte, error)
 	// Put writes the given value for this Row.
@@ -1384,6 +1453,11 @@
 	name string
 }
 
+func (c implRowClientStub) Exists(ctx *context.T, opts ...rpc.CallOpt) (o0 bool, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "Exists", nil, []interface{}{&o0}, opts...)
+	return
+}
+
 func (c implRowClientStub) Get(ctx *context.T, opts ...rpc.CallOpt) (o0 []byte, err error) {
 	err = v23.GetClient(ctx).Call(ctx, c.name, "Get", nil, []interface{}{&o0}, opts...)
 	return
@@ -1409,6 +1483,11 @@
 // and Scan. If there's a way to avoid encoding/decoding on the server side, we
 // can use vdl.Value everywhere without sacrificing performance.
 type RowServerMethods interface {
+	// Exists 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.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 	// Get returns the value for this Row.
 	Get(*context.T, rpc.ServerCall) ([]byte, error)
 	// Put writes the given value for this Row.
@@ -1452,6 +1531,10 @@
 	gs   *rpc.GlobState
 }
 
+func (s implRowServerStub) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	return s.impl.Exists(ctx, call)
+}
+
 func (s implRowServerStub) Get(ctx *context.T, call rpc.ServerCall) ([]byte, error) {
 	return s.impl.Get(ctx, call)
 }
@@ -1482,6 +1565,14 @@
 	Doc:     "// Row represents a single row in a Table.\n// All access checks are performed against the most specific matching prefix\n// permissions in the Table.\n// NOTE(sadovsky): Currently we send []byte values over the wire for Get, Put,\n// and Scan. If there's a way to avoid encoding/decoding on the server side, we\n// can use vdl.Value everywhere without sacrificing performance.",
 	Methods: []rpc.MethodDesc{
 		{
+			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.",
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // bool
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
+		{
 			Name: "Get",
 			Doc:  "// Get returns the value for this Row.",
 			OutArgs: []rpc.ArgDesc{
diff --git a/v23/services/syncbase/service.vdl b/v23/services/syncbase/service.vdl
index 57810f0..c87e4c0 100644
--- a/v23/services/syncbase/service.vdl
+++ b/v23/services/syncbase/service.vdl
@@ -34,6 +34,10 @@
 	// Delete deletes this App.
 	Delete() error {access.Write}
 
+	// Exists returns true only if this App exists. Insufficient permissions
+	// cause Exists to return false instead of an error.
+	Exists() (bool | error) {access.Read}
+
 	// SetPermissions and GetPermissions are included from the Object interface.
 	permissions.Object
 }
diff --git a/v23/services/syncbase/service.vdl.go b/v23/services/syncbase/service.vdl.go
index ae135a0..29f645b 100644
--- a/v23/services/syncbase/service.vdl.go
+++ b/v23/services/syncbase/service.vdl.go
@@ -277,6 +277,9 @@
 	Create(ctx *context.T, perms access.Permissions, opts ...rpc.CallOpt) error
 	// Delete deletes this App.
 	Delete(*context.T, ...rpc.CallOpt) error
+	// Exists returns true only if this App exists. Insufficient permissions
+	// cause Exists to return false instead of an error.
+	Exists(*context.T, ...rpc.CallOpt) (bool, error)
 }
 
 // AppClientStub adds universal methods to AppClientMethods.
@@ -306,6 +309,11 @@
 	return
 }
 
+func (c implAppClientStub) Exists(ctx *context.T, opts ...rpc.CallOpt) (o0 bool, err error) {
+	err = v23.GetClient(ctx).Call(ctx, c.name, "Exists", nil, []interface{}{&o0}, opts...)
+	return
+}
+
 // AppServerMethods is the interface a server writer
 // implements for App.
 //
@@ -364,6 +372,9 @@
 	Create(ctx *context.T, call rpc.ServerCall, perms access.Permissions) error
 	// Delete deletes this App.
 	Delete(*context.T, rpc.ServerCall) error
+	// Exists returns true only if this App exists. Insufficient permissions
+	// cause Exists to return false instead of an error.
+	Exists(*context.T, rpc.ServerCall) (bool, error)
 }
 
 // AppServerStubMethods is the server interface containing
@@ -411,6 +422,10 @@
 	return s.impl.Delete(ctx, call)
 }
 
+func (s implAppServerStub) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	return s.impl.Exists(ctx, call)
+}
+
 func (s implAppServerStub) Globber() *rpc.GlobState {
 	return s.gs
 }
@@ -444,5 +459,13 @@
 			Doc:  "// Delete deletes this App.",
 			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))},
 		},
+		{
+			Name: "Exists",
+			Doc:  "// Exists returns true only if this App exists. Insufficient permissions\n// cause Exists to return false instead of an error.",
+			OutArgs: []rpc.ArgDesc{
+				{"", ``}, // bool
+			},
+			Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))},
+		},
 	},
 }
diff --git a/v23/syncbase/app.go b/v23/syncbase/app.go
index 94ab40b..e9ec228 100644
--- a/v23/syncbase/app.go
+++ b/v23/syncbase/app.go
@@ -42,6 +42,11 @@
 	return a.fullName
 }
 
+// Exists implements App.Exists.
+func (a *app) Exists(ctx *context.T) (bool, error) {
+	return a.c.Exists(ctx)
+}
+
 // NoSQLDatabase implements App.NoSQLDatabase.
 func (a *app) NoSQLDatabase(relativeName string) nosql.Database {
 	return nosql.NewDatabase(a.fullName, relativeName)
diff --git a/v23/syncbase/model.go b/v23/syncbase/model.go
index 9c54b95..184bd8c 100644
--- a/v23/syncbase/model.go
+++ b/v23/syncbase/model.go
@@ -49,6 +49,10 @@
 	// FullName returns the full name (object name) of this App.
 	FullName() string
 
+	// Exists returns true only if this App exists. Insufficient permissions
+	// cause Exists to return false instead of an error.
+	Exists(ctx *context.T) (bool, error)
+
 	// NoSQLDatabase returns the nosql.Database with the given name.
 	// relativeName must not contain slashes.
 	NoSQLDatabase(relativeName string) nosql.Database
diff --git a/v23/syncbase/nosql/database.go b/v23/syncbase/nosql/database.go
index 8346109..030a44b 100644
--- a/v23/syncbase/nosql/database.go
+++ b/v23/syncbase/nosql/database.go
@@ -43,6 +43,11 @@
 	return d.fullName
 }
 
+// Exists implements Database.Exists.
+func (d *database) Exists(ctx *context.T) (bool, error) {
+	return d.c.Exists(ctx)
+}
+
 // Table implements Database.Table.
 func (d *database) Table(relativeName string) Table {
 	return newTable(d.fullName, relativeName)
diff --git a/v23/syncbase/nosql/model.go b/v23/syncbase/nosql/model.go
index 858c59a..cb74e01 100644
--- a/v23/syncbase/nosql/model.go
+++ b/v23/syncbase/nosql/model.go
@@ -59,6 +59,12 @@
 	// Delete deletes this Database.
 	Delete(ctx *context.T) error
 
+	// Exists 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.
+	Exists(ctx *context.T) (bool, error)
+
 	// Create creates the specified Table.
 	// If perms is nil, we inherit (copy) the Database perms.
 	// relativeName must not contain slashes.
@@ -135,6 +141,12 @@
 	// FullName returns the full name (object name) of this Table.
 	FullName() string
 
+	// Exists 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.
+	Exists(ctx *context.T) (bool, error)
+
 	// Row returns the Row with the given primary key.
 	Row(key string) Row
 
@@ -198,6 +210,12 @@
 	// FullName returns the full name (object name) of this Row.
 	FullName() string
 
+	// Exists 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.
+	Exists(ctx *context.T) (bool, error)
+
 	// Get returns the value for this Row.
 	Get(ctx *context.T, value interface{}) error
 
diff --git a/v23/syncbase/nosql/row.go b/v23/syncbase/nosql/row.go
index 4fc7c61..833f4a6 100644
--- a/v23/syncbase/nosql/row.go
+++ b/v23/syncbase/nosql/row.go
@@ -41,6 +41,11 @@
 	return r.fullName
 }
 
+// Exists implements Row.Exists.
+func (r *row) Exists(ctx *context.T) (bool, error) {
+	return r.c.Exists(ctx)
+}
+
 // Get implements Row.Get.
 func (r *row) Get(ctx *context.T, value interface{}) error {
 	bytes, err := r.c.Get(ctx)
diff --git a/v23/syncbase/nosql/table.go b/v23/syncbase/nosql/table.go
index 67aa074..759706c 100644
--- a/v23/syncbase/nosql/table.go
+++ b/v23/syncbase/nosql/table.go
@@ -40,6 +40,11 @@
 	return t.fullName
 }
 
+// Exists implements Table.Exists.
+func (t *table) Exists(ctx *context.T) (bool, error) {
+	return t.c.Exists(ctx)
+}
+
 // Row implements Table.Row.
 func (t *table) Row(key string) Row {
 	return newRow(t.fullName, key)
diff --git a/v23/syncbase/testutil/layer.go b/v23/syncbase/testutil/layer.go
index 7e176e9..372ca3c 100644
--- a/v23/syncbase/testutil/layer.go
+++ b/v23/syncbase/testutil/layer.go
@@ -17,6 +17,7 @@
 	"v.io/v23/security/access"
 	"v.io/v23/verror"
 	"v.io/x/lib/vlog"
+	"v.io/x/ref/test/testutil"
 )
 
 // TestCreate tests that object creation works as expected.
@@ -30,6 +31,11 @@
 		t.Fatalf("child.Create() should have failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", false)
+	// TODO(ivanpi): Exists on child when parent does not exist currently fails
+	// with an error instead of returning false.
+	//assertExists(t, ctx, child, "child", false)
+
 	// Create self.
 	if err := self.Create(ctx, nil); err != nil {
 		t.Fatalf("self.Create() failed: %v", err)
@@ -38,16 +44,23 @@
 		t.Errorf("Perms do not match: got %v, want %v", gotPerms, wantPerms)
 	}
 
+	assertExists(t, ctx, self, "self", true)
+	assertExists(t, ctx, child, "child", false)
+
 	// child.Create should now succeed.
 	if err := child.Create(ctx, nil); err != nil {
 		t.Fatalf("child.Create() failed: %v", err)
 	}
 
+	assertExists(t, ctx, child, "child", true)
+
 	// self.Create should fail since self already exists.
 	if err := self.Create(ctx, nil); verror.ErrorID(err) != verror.ErrExist.ID {
 		t.Fatalf("self.Create() should have failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", true)
+
 	// Test create with non-default perms.
 	self2 := parent.Child("self2")
 	perms := access.Permissions{}
@@ -59,15 +72,22 @@
 		t.Errorf("Perms do not match: got %v, want %v", gotPerms, wantPerms)
 	}
 
+	// Even though self2 exists, Exists returns false because Read access is needed.
+	assertExists(t, ctx, self2, "self2", false)
+
 	// Test that create fails if the parent perms disallow access.
 	perms = DefaultPerms("root/client")
 	perms.Blacklist("root/client", string(access.Write))
 	if err := parent.SetPermissions(ctx, perms, ""); err != nil {
 		t.Fatalf("parent.SetPermissions() failed: %v", err)
 	}
-	if err := parent.Child("self3").Create(ctx, nil); verror.ErrorID(err) != verror.ErrNoAccess.ID {
+	self3 := parent.Child("self3")
+	if err := self3.Create(ctx, nil); verror.ErrorID(err) != verror.ErrNoAccess.ID {
 		t.Fatalf("self3.Create() should have failed: %v", err)
 	}
+
+	assertExists(t, ctx, self, "self", true)
+	assertExists(t, ctx, self3, "self3", false)
 }
 
 // TestDelete tests that object deletion works as expected.
@@ -81,27 +101,41 @@
 		t.Fatalf("self.Create() failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", true)
+
 	// self.Create should fail, since self already exists.
 	if err := self.Create(ctx, nil); verror.ErrorID(err) != verror.ErrExist.ID {
 		t.Fatalf("self.Create() should have failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", true)
+
 	// By default, self perms are copied from parent, so self.Delete should
 	// succeed.
 	if err := self.Delete(ctx); err != nil {
 		t.Fatalf("self.Delete() failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", false)
+
 	// child.Create should fail, since self does not exist.
 	if err := child.Create(ctx, nil); verror.ErrorID(err) != verror.ErrNoExist.ID {
 		t.Fatalf("child.Create() should have failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", false)
+	// TODO(ivanpi): Exists on child when parent does not exist currently fails
+	// with an error instead of returning false.
+	//assertExists(t, ctx, child, "child", false)
+
 	// self.Create should succeed, since self was deleted.
 	if err := self.Create(ctx, nil); err != nil {
 		t.Fatalf("self.Create() failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", true)
+	assertExists(t, ctx, child, "child", false)
+
 	// Test that delete fails if the perms disallow access.
 	self2 := parent.Child("self2")
 	if err := self2.Create(ctx, nil); err != nil {
@@ -116,6 +150,8 @@
 		t.Fatalf("self2.Delete() should have failed: %v", err)
 	}
 
+	assertExists(t, ctx, self2, "self2", true)
+
 	// Test that delete succeeds even if the parent perms disallow access.
 	perms = DefaultPerms("root/client")
 	perms.Blacklist("root/client", string(access.Write))
@@ -126,10 +162,14 @@
 		t.Fatalf("self.Delete() failed: %v", err)
 	}
 
+	assertExists(t, ctx, self, "self", false)
+
 	// Test that delete is idempotent.
 	if err := self.Delete(ctx); err != nil {
 		t.Fatalf("self.Delete() failed: %v", err)
 	}
+
+	assertExists(t, ctx, self, "self", false)
 }
 
 func TestListChildren(t *testing.T, ctx *context.T, i interface{}) {
@@ -275,6 +315,7 @@
 	util.AccessController
 	Create(ctx *context.T, perms access.Permissions) error
 	Delete(ctx *context.T) error
+	Exists(ctx *context.T) (bool, error)
 	ListChildren(ctx *context.T) ([]string, error)
 	Child(childName string) layer
 }
@@ -289,6 +330,9 @@
 func (s *service) Delete(ctx *context.T) error {
 	panic(notAvailable)
 }
+func (s *service) Exists(ctx *context.T) (bool, error) {
+	panic(notAvailable)
+}
 func (s *service) ListChildren(ctx *context.T) ([]string, error) {
 	return s.ListApps(ctx)
 }
@@ -385,3 +429,11 @@
 	}
 	return nil
 }
+
+func assertExists(t *testing.T, ctx *context.T, l layer, name string, want bool) {
+	if got, err := l.Exists(ctx); err != nil {
+		t.Fatal(testutil.FormatLogLine(2, "%s.Exists() failed: %v", name, err))
+	} else if got != want {
+		t.Error(testutil.FormatLogLine(2, "%s.Exists() got %v, want %v", name, got, want))
+	}
+}
diff --git a/x/ref/services/syncbase/server/app.go b/x/ref/services/syncbase/server/app.go
index 48338f5..0c8958f 100644
--- a/x/ref/services/syncbase/server/app.go
+++ b/x/ref/services/syncbase/server/app.go
@@ -56,6 +56,13 @@
 	return a.s.deleteApp(ctx, call, a.name)
 }
 
+func (a *app) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	if !a.exists {
+		return false, nil
+	}
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, a.s.st, a.stKey(), &appData{}))
+}
+
 func (a *app) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
 	if !a.exists {
 		return verror.New(verror.ErrNoExist, ctx, a.name)
diff --git a/x/ref/services/syncbase/server/nosql/database.go b/x/ref/services/syncbase/server/nosql/database.go
index 62ad371..cb97fb9 100644
--- a/x/ref/services/syncbase/server/nosql/database.go
+++ b/x/ref/services/syncbase/server/nosql/database.go
@@ -140,6 +140,13 @@
 	return d.a.DeleteNoSQLDatabase(ctx, call, d.name)
 }
 
+func (d *databaseReq) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	if !d.exists {
+		return false, nil
+	}
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, d.st, d.stKey(), &databaseData{}))
+}
+
 var rng *rand.Rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
 
 func (d *databaseReq) BeginBatch(ctx *context.T, call rpc.ServerCall, bo wire.BatchOptions) (string, error) {
diff --git a/x/ref/services/syncbase/server/nosql/row.go b/x/ref/services/syncbase/server/nosql/row.go
index 88c08c7..a562c6d 100644
--- a/x/ref/services/syncbase/server/nosql/row.go
+++ b/x/ref/services/syncbase/server/nosql/row.go
@@ -26,6 +26,11 @@
 ////////////////////////////////////////
 // RPC methods
 
+func (r *rowReq) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	_, err := r.Get(ctx, call)
+	return util.ErrorToExists(err)
+}
+
 func (r *rowReq) Get(ctx *context.T, call rpc.ServerCall) ([]byte, error) {
 	impl := func(st store.StoreReader) ([]byte, error) {
 		return r.get(ctx, call, st)
diff --git a/x/ref/services/syncbase/server/nosql/table.go b/x/ref/services/syncbase/server/nosql/table.go
index 37e1fbe..2eb1f25 100644
--- a/x/ref/services/syncbase/server/nosql/table.go
+++ b/x/ref/services/syncbase/server/nosql/table.go
@@ -77,6 +77,10 @@
 	})
 }
 
+func (t *tableReq) Exists(ctx *context.T, call rpc.ServerCall) (bool, error) {
+	return util.ErrorToExists(util.GetWithAuth(ctx, call, t.d.st, t.stKey(), &tableData{}))
+}
+
 func (t *tableReq) DeleteRowRange(ctx *context.T, call rpc.ServerCall, start, limit []byte) error {
 	impl := func(st store.StoreReadWriter) error {
 		// Check for table-level access before doing a scan.
diff --git a/x/ref/services/syncbase/server/util/store_util.go b/x/ref/services/syncbase/server/util/store_util.go
index d62c222..38fa8ce 100644
--- a/x/ref/services/syncbase/server/util/store_util.go
+++ b/x/ref/services/syncbase/server/util/store_util.go
@@ -98,6 +98,24 @@
 	return Put(ctx, st, k, v)
 }
 
+// Wraps a call to Get and returns true if Get found the object, false
+// otherwise, suppressing ErrNoExist. Access errors are suppressed as well
+// because they imply existence in some Get implementations.
+// TODO(ivanpi): Revisit once ACL specification is finalized.
+func ErrorToExists(err error) (bool, error) {
+	if err == nil {
+		return true, nil
+	}
+	switch verror.ErrorID(err) {
+	case verror.ErrNoExist.ID:
+		return false, nil
+	case verror.ErrNoAccess.ID, verror.ErrNoExistOrNoAccess.ID:
+		return false, nil
+	default:
+		return false, err
+	}
+}
+
 type OpenOptions struct {
 	CreateIfMissing bool
 	ErrorIfExists   bool