[CalendarServer-changes] [11460] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Jul 3 19:29:51 PDT 2013


Revision: 11460
          http://trac.calendarserver.org//changeset/11460
Author:   cdaboo at apple.com
Date:     2013-07-03 19:29:51 -0700 (Wed, 03 Jul 2013)
Log Message:
-----------
Event splitter implementation that includes a work queue to split in the background. This also required
some changes to managed attachments to make splitting easier (plus some clean up of managed attachments
api). Still need to work on splitting for users not on the local server.

Modified Paths:
--------------
    CalendarServer/trunk/support/build.sh
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
    CalendarServer/trunk/txdav/caldav/datastore/file.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/caldav/icalendarstore.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql

Added Paths:
-----------
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v22.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v22.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_22_to_23.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_22_to_23.sql

Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/support/build.sh	2013-07-04 02:29:51 UTC (rev 11460)
@@ -795,7 +795,7 @@
     "${pypi}/p/${n}/${p}.tar.gz";
 
   # XXX actually PyCalendar should be imported in-place.
-  py_dependency -fe -i "src" -r 11390 \
+  py_dependency -fe -i "src" -r 11458 \
     "PyCalendar" "pycalendar" "pycalendar" \
     "${svn_uri_base}/PyCalendar/trunk";
 

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -1083,9 +1083,9 @@
         @type rid: L{PyCalendarDateTime}
         """
 
+        if not self.isRecurring():
+            return
         master = self.masterComponent()
-        if not master.isRecurring():
-            return
         if master:
             # Adjust any RRULE first
             rrules = master._pycalendar.getRecurrenceSet()
@@ -1128,7 +1128,12 @@
 
         self._markAsDirty()
 
+        # We changed the instance set so remove any instance cache
+        # TODO: we could be smarter here and truncate the instance list
+        if hasattr(self, "cachedInstances"):
+            delattr(self, "cachedInstances")
 
+
     def onlyFutureInstances(self, rid):
         """
         Remove all recurrence instances from the specified recurrence-id into the past. Adjust the bounds of
@@ -1140,9 +1145,9 @@
         @type rid: L{PyCalendarDateTime}
         """
 
+        if not self.isRecurring():
+            return
         master = self.masterComponent()
-        if not master.isRecurring():
-            return
         if master:
             # Check if cut-off matches an RDATE
             adjusted_rid = rid
@@ -1221,7 +1226,12 @@
 
         self._markAsDirty()
 
+        # We changed the instance set so remove any instance cache
+        # TODO: we could be smarter here and truncate the instance list
+        if hasattr(self, "cachedInstances"):
+            delattr(self, "cachedInstances")
 
+
     def expand(self, start, end, timezone=None):
         """
         Expand the components into a set of new components, one for each
@@ -1617,13 +1627,13 @@
         return self._resource_uid
 
 
-    def newUID(self):
+    def newUID(self, newUID=None):
         """
         Generate a new UID for all components in this VCALENDAR
         """
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
-        newUID = str(uuid.uuid4())
+        newUID = str(uuid.uuid4()) if newUID is None else newUID
         self._pycalendar.changeUID(self.resourceUID(), newUID)
         self._resource_uid = newUID
         self._markAsDirty()

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -730,6 +730,13 @@
                                                                    # "decline-if-busy" - decline if busy, do nothing if free
                                                                    # "automatic"       - accept if free, decline if busy
                 "FutureFreeBusyDays"              : 3 * 365,       # How far into the future to check for booking conflicts
+            },
+
+            "Splitting": {
+                "Enabled"                         : True,
+                "Size"                            : 100 * 1024,     # Consider splitting when greater than 100KB
+                "PastDays"                        : 14,             # Number of days in the past where the split will occur
+                "Delay"                           : 60,             # How many seconds to delay the split work item
             }
         }
     },

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -2421,28 +2421,7 @@
         returnValue(NO_CONTENT)
 
 
-    @inlineCallbacks
-    def preProcessManagedAttachments(self, calendar):
-        # If store object exists pass through, otherwise use underlying store ManagedAttachments object to determine changes
-        if self._newStoreObject:
-            copied, removed = (yield self._newStoreObject.updatingResourceCheckAttachments(calendar))
-        else:
-            copied = (yield self._newStoreParent.creatingResourceCheckAttachments(calendar))
-            removed = None
 
-        returnValue((copied, removed,))
-
-
-    @inlineCallbacks
-    def postProcessManagedAttachments(self, copied, removed):
-        # Pass through directly to store object
-        if copied:
-            yield self._newStoreObject.copyResourceAttachments(copied)
-        if removed:
-            yield self._newStoreObject.removeResourceAttachments(removed)
-
-
-
 class _MetadataProperty(object):
     """
     A python property which can be set either on a _newStoreObject or on some

Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -50,7 +50,7 @@
 
         # Need to not do implicit behavior during these tests
         def _fakeDoImplicitScheduling(self, component, inserting, internal_state):
-            return False, None, False
+            return False, None, False, None
 
         self.patch(CalendarObject , "doImplicitScheduling",
                    _fakeDoImplicitScheduling)

Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/file.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/file.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -379,17 +379,7 @@
         pass
 
 
-    def creatingResourceCheckAttachments(self, component):
-        """
-        When component data is created or changed we need to look for changes related to managed attachments.
 
-        @param component: the new calendar data
-        @type component: L{Component}
-        """
-        return succeed(None)
-
-
-
 class CalendarObject(CommonObjectResource, CalendarObjectBase):
     """
     @ivar _path: The path of the .ics file on disk

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/icalsplitter.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -13,7 +13,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+
 from pycalendar.datetime import PyCalendarDateTime
+
 from twistedcaldav.ical import Property
 
 
@@ -61,35 +63,35 @@
         now.offsetDay(1)
         instances = ical.cacheExpandedTimeRanges(now)
         instances = sorted(instances.instances.values(), key=lambda x: x.start)
-        if instances[0].start > self.past or instances[-1].start < self.now or len(instances) <= 1:
+        if instances[0].start >= self.past or instances[-1].start < self.now or len(instances) <= 1:
             return False
 
         # Make sure there are some overridden components in the past - as splitting only makes sense when
         # overrides are present
+        past_count = 0
         for instance in instances:
-            if instance.start > self.past:
-                return False
+            if instance.start >= self.past:
+                break
             elif instance.component.hasProperty("RECURRENCE-ID"):
-                break
-        else:
+                past_count += 1
+
+        # Only split when there is more than one past override to split off
+        if past_count < 2:
             return False
 
         # Now see if overall size exceeds our threshold
         return len(str(ical)) > self.threshold
 
 
-    def split(self, ical):
+    def whereSplit(self, ical):
         """
-        Split the specified iCalendar object. This assumes that L{willSplit} has already
-        been called and returned C{True}. Splitting is done by carving out old instances
-        into a new L{Component} and adjusting the specified component for the on-going
-        set. A RELATED-TO property is added to link old to new.
+        Determine where a split is going to happen - i.e., the RECURRENCE-ID.
 
-        @param ical: the iCalendar object to split
+        @param ical: the iCalendar object to test
         @type ical: L{Component}
 
-        @return: iCalendar object for the old "carved out" instances
-        @rtype: L{Component}
+        @return: recurrence-id of the split
+        @rtype: L{PyCalendarDateTime}
         """
 
         # Find the instance RECURRENCE-ID where a split is going to happen
@@ -102,10 +104,41 @@
             rid = instance.rid
             if instance.start >= self.past:
                 break
+        else:
+            # We can get here when splitting and event for overrides only in the past,
+            # which happens when splitting an Attendee's copy of an Organizer event
+            # where the Organizer event has L{willSplit} == C{True}
+            rid = self.past
 
-        # Create the old one with a new UID value
+        return rid
+
+
+    def split(self, ical, rid=None, newUID=None):
+        """
+        Split the specified iCalendar object. This assumes that L{willSplit} has already
+        been called and returned C{True}. Splitting is done by carving out old instances
+        into a new L{Component} and adjusting the specified component for the on-going
+        set. A RELATED-TO property is added to link old to new.
+
+        @param ical: the iCalendar object to split
+        @type ical: L{Component}
+
+        @param rid: recurrence-id where the split should occur, or C{None} to determine it here
+        @type rid: L{PyCalendarDateTime} or C{None}
+
+        @param newUID: UID to use for the split off component, or C{None} to generate one here
+        @type newUID: C{str} or C{None}
+
+        @return: iCalendar object for the old "carved out" instances
+        @rtype: L{Component}
+        """
+
+        # Find the instance RECURRENCE-ID where a split is going to happen
+        rid = self.whereSplit(ical) if rid is None else rid
+
+        # Create the old one with a new UID value (or the one passed in)
         icalOld = ical.duplicate()
-        oldUID = icalOld.newUID()
+        oldUID = icalOld.newUID(newUID=newUID)
         icalOld.onlyPastInstances(rid)
 
         # Adjust the current one

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_icalsplitter.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -290,7 +290,7 @@
                 False,
             ),
             (
-                "#4.2 Large, old, recurring component with past override",
+                "#4.2 Large, old, recurring component with one past override",
                 """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -337,6 +337,65 @@
 END:VEVENT
 END:VCALENDAR
 """,
+                False,
+            ),
+            (
+                "#4.2 Large, old, recurring component with two past overrides",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+ATTENDEE:mailto:user4 at example.com
+ATTENDEE:mailto:user5 at example.com
+ATTENDEE:mailto:user6 at example.com
+ATTENDEE:mailto:user7 at example.com
+ATTENDEE:mailto:user8 at example.com
+ATTENDEE:mailto:user9 at example.com
+ATTENDEE:mailto:user10 at example.com
+ATTENDEE:mailto:user11 at example.com
+ATTENDEE:mailto:user12 at example.com
+ATTENDEE:mailto:user13 at example.com
+ATTENDEE:mailto:user14 at example.com
+ATTENDEE:mailto:user15 at example.com
+ATTENDEE:mailto:user16 at example.com
+ATTENDEE:mailto:user17 at example.com
+ATTENDEE:mailto:user18 at example.com
+ATTENDEE:mailto:user19 at example.com
+ATTENDEE:mailto:user20 at example.com
+ATTENDEE:mailto:user21 at example.com
+ATTENDEE:mailto:user22 at example.com
+ATTENDEE:mailto:user23 at example.com
+ATTENDEE:mailto:user24 at example.com
+ATTENDEE:mailto:user25 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
                 True,
             ),
             (
@@ -367,7 +426,7 @@
                 False,
             ),
             (
-                "#4.2 Large, old, limited recurring component with past override",
+                "#5.2 Large, old, limited recurring component with past override",
                 """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -444,7 +503,7 @@
                 False,
             ),
             (
-                "#6.2 Large, old, limited future recurring component with past override",
+                "#6.2 Large, old, limited future recurring component with two past overrides",
                 """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
@@ -489,6 +548,15 @@
 ATTENDEE:mailto:user1 at example.com
 ATTENDEE:mailto:user2 at example.com
 END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
 END:VCALENDAR
 """,
                 True,
@@ -552,6 +620,15 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
 END:VCALENDAR
 """,
                 """BEGIN:VCALENDAR
@@ -638,6 +715,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -693,6 +780,15 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
 END:VCALENDAR
 """,
                 """BEGIN:VCALENDAR
@@ -785,6 +881,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -835,6 +941,15 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
 END:VCALENDAR
 """,
                 """BEGIN:VCALENDAR
@@ -922,6 +1037,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -963,7 +1088,7 @@
 RDATE:%(now_back15)s
 RDATE:%(now_back14)s
 RDATE:%(now_back13)s
-RRULE:FREQ=DAILY;INTERVAL=20
+RRULE:FREQ=DAILY;INTERVAL=10
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890
@@ -976,6 +1101,15 @@
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890
+RECURRENCE-ID:%(now_back20)s
+DTSTART:%(now_back20)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
 RECURRENCE-ID:%(now_back10)s
 DTSTART:%(now_back10)s
 DURATION:PT1H
@@ -1021,7 +1155,7 @@
 RDATE:%(now_back14)s
 RDATE:%(now_back13)s
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-RRULE:FREQ=DAILY;INTERVAL=20
+RRULE:FREQ=DAILY;INTERVAL=10
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890
@@ -1070,7 +1204,7 @@
 ORGANIZER:mailto:user1 at example.com
 RDATE:%(now_back15)s
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
-RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s;INTERVAL=20
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s;INTERVAL=10
 END:VEVENT
 BEGIN:VEVENT
 UID:%(relID)s
@@ -1082,6 +1216,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back20)s
+DTSTART:%(now_back20)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -1138,6 +1282,15 @@
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890
+RECURRENCE-ID:%(now_back15)s
+DTSTART:%(now_back15)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
 RECURRENCE-ID:%(now_back14)s
 DTSTART:%(now_back14)s
 DURATION:PT1H
@@ -1263,6 +1416,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back15)s
+DTSTART:%(now_back15)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -1318,6 +1481,15 @@
 END:VEVENT
 BEGIN:VEVENT
 UID:12345-67890
+RECURRENCE-ID:%(now_back15)s
+DTSTART:%(now_back15)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
 RECURRENCE-ID:%(now_fwd1)s
 DTSTART:%(now_fwd1)s
 DURATION:PT1H
@@ -1423,6 +1595,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back15)s
+DTSTART:%(now_back15)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 END:VCALENDAR
 """,
             ),
@@ -1472,6 +1654,15 @@
 ATTENDEE:mailto:user2 at example.com
 ORGANIZER:mailto:user1 at example.com
 END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:12345-67890
 X-CALENDARSERVER-PERUSER-UID:user01
@@ -1609,6 +1800,16 @@
 ORGANIZER:mailto:user1 at example.com
 RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
 END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
 BEGIN:X-CALENDARSERVER-PERUSER
 UID:%(relID)s
 X-CALENDARSERVER-PERUSER-UID:user01
@@ -1633,14 +1834,187 @@
 END:VCALENDAR
 """,
             ),
+            (
+                "2.1 - Overrides only - one future, one past",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "2.2 - Overrides only - all past",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+            (
+                "2.3 - Overrides only - all future",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd9)s
+DTSTART:%(now_fwd9)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd9)s
+DTSTART:%(now_fwd9)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+            ),
         )
 
         for title, calendar, split_future, split_past in data:
             ical = Component.fromString(calendar % self.subs)
             splitter = iCalSplitter(1024, 14)
-            self.assertTrue(splitter.willSplit(ical))
+            if title[0] == "1":
+                self.assertTrue(splitter.willSplit(ical), "Failed will split: %s" % (title,))
             icalOld = splitter.split(ical)
             relsubs = dict(self.subs)
             relsubs["relID"] = icalOld.resourceUID()
             self.assertEqual(str(ical).replace("\r\n ", ""), split_future.replace("\n", "\r\n") % relsubs, "Failed future: %s" % (title,))
             self.assertEqual(str(icalOld).replace("\r\n ", ""), split_past.replace("\n", "\r\n") % relsubs, "Failed past: %s" % (title,))
+
+            # Make sure new items won't split again
+            self.assertFalse(splitter.willSplit(ical), "Failed future will split: %s" % (title,))
+            self.assertFalse(splitter.willSplit(icalOld), "Failed past will split: %s" % (title,))

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -26,6 +26,7 @@
     "CalendarObject",
 ]
 
+from twext.enterprise.dal.record import fromTable
 from twext.enterprise.dal.syntax import Delete
 from twext.enterprise.dal.syntax import Insert
 from twext.enterprise.dal.syntax import Len
@@ -34,6 +35,7 @@
 from twext.enterprise.dal.syntax import Update
 from twext.enterprise.dal.syntax import utcNowSQL
 from twext.enterprise.locking import NamedLock
+from twext.enterprise.queue import WorkItem
 from twext.enterprise.util import parseSQLTimestamp
 from twext.python.clsprop import classproperty
 from twext.python.filepath import CachingFilePath
@@ -45,7 +47,6 @@
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.python import hashlib
 
-
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.config import config
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
@@ -54,7 +55,9 @@
 from twistedcaldav.ical import Component, InvalidICalendarDataError, Property
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcacher import Memcacher
+
 from txdav.base.propertystore.base import PropertyName
+from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
 from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
 from txdav.caldav.datastore.util import AttachmentRetrievalTransport, \
     normalizationLookup
@@ -95,6 +98,7 @@
 
 from urlparse import urlparse, urlunparse
 import collections
+import datetime
 import os
 import tempfile
 import urllib
@@ -317,7 +321,75 @@
         returnValue(results)
 
 
+    @inlineCallbacks
+    def calendarObjectsWithUID(self, txn, uid):
+        """
+        Return all child object resources with the specified UID. Only "owned" resources are returned,
+        no shared resources.
 
+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param uid: UID to query
+        @type uid: C{str}
+
+        @return: list of matching L{CalendarObject}s
+        @rtype: C{list}
+        """
+
+        # First find the resource-ids of the (home, parent, object) for each object matching the UID.
+        obj = CalendarHome._objectSchema
+        bind = CalendarHome._bindSchema
+        rows = (yield Select(
+            [bind.HOME_RESOURCE_ID, obj.PARENT_RESOURCE_ID, obj.RESOURCE_ID],
+            From=obj.join(bind, obj.PARENT_RESOURCE_ID == bind.RESOURCE_ID),
+            Where=(obj.UID == Parameter("uid")).And(bind.BIND_MODE == _BIND_MODE_OWN),
+        ).on(txn, uid=uid))
+
+        results = []
+        for homeID, parentID, childID in rows:
+            home = (yield txn.calendarHomeWithResourceID(homeID))
+            parent = (yield home.childWithID(parentID))
+            child = (yield parent.objectResourceWithID(childID))
+            results.append(child)
+
+        returnValue(results)
+
+
+    @inlineCallbacks
+    def calendarObjectWithID(self, txn, rid):
+        """
+        Return all child object resources with the specified UID. Only "owned" resources are returned,
+        no shared resources.
+
+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param rid: resource-id to query
+        @type rid: C{int}
+
+        @return: matching calendar object, or None if not found
+        @rtype: L{CalendarObject} or C{None}
+        """
+
+        # First find the resource-ids of the (home, parent, object) for each object matching the UID.
+        obj = CalendarHome._objectSchema
+        bind = CalendarHome._bindSchema
+        rows = (yield Select(
+            [bind.HOME_RESOURCE_ID, obj.PARENT_RESOURCE_ID, obj.RESOURCE_ID],
+            From=obj.join(bind, obj.PARENT_RESOURCE_ID == bind.RESOURCE_ID),
+            Where=(obj.RESOURCE_ID == Parameter("rid")).And(bind.BIND_MODE == _BIND_MODE_OWN),
+        ).on(txn, rid=rid))
+
+        results = []
+        for homeID, parentID, childID in rows:
+            home = (yield txn.calendarHomeWithResourceID(homeID))
+            parent = (yield home.childWithID(parentID))
+            child = (yield parent.objectResourceWithID(childID))
+            results.append(child)
+
+        returnValue(results[0] if results else None)
+
+
+
 class CalendarHome(CommonHome):
 
     implements(ICalendarHome)
@@ -545,12 +617,6 @@
 
 
     @inlineCallbacks
-    def attachmentObjectWithID(self, managedID):
-        attach = (yield ManagedAttachment.load(self._txn, managedID))
-        returnValue(attach)
-
-
-    @inlineCallbacks
     def createdHome(self):
 
         # Default calendar
@@ -1371,16 +1437,6 @@
         )
 
 
-    def creatingResourceCheckAttachments(self, component):
-        """
-        When component data is created or changed we need to look for changes related to managed attachments.
-
-        @param component: the new calendar data
-        @type component: L{Component}
-        """
-        return CalendarObject.creatingResourceCheckAttachments(self._txn, self, component)
-
-
 icalfbtype_to_indexfbtype = {
     "UNKNOWN"         : 0,
     "FREE"            : 1,
@@ -1879,6 +1935,8 @@
 
         new_component = None
         did_implicit_action = False
+        is_scheduling_resource = False
+        schedule_state = None
 
         is_internal = internal_state not in (ComponentUpdateState.NORMAL, ComponentUpdateState.ATTACHMENT_UPDATE,)
 
@@ -1910,10 +1968,9 @@
                     else:
                         new_component = new_calendar
                 did_implicit_action = True
-        else:
-            is_scheduling_resource = False
+                schedule_state = scheduler.state
 
-        returnValue((is_scheduling_resource, new_component, did_implicit_action,))
+        returnValue((is_scheduling_resource, new_component, did_implicit_action, schedule_state,))
 
 
     @inlineCallbacks
@@ -2028,8 +2085,26 @@
 
         self._componentChanged = False
         self.schedule_tag_match = not self.calendar().isInbox() and internal_state == ComponentUpdateState.NORMAL and smart_merge
+        schedule_state = None
 
-        if internal_state != ComponentUpdateState.RAW:
+        if internal_state == ComponentUpdateState.SPLIT:
+            # When splitting, some state from the previous resource needs to be properly
+            # preserved in thus new one when storing the component. Since we don't do the "full"
+            # store here, we need to add the explicit pieces we need for state preservation.
+
+            # Check access
+            if config.EnablePrivateEvents:
+                self.validAccess(component, inserting, internal_state)
+
+            # Preserve private comments
+            yield self.preservePrivateComments(component, inserting)
+
+            managed_copied, managed_removed = (yield self.resourceCheckAttachments(component, inserting))
+
+            self.isScheduleObject = True
+            self.processScheduleTags(component, inserting, internal_state)
+
+        elif internal_state != ComponentUpdateState.RAW:
             # Handle all validation operations here.
             yield self.fullValidation(component, inserting, internal_state)
 
@@ -2047,7 +2122,7 @@
 
             # Pre-process managed attachments
             if internal_state == ComponentUpdateState.NORMAL:
-                managed_copied, managed_removed = (yield self.updatingResourceCheckAttachments(component, inserting))
+                managed_copied, managed_removed = (yield self.resourceCheckAttachments(component, inserting))
 
             # Default/duplicate alarms
             self.processAlarms(component, inserting)
@@ -2072,7 +2147,7 @@
                     log.error(msg)
                     raise InvalidObjectResourceError(msg)
             else:
-                self.isScheduleObject, new_component, did_implicit_action = implicit_result
+                self.isScheduleObject, new_component, did_implicit_action, schedule_state = implicit_result
                 if new_component is not None:
                     component = new_component
                 if did_implicit_action:
@@ -2090,7 +2165,7 @@
         yield self.updateDatabase(component, inserting=inserting)
 
         # Post process managed attachments
-        if internal_state == ComponentUpdateState.NORMAL:
+        if internal_state in (ComponentUpdateState.NORMAL, ComponentUpdateState.SPLIT):
             if managed_copied:
                 yield self.copyResourceAttachments(managed_copied)
             if managed_removed:
@@ -2103,6 +2178,10 @@
 
         yield self._calendar.notifyChanged()
 
+        # Finally check if a split is needed
+        if internal_state != ComponentUpdateState.SPLIT and schedule_state == "organizer":
+            yield self.checkSplit()
+
         returnValue(self._componentChanged)
 
 
@@ -2662,59 +2741,15 @@
     hasPrivateComment = property(_get_hasPrivateComment, _set_hasPrivateComment)
 
     @inlineCallbacks
-    def _preProcessAttachmentsOnResourceChange(self, component, inserting):
+    def resourceCheckAttachments(self, component, inserting=False):
         """
-        When component data is created or changed we need to look for changes related to managed attachments.
+        A component is being changed or added. Check any ATTACH properties that may be present
+        to verify they are owned by the organizer/owner of the resource. Strip any that are invalid.
 
-        @param component: the new calendar data
-        @type component: L{Component}
-        @param inserting: C{True} if resource is being created
-        @type inserting: C{bool}
-        """
-        if inserting:
-            self._copyAttachments = (yield self.creatingResourceCheckAttachments(component))
-            self._removeAttachments = None
-        else:
-            self._copyAttachments, self._removeAttachments = (yield self.updatingResourceCheckAttachments(component))
-
-
-    @classmethod
-    @inlineCallbacks
-    def creatingResourceCheckAttachments(cls, txn, parent, component):
-        """
-        A new component is going to be stored. Check any ATTACH properties that may be present
-        to verify they are owned by the organizer/owner of the resource and re-write the managed-ids.
-
         @param component: calendar component about to be stored
         @type component: L{Component}
         """
 
-        # Retrieve all ATTACH properties with a MANAGED-ID
-        attached = collections.defaultdict(list)
-        attachments = component.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
-        for attachment in attachments:
-            managed_id = attachment.parameterValue("MANAGED-ID")
-            if managed_id is not None:
-                attached[managed_id].append(attachment)
-
-        # Punt if no managed attachments
-        if len(attached) == 0:
-            returnValue(None)
-
-        changes = yield cls._addingManagedIDs(txn, parent, str(uuid.uuid4()), attached, component.resourceUID())
-        returnValue(changes)
-
-
-    @inlineCallbacks
-    def updatingResourceCheckAttachments(self, component, inserting=False):
-        """
-        A component is being changed. Check any ATTACH properties that may be present
-        to verify they are owned by the organizer/owner of the resource and re-write the managed-ids.
-
-        @param component: calendar component about to be stored
-        @type component: L{Component}
-        """
-
         # Retrieve all ATTACH properties with a MANAGED-ID in new data
         newattached = collections.defaultdict(list)
         newattachments = component.getAllPropertiesInAnyComponent("ATTACH", depth=1,)
@@ -2762,7 +2797,6 @@
             oldattachment = oldattached[managed_id][0]
             for newattachment in newattached[managed_id]:
                 if newattachment != oldattachment:
-                    newattachment.setParameter("MTAG", oldattachment.parameterValue("MTAG"))
                     newattachment.setParameter("FMTTYPE", oldattachment.parameterValue("FMTTYPE"))
                     newattachment.setParameter("FILENAME", oldattachment.parameterValue("FILENAME"))
                     newattachment.setParameter("SIZE", oldattachment.parameterValue("SIZE"))
@@ -2771,9 +2805,8 @@
         returnValue((changes, removed,))
 
 
-    @classmethod
     @inlineCallbacks
-    def _addingManagedIDs(cls, txn, parent, dropbox_id, attached, newuid):
+    def _addingManagedIDs(self, txn, parent, dropbox_id, attached, newuid):
         # Now check each managed-id
         changes = []
         for managed_id, attachments in attached.items():
@@ -2782,38 +2815,40 @@
             details = (yield ManagedAttachment.usedManagedID(txn, managed_id))
             if len(details) == 0:
                 raise AttachmentStoreValidManagedID
-            if len(details) != 1:
+            homes = set()
+            uids = set()
+            for home_id, _ignore_resource_id, uid in details:
+                homes.add(home_id)
+                uids.add(uid)
+            if len(homes) != 1:
                 # This is a bad store error - there should be only one home associated with a managed-id
                 raise InternalDataStoreError
-            home_id, _ignore_resource_id, uid = details[0]
 
             # Policy:
             #
             # 1. If Managed-ID is re-used in a resource with the same UID - it is fine - just rewrite the details
-            # 2. If Managed-ID is re-used in a different resource but owned by the same user - change managed-id to new one
+            # 2. If Managed-ID is re-used in a different resource but owned by the same user - just rewrite the details
             # 3. Otherwise, strip off the managed-id property and treat as unmanaged.
 
             # 1. UID check
-            if uid == newuid:
-                yield cls._syncAttachmentProperty(txn, managed_id, dropbox_id, attachments)
+            if newuid in uids:
+                yield self._syncAttachmentProperty(txn, managed_id, dropbox_id, attachments)
 
             # 2. Same home
             elif home_id == parent.ownerHome()._resourceID:
 
-                # Need to rewrite the managed-id, value in the properties
-                new_id = str(uuid.uuid4())
-                yield cls._syncAttachmentProperty(txn, managed_id, dropbox_id, attachments, new_id)
-                changes.append((managed_id, new_id,))
+                # Record the change as we will need to add a new reference to this attachment
+                yield self._syncAttachmentProperty(txn, managed_id, dropbox_id, attachments)
+                changes.append(managed_id)
 
             else:
-                cls._stripAttachmentProperty(attachments)
+                self._stripAttachmentProperty(attachments)
 
         returnValue(changes)
 
 
-    @classmethod
     @inlineCallbacks
-    def _syncAttachmentProperty(cls, txn, managed_id, dropbox_id, attachments, new_id=None):
+    def _syncAttachmentProperty(self, txn, managed_id, dropbox_id, attachments):
         """
         Make sure the supplied set of attach properties are all sync'd with the current value of the
         matching managed-id attachment.
@@ -2822,19 +2857,19 @@
         @type managed_id: C{str}
         @param attachments: list of attachment properties
         @type attachments: C{list} of L{twistedcaldav.ical.Property}
-        @param new_id: Value of new Managed-ID to use
-        @type new_id: C{str}
         """
-        new_attachment = (yield ManagedAttachment.load(txn, managed_id))
-        if new_id:
-            new_attachment._managedID = new_id
+
+        # Try to load any attachment directly referenced by this object, otherwise load one referenced by some
+        # any other - they are all the same.
+        new_attachment = (yield ManagedAttachment.load(txn, self._resourceID, managed_id))
+        if new_attachment is None:
+            new_attachment = (yield ManagedAttachment.load(txn, None, managed_id))
         new_attachment._objectDropboxID = dropbox_id
         for attachment in attachments:
             yield new_attachment.updateProperty(attachment)
 
 
-    @classmethod
-    def _stripAttachmentProperty(cls, attachments):
+    def _stripAttachmentProperty(self, attachments):
         """
         Strip off managed-id related properties from an attachment.
         """
@@ -2848,11 +2883,11 @@
         """
         Copy an attachment reference for some other resource and link it to this resource.
 
-        @param attached: tuple of old, new managed ids for the attachments to copy
-        @type attached: C{tuple}
+        @param attached: managed id for the attachments to copy
+        @type attached: C{str}
         """
-        for old_id, new_id in attached:
-            yield ManagedAttachment.copyManagedID(self._txn, old_id, new_id, self._resourceID)
+        for managed_id in attached:
+            yield ManagedAttachment.copyManagedID(self._txn, managed_id, self._resourceID)
 
 
     @inlineCallbacks
@@ -3102,13 +3137,13 @@
 
 
     def attachmentWithManagedID(self, managed_id):
-        return ManagedAttachment.load(self._txn, managed_id)
+        return ManagedAttachment.load(self._txn, self._resourceID, managed_id)
 
 
     @inlineCallbacks
     def removeManagedAttachmentWithID(self, managed_id):
         attachment = (yield self.attachmentWithManagedID(managed_id))
-        if attachment._objectResourceID == self._resourceID:
+        if attachment is not None:
             yield attachment.removeFromResource(self._resourceID)
 
 
@@ -3218,7 +3253,116 @@
         return MimeType.fromString("text/calendar; charset=utf-8")
 
 
+    @inlineCallbacks
+    def checkSplit(self):
+        """
+        Determine if the calendar data needs to be split, and enqueue a split work item if needed.
+        """
 
+        if config.Scheduling.Options.Splitting.Enabled:
+            will = (yield self.willSplit())
+            if will:
+                notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=config.Scheduling.Options.Splitting.Delay)
+                work = (yield self._txn.enqueue(CalendarObject.CalendarObjectSplitterWork, resourceID=self._resourceID, notBefore=notBefore))
+                if not hasattr(self, "_workItems"):
+                    self._workItems = []
+                self._workItems.append(work)
+
+
+    @inlineCallbacks
+    def willSplit(self):
+        """
+        Determine if the calendar data needs to be split as per L{iCalSplitter}.
+        """
+
+        splitter = iCalSplitter(config.Scheduling.Options.Splitting.Size, config.Scheduling.Options.Splitting.PastDays)
+        ical = (yield self.component())
+        will_split = splitter.willSplit(ical)
+        returnValue(will_split)
+
+
+    @inlineCallbacks
+    def split(self):
+        """
+        Split this and all matching UID calendar objects as per L{iCalSplitter}.
+        """
+
+        # First job is to grab a UID lock on this entire series of events
+        yield NamedLock.acquire(self._txn, "ImplicitUIDLock:%s" % (hashlib.md5(self._uid).hexdigest(),))
+
+        # Find all other calendar objects on this server with the same UID
+        resources = (yield CalendarStoreFeatures(self._txn._store).calendarObjectsWithUID(self._txn, self._uid))
+
+        splitter = iCalSplitter(config.Scheduling.Options.Splitting.Size, config.Scheduling.Options.Splitting.PastDays)
+
+        # Determine the recurrence-id of the split and create a new UID for it
+        calendar = (yield self.component())
+        rid = splitter.whereSplit(calendar)
+        newUID = str(uuid.uuid4())
+
+        # Now process this resource, but do implicit scheduling for attendees not hosted on this server.
+        # We need to do this before processing attendee copies.
+        calendar_old = splitter.split(calendar, rid=rid, newUID=newUID)
+
+        # Store changed data
+        if calendar.mainType() is not None:
+            yield self._setComponentInternal(calendar, internal_state=ComponentUpdateState.SPLIT)
+        else:
+            yield self._removeInternal(internal_state=ComponentUpdateState.SPLIT)
+        if calendar_old.mainType() is not None:
+            yield self.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (newUID,), calendar_old, ComponentUpdateState.SPLIT)
+
+        # Split each one - but not this resource
+        for resource in resources:
+            if resource._resourceID == self._resourceID:
+                continue
+            ical = (yield resource.component())
+            ical_old = splitter.split(ical, rid=rid, newUID=newUID)
+
+            # Store changed data
+            if ical.mainType() is not None:
+                yield resource._setComponentInternal(ical, internal_state=ComponentUpdateState.SPLIT)
+            else:
+                # The split removed all components from this object - remove it
+                yield resource._removeInternal(internal_state=ComponentUpdateState.SPLIT)
+
+            # Create a new resource and store its data (but not if the parent is "inbox", or if it is empty)
+            if not resource.calendar().isInbox() and ical_old.mainType() is not None:
+                yield resource.calendar()._createCalendarObjectWithNameInternal("%s.ics" % (newUID,), ical_old, ComponentUpdateState.SPLIT)
+
+        # TODO: scheduling currently turned off until we figure out how to properly do that
+
+        returnValue(newUID)
+
+
+    class CalendarObjectSplitterWork(WorkItem, fromTable(schema.CALENDAR_OBJECT_SPLITTER_WORK)):
+
+        group = property(lambda self: "CalendarObjectSplitterWork:%s" % (self.resourceID,))
+
+        @inlineCallbacks
+        def doWork(self):
+
+            log.debug("Splitting calendar object with resource-id: {rid}", rid=self.resourceID)
+
+            # Delete all other work items with the same resourceID
+            yield Delete(From=self.table,
+                         Where=self.table.RESOURCE_ID == self.resourceID
+                        ).on(self.transaction)
+
+            # Get the actual owned calendar object with this ID
+            cobj = (yield CalendarStoreFeatures(self.transaction._store).calendarObjectWithID(self.transaction, self.resourceID))
+            if cobj is None:
+                returnValue(None)
+
+            # Check it is still split-able
+            will = (yield  cobj.willSplit())
+
+            if will:
+                # Now do the spitting
+                yield cobj.split()
+
+
+
 class AttachmentStorageTransport(StorageTransportBase):
 
     _TEMPORARY_UPLOADS_DIRECTORY = "Temporary"
@@ -3679,7 +3823,8 @@
 
         # Create an "orphaned" ManagedAttachment that points to the updated data but without
         # an actual managed-id (which only exists when there is a reference to a calendar object).
-        mattach = (yield ManagedAttachment.load(self._txn, None, attachmentID=self._attachmentID))
+        mattach = (yield ManagedAttachment.load(self._txn, None, None, attachmentID=self._attachmentID))
+        mattach._managedID = str(uuid.uuid4())
         if mattach is None:
             raise AttachmentMigrationFailed
 
@@ -3765,30 +3910,29 @@
 
         # Now create the DB entry
         attachment = (yield cls._create(txn, managedID, ownerHomeID))
+        attachment._objectResourceID = referencedBy
 
         # Create the attachment<->calendar object relationship for managed attachments
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
         yield Insert({
             attco.ATTACHMENT_ID               : attachment._attachmentID,
-            attco.MANAGED_ID                  : managedID,
-            attco.CALENDAR_OBJECT_RESOURCE_ID : referencedBy,
+            attco.MANAGED_ID                  : attachment._managedID,
+            attco.CALENDAR_OBJECT_RESOURCE_ID : attachment._objectResourceID,
         }).on(txn)
-        attachment._managedID = managedID
-        attachment._objectResourceID = referencedBy
 
         returnValue(attachment)
 
 
     @classmethod
     @inlineCallbacks
-    def update(cls, txn, managedID, ownerHomeID, referencedBy, oldAttachmentID):
+    def update(cls, txn, oldManagedID, ownerHomeID, referencedBy, oldAttachmentID):
         """
         Create a new Attachment object.
 
         @param txn: The transaction to use
         @type txn: L{CommonStoreTransaction}
-        @param managedID: the identifier for the attachment
-        @type managedID: C{str}
+        @param oldManagedID: the identifier for the original attachment
+        @type oldManagedID: C{str}
         @param ownerHomeID: the resource-id of the home collection of the attachment owner
         @type ownerHomeID: C{int}
         @param referencedBy: the resource-id of the calendar object referencing the attachment
@@ -3797,21 +3941,22 @@
         @type oldAttachmentID: C{int}
         """
 
-        # Now create the DB entry
-        attachment = (yield cls._create(txn, managedID, ownerHomeID))
+        # Now create the DB entry with a new managed-ID
+        managed_id = str(uuid.uuid4())
+        attachment = (yield cls._create(txn, managed_id, ownerHomeID))
+        attachment._objectResourceID = referencedBy
 
         # Update the attachment<->calendar object relationship for managed attachments
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
         yield Update(
             {
                 attco.ATTACHMENT_ID    : attachment._attachmentID,
+                attco.MANAGED_ID       : attachment._managedID,
             },
-            Where=(attco.MANAGED_ID == managedID).And(
-                attco.CALENDAR_OBJECT_RESOURCE_ID == referencedBy
+            Where=(attco.MANAGED_ID == oldManagedID).And(
+                attco.CALENDAR_OBJECT_RESOURCE_ID == attachment._objectResourceID
             ),
         ).on(txn)
-        attachment._managedID = managedID
-        attachment._objectResourceID = referencedBy
 
         # Now check whether old attachmentID is still referenced - if not delete it
         rows = (yield Select(
@@ -3830,30 +3975,31 @@
 
     @classmethod
     @inlineCallbacks
-    def load(cls, txn, managedID, attachmentID=None):
+    def load(cls, txn, referencedID, managedID, attachmentID=None):
         """
-        Create a ManagedAttachment via either its managedID or attachmentID.
+        Load a ManagedAttachment via either its managedID or attachmentID.
         """
 
         if managedID:
             attco = schema.ATTACHMENT_CALENDAR_OBJECT
+            where = (attco.MANAGED_ID == managedID)
+            if referencedID is not None:
+                where = where.And(attco.CALENDAR_OBJECT_RESOURCE_ID == referencedID)
             rows = (yield Select(
-                [attco.ATTACHMENT_ID, attco.CALENDAR_OBJECT_RESOURCE_ID, ],
+                [attco.ATTACHMENT_ID, ],
                 From=attco,
-                Where=(attco.MANAGED_ID == managedID),
+                Where=where,
             ).on(txn))
             if len(rows) == 0:
                 returnValue(None)
-            elif len(rows) != 1:
+            elif referencedID is not None and len(rows) != 1:
                 raise AttachmentStoreValidManagedID
-            rows = rows[0]
-        else:
-            rows = (attachmentID, None,)
+            attachmentID = rows[0][0]
 
-        attachment = cls(txn, rows[0], None, None)
+        attachment = cls(txn, attachmentID, None, None)
         attachment = (yield attachment.initFromStore())
         attachment._managedID = managedID
-        attachment._objectResourceID = rows[1]
+        attachment._objectResourceID = referencedID
         returnValue(attachment)
 
 
@@ -3912,29 +4058,28 @@
         ).on(txn))
         mids = set([row[0] for row in rows]) if rows is not None else set()
         for managedID in mids:
-            attachment = (yield ManagedAttachment.load(txn, managedID))
+            attachment = (yield ManagedAttachment.load(txn, resourceID, managedID))
             (yield attachment.removeFromResource(resourceID))
 
 
     @classmethod
     @inlineCallbacks
-    def copyManagedID(cls, txn, oldManagedID, newManagedID, referencedBy):
+    def copyManagedID(cls, txn, managedID, referencedBy):
         """
-        Copy a managed-ID to a new ID and associate the original attachment with the
-        new resource.
+        Associate an existing attachment with the new resource.
         """
 
-        # Find all reference attachment-ids and dereference
+        # Find the associated attachment-id and insert new reference
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
         aid = (yield Select(
             [attco.ATTACHMENT_ID, ],
             From=attco,
-            Where=(attco.MANAGED_ID == oldManagedID),
+            Where=(attco.MANAGED_ID == managedID),
         ).on(txn))[0][0]
 
         yield Insert({
             attco.ATTACHMENT_ID               : aid,
-            attco.MANAGED_ID                  : newManagedID,
+            attco.MANAGED_ID                  : managedID,
             attco.CALENDAR_OBJECT_RESOURCE_ID : referencedBy,
         }).on(txn)
 
@@ -4020,7 +4165,7 @@
     @inlineCallbacks
     def newReference(self, resourceID):
         """
-        Create a new managed-id that references the supplied calendar object resource id, and
+        Create a new reference of this attachment to the supplied calendar object resource id, and
         return a ManagedAttachment for the new reference.
 
         @param resourceID: the resource id to reference
@@ -4030,15 +4175,14 @@
         @rtype: L{ManagedAttachment}
         """
 
-        managed_id = str(uuid.uuid4())
         attco = schema.ATTACHMENT_CALENDAR_OBJECT
         yield Insert({
             attco.ATTACHMENT_ID               : self._attachmentID,
-            attco.MANAGED_ID                  : managed_id,
+            attco.MANAGED_ID                  : self._managedID,
             attco.CALENDAR_OBJECT_RESOURCE_ID : resourceID,
         }).on(self._txn)
 
-        mattach = (yield ManagedAttachment.load(self._txn, managed_id))
+        mattach = (yield ManagedAttachment.load(self._txn, resourceID, self._managedID))
         returnValue(mattach)
 
 
@@ -4082,7 +4226,6 @@
         location = (yield self.location())
 
         attach.setParameter("MANAGED-ID", self.managedID())
-        attach.setParameter("MTAG", self.md5())
         attach.setParameter("FMTTYPE", "%s/%s" % (self.contentType().mediaType, self.contentType().mediaSubtype))
         attach.setParameter("FILENAME", self.name())
         attach.setParameter("SIZE", str(self.size()))

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_attachments.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -1602,7 +1602,7 @@
 
         # Managed attachment present
         txn = self._sqlCalendarStore.newTransaction()
-        mattachment2 = (yield ManagedAttachment.load(txn, None, attachmentID=dattachment._attachmentID))
+        mattachment2 = (yield ManagedAttachment.load(txn, None, None, attachmentID=dattachment._attachmentID))
         self.assertNotEqual(mattachment2, None)
         self.assertTrue(mattachment2.isManaged())
 
@@ -1626,21 +1626,21 @@
         self.assertTrue(dattachment._path.exists())
         mattachment = (yield dattachment.convertToManaged())
         self.assertNotEqual(mattachment, None)
-        self.assertEqual(mattachment.managedID(), None)
+        self.assertNotEqual(mattachment.managedID(), None)
 
         mnew4 = (yield mattachment.newReference(event4._resourceID))
         self.assertNotEqual(mnew4, None)
-        self.assertNotEqual(mnew4.managedID(), None)
+        self.assertEqual(mnew4.managedID(), mattachment.managedID())
 
         mnew5 = (yield mattachment.newReference(event5._resourceID))
         self.assertNotEqual(mnew5, None)
-        self.assertNotEqual(mnew5.managedID(), None)
+        self.assertEqual(mnew5.managedID(), mattachment.managedID())
 
         yield txn.commit()
 
         # Managed attachment present
         txn = self._sqlCalendarStore.newTransaction()
-        mtest4 = (yield ManagedAttachment.load(txn, mnew4.managedID()))
+        mtest4 = (yield ManagedAttachment.load(txn, event4._resourceID, mnew4.managedID()))
         self.assertNotEqual(mtest4, None)
         self.assertTrue(mtest4.isManaged())
         self.assertEqual(mtest4._objectResourceID, event4._resourceID)
@@ -1648,7 +1648,7 @@
 
         # Managed attachment present
         txn = self._sqlCalendarStore.newTransaction()
-        mtest5 = (yield ManagedAttachment.load(txn, mnew5.managedID()))
+        mtest5 = (yield ManagedAttachment.load(txn, event5._resourceID, mnew5.managedID()))
         self.assertNotEqual(mtest5, None)
         self.assertTrue(mtest5.isManaged())
         self.assertEqual(mtest5._objectResourceID, event5._resourceID)

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -24,6 +24,8 @@
 
 from twext.enterprise.dal.syntax import Select, Parameter, Insert, Delete
 from twext.python.vcomponent import VComponent
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
 
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue, DeferredList
@@ -48,7 +50,8 @@
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
     _BIND_STATUS_ACCEPTED, _BIND_MODE_WRITE, _BIND_STATUS_INVITED
-from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.test.util import populateCalendarsFrom, \
+    CommonCommonTests
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 
@@ -1936,3 +1939,1550 @@
         self.assertNotEqual(caldata2, None)
         self.assertNotEqual(caldata3, None)
         self.assertNotEqual(caldata4, None)
+
+
+
+class CalendarObjectSplitting(CommonCommonTests, unittest.TestCase):
+    """
+    CalendarObject splitting tests
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(CalendarObjectSplitting, self).setUp()
+        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
+
+        # Make sure homes are provisioned
+        txn = self.transactionUnderTest()
+        for ctr in range(1, 5):
+            home_uid = yield txn.homeWithUID(ECALENDARTYPE, "user%02d" % (ctr,), create=True)
+            self.assertNotEqual(home_uid, None)
+        yield self.commit()
+
+        self.subs = {}
+
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.now.setHHMMSS(0, 0, 0)
+
+        self.subs["now"] = self.now
+
+        for i in range(30):
+            attrname = "now_back%s" % (i + 1,)
+            setattr(self, attrname, self.now.duplicate())
+            getattr(self, attrname).offsetDay(-(i + 1))
+            self.subs[attrname] = getattr(self, attrname)
+
+            attrname_12h = "now_back%s_12h" % (i + 1,)
+            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
+            getattr(self, attrname_12h).offsetHours(12)
+            self.subs[attrname_12h] = getattr(self, attrname_12h)
+
+            attrname_1 = "now_back%s_1" % (i + 1,)
+            setattr(self, attrname_1, getattr(self, attrname).duplicate())
+            getattr(self, attrname_1).offsetSeconds(-1)
+            self.subs[attrname_1] = getattr(self, attrname_1)
+
+        for i in range(30):
+            attrname = "now_fwd%s" % (i + 1,)
+            setattr(self, attrname, self.now.duplicate())
+            getattr(self, attrname).offsetDay(i + 1)
+            self.subs[attrname] = getattr(self, attrname)
+
+            attrname_12h = "now_fwd%s_12h" % (i + 1,)
+            setattr(self, attrname_12h, getattr(self, attrname).duplicate())
+            getattr(self, attrname_12h).offsetHours(12)
+            self.subs[attrname_12h] = getattr(self, attrname_12h)
+
+        self.patch(config, "MaxAllowedInstances", 500)
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit(self):
+        """
+        Test that (manual) splitting of calendar objects works.
+        """
+
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", False)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user1 at example.org
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+ATTENDEE:mailto:user3 at example.org
+ATTENDEE:mailto:user4 at example.org
+ATTENDEE:mailto:user5 at example.org
+ATTENDEE:mailto:user6 at example.org
+ATTENDEE:mailto:user7 at example.org
+ATTENDEE:mailto:user8 at example.org
+ATTENDEE:mailto:user9 at example.org
+ATTENDEE:mailto:user10 at example.org
+ATTENDEE:mailto:user11 at example.org
+ATTENDEE:mailto:user12 at example.org
+ATTENDEE:mailto:user13 at example.org
+ATTENDEE:mailto:user14 at example.org
+ATTENDEE:mailto:user15 at example.org
+ATTENDEE:mailto:user16 at example.org
+ATTENDEE:mailto:user17 at example.org
+ATTENDEE:mailto:user18 at example.org
+ATTENDEE:mailto:user19 at example.org
+ATTENDEE:mailto:user20 at example.org
+ATTENDEE:mailto:user21 at example.org
+ATTENDEE:mailto:user22 at example.org
+ATTENDEE:mailto:user23 at example.org
+ATTENDEE:mailto:user24 at example.org
+ATTENDEE:mailto:user25 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.org
+ATTENDEE:mailto:user2 at example.org
+DTSTAMP:20051222T210507Z
+ORGANIZER;SCHEDULE-AGENT=NONE;SCHEDULE-STATUS=5.3:mailto:user1 at example.org
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Do manual split
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        will = yield cobj.willSplit()
+        self.assertTrue(will)
+
+        newUID = yield cobj.split()
+        yield self.commit()
+
+        cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        cobj2 = yield self.calendarObjectUnderTest(name="%s.ics" % (newUID,), calendar_name="calendar", home="user01")
+        self.assertTrue(cobj2 is not None)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        title = "temp"
+        relsubs = dict(self.subs)
+        relsubs["relID"] = newUID
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n") % relsubs, "Failed past: %s" % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_work(self):
+        """
+        Test that splitting of calendar objects works.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:user03 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:user04 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user05 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 05;EMAIL=user05 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user05
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+ATTENDEE;CN=User 04;EMAIL=user04 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user04
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 04;EMAIL=user04 at example.com;RSVP=TRUE:urn:uuid:user04
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_inbox2 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future3 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user03
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past3 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_back25)s
+EXDATE:%(now_back24)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user03
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_inbox3 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 03;EMAIL=user03 at example.com;RSVP=TRUE:urn:uuid:user03
+DTSTAMP:20051222T210507Z
+EXDATE:%(now_fwd10)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past4 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+ATTENDEE;CN=User 04;EMAIL=user04 at example.com;RSVP=TRUE:urn:uuid:user04
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user04
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back25)s
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_future5 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 05;EMAIL=user05 at example.com;RSVP=TRUE:urn:uuid:user05
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user05
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_fwd10)s
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_inbox5 = """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 05;EMAIL=user05 at example.com;RSVP=TRUE:urn:uuid:user05
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertTrue(hasattr(cobj, "_workItems"))
+        work = cobj._workItems[0]
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 1)
+        self.assertEqual(rows[0][0], cobj._resourceID)
+        yield self.abort()
+
+        # Wait for it to complete
+        yield work.whenExecuted()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Get the existing and new object data
+        cobj1 = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        self.assertTrue(cobj1.isScheduleObject)
+        ical1 = yield cobj1.component()
+        newUID = ical1.masterComponent().propertyValue("RELATED-TO")
+
+        cobj2 = yield self.calendarObjectUnderTest(name="%s.ics" % (newUID,), calendar_name="calendar", home="user01")
+        self.assertTrue(cobj2 is not None)
+        self.assertTrue(cobj2.isScheduleObject)
+
+        ical_future = yield cobj1.component()
+        ical_past = yield cobj2.component()
+
+        # Verify user01 data
+        title = "user01"
+        relsubs = dict(self.subs)
+        relsubs["relID"] = newUID
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == "12345-67890":
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        cal = yield self.calendarUnderTest(name="inbox", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user02 data
+        title = "user02"
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+
+        # Get user03 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user03")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == "12345-67890":
+                ical_future = ical
+            else:
+                ical_past = ical
+            self.assertTrue(cobj.isScheduleObject)
+
+        cal = yield self.calendarUnderTest(name="inbox", home="user03")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user03 data
+        title = "user03"
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox3.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+
+        # Get user04 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user04")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_past = yield cobjs[0].component()
+        self.assertTrue(cobjs[0].isScheduleObject)
+
+        cal = yield self.calendarUnderTest(name="inbox", home="user04")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 0)
+
+        # Verify user04 data
+        title = "user04"
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past4.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+
+        # Get user05 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user05")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_future = yield cobjs[0].component()
+        self.assertTrue(cobjs[0].isScheduleObject)
+
+        cal = yield self.calendarUnderTest(name="inbox", home="user05")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        ical_inbox = yield cobjs[0].component()
+
+        # Verify user05 data
+        title = "user05"
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future5.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_inbox).replace("\r\n ", ""), data_inbox5.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed inbox: %s" % (title,))
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_removed(self):
+        """
+        Test that splitting of calendar objects dioes not occur when the object is
+        removed before the work can be done.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 10)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:user03 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:user04 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user05 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertTrue(hasattr(cobj, "_workItems"))
+        work = cobj._workItems[0]
+        yield self.commit()
+
+        w = schema.CALENDAR_OBJECT_SPLITTER_WORK
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 1)
+        self.assertEqual(rows[0][0], cobj._resourceID)
+        yield self.abort()
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        yield cobj.remove()
+        yield self.commit()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        # Wait for it to complete
+        yield work.whenExecuted()
+
+        rows = yield Select(
+            [w.RESOURCE_ID, ],
+            From=w
+        ).on(self.transactionUnderTest())
+        self.assertEqual(len(rows), 0)
+        yield self.abort()
+
+        cal = yield self.calendarUnderTest(name="calendar", home="user01")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 0)
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_no_attendee_split(self):
+        """
+        Test that calendar objects do not split on attendee change.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_2_update = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_2_changed = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:20051222T210507Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_back25)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:%(now_fwd10)s
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER;RELATED=START:-PT5M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 1)
+        cobj = cobjs[0]
+        cname2 = cobj.name()
+        ical = yield cobj.component()
+        self.assertEqual(str(ical).replace("\r\n ", ""), data_2.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        yield cobj.setComponent(Component.fromString(data_2_update % self.subs))
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        ical = yield cobj.component()
+        self.assertEqual(str(ical).replace("\r\n ", ""), data_1.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        cobj = yield self.calendarObjectUnderTest(name=cname2, calendar_name="calendar", home="user02")
+        ical = yield cobj.component()
+        self.assertEqual(str(ical).replace("\r\n ", ""), data_2_changed.replace("\n", "\r\n").replace("\r\n ", "") % self.subs, "Failed 2")
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_no_non_organizer_split(self):
+        """
+        Test that calendar objects do not split on attendee change.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event that will not split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+END:VCALENDAR
+"""
+
+        component = Component.fromString(data % self.subs)
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", component)
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+
+    @inlineCallbacks
+    def test_calendarObjectSplit_attachments(self):
+        """
+        Test that splitting of calendar objects with managed attachments works.
+        """
+        self.patch(config.Scheduling.Options.Splitting, "Enabled", True)
+        self.patch(config.Scheduling.Options.Splitting, "Size", 1024)
+        self.patch(config.Scheduling.Options.Splitting, "PastDays", 14)
+        self.patch(config.Scheduling.Options.Splitting, "Delay", 2)
+
+        # Create one event that will split
+        calendar = yield self.calendarUnderTest(name="calendar", home="user01")
+
+        data_1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+DTSTAMP:20051222T210507Z
+ORGANIZER:mailto:user01 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_attach_1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_split_1 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RRULE:FREQ=DAILY
+SEQUENCE:1
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:2
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_past = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:2
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_future2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:%(now_back14)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY
+SEQUENCE:2
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:%(now_fwd10)s
+DTSTART:%(now_fwd10)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(mid)s;SIZE=14:%(att_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        data_past2 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:%(relID)s
+DTSTART:%(now_back30)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+RRULE:FREQ=DAILY;UNTIL=%(now_back14_1)s
+SEQUENCE:2
+SUMMARY:1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+ 1234567890123456789012345678901234567890
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back25)s
+DTSTART:%(now_back25)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+BEGIN:VEVENT
+UID:%(relID)s
+RECURRENCE-ID:%(now_back24)s
+DTSTART:%(now_back24)s
+DURATION:PT1H
+ATTACH;FILENAME=new.attachment;FMTTYPE=text/x-fixture;MANAGED-ID=%(past_mid)s;SIZE=14:%(att_past_uri)s
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;PARTSTAT=ACCEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:uuid:user02
+DTSTAMP:%(dtstamp)s
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+RELATED-TO;RELTYPE=X-CALENDARSERVER-RECURRENCE-SET:%(relID)s
+SEQUENCE:2
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:%(relID)s
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+"""
+
+        # Create initial non-split event
+        cobj = yield calendar.createCalendarObjectWithName("data1.ics", Component.fromString(data_1 % self.subs))
+        self.assertFalse(hasattr(cobj, "_workItems"))
+        yield self.commit()
+
+        # Add a managed attachment
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        attachment, location = yield cobj.addAttachment(None, MimeType("text", "x-fixture"), "new.attachment", MemoryStream("new attachment"))
+        mid = attachment.managedID()
+        yield self.commit()
+
+        # Get attachment details
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        ical = yield cobj.component()
+        attachment = ical.masterComponent().getProperty("ATTACH")
+        self.assertEqual(attachment.parameterValue("MANAGED-ID"), mid)
+        self.assertEqual(attachment.value(), location)
+
+        relsubs = dict(self.subs)
+        relsubs["mid"] = mid
+        relsubs["att_uri"] = location
+        relsubs["dtstamp"] = str(ical.masterComponent().propertyValue("DTSTAMP"))
+        self.assertEqual(str(ical).replace("\r\n ", ""), data_attach_1.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed attachment user01")
+        yield self.commit()
+
+        # Add overrides to cause a split
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        yield cobj.setComponent(Component.fromString(data_split_1 % relsubs))
+        self.assertTrue(hasattr(cobj, "_workItems"))
+        work = cobj._workItems[0]
+        yield self.commit()
+
+        # Wait for it to complete
+        yield work.whenExecuted()
+
+        # Get the existing and new object data
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="user01")
+        ical_future = yield cobj.component()
+        newUID = ical_future.masterComponent().propertyValue("RELATED-TO")
+        relsubs["relID"] = newUID
+        relsubs["dtstamp"] = str(ical_future.masterComponent().propertyValue("DTSTAMP"))
+
+        cobj = yield self.calendarObjectUnderTest(name="%s.ics" % (newUID,), calendar_name="calendar", home="user01")
+        self.assertTrue(cobj is not None)
+        ical_past = yield cobj.component()
+        attachment = ical.masterComponent().getProperty("ATTACH")
+        self.assertEqual(attachment.parameterValue("MANAGED-ID"), mid)
+        self.assertEqual(attachment.value(), location)
+
+        relsubs["past_mid"] = attachment.parameterValue("MANAGED-ID")
+        relsubs["att_past_uri"] = attachment.value()
+
+        # Verify user01 data
+        title = "user01"
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))
+
+        # Get user02 data
+        cal = yield self.calendarUnderTest(name="calendar", home="user02")
+        cobjs = yield cal.calendarObjects()
+        self.assertEqual(len(cobjs), 2)
+        for cobj in cobjs:
+            ical = yield cobj.component()
+            if ical.resourceUID() == "12345-67890":
+                ical_future = ical
+            else:
+                ical_past = ical
+
+        # Verify user02 data
+        title = "user02"
+        self.assertEqual(str(ical_future).replace("\r\n ", ""), data_future2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed future: %s" % (title,))
+        self.assertEqual(str(ical_past).replace("\r\n ", ""), data_past2.replace("\n", "\r\n").replace("\r\n ", "") % relsubs, "Failed past: %s" % (title,))

Modified: CalendarServer/trunk/txdav/caldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/caldav/icalendarstore.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -863,6 +863,9 @@
 
     ATTACHMENT_UPDATE     - change to a managed attachment that is re-writing calendar data.
 
+    SPLIT                 - calendar data is being split. Some validation and implicit scheduling is not done.
+                            Schedule-Tag is changed.
+
     RAW                   - store the supplied data as-is without any processing or validation. This is used
                             for unit testing purposes only.
     """
@@ -872,6 +875,7 @@
     ORGANIZER_ITIP_UPDATE = NamedConstant()
     ATTENDEE_ITIP_UPDATE = NamedConstant()
     ATTACHMENT_UPDATE = NamedConstant()
+    SPLIT = NamedConstant()
     RAW = NamedConstant()
 
     NORMAL.description = "normal"
@@ -879,6 +883,7 @@
     ORGANIZER_ITIP_UPDATE.description = "organizer-update"
     ATTENDEE_ITIP_UPDATE.description = "attendee-update"
     ATTACHMENT_UPDATE.description = "attachment-update"
+    SPLIT.description = "split"
     RAW.description = "raw"
 
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2013-07-04 02:29:51 UTC (rev 11460)
@@ -203,7 +203,7 @@
 
 
     @inlineCallbacks
-    def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize):  #@UnusedVariable
+    def _withEachHomeDo(self, homeTable, homeFromTxn, action, batchSize): #@UnusedVariable
         """
         Implementation of L{ICalendarStore.withEachCalendarHomeDo} and
         L{IAddressbookStore.withEachAddressbookHomeDo}.
@@ -503,7 +503,7 @@
 
 
     @classproperty
-    def _calendarserver(cls):  #@NoSelf
+    def _calendarserver(cls): #@NoSelf
         cs = schema.CALENDARSERVER
         return Select(
             [cs.VALUE, ],
@@ -548,7 +548,7 @@
         return self.homeWithUID(EADDRESSBOOKTYPE, uid, create=create)
 
 
-    def _determineMemo(self, storeType, uid, create=False):  #@UnusedVariable
+    def _determineMemo(self, storeType, uid, create=False): #@UnusedVariable
         """
         Determine the memo dictionary to use for homeWithUID.
         """
@@ -612,7 +612,7 @@
 
 
     @classproperty
-    def _insertAPNSubscriptionQuery(cls):  #@NoSelf
+    def _insertAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Insert({apn.TOKEN: Parameter("token"),
                        apn.RESOURCE_KEY: Parameter("resourceKey"),
@@ -623,7 +623,7 @@
 
 
     @classproperty
-    def _updateAPNSubscriptionQuery(cls):  #@NoSelf
+    def _updateAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Update({apn.MODIFIED: Parameter("modified"),
                        apn.SUBSCRIBER_GUID: Parameter("subscriber"),
@@ -634,7 +634,7 @@
 
 
     @classproperty
-    def _selectAPNSubscriptionQuery(cls):  #@NoSelf
+    def _selectAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Select([apn.MODIFIED, apn.SUBSCRIBER_GUID], From=apn,
                 Where=(
@@ -678,7 +678,7 @@
 
 
     @classproperty
-    def _removeAPNSubscriptionQuery(cls):  #@NoSelf
+    def _removeAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Delete(From=apn,
                       Where=(apn.TOKEN == Parameter("token")).And(
@@ -691,7 +691,7 @@
 
 
     @classproperty
-    def _purgeOldAPNSubscriptionQuery(cls):  #@NoSelf
+    def _purgeOldAPNSubscriptionQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Delete(From=apn,
                       Where=(apn.MODIFIED < Parameter("olderThan")))
@@ -703,7 +703,7 @@
 
 
     @classproperty
-    def _apnSubscriptionsByTokenQuery(cls):  #@NoSelf
+    def _apnSubscriptionsByTokenQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Select([apn.RESOURCE_KEY, apn.MODIFIED, apn.SUBSCRIBER_GUID],
                       From=apn, Where=apn.TOKEN == Parameter("token"))
@@ -714,7 +714,7 @@
 
 
     @classproperty
-    def _apnSubscriptionsByKeyQuery(cls):  #@NoSelf
+    def _apnSubscriptionsByKeyQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Select([apn.TOKEN, apn.SUBSCRIBER_GUID],
                       From=apn, Where=apn.RESOURCE_KEY == Parameter("resourceKey"))
@@ -725,7 +725,7 @@
 
 
     @classproperty
-    def _apnSubscriptionsBySubscriberQuery(cls):  #@NoSelf
+    def _apnSubscriptionsBySubscriberQuery(cls): #@NoSelf
         apn = schema.APN_SUBSCRIPTIONS
         return Select([apn.TOKEN, apn.RESOURCE_KEY, apn.MODIFIED, apn.USER_AGENT, apn.IP_ADDR],
                       From=apn, Where=apn.SUBSCRIBER_GUID == Parameter("subscriberGUID"))
@@ -738,7 +738,7 @@
     # Create IMIP token
 
     @classproperty
-    def _insertIMIPTokenQuery(cls):  #@NoSelf
+    def _insertIMIPTokenQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Insert({imip.TOKEN: Parameter("token"),
                        imip.ORGANIZER: Parameter("organizer"),
@@ -768,7 +768,7 @@
 
 
     @classproperty
-    def _selectIMIPTokenByTokenQuery(cls):  #@NoSelf
+    def _selectIMIPTokenByTokenQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Select([imip.ORGANIZER, imip.ATTENDEE, imip.ICALUID], From=imip,
                       Where=(imip.TOKEN == Parameter("token")))
@@ -781,7 +781,7 @@
 
 
     @classproperty
-    def _selectIMIPTokenQuery(cls):  #@NoSelf
+    def _selectIMIPTokenQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Select([imip.TOKEN], From=imip,
                       Where=(imip.ORGANIZER == Parameter("organizer")).And(
@@ -790,7 +790,7 @@
 
 
     @classproperty
-    def _updateIMIPTokenQuery(cls):  #@NoSelf
+    def _updateIMIPTokenQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Update({imip.ACCESSED: utcNowSQL, },
                       Where=(imip.ORGANIZER == Parameter("organizer")).And(
@@ -814,7 +814,7 @@
 
     # Remove IMIP token
     @classproperty
-    def _removeIMIPTokenQuery(cls):  #@NoSelf
+    def _removeIMIPTokenQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Delete(From=imip,
                       Where=(imip.TOKEN == Parameter("token")))
@@ -826,7 +826,7 @@
 
     # Purge old IMIP tokens
     @classproperty
-    def _purgeOldIMIPTokensQuery(cls):  #@NoSelf
+    def _purgeOldIMIPTokensQuery(cls): #@NoSelf
         imip = schema.IMIP_TOKENS
         return Delete(From=imip,
                       Where=(imip.ACCESSED < Parameter("olderThan")))
@@ -1356,7 +1356,7 @@
 
         count = 0
         for _ignore, managedID in results:
-            attachment = (yield ManagedAttachment.load(self, managedID))
+            attachment = (yield ManagedAttachment.load(self, None, managedID))
             yield attachment.remove()
             count += 1
         returnValue(count)
@@ -1365,21 +1365,23 @@
     def acquireUpgradeLock(self):
         return self.execSQL("select pg_advisory_lock(1)")
 
+
     def releaseUpgradeLock(self):
         return self.execSQL("select pg_advisory_unlock(1)")
 
 
+
 class _EmptyCacher(object):
 
-    def set(self, key, value):  #@UnusedVariable
+    def set(self, key, value): #@UnusedVariable
         return succeed(True)
 
 
-    def get(self, key, withIdentifier=False):  #@UnusedVariable
+    def get(self, key, withIdentifier=False): #@UnusedVariable
         return succeed(None)
 
 
-    def delete(self, key):  #@UnusedVariable
+    def delete(self, key): #@UnusedVariable
         return succeed(True)
 
 
@@ -1429,14 +1431,14 @@
 
 
     @classproperty
-    def _resourceIDFromOwnerQuery(cls):  #@NoSelf
+    def _resourceIDFromOwnerQuery(cls): #@NoSelf
         home = cls._homeSchema
         return Select([home.RESOURCE_ID],
                       From=home, Where=home.OWNER_UID == Parameter("ownerUID"))
 
 
     @classproperty
-    def _ownerFromResourceID(cls):  #@NoSelf
+    def _ownerFromResourceID(cls): #@NoSelf
         home = cls._homeSchema
         return Select([home.OWNER_UID],
                       From=home,
@@ -1444,7 +1446,7 @@
 
 
     @classproperty
-    def _metaDataQuery(cls):  #@NoSelf
+    def _metaDataQuery(cls): #@NoSelf
         metadata = cls._homeMetaDataSchema
         return Select(cls.metadataColumns(),
                       From=metadata,
@@ -1762,7 +1764,7 @@
 
 
     @classproperty
-    def _syncTokenQuery(cls):  #@NoSelf
+    def _syncTokenQuery(cls): #@NoSelf
         """
         DAL Select statement to find the sync token.
 
@@ -1826,7 +1828,7 @@
 
 
     @classproperty
-    def _changesQuery(cls):  #@NoSelf
+    def _changesQuery(cls): #@NoSelf
         bind = cls._bindSchema
         rev = cls._revisionsSchema
         return Select(
@@ -2032,12 +2034,12 @@
 
 
     @classproperty
-    def _resourceByUIDQuery(cls):  #@NoSelf
+    def _resourceByUIDQuery(cls): #@NoSelf
         return cls._objectResourceQuery(checkBindMode=False)
 
 
     @classproperty
-    def _resourceByUIDBindQuery(cls):  #@NoSelf
+    def _resourceByUIDBindQuery(cls): #@NoSelf
         return cls._objectResourceQuery(checkBindMode=True)
 
 
@@ -2098,7 +2100,7 @@
 
 
     @classproperty
-    def _quotaQuery(cls):  #@NoSelf
+    def _quotaQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Select(
             [meta.QUOTA_USED_BYTES], From=meta,
@@ -2115,7 +2117,7 @@
 
 
     @classproperty
-    def _preLockResourceIDQuery(cls):  #@NoSelf
+    def _preLockResourceIDQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Select(From=meta,
                       Where=meta.RESOURCE_ID == Parameter("resourceID"),
@@ -2123,7 +2125,7 @@
 
 
     @classproperty
-    def _increaseQuotaQuery(cls):  #@NoSelf
+    def _increaseQuotaQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Update({meta.QUOTA_USED_BYTES: meta.QUOTA_USED_BYTES +
                        Parameter("delta")},
@@ -2132,7 +2134,7 @@
 
 
     @classproperty
-    def _resetQuotaQuery(cls):  #@NoSelf
+    def _resetQuotaQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Update({meta.QUOTA_USED_BYTES: 0},
                       Where=meta.RESOURCE_ID == Parameter("resourceID"))
@@ -2198,7 +2200,7 @@
 
 
     @classproperty
-    def _lockLastModifiedQuery(cls):  #@NoSelf
+    def _lockLastModifiedQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Select(
             From=meta,
@@ -2209,7 +2211,7 @@
 
 
     @classproperty
-    def _changeLastModifiedQuery(cls):  #@NoSelf
+    def _changeLastModifiedQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
         return Update({meta.MODIFIED: utcNowSQL},
                       Where=meta.RESOURCE_ID == Parameter("resourceID"),
@@ -2276,7 +2278,7 @@
     """
 
     @classproperty
-    def _childSyncTokenQuery(cls):  #@NoSelf
+    def _childSyncTokenQuery(cls): #@NoSelf
         """
         DAL query for retrieving the sync token of a L{CommonHomeChild} based on
         its resource ID.
@@ -2309,7 +2311,7 @@
 
 
     @classproperty
-    def _objectNamesSinceRevisionQuery(cls):  #@NoSelf
+    def _objectNamesSinceRevisionQuery(cls): #@NoSelf
         """
         DAL query for (resource, deleted-flag)
         """
@@ -2363,7 +2365,7 @@
 
 
     @classproperty
-    def _removeDeletedRevision(cls):  #@NoSelf
+    def _removeDeletedRevision(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Delete(From=rev,
                       Where=(rev.HOME_RESOURCE_ID == Parameter("homeID")).And(
@@ -2371,7 +2373,7 @@
 
 
     @classproperty
-    def _addNewRevision(cls):  #@NoSelf
+    def _addNewRevision(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Insert({rev.HOME_RESOURCE_ID: Parameter("homeID"),
                        rev.RESOURCE_ID: Parameter("resourceID"),
@@ -2396,7 +2398,7 @@
 
 
     @classproperty
-    def _renameSyncTokenQuery(cls):  #@NoSelf
+    def _renameSyncTokenQuery(cls): #@NoSelf
         """
         DAL query to change sync token for a rename (increment and adjust
         resource name).
@@ -2421,7 +2423,7 @@
 
 
     @classproperty
-    def _bumpSyncTokenQuery(cls):  #@NoSelf
+    def _bumpSyncTokenQuery(cls): #@NoSelf
         """
         DAL query to change collection sync token.
         """
@@ -2444,7 +2446,7 @@
 
 
     @classproperty
-    def _deleteSyncTokenQuery(cls):  #@NoSelf
+    def _deleteSyncTokenQuery(cls): #@NoSelf
         """
         DAL query to update a sync revision to be a tombstone instead.
         """
@@ -2458,7 +2460,7 @@
 
 
     @classproperty
-    def _sharedRemovalQuery(cls):  #@NoSelf
+    def _sharedRemovalQuery(cls): #@NoSelf
         """
         DAL query to update the sync token for a shared collection.
         """
@@ -2473,7 +2475,7 @@
 
 
     @classproperty
-    def _unsharedRemovalQuery(cls):  #@NoSelf
+    def _unsharedRemovalQuery(cls): #@NoSelf
         """
         DAL query to update the sync token for an owned collection.
         """
@@ -2520,7 +2522,7 @@
 
 
     @classproperty
-    def _deleteBumpTokenQuery(cls):  #@NoSelf
+    def _deleteBumpTokenQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Update({rev.REVISION: schema.REVISION_SEQ,
                        rev.DELETED: True},
@@ -2530,7 +2532,7 @@
 
 
     @classproperty
-    def _updateBumpTokenQuery(cls):  #@NoSelf
+    def _updateBumpTokenQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Update({rev.REVISION: schema.REVISION_SEQ},
                       Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
@@ -2539,7 +2541,7 @@
 
 
     @classproperty
-    def _insertFindPreviouslyNamedQuery(cls):  #@NoSelf
+    def _insertFindPreviouslyNamedQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Select([rev.RESOURCE_ID], From=rev,
                       Where=(rev.RESOURCE_ID == Parameter("resourceID")).And(
@@ -2547,7 +2549,7 @@
 
 
     @classproperty
-    def _updatePreviouslyNamedQuery(cls):  #@NoSelf
+    def _updatePreviouslyNamedQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Update({rev.REVISION: schema.REVISION_SEQ,
                        rev.DELETED: False},
@@ -2557,7 +2559,7 @@
 
 
     @classproperty
-    def _completelyNewRevisionQuery(cls):  #@NoSelf
+    def _completelyNewRevisionQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Insert({rev.HOME_RESOURCE_ID: Parameter("homeID"),
                        rev.RESOURCE_ID: Parameter("resourceID"),
@@ -2624,7 +2626,7 @@
     """
 
     @classproperty
-    def _bindInsertQuery(cls, **kw):  #@NoSelf #@UnusedVariable
+    def _bindInsertQuery(cls, **kw): #@NoSelf #@UnusedVariable
         """
         DAL statement to create a bind entry that connects a collection to its
         home.
@@ -2641,7 +2643,7 @@
 
 
     @classmethod
-    def _updateBindColumnsQuery(cls, columnMap):  #@NoSelf
+    def _updateBindColumnsQuery(cls, columnMap): #@NoSelf
         bind = cls._bindSchema
         return Update(columnMap,
                       Where=(bind.RESOURCE_ID == Parameter("resourceID"))
@@ -2650,7 +2652,7 @@
 
 
     @classproperty
-    def _updateBindQuery(cls):  #@NoSelf
+    def _updateBindQuery(cls): #@NoSelf
         bind = cls._bindSchema
         return cls._updateBindColumnsQuery(
                     {bind.BIND_MODE: Parameter("mode"),
@@ -2659,7 +2661,7 @@
 
 
     @classproperty
-    def _deleteBindForResourceIDAndHomeID(cls):  #@NoSelf
+    def _deleteBindForResourceIDAndHomeID(cls): #@NoSelf
         bind = cls._bindSchema
         return Delete(
             From=bind,
@@ -2670,7 +2672,7 @@
 
 
     @classmethod
-    def _bindFor(cls, condition):  #@NoSelf
+    def _bindFor(cls, condition): #@NoSelf
         bind = cls._bindSchema
         columns = cls.bindColumns() + cls.additionalBindColumns()
         return Select(
@@ -2681,7 +2683,7 @@
 
 
     @classproperty
-    def _sharedBindForResourceID(cls):  #@NoSelf
+    def _sharedBindForResourceID(cls): #@NoSelf
         bind = cls._bindSchema
         return cls._bindFor((bind.RESOURCE_ID == Parameter("resourceID"))
                             .And(bind.BIND_STATUS == _BIND_STATUS_ACCEPTED)
@@ -2690,14 +2692,14 @@
 
 
     @classproperty
-    def _acceptedBindForHomeID(cls):  #@NoSelf
+    def _acceptedBindForHomeID(cls): #@NoSelf
         bind = cls._bindSchema
         return cls._bindFor((bind.HOME_RESOURCE_ID == Parameter("homeID"))
                             .And(bind.BIND_STATUS == _BIND_STATUS_ACCEPTED))
 
 
     @classproperty
-    def _unacceptedBindForResourceID(cls):  #@NoSelf
+    def _unacceptedBindForResourceID(cls): #@NoSelf
         bind = cls._bindSchema
         return cls._bindFor((bind.RESOURCE_ID == Parameter("resourceID"))
                             .And(bind.BIND_STATUS != _BIND_STATUS_ACCEPTED)
@@ -2705,7 +2707,7 @@
 
 
     @classproperty
-    def _bindForResourceIDAndHomeID(cls):  #@NoSelf
+    def _bindForResourceIDAndHomeID(cls): #@NoSelf
         """
         DAL query that looks up home bind rows by home child
         resource ID and home resource ID.
@@ -2717,7 +2719,7 @@
 
 
     @classproperty
-    def _bindForNameAndHomeID(cls):  #@NoSelf
+    def _bindForNameAndHomeID(cls): #@NoSelf
         """
         DAL query that looks up any bind rows by home child
         resource ID and home resource ID.
@@ -2946,7 +2948,7 @@
 
         result = []
         for row in acceptedRows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:self.bindColumnCount]  #@UnusedVariable
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:self.bindColumnCount] #@UnusedVariable
             home = yield self._txn.homeWithResourceID(self._home._homeType, homeID)
             new = yield home.objectWithShareUID(bindName)
             result.append(new)
@@ -2975,7 +2977,7 @@
 
         result = []
         for row in rows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:self.bindColumnCount]  #@UnusedVariable
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:self.bindColumnCount] #@UnusedVariable
             home = yield self._txn.homeWithResourceID(self._home._homeType, homeID)
             new = yield home.invitedObjectWithShareUID(bindName)
             result.append(new)
@@ -3144,7 +3146,7 @@
 
 
     @classproperty
-    def _childrenAndMetadataForHomeID(cls):  #@NoSelf
+    def _childrenAndMetadataForHomeID(cls): #@NoSelf
         bind = cls._bindSchema
         child = cls._homeChildSchema
         childMetaData = cls._homeChildMetaDataSchema
@@ -3234,7 +3236,7 @@
         self._index = None  # Derived classes need to set this
 
 
-    def memoMe(self, key, memo):  #@UnusedVariable
+    def memoMe(self, key, memo): #@UnusedVariable
         """
         Add this object to the memo dictionary in whatever fashion is appropriate.
 
@@ -3292,7 +3294,7 @@
 
         # Create the actual objects merging in properties
         for dataRow in dataRows:
-            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount]  #@UnusedVariable
+            bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
             additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
             metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
 
@@ -3361,7 +3363,7 @@
             returnValue(None)
 
         row = rows[0]
-        bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount]  #@UnusedVariable
+        bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
         if (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
         additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
@@ -3402,7 +3404,7 @@
             returnValue(None)
 
         row = rows[0]
-        bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount]  #@UnusedVariable]
+        bindMode, homeID, resourceID, bindName, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable]
         if (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
 
@@ -3413,7 +3415,7 @@
 
 
     @classproperty
-    def _insertHomeChild(cls):  #@NoSelf
+    def _insertHomeChild(cls): #@NoSelf
         """
         DAL statement to create a home child with all default values.
         """
@@ -3423,7 +3425,7 @@
 
 
     @classproperty
-    def _insertHomeChildMetaData(cls):  #@NoSelf
+    def _insertHomeChildMetaData(cls): #@NoSelf
         """
         DAL statement to create a home child with all default values.
         """
@@ -3473,7 +3475,7 @@
 
 
     @classproperty
-    def _metadataByIDQuery(cls):  #@NoSelf
+    def _metadataByIDQuery(cls): #@NoSelf
         """
         DAL query to retrieve created/modified dates based on a resource ID.
         """
@@ -3554,7 +3556,7 @@
 
 
     @classproperty
-    def _renameQuery(cls):  #@NoSelf
+    def _renameQuery(cls): #@NoSelf
         """
         DAL statement to rename a L{CommonHomeChild}
         """
@@ -3593,7 +3595,7 @@
 
 
     @classproperty
-    def _deleteQuery(cls):  #@NoSelf
+    def _deleteQuery(cls): #@NoSelf
         """
         DAL statement to delete a L{CommonHomeChild} by its resource ID.
         """
@@ -3643,7 +3645,7 @@
 
 
     @classproperty
-    def _ownerHomeWithResourceID(cls):  #@NoSelf
+    def _ownerHomeWithResourceID(cls): #@NoSelf
         """
         DAL query to retrieve the home resource ID and resource name of the owner from the bound
         home-child ID.
@@ -3684,7 +3686,7 @@
 
 
     @classproperty
-    def _objectResourceNamesQuery(cls):  #@NoSelf
+    def _objectResourceNamesQuery(cls): #@NoSelf
         """
         DAL query to load all object resource names for a home child.
         """
@@ -3703,7 +3705,7 @@
 
 
     @classproperty
-    def _objectCountQuery(cls):  #@NoSelf
+    def _objectCountQuery(cls): #@NoSelf
         """
         DAL query to count all object resources for a home child.
         """
@@ -3769,7 +3771,7 @@
 
 
     @classproperty
-    def _resourceNameForUIDQuery(cls):  #@NoSelf
+    def _resourceNameForUIDQuery(cls): #@NoSelf
         """
         DAL query to retrieve the resource name for an object resource based on
         its UID column.
@@ -3798,7 +3800,7 @@
 
 
     @classproperty
-    def _resourceUIDForNameQuery(cls):  #@NoSelf
+    def _resourceUIDForNameQuery(cls): #@NoSelf
         """
         DAL query to retrieve the UID for an object resource based on its
         resource name column.
@@ -3866,7 +3868,7 @@
 
 
     @classproperty
-    def _moveParentUpdateQuery(cls, adjustName=False):  #@NoSelf
+    def _moveParentUpdateQuery(cls, adjustName=False): #@NoSelf
         """
         DAL query to update a child to be in a new parent.
         """
@@ -3882,7 +3884,7 @@
         )
 
 
-    def _movedObjectResource(self, child, newparent):  #@UnusedVariable
+    def _movedObjectResource(self, child, newparent): #@UnusedVariable
         """
         Method that subclasses can override to do an extra DB adjustments when a resource
         is moved.
@@ -4102,7 +4104,7 @@
 
 
     @classproperty
-    def _lockLastModifiedQuery(cls):  #@NoSelf
+    def _lockLastModifiedQuery(cls): #@NoSelf
         schema = cls._homeChildMetaDataSchema
         return Select(
             From=schema,
@@ -4113,7 +4115,7 @@
 
 
     @classproperty
-    def _changeLastModifiedQuery(cls):  #@NoSelf
+    def _changeLastModifiedQuery(cls): #@NoSelf
         schema = cls._homeChildMetaDataSchema
         return Update({schema.MODIFIED: utcNowSQL},
                       Where=schema.RESOURCE_ID == Parameter("resourceID"),
@@ -4163,7 +4165,7 @@
 
     BATCH_LOAD_SIZE = 50
 
-    def __init__(self, parent, name, uid, resourceID=None, options=None):  #@UnusedVariable
+    def __init__(self, parent, name, uid, resourceID=None, options=None): #@UnusedVariable
         self._parentCollection = parent
         self._resourceID = resourceID
         self._name = name
@@ -4178,7 +4180,7 @@
 
 
     @classproperty
-    def _allColumnsWithParentQuery(cls):  #@NoSelf
+    def _allColumnsWithParentQuery(cls): #@NoSelf
         obj = cls._objectSchema
         return Select(cls._allColumns, From=obj,
                       Where=obj.PARENT_RESOURCE_ID == Parameter("parentID"))
@@ -4352,17 +4354,17 @@
 
 
     @classproperty
-    def _allColumnsWithParentAndName(cls):  #@NoSelf
+    def _allColumnsWithParentAndName(cls): #@NoSelf
         return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_NAME, "name")
 
 
     @classproperty
-    def _allColumnsWithParentAndUID(cls):  #@NoSelf
+    def _allColumnsWithParentAndUID(cls): #@NoSelf
         return cls._allColumnsWithParentAnd(cls._objectSchema.UID, "uid")
 
 
     @classproperty
-    def _allColumnsWithParentAndID(cls):  #@NoSelf
+    def _allColumnsWithParentAndID(cls): #@NoSelf
         return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_ID, "resourceID")
 
 
@@ -4398,7 +4400,7 @@
 
 
     @classproperty
-    def _allColumns(cls):  #@NoSelf
+    def _allColumns(cls): #@NoSelf
         """
         Full set of columns in the object table that need to be loaded to
         initialize the object resource state.
@@ -4497,7 +4499,7 @@
 
 
     @classmethod
-    def _selectForUpdateQuery(cls, nowait):  #@NoSelf
+    def _selectForUpdateQuery(cls, nowait): #@NoSelf
         """
         DAL statement to lock a L{CommonObjectResource} by its resource ID.
         """
@@ -4541,7 +4543,7 @@
 
 
     @classproperty
-    def _deleteQuery(cls):  #@NoSelf
+    def _deleteQuery(cls): #@NoSelf
         """
         DAL statement to delete a L{CommonObjectResource} by its resource ID.
         """
@@ -4616,7 +4618,7 @@
 
 
     @classproperty
-    def _textByIDQuery(cls):  #@NoSelf
+    def _textByIDQuery(cls): #@NoSelf
         """
         DAL query to load iCalendar/vCard text via an object's resource ID.
         """
@@ -4931,7 +4933,7 @@
 
 
     @classproperty
-    def _completelyNewRevisionQuery(cls):  #@NoSelf
+    def _completelyNewRevisionQuery(cls): #@NoSelf
         rev = cls._revisionsSchema
         return Insert({rev.HOME_RESOURCE_ID: Parameter("homeID"),
                        # rev.RESOURCE_ID: Parameter("resourceID"),
@@ -5003,7 +5005,7 @@
 
 
     @classproperty
-    def _allColumnsByHomeIDQuery(cls):  #@NoSelf
+    def _allColumnsByHomeIDQuery(cls): #@NoSelf
         """
         DAL query to load all columns by home ID.
         """
@@ -5062,7 +5064,7 @@
 
 
     @classproperty
-    def _oneNotificationQuery(cls):  #@NoSelf
+    def _oneNotificationQuery(cls): #@NoSelf
         no = cls._objectSchema
         return Select(
             [
@@ -5103,7 +5105,7 @@
             returnValue(None)
 
 
-    def _loadPropertyStore(self, props=None, created=False):  #@UnusedVariable
+    def _loadPropertyStore(self, props=None, created=False): #@UnusedVariable
         if props is None:
             props = NonePropertyStore(self._home.uid())
         self._propertyStore = props
@@ -5141,7 +5143,7 @@
 
 
     @classproperty
-    def _newNotificationQuery(cls):  #@NoSelf
+    def _newNotificationQuery(cls): #@NoSelf
         no = cls._objectSchema
         return Insert(
             {
@@ -5156,7 +5158,7 @@
 
 
     @classproperty
-    def _updateNotificationQuery(cls):  #@NoSelf
+    def _updateNotificationQuery(cls): #@NoSelf
         no = cls._objectSchema
         return Update(
             {

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -75,7 +75,7 @@
     "ALARM_VEVENT_TIMED" nclob default null,
     "ALARM_VEVENT_ALLDAY" nclob default null,
     "ALARM_VTODO_TIMED" nclob default null,
-    "ALARM_VTODO_ALLDAY" 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")
@@ -354,12 +354,18 @@
     "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
 );
 
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
 create table CALENDARSERVER (
     "NAME" nvarchar2(255) primary key,
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '22');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '23');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
 create index NOTIFICATION_NOTIFICA_f891f5f9 on NOTIFICATION (

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-07-04 02:15:52 UTC (rev 11459)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -658,6 +658,16 @@
 );
 
 
+--------------------------
+-- Object Splitter Work --
+--------------------------
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
 --------------------
 -- Schema Version --
 --------------------
@@ -667,6 +677,6 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '22');
+insert into CALENDARSERVER values ('VERSION', '23');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v22.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v22.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/oracle-dialect/v22.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -0,0 +1,457 @@
+create sequence RESOURCE_ID_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 CALENDAR_HOME (
+    "RESOURCE_ID" integer primary key,
+    "OWNER_UID" nvarchar2(255) unique,
+    "DATAVERSION" integer default 0 not null
+);
+
+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,
+    "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
+);
+
+create table NOTIFICATION (
+    "RESOURCE_ID" integer primary key,
+    "NOTIFICATION_HOME_RESOURCE_ID" integer not null references NOTIFICATION_HOME,
+    "NOTIFICATION_UID" nvarchar2(255),
+    "XML_TYPE" nvarchar2(255),
+    "XML_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,
+    "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);
+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);
+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', 
+    unique("CALENDAR_RESOURCE_ID", "RESOURCE_NAME")
+);
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MO (
+    "ID" integer primary key,
+    "DESCRIPTION" nvarchar2(16) unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('none', 0);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (DESCRIPTION, ID) values ('read', 1);
+insert into CALENDAR_OBJECT_ATTACHMENTS_MO (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 TRANSPARENCY (
+    "TIME_RANGE_INSTANCE_ID" integer not null references TIME_RANGE on delete cascade,
+    "USER_ID" nvarchar2(255),
+    "TRANSPARENT" integer not null
+);
+
+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,
+    "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_ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "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_ADDRESSBOOK_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', 
+    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 references ADDRESSBOOK_OBJECT on delete cascade,
+    "ADDRESSBOOK_ID" integer not null references ADDRESSBOOK_HOME on delete cascade,
+    "MEMBER_ID" integer not null references ADDRESSBOOK_OBJECT, 
+    primary key("GROUP_ID", "MEMBER_ID")
+);
+
+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,
+    "GROUP_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", "GROUP_RESOURCE_ID"), 
+    unique("ADDRESSBOOK_HOME_RESOURCE_ID", "GROUP_ADDRESSBOOK_RESOURCE_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
+);
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+    "ADDRESSBOOK_HOME_RESOURCE_ID" integer not null references ADDRESSBOOK_HOME,
+    "OWNER_ADDRESSBOOK_HOME_RESOURCE_ID" integer references ADDRESSBOOK_HOME,
+    "ADDRESSBOOK_NAME" nvarchar2(255) default null,
+    "RESOURCE_NAME" nvarchar2(255),
+    "REVISION" integer not null,
+    "DELETED" integer not null
+);
+
+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, 
+    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,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "FROM_ADDR" nvarchar2(255),
+    "TO_ADDR" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table IMIP_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table IMIP_REPLY_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "ORGANIZER" nvarchar2(255),
+    "ATTENDEE" nvarchar2(255),
+    "ICALENDAR_TEXT" nclob
+);
+
+create table PUSH_NOTIFICATION_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "PUSH_ID" nvarchar2(255)
+);
+
+create table GROUP_CACHER_POLLING_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
+);
+
+create table CALENDARSERVER (
+    "NAME" nvarchar2(255) primary key,
+    "VALUE" nvarchar2(255)
+);
+
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '22');
+insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
+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_96e83b73 on CALENDAR_OBJECT (
+    CALENDAR_RESOURCE_ID,
+    RECURRANCE_MAX
+);
+
+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 TRANSPARENCY_TIME_RAN_5f34467f on TRANSPARENCY (
+    TIME_RANGE_INSTANCE_ID
+);
+
+create index ATTACHMENT_CALENDAR_H_0078845c on ATTACHMENT (
+    CALENDAR_HOME_RESOURCE_ID
+);
+
+create index SHARED_ADDRESSBOOK_BI_e9a2e6d4 on SHARED_ADDRESSBOOK_BIND (
+    OWNER_ADDRESSBOOK_HOME_RESOURCE_ID
+);
+
+create index SHARED_GROUP_BIND_RES_cf52f95d on SHARED_GROUP_BIND (
+    GROUP_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_3a3956c4 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_HOME_RESOURCE_ID,
+    CALENDAR_RESOURCE_ID
+);
+
+create index CALENDAR_OBJECT_REVIS_2643d556 on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index CALENDAR_OBJECT_REVIS_265c8acf on CALENDAR_OBJECT_REVISIONS (
+    CALENDAR_RESOURCE_ID,
+    REVISION
+);
+
+create index ADDRESSBOOK_OBJECT_RE_40cc2d73 on ADDRESSBOOK_OBJECT_REVISIONS (
+    ADDRESSBOOK_HOME_RESOURCE_ID,
+    OWNER_ADDRESSBOOK_HOME_RESOURCE_ID
+);
+
+create index ADDRESSBOOK_OBJECT_RE_980b9872 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_ADDRESSBOOK_HOME_RESOURCE_ID,
+    RESOURCE_NAME
+);
+
+create index ADDRESSBOOK_OBJECT_RE_45004780 on ADDRESSBOOK_OBJECT_REVISIONS (
+    OWNER_ADDRESSBOOK_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
+);
+

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v22.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v22.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/old/postgres-dialect/v22.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -0,0 +1,667 @@
+-- -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+
+----
+-- Copyright (c) 2010-2013 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
+);
+
+
+-------------------
+-- 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
+  DATAVERSION      integer      default 0 not null
+);
+
+--------------
+-- 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,
+  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)
+);
+
+
+-----------------------
+-- 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
+);
+
+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,
+  XML_TYPE                      varchar(255) not null,
+  XML_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,
+  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');
+
+-- 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');
+
+
+-- 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_OBJECT_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),
+
+  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 on
+  CALENDAR_OBJECT(CALENDAR_RESOURCE_ID, RECURRANCE_MAX);
+
+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_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'none' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'read' );
+insert into CALENDAR_OBJECT_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'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+create index TRANSPARENCY_TIME_RANGE_INSTANCE_ID on
+  TRANSPARENCY(TIME_RANGE_INSTANCE_ID);
+
+
+----------------
+-- 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);
+
+-- 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
+);
+
+
+-----------------------
+-- 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
+  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_ADDRESSBOOK_HOME_RESOURCE_ID    integer      	not null references ADDRESSBOOK_HOME on delete cascade,
+  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_ADDRESSBOOK_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_ADDRESSBOOK_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),
+
+  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');
+
+
+---------------------------------
+-- 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
+    primary key (GROUP_ID, MEMBER_ID) -- implicit index
+);
+
+
+------------------------------------------
+-- 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
+);
+
+
+-----------------------
+-- 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,
+  GROUP_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, GROUP_RESOURCE_ID), -- implicit index
+  unique (ADDRESSBOOK_HOME_RESOURCE_ID, GROUP_ADDRESSBOOK_RESOURCE_NAME)     -- implicit index
+);
+
+create index SHARED_GROUP_BIND_RESOURCE_ID on
+  SHARED_GROUP_BIND(GROUP_RESOURCE_ID);
+
+
+---------------
+-- Revisions --
+---------------
+
+create sequence REVISION_SEQ;
+
+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
+);
+
+create index CALENDAR_OBJECT_REVISIONS_HOME_RESOURCE_ID_CALENDAR_RESOURCE_ID
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_RESOURCE_NAME
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, RESOURCE_NAME);
+
+create index CALENDAR_OBJECT_REVISIONS_RESOURCE_ID_REVISION
+  on CALENDAR_OBJECT_REVISIONS(CALENDAR_RESOURCE_ID, REVISION);
+
+
+----------------------------------
+-- AddressBook Object Revisions --
+----------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_HOME_RESOURCE_ID 			integer			not null references ADDRESSBOOK_HOME,
+  OWNER_ADDRESSBOOK_HOME_RESOURCE_ID    integer     	references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_NAME             			varchar(255) 	default null,
+  RESOURCE_NAME                			varchar(255),
+  REVISION                     			integer     	default nextval('REVISION_SEQ') not null,
+  DELETED                      			boolean      	not null
+);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_HOME_RESOURCE_ID_OWNER_ADDRESSBOOK_HOME_RESOURCE_ID
+  on ADDRESSBOOK_OBJECT_REVISIONS(ADDRESSBOOK_HOME_RESOURCE_ID, OWNER_ADDRESSBOOK_HOME_RESOURCE_ID);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_RESOURCE_NAME
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_ADDRESSBOOK_HOME_RESOURCE_ID, RESOURCE_NAME);
+
+create index ADDRESSBOOK_OBJECT_REVISIONS_OWNER_HOME_RESOURCE_ID_REVISION
+  on ADDRESSBOOK_OBJECT_REVISIONS(OWNER_ADDRESSBOOK_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,
+
+  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,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  FROM_ADDR                     varchar(255) not null,
+  TO_ADDR                       varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+
+-----------------------
+-- IMIP Polling Work --
+-----------------------
+
+create table IMIP_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+---------------------
+-- IMIP Reply Work --
+---------------------
+
+create table IMIP_REPLY_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  ORGANIZER                     varchar(255) not null,
+  ATTENDEE                      varchar(255) not null,
+  ICALENDAR_TEXT                text         not null
+);
+
+
+------------------------
+-- Push Notifications --
+------------------------
+
+create table PUSH_NOTIFICATION_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  PUSH_ID                       varchar(255) not null
+);
+
+-----------------
+-- GroupCacher --
+-----------------
+
+create table GROUP_CACHER_POLLING_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+--------------------
+-- Schema Version --
+--------------------
+
+create table CALENDARSERVER (
+  NAME                          varchar(255) primary key, -- implicit index
+  VALUE                         varchar(255)
+);
+
+insert into CALENDARSERVER values ('VERSION', '22');
+insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '5');
+insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_22_to_23.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_22_to_23.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_22_to_23.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -0,0 +1,32 @@
+----
+-- Copyright (c) 2011-2013 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 22 to 23 --
+---------------------------------------------------
+  
+-- Object Splitter Work --
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+    "WORK_ID" integer primary key not null,
+    "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
+    "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
+);
+
+
+-- update schema version
+update CALENDARSERVER set VALUE = '23' where NAME = 'VERSION';

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_22_to_23.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_22_to_23.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_22_to_23.sql	2013-07-04 02:29:51 UTC (rev 11460)
@@ -0,0 +1,32 @@
+----
+-- Copyright (c) 2011-2013 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 22 to 23 --
+---------------------------------------------------
+
+-- Object Splitter Work --
+
+create table CALENDAR_OBJECT_SPLITTER_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null,
+  NOT_BEFORE                    timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade
+);
+
+
+ -- update schema version
+update CALENDARSERVER set VALUE = '23' where NAME = 'VERSION';
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130703/03efb2f5/attachment-0001.html>


More information about the calendarserver-changes mailing list