swift: Filter out internal watch changes (root entity & userdata)

Hide the Root entity update from HLAPI users as it's only useful
to mark the initial state, and userdata changes as that's an
an internal collection. Added the internal ability to watch
userdata explicitly, which will also be useful if we ever want
to allow that publically.

Fixes: #1373
Change-Id: Ic939a8265eaf2adc490c89dc83320d002346d933
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
index 2be7690..bf8fb72 100644
--- a/Syncbase/Source/Database.swift
+++ b/Syncbase/Source/Database.swift
@@ -309,6 +309,29 @@
     pattern pattern: CollectionRowPattern = CollectionRowPattern.Everything,
     resumeMarker: ResumeMarker? = nil,
     handler: WatchChangeHandler) throws {
+      try addWatchChangeHandler(
+        pattern: pattern,
+        resumeMarker: resumeMarker,
+        isWatchingUserdata: false,
+        handler: handler)
+  }
+
+  /// Internal function to watch only the userData collection which normally gets filtered out.
+  func addUserDataWatchChangeHandler(
+    resumeMarker: ResumeMarker? = nil,
+    handler: WatchChangeHandler) throws {
+      try addWatchChangeHandler(
+        pattern: CollectionRowPattern(collectionName: Syncbase.USERDATA_SYNCGROUP_NAME),
+        resumeMarker: resumeMarker,
+        isWatchingUserdata: true,
+        handler: handler)
+  }
+
+  private func addWatchChangeHandler(
+    pattern pattern: CollectionRowPattern,
+    resumeMarker: ResumeMarker?,
+    isWatchingUserdata: Bool,
+    handler: WatchChangeHandler) throws {
       // Note: Eventually we'll add a watch variant that takes a query, where the query can be
       // constructed using some sort of query builder API.
       // TODO(sadovsky): Support specifying resumeMarker. Note, watch-from-resumeMarker may be
@@ -340,12 +363,15 @@
             if isCanceled {
               break
             }
-            let change = WatchChange(coreChange: coreChange)
-            // Ignore changes to userdata collection.
-            if (change.collectionId?.name != Syncbase.USERDATA_SYNCGROUP_NAME) {
-              batch.append(change)
+            // Don't pass root entity to end-user.
+            if coreChange.entityType != .Root {
+              let change = WatchChange(coreChange: coreChange)
+              // Ignore changes to userdata collection unless we're explicitly looking for it, and
+              if (isWatchingUserdata || change.collectionId?.name != Syncbase.USERDATA_SYNCGROUP_NAME) {
+                batch.append(change)
+              }
             }
-            if (!change.isContinued) {
+            if (!coreChange.isContinued) {
               if (!gotFirstBatch) {
                 gotFirstBatch = true
                 // We synchronously run on Syncbase.queue to facilitate flow control. Go blocks
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
index 1058b0b..f275fcd 100644
--- a/Syncbase/Source/Syncbase.swift
+++ b/Syncbase/Source/Syncbase.swift
@@ -137,8 +137,7 @@
         try syncgroup.createIfMissing([userDataCollection!])
       }
       // TODO(zinman): Figure out when/how this can throw and if we should handle it better.
-      try database.addWatchChangeHandler(
-        pattern: CollectionRowPattern(collectionName: Syncbase.USERDATA_SYNCGROUP_NAME),
+      try database.addUserDataWatchChangeHandler(
         handler: WatchChangeHandler(
           onInitialState: onUserDataWatchChange,
           onChangeBatch: onUserDataWatchChange,
diff --git a/Syncbase/Source/WatchChange.swift b/Syncbase/Source/WatchChange.swift
index 1f07c31..542fa9a 100644
--- a/Syncbase/Source/WatchChange.swift
+++ b/Syncbase/Source/WatchChange.swift
@@ -56,9 +56,9 @@
 /// Describes a change to a database.
 public class WatchChange: CustomStringConvertible {
   public enum EntityType: Int {
-    case Root
-    case Collection
-    case Row
+    // Root is filtered out of the higher-level API.
+    case Collection = 1
+    case Row = 2
   }
 
   public enum ChangeType: Int {
diff --git a/Syncbase/Tests/BasicDatabaseTests.swift b/Syncbase/Tests/BasicDatabaseTests.swift
index bc8f8d4..65fa4d4 100644
--- a/Syncbase/Tests/BasicDatabaseTests.swift
+++ b/Syncbase/Tests/BasicDatabaseTests.swift
@@ -64,10 +64,18 @@
       XCTAssertEqual(coreCollections.count, 1)
       XCTAssertEqual(coreCollections[0].name, Syncbase.USERDATA_SYNCGROUP_NAME)
 
+      // Expect filtered from HLAPI
+      let collections = try db.collections()
+      XCTAssertEqual(collections.count, 0)
+
       let coreSyncgroups = try db.coreDatabase.listSyncgroups()
       XCTAssertEqual(coreSyncgroups.count, 1)
       XCTAssertEqual(coreSyncgroups[0].name, Syncbase.USERDATA_SYNCGROUP_NAME)
 
+      // Expect filtered from HLAPI
+      let syncgroups = try db.syncgroups()
+      XCTAssertEqual(syncgroups.count, 0)
+
       let verSpec = try db.coreDatabase.syncgroup(Syncbase.USERDATA_SYNCGROUP_NAME).getSpec()
       XCTAssertEqual(verSpec.spec.collections.count, 1)
 
@@ -75,4 +83,40 @@
       XCTAssertEqual(verSpec.spec.isPrivate, false)
     }
   }
+
+  func testWatchIgnoresUserData() {
+    withDb { db in
+      var semaphore = dispatch_semaphore_create(0)
+      // Nothing for a generic watch.
+      try db.addWatchChangeHandler(handler: WatchChangeHandler(
+        onInitialState: { changes in
+          XCTAssertEqual(changes.count, 0)
+          dispatch_semaphore_signal(semaphore)
+        },
+        onChangeBatch: { changes in
+          XCTFail("Unexpected changes: \(changes)") },
+        onError: { err in
+          XCTFail("Unexpected error: \(err)")
+        }))
+      dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
+      // TODO(zinman): Add this when we support canceling watches.
+//      db.removeAllWatchChangeHandlers()
+
+      // Userdata only appears when explicitly asking for it.
+      semaphore = dispatch_semaphore_create(0)
+      try db.addUserDataWatchChangeHandler(
+        handler: WatchChangeHandler(
+          onInitialState: { changes in
+            XCTAssertEqual(changes.count, 1)
+            XCTAssert(changes[0].collectionId?.name == Syncbase.USERDATA_SYNCGROUP_NAME)
+            dispatch_semaphore_signal(semaphore)
+          },
+          onChangeBatch: { changes in
+            XCTFail("Unexpected changes: \(changes)") },
+          onError: { err in
+            XCTFail("Unexpected error: \(err)")
+        }))
+      dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
+    }
+  }
 }