query: wire up query's delete to syncbase.

Previous CLs imlemented delete in the query package.  This change
wires it up to syncbase.  The complicating factor is that, in
the non-batch case, we don't know if we should create a
snapshot (readonly) or a transaction (read/write) when
query/engine.Exec is called.  It is only when the GetTable
function is called (on the database interface) that syncbase
knows whether it should create a snapshot or a transaction.

MultiPart: 2/2
Change-Id: I3c091d331690f8cf404ad50d0b5b9e103d059ac6
diff --git a/query/engine/public/model.go b/query/engine/public/model.go
index 0155323..b747d47 100644
--- a/query/engine/public/model.go
+++ b/query/engine/public/model.go
@@ -16,6 +16,7 @@
 	// Exec executes a syncQL query and returns the results. Headers (i.e., column
 	// names) are returned separately from results (i.e., values).
 	// q : the query (e.g., select v from Customers
+	//               (e.g., delete from Customers where k = "101")
 	Exec(q string) ([]string, syncql.ResultStream, error)
 
 	// PrepareStatement parses query q and returns a PreparedStatement.  Queries passed to
diff --git a/services/syncbase/nosql/service.vdl b/services/syncbase/nosql/service.vdl
index 4fa6bcf..d93a725 100644
--- a/services/syncbase/nosql/service.vdl
+++ b/services/syncbase/nosql/service.vdl
@@ -47,7 +47,7 @@
 	ListTables() ([]string | error) {access.Read}
 
 	// Exec executes a syncQL query and returns all results as specified by in the
-	// query's select clause. Concurrency semantics are documented in model.go.
+	// query's select/delete statement. Concurrency semantics are documented in model.go.
 	Exec(schemaVersion int32, query string) stream<_, []any> error {access.Read}
 
 	// BeginBatch creates a new batch. It returns a "batch suffix" string to
diff --git a/services/syncbase/nosql/service.vdl.go b/services/syncbase/nosql/service.vdl.go
index 7b60bed..9764466 100644
--- a/services/syncbase/nosql/service.vdl.go
+++ b/services/syncbase/nosql/service.vdl.go
@@ -1929,7 +1929,7 @@
 	// TODO(sadovsky): Maybe switch to streaming RPC.
 	ListTables(*context.T, ...rpc.CallOpt) ([]string, error)
 	// Exec executes a syncQL query and returns all results as specified by in the
-	// query's select clause. Concurrency semantics are documented in model.go.
+	// query's select/delete statement. Concurrency semantics are documented in model.go.
 	Exec(_ *context.T, schemaVersion int32, query string, _ ...rpc.CallOpt) (DatabaseExecClientCall, error)
 	// BeginBatch creates a new batch. It returns a "batch suffix" string to
 	// append to the object name of this Database, yielding an object name for the
@@ -2210,7 +2210,7 @@
 	// TODO(sadovsky): Maybe switch to streaming RPC.
 	ListTables(*context.T, rpc.ServerCall) ([]string, error)
 	// Exec executes a syncQL query and returns all results as specified by in the
-	// query's select clause. Concurrency semantics are documented in model.go.
+	// query's select/delete statement. Concurrency semantics are documented in model.go.
 	Exec(_ *context.T, _ DatabaseExecServerCall, schemaVersion int32, query string) error
 	// BeginBatch creates a new batch. It returns a "batch suffix" string to
 	// append to the object name of this Database, yielding an object name for the
@@ -2353,7 +2353,7 @@
 	// TODO(sadovsky): Maybe switch to streaming RPC.
 	ListTables(*context.T, rpc.ServerCall) ([]string, error)
 	// Exec executes a syncQL query and returns all results as specified by in the
-	// query's select clause. Concurrency semantics are documented in model.go.
+	// query's select/delete statement. Concurrency semantics are documented in model.go.
 	Exec(_ *context.T, _ *DatabaseExecServerCallStub, schemaVersion int32, query string) error
 	// BeginBatch creates a new batch. It returns a "batch suffix" string to
 	// append to the object name of this Database, yielding an object name for the
@@ -2513,7 +2513,7 @@
 		},
 		{
 			Name: "Exec",
-			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.",
+			Doc:  "// Exec executes a syncQL query and returns all results as specified by in the\n// query's select/delete statement. Concurrency semantics are documented in model.go.",
 			InArgs: []rpc.ArgDesc{
 				{"schemaVersion", ``}, // int32
 				{"query", ``},         // string
diff --git a/syncbase/nosql/batch_test.go b/syncbase/nosql/batch_test.go
index d65ebfc..56ac70f 100644
--- a/syncbase/nosql/batch_test.go
+++ b/syncbase/nosql/batch_test.go
@@ -300,6 +300,45 @@
 	tu.CheckExecError(t, ctx, roBatch, "select k, v from foo", syncql.ErrTableCantAccess.ID)
 }
 
+// Test exec of delete statement in readonly batch (it should fail).
+func TestBatchReadonlyExecDelete(t *testing.T) {
+	ctx, sName, cleanup := tu.SetupOrDie(nil)
+	defer cleanup()
+	a := tu.CreateApp(t, ctx, syncbase.NewService(sName), "a")
+	d := tu.CreateNoSQLDatabase(t, ctx, a, "d")
+	tb := tu.CreateTable(t, ctx, d, "tb")
+
+	foo := Foo{I: 4, S: "f"}
+	if err := tb.Put(ctx, "foo", foo); err != nil {
+		t.Fatalf("tb.Put() failed: %v", err)
+	}
+
+	bar := Bar{F: 0.5, S: "b"}
+	// NOTE: not best practice, but store bar as
+	// optional (by passing the address of bar to Put).
+	// This tests auto-dereferencing.
+	if err := tb.Put(ctx, "bar", &bar); err != nil {
+		t.Fatalf("tb.Put() failed: %v", err)
+	}
+
+	baz := Baz{Name: "John Doe", Active: true}
+	if err := tb.Put(ctx, "baz", baz); err != nil {
+		t.Fatalf("tb.Put() failed: %v", err)
+	}
+
+	// Begin a readonly batch.
+	roBatch, err := d.BeginBatch(ctx, wire.BatchOptions{ReadOnly: true})
+	if err != nil {
+		t.Fatalf("d.BeginBatch() failed: %v", err)
+	}
+
+	// Attempt to delete "foo" k/v pair with a syncQL delete.
+	tu.CheckExecError(t, ctx, roBatch, "delete from tb where k = \"foo\"", syncql.ErrTableCantAccess.ID)
+
+	// start a new batch
+	roBatch.Abort(ctx)
+}
+
 // Tests that BatchDatabase.Exec DOES see changes made inside the transaction
 // but before Exec is called.
 func TestBatchExec(t *testing.T) {
@@ -363,12 +402,11 @@
 	// Delete the first row (bar) and the last row (newRow).
 	// Change the baz row.  Confirm these rows are no longer fetched and that
 	// the change to baz is seen.
-	if err := rwBatchTb.Delete(ctx, "bar"); err != nil {
-		t.Fatalf("rwBatchTb.Delete(bar) failed: %v", err)
-	}
-	if err := rwBatchTb.Delete(ctx, "newRow"); err != nil {
-		t.Fatalf("rwBatchTb.Delete(newRow) failed: %v", err)
-	}
+	tu.CheckExec(t, ctx, rwBatch, "delete from tb where k = \"bar\" or k = \"newRow\"",
+		[]string{"Count"},
+		[][]*vdl.Value{
+			[]*vdl.Value{vdl.ValueOf(2)},
+		})
 	baz2 := Baz{Name: "Batman", Active: false}
 	if err := rwBatchTb.Put(ctx, "baz", baz2); err != nil {
 		t.Fatalf("tb.Put() failed: %v", err)
@@ -392,12 +430,11 @@
 	if err := rwBatchTb.Put(ctx, "newRow", newRow2); err != nil {
 		t.Fatalf("rwBatchTb.Put() failed: %v", err)
 	}
-	if err := rwBatchTb.Delete(ctx, "baz"); err != nil {
-		t.Fatalf("rwBatchTb.Delete(baz) failed: %v", err)
-	}
-	if err := rwBatchTb.Delete(ctx, "foo"); err != nil {
-		t.Fatalf("rwBatchTb.Delete(foo) failed: %v", err)
-	}
+	tu.CheckExec(t, ctx, rwBatch, "delete from tb where k = \"baz\" or k = \"foo\"",
+		[]string{"Count"},
+		[][]*vdl.Value{
+			[]*vdl.Value{vdl.ValueOf(2)},
+		})
 	tu.CheckExec(t, ctx, rwBatch, "select k, v from tb",
 		[]string{"k", "v"},
 		[][]*vdl.Value{
@@ -416,7 +453,6 @@
 	defer roBatch.Abort(ctx)
 
 	// confirm fetching all rows gets the rows committed above
-	// as it was never committed
 	tu.CheckExec(t, ctx, roBatch, "select k, v from tb",
 		[]string{"k", "v"},
 		[][]*vdl.Value{
diff --git a/syncbase/nosql/client_test.go b/syncbase/nosql/client_test.go
index 53291cf..6e6985e 100644
--- a/syncbase/nosql/client_test.go
+++ b/syncbase/nosql/client_test.go
@@ -151,6 +151,21 @@
 			[]*vdl.Value{vdl.ValueOf("baz"), vdl.ValueOf(baz)},
 		})
 
+	// Delete baz
+	tu.CheckExec(t, ctx, d, "delete from tb where Type(v) like \"%.Baz\"",
+		[]string{"Count"},
+		[][]*vdl.Value{
+			[]*vdl.Value{vdl.ValueOf(1)},
+		})
+
+	// Check that bas is no longer in the table.
+	tu.CheckExec(t, ctx, d, "select k, v from tb",
+		[]string{"k", "v"},
+		[][]*vdl.Value{
+			[]*vdl.Value{vdl.ValueOf("bar"), vdl.ValueOf(bar)},
+			[]*vdl.Value{vdl.ValueOf("foo"), vdl.ValueOf(foo)},
+		})
+
 	tu.CheckExecError(t, ctx, d, "select k, v from foo", syncql.ErrTableCantAccess.ID)
 }
 
diff --git a/syncbase/nosql/exec_test.go b/syncbase/nosql/exec_test.go
index 01970c2..7f334f1 100644
--- a/syncbase/nosql/exec_test.go
+++ b/syncbase/nosql/exec_test.go
@@ -51,17 +51,25 @@
 var t2015_07_01 time.Time
 var t2015_07_01_01_23_45 time.Time
 
+var customerTable nosql.Table
+var numbersTable nosql.Table
+var fooTable nosql.Table
+var keyIndexDataTable nosql.Table
+var bigTable nosql.Table
+
 func setup(t *testing.T) {
 	var sName string
 	ctx, sName, cleanup = tu.SetupOrDie(nil)
 	a := tu.CreateApp(t, ctx, syncbase.NewService(sName), "a")
 	db = tu.CreateNoSQLDatabase(t, ctx, a, "db")
-	customerTable := tu.CreateTable(t, ctx, db, "Customer")
-	numbersTable := tu.CreateTable(t, ctx, db, "Numbers")
-	fooTable := tu.CreateTable(t, ctx, db, "Foo")
-	keyIndexDataTable := tu.CreateTable(t, ctx, db, "KeyIndexData")
-	bigTable := tu.CreateTable(t, ctx, db, "BigTable")
+	customerTable = tu.CreateTable(t, ctx, db, "Customer")
+	numbersTable = tu.CreateTable(t, ctx, db, "Numbers")
+	fooTable = tu.CreateTable(t, ctx, db, "Foo")
+	keyIndexDataTable = tu.CreateTable(t, ctx, db, "KeyIndexData")
+	bigTable = tu.CreateTable(t, ctx, db, "BigTable")
+}
 
+func initTables(t *testing.T) {
 	t20150122131101, _ := time.Parse("Jan 2 2006 15:04:05 -0700 MST", "Jan 22 2015 13:11:01 -0800 PST")
 	t20150210161202, _ := time.Parse("Jan 2 2006 15:04:05 -0700 MST", "Feb 10 2015 16:12:02 -0800 PST")
 	t20150311101303, _ := time.Parse("Jan 2 2006 15:04:05 -0700 MST", "Mar 11 2015 10:13:03 -0700 PDT")
@@ -214,6 +222,15 @@
 	r       [][]*vdl.Value
 }
 
+type execDeleteTest struct {
+	delQuery   string
+	delHeaders []string
+	delResults [][]*vdl.Value
+	selQuery   string
+	selHeaders []string
+	selResults [][]*vdl.Value
+}
+
 type preExecFunctionTest struct {
 	query   string
 	headers []string
@@ -227,6 +244,7 @@
 func TestExecSelect(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectTest{
 		{
 			// Select values for all customer records.
@@ -240,7 +258,7 @@
 		},
 		{
 			// Select values where v.InvoiceNum is nil
-			// Since InvoiceNum does not exist for Invoice,
+			// Since InvoiceNum does not exists for Customers,
 			// this will return just customers.
 			"select v from Customer where v.InvoiceNum is nil",
 			[]string{"v"},
@@ -1117,9 +1135,455 @@
 	}
 }
 
+func TestExecDelete(t *testing.T) {
+	basic := []execDeleteTest{
+		{
+			// Delete all k/v pairs in the customer table.
+			"delete from Customer",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(10)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{},
+		},
+		{
+			// Delete Customer type k/v pairs.
+			"delete from Customer where Type(v) like \"%.Customer\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(3)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+			},
+		},
+		{
+			// Delete non-existant k/v pair.
+			"delete from Customer where k = \"foo\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(0)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete k/v pairs where v.InvoiceNum is nil
+			// Since InvoiceNum does not exist for Customers,
+			// this will delete all customer records.
+			"delete from Customer where v.InvoiceNum is nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(3)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+			},
+		},
+		{
+			// Delete values where v.InvoiceNum is nil
+			// or v.Name is nil. This will deleteall customers
+			// with the former and all invoices with the latter.
+			// Hence, all k/v paris will be deleted.
+			"delete from Customer where v.InvoiceNum is nil or v.Name is nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(10)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{},
+		},
+		{
+			// Delete where v.InvoiceNum is nil AND v.Name is nil.
+			// Nothing should be deleted.
+			"delete from Customer where v.InvoiceNum is nil and v.Name is nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(0)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete where v.InvoiceNum is not nil
+			// This will delete all invoices.
+			"delete from Customer where v.InvoiceNum is not nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(7)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete where v.InvoiceNum is not nil
+			// or v.Name is not nil. All records are deleted.
+			"delete from Customer where v.InvoiceNum is not nil or v.Name is not nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(10)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{},
+		},
+		{
+			// Delete where v.InvoiceNum is nil and v.Name is not nil.
+			// All customers are deleted.
+			"delete from Customer where v.InvoiceNum is nil and v.Name is not nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(3)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+			},
+		},
+		{
+			// Delete where v.InvoiceNum is not nil
+			// and v.Name is not nil.  Expect nothing deleted.
+			"delete from Customer where v.InvoiceNum is not nil and v.Name is not nil",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(0)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete all $88 invoices.
+			"delete from Customer where Type(v) like \"%.Invoice\" and v.Amount = 88",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(2)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete all with a key prefix of "001".
+			"delete from Customer where k like \"001%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(4)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete all with a key prefix of "002".
+			"delete from Customer where k like \"002%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(5)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete all with key prefix NOT like "002%".
+			"delete from Customer where k not like \"002%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(5)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+			},
+		},
+		{
+			// delete all with a key prefix of "001" or a key prefix of "002".
+			"delete from Customer where k like \"001%\" or k like \"002%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(9)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Delete 002% or 001%
+			"delete from Customer where k like \"002%\" or k like \"001%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(9)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Let's play with whitespace and mixed case.
+			"   dElEtE  from \n  Customer WhErE k lIkE \"002%\" oR k LiKe \"001%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(9)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Add in a like clause that accepts all strings.
+			"   dElEtE  from \n  Customer WhErE k lIkE \"002%\" oR k LiKe \"001%\" or k lIkE \"%\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(10)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{},
+		},
+		{
+			// delete customers whose last name is Masterson.
+			"delete from Customer where Type(v) like \"%.Customer\" and v.Name like \"%Masterson\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(1)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// delete where v.Address.City is "Collins" or type is Invoice.
+			"delete from Customer where v.Address.City = \"Collins\" or Type(v) like \"%.Invoice\"",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(8)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// delete where v.Bar.Baz.TitleOrValue.Value = 42
+			"delete from Foo where v.Bar.Baz.TitleOrValue.Value = 42",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(1)},
+			},
+			"select k from Foo",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+			},
+		},
+		{
+			// delete where v.Address.City = "Collins" or (type is Invoice and v.InvoiceNum is not nil).
+			// Limit 3
+			"delete from Customer where v.Address.City = \"Collins\" or (Type(v) like \"%.Invoice\" and v.InvoiceNum is not nil) limit 3",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(3)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002001")},
+				[]*vdl.Value{vdl.ValueOf("002002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// delete invoices where date is 2015-03-17
+			"delete from Customer where Type(v) like \"%.Invoice\" and Year(v.InvoiceDate, \"America/Los_Angeles\") = 2015 and Month(v.InvoiceDate, \"America/Los_Angeles\") = 3 and Day(v.InvoiceDate, \"America/Los_Angeles\") = 17",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(2)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf("001")},
+				[]*vdl.Value{vdl.ValueOf("001001")},
+				[]*vdl.Value{vdl.ValueOf("001002")},
+				[]*vdl.Value{vdl.ValueOf("001003")},
+				[]*vdl.Value{vdl.ValueOf("002")},
+				[]*vdl.Value{vdl.ValueOf("002003")},
+				[]*vdl.Value{vdl.ValueOf("002004")},
+				[]*vdl.Value{vdl.ValueOf("003")},
+			},
+		},
+		{
+			// Now will always be > 2012, so all customer records will be deleted.
+			"delete from Customer where Now() > Time(\"2006-01-02 MST\", \"2012-03-17 PDT\")",
+			[]string{"Count"},
+			[][]*vdl.Value{
+				[]*vdl.Value{vdl.ValueOf(10)},
+			},
+			"select k from Customer",
+			[]string{"k"},
+			[][]*vdl.Value{},
+		},
+	}
+
+	setup(t)
+	defer cleanup()
+	for _, test := range basic {
+		initTables(t)
+		headers, rs, err := db.Exec(ctx, test.delQuery)
+		if err != nil {
+			t.Errorf("delQuery: %s; got %v, want nil", test.delQuery, err)
+		} else {
+			// Collect results.
+			r := [][]*vdl.Value{}
+			for rs.Advance() {
+				r = append(r, rs.Result())
+			}
+			if !reflect.DeepEqual(test.delResults, r) {
+				t.Errorf("delQuery: %s; got %v, want %v", test.delQuery, r, test.delResults)
+			}
+			if !reflect.DeepEqual(test.delHeaders, headers) {
+				t.Errorf("delQuery: %s; got %v, want %v", test.delQuery, headers, test.delHeaders)
+			}
+		}
+		headers, rs, err = db.Exec(ctx, test.selQuery)
+		if err != nil {
+			t.Errorf("delQuery: %s; got %v, want nil", test.delQuery, err)
+		} else {
+			// Collect results.
+			r := [][]*vdl.Value{}
+			for rs.Advance() {
+				r = append(r, rs.Result())
+			}
+			if !reflect.DeepEqual(test.selResults, r) {
+				t.Errorf("delQuery: %s; got %v, want %v", test.delQuery, r, test.selResults)
+			}
+			if !reflect.DeepEqual(test.selHeaders, headers) {
+				t.Errorf("delQuery: %s; got %v, want %v", test.delQuery, headers, test.selHeaders)
+			}
+		}
+	}
+}
+
 func TestQuerySelectClause(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectTest{
 		{
 			// Select numeric types
@@ -1306,6 +1770,7 @@
 func TestQueryWhereClause(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectTest{
 		{
 			// Select on numeric comparisons with equals
@@ -1637,6 +2102,7 @@
 func TestQueryEscapeClause(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectTest{
 		{
 			"select k from Customer where \"abc%\" like \"abc^%\" escape '^'",
@@ -1727,6 +2193,7 @@
 func TestQueryLimitAndOffsetClauses(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectTest{
 		{
 			"select k from Customer limit 2 offset 3",
@@ -1789,6 +2256,7 @@
 func TestPreExecFunctions(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []preExecFunctionTest{
 		{
 			"select Now() from Customer",
@@ -1824,6 +2292,7 @@
 func TestExecErrors(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectErrorTest{
 		{
 			"select a from Customer",
@@ -1873,6 +2342,7 @@
 func TestQueryErrors(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectErrorTest{
 		// Produce every error in the book (make that, every one that is possible to produce).
 		{
@@ -2064,6 +2534,7 @@
 func TestQueryErrorsPlatformDependentText(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	basic := []execSelectErrorTest{
 		// These errors contain installation dependent parts to the error.  The test is
 		// more relaxed (it doesn't check the suffix) in order to account for this.
@@ -2091,6 +2562,7 @@
 func TestQueryStatementSizeExceeded(t *testing.T) {
 	setup(t)
 	defer cleanup()
+	initTables(t)
 	q := fmt.Sprintf("select a from b where c = \"%s\"", strings.Repeat("x", 12000))
 
 	_, rs, err := db.Exec(ctx, q)
diff --git a/syncbase/nosql/model.go b/syncbase/nosql/model.go
index b0a8aaf..f55be9c 100644
--- a/syncbase/nosql/model.go
+++ b/syncbase/nosql/model.go
@@ -39,10 +39,18 @@
 	ListTables(ctx *context.T) ([]string, error)
 
 	// Exec executes a syncQL query.
+	// For select statements:
 	// If no error is returned, Exec returns an array of headers (i.e., column
 	// names) and a result stream which contains an array of values for each row
 	// that matches the query.  The number of values returned in each row of the
 	// result stream will match the size of the headers string array.
+	//
+	// For delete statements:
+	// If no error is returned, Exec returns an array of headers with exactly
+	// one column: "Count" and a result stream which contains an array of length
+	// one, with a single element of type vdl.Int64.  The value represents
+	// the number of k/v pairs deleted by the statement.
+	//
 	// Concurrency semantics: It is legal to perform writes concurrently with
 	// Exec. The returned stream reads from a consistent snapshot taken at the
 	// time of the RPC, and will not reflect subsequent writes to keys not yet