[CalendarServer-changes] [11190] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed May 15 09:47:55 PDT 2013


Revision: 11190
          http://trac.calendarserver.org//changeset/11190
Author:   cdaboo at apple.com
Date:     2013-05-15 09:47:55 -0700 (Wed, 15 May 2013)
Log Message:
-----------
Fix depth:1 sync on home collections and also make property changes trigger a sync-token change on home child objects.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/txdav/base/propertystore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -72,7 +72,11 @@
     "calendarserver-partstat-changes",
 )
 
+calendarserver_home_sync_compliance = (
+    "calendarserver-home-sync",
+)
 
+
 @registerElement
 class TwistedCalendarSupportedComponents (WebDAVTextElement):
     """

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -535,24 +535,6 @@
 
 
 
-def updateCacheTokenOnCallback(f):
-    def wrapper(self, *args, **kwargs):
-        if hasattr(self, "notifyChanged"):
-            def updateToken(response):
-                d = self.notifyChanged()
-                d.addCallback(lambda _: response)
-                return d
-
-            d = maybeDeferred(f, self, *args, **kwargs)
-            d.addCallback(updateToken)
-            return d
-        else:
-            return f(self, *args, **kwargs)
-
-    return wrapper
-
-
-
 class DAVResource (DirectoryPrincipalPropertySearchMixIn,
                    SuperDAVResource, LoggingMixIn,
                    DirectoryRenderingMixIn, StaticRenderMixin):
@@ -563,20 +545,6 @@
     that is currently in static.py but is actually applicable to any type of resource.
     """
 
-    @updateCacheTokenOnCallback
-    def http_PROPPATCH(self, request):
-        return super(DAVResource, self).http_PROPPATCH(request)
-
-
-    @updateCacheTokenOnCallback
-    def http_DELETE(self, request):
-        return super(DAVResource, self).http_DELETE(request)
-
-
-    @updateCacheTokenOnCallback
-    def http_ACL(self, request):
-        return super(DAVResource, self).http_ACL(request)
-
     http_REPORT = http_REPORT
 
     def davComplianceClasses(self):

Modified: CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/twistedcaldav/method/report_sync_collection.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -46,7 +46,7 @@
     """
     Generate a sync-collection REPORT.
     """
-    
+
     # These resource support the report
     if not config.EnableSyncReport or element.Report(element.SyncCollection(),) not in self.supportedReports():
         log.err("sync-collection report is only allowed on calendar/inbox/addressbook/notification collection resources %s" % (self,))
@@ -55,24 +55,26 @@
             element.SupportedReport(),
             "Report not supported on this resource",
         ))
-   
+
     responses = []
 
     # Process Depth and sync-level for backwards compatibility
     # Use sync-level if present and ignore Depth, else use Depth
     if sync_collection.sync_level:
         depth = sync_collection.sync_level
+        if depth == "infinite":
+            depth = "infinity"
         descriptor = "DAV:sync-level"
     else:
         depth = request.headers.getHeader("depth", None)
         descriptor = "Depth header without DAV:sync-level"
-    
+
     if depth not in ("1", "infinity"):
         log.err("sync-collection report with invalid depth header: %s" % (depth,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid %s value" % (descriptor,)))
-        
-    propertyreq = sync_collection.property.children if sync_collection.property else None 
-    
+
+    propertyreq = sync_collection.property.children if sync_collection.property else None
+
     @inlineCallbacks
     def _namedPropertiesForResource(request, props, resource, forbidden=False):
         """
@@ -87,7 +89,7 @@
             responsecode.FORBIDDEN : [],
             responsecode.NOT_FOUND : [],
         }
-        
+
         for property in props:
             if isinstance(property, element.WebDAVElement):
                 qname = property.qname()
@@ -106,13 +108,14 @@
                         f = Failure()
                         log.err("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
                         status = statusForFailure(f, "getting property: %s" % (qname,))
-                        if status not in properties_by_status: properties_by_status[status] = []
+                        if status not in properties_by_status:
+                            properties_by_status[status] = []
                         properties_by_status[status].append(propertyName(qname))
                 else:
                     properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
-        
+
         returnValue(properties_by_status)
-    
+
     # Do some optimization of access control calculation by determining any inherited ACLs outside of
     # the child resource loop and supply those to the checkPrivileges on each child.
     filteredaces = (yield self.inheritedACEsforChildren(request))
@@ -173,11 +176,11 @@
     for name in removed:
         href = element.HRef.fromString(joinURL(request.uri, name))
         responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_FOUND)))
-    
+
     for name in notallowed:
         href = element.HRef.fromString(joinURL(request.uri, name))
         responses.append(element.StatusResponse(element.HRef.fromString(href), element.Status.fromResponseCode(responsecode.NOT_ALLOWED)))
-    
+
     if not hasattr(request, "extendedLogItems"):
         request.extendedLogItems = {}
     request.extendedLogItems["responses"] = len(responses)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -202,22 +202,7 @@
 
 calendarPrivilegeSet = _calendarPrivilegeSet()
 
-def updateCacheTokenOnCallback(f):
-    def fun(self, *args, **kwargs):
-        def _updateToken(response):
-            return self.notifyChanged().addCallback(lambda _: response)
 
-        d = maybeDeferred(f, self, *args, **kwargs)
-
-        if hasattr(self, 'notifyChanged'):
-            d.addCallback(_updateToken)
-
-        return d
-
-    return fun
-
-
-
 class CalDAVResource (
         CalDAVComplianceMixIn, SharedCollectionMixin,
         DAVResourceWithChildrenMixin, DAVResource, LoggingMixIn
@@ -391,21 +376,6 @@
     # End transitional new-store interface
 
 
-    @updateCacheTokenOnCallback
-    def http_PROPPATCH(self, request):
-        return super(CalDAVResource, self).http_PROPPATCH(request)
-
-
-    @updateCacheTokenOnCallback
-    def http_DELETE(self, request):
-        return super(CalDAVResource, self).http_DELETE(request)
-
-
-    @updateCacheTokenOnCallback
-    def http_ACL(self, request):
-        return super(CalDAVResource, self).http_ACL(request)
-
-
     ##
     # WebDAV
     ##

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -1542,6 +1542,9 @@
     compliance += customxml.calendarserver_principal_property_search_compliance
     compliance += customxml.calendarserver_principal_search_compliance
 
+    # Home Depth:1 sync report will include WebDAV property changes on home child resources
+    compliance += customxml.calendarserver_home_sync_compliance
+
     configDict.CalDAVComplianceClasses = compliance
 
 

Modified: CalendarServer/trunk/txdav/base/propertystore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/base/propertystore/sql.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/base/propertystore/sql.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -250,9 +250,12 @@
                     name=key_str, uid=uid)
             self._cacher.delete(str(self._resourceID))
 
-        # Call the registered notification callback
+        # Call the registered notification callback - we need to do this as a preCommit since it involves
+        # a bunch of deferred operations, but this propstore api is not deferred. preCommit will execute
+        # the deferreds properly, and it is fine to wait until everything else is done before sending the
+        # notifications.
         if hasattr(self, "_notifyCallback") and self._notifyCallback is not None:
-            self._notifyCallback()
+            self._txn.preCommit(self._notifyCallback)
 
         def justLogIt(f):
             f.trap(AllRetriesFailed)
@@ -278,6 +281,14 @@
                                  name=key_str, uid=uid
                                 )
             self._cacher.delete(str(self._resourceID))
+
+        # Call the registered notification callback - we need to do this as a preCommit since it involves
+        # a bunch of deferred operations, but this propstore api is not deferred. preCommit will execute
+        # the deferreds properly, and it is fine to wait until everything else is done before sending the
+        # notifications.
+        if hasattr(self, "_notifyCallback") and self._notifyCallback is not None:
+            self._txn.preCommit(self._notifyCallback)
+
         def justLogIt(f):
             f.trap(AllRetriesFailed)
             self.log_error("setting a property failed; probably nothing.")

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -636,11 +636,8 @@
         pick another from the calendar home.
         """
 
-        chm = self._homeMetaDataSchema
         componentType = "VTODO" if tasks else "VEVENT"
         test_name = "tasks" if tasks else "calendar"
-        attribute_to_test = "_default_tasks" if tasks else "_default_events"
-        column_to_set = chm.DEFAULT_TASKS if tasks else chm.DEFAULT_EVENTS
 
         defaultCalendar = (yield self.calendarWithName(test_name))
         if defaultCalendar is None or not defaultCalendar.owned():
@@ -669,13 +666,7 @@
                     # Failed to even create a default - bad news...
                     raise RuntimeError("No valid calendars to use as a default %s calendar." % (componentType,))
 
-        setattr(self, attribute_to_test, defaultCalendar._resourceID)
-        yield Update(
-            {column_to_set: defaultCalendar._resourceID},
-            Where=chm.RESOURCE_ID == self._resourceID,
-        ).on(self._txn)
-        yield self.invalidateQueryCache()
-        yield self.notifyChanged()
+        yield self.setDefaultCalendar(defaultCalendar, tasks)
 
         returnValue(defaultCalendar)
 
@@ -713,7 +704,12 @@
         yield self.invalidateQueryCache()
         yield self.notifyChanged()
 
+        # CalDAV stores the default calendar properties on the inbox so we also need to send a changed notification on that
+        inbox = (yield self.calendarWithName("inbox"))
+        if inbox is not None:
+            yield inbox.notifyChanged()
 
+
     @inlineCallbacks
     def defaultCalendar(self, componentType, create=True):
         """
@@ -773,14 +769,7 @@
                     yield default.setSupportedComponents(componentType.upper())
 
             # Update the metadata
-            chm = self._homeMetaDataSchema
-            column_to_set = chm.DEFAULT_TASKS if componentType == "VTODO" else chm.DEFAULT_EVENTS
-            setattr(self, attribute_to_test, default._resourceID)
-            yield Update(
-                {column_to_set: default._resourceID},
-                Where=chm.RESOURCE_ID == self._resourceID,
-            ).on(self._txn)
-            yield self.invalidateQueryCache()
+            yield self.setDefaultCalendar(default, componentType == "VTODO")
 
         returnValue(default)
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -1733,15 +1733,16 @@
         home = yield self.homeUnderTest()
 
         changed, deleted = yield home.resourceNamesSinceToken(
-            self.token2revision(st), "depth_is_ignored")
+            self.token2revision(st), "infinity")
 
-        self.assertEquals(set(changed), set(["calendar_1/new.ics",
+        self.assertEquals(set(changed), set(["calendar_1/",
+                                             "calendar_1/new.ics",
                                              "calendar_1/2.ics",
                                              "other-calendar/"]))
         self.assertEquals(set(deleted), set(["calendar_1/2.ics"]))
 
         changed, deleted = yield home.resourceNamesSinceToken(
-            self.token2revision(st2), "depth_is_ignored")
+            self.token2revision(st2), "infinity")
         self.assertEquals(changed, [])
         self.assertEquals(deleted, [])
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -14,7 +14,7 @@
 # limitations under the License.
 ##
 
-from calendarserver.tap.util import getRootResource, directoryFromConfig
+from calendarserver.tap.util import directoryFromConfig
 
 from pycalendar.datetime import PyCalendarDateTime
 from pycalendar.value import PyCalendarValue
@@ -1404,10 +1404,7 @@
         self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory, directoryFromConfig(config))
         yield self.populate()
 
-        self.rootResource = getRootResource(config, self._sqlCalendarStore)
-        self.directory = self._sqlCalendarStore.directoryService()
 
-
     @inlineCallbacks
     def populate(self):
         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -38,8 +38,9 @@
 from twistedcaldav.query import calendarqueryfilter
 
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.sql import Calendar
 from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
-    test_event_text
+    test_event_text, OTHER_HOME_UID
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
 from txdav.caldav.datastore.test.util import buildCalendarStore
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
@@ -47,7 +48,7 @@
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
-    _BIND_STATUS_ACCEPTED
+    _BIND_STATUS_ACCEPTED, _BIND_MODE_WRITE, _BIND_STATUS_INVITED
 from txdav.common.datastore.test.util import populateCalendarsFrom
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
@@ -1046,7 +1047,7 @@
         children = yield calendar1.listCalendarObjects()
         self.assertEqual(len(children), 3)
         new_sync_token1 = yield calendar1.syncToken()
-        self.assertEqual(new_sync_token1, original_sync_token1)
+        self.assertNotEqual(new_sync_token1, original_sync_token1)
         result = yield calendar1.getSupportedComponents()
         self.assertEquals(result, "VEVENT")
 
@@ -1692,3 +1693,87 @@
             self.assertEquals(alarm_result, None)
 
         yield self.commit()
+
+
+    @inlineCallbacks
+    def test_shareWithRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        cal = yield self.calendarUnderTest()
+        self.assertEqual(cal._bindRevision, 0)
+        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest()
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        otherCal = yield otherHome.childWithName(newCalName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_updateShareRevision(self):
+        """
+        Verify that bindRevision on calendars and shared calendars has the correct value.
+        """
+        cal = yield self.calendarUnderTest()
+        self.assertEqual(cal._bindRevision, 0)
+        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE, status=_BIND_STATUS_INVITED)
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest()
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        otherCal = yield Calendar.invitedObjectWithName(otherHome, newCalName)
+        self.assertEqual(otherCal._bindRevision, 0)
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest()
+        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        otherCal = yield Calendar.invitedObjectWithName(otherHome, newCalName)
+        yield normalCal.updateShare(otherCal, status=_BIND_STATUS_ACCEPTED)
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest()
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        otherCal = yield otherHome.childWithName(newCalName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+
+    @inlineCallbacks
+    def test_sharedRevisions(self):
+        """
+        Verify that resourceNamesSinceRevision returns all resources after initial bind and sync.
+        """
+        cal = yield self.calendarUnderTest()
+        self.assertEqual(cal._bindRevision, 0)
+        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
+        yield self.commit()
+
+        normalCal = yield self.calendarUnderTest()
+        self.assertEqual(normalCal._bindRevision, 0)
+        otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        otherCal = yield otherHome.childWithName(newCalName)
+        self.assertNotEqual(otherCal._bindRevision, 0)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision - 1)
+        self.assertNotEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        changed, deleted = yield otherCal.resourceNamesSinceRevision(otherCal._bindRevision)
+        self.assertEqual(len(changed), 0)
+        self.assertEqual(len(deleted), 0)
+
+        for depth in ("1", "infinity",):
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision - 1, depth)
+            self.assertNotEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)
+
+            changed, deleted = yield otherHome.resourceNamesSinceRevision(otherCal._bindRevision, depth)
+            self.assertEqual(len(changed), 0)
+            self.assertEqual(len(deleted), 0)

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -286,7 +286,6 @@
         self._notificationHomes = {}
         self._notifierFactories = notifierFactories
         self._notifiedAlready = set()
-        self._bumpedAlready = set()
         self._migrating = migrating
 
         extraInterfaces = []
@@ -405,25 +404,7 @@
         self._notifiedAlready.add(obj)
 
 
-    def isBumpedAlready(self, obj):
-        """
-        Indicates whether or not bumpAddedForObject has already been
-        called for the given object, in order to facilitate calling
-        bumpModified only once per object.
-        """
-        return obj in self._bumpedAlready
 
-
-    def bumpAddedForObject(self, obj):
-        """
-        Records the fact that a bumpModified( ) call has already been
-        done, in order to facilitate calling bumpModified only once per
-        object.
-        """
-        self._bumpedAlready.add(obj)
-
-
-
 class StubResource(object):
     """
     Just enough resource to keep the shared sql DB classes going.

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2013-05-15 16:47:55 UTC (rev 11190)
@@ -452,7 +452,7 @@
         self._notificationHomes = {}
         self._notifierFactories = notifierFactories
         self._notifiedAlready = set()
-        self._bumpedAlready = set()
+        self._bumpedRevisionAlready = set()
         self._label = label
         self._migrating = migrating
         self._primaryHomeType = None
@@ -886,22 +886,20 @@
         self._notifiedAlready.add(obj.id())
 
 
-    def isBumpedAlready(self, obj):
+    def isRevisionBumpedAlready(self, obj):
         """
-        Indicates whether or not bumpAddedForObject has already been
-        called for the given object, in order to facilitate calling
-        bumpModified only once per object.
+        Indicates whether or not bumpRevisionForObject has already been
+        called for the given object, in order to facilitate changing the
+        revision only once per object.
         """
-        return obj.id() in self._bumpedAlready
+        return obj.id() in self._bumpedRevisionAlready
 
 
-    def bumpAddedForObject(self, obj):
+    def bumpRevisionForObject(self, obj):
         """
-        Records the fact that a bumpModified( ) call has already been
-        done, in order to facilitate calling bumpModified only once per
-        object.
+        Records the fact that a revision token for the object has been bumped.
         """
-        self._bumpedAlready.add(obj.id())
+        self._bumpedRevisionAlready.add(obj.id())
 
     _savepointCounter = 0
 
@@ -1783,6 +1781,14 @@
     def _syncTokenQuery(cls): #@NoSelf
         """
         DAL Select statement to find the sync token.
+
+        This is the max(REVISION) from the union of:
+
+        1) REVISION's for all object resources in all home child collections in the targeted home
+        2) REVISION's for all child collections in the targeted home
+
+        Note the later is needed to track changes directly to the home child themselves (e.g.
+        property changes, deletion etc).
         """
         rev = cls._revisionsSchema
         bind = cls._bindSchema
@@ -1812,6 +1818,16 @@
         )
 
 
+    def revisionFromToken(self, token):
+        if token is None:
+            return 0
+        elif isinstance(token, str):
+            _ignore_uuid, revision = token.split("_", 1)
+            return int(revision)
+        else:
+            return token
+
+
     @inlineCallbacks
     def syncToken(self):
         """
@@ -1829,23 +1845,58 @@
     def _changesQuery(cls): #@NoSelf
         bind = cls._bindSchema
         rev = cls._revisionsSchema
-        return Select([bind.RESOURCE_NAME, rev.COLLECTION_NAME,
-                       rev.RESOURCE_NAME, rev.DELETED],
-                      From=rev.join(
-                          bind,
-                          (bind.HOME_RESOURCE_ID ==
-                           Parameter("resourceID")).And(
-                               rev.RESOURCE_ID ==
-                               bind.RESOURCE_ID),
-                          'left outer'),
-                      Where=(rev.REVISION > Parameter("token")).And(
-                          rev.HOME_RESOURCE_ID ==
-                          Parameter("resourceID")))
+        return Select(
+            [
+                bind.RESOURCE_NAME,
+                rev.COLLECTION_NAME,
+                rev.RESOURCE_NAME,
+                rev.DELETED,
+            ],
+            From=rev.join(
+                bind,
+                (bind.HOME_RESOURCE_ID == Parameter("resourceID")).And
+                (rev.RESOURCE_ID == bind.RESOURCE_ID),
+                'left outer'
+            ),
+            Where=(rev.REVISION > Parameter("revision")).And
+                  (rev.HOME_RESOURCE_ID == Parameter("resourceID"))
+        )
 
 
-    @inlineCallbacks
     def resourceNamesSinceToken(self, token, depth):
+        """
+        Return the changed and deleted resources since a particular sync-token. This simply extracts
+        the revision from from the token then calls L{resourceNamesSinceRevision}.
 
+        @param revision: the revision to determine changes since
+        @type revision: C{int}
+        """
+
+        return self.resourceNamesSinceRevision(self.revisionFromToken(token), depth)
+
+
+    @inlineCallbacks
+    def resourceNamesSinceRevision(self, revision, depth):
+        """
+        Determine the list of child resources that have changed since the specified sync revision.
+        We do the same SQL query for both depth "1" and "infinity", but filter the results for
+        "1" to only account for a collection change.
+
+        We need to handle shared collection a little differently from owned ones. When a shared collection
+        is bound into a home we record a revision for it using the sharee home id and sharee collection name.
+        That revision is the "starting point" for changes: so if sync occurs with a revision earlier than
+        that, we return the list of all resources in the shared collection since they are all "new" as far
+        as the client is concerned since the shared collection has just appeared. For a later revision, we
+        just report the changes since that one. When a shared collection is removed from a home, we again
+        record a revision for the sharee home and sharee collection name with the "deleted" flag set. That way
+        the shared collection can be reported as removed.
+
+        @param revision: the sync revision to compare to
+        @type revision: C{str}
+        @param depth: depth for determine what changed
+        @type depth: C{str}
+        """
+
         results = [
             (
                 path if path else (collection if collection else ""),
@@ -1855,33 +1906,48 @@
             for path, collection, name, wasdeleted in
             (yield self._changesQuery.on(self._txn,
                                          resourceID=self._resourceID,
-                                         token=token))
+                                         revision=revision))
         ]
 
-        deleted = []
+        changed = set()
+        deleted = set()
         deleted_collections = set()
         changed_collections = set()
         for path, name, wasdeleted in results:
             if wasdeleted:
-                if token:
-                    deleted.append("%s/%s" % (path, name,))
-                if not name:
-                    deleted_collections.add(path)
+                if revision:
+                    if name:
+                        # Resource deleted - for depth "1" report collection as changed,
+                        # otherwise report resource as deleted
+                        if depth == "1":
+                            changed.add("%s/" % (path,))
+                        else:
+                            deleted.add("%s/%s" % (path, name,))
+                    else:
+                        # Collection was deleted
+                        deleted.add("%s/" % (path,))
+                        deleted_collections.add(path)
 
-        changed = []
         for path, name, wasdeleted in results:
             if path not in deleted_collections:
-                changed.append("%s/%s" % (path, name,))
-                if not name:
+                # Always report collection as changed
+                changed.add("%s/" % (path,))
+                if name:
+                    # Resource changed - for depth "infinity" report resource as changed
+                    if depth != "1":
+                        changed.add("%s/%s" % (path, name,))
+                else:
+                    # Collection was changed
                     changed_collections.add(path)
 
         # Now deal with shared collections
+        # TODO: think about whether this can be done in one query rather than looping over each share
         bind = self._bindSchema
         rev = self._revisionsSchema
         shares = yield self.children()
         for share in shares:
             if not share.owned():
-                sharetoken = 0 if share.name() in changed_collections else token
+                sharerevision = 0 if revision < share._bindRevision else revision
                 shareID = (yield Select(
                     [bind.RESOURCE_ID], From=bind,
                     Where=(bind.RESOURCE_NAME == share.name()).And(
@@ -1897,21 +1963,24 @@
                     for name, wasdeleted in
                     (yield Select([rev.RESOURCE_NAME, rev.DELETED],
                                      From=rev,
-                                    Where=(rev.REVISION > sharetoken).And(
+                                    Where=(rev.REVISION > sharerevision).And(
                                     rev.RESOURCE_ID == shareID)).on(self._txn))
                     if name
                 ]
 
                 for path, name, wasdeleted in results:
                     if wasdeleted:
-                        if sharetoken:
-                            deleted.append("%s/%s" % (path, name,))
+                        if sharerevision:
+                            if depth == "1":
+                                changed.add("%s/" % (path,))
+                            else:
+                                deleted.add("%s/%s" % (path, name,))
 
                 for path, name, wasdeleted in results:
-                    changed.append("%s/%s" % (path, name,))
+                    changed.add("%s/%s" % (path, "" if depth == "1" else name,))
 
-        changed.sort()
-        deleted.sort()
+        changed = sorted(changed)
+        deleted = sorted(deleted)
         returnValue((changed, deleted))
 
 
@@ -2118,18 +2187,22 @@
     @inlineCallbacks
     def notifyChanged(self):
         """
-        Trigger a notification of a change
+        Send notifications, change sync token and bump last modified because the resource has changed. We ensure
+        we only do this once per object per transaction.
         """
 
+        if self._txn.isNotifiedAlready(self):
+            returnValue(None)
+        self._txn.notificationAddedForObject(self)
+
         # Update modified if object still exists
         if self._resourceID:
             yield self.bumpModified()
 
-        # Only send one set of change notifications per transaction
-        if self._notifiers and not self._txn.isNotifiedAlready(self):
+        # Send notifications
+        if self._notifiers:
             for notifier in self._notifiers.values():
                 self._txn.postCommit(notifier.notify)
-            self._txn.notificationAddedForObject(self)
 
 
     @classproperty
@@ -2161,10 +2234,6 @@
         delay the transaction whilst waiting for deadlock detection to kick in.
         """
 
-        if self._txn.isBumpedAlready(self):
-            returnValue(None)
-        self._txn.bumpAddedForObject(self)
-
         # NB if modified is bumped we know that sync token will have changed too, so invalidate the cached value
         self._syncTokenRevision = None
 
@@ -2215,8 +2284,13 @@
 
 
     def revisionFromToken(self, token):
-        _ignore_uuid, revision = token.split("_", 1)
-        return int(revision)
+        if token is None:
+            return 0
+        elif isinstance(token, str):
+            _ignore_uuid, revision = token.split("_", 1)
+            return int(revision)
+        else:
+            return token
 
 
     @inlineCallbacks
@@ -2243,19 +2317,32 @@
                           rev.RESOURCE_ID == Parameter("resourceID")))
 
 
-    @inlineCallbacks
     def resourceNamesSinceToken(self, token):
+        """
+        Return the changed and deleted resources since a particular sync-token. This simply extracts
+        the revision from from the token then calls L{resourceNamesSinceRevision}.
 
-        if token is None:
-            token = 0
-        elif isinstance(token, str):
-            token = self.revisionFromToken(token)
+        @param revision: the revision to determine changes since
+        @type revision: C{int}
+        """
 
+        return self.resourceNamesSinceRevision(self.revisionFromToken(token))
+
+
+    @inlineCallbacks
+    def resourceNamesSinceRevision(self, revision):
+        """
+        Return the changed and deleted resources since a particular revision.
+
+        @param revision: the revision to determine changes since
+        @type revision: C{int}
+        """
+
         results = [
             (name if name else "", deleted)
             for name, deleted in
             (yield self._objectNamesSinceRevisionQuery.on(
-                self._txn, revision=token, resourceID=self._resourceID))
+                self._txn, revision=revision, resourceID=self._resourceID))
         ]
         results.sort(key=lambda x: x[1])
 
@@ -2264,7 +2351,7 @@
         for name, wasdeleted in results:
             if name:
                 if wasdeleted:
-                    if token:
+                    if revision:
                         deleted.append(name)
                 else:
                     changed.append(name)
@@ -2302,6 +2389,7 @@
             self._addNewRevision.on(self._txn, homeID=self._home._resourceID,
                                     resourceID=self._resourceID,
                                     collectionName=self._name)))[0][0]
+        self._txn.bumpRevisionForObject(self)
 
 
     @classproperty
@@ -2311,11 +2399,13 @@
         resource name).
         """
         rev = cls._revisionsSchema
-        return Update({
-            rev.REVISION: schema.REVISION_SEQ,
-            rev.COLLECTION_NAME: Parameter("name")},
-            Where=(rev.RESOURCE_ID == Parameter("resourceID")
-                  ).And(rev.RESOURCE_NAME == None),
+        return Update(
+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.COLLECTION_NAME: Parameter("name")
+            },
+            Where=(rev.RESOURCE_ID == Parameter("resourceID")).And
+                  (rev.RESOURCE_NAME == None),
             Return=rev.REVISION
         )
 
@@ -2324,18 +2414,44 @@
     def _renameSyncToken(self):
         self._syncTokenRevision = (yield self._renameSyncTokenQuery.on(
             self._txn, name=self._name, resourceID=self._resourceID))[0][0]
+        self._txn.bumpRevisionForObject(self)
 
 
     @classproperty
+    def _bumpSyncTokenQuery(cls): #@NoSelf
+        """
+        DAL query to change collection sync token.
+        """
+        rev = cls._revisionsSchema
+        return Update(
+            {rev.REVISION: schema.REVISION_SEQ, },
+            Where=(rev.RESOURCE_ID == Parameter("resourceID")).And
+                  (rev.RESOURCE_NAME == None),
+            Return=rev.REVISION
+        )
+
+
+    @inlineCallbacks
+    def _bumpSyncToken(self):
+
+        if not self._txn.isRevisionBumpedAlready(self):
+            self._txn.bumpRevisionForObject(self)
+            self._syncTokenRevision = (yield self._bumpSyncTokenQuery.on(
+                self._txn, resourceID=self._resourceID))[0][0]
+
+
+    @classproperty
     def _deleteSyncTokenQuery(cls): #@NoSelf
         """
         DAL query to update a sync revision to be a tombstone instead.
         """
         rev = cls._revisionsSchema
-        return Delete(From=rev, Where=(
-            rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
-                rev.RESOURCE_ID == Parameter("resourceID")).And(
-                rev.COLLECTION_NAME == None))
+        return Delete(
+            From=rev,
+            Where=(rev.HOME_RESOURCE_ID == Parameter("homeID")).And
+                  (rev.RESOURCE_ID == Parameter("resourceID")).And
+                  (rev.COLLECTION_NAME == None)
+        )
 
 
     @classproperty
@@ -2527,7 +2643,7 @@
     _objectTable = None
 
 
-    def __init__(self, home, name, resourceID, mode, status, message=None, ownerHome=None, ownerName=None):
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
 
         self._home = home
         self._name = name
@@ -2535,6 +2651,7 @@
         self._bindMode = mode
         self._bindStatus = status
         self._bindMessage = message
+        self._bindRevision = revision
         self._ownerHome = home if ownerHome is None else ownerHome
         self._ownerName = name if ownerName is None else ownerName
         self._created = None
@@ -2610,6 +2727,26 @@
 
 
     @classmethod
+    def regularBindColumns(cls):
+        """
+        Return a list of column names for retrieval during creation. This allows
+        different child classes to have their own type specific data, but still make use of the
+        common base logic.
+        """
+
+        return (
+            cls._bindSchema.BIND_MODE,
+            cls._bindSchema.HOME_RESOURCE_ID,
+            cls._bindSchema.RESOURCE_ID,
+            cls._bindSchema.RESOURCE_NAME,
+            cls._bindSchema.BIND_STATUS,
+            cls._bindSchema.BIND_REVISION,
+            cls._bindSchema.MESSAGE
+        )
+
+    bindColumnCount = 7
+
+    @classmethod
     def additionalBindColumns(cls):
         """
         Return a list of column names for retrieval during creation. This allows
@@ -2674,12 +2811,7 @@
         child = cls._homeChildSchema
         childMetaData = cls._homeChildMetaDataSchema
 
-        columns = [bind.BIND_MODE,
-                   bind.HOME_RESOURCE_ID,
-                   bind.RESOURCE_ID,
-                   bind.RESOURCE_NAME,
-                   bind.BIND_STATUS,
-                   bind.MESSAGE]
+        columns = list(cls.regularBindColumns())
         columns.extend(cls.additionalBindColumns())
         columns.extend(cls.metadataColumns())
         return Select(columns,
@@ -2758,6 +2890,11 @@
                 resourceID=self._resourceID, homeID=shareeHome._resourceID
             ))[0][0]
 
+        if status == _BIND_STATUS_ACCEPTED:
+            shareeView = yield shareeHome.childWithName(sharedName)
+            yield shareeView._initSyncToken()
+            yield shareeView._initBindRevision()
+
         # Must send notification to ensure cache invalidation occurs
         yield self.notifyChanged()
         yield shareeHome.notifyChanged()
@@ -2820,6 +2957,7 @@
                 shareeView._bindStatus = columnMap[bind.BIND_STATUS]
                 if shareeView._bindStatus == _BIND_STATUS_ACCEPTED:
                     yield shareeView._initSyncToken()
+                    yield shareeView._initBindRevision()
                 elif shareeView._bindStatus == _BIND_STATUS_DECLINED:
                     shareeView._deletedSyncToken(sharedRemoval=True)
 
@@ -2888,6 +3026,23 @@
         returnValue(resourceName)
 
 
+    @inlineCallbacks
+    def _initBindRevision(self):
+        bind = self._bindSchema
+        self._bindRevision = self._syncTokenRevision
+        yield Update(
+            {bind.BIND_REVISION : Parameter("revision"), },
+            Where=(bind.RESOURCE_ID == Parameter("resourceID")).And
+                  (bind.HOME_RESOURCE_ID == Parameter("homeID")),
+        ).on(
+            self._txn,
+            revision=self._bindRevision,
+            resourceID=self._resourceID,
+            homeID=self.viewerHome()._resourceID,
+        )
+        yield self.invalidateQueryCache()
+
+
     def shareMode(self):
         """
         @see: L{ICalendar.shareMode}
@@ -2982,12 +3137,7 @@
     @classmethod
     def _bindFor(cls, condition): #@NoSelf
         bind = cls._bindSchema
-        columns = [bind.BIND_MODE,
-                   bind.HOME_RESOURCE_ID,
-                   bind.RESOURCE_ID,
-                   bind.RESOURCE_NAME,
-                   bind.BIND_STATUS,
-                   bind.MESSAGE]
+        columns = list(cls.regularBindColumns())
         columns.extend(cls.additionalBindColumns())
         return Select(
             columns,
@@ -3028,8 +3178,8 @@
         cls = self.__class__ # for ease of grepping...
         result = []
         for item in acceptedRows:
-            bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = item[:6] #@UnusedVariable
-            additionalBind = item[6:]
+            bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = item[:self.bindColumnCount] #@UnusedVariable
+            additionalBind = item[self.bindColumnCount:]
 
             assert bindStatus == _BIND_STATUS_ACCEPTED
             # TODO: this could all be issued in parallel; no need to serialize
@@ -3038,6 +3188,7 @@
                 home=(yield self._txn.homeWithResourceID(self._home._homeType, homeID)),
                 name=resourceName, resourceID=self._resourceID,
                 mode=bindMode, status=bindStatus,
+                revision=bindRevision,
                 message=bindMessage, ownerHome=self._home
             )
             yield new.initFromStore(additionalBind)
@@ -3075,14 +3226,15 @@
 
         result = []
         for item in rows:
-            bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = item[:6] #@UnusedVariable
-            additionalBind = item[6:]
+            bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = item[:self.bindColumnCount] #@UnusedVariable
+            additionalBind = item[self.bindColumnCount:]
             # TODO: this could all be issued in parallel; no need to serialize
             # the loop.
             new = cls(
                 home=(yield self._txn.homeWithResourceID(self._home._homeType, homeID)),
                 name=resourceName, resourceID=self._resourceID,
                 mode=bindMode, status=bindStatus,
+                revision=bindRevision,
                 message=bindMessage, ownerHome=self._home
             )
             yield new.initFromStore(additionalBind)
@@ -3127,9 +3279,9 @@
 
         # Create the actual objects merging in properties
         for items in dataRows:
-            bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = items[:6] #@UnusedVariable
-            additionalBind = items[6:6 + len(cls.additionalBindColumns())]
-            metadata = items[6 + len(cls.additionalBindColumns()):]
+            bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = items[:cls.bindColumnCount] #@UnusedVariable
+            additionalBind = items[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+            metadata = items[cls.bindColumnCount + len(cls.additionalBindColumns()):]
 
             if bindStatus == _BIND_MODE_OWN:
                 ownerHome = home
@@ -3143,7 +3295,7 @@
                 home=home,
                 name=resourceName, resourceID=resourceID,
                 mode=bindMode, status=bindStatus,
-                message=bindMessage,
+                revision=bindRevision, message=bindMessage,
                 ownerHome=ownerHome, ownerName=ownerName
             )
             for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
@@ -3193,8 +3345,8 @@
             returnValue(None)
 
         item = rows[0]
-        bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = item[:6] #@UnusedVariable
-        additionalBind = item[6:]
+        bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = item[:cls.bindColumnCount] #@UnusedVariable
+        additionalBind = item[cls.bindColumnCount:]
 
         #TODO:  combine with _invitedBindForNameAndHomeID and sort results
         ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(home._txn, resourceID=resourceID))[0]
@@ -3204,7 +3356,7 @@
             home=home,
             name=resourceName, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage,
+            revision=bindRevision, message=bindMessage,
             ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore(additionalBind)
@@ -3249,7 +3401,7 @@
 
             if rows:
                 item = rows[0]
-                bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = item[:6] #@UnusedVariable
+                bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = item[:cls.bindColumnCount] #@UnusedVariable
                 # get ownerHomeID
                 if bindMode == _BIND_MODE_OWN:
                     ownerHomeID = homeID
@@ -3257,8 +3409,8 @@
                 else:
                     ownerHomeID, ownerName = (yield cls._ownerHomeWithResourceID.on(
                                     home._txn, resourceID=resourceID))[0]
-                rows[0].insert(6, ownerHomeID)
-                rows[0].insert(7, ownerName)
+                rows[0].insert(cls.bindColumnCount, ownerHomeID)
+                rows[0].insert(cls.bindColumnCount + 1, ownerName)
 
             if rows and queryCacher:
                 # Cache the result
@@ -3268,8 +3420,8 @@
             returnValue(None)
 
         item = rows[0] #@UnusedVariable
-        bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage, ownerHomeID, ownerName = item[:8]
-        additionalBind = item[8:]
+        bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage, ownerHomeID, ownerName = item[:cls.bindColumnCount + 2]
+        additionalBind = item[cls.bindColumnCount + 2:]
 
         if bindMode == _BIND_MODE_OWN:
             ownerHome = home
@@ -3280,7 +3432,7 @@
             home=home,
             name=name, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage,
+            revision=bindRevision, message=bindMessage,
             ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore(additionalBind)
@@ -3317,8 +3469,8 @@
             returnValue(None)
 
         item = rows[0]
-        bindMode, homeID, resourceID, resourceName, bindStatus, bindMessage = item[:6] #@UnusedVariable
-        additionalBind = item[6:]
+        bindMode, homeID, resourceID, resourceName, bindStatus, bindRevision, bindMessage = item[:cls.bindColumnCount] #@UnusedVariable
+        additionalBind = item[cls.bindColumnCount:]
 
         if bindMode == _BIND_MODE_OWN:
             ownerHome = home
@@ -3330,7 +3482,7 @@
             home=home,
             name=resourceName, resourceID=resourceID,
             mode=bindMode, status=bindStatus,
-            message=bindMessage,
+            revision=bindRevision, message=bindMessage,
             ownerHome=ownerHome, ownerName=ownerName,
         )
         yield child.initFromStore(additionalBind)
@@ -3940,6 +4092,22 @@
         return False
 
 
+    def resourceNamesSinceRevision(self, revision):
+        """
+        Return the changed and deleted resources since a particular revision. This implementation takes
+        into account sharing by making use of the bindRevision attribute to determine if the requested
+        revision is earlier than the share acceptance. If so, then we need to return all resources in
+        the results since the collection is in effect "new".
+
+        @param revision: the revision to determine changes since
+        @type revision: C{int}
+        """
+
+        if revision < self._bindRevision:
+            revision = 0
+        return super(CommonHomeChild, self).resourceNamesSinceRevision(revision)
+
+
     @inlineCallbacks
     def _loadPropertyStore(self, props=None):
         if props is None:
@@ -4010,18 +4178,25 @@
     @inlineCallbacks
     def notifyChanged(self):
         """
-        Trigger a notification of a change
+        Send notifications, change sync token and bump last modified because the resource has changed. We ensure
+        we only do this once per object per transaction.
         """
 
+        if self._txn.isNotifiedAlready(self):
+            returnValue(None)
+        self._txn.notificationAddedForObject(self)
+
         # Update modified if object still exists
         if self._resourceID:
             yield self.bumpModified()
 
-        # Only send one set of change notifications per transaction
-        if self._notifiers and not self._txn.isNotifiedAlready(self):
+            # We now also bump the collection level sync token on any change
+            yield self._bumpSyncToken()
+
+        # Send notifications
+        if self._notifiers:
             for notifier in self._notifiers.values():
                 self._txn.postCommit(notifier.notify)
-            self._txn.notificationAddedForObject(self)
 
 
     @classproperty
@@ -4053,10 +4228,6 @@
         delay the transaction whilst waiting for deadlock detection to kick in.
         """
 
-        if self._txn.isBumpedAlready(self):
-            returnValue(None)
-        self._txn.bumpAddedForObject(self)
-
         @inlineCallbacks
         def _bumpModified(subtxn):
             yield self._lockLastModifiedQuery.on(subtxn, resourceID=self._resourceID)
@@ -4822,14 +4993,18 @@
 
     def notifyChanged(self):
         """
-        Trigger a notification of a change
+        Send notifications, change sync token and bump last modified because the resource has changed. We ensure
+        we only do this once per object per transaction.
         """
 
-        # Only send one set of change notifications per transaction
-        if self._notifiers and not self._txn.isNotifiedAlready(self):
+        if self._txn.isNotifiedAlready(self):
+            returnValue(None)
+        self._txn.notificationAddedForObject(self)
+
+        # Send notifications
+        if self._notifiers:
             for notifier in self._notifiers.values():
                 self._txn.postCommit(notifier.notify)
-            self._txn.notificationAddedForObject(self)
 
 
     @classproperty

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-05-15 16:47:55 UTC (rev 11190)
@@ -68,6 +68,7 @@
     "CALENDAR_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
     "MESSAGE" nclob,
     "TRANSP" integer default 0 not null,
     "ALARM_VEVENT_TIMED" nclob default null,
@@ -228,6 +229,7 @@
     "ADDRESSBOOK_RESOURCE_NAME" nvarchar2(255),
     "BIND_MODE" integer not null,
     "BIND_STATUS" integer not null,
+    "BIND_REVISION" integer default 0 not null,
     "MESSAGE" nclob, 
     primary key("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_ID"), 
     unique("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-05-15 16:47:55 UTC (rev 11190)
@@ -130,6 +130,7 @@
   CALENDAR_RESOURCE_NAME    varchar(255) not null,
   BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION				integer      default 0 not null,
   MESSAGE                   text,
   TRANSP                    integer      default 0 not null, -- enum CALENDAR_TRANSP
   ALARM_VEVENT_TIMED        text         default null,
@@ -405,6 +406,7 @@
   ADDRESSBOOK_RESOURCE_NAME    varchar(255) not null,
   BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
   BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
+  BIND_REVISION				   integer      default 0 not null,
   MESSAGE                      text,                  -- FIXME: xml?
 
   primary key (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID), -- implicit index

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql	2013-05-15 16:47:55 UTC (rev 11190)
@@ -32,7 +32,8 @@
 -- Calendar bind related updates
 
 alter table CALENDAR_BIND
- add ("TRANSP" integer default 0 not null,
+ add ("BIND_REVISION" integer default 0 not null,
+      "TRANSP" integer default 0 not null,
       "ALARM_VEVENT_TIMED" nclob default null,
       "ALARM_VEVENT_ALLDAY" nclob default null,
       "ALARM_VTODO_TIMED" nclob default null,
@@ -46,6 +47,13 @@
 insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
 insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
 
+ 	  
+-- Addressbook bind related updates
+
+alter table ADDRESSBOOK_BIND
+ add ("BIND_REVISION" integer default 0 not null);
+
+ 
 -- Now update the version
 -- No data upgrades
 update CALENDARSERVER set VALUE = '19' where NAME = 'VERSION';

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql	2013-05-15 16:44:50 UTC (rev 11189)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql	2013-05-15 16:47:55 UTC (rev 11190)
@@ -33,6 +33,7 @@
 -- Calendar bind related updates
 
 alter table CALENDAR_BIND
+ add column BIND_REVISION			 integer     default 0 not null,
  add column TRANSP                   integer     default 0 not null,
  add column ALARM_VEVENT_TIMED       text        default null,
  add column ALARM_VEVENT_ALLDAY      text        default null,
@@ -47,6 +48,13 @@
 insert into CALENDAR_TRANSP values (0, 'opaque' );
 insert into CALENDAR_TRANSP values (1, 'transparent');
 
+
+-- Addressbook bind related updates
+
+alter table ADDRESSBOOK_BIND
+ add column BIND_REVISION			 integer     default 0 not null;
+
+
 -- Now update the version
 -- No data upgrades
 update CALENDARSERVER set VALUE = '19' where NAME = 'VERSION';
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130515/d856fc4b/attachment-0001.html>


More information about the calendarserver-changes mailing list