<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[14925] CalendarServer/trunk/txdav</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/14925">14925</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-06-26 14:21:39 -0700 (Fri, 26 Jun 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Fix revision clean-up and revision MODIFIED updating.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunktxdavcaldavdatastoresqlpy">CalendarServer/trunk/txdav/caldav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy">CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcarddavdatastoresqlpy">CalendarServer/trunk/txdav/carddav/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcarddavdatastoretesttest_sqlpy">CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresqlpy">CalendarServer/trunk/txdav/common/datastore/sql.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_sharingpy">CalendarServer/trunk/txdav/common/datastore/sql_sharing.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoresql_utilpy">CalendarServer/trunk/txdav/common/datastore/sql_util.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoreworkrevision_cleanuppy">CalendarServer/trunk/txdav/common/datastore/work/revision_cleanup.py</a></li>
<li><a href="#CalendarServertrunktxdavcommondatastoreworktesttest_revision_cleanuppy">CalendarServer/trunk/txdav/common/datastore/work/test/test_revision_cleanup.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunktxdavcaldavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/sql.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -2568,11 +2568,8 @@
</span><span class="cx"> }
</span><span class="cx"> accesstype_to_accessMode = dict([(v, k) for k, v in accessMode_to_type.items()])
</span><span class="cx"> 
</span><del>-def _pathToName(path):
-    return path.rsplit(&quot;.&quot;, 1)[0]
</del><span class="cx"> 
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> class CalendarObject(CommonObjectResource, CalendarObjectBase):
</span><span class="cx">     implements(ICalendarObject)
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServertrunktxdavcaldavdatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -58,10 +58,10 @@
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
</span><span class="cx">     _BIND_STATUS_ACCEPTED, _TRANSP_OPAQUE
</span><span class="cx"> from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
</span><del>-    test_event_text
</del><ins>+    test_event_text, cal1Root
</ins><span class="cx"> from txdav.caldav.datastore.test.test_file import setUpCalendarStore
</span><span class="cx"> from txdav.common.datastore.test.util import populateCalendarsFrom, \
</span><del>-    CommonCommonTests
</del><ins>+    CommonCommonTests, updateToCurrentYear
</ins><span class="cx"> from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
</span><span class="cx"> from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar, \
</span><span class="cx">     InvalidSplit, UnknownTimezone
</span><span class="lines">@@ -1913,117 +1913,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><del>-    def test_calendarRevisionChangeConcurrency(self):
-        &quot;&quot;&quot;
-        Test that two concurrent attempts to add resources in two separate
-        calendar homes does not deadlock on the revision table update.
-        &quot;&quot;&quot;
-
-        calendarStore = self._sqlCalendarStore
-
-        # Make sure homes are provisioned
-        txn = self.transactionUnderTest()
-        home_uid1 = yield txn.homeWithUID(ECALENDARTYPE, &quot;user01&quot;, create=True)
-        home_uid2 = yield txn.homeWithUID(ECALENDARTYPE, &quot;user02&quot;, create=True)
-        self.assertNotEqual(home_uid1, None)
-        self.assertNotEqual(home_uid2, None)
-        yield self.commit()
-
-        # Create first events in different calendar homes
-        txn1 = calendarStore.newTransaction()
-        txn2 = calendarStore.newTransaction()
-
-        calendar_uid1_in_txn1 = yield self.calendarUnderTest(txn1, &quot;calendar&quot;, &quot;user01&quot;)
-        calendar_uid2_in_txn2 = yield self.calendarUnderTest(txn2, &quot;calendar&quot;, &quot;user02&quot;)
-
-        data = &quot;&quot;&quot;BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:data%(ctr)s
-DTSTART:20130102T140000Z
-DURATION:PT1H
-CREATED:20060102T190000Z
-DTSTAMP:20051222T210507Z
-SUMMARY:data%(ctr)s
-END:VEVENT
-END:VCALENDAR
-&quot;&quot;&quot;
-
-        component = Component.fromString(data % {&quot;ctr&quot;: 1})
-        yield calendar_uid1_in_txn1.createCalendarObjectWithName(&quot;data1.ics&quot;, component)
-
-        component = Component.fromString(data % {&quot;ctr&quot;: 2})
-        yield calendar_uid2_in_txn2.createCalendarObjectWithName(&quot;data2.ics&quot;, component)
-
-        # Setup deferreds to run concurrently and create second events in the calendar homes
-        # previously used by the other transaction - this could create the deadlock.
-        @inlineCallbacks
-        def _defer_uid3():
-            calendar_uid1_in_txn2 = yield self.calendarUnderTest(txn2, &quot;calendar&quot;, &quot;user01&quot;)
-            component = Component.fromString(data % {&quot;ctr&quot;: 3})
-            yield calendar_uid1_in_txn2.createCalendarObjectWithName(&quot;data3.ics&quot;, component)
-            yield txn2.commit()
-        d1 = _defer_uid3()
-
-        @inlineCallbacks
-        def _defer_uid4():
-            calendar_uid2_in_txn1 = yield self.calendarUnderTest(txn1, &quot;calendar&quot;, &quot;user02&quot;)
-            component = Component.fromString(data % {&quot;ctr&quot;: 4})
-            yield calendar_uid2_in_txn1.createCalendarObjectWithName(&quot;data4.ics&quot;, component)
-            yield txn1.commit()
-        d2 = _defer_uid4()
-
-        # Now do the concurrent provision attempt
-        yield DeferredList([d1, d2])
-
-        # Verify we did not have a deadlock and all resources have been created.
-        caldata1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
-        caldata2 = yield self.calendarObjectUnderTest(name=&quot;data2.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user02&quot;)
-        caldata3 = yield self.calendarObjectUnderTest(name=&quot;data3.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
-        caldata4 = yield self.calendarObjectUnderTest(name=&quot;data4.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user02&quot;)
-        self.assertNotEqual(caldata1, None)
-        self.assertNotEqual(caldata2, None)
-        self.assertNotEqual(caldata3, None)
-        self.assertNotEqual(caldata4, None)
-
-
-    @inlineCallbacks
-    def test_calendarMissingRevision(self):
-        &quot;&quot;&quot;
-        Test that two concurrent attempts to add resources in two separate
-        calendar homes does not deadlock on the revision table update.
-        &quot;&quot;&quot;
-
-        # Get details
-        home = yield self.homeUnderTest(name=&quot;user01&quot;, create=True)
-        self.assertNotEqual(home, None)
-        calendar = yield home.childWithName(&quot;calendar&quot;)
-        self.assertNotEqual(calendar, None)
-
-        rev = calendar._revisionsSchema
-        yield Delete(
-            From=rev,
-            Where=(
-                rev.HOME_RESOURCE_ID == Parameter(&quot;homeID&quot;)).And(
-                rev.COLLECTION_NAME == Parameter(&quot;collectionName&quot;)
-            )
-        ).on(self.transactionUnderTest(), homeID=home.id(), collectionName=&quot;calendar&quot;)
-
-        yield self.commit()
-
-        home = yield self.homeUnderTest(name=&quot;user01&quot;)
-        children = yield home.loadChildren()
-        self.assertEqual(len(children), 3)
-        yield self.commit()
-
-        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
-        token = yield calendar.syncToken()
-        self.assertTrue(token is not None)
-
-
-    @inlineCallbacks
</del><span class="cx">     def test_inboxTransp(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Make sure inbox is always transparent no matter what is stored in the DB.
</span><span class="lines">@@ -2319,7 +2208,164 @@
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
+class SyncTests(CommonCommonTests, unittest.TestCase):
+    &quot;&quot;&quot;
+    Revision table/sync report tests.
+    &quot;&quot;&quot;
+
</ins><span class="cx">     @inlineCallbacks
</span><ins>+    def setUp(self):
+        yield super(SyncTests, self).setUp()
+        yield self.buildStoreAndDirectory()
+        yield self.populate()
+
+
+    requirements = {
+        &quot;user01&quot;: {
+            &quot;calendar&quot;: {
+                &quot;1.ics&quot;: (cal1Root.child(&quot;1.ics&quot;).getContent(), CalendarCommonTests.metadata1),
+                &quot;2.ics&quot;: (cal1Root.child(&quot;2.ics&quot;).getContent(), CalendarCommonTests.metadata2),
+                &quot;3.ics&quot;: (cal1Root.child(&quot;3.ics&quot;).getContent(), CalendarCommonTests.metadata3),
+                &quot;4.ics&quot;: (cal1Root.child(&quot;4.ics&quot;).getContent(), CalendarCommonTests.metadata4),
+                &quot;5.ics&quot;: (cal1Root.child(&quot;5.ics&quot;).getContent(), CalendarCommonTests.metadata5),
+            },
+        },
+    }
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def token2revision(self, token):
+        &quot;&quot;&quot;
+        FIXME: the API names for L{syncToken}() and L{resourceNamesSinceToken}()
+        are slightly inaccurate; one doesn't produce input for the other.
+        Actually it should be resource names since I{revision} and you need to
+        understand the structure of the tokens to extract the revision.  Right
+        now that logic lives in the protocol layer, so this testing method
+        replicates it.
+        &quot;&quot;&quot;
+        _ignore_uuid, rev = token.split(&quot;_&quot;, 1)
+        rev = int(rev)
+        return rev
+
+
+    @inlineCallbacks
+    def test_calendarRevisionChangeConcurrency(self):
+        &quot;&quot;&quot;
+        Test that two concurrent attempts to add resources in two separate
+        calendar homes does not deadlock on the revision table update.
+        &quot;&quot;&quot;
+
+        calendarStore = self._sqlCalendarStore
+
+        # Make sure homes are provisioned
+        txn = self.transactionUnderTest()
+        home_uid1 = yield txn.homeWithUID(ECALENDARTYPE, &quot;user01&quot;, create=True)
+        home_uid2 = yield txn.homeWithUID(ECALENDARTYPE, &quot;user02&quot;, create=True)
+        self.assertNotEqual(home_uid1, None)
+        self.assertNotEqual(home_uid2, None)
+        yield self.commit()
+
+        # Create first events in different calendar homes
+        txn1 = calendarStore.newTransaction()
+        txn2 = calendarStore.newTransaction()
+
+        calendar_uid1_in_txn1 = yield self.calendarUnderTest(txn1, &quot;calendar&quot;, &quot;user01&quot;)
+        calendar_uid2_in_txn2 = yield self.calendarUnderTest(txn2, &quot;calendar&quot;, &quot;user02&quot;)
+
+        data = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:data%(ctr)s
+DTSTART:20130102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:data%(ctr)s
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
+        component = Component.fromString(data % {&quot;ctr&quot;: 1})
+        yield calendar_uid1_in_txn1.createCalendarObjectWithName(&quot;data1.ics&quot;, component)
+
+        component = Component.fromString(data % {&quot;ctr&quot;: 2})
+        yield calendar_uid2_in_txn2.createCalendarObjectWithName(&quot;data2.ics&quot;, component)
+
+        # Setup deferreds to run concurrently and create second events in the calendar homes
+        # previously used by the other transaction - this could create the deadlock.
+        @inlineCallbacks
+        def _defer_uid3():
+            calendar_uid1_in_txn2 = yield self.calendarUnderTest(txn2, &quot;calendar&quot;, &quot;user01&quot;)
+            component = Component.fromString(data % {&quot;ctr&quot;: 3})
+            yield calendar_uid1_in_txn2.createCalendarObjectWithName(&quot;data3.ics&quot;, component)
+            yield txn2.commit()
+        d1 = _defer_uid3()
+
+        @inlineCallbacks
+        def _defer_uid4():
+            calendar_uid2_in_txn1 = yield self.calendarUnderTest(txn1, &quot;calendar&quot;, &quot;user02&quot;)
+            component = Component.fromString(data % {&quot;ctr&quot;: 4})
+            yield calendar_uid2_in_txn1.createCalendarObjectWithName(&quot;data4.ics&quot;, component)
+            yield txn1.commit()
+        d2 = _defer_uid4()
+
+        # Now do the concurrent provision attempt
+        yield DeferredList([d1, d2])
+
+        # Verify we did not have a deadlock and all resources have been created.
+        caldata1 = yield self.calendarObjectUnderTest(name=&quot;data1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        caldata2 = yield self.calendarObjectUnderTest(name=&quot;data2.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        caldata3 = yield self.calendarObjectUnderTest(name=&quot;data3.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        caldata4 = yield self.calendarObjectUnderTest(name=&quot;data4.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user02&quot;)
+        self.assertNotEqual(caldata1, None)
+        self.assertNotEqual(caldata2, None)
+        self.assertNotEqual(caldata3, None)
+        self.assertNotEqual(caldata4, None)
+
+
+    @inlineCallbacks
+    def test_calendarMissingRevision(self):
+        &quot;&quot;&quot;
+        Test that two concurrent attempts to add resources in two separate
+        calendar homes does not deadlock on the revision table update.
+        &quot;&quot;&quot;
+
+        # Get details
+        home = yield self.homeUnderTest(name=&quot;user02&quot;, create=True)
+        self.assertNotEqual(home, None)
+        calendar = yield home.childWithName(&quot;calendar&quot;)
+        self.assertNotEqual(calendar, None)
+
+        rev = calendar._revisionsSchema
+        yield Delete(
+            From=rev,
+            Where=(
+                rev.HOME_RESOURCE_ID == Parameter(&quot;homeID&quot;)).And(
+                rev.COLLECTION_NAME == Parameter(&quot;collectionName&quot;)
+            )
+        ).on(self.transactionUnderTest(), homeID=home.id(), collectionName=&quot;calendar&quot;)
+
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name=&quot;user02&quot;)
+        children = yield home.loadChildren()
+        self.assertEqual(len(children), 3)
+        yield self.commit()
+
+        calendar = yield self.calendarUnderTest(home=&quot;user02&quot;, name=&quot;calendar&quot;)
+        token = yield calendar.syncToken()
+        self.assertTrue(token is not None)
+
+
+    @inlineCallbacks
</ins><span class="cx">     def test_removeAfterRevisionCleanup(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Make sure L{Calendar}'s can be renamed after revision cleanup
</span><span class="lines">@@ -2350,6 +2396,58 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><ins>+    def test_revisionModified(self):
+        &quot;&quot;&quot;
+        Make sure the revision table MODIFIED value changes for an update or delete
+        &quot;&quot;&quot;
+
+        @inlineCallbacks
+        def _getModified():
+            home = yield self.homeUnderTest(name=&quot;user01&quot;)
+            calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+            rev = calendar._revisionsSchema
+            modified = yield Select(
+                [rev.MODIFIED, ],
+                From=rev,
+                Where=(
+                    rev.HOME_RESOURCE_ID == Parameter(&quot;homeID&quot;)).And(
+                    rev.CALENDAR_RESOURCE_ID == Parameter(&quot;collectionID&quot;)).And(
+                    rev.RESOURCE_NAME == Parameter(&quot;resourceName&quot;)
+                )
+            ).on(
+                home._txn,
+                homeID=home.id(),
+                collectionID=calendar.id(),
+                resourceName=&quot;1.ics&quot;,
+            )
+            yield self.commit()
+            returnValue(modified[0][0])
+
+        # Get current modified
+        old_modified = yield _getModified()
+        self.assertNotEqual(old_modified, None)
+
+        # Update resource
+        cobj = yield self.calendarObjectUnderTest(home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;1.ics&quot;)
+        yield cobj.setComponent(Component.fromString(updateToCurrentYear(cal1Root.child(&quot;1.ics&quot;).getContent())))
+        yield self.commit()
+
+        # Modified changed
+        update_modified = yield _getModified()
+        self.assertGreater(update_modified, old_modified)
+
+        # Delete resource
+        cobj = yield self.calendarObjectUnderTest(home=&quot;user01&quot;, calendar_name=&quot;calendar&quot;, name=&quot;1.ics&quot;)
+        yield cobj.remove()
+        yield self.commit()
+
+        # Modified changed
+        delete_modified = yield _getModified()
+        self.assertGreater(delete_modified, old_modified)
+        self.assertGreater(delete_modified, update_modified)
+
+
+    @inlineCallbacks
</ins><span class="cx">     def test_homeSyncTokenWithTrash_Visible(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         L{ICalendarHome.resourceNamesSinceToken} will return the names of
</span><span class="lines">@@ -2360,8 +2458,8 @@
</span><span class="cx">         self.patch(config, &quot;EnableTrashCollection&quot;, True)
</span><span class="cx">         self.patch(config, &quot;ExposeTrashCollection&quot;, True)
</span><span class="cx"> 
</span><del>-        home = yield self.homeUnderTest()
-        cal = yield self.calendarUnderTest()
</del><ins>+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        cal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
</ins><span class="cx">         st = yield home.syncToken()
</span><span class="cx">         yield cal.createCalendarObjectWithName(&quot;new.ics&quot;, Component.fromString(
</span><span class="cx">             test_event_text
</span><span class="lines">@@ -2373,12 +2471,12 @@
</span><span class="cx">         st2 = yield home.syncToken()
</span><span class="cx">         self.failIfEquals(st, st2)
</span><span class="cx"> 
</span><del>-        home = yield self.homeUnderTest()
</del><ins>+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
</ins><span class="cx"> 
</span><span class="cx">         expected = [
</span><del>-            &quot;calendar_1/&quot;,
-            &quot;calendar_1/new.ics&quot;,
-            &quot;calendar_1/2.ics&quot;,
</del><ins>+            &quot;calendar/&quot;,
+            &quot;calendar/new.ics&quot;,
+            &quot;calendar/2.ics&quot;,
</ins><span class="cx">             &quot;other-calendar/&quot;
</span><span class="cx">         ]
</span><span class="cx"> 
</span><span class="lines">@@ -2394,7 +2492,7 @@
</span><span class="cx">             self.token2revision(st), &quot;infinity&quot;)
</span><span class="cx"> 
</span><span class="cx">         self.assertEquals(set(changed), set(expected))
</span><del>-        self.assertEquals(set(deleted), set([&quot;calendar_1/2.ics&quot;]))
</del><ins>+        self.assertEquals(set(deleted), set([&quot;calendar/2.ics&quot;]))
</ins><span class="cx">         self.assertEquals(invalid, [])
</span><span class="cx"> 
</span><span class="cx">         changed, deleted, invalid = yield home.resourceNamesSinceToken(
</span><span class="lines">@@ -2414,8 +2512,8 @@
</span><span class="cx"> 
</span><span class="cx">         self.patch(config, &quot;EnableTrashCollection&quot;, True)
</span><span class="cx"> 
</span><del>-        home = yield self.homeUnderTest()
-        cal = yield self.calendarUnderTest()
</del><ins>+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        cal = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
</ins><span class="cx">         st = yield home.syncToken()
</span><span class="cx">         yield cal.createCalendarObjectWithName(&quot;new.ics&quot;, Component.fromString(
</span><span class="cx">             test_event_text
</span><span class="lines">@@ -2427,12 +2525,12 @@
</span><span class="cx">         st2 = yield home.syncToken()
</span><span class="cx">         self.failIfEquals(st, st2)
</span><span class="cx"> 
</span><del>-        home = yield self.homeUnderTest()
</del><ins>+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
</ins><span class="cx"> 
</span><span class="cx">         expected = [
</span><del>-            &quot;calendar_1/&quot;,
-            &quot;calendar_1/new.ics&quot;,
-            &quot;calendar_1/2.ics&quot;,
</del><ins>+            &quot;calendar/&quot;,
+            &quot;calendar/new.ics&quot;,
+            &quot;calendar/2.ics&quot;,
</ins><span class="cx">             &quot;other-calendar/&quot;
</span><span class="cx">         ]
</span><span class="cx"> 
</span><span class="lines">@@ -2440,7 +2538,7 @@
</span><span class="cx">             self.token2revision(st), &quot;infinity&quot;)
</span><span class="cx"> 
</span><span class="cx">         self.assertEquals(set(changed), set(expected))
</span><del>-        self.assertEquals(set(deleted), set([&quot;calendar_1/2.ics&quot;]))
</del><ins>+        self.assertEquals(set(deleted), set([&quot;calendar/2.ics&quot;]))
</ins><span class="cx">         self.assertEquals(invalid, [])
</span><span class="cx"> 
</span><span class="cx">         changed, deleted, invalid = yield home.resourceNamesSinceToken(
</span></span></pre></div>
<a id="CalendarServertrunktxdavcarddavdatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/carddav/datastore/sql.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -570,7 +570,8 @@
</span><span class="cx">             {
</span><span class="cx">                 rev.REVISION: schema.REVISION_SEQ,
</span><span class="cx">                 rev.OBJECT_RESOURCE_ID: Parameter(&quot;id&quot;),
</span><del>-                rev.DELETED: True
</del><ins>+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
</ins><span class="cx">             },
</span><span class="cx">             Where=(
</span><span class="cx">                 rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span></span></pre></div>
<a id="CalendarServertrunktxdavcarddavdatastoretesttest_sqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_sql.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -28,20 +28,21 @@
</span><span class="cx"> from twisted.trial import unittest
</span><span class="cx"> 
</span><span class="cx"> from twistedcaldav import carddavxml
</span><del>-from twistedcaldav.vcard import Component as VCard
</del><ins>+from twistedcaldav.vcard import Component as VCard, Component
</ins><span class="cx"> from twistedcaldav.vcard import Component as VComponent
</span><span class="cx"> 
</span><span class="cx"> from txdav.base.propertystore.base import PropertyName
</span><span class="cx"> 
</span><span class="cx"> from txdav.carddav.datastore.test.common import CommonTests as AddressBookCommonTests, \
</span><del>-    vcard4_text
</del><ins>+    vcard4_text, adbk1Root
</ins><span class="cx"> from txdav.carddav.datastore.test.test_file import setUpAddressBookStore
</span><span class="cx"> from txdav.carddav.datastore.util import _migrateAddressbook, migrateHome
</span><span class="cx"> 
</span><span class="cx"> from txdav.common.icommondatastore import NoSuchObjectResourceError
</span><span class="cx"> from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
</span><span class="cx"> from txdav.common.datastore.sql_tables import _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
</span><del>-from txdav.common.datastore.test.util import cleanStore
</del><ins>+from txdav.common.datastore.test.util import cleanStore, CommonCommonTests, \
+    populateAddressBooksFrom
</ins><span class="cx"> from txdav.carddav.datastore.sql import AddressBook
</span><span class="cx"> 
</span><span class="cx"> from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
</span><span class="lines">@@ -919,7 +920,38 @@
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
+class SyncTests(CommonCommonTests, unittest.TestCase):
+    &quot;&quot;&quot;
+    Revision table/sync report tests.
+    &quot;&quot;&quot;
+
</ins><span class="cx">     @inlineCallbacks
</span><ins>+    def setUp(self):
+        yield super(SyncTests, self).setUp()
+        yield self.buildStoreAndDirectory()
+        yield self.populate()
+
+
+    requirements = {
+        &quot;user01&quot;: {
+            &quot;addressbook&quot;: {
+                &quot;1.vcf&quot;: adbk1Root.child(&quot;1.vcf&quot;).getContent(),
+                &quot;2.vcf&quot;: adbk1Root.child(&quot;2.vcf&quot;).getContent(),
+                &quot;3.vcf&quot;: adbk1Root.child(&quot;3.vcf&quot;).getContent(),
+            },
+            &quot;not_a_addressbook&quot;: None
+        },
+    }
+
+
+    @inlineCallbacks
+    def populate(self):
+        yield populateAddressBooksFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    @inlineCallbacks
</ins><span class="cx">     def test_updateAfterRevisionCleanup(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Make sure L{AddressBookObject}'s can be updated or removed after revision cleanup
</span><span class="lines">@@ -957,26 +989,26 @@
</span><span class="cx"> END:VCARD
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        yield self.homeUnderTest()
-        adbk = yield self.addressbookUnderTest(name=&quot;addressbook&quot;)
</del><ins>+        yield self.addressbookHomeUnderTest(name=&quot;user01&quot;)
+        adbk = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
</ins><span class="cx">         yield adbk.createAddressBookObjectWithName(&quot;person.vcf&quot;, VCard.fromString(person))
</span><span class="cx">         yield adbk.createAddressBookObjectWithName(&quot;group.vcf&quot;, VCard.fromString(group))
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Remove the revision
</span><del>-        adbk = yield self.addressbookUnderTest(name=&quot;addressbook&quot;)
</del><ins>+        adbk = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
</ins><span class="cx">         yield adbk.syncToken()
</span><span class="cx">         yield self.transactionUnderTest().deleteRevisionsBefore(adbk._syncTokenRevision + 1)
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Update the object
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         yield obj.setComponent(VCard.fromString(group_update))
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         self.assertTrue(obj is not None)
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;person.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;person.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         self.assertTrue(obj is not None)
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="lines">@@ -1010,26 +1042,76 @@
</span><span class="cx"> END:VCARD
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><del>-        yield self.homeUnderTest()
-        adbk = yield self.addressbookUnderTest(name=&quot;addressbook&quot;)
</del><ins>+        yield self.addressbookHomeUnderTest(name=&quot;user01&quot;)
+        adbk = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
</ins><span class="cx">         yield adbk.createAddressBookObjectWithName(&quot;person.vcf&quot;, VCard.fromString(person))
</span><span class="cx">         yield adbk.createAddressBookObjectWithName(&quot;group.vcf&quot;, VCard.fromString(group))
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Remove the revision
</span><del>-        adbk = yield self.addressbookUnderTest(name=&quot;addressbook&quot;)
</del><ins>+        adbk = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
</ins><span class="cx">         yield adbk.syncToken()
</span><span class="cx">         yield self.transactionUnderTest().deleteRevisionsBefore(adbk._syncTokenRevision + 1)
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><span class="cx">         # Remove the object
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         self.assertTrue(obj is not None)
</span><span class="cx">         yield obj.remove()
</span><span class="cx">         yield self.commit()
</span><span class="cx"> 
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;group.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         self.assertTrue(obj is None)
</span><del>-        obj = yield self.addressbookObjectUnderTest(name=&quot;person.vcf&quot;, addressbook_name=&quot;addressbook&quot;)
</del><ins>+        obj = yield self.addressbookObjectUnderTest(name=&quot;person.vcf&quot;, addressbook_name=&quot;addressbook&quot;, home=&quot;user01&quot;)
</ins><span class="cx">         self.assertTrue(obj is not None)
</span><span class="cx">         yield self.commit()
</span><ins>+
+
+    @inlineCallbacks
+    def test_revisionModified(self):
+        &quot;&quot;&quot;
+        Make sure the revision table MODIFIED value changes for an update or delete
+        &quot;&quot;&quot;
+
+        @inlineCallbacks
+        def _getModified():
+            home = yield self.addressbookHomeUnderTest(name=&quot;user01&quot;)
+            addressbook = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
+            rev = addressbook._revisionsSchema
+            modified = yield Select(
+                [rev.MODIFIED, ],
+                From=rev,
+                Where=(
+                    rev.ADDRESSBOOK_HOME_RESOURCE_ID == Parameter(&quot;homeID&quot;)).And(
+                    rev.RESOURCE_NAME == Parameter(&quot;resourceName&quot;)
+                )
+            ).on(
+                home._txn,
+                homeID=home.id(),
+                resourceName=&quot;1.vcf&quot;,
+            )
+            yield self.commit()
+            returnValue(modified[0][0])
+
+        # Get current modified
+        old_modified = yield _getModified()
+        self.assertNotEqual(old_modified, None)
+
+        # Update resource
+        aobj = yield self.addressbookObjectUnderTest(home=&quot;user01&quot;, addressbook_name=&quot;addressbook&quot;, name=&quot;1.vcf&quot;)
+        yield aobj.setComponent(Component.fromString(adbk1Root.child(&quot;1.vcf&quot;).getContent()))
+        yield self.commit()
+
+        # Modified changed
+        update_modified = yield _getModified()
+        self.assertGreater(update_modified, old_modified)
+
+        # Delete resource
+        aobj = yield self.addressbookObjectUnderTest(home=&quot;user01&quot;, addressbook_name=&quot;addressbook&quot;, name=&quot;1.vcf&quot;)
+        yield aobj.remove()
+        yield self.commit()
+
+        # Modified changed
+        delete_modified = yield _getModified()
+        self.assertGreater(delete_modified, old_modified)
+        self.assertGreater(delete_modified, update_modified)
</ins></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresqlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -3409,8 +3409,7 @@
</span><span class="cx">             )
</span><span class="cx"> 
</span><span class="cx">             # Get revisions
</span><del>-            revisions = (yield cls._revisionsForResourceIDs(childResourceIDs).on(home._txn, resourceIDs=childResourceIDs))
-            revisions = dict(revisions)
</del><ins>+            revisions = yield cls.childSyncTokenRevisions(home, childResourceIDs)
</ins><span class="cx"> 
</span><span class="cx">         # Create the actual objects merging in properties
</span><span class="cx">         for dataRow in dataRows:
</span><span class="lines">@@ -3421,7 +3420,7 @@
</span><span class="cx">             propstore = propertyStores.get(resourceID, None)
</span><span class="cx"> 
</span><span class="cx">             child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
</span><del>-            child._syncTokenRevision = revisions.get(resourceID, 0)
</del><ins>+            child._syncTokenRevision = revisions.get(resourceID, None)
</ins><span class="cx">             results.append(child)
</span><span class="cx"> 
</span><span class="cx">         returnValue(results)
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_sharingpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql_sharing.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_sharing.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/common/datastore/sql_sharing.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -19,7 +19,7 @@
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> 
</span><span class="cx"> from twext.enterprise.dal.syntax import Insert, Parameter, Update, Delete, \
</span><del>-    Select, Max
</del><ins>+    Select
</ins><span class="cx"> from twext.python.clsprop import classproperty
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> 
</span><span class="lines">@@ -1451,18 +1451,6 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    @classmethod
-    def _revisionsForResourceIDs(cls, resourceIDs):
-        rev = cls._revisionsSchema
-        return Select(
-            [rev.RESOURCE_ID, Max(rev.REVISION)],
-            From=rev,
-            Where=rev.RESOURCE_ID.In(Parameter(&quot;resourceIDs&quot;, len(resourceIDs))).And(
-                (rev.RESOURCE_NAME != None).Or(rev.DELETED == False)),
-            GroupBy=rev.RESOURCE_ID
-        )
-
-
</del><span class="cx">     @inlineCallbacks
</span><span class="cx">     def invalidateQueryCache(self):
</span><span class="cx">         queryCacher = self._txn._queryCacher
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoresql_utilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/sql_util.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/sql_util.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/common/datastore/sql_util.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -16,7 +16,7 @@
</span><span class="cx"> ##
</span><span class="cx"> 
</span><span class="cx"> from twext.enterprise.dal.syntax import Max, Select, Parameter, Delete, Insert, \
</span><del>-    Update, ColumnSyntax, TableSyntax, Upper
</del><ins>+    Update, ColumnSyntax, TableSyntax, Upper, utcNowSQL
</ins><span class="cx"> from twext.python.clsprop import classproperty
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from twisted.internet.defer import succeed, inlineCallbacks, returnValue
</span><span class="lines">@@ -66,6 +66,18 @@
</span><span class="cx">                       Where=rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @classmethod
+    def _revisionsForResourceIDs(cls, resourceIDs):
+        rev = cls._revisionsSchema
+        return Select(
+            [rev.RESOURCE_ID, Max(rev.REVISION)],
+            From=rev,
+            Where=rev.RESOURCE_ID.In(Parameter(&quot;resourceIDs&quot;, len(resourceIDs))).And(
+                (rev.RESOURCE_NAME != None).Or(rev.DELETED == False)),
+            GroupBy=rev.RESOURCE_ID
+        )
+
+
</ins><span class="cx">     def revisionFromToken(self, token):
</span><span class="cx">         if token is None:
</span><span class="cx">             return 0
</span><span class="lines">@@ -91,6 +103,21 @@
</span><span class="cx">         returnValue(revision)
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    @classmethod
+    @inlineCallbacks
+    def childSyncTokenRevisions(cls, home, childResourceIDs):
+        rows = (yield cls._revisionsForResourceIDs(childResourceIDs).on(home._txn, resourceIDs=childResourceIDs))
+        revisions = dict(rows)
+
+        # Add in any that were missing - this assumes that childResourceIDs were all valid to begin with
+        missingIDs = set(childResourceIDs) - set(revisions.keys())
+        if missingIDs:
+            min_revision = int((yield home._txn.calendarserverValue(&quot;MIN-VALID-REVISION&quot;)))
+            for resourceID in missingIDs:
+                revisions[resourceID] = min_revision
+        returnValue(revisions)
+
+
</ins><span class="cx">     def objectResourcesSinceToken(self, token):
</span><span class="cx">         raise NotImplementedError()
</span><span class="cx"> 
</span><span class="lines">@@ -206,7 +233,8 @@
</span><span class="cx">         return Update(
</span><span class="cx">             {
</span><span class="cx">                 rev.REVISION: schema.REVISION_SEQ,
</span><del>-                rev.COLLECTION_NAME: Parameter(&quot;name&quot;)
</del><ins>+                rev.COLLECTION_NAME: Parameter(&quot;name&quot;),
+                rev.MODIFIED: utcNowSQL,
</ins><span class="cx">             },
</span><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And
</span><span class="cx">                   (rev.RESOURCE_NAME == None),
</span><span class="lines">@@ -233,7 +261,10 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         rev = cls._revisionsSchema
</span><span class="cx">         return Update(
</span><del>-            {rev.REVISION: schema.REVISION_SEQ, },
</del><ins>+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.MODIFIED: utcNowSQL,
+            },
</ins><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And
</span><span class="cx">                   (rev.RESOURCE_NAME == None)
</span><span class="cx">         )
</span><span class="lines">@@ -276,7 +307,8 @@
</span><span class="cx">             {
</span><span class="cx">                 rev.RESOURCE_ID: None,
</span><span class="cx">                 rev.REVISION: schema.REVISION_SEQ,
</span><del>-                rev.DELETED: True
</del><ins>+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
</ins><span class="cx">             },
</span><span class="cx">             Where=(rev.HOME_RESOURCE_ID == Parameter(&quot;homeID&quot;)).And(
</span><span class="cx">                 rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span><span class="lines">@@ -294,7 +326,8 @@
</span><span class="cx">             {
</span><span class="cx">                 rev.RESOURCE_ID: None,
</span><span class="cx">                 rev.REVISION: schema.REVISION_SEQ,
</span><del>-                rev.DELETED: True
</del><ins>+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
</ins><span class="cx">             },
</span><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span><span class="cx">                 rev.RESOURCE_NAME == None),
</span><span class="lines">@@ -346,7 +379,11 @@
</span><span class="cx">     def _deleteBumpTokenQuery(cls):
</span><span class="cx">         rev = cls._revisionsSchema
</span><span class="cx">         return Update(
</span><del>-            {rev.REVISION: schema.REVISION_SEQ, rev.DELETED: True},
</del><ins>+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.DELETED: True,
+                rev.MODIFIED: utcNowSQL,
+            },
</ins><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span><span class="cx">                 rev.RESOURCE_NAME == Parameter(&quot;name&quot;)),
</span><span class="cx">             Return=rev.REVISION
</span><span class="lines">@@ -357,7 +394,10 @@
</span><span class="cx">     def _updateBumpTokenQuery(cls):
</span><span class="cx">         rev = cls._revisionsSchema
</span><span class="cx">         return Update(
</span><del>-            {rev.REVISION: schema.REVISION_SEQ},
</del><ins>+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.MODIFIED: utcNowSQL,
+            },
</ins><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span><span class="cx">                 rev.RESOURCE_NAME == Parameter(&quot;name&quot;)),
</span><span class="cx">             Return=rev.REVISION
</span><span class="lines">@@ -379,7 +419,11 @@
</span><span class="cx">     def _updatePreviouslyNamedQuery(cls):
</span><span class="cx">         rev = cls._revisionsSchema
</span><span class="cx">         return Update(
</span><del>-            {rev.REVISION: schema.REVISION_SEQ, rev.DELETED: False},
</del><ins>+            {
+                rev.REVISION: schema.REVISION_SEQ,
+                rev.DELETED: False,
+                rev.MODIFIED: utcNowSQL,
+            },
</ins><span class="cx">             Where=(rev.RESOURCE_ID == Parameter(&quot;resourceID&quot;)).And(
</span><span class="cx">                 rev.RESOURCE_NAME == Parameter(&quot;name&quot;)),
</span><span class="cx">             Return=rev.REVISION
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoreworkrevision_cleanuppy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/work/revision_cleanup.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/work/revision_cleanup.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/common/datastore/work/revision_cleanup.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -53,6 +53,10 @@
</span><span class="cx">         return float(config.RevisionCleanup.CleanupPeriodDays) * 24 * 60 * 60
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def dateCutoff(self):
+        return datetime.datetime.utcnow() - datetime.timedelta(days=float(config.RevisionCleanup.SyncTokenLifetimeDays))
+
+
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def doWork(self):
</span><span class="cx"> 
</span><span class="lines">@@ -60,10 +64,7 @@
</span><span class="cx">         minValidRevision = int((yield self.transaction.calendarserverValue(&quot;MIN-VALID-REVISION&quot;)))
</span><span class="cx"> 
</span><span class="cx">         # get max revision on table rows before dateLimit
</span><del>-        dateLimit = (
-            datetime.datetime.utcnow() -
-            datetime.timedelta(days=float(config.RevisionCleanup.SyncTokenLifetimeDays))
-        )
</del><ins>+        dateLimit = self.dateCutoff()
</ins><span class="cx">         maxRevOlderThanDate = 0
</span><span class="cx"> 
</span><span class="cx">         # TODO: Use one Select statement
</span></span></pre></div>
<a id="CalendarServertrunktxdavcommondatastoreworktesttest_revision_cleanuppy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/common/datastore/work/test/test_revision_cleanup.py (14924 => 14925)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/common/datastore/work/test/test_revision_cleanup.py        2015-06-26 20:56:38 UTC (rev 14924)
+++ CalendarServer/trunk/txdav/common/datastore/work/test/test_revision_cleanup.py        2015-06-26 21:21:39 UTC (rev 14925)
</span><span class="lines">@@ -23,12 +23,14 @@
</span><span class="cx"> from twisted.internet.defer import inlineCallbacks, returnValue
</span><span class="cx"> from twisted.trial.unittest import TestCase
</span><span class="cx"> from twistedcaldav.config import config
</span><ins>+from twistedcaldav.ical import Component
</ins><span class="cx"> from twistedcaldav.vcard import Component as VCard
</span><span class="cx"> from txdav.common.datastore.sql_tables import schema, _BIND_MODE_READ
</span><span class="cx"> from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
</span><span class="cx"> from txdav.common.datastore.work.revision_cleanup import FindMinValidRevisionWork, RevisionCleanupWork
</span><span class="cx"> from txdav.common.icommondatastore import SyncTokenValidException
</span><span class="cx"> import datetime
</span><ins>+import time
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -88,6 +90,21 @@
</span><span class="cx"> END:VCALENDAR
</span><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+    cal1_mod = &quot;&quot;&quot;BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:20131122T140000
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+SUMMARY:event 1.1
+END:VEVENT
+END:VCALENDAR
+&quot;&quot;&quot;
+
</ins><span class="cx">     cal2 = &quot;&quot;&quot;BEGIN:VCALENDAR
</span><span class="cx"> VERSION:2.0
</span><span class="cx"> CALSCALE:GREGORIAN
</span><span class="lines">@@ -242,6 +259,10 @@
</span><span class="cx">         Verify that all extra calendar object revisions are deleted by FindMinValidRevisionWork and RevisionCleanupWork
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><ins>+        # get home sync token
+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        hometoken = yield home.syncToken()
+
</ins><span class="cx">         # get sync token
</span><span class="cx">         calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
</span><span class="cx">         token = yield calendar.syncToken()
</span><span class="lines">@@ -267,7 +288,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Get the minimum valid revision and check it
</span><span class="cx">         minValidRevision = yield self.transactionUnderTest().calendarserverValue(&quot;MIN-VALID-REVISION&quot;)
</span><del>-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
</del><ins>+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
</ins><span class="cx"> 
</span><span class="cx">         # do RevisionCleanupWork
</span><span class="cx">         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
</span><span class="lines">@@ -280,14 +301,111 @@
</span><span class="cx">             [rev.REVISION],
</span><span class="cx">             From=rev,
</span><span class="cx">         ).on(self.transactionUnderTest())
</span><del>-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
</del><ins>+        self.assertEqual(len(revisionRows), 0)
</ins><span class="cx"> 
</span><span class="cx">         # old sync token fails
</span><span class="cx">         calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
</span><span class="cx">         yield self.failUnlessFailure(calendar.resourceNamesSinceToken(token), SyncTokenValidException)
</span><ins>+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        # old sync token fails
+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        yield self.failUnlessFailure(home.resourceNamesSinceToken(hometoken, 1), SyncTokenValidException)
+        yield self.commit()
</ins><span class="cx"> 
</span><ins>+        # calendar sync token changed
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        newtoken = yield calendar.syncToken()
+        self.assertGreater(newtoken, token)
+        yield self.commit()
+
+        # home sync token changed
+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        newhometoken = yield home.syncToken()
+        self.assertGreater(newhometoken, hometoken)
+        yield self.commit()
+
+        # Depth:1 tokens match
+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        yield home.loadChildren()
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        newtoken1 = yield calendar.syncToken()
+        self.assertEqual(newtoken1, newtoken)
+        yield self.commit()
+
+
</ins><span class="cx">     @inlineCallbacks
</span><ins>+    def test_calendarObjectRevisions_Modified(self):
+        &quot;&quot;&quot;
+        Verify that a calendar object created before the revision cut-off, but modified after it is correctly reported as changed
+        after revision clean-up
+        &quot;&quot;&quot;
+
+        # Need to add one non-event change that creates a revision after the last event change revisions in order
+        # for the logic in this test to work correctly
+        home = yield self.homeUnderTest(name=&quot;user01&quot;)
+        yield home.createCalendarWithName(&quot;_ignore_me&quot;)
+        yield self.commit()
+
+        # get initial sync token
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        initial_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # Pause to give some space in the modified time
+        time.sleep(1)
+        modified = datetime.datetime.utcnow()
+        time.sleep(1)
+
+        # Patch the work item to use the modified cut-off we need
+        def _dateCutoff(self):
+            return modified
+        self.patch(FindMinValidRevisionWork, &quot;dateCutoff&quot;, _dateCutoff)
+
+        # Make a change to get a pre-update token
+        cal2Object = yield self.calendarObjectUnderTest(self.transactionUnderTest(), name=&quot;cal2.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        yield cal2Object.remove()
+        yield self.commit()
+
+        # get changed sync token
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        pre_update_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # make changes
+        cal1Object = yield self.calendarObjectUnderTest(self.transactionUnderTest(), name=&quot;cal1.ics&quot;, calendar_name=&quot;calendar&quot;, home=&quot;user01&quot;)
+        yield cal1Object.setComponent(Component.fromString(self.cal1_mod))
+        yield self.commit()
+
+        # get changed sync token
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        update_token = yield calendar.syncToken()
+        yield self.commit()
+
+        # do FindMinValidRevisionWork and RevisionCleanupWork
+        yield FindMinValidRevisionWork.reschedule(self.transactionUnderTest(), 0)
+        yield self.commit()
+        yield JobItem.waitEmpty(self.storeUnderTest().newTransaction, reactor, 60)
+
+        # initial sync token fails
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        yield self.failUnlessFailure(calendar.resourceNamesSinceToken(initial_token), SyncTokenValidException)
+        yield self.commit()
+
+        # Pre-update sync token returns one item
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        names = yield calendar.resourceNamesSinceToken(pre_update_token)
+        self.assertEqual(names, (['cal1.ics'], [], []))
+        yield self.commit()
+
+        # Post-update sync token returns one item
+        calendar = yield self.calendarUnderTest(home=&quot;user01&quot;, name=&quot;calendar&quot;)
+        names = yield calendar.resourceNamesSinceToken(update_token)
+        self.assertEqual(names, ([], [], []))
+        yield self.commit()
+
+
+    @inlineCallbacks
</ins><span class="cx">     def test_notificationObjectRevisions(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Verify that all extra notification object revisions are deleted by FindMinValidRevisionWork and RevisionCleanupWork
</span><span class="lines">@@ -315,7 +433,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Get the minimum valid revision and check it
</span><span class="cx">         minValidRevision = yield self.transactionUnderTest().calendarserverValue(&quot;MIN-VALID-REVISION&quot;)
</span><del>-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
</del><ins>+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
</ins><span class="cx"> 
</span><span class="cx">         # do RevisionCleanupWork
</span><span class="cx">         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
</span><span class="lines">@@ -328,7 +446,7 @@
</span><span class="cx">             [rev.REVISION],
</span><span class="cx">             From=rev,
</span><span class="cx">         ).on(self.transactionUnderTest())
</span><del>-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
</del><ins>+        self.assertEqual(len(revisionRows), 0)
</ins><span class="cx"> 
</span><span class="cx">         # old sync token fails
</span><span class="cx">         home = yield self.homeUnderTest(name=&quot;user01&quot;)
</span><span class="lines">@@ -367,7 +485,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Get the minimum valid revision and check it
</span><span class="cx">         minValidRevision = yield self.transactionUnderTest().calendarserverValue(&quot;MIN-VALID-REVISION&quot;)
</span><del>-        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]))
</del><ins>+        self.assertEqual(int(minValidRevision), max([row[0] for row in revisionRows]) + 1)
</ins><span class="cx"> 
</span><span class="cx">         # do RevisionCleanupWork
</span><span class="cx">         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
</span><span class="lines">@@ -380,7 +498,7 @@
</span><span class="cx">             [rev.REVISION],
</span><span class="cx">             From=rev,
</span><span class="cx">         ).on(self.transactionUnderTest())
</span><del>-        self.assertEqual(len(revisionRows), 1)  # deleteRevisionsBefore() leaves 1 revision behind
</del><ins>+        self.assertEqual(len(revisionRows), 0)
</ins><span class="cx"> 
</span><span class="cx">         # old sync token fails
</span><span class="cx">         addressbook = yield self.addressbookUnderTest(home=&quot;user01&quot;, name=&quot;addressbook&quot;)
</span><span class="lines">@@ -440,7 +558,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Get the minimum valid revision and check it
</span><span class="cx">         minValidRevision = yield self.transactionUnderTest().calendarserverValue(&quot;MIN-VALID-REVISION&quot;)
</span><del>-        self.assertEqual(int(minValidRevision), max([row[3] for row in group1Rows + group2Rows]))
</del><ins>+        self.assertEqual(int(minValidRevision), max([row[3] for row in group1Rows + group2Rows]) + 1)
</ins><span class="cx"> 
</span><span class="cx">         # do RevisionCleanupWork
</span><span class="cx">         yield self.transactionUnderTest().enqueue(RevisionCleanupWork, notBefore=datetime.datetime.utcnow())
</span></span></pre>
</div>
</div>

</body>
</html>