[CalendarServer-changes] [14247] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Jan 6 08:44:21 PST 2015


Revision: 14247
          http://trac.calendarserver.org//changeset/14247
Author:   cdaboo at apple.com
Date:     2015-01-06 08:44:21 -0800 (Tue, 06 Jan 2015)
Log Message:
-----------
Fix for sequence handling to cope with async iTIP messaging race conditions.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/calendarserver/tools/calverify.py
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/requirements-stable.txt
    CalendarServer/trunk/twistedcaldav/datafilters/hiddeninstance.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_work.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py
    CalendarServer/trunk/txdav/common/datastore/sql_dump.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/test/test_sql_schema_files.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py
    CalendarServer/trunk/txdav/who/cache.py
    CalendarServer/trunk/txdav/who/test/test_group_attendees.py

Added Paths:
-----------
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v49.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v49.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_49_to_50.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_49_to_50.sql
    CalendarServer/trunk/txdav/common/datastore/test/test_sql_dump.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -580,8 +580,8 @@
         )
         root.putChild(".well-known", wellKnownResource)
         for enabled, wellknown_name, redirected_to in (
-            (config.EnableCalDAV, "caldav", "/",),
-            (config.EnableCardDAV, "carddav", "/",),
+            (config.EnableCalDAV, "caldav", "/principals",),
+            (config.EnableCardDAV, "carddav", "/principals",),
             (config.TimezoneService.Enabled, "timezone", "/stdtimezones",),
             (config.Scheduling.iSchedule.Enabled, "ischedule", "/ischedule"),
         ):

Modified: CalendarServer/trunk/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/calverify.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/calendarserver/tools/calverify.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -61,10 +61,7 @@
 from twisted.python.usage import Options
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.dateops import pyCalendarTodatetime
-from twistedcaldav.ical import (
-    Component, ignoredComponents,
-    InvalidICalendarDataError, Property, PERUSER_COMPONENT
-)
+from twistedcaldav.ical import Component, InvalidICalendarDataError, Property, PERUSER_COMPONENT
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 from twistedcaldav.timezones import TimezoneCache
 from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
@@ -1166,9 +1163,7 @@
             self.cuaCache[cuaddr] = result
             returnValue(result)
 
-        for subcomponent in component.subcomponents():
-            if subcomponent.name() in ignoredComponents:
-                continue
+        for subcomponent in component.subcomponents(ignore=True):
             organizer = subcomponent.getProperty("ORGANIZER")
             if organizer:
                 cuaddr = organizer.value()
@@ -2016,9 +2011,7 @@
         Set the TRANSP property based on the PARTSTAT value on matching ATTENDEE properties
         in each component.
         """
-        for component in calendar.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in calendar.subcomponents(ignore=True):
             prop = component.getAttendeeProperty(attendee)
             addTransp = False
             if prop:

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -236,7 +236,6 @@
         sow = schema.SCHEDULE_ORGANIZER_WORK
         sosw = schema.SCHEDULE_ORGANIZER_SEND_WORK
         srw = schema.SCHEDULE_REPLY_WORK
-        srcw = schema.SCHEDULE_REPLY_CANCEL_WORK
         rows = yield Select(
             [sow.HOME_RESOURCE_ID],
             From=sow,
@@ -251,13 +250,6 @@
                             [srw.HOME_RESOURCE_ID],
                             From=srw,
                             Where=(srw.HOME_RESOURCE_ID == self.homeResourceID),
-                            SetExpression=Union(
-                                Select(
-                                    [srcw.HOME_RESOURCE_ID],
-                                    From=srcw,
-                                    Where=(srcw.HOME_RESOURCE_ID == self.homeResourceID),
-                                )
-                            ),
                         )
                     ),
                 )

Modified: CalendarServer/trunk/requirements-stable.txt
===================================================================
--- CalendarServer/trunk/requirements-stable.txt	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/requirements-stable.txt	2015-01-06 16:44:21 UTC (rev 14247)
@@ -5,9 +5,9 @@
 # For CalendarServer development, don't try to get these projects from PyPI; use svn.
 
 -e .
--e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@14228#egg=twextpy
+-e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@14246#egg=twextpy
 -e svn+http://svn.calendarserver.org/repository/calendarserver/PyKerberos/trunk@13420#egg=kerberos
--e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14234#egg=pycalendar
+-e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@14245#egg=pycalendar
 
 # Specify specific versions of our dependencies so that we're all testing the same config.
 

Modified: CalendarServer/trunk/twistedcaldav/datafilters/hiddeninstance.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/hiddeninstance.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/twistedcaldav/datafilters/hiddeninstance.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -15,7 +15,7 @@
 ##
 
 from twistedcaldav.datafilters.filter import CalendarFilter
-from twistedcaldav.ical import Component, ignoredComponents
+from twistedcaldav.ical import Component
 
 __all__ = [
     "HiddenInstanceFilter",
@@ -38,9 +38,7 @@
         """
 
         master = ical.masterComponent()
-        for component in tuple(ical.subcomponents()):
-            if component.name() in ignoredComponents:
-                continue
+        for component in tuple(ical.subcomponents(ignore=True)):
             rid = component.getRecurrenceIDUTC()
             if rid is None:
                 continue

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -712,6 +712,14 @@
         return self._pycalendar.getType()
 
 
+    def ignored(self):
+        """
+        @return: where this component is one of the L{ignoredComponents}.
+        @rtype: L{bool}
+        """
+        return self.name() in ignoredComponents
+
+
     def mainType(self):
         """
         Determine the primary type of iCal component in this calendar.
@@ -721,10 +729,8 @@
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)
 
         mtype = None
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
-            elif mtype and (mtype != component.name()):
+        for component in self.subcomponents(ignore=True):
+            if mtype and (mtype != component.name()):
                 raise InvalidICalendarDataError("Component contains more than one type of primary type: {0!r}".format(self,))
             else:
                 mtype = component.name()
@@ -742,9 +748,7 @@
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)
 
         result = None
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             if not component.hasProperty("RECURRENCE-ID"):
                 return component
             elif result is None:
@@ -761,9 +765,7 @@
         """
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: {0!r}".format(self,)
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             if not component.hasProperty("RECURRENCE-ID"):
                 return component
 
@@ -785,9 +787,7 @@
         if isinstance(recurrence_id, str):
             recurrence_id = DateTime.parseText(recurrence_id) if recurrence_id else None
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             rid = component.getRecurrenceIDUTC()
             if rid and recurrence_id and rid == recurrence_id:
                 return component
@@ -821,7 +821,7 @@
         return result
 
 
-    def subcomponents(self):
+    def subcomponents(self, ignore=False):
         """
         @return: an iterable of L{Component} objects, one for each subcomponent
             of this component.
@@ -829,6 +829,7 @@
         return (
             Component(None, pycalendar=c, parent=self)
             for c in self._pycalendar.getComponents()
+            if not ignore or (c.getType() not in ignoredComponents)
         )
 
 
@@ -895,13 +896,13 @@
         )
 
 
-    def propertyValue(self, name):
+    def propertyValue(self, name, default=None):
         properties = tuple(self.properties(name))
         if len(properties) == 1:
             return properties[0].value()
         if len(properties) > 1:
             raise InvalidICalendarDataError("More than one {0} property in component {1!r}".format(name, self))
-        return None
+        return default
 
 
     def getStartDateUTC(self):
@@ -1269,9 +1270,7 @@
                     master.removeProperty(property)
 
         # Remove overrides in the future
-        for component in list(self.subcomponents()):
-            if component.name() in ignoredComponents:
-                continue
+        for component in list(self.subcomponents(ignore=True)):
             c_rid = component.getRecurrenceIDUTC()
             if c_rid is not None and c_rid >= rid:
                 self.removeComponent(component)
@@ -1375,9 +1374,7 @@
 
         # Remove overrides in the past - but do not remove any override matching
         # the cut-off as that is still a valid override after "re-basing" the master.
-        for component in list(self.subcomponents()):
-            if component.name() in ignoredComponents:
-                continue
+        for component in list(self.subcomponents(ignore=True)):
             c_rid = component.getRecurrenceIDUTC()
             if c_rid is not None and c_rid < rid:
                 self.removeComponent(component)
@@ -1570,9 +1567,8 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             result = ()
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    result += component.getComponentInstances()
+            for component in self.subcomponents(ignore=True):
+                result += component.getComponentInstances()
             return result
         else:
             rid = self.getRecurrenceIDUTC()
@@ -1586,8 +1582,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents and component.isRecurring():
+            for component in self.subcomponents(ignore=True):
+                if component.isRecurring():
                     return True
         else:
             for propname in ("RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",):
@@ -1829,10 +1825,9 @@
         assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
 
         if not hasattr(self, "_resource_uid"):
-            for subcomponent in self.subcomponents():
-                if subcomponent.name() not in ignoredComponents:
-                    self._resource_uid = subcomponent.propertyValue("UID")
-                    break
+            for subcomponent in self.subcomponents(ignore=True):
+                self._resource_uid = subcomponent.propertyValue("UID")
+                break
             else:
                 self._resource_uid = None
 
@@ -1866,7 +1861,7 @@
                 name = subcomponent.name()
                 if name == "VTIMEZONE":
                     has_timezone = True
-                elif subcomponent.name() in ignoredComponents:
+                elif subcomponent.ignored():
                     continue
                 else:
                     self._resource_type = name
@@ -2044,7 +2039,7 @@
         for subcomponent in self.subcomponents():
             if subcomponent.name() == "VTIMEZONE":
                 timezones.add(subcomponent.propertyValue("TZID"))
-            elif subcomponent.name() in ignoredComponents:
+            elif subcomponent.ignored():
                 continue
             else:
                 if ctype is None:
@@ -2264,9 +2259,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    return component.getOrganizer()
+            for component in self.subcomponents(ignore=True):
+                return component.getOrganizer()
         else:
             try:
                 # Find the primary subcomponent
@@ -2287,9 +2281,8 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             result = ()
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    result += component.getOrganizersByInstance()
+            for component in self.subcomponents(ignore=True):
+                result += component.getOrganizersByInstance()
             return result
         else:
             try:
@@ -2312,9 +2305,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    return component.getOrganizerProperty()
+            for component in self.subcomponents(ignore=True):
+                return component.getOrganizerProperty()
         else:
             try:
                 # Find the primary subcomponent
@@ -2346,10 +2338,7 @@
         """
 
         changed = False
-        for component in tuple(self.subcomponents()):
-            if component.name() in ignoredComponents:
-                continue
-
+        for component in tuple(self.subcomponents(ignore=True)):
             organizerProp = component.getOrganizerProperty()
             if organizerProp is not None:
                 if organizerProp.parameterValue("SCHEDULE-AGENT", "SERVER") != "SERVER":
@@ -2373,9 +2362,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    return component.getAttendees()
+            for component in self.subcomponents(ignore=True):
+                return component.getAttendees()
         else:
             # Find the property values
             return [p.value() for p in self.properties(self.recipientPropertyName())]
@@ -2397,9 +2385,8 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             result = ()
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
+            for component in self.subcomponents(ignore=True):
+                result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
             return result
         else:
             result = ()
@@ -2457,11 +2444,10 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    attendee = component.getAttendeeProperty(match)
-                    if attendee is not None:
-                        return attendee
+            for component in self.subcomponents(ignore=True):
+                attendee = component.getAttendeeProperty(match)
+                if attendee is not None:
+                    return attendee
         else:
             # Find the primary subcomponent
             for attendee in self.properties(self.recipientPropertyName()):
@@ -2483,11 +2469,10 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         results = []
-        for component in self.subcomponents():
-            if component.name() not in ignoredComponents:
-                attendee = component.getAttendeeProperty(match)
-                if attendee:
-                    results.append(attendee)
+        for component in self.subcomponents(ignore=True):
+            attendee = component.getAttendeeProperty(match)
+            if attendee:
+                results.append(attendee)
 
         return results
 
@@ -2501,10 +2486,9 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    for attendee in component.getAllAttendeeProperties():
-                        yield attendee
+            for component in self.subcomponents(ignore=True):
+                for attendee in component.getAllAttendeeProperties():
+                    yield attendee
         else:
             # Find the primary subcomponent
             for attendee in self.properties(self.recipientPropertyName()):
@@ -2528,9 +2512,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    return component.getMaskUID()
+            for component in self.subcomponents(ignore=True):
+                return component.getMaskUID()
         else:
             try:
                 # Find the primary subcomponent
@@ -2550,9 +2533,8 @@
 
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() not in ignoredComponents:
-                    return component.getExtendedFreeBusy()
+            for component in self.subcomponents(ignore=True):
+                return component.getExtendedFreeBusy()
         else:
             try:
                 # Find the primary subcomponent
@@ -2599,9 +2581,7 @@
         @type propvalue: C{str} or C{None}
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             for property in component.properties(propname):
                 if propvalue is None or property.value() == propvalue:
                     for paramname, paramvalue in params.items():
@@ -2695,9 +2675,7 @@
         @type property: L{Property}
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             found = component.getProperty(property.name())
             if not found or found.value() != property.value():
                 return False
@@ -2713,9 +2691,7 @@
         @type property: L{Property}
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             component.addProperty(property)
 
 
@@ -2725,9 +2701,7 @@
         @param property: the L{Property} to replace in this component.
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             component.replaceProperty(property)
 
 
@@ -2747,9 +2721,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 if component.hasPropertyWithParameterMatch(propname, param_name, param_value, param_value_is_default):
                     return True
         else:
@@ -2773,9 +2745,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.replaceAllPropertiesWithParameterMatch(property, param_name, param_value, param_value_is_default)
         else:
             for oldprop in tuple(self.properties(property.name())):
@@ -2791,9 +2761,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.removeAllPropertiesWithParameterMatch(propname, param_name, param_value, param_value_is_default)
         else:
             for oldprop in tuple(self.properties(propname)):
@@ -2816,9 +2784,7 @@
         assert from_calendar.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.transferProperties(from_calendar, properties)
         else:
             # Is there a matching component
@@ -2847,9 +2813,7 @@
         remove_components = []
         master_component = None
         removed_master = False
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             found_all_attendees = True
             for attendee in attendees:
                 foundAttendee = component.getAttendeeProperty((attendee,))
@@ -2890,12 +2854,9 @@
         assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
 
         # Remove components not in the list
-        components = tuple(self.subcomponents())
+        components = tuple(self.subcomponents(ignore=True))
         remaining = len(components)
         for component in components:
-            if component.name() in ignoredComponents:
-                remaining -= 1
-                continue
             rid = component.getRecurrenceIDUTC()
             if rid not in rids:
                 self.removeComponent(component)
@@ -2911,9 +2872,7 @@
 
         assert self.name() == "VCALENDAR", "Not a calendar: {0!r}".format(self,)
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             [component.removeProperty(p) for p in tuple(component.properties(component.recipientPropertyName())) if p.value().lower() != attendee.lower()]
 
 
@@ -2926,9 +2885,7 @@
 
         attendees = set([attendee.lower() for attendee in attendees])
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             [component.removeProperty(p) for p in tuple(component.properties(component.recipientPropertyName())) if p.value().lower() not in attendees]
 
 
@@ -3008,9 +2965,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.removeAlarms()
         else:
             for component in tuple(self.subcomponents()):
@@ -3051,7 +3006,7 @@
             for component in self.subcomponents():
                 component.filterProperties(remove, keep, do_subcomponents=False)
         else:
-            if self.name() in ignoredComponents:
+            if self.ignored():
                 return
 
             for p in tuple(self.properties()):
@@ -3069,24 +3024,25 @@
                 self.removeComponent(component)
 
 
-    def removeXProperties(self, keep_properties=(), remove_x_parameters=True, do_subcomponents=True):
+    def removeXProperties(self, keep_properties=(), keep_parameters={}, remove_x_parameters=True, do_subcomponents=True):
         """
         Remove all X- properties except the specified ones
         """
 
         if do_subcomponents and self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                component.removeXProperties(keep_properties, remove_x_parameters, do_subcomponents=False)
+                component.removeXProperties(keep_properties, keep_parameters, remove_x_parameters, do_subcomponents=False)
         else:
-            if self.name() in ignoredComponents:
+            if self.ignored():
                 return
             for p in tuple(self.properties()):
                 xpname = p.name().startswith("X-")
                 if xpname and p.name() not in keep_properties:
                     self.removeProperty(p)
                 elif not xpname and remove_x_parameters:
+                    preserve = keep_parameters.get(p.name(), set())
                     for paramname in p.parameterNames():
-                        if paramname.startswith("X-"):
+                        if paramname.startswith("X-") and paramname not in preserve:
                             p.removeParameter(paramname)
 
 
@@ -3096,9 +3052,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.removePropertyParameters(property, params)
         else:
             props = self.properties(property)
@@ -3113,9 +3067,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.removePropertyParametersByValue(property, paramvalues)
         else:
             props = self.properties(property)
@@ -3133,9 +3085,7 @@
         try:
             # Extract items from component
             uid = self.propertyValue("UID")
-            seq = self.propertyValue("SEQUENCE")
-            if seq:
-                seq = int(seq)
+            seq = self.propertyValue("SEQUENCE", 0)
             dtstamp = self.propertyValue("DTSTAMP")
             rid = self.propertyValue("RECURRENCE-ID")
 
@@ -3181,14 +3131,9 @@
         assert uid1 == uid2
 
         # Look for sequence
-        if (seq1 is not None) and (seq2 is not None):
-            if seq1 > seq2:
-                return 1
-            if seq1 < seq2:
-                return -1
-        elif (seq1 is not None) and (seq2 is None):
+        if seq1 > seq2:
             return 1
-        elif (seq1 is None) and (seq2 is not None):
+        if seq1 < seq2:
             return -1
 
         # Look for DTSTAMP
@@ -3212,20 +3157,14 @@
         that is always greater than the old.
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in self.subcomponents(ignore=True):
             oldcomponent = oldcalendar.overriddenComponent(component.getRecurrenceIDUTC())
             if oldcomponent is None:
                 oldcomponent = oldcalendar.masterComponent()
                 if oldcomponent is None:
                     continue
-            newseq = component.propertyValue("SEQUENCE")
-            if newseq is None:
-                newseq = 0
-            oldseq = oldcomponent.propertyValue("SEQUENCE")
-            if oldseq is None:
-                oldseq = 0
+            newseq = component.propertyValue("SEQUENCE", 0)
+            oldseq = oldcomponent.propertyValue("SEQUENCE", 0)
             if newseq <= oldseq:
                 return True
 
@@ -3389,9 +3328,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.normalizePropertyValueLists(propname)
         else:
             for prop in tuple(self.properties(propname)):
@@ -3407,9 +3344,7 @@
         """
 
         if self.name() == "VCALENDAR":
-            for component in self.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in self.subcomponents(ignore=True):
                 component.normalizeAttachments()
         else:
             dropboxPrefix = self.propertyValue("X-APPLE-DROPBOX")
@@ -3441,9 +3376,11 @@
         @type toCanonical: L{bool}
         """
 
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        # Keep a cache of records because events with lots of recurrence overrides can contain
+        # the same attendee cu-address multuiple times
+        cache = {}
+
+        for component in self.subcomponents(ignore=True):
             for prop in itertools.chain(
                 component.properties("ORGANIZER"),
                 component.properties("ATTENDEE"),
@@ -3453,7 +3390,11 @@
                 # Check that we can lookup this calendar user address - if not
                 # we cannot do anything with it
                 cuaddr = normalizeCUAddr(prop.value())
-                name, uid, cutype, cuaddrs = yield lookupFunction(cuaddr, recordFunction, config)
+                if cuaddr not in cache:
+                    result = yield lookupFunction(cuaddr, recordFunction, config)
+                    cache[cuaddr] = result
+
+                name, uid, cutype, cuaddrs = cache[cuaddr]
                 if uid is None:
                     continue
 
@@ -3563,10 +3504,7 @@
     def _reconcileGroupAttendee(self, groupCUA, memberAttendeeProps):
 
         changed = False
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
-
+        for component in self.subcomponents(ignore=True):
             oldAttendeeProps = tuple(component.properties("ATTENDEE"))
             oldAttendeeCUAs = set([attendeeProp.value() for attendeeProp in oldAttendeeProps])
 
@@ -3611,10 +3549,7 @@
                 nonemptyGroupCUAs.add(groupCUA)
 
         # remove orphans
-        for component in self.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
-
+        for component in self.subcomponents(ignore=True):
             for attendeeProp in tuple(component.properties("ATTENDEE")):
                 if attendeeProp.hasParameter("MEMBER"):
                     attendeeCUA = attendeeProp.value()
@@ -3673,7 +3608,7 @@
                             adjusted_start = subcomponent.propertyValue("X-APPLE-TRAVEL-DURATION")
                             adjusted_end = subcomponent.propertyValue("X-APPLE-TRAVEL-RETURN-DURATION")
                             self._perUserData.setdefault(uid, {})[instancerid] = (transp, adjusted_start, adjusted_end,)
-                elif component.name() not in ignoredComponents:
+                elif not component.ignored():
                     instancerid = component.propertyValue("RECURRENCE-ID")
                     transp = component.propertyValue("TRANSP") == "TRANSPARENT"
                     self._perUserData.setdefault("", {})[instancerid] = (transp, None, None,)
@@ -3940,7 +3875,7 @@
 
 
 
-def normalize_iCalStr(icalstr):
+def normalize_iCalStr(icalstr, filter_params=("ATTENDEE;X-CALENDARSERVER-DTSTAMP", "ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT",)):
     """
     Normalize a string representation of ical data for easy test comparison.
     """
@@ -3948,10 +3883,17 @@
     icalstr = str(icalstr).replace("\r\n ", "")
     icalstr = icalstr.replace("\n ", "")
     lines = [line for line in icalstr.splitlines() if not line.startswith("DTSTAMP")]
-    for ctr, line in enumerate(lines[:]):
-        pos = line.find(";X-CALENDARSERVER-DTSTAMP=")
-        if pos != -1:
-            lines[ctr] = line[:pos] + line[pos + len(";X-CALENDARSERVER-DTSTAMP=") + 16:]
+    for param in filter_params:
+        propname, paramname = param.split(";")
+        for ctr, line in enumerate(lines[:]):
+            if line.startswith(propname + ";"):
+                pos = line.find(";{}=".format(paramname))
+                if pos != -1:
+                    next_segment = line[pos + len(paramname) + 2:]
+                    end_pos = next_segment.find(";")
+                    if end_pos == -1:
+                        end_pos = next_segment.find(":")
+                    lines[ctr] = line[:pos] + line[pos + len(paramname) + 2 + end_pos:]
     icalstr = "\r\n".join(lines)
     return icalstr + "\r\n"
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icaldiff.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -772,6 +772,7 @@
             return map
 
         rids = {}
+        needs_action_changes = {}
 
         oldmap = mapComponents(self.oldcalendar)
         oldset = set(oldmap.keys())
@@ -782,7 +783,7 @@
         for key in (oldset & newset):
             component1 = oldmap[key]
             component2 = newmap[key]
-            self._diffComponents(component1, component2, rids, isiTip)
+            self._diffComponents(component1, component2, rids, needs_action_changes, isiTip)
 
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in oldset - newset:
@@ -796,7 +797,7 @@
                 if not isiTip:
                     rids[rid.getText() if rid is not None else ""] = {"DTSTART": set()}
                 continue
-            self._diffComponents(oldcomponent, newcomponent, rids, isiTip)
+            self._diffComponents(oldcomponent, newcomponent, rids, needs_action_changes, isiTip)
 
         # Now verify that each additional component in oldset matches a derived component in newset
         for key in newset - oldset:
@@ -810,9 +811,9 @@
                     rids[rid.getText() if rid is not None else ""] = {"DTSTART": set()}
                 continue
             newcomponent = newmap[key]
-            self._diffComponents(oldcomponent, newcomponent, rids, isiTip)
+            self._diffComponents(oldcomponent, newcomponent, rids, needs_action_changes, isiTip)
 
-        return rids
+        return (rids, needs_action_changes,)
 
 
     TRPROPS = frozenset((
@@ -839,7 +840,8 @@
         @rtype: L{bool}
         """
 
-        for props in self.whatIsDifferent(isiTip=False).values():
+        rids, _ignore_changes = self.whatIsDifferent(isiTip=False)
+        for props in rids.values():
             props = frozenset(props.keys())
             if props & self.TRPROPS:
                 return True
@@ -930,7 +932,7 @@
         return comp
 
 
-    def _diffComponents(self, comp1, comp2, rids, isiTip=True):
+    def _diffComponents(self, comp1, comp2, rids, needs_action_rids, isiTip=True):
 
         assert isinstance(comp1, Component) and isinstance(comp2, Component)
 
@@ -945,6 +947,7 @@
         # Diff all the properties
         propdiff = set(comp1.properties()) ^ set(comp2.properties())
         addedChanges = False
+        attendeeChanges = False
 
         propsChanged = {}
         for prop in propdiff:
@@ -955,6 +958,8 @@
                 "X-CALENDARSERVER-PRIVATE-COMMENT",
             ):
                 continue
+            if prop.name() == "ATTENDEE":
+                attendeeChanges = True
             propsChanged.setdefault(prop.name(), set())
             addedChanges = True
             prop1s = tuple(comp1.properties(prop.name()))
@@ -972,7 +977,30 @@
             rid = comp1.getRecurrenceIDUTC()
             rids[rid] = propsChanged
 
+        if attendeeChanges:
+            self._diffNeedsAction(comp1, comp2, needs_action_rids)
 
+
+    def _diffNeedsAction(self, comp1, comp2, needs_action_rids):
+
+        rid = comp1.getRecurrenceIDUTC()
+
+        # Get all the properties
+        attendees1 = comp1.getAllAttendeeProperties()
+        attendees2 = comp2.getAllAttendeeProperties()
+
+        # Invert
+        attendees1ByValue = dict([(attendee.value(), attendee) for attendee in attendees1])
+        attendees2ByValue = dict([(attendee.value(), attendee) for attendee in attendees2])
+
+        for attendee in attendees2ByValue.keys():
+            if attendee not in attendees1ByValue or (
+                attendees2ByValue[attendee].parameterValue("PARTSTAT", "NEEDS-ACTION") == "NEEDS-ACTION" and
+                attendees1ByValue[attendee].parameterValue("PARTSTAT", "NEEDS-ACTION") != "NEEDS-ACTION"
+            ):
+                needs_action_rids.setdefault(rid, set()).add(attendee)
+
+
     def _logDiffError(self, title):
 
         strcal1 = str(self.oldcalendar)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/implicit.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -36,7 +36,7 @@
 from txdav.caldav.datastore.scheduling.itip import iTipGenerator, iTIPRequestStatus
 from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
 from txdav.caldav.datastore.scheduling.work import ScheduleReplyWork, \
-    ScheduleReplyCancelWork, ScheduleOrganizerWork, ScheduleOrganizerSendWork
+    ScheduleOrganizerWork, ScheduleOrganizerSendWork
 from txdav.caldav.icalendarstore import SetComponentOptions
 
 import collections
@@ -668,10 +668,7 @@
         self.changed_rids = None
         self.cancelledAttendees = ()
         self.reinvites = None
-        self.needs_action_rids = None
 
-        self.needs_sequence_change = False
-
         self.coerceOrganizerScheduleAgent()
 
         # Check for a delete
@@ -683,8 +680,9 @@
             # Cancel all attendees
             self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
 
-            # CANCEL always bumps sequence
-            self.needs_sequence_change = True
+            # CANCEL always bumps sequence (if queued, sequence has already changed)
+            if not queued:
+                self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
 
         # Check for a new resource or an update
         elif self.action in ("modify", "modify-cancelled"):
@@ -701,7 +699,8 @@
                 self.calendar.sequenceInSync(self.oldcalendar)
 
             # Significant change
-            no_change, self.changed_rids, self.needs_action_rids, reinvites, recurrence_reschedule, status_cancelled, only_status = self.isOrganizerChangeInsignificant()
+            no_change, self.changed_rids, needs_action_rids, needs_action_changed_rids, reinvites, \
+                recurrence_reschedule, status_cancelled, only_status = self.isOrganizerChangeInsignificant()
             if no_change:
                 if reinvites:
                     log.debug("Implicit - organizer '{organizer}' is re-inviting UID: '{uid}', attendees: {attendees}", organizer=self.organizer, uid=self.uid, attendees=", ".join(reinvites))
@@ -711,11 +710,21 @@
                     log.debug("Implicit - organizer '{organizer}' is modifying UID: '{uid}' but change is not significant", organizer=self.organizer, uid=self.uid)
                     returnValue(None)
             else:
-                # Do not change PARTSTATs for a split operation
+
+                # For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
+                # the Attendee side. But we check the old and the new and only bump if the client did not already do it.
+                # Also, if queued, the sequence has already been changed
+                if not queued and self.calendar.needsiTIPSequenceChange(self.oldcalendar):
+                    self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
+
+                # Only change PARTSTATs for a non-split operation
                 if self.split_details is None:
+
+                    # Adjust ATTENDEE;PARTSTAT for instances that have changed such that a re-evaluation of partstat is needed
                     log.debug("Implicit - organizer '{organizer}' is modifying UID: '{uid}'", organizer=self.organizer, uid=self.uid)
 
-                    for rid in self.needs_action_rids:
+                    # Look for changes to an instance that require all attendees of that instance to be reset
+                    for rid in needs_action_rids:
                         comp = self.calendar.overriddenComponent(rid)
                         if comp is None:
                             comp = self.calendar.deriveInstance(rid)
@@ -723,16 +732,28 @@
                                 self.calendar.addComponent(comp)
 
                         for attendee in comp.getAllAttendeeProperties():
+                            if attendee.value() in self.organizerAddress.record.calendarUserAddresses:
+                                # If the attendee is the organizer then do not update
+                                # the PARTSTAT to NEEDS-ACTION.
+                                # The organizer is automatically ACCEPTED to the event.
+                                continue
                             if attendee.hasParameter("PARTSTAT"):
-                                cuaddr = attendee.value()
+                                attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
+                            seq = comp.propertyValue("SEQUENCE", 0)
+                            attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
 
-                                if cuaddr in self.organizerAddress.record.calendarUserAddresses:
-                                    # If the attendee is the organizer then do not update
-                                    # the PARTSTAT to NEEDS-ACTION.
-                                    # The organizer is automatically ACCEPTED to the event.
-                                    continue
+                    # Look for changes to a specific attendee within an instance
+                    for rid, attendees in needs_action_changed_rids.items():
+                        comp = self.calendar.overriddenComponent(rid)
+                        if comp is None:
+                            comp = self.calendar.deriveInstance(rid)
+                            if comp is not None:
+                                self.calendar.addComponent(comp)
 
-                                attendee.setParameter("PARTSTAT", "NEEDS-ACTION")
+                        for attendee in comp.getAllAttendeeProperties():
+                            if attendee.value() in attendees:
+                                seq = comp.propertyValue("SEQUENCE", 0)
+                                attendee.setParameter("X-CALENDARSERVER-RESET-PARTSTAT", str(seq))
                 else:
                     log.debug("Implicit - organizer '{organizer}' is splitting UID: '{uid}'", organizer=self.organizer, uid=self.uid)
 
@@ -744,18 +765,23 @@
 
                 self.checkStatusCancelled(status_cancelled, only_status)
 
-                # For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
-                # the Attendee side. But we check the old and the new and only bump if the client did not already do it.
-                self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
-
         elif self.action == "create":
             if self.split_details is None:
                 log.debug("Implicit - organizer '{organizer}' is creating UID: '{uid}'", organizer=self.organizer, uid=self.uid)
                 self.coerceAttendeesPartstatOnCreate()
 
+                # We need to handle the case where an organizer "restores" a previously delete event that has a sequence
+                # lower than the one used in the cancel that attendees may still have. In this case what we need to do
+                # is force the sequence to a new value that is significantly higher than the highest one present.
+                # Also, if queued, the sequence has already been changed
+                if not queued:
+                    seqs = map(lambda x: x.value(), self.calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1))
+                    maxseq = max(seqs) if seqs else 0
+                    if maxseq != 0:
+                        self.calendar.replacePropertyInAllComponents(Property("SEQUENCE", maxseq + 1000))
+
             else:
                 log.debug("Implicit - organizer '{organizer}' is creating a split UID: '{uid}'", organizer=self.organizer, uid=self.uid)
-                self.needs_sequence_change = False
 
         # Always set RSVP=TRUE for any NEEDS-ACTION
         for attendee in self.calendar.getAllAttendeeProperties():
@@ -768,20 +794,8 @@
                 attendee.removeParameter("PARTSTAT")
 
         # If processing a queue item, actually execute the scheduling operations, else queue it.
-        # Note a split is always queued, so we do not need to re-queue
+        # Note a split is always a queued execution, so we do not need to re-queue
         if queued or not config.Scheduling.Options.WorkQueues.Enabled or self.split_details is not None:
-            if self.action == "create":
-                if self.split_details is None:
-                    # We need to handle the case where an organizer "restores" a previously delete event that has a sequence
-                    # lower than the one used in the cancel that attendees may still have. In this case what we need to do
-                    # is force the sequence to a new value that is significantly higher than the highest one present.
-                    seqs = map(lambda x: x.value(), self.calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1))
-                    maxseq = max(seqs) if seqs else 0
-                    if maxseq != 0:
-                        self.calendar.replacePropertyInAllComponents(Property("SEQUENCE", maxseq + 1000))
-            elif self.needs_sequence_change:
-                self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
-
             yield self.scheduleWithAttendees()
         else:
             yield self.queuedScheduleWithAttendees()
@@ -795,9 +809,25 @@
 
 
     def isOrganizerChangeInsignificant(self):
+        """
+        Detect exactly how an organizer update has changed an event and report back the key items
+        needed for iTIP scheduling for those changes. These items are returned:
 
+        L{rids} - the L{DateTime} for each instance that had any kind of change
+        L{date_changed_rids} - the L{DateTime} for each instance that had a scheduling
+            change that needs to reset every ATTENDEE;PARTSTAT to NEEDS-ACTION
+        L{needs_action_changed_rids} - the L{DateTime}:L{set} map for each instance
+            where an ATTENDEE property was reset to NEEDS-ACTION by the organizer
+        L{reinvites} - the L{set} of each attendee that has a SCHEDULE-FORCE-SEND
+        L{recurrence_reschedule} - whether or not an entire reschedule of an RRULE
+            has occurred
+        L{status_cancelled} - the L{DateTime} for each instance that now has STATUS:CANCELLED
+        L{only_status} - L{True} if the only change was a STATUS change
+        """
+
         rids = None
         date_changed_rids = None
+        needs_action_changed_rids = None
         reinvites = None
         recurrence_reschedule = False
         status_cancelled = set()
@@ -806,11 +836,16 @@
         no_change = differ.organizerDiff()
         if not no_change:
             # ORGANIZER change is absolutely not allowed!
-            diffs = differ.whatIsDifferent()
+            diffs, needs_action_changed_rids = differ.whatIsDifferent()
             rids = set()
             date_changed_rids = set()
             checkOrganizerValue = False
             for rid, props in diffs.iteritems():
+
+                # Ignore sequence only changes
+                if "SEQUENCE" in props and len(props) == 1:
+                    continue
+
                 if "ORGANIZER" in props:
                     checkOrganizerValue = True
                 rids.add(rid)
@@ -904,7 +939,7 @@
                     pass
 
         return (
-            no_change, rids, date_changed_rids, reinvites,
+            no_change, rids, date_changed_rids, needs_action_changed_rids, reinvites,
             recurrence_reschedule, status_cancelled, only_status
         )
 
@@ -1147,22 +1182,6 @@
             self.do_smart_merge,
         )
 
-        # We bump the sequence AFTER storing the work item data to make sure that the sequence
-        # change does not cause unchanged components to be treated as changed when the work
-        # item executes.
-
-        if self.action == "create":
-            # We need to handle the case where an organizer "restores" a previously delete event that has a sequence
-            # lower than the one used in the cancel that attendees may still have. In this case what we need to do
-            # is force the sequence to a new value that is significantly higher than the highest one present.
-            seqs = map(lambda x: x.value(), self.calendar.getAllPropertiesInAnyComponent("SEQUENCE", depth=1))
-            maxseq = max(seqs) if seqs else 0
-            if maxseq != 0:
-                self.calendar.replacePropertyInAllComponents(Property("SEQUENCE", maxseq + 1000))
-
-        elif self.needs_sequence_change:
-            self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
-
         # First process cancelled attendees
         total = (yield self.processCancels(queued=True))
 
@@ -1701,6 +1720,7 @@
         if self.logItems is not None:
             self.logItems["itip.reply"] = "reply"
 
+        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
         if config.Scheduling.Options.WorkQueues.Enabled:
             # Always make it look like scheduling succeeded when queuing
             self.calendar.setParameterToValueForPropertyWithValue(
@@ -1710,11 +1730,9 @@
                 self.organizer,
             )
 
-            return ScheduleReplyWork.reply(self.txn, self.calendar_home, self.resource, changedRids, self.attendee)
+            return ScheduleReplyWork.reply(self.txn, self.calendar_home, self.resource, itipmsg, self.attendee)
 
         else:
-            itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, changedRids=changedRids)
-
             # Send scheduling message
             return self.sendToOrganizer("REPLY", itipmsg)
 
@@ -1727,12 +1745,11 @@
         if self.logItems is not None:
             self.logItems["itip.reply"] = "cancel"
 
+        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)
+
         if config.Scheduling.Options.WorkQueues.Enabled:
-            return ScheduleReplyCancelWork.replyCancel(self.txn, self.calendar_home, self.calendar, self.attendee)
-
+            return ScheduleReplyWork.reply(self.txn, self.calendar_home, None, itipmsg, self.attendee)
         else:
-            itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, force_decline=True)
-
             # Send scheduling message
             return self.sendToOrganizer("CANCEL", itipmsg)
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/itip.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -17,21 +17,21 @@
 """
 iTIP (RFC5546) scheduling message processing and generation.
 
+This modules takes an iTIP message, and a matching existing calendar object, and it
+applies the changes implied by the iTIP message to the existing data (or creates new
+data if needed).
 
-This is currently used for handling auto-replies to schedule requests arriving
-in an inbox. It is called in a delayed fashion via reactor.callLater.
-
 We assume that all the components/calendars we deal with have been determined
 as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
 types, etc.
 """
 
+from collections import namedtuple
 
 from twext.python.log import Logger
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import Property, iCalendarProductID, Component, \
-    ignoredComponents
+from twistedcaldav.ical import Property, iCalendarProductID, Component
 
 from pycalendar.datetime import DateTime
 
@@ -45,7 +45,60 @@
 
 class iTipProcessing(object):
 
+    # Information to be transferred from one component to another
+    TransferDetails = namedtuple(
+        "TransferDetails",
+        [
+            "valarms", "private_comments", "transps", "completeds", "organizer",
+            "organizer_schedule_status", "attendee", "attendee_dtstamp", "sequence",
+            "other_props", "recipient",
+        ]
+    )
+
     @staticmethod
+    def _transferDetails(component, recipient):
+        """
+        Get the L{TransferDetails} from the specified component, which may be L{None}.
+
+        @param component: the component to extract details from
+        @type component: L{Component} or L{None}
+        @param recipient: the attendee whose iTIP message is being processed
+        @type recipient: L{str}
+        """
+        if component:
+            valarms = [comp for comp in component.subcomponents() if comp.name() == "VALARM"]
+            private_comments = tuple(component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
+            transps = tuple(component.properties("TRANSP"))
+            completeds = tuple(component.properties("COMPLETED"))
+            organizer = component.getProperty("ORGANIZER")
+            organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
+            attendee = component.getAttendeeProperty((recipient,))
+            attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
+            sequence = component.propertyValue("SEQUENCE", 0)
+            other_props = {}
+            for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
+                props = tuple(component.properties(pname))
+                other_props[pname] = props
+        else:
+            valarms = ()
+            private_comments = ()
+            transps = ()
+            completeds = ()
+            organizer = None
+            organizer_schedule_status = None
+            attendee = None
+            attendee_dtstamp = None
+            sequence = 0,
+            other_props = {}
+
+        return iTipProcessing.TransferDetails(
+            valarms, private_comments, transps, completeds, organizer,
+            organizer_schedule_status, attendee, attendee_dtstamp, sequence,
+            other_props, recipient,
+        )
+
+
+    @staticmethod
     def processNewRequest(itip_message, recipient=None, creating=False):
         """
         Process a METHOD=REQUEST for a brand new calendar object (for creating set to C{True}. This is also
@@ -72,10 +125,11 @@
             # Check for incoming DECLINED
             if creating:
                 iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient)
+                calendar.removePropertyParameters("ATTENDEE", ("X-CALENDARSERVER-RESET-PARTSTAT",))
 
                 master = calendar.masterComponent()
-                for component in tuple(calendar.subcomponents()):
-                    if component in ignoredComponents or component is master:
+                for component in tuple(calendar.subcomponents(ignore=True)):
+                    if component is master:
                         continue
                     attendee = component.getAttendeeProperty((recipient,))
                     if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
@@ -118,35 +172,14 @@
         # Merge Organizer data with Attendee's own changes (VALARMs, Comment only for now).
         from txdav.caldav.datastore.scheduling.icaldiff import iCalDiff
         differ = iCalDiff(calendar, itip_message, False)
-        rids = differ.whatIsDifferent()
+        rids, _ignore_changes = differ.whatIsDifferent()
         needs_action_rids, reschedule = differ.attendeeNeedsAction(rids)
 
         # Different behavior depending on whether a master component is present or not
         # Here we cache per-attendee data from the existing master that we need to use in any new
         # overridden components that the organizer added
         current_master = calendar.masterComponent()
-        if current_master:
-            valarms = [comp for comp in current_master.subcomponents() if comp.name() == "VALARM"]
-            private_comments = tuple(current_master.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
-            transps = tuple(current_master.properties("TRANSP"))
-            completeds = tuple(current_master.properties("COMPLETED"))
-            organizer = current_master.getProperty("ORGANIZER")
-            organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
-            attendee = current_master.getAttendeeProperty((recipient,))
-            attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
-            other_props = {}
-            for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
-                props = tuple(current_master.properties(pname))
-                other_props[pname] = props
-        else:
-            valarms = ()
-            private_comments = ()
-            transps = ()
-            completeds = ()
-            organizer_schedule_status = None
-            attendee = None
-            attendee_dtstamp = None
-            other_props = {}
+        details = iTipProcessing._transferDetails(current_master, recipient)
 
         if itip_message.masterComponent() is not None:
 
@@ -156,13 +189,12 @@
             # Copy over master alarms, comments etc
             master_component = new_calendar.masterComponent()
             transfer_partstat = None not in needs_action_rids and not reschedule
-            seq_change = Component.compareComponentsForITIP(master_component, current_master, use_dtstamp=False) <= 0 if current_master is not None else False
-            iTipProcessing._transferItems(master_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient)
+            iTipProcessing._transferItems(master_component, transfer_partstat, details)
 
             # Now try to match recurrences in the new calendar
             for component in tuple(new_calendar.subcomponents()):
                 if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
-                    iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient)
+                    iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, details)
 
             # Now try to match recurrences from the old calendar
             for component in calendar.subcomponents():
@@ -181,11 +213,12 @@
                                 new_calendar.addComponent(component.duplicate())
                             else:
                                 new_calendar.addComponent(new_component)
-                                iTipProcessing.transferItems(calendar, new_component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient)
+                                iTipProcessing.transferItems(calendar, new_component, needs_action_rids, reschedule, details)
                                 if hidden:
                                     new_component.addProperty(Property(Component.HIDDEN_INSTANCE_PROPERTY, "T"))
 
             iTipProcessing.addTranspForNeedsAction(new_calendar.subcomponents(), recipient)
+            new_calendar.removePropertyParameters("ATTENDEE", ("X-CALENDARSERVER-RESET-PARTSTAT",))
 
             # Replace the entire object
             return new_calendar, rids
@@ -202,7 +235,7 @@
                         calendar.addComponent(component)
                 else:
                     component = component.duplicate()
-                    missingDeclined = iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient, remove_matched=True)
+                    missingDeclined = iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, details, remove_matched=True)
                     if not missingDeclined:
                         # Add the component and make sure to remove any matching EXDATE
                         calendar.addComponent(component)
@@ -210,6 +243,7 @@
                             current_master.removeExdate(component.getRecurrenceIDUTC())
 
             iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient)
+            calendar.removePropertyParameters("ATTENDEE", ("X-CALENDARSERVER-RESET-PARTSTAT",))
 
             # Write back the modified object
             return calendar, rids
@@ -251,7 +285,7 @@
             else:
                 # Cancel every instance in the existing event and sync over SEQUENCE
                 calendar.replacePropertyInAllComponents(Property("STATUS", "CANCELLED"))
-                newseq = itip_message.masterComponent().propertyValue("SEQUENCE")
+                newseq = itip_message.masterComponent().propertyValue("SEQUENCE", 0)
                 calendar.replacePropertyInAllComponents(Property("SEQUENCE", newseq))
                 return True, False, None
 
@@ -291,7 +325,7 @@
                 else:
                     # Existing component is cancelled.
                     overridden.replaceProperty(Property("STATUS", "CANCELLED"))
-                    newseq = component.propertyValue("SEQUENCE")
+                    newseq = component.propertyValue("SEQUENCE", 0)
                     overridden.replaceProperty(Property("SEQUENCE", newseq))
 
             elif calendar_master:
@@ -306,7 +340,7 @@
                     if overridden is not None:
                         overridden.replaceProperty(Property("STATUS", "CANCELLED"))
                         calendar.addComponent(overridden)
-                        newseq = component.propertyValue("SEQUENCE")
+                        newseq = component.propertyValue("SEQUENCE", 0)
                         overridden.replaceProperty(Property("SEQUENCE", newseq))
 
         # If we have any EXDATEs lets add them to the existing calendar object.
@@ -355,7 +389,7 @@
         attendees = set()
         rids = set()
         if new_master is not None and old_master is not None:
-            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(new_master, old_master)
+            attendee, partstat, private_comment = iTipProcessing.updateAttendeeDataFromReply(new_master, old_master)
             if attendee:
                 attendees.add(attendee)
                 if partstat or private_comment:
@@ -405,7 +439,7 @@
                     log.error("Ignoring instance: %s in iTIP REPLY for: %s" % (rid, itip_message.resourceUID()))
                     continue
 
-            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(itip_component, match_component)
+            attendee, partstat, private_comment = iTipProcessing.updateAttendeeDataFromReply(itip_component, match_component)
             if attendee:
                 attendees.add(attendee)
                 if rids is not None and (partstat or private_comment):
@@ -423,20 +457,20 @@
 
 
     @staticmethod
-    def updateAttendeeData(from_component, to_component):
+    def updateAttendeeDataFromReply(reply_component, organizer_component):
         """
         Called when processing a REPLY only.
 
-        Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE
-        in the to_component. Ignore if no match found. Also update the private comments.
+        Copy the PARTSTAT of the Attendee in the reply_component to the matching ATTENDEE
+        in the organizer_component. Ignore if no match found. Also update the private comments.
 
         For VPOLL we need to copy POLL-ITEM-ID response values into the actual matching
         polled sub-components as VOTER properties.
 
-        @param from_component: component to copy from
-        @type from_component: L{Component}
-        @param to_component: component to copy to
-        @type to_component: L{Component}
+        @param reply_component: component to copy from
+        @type reply_component: L{Component}
+        @param organizer_component: component to copy to
+        @type organizer_component: L{Component}
         """
 
         # Track what changed
@@ -444,24 +478,28 @@
         private_comment_changed = False
 
         # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property
-        reqstatus = tuple(from_component.properties("REQUEST-STATUS"))
+        reqstatus = tuple(reply_component.properties("REQUEST-STATUS"))
         if reqstatus:
             reqstatus = ",".join(status.value()[0] for status in reqstatus)
         else:
             reqstatus = "2.0"
 
-        # Get attendee in from_component - there MUST be only one
-        attendees = tuple(from_component.properties(from_component.recipientPropertyName()))
+        # Get attendee in reply_component - there MUST be only one
+        attendees = tuple(reply_component.properties(reply_component.recipientPropertyName()))
         if len(attendees) != 1:
-            log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(from_component),))
+            log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(reply_component),))
             return None, False, False
 
         attendee = attendees[0]
         partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
+        reply_sequence = reply_component.propertyValue("SEQUENCE", 0)
 
-        # Now find matching ATTENDEE in to_component
-        existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
-        if existing_attendee:
+        # Now find matching ATTENDEE in organizer_component
+        existing_attendee = organizer_component.getAttendeeProperty((attendee.value(),))
+        existing_reset_sequence = int(existing_attendee.parameterValue("X-CALENDARSERVER-RESET-PARTSTAT", "0")) if existing_attendee else 0
+
+        # Only process the change for this component if it was made after the last partstat reset
+        if existing_attendee and reply_sequence >= existing_reset_sequence:
             oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
             existing_attendee.setParameter("PARTSTAT", partstat)
             existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
@@ -477,11 +515,11 @@
             # Handle attendee comments
             if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
                 # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec)
-                attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
+                attendee_comment = tuple(reply_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
                 attendee_comment = attendee_comment[0] if len(attendee_comment) else None
 
                 # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec)
-                private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT"))
+                private_comments = tuple(organizer_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT"))
                 for comment in private_comments:
                     attendeeref = comment.parameterValue("X-CALENDARSERVER-ATTENDEE-REF")
                     if attendeeref == attendee.value():
@@ -500,7 +538,7 @@
 
             elif attendee_comment is None and private_comment is not None:
                 # We now remove the private comment on the organizer's side if the attendee removed it
-                to_component.removeProperty(private_comment)
+                organizer_component.removeProperty(private_comment)
 
                 private_comment_changed = True
 
@@ -515,7 +553,7 @@
                         "X-CALENDARSERVER-DTSTAMP": DateTime.getNowUTC().getText(),
                     }
                 )
-                to_component.addProperty(private_comment)
+                organizer_component.addProperty(private_comment)
 
                 private_comment_changed = True
 
@@ -535,33 +573,31 @@
                     private_comment_changed = True
 
             # Do VPOLL transfer
-            if from_component.name() == "VPOLL":
+            if reply_component.name() == "VPOLL":
                 # TODO: figure out how to report changes back
-                iTipProcessing.updateVPOLLData(from_component, to_component, attendee)
+                iTipProcessing.updateVPOLLDataFromReply(reply_component, organizer_component, attendee)
 
         return attendee.value(), partstat_changed, private_comment_changed
 
 
     @staticmethod
-    def updateVPOLLData(from_component, to_component, attendee):
+    def updateVPOLLDataFromReply(reply_component, organizer_component, attendee):
         """
         Update VPOLL sub-components with voter's response.
 
-        @param from_component: component to copy from
-        @type from_component: L{Component}
-        @param to_component: component to copy to
-        @type to_component: L{Component}
+        @param reply_component: component to copy from
+        @type reply_component: L{Component}
+        @param organizer_component: component to copy to
+        @type organizer_component: L{Component}
         @param attendee: attendee being processed
         @type attendee: L{Property}
         """
 
         responses = {}
-        for prop in from_component.properties("POLL-ITEM-ID"):
+        for prop in reply_component.properties("POLL-ITEM-ID"):
             responses[prop.value()] = prop
 
-        for component in to_component.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in organizer_component.subcomponents(ignore=True):
             poll_item_id = component.propertyValue("POLL-ITEM-ID")
             if poll_item_id is None:
                 continue
@@ -581,7 +617,7 @@
 
 
     @staticmethod
-    def transferItems(from_calendar, to_component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient, remove_matched=False):
+    def transferItems(from_calendar, to_component, needs_action_rids, reschedule, master_details, remove_matched=False):
         """
         Transfer properties from a calendar to a component by first trying to match the component in the original calendar and
         use the properties from that, or use the values provided as arguments (which have been derived from the original calendar's
@@ -591,22 +627,8 @@
         @type from_calendar: L{Component}
         @param to_component: the new component to transfer items to
         @type to_component: L{Component}
-        @param valarms: a C{list} of VALARM components from the old master to use
-        @type valarms: C{list}
-        @param private_comments: a C{list} of private comment properties from the old master to use
-        @type private_comments: C{list}
-        @param transps: a C{list} of TRANSP properties from the old master to use
-        @type transps: C{list}
-        @param completeds: a C{list} of COMPLETED properties from the old master to use
-        @type completeds: C{list}
-        @param organizer_schedule_status: a the SCHEDULE-STATUS value for the organizer from the old master to use
-        @type organizer_schedule_status: C{str}
-        @param attendee_dtstamp: an the ATTENDEE DTSTAMP parameter value from the old master to use
-        @type attendee_dtstamp: C{str}
-        @param other_props: other properties from the old master to use
-        @type other_props: C{list}
-        @param recipient: the calendar user address of the attendee whose data is being processed
-        @type recipient: C{str}
+        @param master_details: the L{TransferDetails} for the master component
+        @type master_details: L{TransferDetails}
         @param remove_matched: whether or not to remove the matching component rather than transfer items
         @type remove_matched: C{bool}
 
@@ -620,24 +642,13 @@
         # Is there a matching component
         matched = from_calendar.overriddenComponent(rid)
         if matched:
-            valarms = [comp for comp in matched.subcomponents() if comp.name() == "VALARM"]
-            private_comments = tuple(matched.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
-            transps = tuple(matched.properties("TRANSP"))
-            completeds = tuple(matched.properties("COMPLETED"))
-            organizer = matched.getProperty("ORGANIZER")
-            organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
-            attendee = matched.getAttendeeProperty((recipient,))
-            attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
-            other_props = {}
-            for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
-                props = tuple(matched.properties(pname))
-                other_props[pname] = props
 
-            seq_change = Component.compareComponentsForITIP(to_component, matched, use_dtstamp=False) <= 0
-            iTipProcessing._transferItems(to_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient)
+            details = iTipProcessing._transferDetails(matched, master_details.recipient)
 
+            iTipProcessing._transferItems(to_component, transfer_partstat, details)
+
             # Check for incoming DECLINED
-            to_attendee = to_component.getAttendeeProperty((recipient,))
+            to_attendee = to_component.getAttendeeProperty((details.recipient,))
             if to_attendee and to_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
                 # If existing item has HIDDEN property copy that over
                 if matched.hasProperty(Component.HIDDEN_INSTANCE_PROPERTY):
@@ -649,16 +660,14 @@
 
             # Check to see if the new component is cancelled as that could mean we are copying in the wrong attendee state
             if to_component.propertyValue("STATUS") == "CANCELLED":
-                if attendee and to_attendee:
-                    to_attendee.setParameter("PARTSTAT", attendee.parameterValue("PARTSTAT", "NEEDS-ACTION"))
+                if details.attendee and to_attendee:
+                    to_attendee.setParameter("PARTSTAT", details.attendee.parameterValue("PARTSTAT", "NEEDS-ACTION"))
 
         else:
-            master_component = from_calendar.masterComponent()
-            seq_change = (Component.compareComponentsForITIP(to_component, master_component, use_dtstamp=False) <= 0) if master_component is not None else True
-            iTipProcessing._transferItems(to_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient)
+            iTipProcessing._transferItems(to_component, transfer_partstat, master_details)
 
             # Check for incoming DECLINED
-            attendee = to_component.getAttendeeProperty((recipient,))
+            attendee = to_component.getAttendeeProperty((master_details.recipient,))
             if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED":
                 return True
 
@@ -666,7 +675,7 @@
 
 
     @staticmethod
-    def _transferItems(to_component, transfer_partstat, valarms, private_comments, transps, completeds, organizer_schedule_status, old_attendee, attendee_dtstamp, other_props, recipient):
+    def _transferItems(to_component, transfer_partstat, details):
         """
         Transfer properties the key per-attendee properties from one component to another. Note that the key properties are pulled out into separate items, because they
         may have been derived from the master.
@@ -675,22 +684,8 @@
         @type to_component: L{Component}
         @param partstat_change: whether not to transfer the old PARTSTAT over
         @type partstat_change: C{bool}
-        @param valarms: a C{list} of VALARM components from the old master to use
-        @type valarms: C{list}
-        @param private_comments: a C{list} of private comment properties from the old master to use
-        @type private_comments: C{list}
-        @param transps: a C{list} of TRANSP properties from the old master to use
-        @type transps: C{list}
-        @param completeds: a C{list} of COMPLETED properties from the old master to use
-        @type completeds: C{list}
-        @param organizer_schedule_status: a the SCHEDULE-STATUS value for the organizer from the old master to use
-        @type organizer_schedule_status: C{str}
-        @param attendee_dtstamp: an the ATTENDEE DTSTAMP parameter value from the old master to use
-        @type attendee_dtstamp: C{str}
-        @param other_props: other properties from the old master to use
-        @type other_props: C{list}
-        @param recipient: the calendar user address of the attendee whose data is being processed
-        @type recipient: C{str}
+        @param details: the L{TransferDetails} for the master component
+        @type details: L{TransferDetails}
 
         @return: C{True} if an EXDATE match occurred requiring the incoming component to be removed.
         """
@@ -702,29 +697,37 @@
         to_component.removeProperty("X-CALENDARSERVER-PRIVATE-COMMENT")
         to_component.removeProperty("TRANSP")
         to_component.removeProperty("COMPLETED")
-        for propname in other_props.keys():
+        for propname in details.other_props.keys():
             to_component.removeProperty(propname)
 
-        [to_component.addComponent(alarm) for alarm in valarms]
-        [to_component.addProperty(comment) for comment in private_comments]
-        [to_component.replaceProperty(transp) for transp in transps]
-        [to_component.replaceProperty(completed) for completed in completeds]
-        for props in other_props.values():
+        [to_component.addComponent(alarm) for alarm in details.valarms]
+        [to_component.addProperty(comment) for comment in details.private_comments]
+        [to_component.replaceProperty(transp) for transp in details.transps]
+        [to_component.replaceProperty(completed) for completed in details.completeds]
+        for props in details.other_props.values():
             [to_component.replaceProperty(prop) for prop in props]
 
-        if organizer_schedule_status:
+        if details.organizer_schedule_status:
             organizer = to_component.getProperty("ORGANIZER")
             if organizer:
-                organizer.setParameter("SCHEDULE-STATUS", organizer_schedule_status)
+                organizer.setParameter("SCHEDULE-STATUS", details.organizer_schedule_status)
 
         # ATTENDEE property merge
-        attendee = to_component.getAttendeeProperty((recipient,))
-        if old_attendee and attendee and transfer_partstat:
-            iTipProcessing.mergePartStat(old_attendee, attendee)
+        attendee = to_component.getAttendeeProperty((details.recipient,))
 
-        if attendee_dtstamp and attendee:
-            attendee.setParameter("X-CALENDARSERVER-DTSTAMP", attendee_dtstamp)
+        # Handle explicit PARTSTAT reset by checking for the X-CALENDARSERVER-RESET-PARTSTAT parameter and
+        # comparing it to the sequence of the original component
+        if attendee:
+            reset_sequence = int(attendee.parameterValue("X-CALENDARSERVER-RESET-PARTSTAT", "0"))
+            if details.sequence < reset_sequence:
+                transfer_partstat = False
 
+        if details.attendee and attendee and transfer_partstat:
+            iTipProcessing.mergePartStat(details.attendee, attendee)
+
+        if details.attendee_dtstamp and attendee:
+            attendee.setParameter("X-CALENDARSERVER-DTSTAMP", details.attendee_dtstamp)
+
         return False
 
 
@@ -789,9 +792,7 @@
 
         # If master component exists, compare all in iTIP and update if any are new
         if cal_master:
-            for itip_component in itip.subcomponents():
-                if itip_component.name() in ignoredComponents:
-                    continue
+            for itip_component in itip.subcomponents(ignore=True):
                 cal_component = calendar.overriddenComponent(itip_component.getRecurrenceIDUTC())
                 if cal_component is None:
                     cal_component = cal_master
@@ -806,9 +807,7 @@
         elif itip_master:
 
             # Do comparison of each appropriate component if any one is new, process the itip
-            for cal_component in calendar.subcomponents():
-                if cal_component.name() in ignoredComponents:
-                    continue
+            for cal_component in calendar.subcomponents(ignore=True):
                 itip_component = itip.overriddenComponent(cal_component.getRecurrenceIDUTC())
                 if itip_component is None:
                     itip_component = itip_master
@@ -826,14 +825,10 @@
             # that has been removed. Not much we can do about it without additional tracking.
 
             cal_rids = set()
-            for cal_component in calendar.subcomponents():
-                if cal_component.name() in ignoredComponents:
-                    continue
+            for cal_component in calendar.subcomponents(ignore=True):
                 cal_rids.add(cal_component.getRecurrenceIDUTC())
             itip_rids = set()
-            for itip_component in itip.subcomponents():
-                if itip_component.name() in ignoredComponents:
-                    continue
+            for itip_component in itip.subcomponents(ignore=True):
                 itip_rids.add(itip_component.getRecurrenceIDUTC())
 
             # Compare ones that match
@@ -902,8 +897,8 @@
             # Add some required properties extracted from the original
             comp.addProperty(Property("DTSTAMP", instance.propertyValue("DTSTAMP")))
             comp.addProperty(Property("UID", instance.propertyValue("UID")))
-            seq = instance.propertyValue("SEQUENCE")
-            seq = int(seq) + 1 if seq else 1
+            seq = instance.propertyValue("SEQUENCE", 0)
+            seq = seq + 1
             comp.addProperty(Property("SEQUENCE", seq))
             comp.addProperty(instance.getOrganizerProperty())
             if instance_rid:
@@ -1020,9 +1015,7 @@
         itip.removeAllButOneAttendee(attendee)
 
         # Remove all components which are missing the attendee
-        for component in itip.subcomponents():
-            if component.name() in ignoredComponents:
-                continue
+        for component in itip.subcomponents(ignore=True):
             if not component.getAttendeeProperty((attendee,)):
                 itip.removeComponent(component)
 
@@ -1083,9 +1076,7 @@
         @type attendee: C{str}
         """
 
-        for component in tuple(vpoll.subcomponents()):
-            if component.name() in ignoredComponents:
-                continue
+        for component in tuple(vpoll.subcomponents(ignore=True)):
             poll_item_id = component.propertyValue("POLL-ITEM-ID")
             if poll_item_id is None:
                 continue
@@ -1117,7 +1108,10 @@
         else:
             # Attendee properties that need to go to the Organizer
             keep_properties = ("X-CALENDARSERVER-PRIVATE-COMMENT",)
-        itip.removeXProperties(keep_properties=keep_properties)
+        keep_parameters = {
+            "ATTENDEE": set(("X-CALENDARSERVER-RESET-PARTSTAT",)),
+        }
+        itip.removeXProperties(keep_properties=keep_properties, keep_parameters=keep_parameters)
 
         # Property Parameters
         itip.removePropertyParameters("ATTENDEE", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND", "X-CALENDARSERVER-DTSTAMP",))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -348,7 +348,7 @@
     @inlineCallbacks
     def doImplicitAttendeeUpdate(self):
         """
-        An iTIP message has been sent by to an attendee by the organizer. We need to update the attendee state
+        An iTIP message has been sent to an attendee by the organizer. We need to update the attendee state
         based on the nature of the iTIP message.
         """
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icaldiff.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -3542,6 +3542,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#1.2 Simple component, one property change",
@@ -3568,6 +3569,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), }},
+                {},
             ),
             (
                 "#1.3 Simple component, one property change, one addition, one removal",
@@ -3596,6 +3598,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
+                {},
             ),
             (
                 "#1.4 Simple component, add attendee",
@@ -3629,6 +3632,7 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user3 at example.com",))},
             ),
             (
                 "#1.5 Simple component, remove attendee",
@@ -3660,6 +3664,7 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {},
             ),
             (
                 "#1.6 Simple component, attendee PARTSTAT only",
@@ -3692,8 +3697,42 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {},
             ),
             (
+                "#1.6a Simple component, attendee PARTSTAT->NEEDS-ACTION only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user2 at example.com",))},
+            ),
+            (
                 "#1.7 Simple component, attendee PARTSTAT and addition",
                 """BEGIN:VCALENDAR
 VERSION:2.0
@@ -3725,8 +3764,43 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user3 at example.com",))},
             ),
             (
+                "#1.7a Simple component, attendee PARTSTAT->NEEDS-ACTION and addition",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user2 at example.com", "mailto:user3 at example.com",))},
+            ),
+            (
                 "#1.8 Simple component, attendee RSVP only",
                 """BEGIN:VCALENDAR
 VERSION:2.0
@@ -3757,6 +3831,7 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {},
             ),
             (
                 "#1.9 Simple component, DTSTART/DTEND VALUE",
@@ -3789,6 +3864,7 @@
 END:VCALENDAR
 """,
                 {"": {"DTEND": set(("VALUE",)), "DTSTART": set(("VALUE",)), }},
+                {},
             ),
             (
                 "#1.10 Simple component, DTSTART/DTEND TZID",
@@ -3857,6 +3933,7 @@
 END:VCALENDAR
 """,
                 {"": {"DTEND": set(("TZID",)), "DTSTART": set(("TZID",)), }},
+                {},
             ),
         )
 
@@ -3894,6 +3971,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#2.2 Simple component, one property change",
@@ -3922,6 +4000,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), }},
+                {},
             ),
             (
                 "#2.3 Simple component, one property change, one addition, one removal",
@@ -3952,6 +4031,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), "LOCATION": set(), "DESCRIPTION": set(), }},
+                {},
             ),
             (
                 "#2.4 Simple component, add attendee",
@@ -3987,6 +4067,7 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user3 at example.com",))},
             ),
             (
                 "#2.5 Simple component, remove attendee",
@@ -4020,6 +4101,7 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {},
             ),
             (
                 "#2.6 Simple component, attendee PARTSTAT only",
@@ -4054,8 +4136,44 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {},
             ),
             (
+                "#2.6a Simple component, attendee PARTSTAT->NEEDS-ACTION only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+RRULE:COUNT=400;FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:COUNT=400;FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user2 at example.com",))},
+            ),
+            (
                 "#2.7 Simple component, attendee PARTSTAT and addition",
                 """BEGIN:VCALENDAR
 VERSION:2.0
@@ -4089,8 +4207,45 @@
 END:VCALENDAR
 """,
                 {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user3 at example.com",))},
             ),
             (
+                "#2.7a Simple component, attendee PARTSTAT->NEEDS-ACTION and addition",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+RRULE:COUNT=400;FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:COUNT=400;FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                {"": {"ATTENDEE": set(), }},
+                {"": set(("mailto:user2 at example.com", "mailto:user3 at example.com",))},
+            ),
+            (
                 "#2.8 Simple recurring component, property order change",
                 """BEGIN:VCALENDAR
 VERSION:2.0
@@ -4127,6 +4282,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
         )
 
@@ -4184,6 +4340,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#3.2 Simple component, one property change in instance",
@@ -4232,6 +4389,7 @@
 END:VCALENDAR
 """,
                 {"20080602T120000Z": {"SUMMARY": set(), }},
+                {},
             ),
             (
                 "#3.3 Simple component, one property change in master",
@@ -4280,6 +4438,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), }},
+                {},
             ),
             (
                 "#3.4 Simple component, one property change in master and instance",
@@ -4328,6 +4487,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set(), }, "20080602T120000Z": {"SUMMARY": set(), }},
+                {},
             ),
             (
                 "#3.5 Simple component, different property change in master and instance",
@@ -4377,6 +4537,7 @@
 END:VCALENDAR
 """,
                 {"": {"SUMMARY": set()}, "20080602T120000Z": {"Description": set()}},
+                {},
             ),
             (
                 "#3.6 Simple component, instance added no change",
@@ -4412,6 +4573,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#3.7 Simple component, instance added time change",
@@ -4447,6 +4609,7 @@
 END:VCALENDAR
 """,
                 {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
+                {},
             ),
             (
                 "#3.8 Simple component, instance removed no change",
@@ -4482,6 +4645,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#3.9 Simple component, instance removed time change",
@@ -4517,6 +4681,7 @@
 END:VCALENDAR
 """,
                 {"20080602T120000Z": {"DTSTART": set(), "DTEND": set(), }},
+                {},
             ),
         )
 
@@ -4555,6 +4720,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
             (
                 "#4.2 Override component added",
@@ -4590,6 +4756,7 @@
 END:VCALENDAR
 """,
                 {},
+                {},
             ),
         )
 
@@ -4628,6 +4795,7 @@
 END:VCALENDAR
 """,
                 {"20080602T120000Z": {"DTSTART": set()}},
+                {},
             ),
             (
                 "#5.2 Override component added",
@@ -4663,19 +4831,23 @@
 END:VCALENDAR
 """,
                 {"20080602T120000Z": {"DTSTART": set()}},
+                {},
             ),
         )
 
-        for description, calendar1, calendar2, rids in itertools.chain(data1, data2, data3, data4,):
+        for description, calendar1, calendar2, rids, changes in itertools.chain(data1, data2, data3, data4,):
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
-            got_rids = differ.whatIsDifferent()
+            got_rids, got_changes = differ.whatIsDifferent()
             rids = dict([(DateTime.parseText(k) if k else None, v) for k, v in rids.items()])
+            changes = dict([(DateTime.parseText(k) if k else None, v) for k, v in changes.items()])
             self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))
+            self.assertEqual(got_changes, changes, msg="%s expected changes R-IDs: '%s', got: '%s'" % (description, changes, got_changes,))
 
-        for description, calendar1, calendar2, rids in itertools.chain(data5,):
+        for description, calendar1, calendar2, rids, changes in itertools.chain(data5,):
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
-            got_rids = differ.whatIsDifferent(isiTip=False)
+            got_rids, got_changes = differ.whatIsDifferent(isiTip=False)
             self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))
+            self.assertEqual(got_changes, changes, msg="%s expected changes R-IDs: '%s', got: '%s'" % (description, changes, got_changes,))
 
 
     def test_attendee_needs_action(self):
@@ -5241,7 +5413,7 @@
 
         for description, calendar1, calendar2, rids, rescheduled in data:
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
-            diffs = differ.whatIsDifferent()
+            diffs, _ignore_changes = differ.whatIsDifferent()
             got_rids, got_rescheduled = differ.attendeeNeedsAction(diffs)
             rids = set([DateTime.parseText(k) if k else None for k in rids])
             self.assertEqual(got_rids, rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, rids, got_rids,))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -115,16 +115,214 @@
 END:VEVENT
 END:VCALENDAR
 """,
+                0,
             ),
+            (
+                "2.1 partstat change pinned to sequence - changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;X-CALENDARSERVER-RESET-PARTSTAT=2:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:2
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:2
+SUMMARY:Test - override
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                1,
+            ),
+            (
+                "2.2 partstat change pinned to sequence - not changed",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;X-CALENDARSERVER-RESET-PARTSTAT=1:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                1,
+            ),
+            (
+                "3.1 partstat change pinned to sequence - with recurrence override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Test - override
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T010000Z
+DTSTART:20071115T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Test
+BEGIN:VALARM
+DESCRIPTION:Event reminder
+TRIGGER:-PT1M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;X-CALENDARSERVER-RESET-PARTSTAT=1:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:Test - override
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T010000Z
+DTSTART:20071115T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SEQUENCE:1
+SUMMARY:Test - override
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+DESCRIPTION:Event reminder
+TRIGGER:-PT1M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                2,
+            ),
         )
 
-        for title, calendar_txt, itip_txt, changed_txt in data:
+        for title, calendar_txt, itip_txt, changed_txt, changed_rid_count in data:
             calendar = Component.fromString(calendar_txt)
             itip = Component.fromString(itip_txt)
             changed = Component.fromString(changed_txt)
 
             result, rids = iTipProcessing.processRequest(itip, calendar, "mailto:user02 at example.com")
-            self.assertEqual(len(rids), 0)
+            self.assertEqual(len(rids), changed_rid_count, msg="Rids mismatch: %s" % (title,))
             self.assertEqual(result, changed, msg="Calendar mismatch: %s" % (title,))
 
 
@@ -1393,6 +1591,190 @@
 """,
                 True,
             ),
+            (
+                "2.1 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=10;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+SEQUENCE:9
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=10;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.2 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=10;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=10;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.3 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=9;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+SEQUENCE:9
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=9;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "2.4 X-CALENDARSERVER-RESET-PARTSTAT Reply - non recurring",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=9;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-CALENDARSERVER-RESET-PARTSTAT=9;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+SEQUENCE:10
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
         )
 
         for title, calendar_txt, itip_txt, changed_txt, expected in data:

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_work.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_work.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_work.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, diff_iCalStrs, normalize_iCalStr
 from twext.enterprise.jobqueue import JobItem, WorkItem
 from txdav.common.datastore.sql_tables import scheduleActionFromSQL
 from twisted.internet import reactor
@@ -22,7 +22,7 @@
 Tests for txdav.caldav.datastore.utils
 """
 
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial import unittest
 
 from txdav.caldav.datastore.scheduling.work import ScheduleOrganizerWork, \
@@ -77,7 +77,115 @@
         return self._sqlCalendarStore
 
 
+    @inlineCallbacks
+    def _runAllJobs(self):
+        """
+        Run all outstanding jobs.
+        """
+        # Run jobs
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        while jobs:
+            yield jobs[0].run()
+            yield self.commit()
+            jobs = yield JobItem.all(self.transactionUnderTest())
+        yield self.commit()
 
+
+    @inlineCallbacks
+    def _runOneJob(self):
+        """
+        Run the first outstanding jobs.
+        """
+        # Run jobs
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        for job in jobs:
+            yield job.run()
+            break
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def createOrganizerEvent(self, organizer, ical, run_jobs=True):
+        """
+        Create an organizer event and wait for the jobs to complete.
+        """
+        cal = yield self.calendarUnderTest(name="calendar", home=organizer)
+        yield cal.createCalendarObjectWithName("invite.ics", ical)
+        yield self.commit()
+
+        if run_jobs:
+            yield self._runAllJobs()
+
+
+    @inlineCallbacks
+    def getOrganizerResource(self, organizer):
+        """
+        Get the attendee's event.
+        """
+        calobj = yield self.calendarObjectUnderTest(name="invite.ics", calendar_name="calendar", home=organizer)
+        returnValue(calobj)
+
+
+    @inlineCallbacks
+    def setOrganizerEvent(self, organizer, ical, run_jobs=True):
+        """
+        Set the organizer's event.
+        """
+        calobj = yield self.getOrganizerResource(organizer)
+        yield calobj.setComponent(ical)
+        yield self.commit()
+
+        if run_jobs:
+            yield self._runAllJobs()
+
+
+    @inlineCallbacks
+    def getOrganizerEvent(self, organizer):
+        """
+        Get the organizer's event.
+        """
+        calobj = yield self.getOrganizerResource(organizer)
+        comp = yield calobj.componentForUser()
+        yield self.commit()
+        returnValue(comp)
+
+
+    @inlineCallbacks
+    def getAttendeeResource(self, attendee):
+        """
+        Get the attendee's event.
+        """
+        cal = yield self.calendarUnderTest(name="calendar", home=attendee)
+        calobjs = yield cal.calendarObjects()
+        self.assertEqual(len(calobjs), 1)
+        returnValue(calobjs[0])
+
+
+    @inlineCallbacks
+    def setAttendeeEvent(self, attendee, ical, run_jobs=True):
+        """
+        Set the attendee's event.
+        """
+        calobj = yield self.getAttendeeResource(attendee)
+        yield calobj.setComponent(ical)
+        yield self.commit()
+
+        if run_jobs:
+            yield self._runAllJobs()
+
+
+    @inlineCallbacks
+    def getAttendeeEvent(self, attendee):
+        """
+        Get the attendee's event.
+        """
+        calobj = yield self.getAttendeeResource(attendee)
+        comp = yield calobj.componentForUser()
+        yield self.commit()
+        returnValue(comp)
+
+
+
 class TestScheduleOrganizerWork(BaseWorkTests):
     """
     Test creation of L{ScheduleOrganizerWork} items.
@@ -264,3 +372,595 @@
         yield work.delete()
         yield jobs[0].delete()
         yield self.commit()
+
+
+
+class TestScheduleWork(BaseWorkTests):
+    """
+    Test various scheduling work scenarios that are potential race conditions and could give rise to
+    partstat mismatches between organizer and attendee, or cause work items to fail.
+    """
+
+    def configure(self):
+        super(TestScheduleWork, self).configure()
+
+        # Enable the queue and make it slow
+        self.patch(self.config.Scheduling.Options.WorkQueues, "Enabled", True)
+        self.patch(self.config.Scheduling.Options.WorkQueues, "RequestDelaySeconds", 1000)
+        self.patch(self.config.Scheduling.Options.WorkQueues, "ReplyDelaySeconds", 1000)
+        self.patch(self.config.Scheduling.Options.WorkQueues, "AutoReplyDelaySeconds", 1000)
+        self.patch(self.config.Scheduling.Options.WorkQueues, "AttendeeRefreshBatchDelaySeconds", 1000)
+        self.patch(self.config.Scheduling.Options.WorkQueues, "AttendeeRefreshBatchIntervalSeconds", 1000)
+        self.patch(JobItem, "failureRescheduleInterval", 1000)
+        self.patch(JobItem, "lockRescheduleInterval", 1000)
+
+
+    @inlineCallbacks
+    def test_replyBeforeResourceDelete(self):
+        """
+        Test that a reply is sent if an attendee changes an event, then immediately deletes it.
+        """
+
+        organizer1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=DECLINED;SCHEDULE-STATUS=2.0:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=DECLINED:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        yield self.createOrganizerEvent("user01", organizer1)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(attendee, attendee1, msg=diff_iCalStrs(attendee, attendee1))
+
+        yield self.setAttendeeEvent("user02", attendee2, run_jobs=False)
+        calobj = yield self.getAttendeeResource("user02")
+        yield calobj.remove()
+        yield self.commit()
+
+        yield self._runAllJobs()
+
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        self.assertEqual(len(jobs), 0)
+        yield self.commit()
+
+        organizer = yield self.getOrganizerEvent("user01")
+        self.assertEqual(organizer, organizer2, msg=diff_iCalStrs(organizer, organizer2))
+
+
+    @inlineCallbacks
+    def test_replyBeforeOrganizerEXDATE(self):
+        """
+        Test that a reply is sent if an attendee changes an event, but the organizer exdate's
+        the instance before the reply work is processed.
+        """
+
+        organizer1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+RRULE:FREQ=DAILY
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+EXDATE:20080602T130000Z
+RRULE:FREQ=DAILY
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+RRULE:FREQ=DAILY
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T130000Z
+DTSTAMP:20080601T130000Z
+DTSTART:20080602T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=DECLINED:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+EXDATE:20080602T130000Z
+RRULE:FREQ=DAILY
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        yield self.createOrganizerEvent("user01", organizer1)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(attendee, attendee1, msg=diff_iCalStrs(attendee, attendee1))
+
+        yield self.setOrganizerEvent("user01", organizer2, run_jobs=False)
+        yield self._runOneJob()
+        yield self.setAttendeeEvent("user02", attendee2, run_jobs=False)
+        yield self.setAttendeeEvent("user02", attendee3, run_jobs=False)
+
+        yield self._runAllJobs()
+
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        self.assertEqual(len(jobs), 0)
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_replyBeforeOrganizerInconsequentialChange(self):
+        """
+        Test that the organizer and attendee see the attendee's partstat change when the organizer makes
+        an inconsequential change whilst the attendee reply is in progress.
+        """
+
+        organizer1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:urn:x-uid:user02
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com;SCHEDULE-STATUS=1.2:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+
+        yield self.createOrganizerEvent("user01", organizer1)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(attendee, attendee1, msg=diff_iCalStrs(attendee, attendee1))
+
+        yield self.setOrganizerEvent("user01", organizer2, run_jobs=False)
+        yield self._runOneJob()
+        yield self.setAttendeeEvent("user02", attendee2, run_jobs=False)
+
+        yield self._runAllJobs()
+
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        self.assertEqual(len(jobs), 0)
+        yield self.commit()
+
+        organizer = yield self.getOrganizerEvent("user01")
+        self.assertEqual(normalize_iCalStr(organizer), normalize_iCalStr(organizer3), msg=diff_iCalStrs(organizer3, organizer))
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(normalize_iCalStr(attendee), normalize_iCalStr(attendee3), msg=diff_iCalStrs(attendee3, attendee))
+
+
+    @inlineCallbacks
+    def test_replyBeforeOrganizerConsequentialChange(self):
+        """
+        Test that the organizer and attendee see the attendee's partstat change when the organizer makes
+        a consequential change whilst the attendee reply is in progress.
+        """
+
+        organizer1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080602T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080602T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2;X-CALENDARSERVER-RESET-PARTSTAT=1:urn:x-uid:user02
+SEQUENCE:1
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080602T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com;SCHEDULE-STATUS=1.2:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+SEQUENCE:1
+SUMMARY:Test
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+
+        yield self.createOrganizerEvent("user01", organizer1)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(attendee, attendee1, msg=diff_iCalStrs(attendee, attendee1))
+
+        yield self.setOrganizerEvent("user01", organizer2, run_jobs=False)
+        yield self._runOneJob()
+        yield self.setAttendeeEvent("user02", attendee2, run_jobs=False)
+
+        yield self._runAllJobs()
+
+        jobs = yield JobItem.all(self.transactionUnderTest())
+        self.assertEqual(len(jobs), 0)
+        yield self.commit()
+
+        organizer = yield self.getOrganizerEvent("user01")
+        self.assertEqual(normalize_iCalStr(organizer), normalize_iCalStr(organizer3), msg=diff_iCalStrs(organizer3, organizer))
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(normalize_iCalStr(attendee), normalize_iCalStr(attendee3), msg=diff_iCalStrs(attendee3, attendee))
+
+
+    @inlineCallbacks
+    def test_needsActionOrganizerChange(self):
+        """
+        Test that if the organizer makes an inconsequential change and also changes the
+        attendee partstat, then the new partstat is sent to the attendee.
+        """
+
+        organizer1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        organizer3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION:urn:x-uid:user02
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee1 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee2 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user02
+END:VEVENT
+END:VCALENDAR
+""")
+
+        attendee3 = Component.fromString("""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTAMP:20080601T130000Z
+DTSTART:20080601T130000Z
+DURATION:PT1H
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com;SCHEDULE-STATUS=1.2:urn:x-uid:user01
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:x-uid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:urn:x-uid:user02
+SEQUENCE:1
+SUMMARY:Test
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""")
+
+
+        yield self.createOrganizerEvent("user01", organizer1)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(attendee, attendee1, msg=diff_iCalStrs(attendee, attendee1))
+        yield self.setAttendeeEvent("user02", attendee2)
+        organizer = yield self.getOrganizerEvent("user01")
+        self.assertEqual(normalize_iCalStr(organizer), normalize_iCalStr(organizer2), msg=diff_iCalStrs(organizer2, organizer))
+
+        yield self.setOrganizerEvent("user01", organizer3)
+        attendee = yield self.getAttendeeEvent("user02")
+        self.assertEqual(normalize_iCalStr(attendee), normalize_iCalStr(attendee3), msg=diff_iCalStrs(attendee3, attendee))

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/work.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -26,13 +26,11 @@
 from twistedcaldav.ical import Component
 
 from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserUID
-from txdav.caldav.datastore.scheduling.itip import iTipGenerator, iTIPRequestStatus
+from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
 from txdav.caldav.icalendarstore import ComponentUpdateState
 from txdav.common.datastore.sql_tables import schema, \
     scheduleActionToSQL, scheduleActionFromSQL
 
-from pycalendar.datetime import DateTime
-
 import datetime
 import hashlib
 import traceback
@@ -40,7 +38,6 @@
 __all__ = [
     "ScheduleOrganizerWork",
     "ScheduleReplyWork",
-    "ScheduleReplyCancelWork",
     "ScheduleRefreshWork",
     "ScheduleAutoReplyWork",
 ]
@@ -504,33 +501,7 @@
 
 
 
-class ScheduleReplyWorkMixin(ScheduleWorkMixin):
-
-
-    def makeScheduler(self, home):
-        """
-        Convenience method which we can override in unit tests to make testing easier.
-        """
-        from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
-        return CalDAVScheduler(self.transaction, home.uid())
-
-
-    @inlineCallbacks
-    def sendToOrganizer(self, home, action, itipmsg, originator, recipient):
-
-        # Send scheduling message
-
-        # This is a local CALDAV scheduling operation.
-        scheduler = self.makeScheduler(home)
-
-        # Do the PUT processing
-        log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, originator, recipient, itipmsg.resourceUID(),))
-        response = (yield scheduler.doSchedulingViaPUT(originator, (recipient,), itipmsg, internal_request=True))
-        returnValue(response)
-
-
-
-class ScheduleReplyWork(ScheduleReplyWorkMixin, fromTable(schema.SCHEDULE_REPLY_WORK)):
+class ScheduleReplyWork(ScheduleWorkMixin, fromTable(schema.SCHEDULE_REPLY_WORK)):
     """
     The associated work item table is SCHEDULE_REPLY_WORK.
 
@@ -540,121 +511,75 @@
 
     @classmethod
     @inlineCallbacks
-    def reply(cls, txn, home, resource, changedRids, attendee):
+    def reply(cls, txn, home, resource, itipmsg, attendee):
         # Always queue up new work - coalescing happens when work is executed
         notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=config.Scheduling.Options.WorkQueues.ReplyDelaySeconds)
+        uid = itipmsg.resourceUID()
         proposal = (yield txn.enqueue(
             cls,
             notBefore=notBefore,
-            icalendarUid=resource.uid(),
+            icalendarUid=uid,
             homeResourceID=home.id(),
-            resourceID=resource.id(),
-
-            # Serialize None as ""
-            changedRids=",".join(map(lambda x: "" if x is None else str(x), changedRids)) if changedRids else None,
+            resourceID=resource.id() if resource else None,
+            itipMsg=itipmsg.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference),
         ))
         cls._enqueued()
-        log.debug("ScheduleReplyWork - enqueued for ID: {id}, UID: {uid}, attendee: {att}", id=proposal.workItem.workID, uid=resource.uid(), att=attendee)
+        log.debug("ScheduleReplyWork - enqueued for ID: {id}, UID: {uid}, attendee: {att}", id=proposal.workItem.workID, uid=uid, att=attendee)
 
 
     @inlineCallbacks
-    def doWork(self):
+    def sendToOrganizer(self, home, itipmsg, originator, recipient):
 
-        try:
-            home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
-            resource = (yield home.objectResourceWithID(self.resourceID))
-            attendeeAddress = yield calendarUserFromCalendarUserUID(home.uid(), self.transaction)
-            attendee = attendeeAddress.record.canonicalCalendarUserAddress()
-            calendar = (yield resource.componentForUser())
-            organizer = calendar.validOrganizerForScheduling()
+        # Send scheduling message
 
-            # Deserialize "" as None
-            changedRids = map(lambda x: DateTime.parseText(x) if x else None, self.changedRids.split(",")) if self.changedRids else None
+        # This is a local CALDAV scheduling operation.
+        from txdav.caldav.datastore.scheduling.caldav.scheduler import CalDAVScheduler
+        scheduler = CalDAVScheduler(self.transaction, home.uid())
 
-            log.debug("ScheduleReplyWork - running for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=calendar.resourceUID(), att=attendee)
+        # Do the PUT processing
+        log.info("Implicit REPLY - attendee: '%s' to organizer: '%s', UID: '%s'" % (originator, recipient, itipmsg.resourceUID(),))
+        response = (yield scheduler.doSchedulingViaPUT(originator, (recipient,), itipmsg, internal_request=True))
+        returnValue(response)
 
-            # We need to get the UID lock for implicit processing.
-            yield NamedLock.acquire(self.transaction, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
 
-            itipmsg = iTipGenerator.generateAttendeeReply(calendar, attendee, changedRids=changedRids)
-
-            # Send scheduling message and process response
-            response = (yield self.sendToOrganizer(home, "REPLY", itipmsg, attendee, organizer))
-            responses, all_delivered = self.extractSchedulingResponse((response,))
-            if not all_delivered:
-                changed = yield self.handleSchedulingResponse(responses, calendar, False)
-                if changed:
-                    yield resource._setComponentInternal(calendar, internal_state=ComponentUpdateState.ATTENDEE_ITIP_UPDATE)
-
-            self._dequeued()
-
-        except Exception, e:
-            # FIXME: calendar may not be set here!
-            log.debug("ScheduleReplyWork - exception ID: {id}, UID: '{uid}', {err}", id=self.workID, uid=calendar.resourceUID(), err=str(e))
-            raise
-        except:
-            log.debug("ScheduleReplyWork - bare exception ID: {id}, UID: '{uid}'", id=self.workID, uid=calendar.resourceUID())
-            raise
-
-        log.debug("ScheduleReplyWork - done for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=calendar.resourceUID(), att=attendee)
-
-
-
-class ScheduleReplyCancelWork(ScheduleReplyWorkMixin, fromTable(schema.SCHEDULE_REPLY_CANCEL_WORK)):
-    """
-    The associated work item table is SCHEDULE_REPLY_CANCEL_WORK.
-
-    This work item is used to send an iTIP reply message when an attendee deletes
-    their copy of the calendar object resource. For this to work we need to store a copy
-    of the original resource data.
-    """
-
-    @classmethod
     @inlineCallbacks
-    def replyCancel(cls, txn, home, calendar, attendee):
-        # Always queue up new work - coalescing happens when work is executed
-        notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=config.Scheduling.Options.WorkQueues.ReplyDelaySeconds)
-        proposal = (yield txn.enqueue(
-            cls,
-            notBefore=notBefore,
-            icalendarUid=calendar.resourceUID(),
-            homeResourceID=home.id(),
-            icalendarText=calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference),
-        ))
-        cls._enqueued()
-        log.debug("ScheduleReplyCancelWork - enqueued for ID: {id}, UID: {uid}, attendee: {att}", id=proposal.workItem.workID, uid=calendar.resourceUID(), att=attendee)
-
-
-    @inlineCallbacks
     def doWork(self):
 
         try:
             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
+            resource = (yield home.objectResourceWithID(self.resourceID))
+            itipmsg = Component.fromString(self.itipMsg)
             attendeeAddress = yield calendarUserFromCalendarUserUID(home.uid(), self.transaction)
             attendee = attendeeAddress.record.canonicalCalendarUserAddress()
-            calendar = Component.fromString(self.icalendarText)
-            organizer = calendar.validOrganizerForScheduling()
+            organizer = itipmsg.validOrganizerForScheduling()
 
-            log.debug("ScheduleReplyCancelWork - running for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=calendar.resourceUID(), att=attendee)
+            log.debug("ScheduleReplyWork - running for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=itipmsg.resourceUID(), att=attendee)
 
             # We need to get the UID lock for implicit processing.
-            yield NamedLock.acquire(self.transaction, "ImplicitUIDLock:%s" % (hashlib.md5(calendar.resourceUID()).hexdigest(),))
+            yield NamedLock.acquire(self.transaction, "ImplicitUIDLock:%s" % (hashlib.md5(itipmsg.resourceUID()).hexdigest(),))
 
-            itipmsg = iTipGenerator.generateAttendeeReply(calendar, attendee, force_decline=True)
+            # Send scheduling message and process response
+            response = (yield self.sendToOrganizer(home, itipmsg, attendee, organizer))
 
-            # Send scheduling message - no need to process response as original resource is gone
-            yield self.sendToOrganizer(home, "CANCEL", itipmsg, attendee, organizer)
+            if resource is not None:
+                responses, all_delivered = self.extractSchedulingResponse((response,))
+                if not all_delivered:
+                    calendar = (yield resource.componentForUser())
+                    changed = yield self.handleSchedulingResponse(responses, calendar, False)
+                    if changed:
+                        yield resource._setComponentInternal(calendar, internal_state=ComponentUpdateState.ATTENDEE_ITIP_UPDATE)
 
             self._dequeued()
 
         except Exception, e:
-            log.debug("ScheduleReplyCancelWork - exception ID: {id}, UID: '{uid}', {err}", id=self.workID, uid=calendar.resourceUID(), err=str(e))
+            # FIXME: calendar may not be set here!
+            log.debug("ScheduleReplyWork - exception ID: {id}, UID: '{uid}', {err}", id=self.workID, uid=itipmsg.resourceUID(), err=str(e))
             raise
         except:
-            log.debug("ScheduleReplyCancelWork - bare exception ID: {id}, UID: '{uid}'", id=self.workID, uid=calendar.resourceUID())
+            log.debug("ScheduleReplyWork - bare exception ID: {id}, UID: '{uid}'", id=self.workID, uid=itipmsg.resourceUID())
             raise
 
-        log.debug("ScheduleReplyCancelWork - done for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=calendar.resourceUID(), att=attendee)
+        log.debug("ScheduleReplyWork - done for ID: {id}, UID: {uid}, attendee: {att}", id=self.workID, uid=itipmsg.resourceUID(), att=attendee)
 
 
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql_dump.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_dump.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/common/datastore/sql_dump.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -17,7 +17,7 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twext.enterprise.dal.model import Schema, Table, Column, Sequence, Function, \
-    SQLType, ProcedureCall
+    SQLType, ProcedureCall, Constraint
 from twext.enterprise.dal.parseschema import addSQLToSchema
 
 """
@@ -63,8 +63,8 @@
         tables[name.upper()] = table
 
         # Columns
-        rows = yield txn.execSQL("select column_name, data_type, character_maximum_length, column_default from information_schema.columns where table_schema = '%s' and table_name = '%s';" % (schemaname, name,))
-        for name, datatype, charlen, default in rows:
+        rows = yield txn.execSQL("select column_name, data_type, is_nullable, character_maximum_length, column_default from information_schema.columns where table_schema = '%s' and table_name = '%s';" % (schemaname, name,))
+        for name, datatype, is_nullable, charlen, default in rows:
             # TODO: figure out the type
             column = Column(table, name.upper(), SQLType(DTYPE_MAP.get(datatype, datatype), charlen))
             table.columns.append(column)
@@ -79,6 +79,8 @@
                         column.default = int(default)
                     except ValueError:
                         column.default = default
+            if is_nullable == "NO":
+                table.tableConstraint(Constraint.NOT_NULL, [column.name, ])
 
     # Key columns
     keys = {}

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -17,7 +17,7 @@
 );
 
 create table JOB (
-    "JOB_ID" integer primary key not null,
+    "JOB_ID" integer primary key,
     "WORK_TYPE" nvarchar2(255),
     "PRIORITY" integer default 0,
     "WEIGHT" integer default 0,
@@ -370,7 +370,7 @@
 );
 
 create table IMIP_INVITATION_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "FROM_ADDR" nvarchar2(255),
     "TO_ADDR" nvarchar2(255),
@@ -378,12 +378,12 @@
 );
 
 create table IMIP_POLLING_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table IMIP_REPLY_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "ORGANIZER" nvarchar2(255),
     "ATTENDEE" nvarchar2(255),
@@ -391,25 +391,25 @@
 );
 
 create table PUSH_NOTIFICATION_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "PUSH_ID" nvarchar2(255),
     "PUSH_PRIORITY" integer not null
 );
 
 create table GROUP_CACHER_POLLING_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table GROUP_REFRESH_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "GROUP_UID" nvarchar2(255)
 );
 
 create table GROUP_DELEGATE_CHANGES_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "DELEGATOR_UID" nvarchar2(255),
     "READ_DELEGATE_UID" nvarchar2(255),
@@ -433,7 +433,7 @@
 );
 
 create table GROUP_ATTENDEE_RECONCILE_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
     "GROUP_ID" integer not null references GROUPS on delete cascade
@@ -447,7 +447,7 @@
 );
 
 create table GROUP_SHAREE_RECONCILE_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
     "GROUP_ID" integer not null references GROUPS on delete cascade
@@ -483,34 +483,34 @@
 );
 
 create table CALENDAR_OBJECT_SPLITTER_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
 );
 
 create table FIND_MIN_VALID_REVISION_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table REVISION_CLEANUP_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table INBOX_CLEANUP_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table CLEANUP_ONE_INBOX_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "HOME_ID" integer not null unique references CALENDAR_HOME on delete cascade
 );
 
 create table SCHEDULE_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "ICALENDAR_UID" nvarchar2(255),
     "WORK_TYPE" nvarchar2(255)
@@ -569,35 +569,29 @@
 create table SCHEDULE_REPLY_WORK (
     "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
     "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
-    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
-    "CHANGED_RIDS" nclob
+    "RESOURCE_ID" integer,
+    "ITIP_MSG" nclob
 );
 
-create table SCHEDULE_REPLY_CANCEL_WORK (
-    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
-    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
-    "ICALENDAR_TEXT" nclob
-);
-
 create table PRINCIPAL_PURGE_POLLING_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB
 );
 
 create table PRINCIPAL_PURGE_CHECK_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "UID" nvarchar2(255)
 );
 
 create table PRINCIPAL_PURGE_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "UID" nvarchar2(255)
 );
 
 create table PRINCIPAL_PURGE_HOME_WORK (
-    "WORK_ID" integer primary key not null,
+    "WORK_ID" integer primary key,
     "JOB_ID" integer not null references JOB,
     "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
 );
@@ -607,7 +601,7 @@
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '49');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '50');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
@@ -900,10 +894,6 @@
     RESOURCE_ID
 );
 
-create index SCHEDULE_REPLY_CANCEL_dab513ef on SCHEDULE_REPLY_CANCEL_WORK (
-    HOME_RESOURCE_ID
-);
-
 create index PRINCIPAL_PURGE_POLLI_6383e68a on PRINCIPAL_PURGE_POLLING_WORK (
     JOB_ID
 );

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -54,7 +54,7 @@
 create sequence JOB_SEQ;
 
 create table JOB (
-  JOB_ID      integer primary key default nextval('JOB_SEQ') not null, --implicit index
+  JOB_ID      integer primary key default nextval('JOB_SEQ'), --implicit index
   WORK_TYPE   varchar(255) not null,
   PRIORITY    integer default 0,
   WEIGHT      integer default 0,
@@ -688,7 +688,7 @@
 ---------------------------
 
 create table IMIP_INVITATION_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   FROM_ADDR                     varchar(255) not null,
   TO_ADDR                       varchar(255) not null,
@@ -703,7 +703,7 @@
 -----------------------
 
 create table IMIP_POLLING_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -716,7 +716,7 @@
 ---------------------
 
 create table IMIP_REPLY_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   ORGANIZER                     varchar(255) not null,
   ATTENDEE                      varchar(255) not null,
@@ -732,7 +732,7 @@
 ------------------------
 
 create table PUSH_NOTIFICATION_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   PUSH_ID                       varchar(255) not null,
   PUSH_PRIORITY                 integer      not null -- 1:low 5:medium 10:high
@@ -748,7 +748,7 @@
 -----------------
 
 create table GROUP_CACHER_POLLING_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -756,7 +756,7 @@
   GROUP_CACHER_POLLING_WORK(JOB_ID);
 
 create table GROUP_REFRESH_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   GROUP_UID                     varchar(255) not null
 );
@@ -767,7 +767,7 @@
   GROUP_REFRESH_WORK(GROUP_UID);
 
 create table GROUP_DELEGATE_CHANGES_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   DELEGATOR_UID                 varchar(255) not null,
   READ_DELEGATE_UID             varchar(255) not null,
@@ -802,7 +802,7 @@
   GROUP_MEMBERSHIP(MEMBER_UID);
 
 create table GROUP_ATTENDEE_RECONCILE_WORK (
-  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer not null references JOB,
   RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
   GROUP_ID                      integer not null references GROUPS on delete cascade
@@ -829,7 +829,7 @@
 
 
 create table GROUP_SHAREE_RECONCILE_WORK (
-  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer not null references JOB,
   CALENDAR_ID                   integer	not null references CALENDAR on delete cascade,
   GROUP_ID                      integer not null references GROUPS on delete cascade
@@ -881,7 +881,7 @@
   DELEGATE_GROUPS(GROUP_ID);
 
 create table EXTERNAL_DELEGATE_GROUPS (
-  DELEGATOR                     varchar(255) primary key not null,
+  DELEGATOR                     varchar(255) primary key,
   GROUP_UID_READ                varchar(255),
   GROUP_UID_WRITE               varchar(255)
 );
@@ -891,7 +891,7 @@
 --------------------------
 
 create table CALENDAR_OBJECT_SPLITTER_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
 );
@@ -906,7 +906,7 @@
 ---------------------------
 
 create table FIND_MIN_VALID_REVISION_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -914,7 +914,7 @@
   FIND_MIN_VALID_REVISION_WORK(JOB_ID);
 
 create table REVISION_CLEANUP_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -926,7 +926,7 @@
 ------------------------
 
 create table INBOX_CLEANUP_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -934,7 +934,7 @@
    INBOX_CLEANUP_WORK(JOB_ID);
 
 create table CLEANUP_ONE_INBOX_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   HOME_ID                       integer      not null unique references CALENDAR_HOME on delete cascade -- implicit index
 );
@@ -947,7 +947,7 @@
 -------------------
 
 create table SCHEDULE_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   ICALENDAR_UID                 varchar(255) not null,
   WORK_TYPE                     varchar(255) not null
@@ -1058,8 +1058,8 @@
 create table SCHEDULE_REPLY_WORK (
   WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
   HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
-  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
-  CHANGED_RIDS                  text
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ITIP_MSG                      text
 );
 
 create index SCHEDULE_REPLY_WORK_HOME_RESOURCE_ID on
@@ -1067,25 +1067,12 @@
 create index SCHEDULE_REPLY_WORK_RESOURCE_ID on
   SCHEDULE_REPLY_WORK(RESOURCE_ID);
 
---------------------------------
--- Schedule Reply Cancel Work --
---------------------------------
-
-create table SCHEDULE_REPLY_CANCEL_WORK (
-  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
-  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
-  ICALENDAR_TEXT                text         not null
-);
-
-create index SCHEDULE_REPLY_CANCEL_WORK_HOME_RESOURCE_ID on
-  SCHEDULE_REPLY_CANCEL_WORK(HOME_RESOURCE_ID);
-
 ----------------------------------
 -- Principal Purge Polling Work --
 ----------------------------------
 
 create table PRINCIPAL_PURGE_POLLING_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null
 );
 
@@ -1097,7 +1084,7 @@
 --------------------------------
 
 create table PRINCIPAL_PURGE_CHECK_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   UID                           varchar(255) not null
 );
@@ -1112,7 +1099,7 @@
 --------------------------
 
 create table PRINCIPAL_PURGE_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   UID                           varchar(255) not null
 );
@@ -1128,7 +1115,7 @@
 --------------------------------
 
 create table PRINCIPAL_PURGE_HOME_WORK (
-  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ'), -- implicit index
   JOB_ID                        integer      references JOB not null,
   HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
 );
@@ -1148,7 +1135,7 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '49');
+insert into CALENDARSERVER values ('VERSION', '50');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v49.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v49.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v49.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,935 @@
+create sequence RESOURCE_ID_SEQ;
+create sequence JOB_SEQ;
+create sequence INSTANCE_ID_SEQ;
+create sequence ATTACHMENT_ID_SEQ;
+create sequence REVISION_SEQ;
+create sequence WORKITEM_SEQ;
+create table NODE_INFO (
+    "HOSTNAME" nvarchar2(255),
+    "PID" integer not null,
+    "PORT" integer not null,
+    "TIME" timestamp default CURRENT_TIMESTAMP at time zone 'UTC' not null, 
+    primary key ("HOSTNAME", "PORT")
+);
+
+create table NAMED_LOCK (
+    "LOCK_NAME" nvarchar2(255) primary key
+);
+
+create table JOB (
+    "JOB_ID" integer primary key not null,
+    "WORK_TYPE" nvarchar2(255),
+    "PRIORITY" integer default 0,
+    "WEIGHT" integer default 0,
+    "NOT_BEFORE" timestamp not null,
+    "ASSIGNED" timestamp default null,
+    "OVERDUE" timestamp default null,
+    "FAILED" integer default 0
+);
+
+create table CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table HOME_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into HOME_STATUS (DESCRIPTION, ID) values ('normal', 0);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('external', 1);
+insert into HOME_STATUS (DESCRIPTION, ID) values ('purging', 2);
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
+create table CALENDAR_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_POLLS" integer default null references CALENDAR on delete set null,
+    "ALARM_VEVENT_TIMED" nclob default null,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "AVAILABILITY" nclob default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDAR_METADATA (
+    "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
+    "SUPPORTED_COMPONENTS" nvarchar2(255) default null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table NOTIFICATION_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "NOTIFICATION_TYPE" nvarchar2(255),
+    "NOTIFICATION_DATA" nclob,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("NOTIFICATION_UID", "NOTIFICATION_HOME_RESOURCE_ID")
+);
+
+create table CALENDAR_BIND (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "EXTERNAL_ID" integer default null,
+    "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,
+    "ALARM_VEVENT_ALLDAY" nclob default null,
+    "ALARM_VTODO_TIMED" nclob default null,
+    "ALARM_VTODO_ALLDAY" nclob default null,
+    "TIMEZONE" nclob default null, 
+    primary key ("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID"), 
+    unique ("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_NAME")
+);
+
+create table CALENDAR_BIND_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('own', 0);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('write', 2);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('direct', 3);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('indirect', 4);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group', 5);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_read', 6);
+insert into CALENDAR_BIND_MODE (DESCRIPTION, ID) values ('group_write', 7);
+create table CALENDAR_BIND_STATUS (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invited', 0);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('accepted', 1);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('declined', 2);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('invalid', 3);
+insert into CALENDAR_BIND_STATUS (DESCRIPTION, ID) values ('deleted', 4);
+create table CALENDAR_TRANSP (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('opaque', 0);
+insert into CALENDAR_TRANSP (DESCRIPTION, ID) values ('transparent', 1);
+create table CALENDAR_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob,
+    "ICALENDAR_UID" nvarchar2(255),
+    "ICALENDAR_TYPE" nvarchar2(255),
+    "ATTACHMENTS_MODE" integer default 0 not null,
+    "DROPBOX_ID" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "RECURRANCE_MIN" date,
+    "RECURRANCE_MAX" date,
+    "ACCESS" integer default 0 not null,
+    "SCHEDULE_OBJECT" integer default 0,
+    "SCHEDULE_TAG" nvarchar2(36) default null,
+    "SCHEDULE_ETAGS" nclob default null,
+    "PRIVATE_COMMENTS" integer default 0 not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE (DESCRIPTION, ID) values ('write', 2);
+create table CALENDAR_ACCESS_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(32) unique
+);
+
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('', 0);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('public', 1);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('private', 2);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('confidential', 3);
+insert into CALENDAR_ACCESS_TYPE (DESCRIPTION, ID) values ('restricted', 4);
+create table TIME_RANGE (
+    "INSTANCE_ID" integer primary key,
+    "CALENDAR_RESOURCE_ID" integer not null references CALENDAR on delete cascade,
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "FLOATING" integer not null,
+    "START_DATE" timestamp not null,
+    "END_DATE" timestamp not null,
+    "FBTYPE" integer not null,
+    "TRANSPARENT" integer not null
+);
+
+create table FREE_BUSY_TYPE (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('unknown', 0);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('free', 1);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy', 2);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-unavailable', 3);
+insert into FREE_BUSY_TYPE (DESCRIPTION, ID) values ('busy-tentative', 4);
+create table PERUSER (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null,
+    "ADJUSTED_START_DATE" timestamp default null,
+    "ADJUSTED_END_DATE" timestamp default null, 
+    primary key ("TIME_RANGE_INSTANCE_ID", "USER_ID")
+);
+
+create table ATTACHMENT (
+    "ATTACHMENT_ID" integer primary key,
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "DROPBOX_ID" nvarchar2(255),
+    "CONTENT_TYPE" nvarchar2(255),
+    "SIZE" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PATH" nvarchar2(1024)
+);
+
+create table ATTACHMENT_CALENDAR_OBJECT (
+    "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,
+    "MANAGED_ID" nvarchar2(255),
+    "CALENDAR_OBJECT_RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade, 
+    primary key ("ATTACHMENT_ID", "CALENDAR_OBJECT_RESOURCE_ID"), 
+    unique ("MANAGED_ID", "CALENDAR_OBJECT_RESOURCE_ID")
+);
+
+create table RESOURCE_PROPERTY (
+    "RESOURCE_ID" integer not null,
+    "NAME" nvarchar2(255),
+    "VALUE" nclob,
+    "VIEWER_UID" nvarchar2(255), 
+    primary key ("RESOURCE_ID", "NAME", "VIEWER_UID")
+);
+
+create table ADDRESSBOOK_HOME (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_PROPERTY_STORE_ID" integer not null,
+    "OWNER_UID" nvarchar2(255) unique,
+    "STATUS" integer default 0 not null,
+    "DATAVERSION" integer default 0 not null
+);
+
+create table ADDRESSBOOK_HOME_METADATA (
+    "RESOURCE_ID" integer primary key references ADDRESSBOOK_HOME on delete cascade,
+    "QUOTA_USED_BYTES" integer default 0 not null,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table SHARED_ADDRESSBOOK_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "EXTERNAL_ID" integer default null,
+    "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", "OWNER_HOME_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "ADDRESSBOOK_RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT (
+    "RESOURCE_ID" integer primary key,
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "VCARD_TEXT" nclob,
+    "VCARD_UID" nvarchar2(255),
+    "KIND" integer not null,
+    "MD5" nchar(32),
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "DATAVERSION" integer default 0 not null, 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "RESOURCE_NAME"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "VCARD_UID")
+);
+
+create table ADDRESSBOOK_OBJECT_KIND (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('person', 0);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('group', 1);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('resource', 2);
+insert into ADDRESSBOOK_OBJECT_KIND (DESCRIPTION, ID) values ('location', 3);
+create table ABO_MEMBERS (
+    "GROUP_ID" integer not null,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ID" integer not null,
+    "REVISION" integer not null,
+    "REMOVED" integer default 0 not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key ("GROUP_ID", "MEMBER_ID", "REVISION")
+);
+
+create table ABO_FOREIGN_MEMBERS (
+    "GROUP_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ADDRESS" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_ADDRESS")
+);
+
+create table SHARED_GROUP_BIND (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "GROUP_RESOURCE_ID" integer not null references ADDRESSBOOK_OBJECT on delete cascade,
+    "EXTERNAL_ID" integer default null,
+    "GROUP_ADDRESSBOOK_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", "GROUP_RESOURCE_ID"), 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_NAME")
+);
+
+create table CALENDAR_OBJECT_REVISIONS (
+    "CALENDAR_HOME_RESOURCE_ID" integer not null references CALENDAR_HOME,
+    "CALENDAR_RESOURCE_ID" integer references CALENDAR,
+    "CALENDAR_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("CALENDAR_HOME_RESOURCE_ID", "CALENDAR_RESOURCE_ID", "CALENDAR_NAME", "RESOURCE_NAME")
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "OBJECT_RESOURCE_ID" integer default 0,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("ADDRESSBOOK_HOME_RESOURCE_ID", "OWNER_HOME_RESOURCE_ID", "ADDRESSBOOK_NAME", "RESOURCE_NAME")
+);
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME on delete cascade,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null,
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    unique ("NOTIFICATION_HOME_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table APN_SUBSCRIPTIONS (
+    "TOKEN" nvarchar2(255),
+    "RESOURCE_KEY" nvarchar2(255),
+    "MODIFIED" integer not null,
+    "SUBSCRIBER_GUID" nvarchar2(255),
+    "USER_AGENT" nvarchar2(255) default null,
+    "IP_ADDR" nvarchar2(255) default null, 
+    primary key ("TOKEN", "RESOURCE_KEY")
+);
+
+create table IMIP_TOKENS (
+    "TOKEN" nvarchar2(255),
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALUID" nvarchar2(255),
+    "ACCESSED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC', 
+    primary key ("ORGANIZER", "ATTENDEE", "ICALUID")
+);
+
+create table IMIP_INVITATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "PUSH_ID" nvarchar2(255),
+    "PUSH_PRIORITY" integer not null
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table GROUP_REFRESH_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "GROUP_UID" nvarchar2(255)
+);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "DELEGATOR_UID" nvarchar2(255),
+    "READ_DELEGATE_UID" nvarchar2(255),
+    "WRITE_DELEGATE_UID" nvarchar2(255)
+);
+
+create table GROUPS (
+    "GROUP_ID" integer primary key,
+    "NAME" nvarchar2(255),
+    "GROUP_UID" nvarchar2(255) unique,
+    "MEMBERSHIP_HASH" nvarchar2(255),
+    "EXTANT" integer default 1,
+    "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table GROUP_MEMBERSHIP (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "MEMBER_UID" nvarchar2(255), 
+    primary key ("GROUP_ID", "MEMBER_UID")
+);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_ATTENDEE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "RESOURCE_ID")
+);
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_ID" integer not null references GROUPS on delete cascade
+);
+
+create table GROUP_SHAREE (
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "CALENDAR_ID" integer not null references CALENDAR on delete cascade,
+    "GROUP_BIND_MODE" integer not null,
+    "MEMBERSHIP_HASH" nvarchar2(255), 
+    primary key ("GROUP_ID", "CALENDAR_ID")
+);
+
+create table DELEGATES (
+    "DELEGATOR" nvarchar2(255),
+    "DELEGATE" nvarchar2(255),
+    "READ_WRITE" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "DELEGATE")
+);
+
+create table DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255),
+    "GROUP_ID" integer not null references GROUPS on delete cascade,
+    "READ_WRITE" integer not null,
+    "IS_EXTERNAL" integer not null, 
+    primary key ("DELEGATOR", "READ_WRITE", "GROUP_ID")
+);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+    "DELEGATOR" nvarchar2(255) primary key,
+    "GROUP_UID_READ" nvarchar2(255),
+    "GROUP_UID_WRITE" nvarchar2(255)
+);
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+create table FIND_MIN_VALID_REVISION_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table REVISION_CLEANUP_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table INBOX_CLEANUP_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table CLEANUP_ONE_INBOX_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "HOME_ID" integer not null unique references CALENDAR_HOME on delete cascade
+);
+
+create table SCHEDULE_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "ICALENDAR_UID" nvarchar2(255),
+    "WORK_TYPE" nvarchar2(255)
+);
+
+create table SCHEDULE_REFRESH_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE_COUNT" integer
+);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "ATTENDEE" nvarchar2(255), 
+    primary key ("RESOURCE_ID", "ATTENDEE")
+);
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "PARTSTAT" nvarchar2(255)
+);
+
+create table SCHEDULE_ORGANIZER_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ICALENDAR_TEXT_OLD" nclob,
+    "ICALENDAR_TEXT_NEW" nclob,
+    "ATTENDEE_COUNT" integer,
+    "SMART_MERGE" integer
+);
+
+create table SCHEDULE_ACTION (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "SCHEDULE_ACTION" integer not null,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer,
+    "ATTENDEE" nvarchar2(255),
+    "ITIP_MSG" nclob,
+    "NO_REFRESH" integer
+);
+
+create table SCHEDULE_REPLY_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+    "CHANGED_RIDS" nclob
+);
+
+create table SCHEDULE_REPLY_CANCEL_WORK (
+    "WORK_ID" integer primary key references SCHEDULE_WORK on delete cascade,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB
+);
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "UID" nvarchar2(255)
+);
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '49');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER (NAME, VALUE) values ('MIN-VALID-REVISION', '1');
+create index CALENDAR_HOME_METADAT_3cb9049e on CALENDAR_HOME_METADATA (
+    DEFAULT_EVENTS
+);
+
+create index CALENDAR_HOME_METADAT_d55e5548 on CALENDAR_HOME_METADATA (
+    DEFAULT_TASKS
+);
+
+create index CALENDAR_HOME_METADAT_910264ce on CALENDAR_HOME_METADATA (
+    DEFAULT_POLLS
+);
+
+create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (
+    NOTIFICATION_HOME_RESOURCE_ID
+);
+
+create index CALENDAR_BIND_RESOURC_e57964d4 on CALENDAR_BIND (
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_CALEN_a9a453a9 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_CALEN_c4dc619c on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX,
+    RECURRANCE_MIN
+);
+
+create index CALENDAR_OBJECT_ICALE_82e731d5 on CALENDAR_OBJECT (
+    ICALENDAR_UID
+);
+
+create index CALENDAR_OBJECT_DROPB_de041d80 on CALENDAR_OBJECT (
+    DROPBOX_ID
+);
+
+create index TIME_RANGE_CALENDAR_R_beb6e7eb on TIME_RANGE (
+    CALENDAR_RESOURCE_ID
+);
+
+create index TIME_RANGE_CALENDAR_O_acf37bd1 on TIME_RANGE (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index ATTACHMENT_DROPBOX_ID_5073cf23 on ATTACHMENT (
+    DROPBOX_ID
+);
+
+create index ATTACHMENT_CALENDAR_O_81508484 on ATTACHMENT_CALENDAR_OBJECT (
+    CALENDAR_OBJECT_RESOURCE_ID
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    OWNER_HOME_RESOURCE_ID
+);
+
+create index ABO_MEMBERS_ADDRESSBO_4effa879 on ABO_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index ABO_MEMBERS_MEMBER_ID_8d66adcf on ABO_MEMBERS (
+    MEMBER_ID
+);
+
+create index ABO_FOREIGN_MEMBERS_A_1fd2c5e9 on ABO_FOREIGN_MEMBERS (
+    ADDRESSBOOK_ID
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    GROUP_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_6d9d929c on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index CALENDAR_OBJECT_REVIS_550b1c56 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_00fe8288 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    RESOURCE_NAME,
+    DELETED,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index NOTIFICATION_OBJECT_R_036a9cee on NOTIFICATION_OBJECT_REVISIONS (
+    NOTIFICATION_HOME_RESOURCE_ID,
+    REVISION
+);
+
+create index APN_SUBSCRIPTIONS_RES_9610d78e on APN_SUBSCRIPTIONS (
+    RESOURCE_KEY
+);
+
+create index IMIP_TOKENS_TOKEN_e94b918f on IMIP_TOKENS (
+    TOKEN
+);
+
+create index IMIP_INVITATION_WORK__586d064c on IMIP_INVITATION_WORK (
+    JOB_ID
+);
+
+create index IMIP_POLLING_WORK_JOB_d5535891 on IMIP_POLLING_WORK (
+    JOB_ID
+);
+
+create index IMIP_REPLY_WORK_JOB_I_bf4ae73e on IMIP_REPLY_WORK (
+    JOB_ID
+);
+
+create index PUSH_NOTIFICATION_WOR_8bbab117 on PUSH_NOTIFICATION_WORK (
+    JOB_ID
+);
+
+create index PUSH_NOTIFICATION_WOR_3a3ee588 on PUSH_NOTIFICATION_WORK (
+    PUSH_ID
+);
+
+create index GROUP_CACHER_POLLING__6eb3151c on GROUP_CACHER_POLLING_WORK (
+    JOB_ID
+);
+
+create index GROUP_REFRESH_WORK_JO_717ede20 on GROUP_REFRESH_WORK (
+    JOB_ID
+);
+
+create index GROUP_REFRESH_WORK_GR_0325f3a8 on GROUP_REFRESH_WORK (
+    GROUP_UID
+);
+
+create index GROUP_DELEGATE_CHANGE_8bf9e6d8 on GROUP_DELEGATE_CHANGES_WORK (
+    JOB_ID
+);
+
+create index GROUP_DELEGATE_CHANGE_d8f7af69 on GROUP_DELEGATE_CHANGES_WORK (
+    DELEGATOR_UID
+);
+
+create index GROUPS_GROUP_UID_b35cce23 on GROUPS (
+    GROUP_UID
+);
+
+create index GROUP_MEMBERSHIP_MEMB_0ca508e8 on GROUP_MEMBERSHIP (
+    MEMBER_UID
+);
+
+create index GROUP_ATTENDEE_RECONC_da73d3c2 on GROUP_ATTENDEE_RECONCILE_WORK (
+    JOB_ID
+);
+
+create index GROUP_ATTENDEE_RECONC_b894ee7a on GROUP_ATTENDEE_RECONCILE_WORK (
+    RESOURCE_ID
+);
+
+create index GROUP_ATTENDEE_RECONC_5eabc549 on GROUP_ATTENDEE_RECONCILE_WORK (
+    GROUP_ID
+);
+
+create index GROUP_ATTENDEE_RESOUR_855124dc on GROUP_ATTENDEE (
+    RESOURCE_ID
+);
+
+create index GROUP_SHAREE_RECONCIL_9aad0858 on GROUP_SHAREE_RECONCILE_WORK (
+    JOB_ID
+);
+
+create index GROUP_SHAREE_RECONCIL_4dc60f78 on GROUP_SHAREE_RECONCILE_WORK (
+    CALENDAR_ID
+);
+
+create index GROUP_SHAREE_RECONCIL_1d14c921 on GROUP_SHAREE_RECONCILE_WORK (
+    GROUP_ID
+);
+
+create index GROUP_SHAREE_CALENDAR_28a88850 on GROUP_SHAREE (
+    CALENDAR_ID
+);
+
+create index DELEGATE_TO_DELEGATOR_5e149b11 on DELEGATES (
+    DELEGATE,
+    READ_WRITE,
+    DELEGATOR
+);
+
+create index DELEGATE_GROUPS_GROUP_25117446 on DELEGATE_GROUPS (
+    GROUP_ID
+);
+
+create index CALENDAR_OBJECT_SPLIT_af71dcda on CALENDAR_OBJECT_SPLITTER_WORK (
+    RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_SPLIT_33603b72 on CALENDAR_OBJECT_SPLITTER_WORK (
+    JOB_ID
+);
+
+create index FIND_MIN_VALID_REVISI_78d17400 on FIND_MIN_VALID_REVISION_WORK (
+    JOB_ID
+);
+
+create index REVISION_CLEANUP_WORK_eb062686 on REVISION_CLEANUP_WORK (
+    JOB_ID
+);
+
+create index INBOX_CLEANUP_WORK_JO_799132bd on INBOX_CLEANUP_WORK (
+    JOB_ID
+);
+
+create index CLEANUP_ONE_INBOX_WOR_375dac36 on CLEANUP_ONE_INBOX_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_WORK_JOB_ID_65e810ee on SCHEDULE_WORK (
+    JOB_ID
+);
+
+create index SCHEDULE_WORK_ICALEND_089f33dc on SCHEDULE_WORK (
+    ICALENDAR_UID
+);
+
+create index SCHEDULE_REFRESH_WORK_26084c7b on SCHEDULE_REFRESH_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_REFRESH_WORK_989efe54 on SCHEDULE_REFRESH_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_REFRESH_ATTE_83053b91 on SCHEDULE_REFRESH_ATTENDEES (
+    RESOURCE_ID,
+    ATTENDEE
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0256478d on SCHEDULE_AUTO_REPLY_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_AUTO_REPLY_W_0755e754 on SCHEDULE_AUTO_REPLY_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_WO_18ce4edd on SCHEDULE_ORGANIZER_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_WO_14702035 on SCHEDULE_ORGANIZER_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_SE_9ec9f827 on SCHEDULE_ORGANIZER_SEND_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_ORGANIZER_SE_699fefc4 on SCHEDULE_ORGANIZER_SEND_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_WORK_H_745af8cf on SCHEDULE_REPLY_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_WORK_R_11bd3fbb on SCHEDULE_REPLY_WORK (
+    RESOURCE_ID
+);
+
+create index SCHEDULE_REPLY_CANCEL_dab513ef on SCHEDULE_REPLY_CANCEL_WORK (
+    HOME_RESOURCE_ID
+);
+
+create index PRINCIPAL_PURGE_POLLI_6383e68a on PRINCIPAL_PURGE_POLLING_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_CHECK_b0c024c1 on PRINCIPAL_PURGE_CHECK_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_CHECK_198388a5 on PRINCIPAL_PURGE_CHECK_WORK (
+    UID
+);
+
+create index PRINCIPAL_PURGE_WORK__7a8141a3 on PRINCIPAL_PURGE_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_WORK__db35cfdc on PRINCIPAL_PURGE_WORK (
+    UID
+);
+
+create index PRINCIPAL_PURGE_HOME__f35eea7a on PRINCIPAL_PURGE_HOME_WORK (
+    JOB_ID
+);
+
+create index PRINCIPAL_PURGE_HOME__967e4480 on PRINCIPAL_PURGE_HOME_WORK (
+    HOME_RESOURCE_ID
+);
+
+-- Extra schema to add to current-oracle-dialect.sql

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v49.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v49.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v49.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,1155 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------------
+-- Cluster Bookkeeping --
+-------------------------
+
+-- Information about a process connected to this database.
+
+-- Note that this must match the node info schema in twext.enterprise.queue.
+create table NODE_INFO (
+  HOSTNAME  varchar(255) not null,
+  PID       integer      not null,
+  PORT      integer      not null,
+  TIME      timestamp    not null default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (HOSTNAME, PORT)
+);
+
+-- Unique named locks.  This table should always be empty, but rows are
+-- temporarily created in order to prevent undesirable concurrency.
+create table NAMED_LOCK (
+    LOCK_NAME varchar(255) primary key
+);
+
+
+--------------------
+-- Jobs           --
+--------------------
+
+create sequence JOB_SEQ;
+
+create table JOB (
+  JOB_ID      integer primary key default nextval('JOB_SEQ') not null, --implicit index
+  WORK_TYPE   varchar(255) not null,
+  PRIORITY    integer default 0,
+  WEIGHT      integer default 0,
+  NOT_BEFORE  timestamp not null,
+  ASSIGNED    timestamp default null,
+  OVERDUE     timestamp default null,
+  FAILED      integer default 0
+);
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID      integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID        varchar(255) not null unique,                                -- implicit index
+  STATUS           integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION      integer      default 0 not null
+);
+
+-- Enumeration of statuses
+
+create table HOME_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into HOME_STATUS values (0, 'normal' );
+insert into HOME_STATUS values (1, 'external');
+insert into HOME_STATUS values (2, 'purging');
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
+
+----------------------------
+-- Calendar Home Metadata --
+----------------------------
+
+create table CALENDAR_HOME_METADATA (
+  RESOURCE_ID              integer     primary key references CALENDAR_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES         integer     default 0 not null,
+  DEFAULT_EVENTS           integer     default null references CALENDAR on delete set null,
+  DEFAULT_TASKS            integer     default null references CALENDAR on delete set null,
+  DEFAULT_POLLS            integer     default null references CALENDAR on delete set null,
+  ALARM_VEVENT_TIMED       text        default null,
+  ALARM_VEVENT_ALLDAY      text        default null,
+  ALARM_VTODO_TIMED        text        default null,
+  ALARM_VTODO_ALLDAY       text        default null,
+  AVAILABILITY             text        default null,
+  CREATED                  timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                 timestamp   default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+create index CALENDAR_HOME_METADATA_DEFAULT_EVENTS on
+  CALENDAR_HOME_METADATA(DEFAULT_EVENTS);
+create index CALENDAR_HOME_METADATA_DEFAULT_TASKS on
+  CALENDAR_HOME_METADATA(DEFAULT_TASKS);
+create index CALENDAR_HOME_METADATA_DEFAULT_POLLS on
+  CALENDAR_HOME_METADATA(DEFAULT_POLLS);
+
+
+-----------------------
+-- Calendar Metadata --
+-----------------------
+
+create table CALENDAR_METADATA (
+  RESOURCE_ID           integer      primary key references CALENDAR on delete cascade, -- implicit index
+  SUPPORTED_COMPONENTS  varchar(255) default null,
+  CREATED               timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  OWNER_UID   varchar(255) not null unique,                                -- implicit index
+  STATUS      integer      default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION integer      default 0 not null
+);
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  NOTIFICATION_TYPE             varchar(255) not null,
+  NOTIFICATION_DATA             text         not null,
+  MD5                           char(32)     not null,
+  CREATED                       timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID) -- implicit index
+);
+
+create index NOTIFICATION_NOTIFICATION_HOME_RESOURCE_ID on
+  NOTIFICATION(NOTIFICATION_HOME_RESOURCE_ID);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      not null references CALENDAR on delete cascade,
+  EXTERNAL_ID               integer      default null,
+  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,
+  ALARM_VEVENT_ALLDAY       text         default null,
+  ALARM_VTODO_TIMED         text         default null,
+  ALARM_VTODO_ALLDAY        text         default null,
+  TIMEZONE                  text         default null,
+
+  primary key (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID), -- implicit index
+  unique (CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)     -- implicit index
+);
+
+create index CALENDAR_BIND_RESOURCE_ID on
+  CALENDAR_BIND(CALENDAR_RESOURCE_ID);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own'  );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+insert into CALENDAR_BIND_MODE values (3, 'direct');
+insert into CALENDAR_BIND_MODE values (4, 'indirect');
+insert into CALENDAR_BIND_MODE values (5, 'group');
+insert into CALENDAR_BIND_MODE values (6, 'group_read');
+insert into CALENDAR_BIND_MODE values (7, 'group_write');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+insert into CALENDAR_BIND_STATUS values (4, 'deleted');
+
+
+-- Enumeration of transparency
+
+create table CALENDAR_TRANSP (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_TRANSP values (0, 'opaque' );
+insert into CALENDAR_TRANSP values (1, 'transparent');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  ICALENDAR_TYPE       varchar(255) not null,
+  ATTACHMENTS_MODE     integer      default 0 not null, -- enum CALENDAR_OBJ_ATTACHMENTS_MODE
+  DROPBOX_ID           varchar(255),
+  ORGANIZER            varchar(255),
+  RECURRANCE_MIN       date,        -- minimum date that recurrences have been expanded to.
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  ACCESS               integer      default 0 not null,
+  SCHEDULE_OBJECT      boolean      default false,
+  SCHEDULE_TAG         varchar(36)  default null,
+  SCHEDULE_ETAGS       text         default null,
+  PRIVATE_COMMENTS     boolean      default false not null,
+  MD5                  char(32)     not null,
+  CREATED              timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION          integer      default 0 not null,
+
+  unique (CALENDAR_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+
+  -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+  -- calendar objects, this constraint has to be selectively enforced by the
+  -- application layer.
+
+  -- unique (CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_AND_ICALENDAR_UID on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_CALENDAR_RESOURCE_ID_RECURRANCE_MAX_MIN on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX, RECURRANCE_MIN);
+
+create index CALENDAR_OBJECT_ICALENDAR_UID on
+  CALENDAR_OBJECT(ICALENDAR_UID);
+
+create index CALENDAR_OBJECT_DROPBOX_ID on
+  CALENDAR_OBJECT(DROPBOX_ID);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJ_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJ_ATTACHMENTS_MODE values (2, 'write');
+
+
+-- Enumeration of calendar access types
+
+create table CALENDAR_ACCESS_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(32) not null unique
+);
+
+insert into CALENDAR_ACCESS_TYPE values (0, ''             );
+insert into CALENDAR_ACCESS_TYPE values (1, 'public'       );
+insert into CALENDAR_ACCESS_TYPE values (2, 'private'      );
+insert into CALENDAR_ACCESS_TYPE values (3, 'confidential' );
+insert into CALENDAR_ACCESS_TYPE values (4, 'restricted'   );
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'), -- implicit index
+  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR on delete cascade,
+  CALENDAR_OBJECT_RESOURCE_ID integer        not null references CALENDAR_OBJECT on delete cascade,
+  FLOATING                    boolean        not null,
+  START_DATE                  timestamp      not null,
+  END_DATE                    timestamp      not null,
+  FBTYPE                      integer        not null,
+  TRANSPARENT                 boolean        not null
+);
+
+create index TIME_RANGE_CALENDAR_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_RESOURCE_ID);
+create index TIME_RANGE_CALENDAR_OBJECT_RESOURCE_ID on
+  TIME_RANGE(CALENDAR_OBJECT_RESOURCE_ID);
+
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+-------------------
+-- Per-user data --
+-------------------
+
+create table PERUSER (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null,
+  ADJUSTED_START_DATE         timestamp    default null,
+  ADJUSTED_END_DATE           timestamp    default null,
+  
+  primary key (TIME_RANGE_INSTANCE_ID, USER_ID)    -- implicit index
+);
+
+
+----------------
+-- Attachment --
+----------------
+
+create sequence ATTACHMENT_ID_SEQ;
+
+create table ATTACHMENT (
+  ATTACHMENT_ID               integer           primary key default nextval('ATTACHMENT_ID_SEQ'), -- implicit index
+  CALENDAR_HOME_RESOURCE_ID   integer           not null references CALENDAR_HOME,
+  DROPBOX_ID                  varchar(255),
+  CONTENT_TYPE                varchar(255)      not null,
+  SIZE                        integer           not null,
+  MD5                         char(32)          not null,
+  CREATED                     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                    timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  PATH                        varchar(1024)     not null
+);
+
+create index ATTACHMENT_CALENDAR_HOME_RESOURCE_ID on
+  ATTACHMENT(CALENDAR_HOME_RESOURCE_ID);
+
+create index ATTACHMENT_DROPBOX_ID on
+  ATTACHMENT(DROPBOX_ID);
+
+-- Many-to-many relationship between attachments and calendar objects
+create table ATTACHMENT_CALENDAR_OBJECT (
+  ATTACHMENT_ID                  integer      not null references ATTACHMENT on delete cascade,
+  MANAGED_ID                     varchar(255) not null,
+  CALENDAR_OBJECT_RESOURCE_ID    integer      not null references CALENDAR_OBJECT on delete cascade,
+
+  primary key (ATTACHMENT_ID, CALENDAR_OBJECT_RESOURCE_ID), -- implicit index
+  unique (MANAGED_ID, CALENDAR_OBJECT_RESOURCE_ID) --implicit index
+);
+
+create index ATTACHMENT_CALENDAR_OBJECT_CALENDAR_OBJECT_RESOURCE_ID on
+  ATTACHMENT_CALENDAR_OBJECT(CALENDAR_OBJECT_RESOURCE_ID);
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+  RESOURCE_ID integer      not null, -- foreign key: *.RESOURCE_ID
+  NAME        varchar(255) not null,
+  VALUE       text         not null, -- FIXME: xml?
+  VIEWER_UID  varchar(255),
+
+  primary key (RESOURCE_ID, NAME, VIEWER_UID) -- implicit index
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'), -- implicit index
+  ADDRESSBOOK_PROPERTY_STORE_ID integer         default nextval('RESOURCE_ID_SEQ') not null,    -- implicit index
+  OWNER_UID                     varchar(255)    not null unique,                                -- implicit index
+  STATUS                        integer         default 0 not null,                             -- enum HOME_STATUS
+  DATAVERSION                   integer         default 0 not null
+);
+
+
+-------------------------------
+-- AddressBook Home Metadata --
+-------------------------------
+
+create table ADDRESSBOOK_HOME_METADATA (
+  RESOURCE_ID      integer      primary key references ADDRESSBOOK_HOME on delete cascade, -- implicit index
+  QUOTA_USED_BYTES integer      default 0 not null,
+  CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+-----------------------------
+-- Shared AddressBook Bind --
+-----------------------------
+
+-- Joins sharee ADDRESSBOOK_HOME and owner ADDRESSBOOK_HOME
+
+create table SHARED_ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID          integer         not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID                integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  EXTERNAL_ID                           integer         default null,
+  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, OWNER_HOME_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_ADDRESSBOOK_BIND_RESOURCE_ID on
+  SHARED_ADDRESSBOOK_BIND(OWNER_HOME_RESOURCE_ID);
+
+
+------------------------
+-- AddressBook Object --
+------------------------
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID                   integer         primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer         not null references ADDRESSBOOK_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255)    not null,
+  VCARD_TEXT                    text            not null,
+  VCARD_UID                     varchar(255)    not null,
+  KIND                          integer         not null,  -- enum ADDRESSBOOK_OBJECT_KIND
+  MD5                           char(32)        not null,
+  CREATED                       timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp       default timezone('UTC', CURRENT_TIMESTAMP),
+  DATAVERSION                   integer         default 0 not null,
+
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, VCARD_UID)      -- implicit index
+);
+
+
+-----------------------------
+-- AddressBook Object kind --
+-----------------------------
+
+create table ADDRESSBOOK_OBJECT_KIND (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into ADDRESSBOOK_OBJECT_KIND values (0, 'person');
+insert into ADDRESSBOOK_OBJECT_KIND values (1, 'group' );
+insert into ADDRESSBOOK_OBJECT_KIND values (2, 'resource');
+insert into ADDRESSBOOK_OBJECT_KIND values (3, 'location');
+
+
+----------------------------------
+-- Revisions, forward reference --
+----------------------------------
+
+create sequence REVISION_SEQ;
+
+---------------------------------
+-- Address Book Object Members --
+---------------------------------
+
+create table ABO_MEMBERS (
+  GROUP_ID        integer     not null, -- references ADDRESSBOOK_OBJECT on delete cascade,   -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID  integer     not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ID       integer     not null, -- references ADDRESSBOOK_OBJECT,                     -- member AddressBook Object's RESOURCE_ID
+  REVISION        integer     default nextval('REVISION_SEQ') not null,
+  REMOVED         boolean     default false not null,
+  MODIFIED        timestamp   default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (GROUP_ID, MEMBER_ID, REVISION) -- implicit index
+);
+
+create index ABO_MEMBERS_ADDRESSBOOK_ID on
+  ABO_MEMBERS(ADDRESSBOOK_ID);
+create index ABO_MEMBERS_MEMBER_ID on
+  ABO_MEMBERS(MEMBER_ID);
+
+------------------------------------------
+-- Address Book Object Foreign Members  --
+------------------------------------------
+
+create table ABO_FOREIGN_MEMBERS (
+  GROUP_ID           integer      not null references ADDRESSBOOK_OBJECT on delete cascade,  -- AddressBook Object's (kind=='group') RESOURCE_ID
+  ADDRESSBOOK_ID     integer      not null references ADDRESSBOOK_HOME on delete cascade,
+  MEMBER_ADDRESS     varchar(255) not null,                                                  -- member AddressBook Object's 'calendar' address
+
+  primary key (GROUP_ID, MEMBER_ADDRESS) -- implicit index
+);
+
+create index ABO_FOREIGN_MEMBERS_ADDRESSBOOK_ID on
+  ABO_FOREIGN_MEMBERS(ADDRESSBOOK_ID);
+
+-----------------------
+-- Shared Group Bind --
+-----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK_OBJECT (kind == group)
+
+create table SHARED_GROUP_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID      integer      not null references ADDRESSBOOK_HOME,
+  GROUP_RESOURCE_ID                 integer      not null references ADDRESSBOOK_OBJECT on delete cascade,
+  EXTERNAL_ID                       integer      default null,
+  GROUP_ADDRESSBOOK_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, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_NAME)  -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+-- create sequence REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_HOME_RESOURCE_ID integer      not null references CALENDAR_HOME,
+  CALENDAR_RESOURCE_ID      integer      references CALENDAR,
+  CALENDAR_NAME             varchar(255) default null,
+  RESOURCE_NAME             varchar(255),
+  REVISION                  integer      default nextval('REVISION_SEQ') not null,
+  DELETED                   boolean      not null,
+  MODIFIED                  timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID, CALENDAR_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID  integer      not null references ADDRESSBOOK_HOME,
+  OWNER_HOME_RESOURCE_ID        integer      references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME              varchar(255) default null,
+  OBJECT_RESOURCE_ID            integer      default 0,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  
+  unique(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_HOME_RESOURCE_ID, ADDRESSBOOK_NAME, RESOURCE_NAME)    -- implicit index
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME_DELETED_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, RESOURCE_NAME, DELETED, REVISION);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_HOME_RESOURCE_ID, REVISION);
+
+
+-----------------------------------
+-- Notification Object Revisions --
+-----------------------------------
+
+create table NOTIFICATION_OBJECT_REVISIONS (
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME on delete cascade,
+  RESOURCE_NAME                 varchar(255),
+  REVISION                      integer      default nextval('REVISION_SEQ') not null,
+  DELETED                       boolean      not null,
+  MODIFIED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique (NOTIFICATION_HOME_RESOURCE_ID, RESOURCE_NAME) -- implicit index
+);
+
+create index NOTIFICATION_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on NOTIFICATION_OBJECT_REVISIONS(NOTIFICATION_HOME_RESOURCE_ID, REVISION);
+
+
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer      not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  USER_AGENT                    varchar(255) default null,
+  IP_ADDR                       varchar(255) default null,
+
+  primary key (TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
+-----------------
+-- IMIP Tokens --
+-----------------
+
+create table IMIP_TOKENS (
+  TOKEN                         varchar(255) not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALUID                       varchar(255) not null,
+  ACCESSED                      timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  primary key (ORGANIZER, ATTENDEE, ICALUID) -- implicit index
+);
+
+create index IMIP_TOKENS_TOKEN
+  on IMIP_TOKENS(TOKEN);
+
+
+----------------
+-- Work Items --
+----------------
+
+create sequence WORKITEM_SEQ;
+
+
+---------------------------
+-- IMIP Inivitation Work --
+---------------------------
+
+create table IMIP_INVITATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_INVITATION_WORK_JOB_ID on
+  IMIP_INVITATION_WORK(JOB_ID);
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index IMIP_POLLING_WORK_JOB_ID on
+  IMIP_POLLING_WORK(JOB_ID);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index IMIP_REPLY_WORK_JOB_ID on
+  IMIP_REPLY_WORK(JOB_ID);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  PUSH_ID                       varchar(255) not null,
+  PUSH_PRIORITY                 integer      not null -- 1:low 5:medium 10:high
+);
+
+create index PUSH_NOTIFICATION_WORK_JOB_ID on
+  PUSH_NOTIFICATION_WORK(JOB_ID);
+create index PUSH_NOTIFICATION_WORK_PUSH_ID on
+  PUSH_NOTIFICATION_WORK(PUSH_ID);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index GROUP_CACHER_POLLING_WORK_JOB_ID on
+  GROUP_CACHER_POLLING_WORK(JOB_ID);
+
+create table GROUP_REFRESH_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  GROUP_UID                     varchar(255) not null
+);
+
+create index GROUP_REFRESH_WORK_JOB_ID on
+  GROUP_REFRESH_WORK(JOB_ID);
+create index GROUP_REFRESH_WORK_GROUP_UID on
+  GROUP_REFRESH_WORK(GROUP_UID);
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELEGATOR_UID                 varchar(255) not null,
+  READ_DELEGATE_UID             varchar(255) not null,
+  WRITE_DELEGATE_UID            varchar(255) not null
+);
+
+create index GROUP_DELEGATE_CHANGES_WORK_JOB_ID on
+  GROUP_DELEGATE_CHANGES_WORK(JOB_ID);
+create index GROUP_DELEGATE_CHANGES_WORK_DELEGATOR_UID on
+  GROUP_DELEGATE_CHANGES_WORK(DELEGATOR_UID);
+
+create table GROUPS (
+  GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
+  NAME                          varchar(255) not null,
+  GROUP_UID                     varchar(255) not null unique,
+  MEMBERSHIP_HASH               varchar(255) not null,
+  EXTANT                        integer default 1,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+create index GROUPS_GROUP_UID on
+  GROUPS(GROUP_UID);
+
+create table GROUP_MEMBERSHIP (
+  GROUP_ID                     integer not null references GROUPS on delete cascade,
+  MEMBER_UID                   varchar(255) not null,
+
+  primary key (GROUP_ID, MEMBER_UID)
+);
+
+create index GROUP_MEMBERSHIP_MEMBER on
+  GROUP_MEMBERSHIP(MEMBER_UID);
+
+create table GROUP_ATTENDEE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer not null references JOB,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_ATTENDEE_RECONCILE_WORK_JOB_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(JOB_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_RESOURCE_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(RESOURCE_ID);
+create index GROUP_ATTENDEE_RECONCILE_WORK_GROUP_ID on
+  GROUP_ATTENDEE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_ATTENDEE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, RESOURCE_ID)
+);
+
+create index GROUP_ATTENDEE_RESOURCE_ID on
+  GROUP_ATTENDEE(RESOURCE_ID);
+
+
+create table GROUP_SHAREE_RECONCILE_WORK (
+  WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer not null references JOB,
+  CALENDAR_ID                   integer	not null references CALENDAR on delete cascade,
+  GROUP_ID                      integer not null references GROUPS on delete cascade
+);
+
+create index GROUP_SHAREE_RECONCILE_WORK_JOB_ID on
+  GROUP_SHAREE_RECONCILE_WORK(JOB_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_CALENDAR_ID on
+  GROUP_SHAREE_RECONCILE_WORK(CALENDAR_ID);
+create index GROUP_SHAREE_RECONCILE_WORK_GROUP_ID on
+  GROUP_SHAREE_RECONCILE_WORK(GROUP_ID);
+
+
+create table GROUP_SHAREE (
+  GROUP_ID                      integer not null references GROUPS on delete cascade,
+  CALENDAR_ID      				integer not null references CALENDAR on delete cascade,
+  GROUP_BIND_MODE               integer not null, -- enum CALENDAR_BIND_MODE
+  MEMBERSHIP_HASH               varchar(255) not null,
+
+  primary key (GROUP_ID, CALENDAR_ID)
+);
+
+create index GROUP_SHAREE_CALENDAR_ID on
+  GROUP_SHAREE(CALENDAR_ID);
+
+---------------
+-- Delegates --
+---------------
+
+create table DELEGATES (
+  DELEGATOR                     varchar(255) not null,
+  DELEGATE                      varchar(255) not null,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, DELEGATE)
+);
+create index DELEGATE_TO_DELEGATOR on
+  DELEGATES(DELEGATE, READ_WRITE, DELEGATOR);
+
+create table DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) not null,
+  GROUP_ID                      integer      not null references GROUPS on delete cascade,
+  READ_WRITE                    integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+  IS_EXTERNAL                   integer      not null, -- 1 = ReadWrite, 0 = ReadOnly
+
+  primary key (DELEGATOR, READ_WRITE, GROUP_ID)
+);
+create index DELEGATE_GROUPS_GROUP_ID on
+  DELEGATE_GROUPS(GROUP_ID);
+
+create table EXTERNAL_DELEGATE_GROUPS (
+  DELEGATOR                     varchar(255) primary key not null,
+  GROUP_UID_READ                varchar(255),
+  GROUP_UID_WRITE               varchar(255)
+);
+
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+create index CALENDAR_OBJECT_SPLITTER_WORK_RESOURCE_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(RESOURCE_ID);
+create index CALENDAR_OBJECT_SPLITTER_WORK_JOB_ID on
+  CALENDAR_OBJECT_SPLITTER_WORK(JOB_ID);
+
+---------------------------
+-- Revision Cleanup Work --
+---------------------------
+
+create table FIND_MIN_VALID_REVISION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index FIND_MIN_VALID_REVISION_WORK_JOB_ID on
+  FIND_MIN_VALID_REVISION_WORK(JOB_ID);
+
+create table REVISION_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index REVISION_CLEANUP_WORK_JOB_ID on
+  REVISION_CLEANUP_WORK(JOB_ID);
+
+------------------------
+-- Inbox Cleanup Work --
+------------------------
+
+create table INBOX_CLEANUP_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index INBOX_CLEANUP_WORK_JOB_ID on
+   INBOX_CLEANUP_WORK(JOB_ID);
+
+create table CLEANUP_ONE_INBOX_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_ID                       integer      not null unique references CALENDAR_HOME on delete cascade -- implicit index
+);
+
+create index CLEANUP_ONE_INBOX_WORK_JOB_ID on
+  CLEANUP_ONE_INBOX_WORK(JOB_ID);
+
+-------------------
+-- Schedule Work --
+-------------------
+
+create table SCHEDULE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  ICALENDAR_UID                 varchar(255) not null,
+  WORK_TYPE                     varchar(255) not null
+);
+
+create index SCHEDULE_WORK_JOB_ID on
+  SCHEDULE_WORK(JOB_ID);
+create index SCHEDULE_WORK_ICALENDAR_UID on
+  SCHEDULE_WORK(ICALENDAR_UID);
+
+---------------------------
+-- Schedule Refresh Work --
+---------------------------
+
+create table SCHEDULE_REFRESH_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE_COUNT                integer
+);
+
+create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REFRESH_WORK_RESOURCE_ID on
+  SCHEDULE_REFRESH_WORK(RESOURCE_ID);
+
+create table SCHEDULE_REFRESH_ATTENDEES (
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  ATTENDEE                      varchar(255) not null,
+
+  primary key (RESOURCE_ID, ATTENDEE)
+);
+
+create index SCHEDULE_REFRESH_ATTENDEES_RESOURCE_ID_ATTENDEE on
+  SCHEDULE_REFRESH_ATTENDEES(RESOURCE_ID, ATTENDEE);
+
+------------------------------
+-- Schedule Auto Reply Work --
+------------------------------
+
+create table SCHEDULE_AUTO_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  PARTSTAT                      varchar(255) not null
+);
+
+create index SCHEDULE_AUTO_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_AUTO_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_AUTO_REPLY_WORK(RESOURCE_ID);
+
+-----------------------------
+-- Schedule Organizer Work --
+-----------------------------
+
+create table SCHEDULE_ORGANIZER_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ICALENDAR_TEXT_OLD            text,
+  ICALENDAR_TEXT_NEW            text,
+  ATTENDEE_COUNT                integer,
+  SMART_MERGE                   boolean
+);
+
+create index SCHEDULE_ORGANIZER_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_WORK(RESOURCE_ID);
+
+-- Enumeration of schedule actions
+
+create table SCHEDULE_ACTION (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into SCHEDULE_ACTION values (0, 'create');
+insert into SCHEDULE_ACTION values (1, 'modify');
+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
+
+----------------------------------
+-- Schedule Organizer Send Work --
+----------------------------------
+
+create table SCHEDULE_ORGANIZER_SEND_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  SCHEDULE_ACTION               integer      not null, -- Enum SCHEDULE_ACTION
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer,     -- this references a possibly non-existent CALENDAR_OBJECT
+  ATTENDEE                      varchar(255) not null,
+  ITIP_MSG                      text,
+  NO_REFRESH                    boolean
+);
+
+create index SCHEDULE_ORGANIZER_SEND_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_ORGANIZER_SEND_WORK_RESOURCE_ID on
+  SCHEDULE_ORGANIZER_SEND_WORK(RESOURCE_ID);
+
+-------------------------
+-- Schedule Reply Work --
+-------------------------
+
+create table SCHEDULE_REPLY_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
+  CHANGED_RIDS                  text
+);
+
+create index SCHEDULE_REPLY_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(HOME_RESOURCE_ID);
+create index SCHEDULE_REPLY_WORK_RESOURCE_ID on
+  SCHEDULE_REPLY_WORK(RESOURCE_ID);
+
+--------------------------------
+-- Schedule Reply Cancel Work --
+--------------------------------
+
+create table SCHEDULE_REPLY_CANCEL_WORK (
+  WORK_ID                       integer      primary key references SCHEDULE_WORK on delete cascade, -- implicit index
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade,
+  ICALENDAR_TEXT                text         not null
+);
+
+create index SCHEDULE_REPLY_CANCEL_WORK_HOME_RESOURCE_ID on
+  SCHEDULE_REPLY_CANCEL_WORK(HOME_RESOURCE_ID);
+
+----------------------------------
+-- Principal Purge Polling Work --
+----------------------------------
+
+create table PRINCIPAL_PURGE_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null
+);
+
+create index PRINCIPAL_PURGE_POLLING_WORK_JOB_ID on
+  PRINCIPAL_PURGE_POLLING_WORK(JOB_ID);
+
+--------------------------------
+-- Principal Purge Check Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_CHECK_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_CHECK_WORK_JOB_ID on
+  PRINCIPAL_PURGE_CHECK_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_CHECK_WORK_UID on
+  PRINCIPAL_PURGE_CHECK_WORK(UID);
+
+--------------------------
+-- Principal Purge Work --
+--------------------------
+
+create table PRINCIPAL_PURGE_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  UID                           varchar(255) not null
+);
+
+create index PRINCIPAL_PURGE_WORK_JOB_ID on
+  PRINCIPAL_PURGE_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_WORK_UID on
+  PRINCIPAL_PURGE_WORK(UID);
+
+
+--------------------------------
+-- Principal Home Remove Work --
+--------------------------------
+
+create table PRINCIPAL_PURGE_HOME_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  HOME_RESOURCE_ID              integer      not null references CALENDAR_HOME on delete cascade
+);
+
+create index PRINCIPAL_PURGE_HOME_WORK_JOB_ID on
+  PRINCIPAL_PURGE_HOME_WORK(JOB_ID);
+create index PRINCIPAL_PURGE_HOME_HOME_RESOURCE_ID on
+  PRINCIPAL_PURGE_HOME_WORK(HOME_RESOURCE_ID);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '49');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
+insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');
+insert into CALENDARSERVER values ('MIN-VALID-REVISION', '1');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_49_to_50.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_49_to_50.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_49_to_50.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,46 @@
+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 49 to 50 --
+---------------------------------------------------
+
+
+-- Update existing table
+alter table SCHEDULE_REPLY_WORK drop column CHANGED_RIDS;
+alter table SCHEDULE_REPLY_WORK add ("ITIP_MSG" nclob);
+
+begin
+    for i in (select constraint_name
+              from   user_cons_columns
+              where  table_name = 'SCHEDULE_REPLY_WORK' and
+                     column_name = 'RESOURCE_ID' ) loop
+        execute immediate 'alter table SCHEDULE_REPLY_WORK drop constraint '|| i.constraint_name;
+    end loop;
+end;
+alter table SCHEDULE_REPLY_WORK modify ("RESOURCE_ID" null);
+
+-- Copy over items from existing table about to be dropped
+insert into SCHEDULE_REPLY_WORK
+	(WORK_ID, HOME_RESOURCE_ID, RESOURCE_ID, ITIP_MSG)
+	(select WORK_ID, HOME_RESOURCE_ID, null, ICALENDAR_TEXT from SCHEDULE_REPLY_CANCEL_WORK);
+
+-- Delete existing table
+drop table SCHEDULE_REPLY_CANCEL_WORK;
+
+
+-- update the version
+update CALENDARSERVER set VALUE = '50' where NAME = 'VERSION';

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_49_to_50.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_49_to_50.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_49_to_50.sql	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,37 @@
+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 49 to 50 --
+---------------------------------------------------
+
+-- Update existing table
+alter table SCHEDULE_REPLY_WORK drop column CHANGED_RIDS;
+alter table SCHEDULE_REPLY_WORK add column ITIP_MSG text;
+alter table SCHEDULE_REPLY_WORK drop constraint SCHEDULE_REPLY_WORK_RESOURCE_ID_FKEY;
+alter table SCHEDULE_REPLY_WORK alter column RESOURCE_ID drop not null;
+
+-- Copy over items from existing table about to be dropped
+insert into SCHEDULE_REPLY_WORK
+	(WORK_ID, HOME_RESOURCE_ID, RESOURCE_ID, ITIP_MSG)
+	(select WORK_ID, HOME_RESOURCE_ID, null, ICALENDAR_TEXT from SCHEDULE_REPLY_CANCEL_WORK);
+
+-- Delete existing table
+drop table SCHEDULE_REPLY_CANCEL_WORK;
+
+
+-- update the version
+update CALENDARSERVER set VALUE = '50' where NAME = 'VERSION';

Added: CalendarServer/trunk/txdav/common/datastore/test/test_sql_dump.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_sql_dump.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_sql_dump.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,220 @@
+##
+# Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from txdav.common.datastore.sql_dump import dumpSchema
+from twext.enterprise.dal.parseschema import schemaFromString
+
+"""
+Tests for L{txdav.common.datastore.upgrade.sql.upgrade}.
+"""
+
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial.unittest import TestCase
+from txdav.common.datastore.test.util import theStoreBuilder, \
+    StubNotifierFactory
+
+class SQLDump(TestCase):
+    """
+    Tests for L{sql_dump}.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        TestCase.setUp(self)
+
+        self.store = yield theStoreBuilder.buildStore(
+            self, {"push": StubNotifierFactory()}, enableJobProcessing=False
+        )
+
+
+    @inlineCallbacks
+    def cleanUp(self):
+        startTxn = self.store.newTransaction("test_sql_dump")
+        yield startTxn.execSQL("set search_path to public;")
+        yield startTxn.execSQL("drop schema test_sql_dump cascade;")
+        yield startTxn.commit()
+
+
+    @inlineCallbacks
+    def _loadSchema(self, schema):
+        """
+        Use the postgres schema mechanism to do tests under a separate "namespace"
+        in postgres that we can quickly wipe clean afterwards.
+        """
+        startTxn = self.store.newTransaction("test_sql_dump")
+        yield startTxn.execSQL("create schema test_sql_dump;")
+        yield startTxn.execSQL("set search_path to test_sql_dump;")
+        yield startTxn.execSQL(schema)
+        yield startTxn.commit()
+
+        self.addCleanup(self.cleanUp)
+
+
+    @inlineCallbacks
+    def _schemaCheck(self, schema, schema_bad):
+
+        # Load old schema and populate with data
+        yield self._loadSchema(schema)
+
+        txn = self.store.newTransaction("loadData")
+        dumped = yield dumpSchema(txn, "test", schemaname="test_sql_dump")
+        yield txn.commit()
+
+        parsed = schemaFromString(schema)
+        self.assertEqual(parsed.compare(dumped), [])
+
+        parsed_bad = schemaFromString(schema_bad)
+        self.assertNotEqual(parsed_bad.compare(dumped), [])
+
+
+    @inlineCallbacks
+    def test_pkey_column(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer primary key,
+    ID2 integer not null
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer primary key,
+    ID2 integer
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)
+
+
+    @inlineCallbacks
+    def test_pkey_table(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer not null,
+    ID2 integer not null,
+
+    primary key (ID1)
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer,
+
+    primary key (ID1)
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)
+
+
+    @inlineCallbacks
+    def test_multiple_pkey_table(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer not null,
+    ID2 integer not null,
+    ID3 integer not null,
+
+    primary key (ID1, ID2)
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer,
+    ID3 integer,
+
+    primary key (ID1, ID2)
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)
+
+
+    @inlineCallbacks
+    def test_unique_column(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer unique,
+    ID2 integer not null
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer unique,
+    ID2 integer
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)
+
+
+    @inlineCallbacks
+    def test_unique_table(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer not null,
+
+    unique (ID1)
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer,
+
+    unique (ID1)
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)
+
+
+    @inlineCallbacks
+    def test_multiple_unique_table(self):
+
+        schema = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer,
+    ID3 integer not null,
+
+    unique (ID1, ID2)
+);
+"""
+
+        schema_bad = """
+CREATE TABLE FOO (
+    ID1 integer,
+    ID2 integer,
+    ID3 integer,
+
+    unique (ID1, ID2)
+);
+"""
+
+        yield self._schemaCheck(schema, schema_bad)

Modified: CalendarServer/trunk/txdav/common/datastore/test/test_sql_schema_files.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/test_sql_schema_files.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/common/datastore/test/test_sql_schema_files.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -14,6 +14,7 @@
 # limitations under the License.
 # #
 
+from twext.enterprise.dal.model import Constraint
 from twext.enterprise.dal.parseschema import schemaFromPath
 from twisted.python.modules import getModule
 from twisted.trial.unittest import TestCase
@@ -102,7 +103,18 @@
 
         self.assertEqual(current_version, current_oracle_version)
 
-        mismatched = schemaFromPath(currentSchema).compare(schemaFromPath(currentOracleSchema))
+        schema_current = schemaFromPath(currentSchema)
+        schema_oracle = schemaFromPath(currentOracleSchema)
+
+        # Remove any not null constraints in the postgres schema for text columns as in
+        # Oracle nclob or nvarchar never uses not null
+        for table in schema_current.tables:
+            for constraint in tuple(table.constraints):
+                if constraint.type == Constraint.NOT_NULL and len(constraint.affectsColumns) == 1:
+                    if constraint.affectsColumns[0].type.name in ("text", "char", "varchar"):
+                        table.constraints.remove(constraint)
+
+        mismatched = schema_current.compare(schema_oracle)
         self.assertEqual(len(mismatched), 0, msg=", ".join(mismatched))
 
 

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -212,7 +212,9 @@
         # These are special case exceptions
         for i in (
             "Table: CALENDAR_HOME, column name DATAVERSION default mismatch",
+            "Table: CALENDAR_HOME, mismatched constraints: set([<Constraint: (NOT NULL ['DATAVERSION'] None)>])",
             "Table: ADDRESSBOOK_HOME, column name DATAVERSION default mismatch",
+            "Table: ADDRESSBOOK_HOME, mismatched constraints: set([<Constraint: (NOT NULL ['DATAVERSION'] None)>])",
             "Table: PUSH_NOTIFICATION_WORK, column name PUSH_PRIORITY default mismatch",
         ):
             try:

Added: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade_with_data.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -0,0 +1,274 @@
+##
+# Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twext.enterprise.dal.syntax import Insert, Select
+from txdav.common.datastore.sql_tables import _populateSchema
+from datetime import datetime
+
+"""
+Tests for L{txdav.common.datastore.upgrade.sql.upgrade}.
+"""
+
+from twext.enterprise.ienterprise import POSTGRES_DIALECT
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.trial.unittest import TestCase
+from txdav.common.datastore.test.util import theStoreBuilder, \
+    StubNotifierFactory
+from txdav.common.datastore.upgrade.sql.upgrade import UpgradeDatabaseSchemaStep
+import re
+
+class SchemaUpgradeWithDataTests(TestCase):
+    """
+    Tests for upgrading schema when data is present in the database to make sure data conversion
+    is done correctly.
+    """
+
+    @staticmethod
+    def _getRawSchemaVersion(fp, versionKey):
+        schema = fp.getContent()
+        found = re.search("insert into CALENDARSERVER (\(NAME, VALUE\) )?values \('%s', '(\d+)'\);" % (versionKey,), schema)
+        return int(found.group(2)) if found else None
+
+
+    def _getSchemaVersion(self, fp, versionKey):
+        found = self._getRawSchemaVersion(fp, versionKey)
+        if found is None:
+            if versionKey == "VERSION":
+                self.fail("Could not determine schema version for: %s" % (fp,))
+            else:
+                return 1
+        return found
+
+
+    @inlineCallbacks
+    def setUp(self):
+        TestCase.setUp(self)
+
+        test_upgrader = UpgradeDatabaseSchemaStep(None)
+        self.upgradePath = test_upgrader.schemaLocation.child("old").child(POSTGRES_DIALECT)
+        self.currentVersion = self._getSchemaVersion(test_upgrader.schemaLocation.child("current.sql"), "VERSION")
+
+        self.store = yield theStoreBuilder.buildStore(
+            self, {"push": StubNotifierFactory()}, enableJobProcessing=False
+        )
+
+
+    @inlineCallbacks
+    def cleanUp(self):
+        startTxn = self.store.newTransaction("test_dbUpgrades")
+        yield startTxn.execSQL("set search_path to public;")
+        yield startTxn.execSQL("drop schema test_dbUpgrades cascade;")
+        yield startTxn.commit()
+
+
+    @inlineCallbacks
+    def _loadOldSchema(self, path):
+        """
+        Use the postgres schema mechanism to do tests under a separate "namespace"
+        in postgres that we can quickly wipe clean afterwards.
+        """
+        startTxn = self.store.newTransaction("test_dbUpgrades")
+        yield startTxn.execSQL("create schema test_dbUpgrades;")
+        yield startTxn.execSQL("set search_path to test_dbUpgrades;")
+        yield startTxn.execSQL(path.getContent())
+        yield startTxn.commit()
+
+        self.addCleanup(self.cleanUp)
+
+        returnValue(_populateSchema(path))
+
+
+    @inlineCallbacks
+    def _loadVersion(self):
+        startTxn = self.store.newTransaction("test_dbUpgrades")
+        new_version = yield startTxn.execSQL("select value from calendarserver where name = 'VERSION';")
+        yield startTxn.commit()
+        returnValue(int(new_version[0][0]))
+
+
+    @inlineCallbacks
+    def test_upgrade_SCHEDULE_REPLY_CANCEL(self):
+
+        cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:1234-5678
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+"""
+
+        # Load old schema and populate with data
+        schema = yield self._loadOldSchema(self.upgradePath.child("v49.sql"))
+
+        txn = self.store.newTransaction("loadData")
+        yield Insert(
+            {
+                schema.CALENDAR_HOME.RESOURCE_ID: 1,
+                schema.CALENDAR_HOME.OWNER_UID: "abcdefg",
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.JOB.JOB_ID: 1,
+                schema.JOB.WORK_TYPE: "SCHEDULE_REPLY_CANCEL_WORK",
+                schema.JOB.NOT_BEFORE: datetime.utcnow(),
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.SCHEDULE_WORK.WORK_ID: 1,
+                schema.SCHEDULE_WORK.JOB_ID: 1,
+                schema.SCHEDULE_WORK.ICALENDAR_UID: "1234-5678",
+                schema.SCHEDULE_WORK.WORK_TYPE: "SCHEDULE_REPLY_CANCEL_WORK",
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.SCHEDULE_REPLY_CANCEL_WORK.WORK_ID: 1,
+                schema.SCHEDULE_REPLY_CANCEL_WORK.HOME_RESOURCE_ID: 1,
+                schema.SCHEDULE_REPLY_CANCEL_WORK.ICALENDAR_TEXT: cal1,
+            }
+        ).on(txn)
+        yield txn.commit()
+
+        # Try to upgrade and verify new version afterwards
+        upgrader = UpgradeDatabaseSchemaStep(self.store)
+        yield upgrader.databaseUpgrade()
+
+        new_version = yield self._loadVersion()
+        self.assertEqual(new_version, self.currentVersion)
+
+        txn = self.store.newTransaction("loadData")
+        jobs = yield Select(
+            From=schema.JOB,
+        ).on(txn)
+        schedules = yield Select(
+            From=schema.SCHEDULE_WORK,
+        ).on(txn)
+        replies = yield Select(
+            From=schema.SCHEDULE_REPLY_WORK,
+        ).on(txn)
+        yield txn.commit()
+
+        self.assertEqual(len(jobs), 1)
+        self.assertEqual(len(schedules), 1)
+        self.assertEqual(len(replies), 1)
+
+        self.assertEqual(replies[0], [1, 1, None, cal1, ])
+
+
+    @inlineCallbacks
+    def test_upgrade_SCHEDULE_REPLY(self):
+
+        cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:1234-5678
+DTSTART:20071114T010000Z
+DURATION:PT1H
+DTSTAMP:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+"""
+
+        # Load old schema and populate with data
+        schema = yield self._loadOldSchema(self.upgradePath.child("v49.sql"))
+
+        txn = self.store.newTransaction("loadData")
+        yield Insert(
+            {
+                schema.CALENDAR_HOME.RESOURCE_ID: 1,
+                schema.CALENDAR_HOME.OWNER_UID: "abcdefg",
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.CALENDAR.RESOURCE_ID: 2,
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.CALENDAR_OBJECT.RESOURCE_ID: 3,
+                schema.CALENDAR_OBJECT.CALENDAR_RESOURCE_ID: 2,
+                schema.CALENDAR_OBJECT.RESOURCE_NAME: "1.ics",
+                schema.CALENDAR_OBJECT.ICALENDAR_TEXT: cal1,
+                schema.CALENDAR_OBJECT.ICALENDAR_UID: "1234-5678",
+                schema.CALENDAR_OBJECT.ICALENDAR_TYPE: "VEVENT",
+                schema.CALENDAR_OBJECT.MD5: "md5-1234567890",
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.JOB.JOB_ID: 1,
+                schema.JOB.WORK_TYPE: "SCHEDULE_REPLY_WORK",
+                schema.JOB.NOT_BEFORE: datetime.utcnow(),
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.SCHEDULE_WORK.WORK_ID: 1,
+                schema.SCHEDULE_WORK.JOB_ID: 1,
+                schema.SCHEDULE_WORK.ICALENDAR_UID: "1234-5678",
+                schema.SCHEDULE_WORK.WORK_TYPE: "SCHEDULE_REPLY_WORK",
+            }
+        ).on(txn)
+        yield Insert(
+            {
+                schema.SCHEDULE_REPLY_WORK.WORK_ID: 1,
+                schema.SCHEDULE_REPLY_WORK.HOME_RESOURCE_ID: 1,
+                schema.SCHEDULE_REPLY_WORK.RESOURCE_ID: 3,
+                schema.SCHEDULE_REPLY_WORK.CHANGED_RIDS: None,
+            }
+        ).on(txn)
+        yield txn.commit()
+
+        # Try to upgrade and verify new version afterwards
+        upgrader = UpgradeDatabaseSchemaStep(self.store)
+        yield upgrader.databaseUpgrade()
+
+        new_version = yield self._loadVersion()
+        self.assertEqual(new_version, self.currentVersion)
+
+        txn = self.store.newTransaction("loadData")
+        jobs = yield Select(
+            From=schema.JOB,
+        ).on(txn)
+        schedules = yield Select(
+            From=schema.SCHEDULE_WORK,
+        ).on(txn)
+        replies = yield Select(
+            From=schema.SCHEDULE_REPLY_WORK,
+        ).on(txn)
+        yield txn.commit()
+
+        self.assertEqual(len(jobs), 1)
+        self.assertEqual(len(schedules), 1)
+        self.assertEqual(len(replies), 1)
+
+        self.assertEqual(replies[0], [1, 1, 3, None, ])

Modified: CalendarServer/trunk/txdav/who/cache.py
===================================================================
--- CalendarServer/trunk/txdav/who/cache.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/who/cache.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -377,7 +377,7 @@
                 # but there might be others in the directory service with that
                 # same email address.
                 self.cacheRecord(
-                    records[0],
+                    list(records)[0],
                     (
                         IndexType.uid, IndexType.guid,
                         IndexType.shortName, IndexType.emailAddress

Modified: CalendarServer/trunk/txdav/who/test/test_group_attendees.py
===================================================================
--- CalendarServer/trunk/txdav/who/test/test_group_attendees.py	2015-01-06 16:42:48 UTC (rev 14246)
+++ CalendarServer/trunk/txdav/who/test/test_group_attendees.py	2015-01-06 16:44:21 UTC (rev 14247)
@@ -26,7 +26,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial import unittest
 from twistedcaldav.config import config
-from twistedcaldav.ical import Component, normalize_iCalStr, ignoredComponents
+from twistedcaldav.ical import Component, normalize_iCalStr
 from txdav.caldav.datastore.test.util import populateCalendarsFrom, CommonCommonTests
 from txdav.common.datastore.sql_tables import schema
 from txdav.who.directory import CalendarDirectoryRecordMixin
@@ -87,10 +87,7 @@
 
         def orderMemberValues(event):
 
-            for component in event.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
-
+            for component in event.subcomponents(ignore=True):
                 # remove all values and add them again
                 # this is sort of a hack, better pycalendar has ordering
                 for attendeeProp in tuple(component.properties("ATTENDEE")):
@@ -1227,9 +1224,7 @@
         cobjs = yield cal.objectResources()
         for cobj in cobjs:
             vcalendar = yield cobj.component()
-            for component in vcalendar.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in vcalendar.subcomponents(ignore=True):
                 props = {
                     "relatedTo": component.getProperty("RELATED-TO"),
                     "start": component.getProperty("DTSTART"),
@@ -1406,9 +1401,7 @@
         cobjs = yield cal.objectResources()
         for cobj in cobjs:
             vcalendar = yield cobj.component()
-            for component in vcalendar.subcomponents():
-                if component.name() in ignoredComponents:
-                    continue
+            for component in vcalendar.subcomponents(ignore=True):
                 props = {
                     "relatedTo": component.getProperty("RELATED-TO"),
                     "start": component.getProperty("DTSTART"),
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150106/8e5cd1ef/attachment-0001.html>


More information about the calendarserver-changes mailing list