[CalendarServer-changes] [13868] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Aug 12 13:45:40 PDT 2014


Revision: 13868
          http://trac.calendarserver.org//changeset/13868
Author:   sagen at apple.com
Date:     2014-08-12 13:45:40 -0700 (Tue, 12 Aug 2014)
Log Message:
-----------
Added support for fetching delegate assignments from the directory

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tools/principals.py
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/requirements-stable.txt
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/trunk/txdav/dps/client.py
    CalendarServer/trunk/txdav/dps/commands.py
    CalendarServer/trunk/txdav/dps/json.py
    CalendarServer/trunk/txdav/dps/server.py
    CalendarServer/trunk/txdav/dps/test/test_client.py
    CalendarServer/trunk/txdav/who/cache.py
    CalendarServer/trunk/txdav/who/directory.py
    CalendarServer/trunk/txdav/who/groups.py
    CalendarServer/trunk/txdav/who/idirectory.py
    CalendarServer/trunk/txdav/who/test/test_groups.py
    CalendarServer/trunk/txdav/who/util.py

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

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -941,7 +941,8 @@
         if config.GroupCaching.Enabled:
             groupCacher = GroupCacher(
                 directory,
-                updateSeconds=config.GroupCaching.UpdateSeconds
+                updateSeconds=config.GroupCaching.UpdateSeconds,
+                useDirectoryBasedDelegates=config.GroupCaching.UseDirectoryBasedDelegates,
             )
         else:
             groupCacher = None
@@ -1091,7 +1092,7 @@
         WorkSchedulingService(
             store,
             config.Scheduling.iMIP.Enabled,
-            (config.GroupCaching.Enabled and config.GroupCaching.EnableUpdater),
+            config.GroupCaching.Enabled,
             config.AutomaticPurging.Enabled
         ).setServiceParent(service)
 
@@ -1309,7 +1310,8 @@
             if config.GroupCaching.Enabled:
                 groupCacher = GroupCacher(
                     directory,
-                    updateSeconds=config.GroupCaching.UpdateSeconds
+                    updateSeconds=config.GroupCaching.UpdateSeconds,
+                    useDirectoryBasedDelegates=config.GroupCaching.UseDirectoryBasedDelegates,
                 )
             else:
                 groupCacher = None
@@ -1895,7 +1897,8 @@
             if config.GroupCaching.Enabled:
                 groupCacher = GroupCacher(
                     directory,
-                    updateSeconds=config.GroupCaching.UpdateSeconds
+                    updateSeconds=config.GroupCaching.UpdateSeconds,
+                    useDirectoryBasedDelegates=config.GroupCaching.UseDirectoryBasedDelegates,
                 )
             else:
                 groupCacher = None

Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/principals.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/calendarserver/tools/principals.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -883,10 +883,16 @@
 
 
 def printRecordList(records):
-    results = [
-        (record.displayName, record.recordType.name, record.uid, record.shortNames)
-        for record in records
-    ]
+    results = []
+    for record in records:
+        try:
+            shortNames = record.shortNames
+        except AttributeError:
+            shortNames = []
+        results.append(
+            (record.displayName, record.recordType.name, record.uid, shortNames)
+        )
+
     results.sort()
     format = "%-22s %-10s %-20s %s"
     print(format % ("Full name", "Type", "UID", "Short names"))

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2014-08-12 20:45:40 UTC (rev 13868)
@@ -883,17 +883,9 @@
     <dict>
       <key>Enabled</key>
       <true/>
-      <key>EnableUpdater</key>
-      <true/>
-      <key>MemcachedPool</key>
-      <string>Default</string>
       <key>UpdateSeconds</key>
       <integer>300</integer>
-      <key>ExpireSeconds</key>
-      <integer>3600</integer>
-      <key>LockSeconds</key>
-      <integer>300</integer>
-      <key>UseExternalProxies</key>
+      <key>UseDirectoryBasedDelegates</key>
       <false/>
     </dict>
 

Modified: CalendarServer/trunk/requirements-stable.txt
===================================================================
--- CalendarServer/trunk/requirements-stable.txt	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/requirements-stable.txt	2014-08-12 20:45:40 UTC (rev 13868)
@@ -5,7 +5,7 @@
 # For CalendarServer development, don't try to get these projects from PyPI; use svn.
 
 -e .
--e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@13857#egg=twextpy
+-e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@13867#egg=twextpy
 -e svn+http://svn.calendarserver.org/repository/calendarserver/PyKerberos/trunk@13420#egg=kerberos
 -e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@13802#egg=pycalendar
 

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -129,12 +129,16 @@
                 guid = record.guid
         except AttributeError:
             guid = ""
+        try:
+            shortNames = record.shortNames
+        except AttributeError:
+            shortNames = []
         return tag.fillSlots(
             directoryGUID=record.service.guid,
             realm=record.service.realmName,
             guid=guid,
             recordType=record.service.recordTypeToOldName(record.recordType),
-            shortNames=record.shortNames,
+            shortNames=shortNames,
             fullName=record.displayName,
             principalUID=parent.principalUID(),
             principalURL=formatLink(parent.principalURL()),

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -79,6 +79,8 @@
             "fullNames": ["cn", ],
             "emailAddresses": ["mail", ],
             "memberDNs": ["uniqueMember", ],
+            "readWriteProxy": ["icsContact", ],
+            "readOnlyProxy": ["icsSecondaryOwners", ],
         }
     },
 }
@@ -965,10 +967,8 @@
 
     "GroupCaching" : {
         "Enabled": True,
-        "MemcachedPool" : "Default",
         "UpdateSeconds" : 300,
-        "EnableUpdater" : True,
-        "UseExternalProxies" : False,
+        "UseDirectoryBasedDelegates" : False,
     },
 
     "GroupAttendees" : {

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -1339,17 +1339,17 @@
 
         """
         if record is not None:
-            members = yield record.expandedMembers()
+            memberUIDs = yield record.expandedMemberUIDs()
             name = record.displayName
             extant = True
         else:
-            members = frozenset()
+            memberUIDs = frozenset()
             name = cachedName
             extant = False
 
         membershipHashContent = hashlib.md5()
-        for member in sorted(members, key=lambda x: x.uid):
-            membershipHashContent.update(str(member.uid))
+        for memberUID in sorted(memberUIDs):
+            membershipHashContent.update(str(memberUID))
         membershipHash = membershipHashContent.hexdigest()
 
         if cachedMembershipHash != membershipHash:
@@ -1368,8 +1368,8 @@
 
         if membershipChanged:
             newMemberUIDs = set()
-            for member in members:
-                newMemberUIDs.add(member.uid)
+            for memberUID in memberUIDs:
+                newMemberUIDs.add(memberUID)
             yield self.synchronizeMembers(groupID, newMemberUIDs)
 
         returnValue(membershipChanged)

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2014-08-12 20:45:40 UTC (rev 13868)
@@ -405,10 +405,18 @@
     "GROUP_UID" nvarchar2(255)
 );
 
+create table GROUP_DELEGATE_CHANGES_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "DELEGATOR_UID" nvarchar2(255),
+    "READ_DELEGATE_UID" nvarchar2(255),
+    "WRITE_DELEGATE_UID" nvarchar2(255)
+);
+
 create table GROUPS (
     "GROUP_ID" integer primary key,
     "NAME" nvarchar2(255),
-    "GROUP_UID" nvarchar2(255),
+    "GROUP_UID" nvarchar2(255) unique,
     "MEMBERSHIP_HASH" nvarchar2(255),
     "EXTANT" integer default 1,
     "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
@@ -596,7 +604,7 @@
     "VALUE" nvarchar2(255)
 );
 
-insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '46');
+insert into CALENDARSERVER (NAME, VALUE) values ('VERSION', '47');
 insert into CALENDARSERVER (NAME, VALUE) values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER (NAME, VALUE) values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER (NAME, VALUE) values ('NOTIFICATION-DATAVERSION', '1');
@@ -754,6 +762,10 @@
     JOB_ID
 );
 
+create index GROUP_DELEGATE_CHANGE_8bf9e6d8 on GROUP_DELEGATE_CHANGES_WORK (
+    JOB_ID
+);
+
 create index GROUPS_GROUP_UID_b35cce23 on GROUPS (
     GROUP_UID
 );

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2014-08-12 20:45:40 UTC (rev 13868)
@@ -762,10 +762,21 @@
 create index GROUP_REFRESH_WORK_JOB_ID on
   GROUP_REFRESH_WORK(JOB_ID);
 
+create table GROUP_DELEGATE_CHANGES_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELEGATOR_UID                 varchar(255) not null,
+  READ_DELEGATE_UID             varchar(255) not null,
+  WRITE_DELEGATE_UID            varchar(255) not null
+);
+
+create index GROUP_DELEGATE_CHANGES_WORK_JOB_ID on
+  GROUP_DELEGATE_CHANGES_WORK(JOB_ID);
+
 create table GROUPS (
   GROUP_ID                      integer      primary key default nextval('RESOURCE_ID_SEQ'),    -- implicit index
   NAME                          varchar(255) not null,
-  GROUP_UID                     varchar(255) not null,
+  GROUP_UID                     varchar(255) not null unique,
   MEMBERSHIP_HASH               varchar(255) not null,
   EXTANT                        integer default 1,
   CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
@@ -777,7 +788,7 @@
 create table GROUP_MEMBERSHIP (
   GROUP_ID                     integer not null references GROUPS on delete cascade,
   MEMBER_UID                   varchar(255) not null,
-  
+
   primary key (GROUP_ID, MEMBER_UID)
 );
 
@@ -798,19 +809,19 @@
 create index GROUP_ATTENDEE_RECONCILE_WORK_GROUP_ID on
   GROUP_ATTENDEE_RECONCILE_WORK(GROUP_ID);
 
-  
+
 create table GROUP_ATTENDEE (
   GROUP_ID                      integer not null references GROUPS on delete cascade,
   RESOURCE_ID                   integer not null references CALENDAR_OBJECT on delete cascade,
   MEMBERSHIP_HASH               varchar(255) not null,
-  
+
   primary key (GROUP_ID, RESOURCE_ID)
 );
 
 create index GROUP_ATTENDEE_RESOURCE_ID on
   GROUP_ATTENDEE(RESOURCE_ID);
 
-  
+
 create table GROUP_SHAREE_RECONCILE_WORK (
   WORK_ID                       integer primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
   JOB_ID                        integer not null references JOB,
@@ -831,7 +842,7 @@
   CALENDAR_ID      				integer not null references CALENDAR on delete cascade,
   GROUP_BIND_MODE               integer not null, -- enum CALENDAR_BIND_MODE
   MEMBERSHIP_HASH               varchar(255) not null,
-  
+
   primary key (GROUP_ID, CALENDAR_ID)
 );
 
@@ -960,7 +971,7 @@
 create table SCHEDULE_REFRESH_ATTENDEES (
   RESOURCE_ID                   integer      not null references CALENDAR_OBJECT on delete cascade,
   ATTENDEE                      varchar(255) not null,
-  
+
   primary key (RESOURCE_ID, ATTENDEE)
 );
 
@@ -1127,7 +1138,7 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '46');
+insert into CALENDARSERVER values ('VERSION', '47');
 insert into CALENDARSERVER values ('CALENDAR-DATAVERSION', '6');
 insert into CALENDARSERVER values ('ADDRESSBOOK-DATAVERSION', '2');
 insert into CALENDARSERVER values ('NOTIFICATION-DATAVERSION', '1');

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

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

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_46_to_47.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_46_to_47.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_46_to_47.sql	2014-08-12 20:45:40 UTC (rev 13868)
@@ -0,0 +1,40 @@
+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 46 to 47 --
+---------------------------------------------------
+
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+    "WORK_ID" integer primary key not null,
+    "JOB_ID" integer not null references JOB,
+    "DELEGATOR_UID" nvarchar2(255),
+    "READ_DELEGATE_UID" nvarchar2(255),
+    "WRITE_DELEGATE_UID" nvarchar2(255)
+);
+
+create index GROUP_DELEGATE_CHANGE_8bf9e6d8 on GROUP_DELEGATE_CHANGES_WORK (
+    JOB_ID
+);
+
+
+-- Add "unique" to GROUPS.GROUP_UID
+alter table GROUPS add unique (GROUP_UID);
+
+
+-- update the version
+update CALENDARSERVER set VALUE = '47' where NAME = 'VERSION';

Added: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_46_to_47.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_46_to_47.sql	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_46_to_47.sql	2014-08-12 20:45:40 UTC (rev 13868)
@@ -0,0 +1,40 @@
+----
+-- Copyright (c) 2012-2014 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+---------------------------------------------------
+-- Upgrade database schema from VERSION 46 to 47 --
+---------------------------------------------------
+
+
+
+create table GROUP_DELEGATE_CHANGES_WORK (
+  WORK_ID                       integer      primary key default nextval('WORKITEM_SEQ') not null, -- implicit index
+  JOB_ID                        integer      references JOB not null,
+  DELEGATOR_UID                 varchar(255) not null,
+  READ_DELEGATE_UID             varchar(255) not null,
+  WRITE_DELEGATE_UID            varchar(255) not null
+);
+
+create index GROUP_DELEGATE_CHANGES_WORK_JOB_ID on
+  GROUP_DELEGATE_CHANGES_WORK(JOB_ID);
+
+
+-- Add "unique" to GROUPS.GROUP_UID
+alter table GROUPS add unique (GROUP_UID);
+
+
+-- update the version
+update CALENDARSERVER set VALUE = '47' where NAME = 'VERSION';

Modified: CalendarServer/trunk/txdav/dps/client.py
===================================================================
--- CalendarServer/trunk/txdav/dps/client.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/dps/client.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -41,7 +41,8 @@
     RecordsMatchingTokensCommand, RecordsMatchingFieldsCommand,
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
-    WikiAccessForUIDCommand, ContinuationCommand, StatsCommand
+    WikiAccessForUIDCommand, ContinuationCommand,
+    StatsCommand, ExternalDelegatesCommand, ExpandedMemberUIDsCommand
 )
 from txdav.who.delegates import RecordType as DelegatesRecordType
 from txdav.who.directory import (
@@ -136,12 +137,12 @@
 
     def _processMultipleRecords(self, result):
         """
-        Takes a dictionary with a "fieldsList" key whose value is an iterable
+        Takes a dictionary with a "items" key whose value is an iterable
         of pickled dictionaries (of records' fields), and returns a list of
         records.
         """
         serializedFieldsList = []
-        for fields in result["fieldsList"]:
+        for fields in result["items"]:
             fields = pickle.loads(fields)
             serializedFieldsList.append(fields)
         results = []
@@ -193,8 +194,8 @@
         numResults = 0
         if "fields" in results:
             numResults = 1
-        if "fieldsList" in results:
-            numResults = len(results["fieldsList"])
+        if "items" in results:
+            numResults = len(results["items"])
         log.debug(
             "DPS call {command} duration={duration:.2f}ms, results={numResults}",
             command=command, duration=1000.0 * duration, numResults=numResults
@@ -235,9 +236,9 @@
             )
             multi.append(results)
 
-        results = {"fieldsList": []}
+        results = {"items": []}
         for result in multi:
-            results["fieldsList"].extend(result["fieldsList"])
+            results["items"].extend(result["items"])
 
         self._logResultTiming(command, startTime, results)
         returnValue(postProcess(results))
@@ -339,6 +340,14 @@
         )
 
 
+    def recordsWithDirectoryBasedDelegates(self):
+        return self._call(
+            ExternalDelegatesCommand,
+            self._processMultipleRecords
+        )
+
+
+
     def recordsFromExpression(self, expression, recordTypes=None):
         raise NotImplementedError(
             "This won't work until expressions are serializable to send "
@@ -426,6 +435,25 @@
         )
 
 
+    def _convertUIDs(self, results):
+        uids = []
+        for uid in results["items"]:
+            uids.append(uid.decode("utf-8"))
+        return uids
+
+
+    def expandedMemberUIDs(self):
+        log.debug("DPS Client expandedMemberUIDs")
+        if self.recordType is RecordType.group:
+            return self.service._call(
+                ExpandedMemberUIDsCommand,
+                self._convertUIDs,
+                uid=self.uid.encode("utf-8")
+            )
+        else:
+            return succeed([])
+
+
     def _convertAccess(self, results):
         access = results["access"].decode("utf-8")
         return txdav.who.wiki.WikiAccessLevel.lookupByName(access)

Modified: CalendarServer/trunk/txdav/dps/commands.py
===================================================================
--- CalendarServer/trunk/txdav/dps/commands.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/dps/commands.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -53,7 +53,7 @@
         ('recordType', amp.String()),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -64,7 +64,7 @@
         ('emailAddress', amp.String()),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -75,7 +75,7 @@
         ('continuation', amp.String(optional=True)),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -87,7 +87,7 @@
         ('context', amp.String(optional=True)),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -100,7 +100,7 @@
         ('recordType', amp.String(optional=True)),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -108,7 +108,7 @@
 
 class UpdateRecordsCommand(amp.Command):
     arguments = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('create', amp.Boolean(optional=True)),
     ]
     response = [
@@ -132,7 +132,7 @@
         ('uid', amp.String()),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -143,7 +143,7 @@
         ('uid', amp.String()),
     ]
     response = [
-        ('fieldsList', amp.ListOf(amp.String())),
+        ('items', amp.ListOf(amp.String())),
         ('continuation', amp.String(optional=True)),
     ]
 
@@ -159,7 +159,17 @@
     ]
 
 
+class ExpandedMemberUIDsCommand(amp.Command):
+    arguments = [
+        ('uid', amp.String()),
+    ]
+    response = [
+        ('items', amp.ListOf(amp.String())),
+        ('continuation', amp.String(optional=True)),
+    ]
 
+
+
 class VerifyPlaintextPasswordCommand(amp.Command):
     arguments = [
         ('uid', amp.String()),
@@ -201,7 +211,14 @@
     ]
 
 
+class ExternalDelegatesCommand(amp.Command):
+    arguments = []
+    response = [
+        ('items', amp.ListOf(amp.String())),
+        ('continuation', amp.String(optional=True)),
+    ]
 
+
 class StatsCommand(amp.Command):
     arguments = []
     response = [

Modified: CalendarServer/trunk/txdav/dps/json.py
===================================================================
--- CalendarServer/trunk/txdav/dps/json.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/dps/json.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -32,6 +32,7 @@
 from twext.who.expression import (
     CompoundExpression, Operand,
     MatchExpression, MatchType, MatchFlags,
+    ExistsExpression, BooleanExpression
 )
 
 
@@ -46,7 +47,21 @@
     )
 
 
+def existsExpressionAsJSON(expression):
+    return dict(
+        type=expression.__class__.__name__,
+        field=expression.fieldName.name,
+    )
 
+
+def booleanExpressionAsJSON(expression):
+    return dict(
+        type=expression.__class__.__name__,
+        field=expression.fieldName.name,
+    )
+
+
+
 def compoundExpressionAsJSON(expression):
     return dict(
         type=expression.__class__.__name__,
@@ -63,6 +78,12 @@
     if isinstance(expression, MatchExpression):
         return matchExpressionAsJSON(expression)
 
+    if isinstance(expression, ExistsExpression):
+        return existsExpressionAsJSON(expression)
+
+    if isinstance(expression, BooleanExpression):
+        return booleanExpressionAsJSON(expression)
+
     raise TypeError(
         "Unknown expression type: {!r}".format(expression)
     )
@@ -116,7 +137,32 @@
     )
 
 
+def existsExpressionFromJSON(service, json):
+    try:
+        jsonField = json["field"]
+    except KeyError as e:
+        raise ValueError(
+            "JSON match expression must have {!r} key.".format(e[0])
+        )
 
+    fieldName = service.fieldName.lookupByName(jsonField)
+
+    return ExistsExpression(fieldName)
+
+
+def booleanExpressionFromJSON(service, json):
+    try:
+        jsonField = json["field"]
+    except KeyError as e:
+        raise ValueError(
+            "JSON match expression must have {!r} key.".format(e[0])
+        )
+
+    fieldName = service.fieldName.lookupByName(jsonField)
+
+    return BooleanExpression(fieldName)
+
+
 def compoundExpressionFromJSON(json):
     try:
         expressions_json = json["expressions"]
@@ -148,6 +194,12 @@
     if json_type == "MatchExpression":
         return matchExpressionFromJSON(json)
 
+    if json_type == "ExistsExpression":
+        return existsExpressionFromJSON(json)
+
+    if json_type == "BooleanExpression":
+        return booleanExpressionFromJSON(json)
+
     raise NotImplementedError(
         "Unknown expression type: {}".format(json_type)
     )

Modified: CalendarServer/trunk/txdav/dps/server.py
===================================================================
--- CalendarServer/trunk/txdav/dps/server.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/dps/server.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -38,7 +38,7 @@
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
     WikiAccessForUIDCommand, ContinuationCommand,
-    StatsCommand,
+    ExternalDelegatesCommand, StatsCommand, ExpandedMemberUIDsCommand
     # UpdateRecordsCommand, RemoveRecordsCommand
 )
 from txdav.who.cache import CachingDirectoryService
@@ -67,7 +67,7 @@
         self._directory = directory
 
         # How to large we let an AMP response get before breaking it up
-        self._maxSize = 60000
+        self._maxSize = 55000
 
         # The cache of results we have not fully responded with.  A dictionary
         # whose keys are "continuation tokens" and whose values are tuples of
@@ -78,19 +78,20 @@
         self._continuations = {}
 
 
-    def _storeContinuation(self, records):
+    def _storeContinuation(self, things, kind):
         """
         Store an iterable of records and generate an opaque token we can
         give back to the client so they can later retrieve these remaining
         results that did not fit in the previous AMP response.
 
-        @param records: an iterable of records
+        @param things: an iterable
+        @param kind: "items" or "records"
         @return: a C{str} token
         """
         token = str(uuid.uuid4())
         # FIXME: I included a timestamp just in case we want to have code that
         # looks for stale continuations to expire them.
-        self._continuations[token] = (datetime.datetime.now(), records)
+        self._continuations[token] = (datetime.datetime.now(), things, kind)
         return token
 
 
@@ -103,11 +104,12 @@
         @return: an iterable of records, or None if the token does not exist
         """
         if token in self._continuations:
-            _ignore_timestamp, records = self._continuations[token]
+            _ignore_timestamp, things, kind = self._continuations[token]
             del self._continuations[token]
         else:
-            records = None
-        return records
+            things = None
+            kind = None
+        return things, kind
 
 
     @ContinuationCommand.responder
@@ -120,8 +122,13 @@
             in the previous response.
         """
         log.debug("Continuation: {c}", c=continuation)
-        records = self._retrieveContinuation(continuation)
-        response = self._recordsToResponse(records)
+        things, kind = self._retrieveContinuation(continuation)
+        if kind == "records":
+            response = self._recordsToResponse(things)
+        elif kind == "items":
+            response = self._itemsToResponse(things)
+        else:
+            response = {}
         # log.debug("Responding with: {response}", response=response)
         return response
 
@@ -135,7 +142,7 @@
 
         @param records: an iterable of records
         @return: the response dictionary, with a list of pickled records
-            stored in the "fieldsList" key, and if there are leftover
+            stored in the "items" key, and if there are leftover
             records that did not fit, there will be a "continuation" key
             containing the token the client must send via ContinuationCommand.
         """
@@ -156,14 +163,53 @@
                 fieldsList.append(pickled)
                 count += 1
 
-        response = {"fieldsList": fieldsList}
+        response = {"items": fieldsList}
 
         if records:
-            response["continuation"] = self._storeContinuation(records)
+            response["continuation"] = self._storeContinuation(records, "records")
 
         return response
 
 
+    def _itemsToResponse(self, items):
+        """
+        Craft an AMP response containing as many items as will fit within
+        the size limit.  Remaining items are stored as a "continuation",
+        identified by a token that is returned to the client to fetch later
+        via the ContinuationCommand.
+
+        @param records: an iterable
+        @return: the response dictionary, with a list of items
+            stored in the "items" key, and if there are leftover
+            items that did not fit, there will be a "continuation" key
+            containing the token the client must send via ContinuationCommand.
+        """
+        itemsToSend = []
+        count = 0
+        if items:
+            size = 0
+            while size < self._maxSize:
+                try:
+                    item = items.pop()
+                except (KeyError, IndexError):
+                    # We're done.
+                    # Note: because records is an iterable (list or set)
+                    # we're catching both KeyError and IndexError.
+                    break
+                size = size + len(item)
+                itemsToSend.append(item)
+                count += 1
+
+        response = {"items": itemsToSend}
+
+        if items:
+            response["continuation"] = self._storeContinuation(items, "items")
+
+        return response
+
+
+
+
     def recordToDict(self, record):
         """
         Turn a record in a dictionary of fields which can be reconstituted
@@ -173,14 +219,12 @@
         if record is not None:
             for field, value in record.fields.iteritems():
                 valueType = record.service.fieldName.valueType(field)
-                # print("%s: %s (%s)" % (field.name, value, valueType))
                 if valueType in (unicode, bool):
                     fields[field.name] = value
                 elif valueType is uuid.UUID:
                     fields[field.name] = str(value)
                 elif issubclass(valueType, (Names, NamedConstant)):
                     fields[field.name] = value.name if value else None
-        # print("Server side fields", fields)
         return fields
 
 
@@ -380,6 +424,23 @@
         returnValue(response)
 
 
+    @ExpandedMemberUIDsCommand.responder
+    @inlineCallbacks
+    def expandedMemberUIDs(self, uid):
+        uid = uid.decode("utf-8")
+        log.debug("ExpandedMemberUIDs: {u}", u=uid)
+        try:
+            record = (yield self._directory.recordWithUID(uid))
+        except Exception as e:
+            log.error("Failed in expandedMemberUIDs", error=e)
+            record = None
+
+        uids = yield record.expandedMemberUIDs()
+        response = self._itemsToResponse([u.encode("utf-8") for u in uids])
+        # log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
     @VerifyPlaintextPasswordCommand.responder
     @inlineCallbacks
     def verifyPlaintextPassword(self, uid, password):
@@ -468,7 +529,17 @@
         returnValue(response)
 
 
+    @ExternalDelegatesCommand.responder
+    @inlineCallbacks
+    def externalDelegates(self):
+        log.debug("ExternalDelegates")
+        records = yield self._directory.recordsWithDirectoryBasedDelegates()
+        response = self._recordsToResponse(records)
+        # log.debug("Responding with: {response}", response=response)
+        returnValue(response)
 
+
+
 class DirectoryProxyAMPFactory(Factory):
     """
     """

Modified: CalendarServer/trunk/txdav/dps/test/test_client.py
===================================================================
--- CalendarServer/trunk/txdav/dps/test/test_client.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/dps/test/test_client.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -47,8 +47,8 @@
 
     # Mix in the calendar-specific service methods
     class CalendarXMLDirectoryService(
-        XMLDirectoryService,
-        CalendarDirectoryServiceMixin
+        CalendarDirectoryServiceMixin,
+        XMLDirectoryService
     ):
         pass
 
@@ -60,8 +60,8 @@
 
     # Mix in the calendar-specific service methods
     class CalendarODDirectoryService(
-        OpenDirectoryService,
-        CalendarDirectoryServiceMixin
+        CalendarDirectoryServiceMixin,
+        OpenDirectoryService
     ):
         pass
 
@@ -590,6 +590,18 @@
 
 
     @inlineCallbacks
+    def test_expandedMemberUIDs(self):
+        group = yield self.client.recordWithUID(u"__top_group_1__")
+        memberUIDs = yield group.expandedMemberUIDs()
+        self.assertEquals(
+            set(memberUIDs),
+            set(
+                [u'__wsanchez1__', u'__cdaboo1__', u'__glyph1__', u'__sagen1__']
+            )
+        )
+
+
+    @inlineCallbacks
     def test_groups(self):
 
         # A group must first be "refreshed" into the DB otherwise we won't
@@ -717,8 +729,8 @@
 
         # Connect the two services directly via an IOPump
         client = AMP()
-        server = DirectoryProxyAMPProtocol(remoteDirectory)
-        pump = returnConnected(server, client)
+        self.server = DirectoryProxyAMPProtocol(remoteDirectory)
+        pump = returnConnected(self.server, client)
 
         # Replace the normal _getConnection method with one that bypasses any
         # actual networking
@@ -768,3 +780,9 @@
         group = yield self.directory.recordWithUID(u"bigGroup")
         members = yield group.members()
         self.assertEquals(len(members), self.numUsers)
+
+        # force the limit small so continuations happen
+        self.server._maxSize = 500
+        # expandedMemberUIDs
+        memberUIDs = yield group.expandedMemberUIDs()
+        self.assertEquals(len(memberUIDs), self.numUsers)

Modified: CalendarServer/trunk/txdav/who/cache.py
===================================================================
--- CalendarServer/trunk/txdav/who/cache.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/cache.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -378,6 +378,10 @@
         )
 
 
+    def recordsWithDirectoryBasedDelegates(self):
+        return self._directory.recordsWithDirectoryBasedDelegates()
+
+
     def recordWithCalendarUserAddress(self, cua):
         # This will get cached by the underlying recordWith... call
         return CalendarDirectoryServiceMixin.recordWithCalendarUserAddress(

Modified: CalendarServer/trunk/txdav/who/directory.py
===================================================================
--- CalendarServer/trunk/txdav/who/directory.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/directory.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -22,7 +22,8 @@
 
 from twext.python.log import Logger
 from twext.who.expression import (
-    MatchType, Operand, MatchExpression, CompoundExpression, MatchFlags
+    MatchType, Operand, MatchExpression, CompoundExpression, MatchFlags,
+    ExistsExpression, BooleanExpression
 )
 from twext.who.idirectory import RecordType as BaseRecordType
 from twisted.cred.credentials import UsernamePassword
@@ -32,7 +33,8 @@
 from txdav.caldav.datastore.scheduling.utils import normalizeCUAddr
 from txdav.who.delegates import RecordType as DelegateRecordType
 from txdav.who.idirectory import (
-    RecordType as DAVRecordType, AutoScheduleMode
+    RecordType as DAVRecordType, AutoScheduleMode,
+    FieldName as CalFieldName
 )
 from txweb2.auth.digest import DigestedCredentials
 
@@ -257,7 +259,39 @@
         return None
 
 
+    @inlineCallbacks
+    def recordsWithDirectoryBasedDelegates(self):
+        """
+        Fetch calendar-enabled locations and resources which have proxy
+        groups assigned.
+        """
 
+        expression = CompoundExpression(
+            (
+                BooleanExpression(CalFieldName.hasCalendars),
+                CompoundExpression(
+                    (
+                        ExistsExpression(CalFieldName.readOnlyProxy),
+                        ExistsExpression(CalFieldName.readWriteProxy)
+                    ),
+                    Operand.OR
+                )
+            ),
+            Operand.AND
+        )
+
+        records = yield self.recordsFromExpression(
+            expression,
+            recordTypes=(self.recordType.location,)
+            # FIXME, commenting out resources for the moment since it's
+            # super slow:
+            # recordTypes=(self.recordType.location, self.recordType.resource)
+
+        )
+        returnValue(records)
+
+
+
 class CalendarDirectoryRecordMixin(object):
     """
     Calendar (and Contacts) specific logic for directory records lives in this
@@ -535,8 +569,8 @@
             organizerRecord = yield service.recordWithCalendarUserAddress(organizer)
             if organizerRecord is not None:
                 autoAcceptGroup = yield service.recordWithUID(autoAcceptGroup)
-                members = yield autoAcceptGroup.expandedMembers()
-                if organizerRecord.uid in ([m.uid for m in members]):
+                memberUIDs = yield autoAcceptGroup.expandedMemberUIDs()
+                if organizerRecord.uid in memberUIDs:
                     returnValue(True)
         returnValue(False)
 
@@ -559,14 +593,25 @@
         if self not in seen:
             seen.add(self)
             for member in (yield self.members()):
-                if member.recordType == BaseRecordType.group:
-                    yield member.expandedMembers(members, seen)
-                else:
-                    members.add(member)
+                if member is not None:
+                    if member.recordType == BaseRecordType.group:
+                        yield member.expandedMembers(members, seen)
+                    else:
+                        members.add(member)
 
         returnValue(members)
 
 
+    @inlineCallbacks
+    def expandedMemberUIDs(self):
+        """
+        Return a L{set} containing the UIDs of the fully expanded membership
+        for this group.
+        """
+        members = yield self.expandedMembers()
+        returnValue([member.uid for member in members])
+
+
     # For scheduling/freebusy
     @inlineCallbacks
     def isProxyFor(self, other):

Modified: CalendarServer/trunk/txdav/who/groups.py
===================================================================
--- CalendarServer/trunk/txdav/who/groups.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/groups.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -31,6 +31,7 @@
 from txdav.caldav.datastore.sql import CalendarStoreFeatures, ComponentUpdateState
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
 import datetime
+import time
 
 log = Logger()
 
@@ -47,7 +48,7 @@
         def _enqueue(txn):
             return GroupCacherPollingWork.reschedule(txn, seconds)
 
-        if config.InboxCleanup.Enabled:
+        if config.GroupCaching.Enabled:
             return store.inTransaction("GroupCacherPollingWork.initialSchedule", _enqueue)
         else:
             return succeed(None)
@@ -67,6 +68,7 @@
         groupCacher = getattr(self.transaction, "_groupCacher", None)
         if groupCacher is not None:
 
+            startTime = time.time()
             try:
                 yield groupCacher.update(self.transaction)
             except Exception, e:
@@ -74,6 +76,11 @@
                     "Failed to update new group membership cache ({error})",
                     error=e
                 )
+            endTime = time.time()
+            log.debug(
+                "GroupCacher polling took {duration:0.2f} seconds",
+                duration=(endTime - startTime)
+            )
 
 
 
@@ -112,6 +119,35 @@
 
 
 
+class GroupDelegateChangesWork(WorkItem, fromTable(schema.GROUP_DELEGATE_CHANGES_WORK)):
+
+    delegator = property(lambda self: (self.table.DELEGATOR_UID == self.delegatorUid))
+
+    @inlineCallbacks
+    def doWork(self):
+        # Delete all other work items for this delegator
+        yield Delete(
+            From=self.table,
+            Where=self.delegator,
+        ).on(self.transaction)
+
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
+        if groupCacher is not None:
+
+            try:
+                yield groupCacher.applyExternalAssignments(
+                    self.transaction,
+                    self.delegatorUid.decode("utf-8"),
+                    self.readDelegateUid.decode("utf-8"),
+                    self.writeDelegateUid.decode("utf-8")
+                )
+            except Exception, e:
+                log.error(
+                    "Failed to apply external delegates for {uid} {err}",
+                    uid=self.delegatorUid, err=e
+                )
+
+
 class GroupAttendeeReconciliationWork(
     WorkItem, fromTable(schema.GROUP_ATTENDEE_RECONCILE_WORK)
 ):
@@ -288,30 +324,45 @@
     def __init__(
         self, directory,
         updateSeconds=600,
-        useExternalProxies=False,
-        externalProxiesSource=None
+        useDirectoryBasedDelegates=False,
+        directoryBasedDelegatesSource=None
     ):
         self.directory = directory
-        self.useExternalProxies = useExternalProxies
-        if useExternalProxies and externalProxiesSource is None:
-            externalProxiesSource = self.directory.getExternalProxyAssignments
-        self.externalProxiesSource = externalProxiesSource
+        self.useDirectoryBasedDelegates = useDirectoryBasedDelegates
+        if useDirectoryBasedDelegates and directoryBasedDelegatesSource is None:
+            directoryBasedDelegatesSource = self.directory.recordsWithDirectoryBasedDelegates
+        self.directoryBasedDelegatesSource = directoryBasedDelegatesSource
         self.updateSeconds = updateSeconds
 
 
     @inlineCallbacks
     def update(self, txn):
-        # TODO
-        # Pull in external delegate assignments and stick in delegate db
-        # if self.useExternalProxies:
-        #     externalAssignments = (yield self.externalProxiesSource())
-        # yield self.applyExternalAssignments(txn, externalAssignments)
 
+        if self.useDirectoryBasedDelegates:
+            # Pull in delegate assignments from the directory and stick them
+            # into the delegate db
+            recordsWithDirectoryBasedDelegates = yield self.directoryBasedDelegatesSource()
+            externalAssignments = {}
+            for record in recordsWithDirectoryBasedDelegates:
+                try:
+                    readWriteProxy = record.readWriteProxy
+                except AttributeError:
+                    readWriteProxy = None
+                try:
+                    readOnlyProxy = record.readOnlyProxy
+                except AttributeError:
+                    readOnlyProxy = None
+
+                if readOnlyProxy or readWriteProxy:
+                    externalAssignments[record.uid] = (readOnlyProxy, readWriteProxy)
+
+            yield self.scheduleExternalAssignments(txn, externalAssignments)
+
         # Figure out which groups matter
         groupUIDs = yield self.groupsToRefresh(txn)
-        self.log.debug(
-            "Groups to refresh: {g}", g=groupUIDs
-        )
+        # self.log.debug(
+        #     "Groups to refresh: {g}", g=groupUIDs
+        # )
 
         gr = schema.GROUPS
         if config.AutomaticPurging.Enabled and groupUIDs:
@@ -351,9 +402,11 @@
 
 
     @inlineCallbacks
-    def applyExternalAssignments(self, txn, newAssignments):
+    def scheduleExternalAssignments(
+        self, txn, newAssignments, immediately=False
+    ):
 
-        oldAssignments = (yield txn.externalDelegates())
+        oldAssignments = yield txn.externalDelegates()
 
         # external assignments is of the form:
         # { delegatorUID: (readDelegateGroupUID, writeDelegateGroupUID),
@@ -364,33 +417,71 @@
             for (
                 delegatorUID, (readDelegateUID, writeDelegateUID)
             ) in changed:
-                readDelegateGroupID = writeDelegateGroupID = None
-                if readDelegateUID:
-                    (
-                        readDelegateGroupID, _ignore_name, _ignore_hash,
-                        _ignore_modified, _ignore_extant
-                    ) = (
-                        yield txn.groupByUID(readDelegateUID)
+                self.log.debug(
+                    "Scheduling external delegate assignment changes for {uid}",
+                    uid=delegatorUID
+                )
+                if not readDelegateUID:
+                    readDelegateUID = ""
+                if not writeDelegateUID:
+                    writeDelegateUID = ""
+                if immediately:
+                    yield self.applyExternalAssignments(
+                        txn, delegatorUID, readDelegateUID, writeDelegateUID
                     )
-                if writeDelegateUID:
-                    (
-                        writeDelegateGroupID, _ignore_name, _ignore_hash,
-                        _ignore_modified, _ignore_extant
-                    ) = (
-                        yield txn.groupByUID(writeDelegateUID)
+                else:
+                    yield GroupDelegateChangesWork.reschedule(
+                        txn, 0, delegatorUid=delegatorUID,
+                        readDelegateUid=readDelegateUID,
+                        writeDelegateUid=writeDelegateUID
                     )
-                yield txn.assignExternalDelegates(
-                    delegatorUID, readDelegateGroupID, writeDelegateGroupID,
-                    readDelegateUID, writeDelegateUID
-                )
         if removed:
             for delegatorUID in removed:
-                yield txn.assignExternalDelegates(
-                    delegatorUID, None, None, None, None
+                self.log.debug(
+                    "Scheduling external delegation assignment removal for {uid}",
+                    uid=delegatorUID
                 )
+                if immediately:
+                    yield self.applyExternalAssignments(
+                        txn, delegatorUID, "", ""
+                    )
+                else:
+                    yield GroupDelegateChangesWork.reschedule(
+                        txn, 0, delegatorUid=delegatorUID,
+                        readDelegateUid="", writeDelegateUid=""
+                    )
 
 
     @inlineCallbacks
+    def applyExternalAssignments(
+        self, txn, delegatorUID, readDelegateUID, writeDelegateUID
+    ):
+        self.log.debug(
+            "External delegate assignments changed for {uid}",
+            uid=delegatorUID
+        )
+        readDelegateGroupID = writeDelegateGroupID = None
+        if readDelegateUID:
+            (
+                readDelegateGroupID, _ignore_name, _ignore_hash,
+                _ignore_modified, _ignore_extant
+            ) = (
+                yield txn.groupByUID(readDelegateUID)
+            )
+        if writeDelegateUID:
+            (
+                writeDelegateGroupID, _ignore_name, _ignore_hash,
+                _ignore_modified, _ignore_extant
+            ) = (
+                yield txn.groupByUID(writeDelegateUID)
+            )
+        yield txn.assignExternalDelegates(
+            delegatorUID, readDelegateGroupID, writeDelegateGroupID,
+            readDelegateUID, writeDelegateUID
+        )
+
+
+    @inlineCallbacks
     def refreshGroup(self, txn, groupUID):
         """
             Does the work of a per-group refresh work item
@@ -398,7 +489,8 @@
             and updates the GROUP_MEMBERSHIP table
             WorkProposal is returned for tests
         """
-        self.log.debug("Faulting in group: {g}", g=groupUID)
+        self.log.debug("Refreshing group: {g}", g=groupUID)
+
         record = (yield self.directory.recordWithUID(groupUID))
         if record is None:
             # the group has disappeared from the directory
@@ -421,9 +513,20 @@
             )
 
             if membershipChanged:
+                self.log.debug(
+                    "Membership changed for group {uid} {name}",
+                    uid=groupUID,
+                    name=cachedName
+                )
                 wpsAttendee = yield self.scheduleGroupAttendeeReconciliations(txn, groupID)
                 wpsShareee = yield self.scheduleGroupShareeReconciliations(txn, groupID)
                 returnValue(wpsAttendee + wpsShareee)
+            else:
+                self.log.debug(
+                    "No membership change for group {uid} {name}",
+                    uid=groupUID,
+                    name=cachedName
+                )
 
         returnValue(tuple())
 

Modified: CalendarServer/trunk/txdav/who/idirectory.py
===================================================================
--- CalendarServer/trunk/txdav/who/idirectory.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/idirectory.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -156,6 +156,16 @@
     autoAcceptGroup.description = u"auto-accept group"
     autoAcceptGroup.valueType = BaseFieldName.valueType(BaseFieldName.uid)
 
+    readOnlyProxy = NamedConstant()
+    readOnlyProxy.description = u"read-only proxy group"
+    readOnlyProxy.valueType = BaseFieldName.valueType(BaseFieldName.uid)
+
+    readWriteProxy = NamedConstant()
+    readWriteProxy.description = u"read-write proxy group"
+    readWriteProxy.valueType = BaseFieldName.valueType(BaseFieldName.uid)
+
+
+
     # For "locations", i.e., scheduled spaces:
 
     associatedAddress = NamedConstant()

Modified: CalendarServer/trunk/txdav/who/test/test_groups.py
===================================================================
--- CalendarServer/trunk/txdav/who/test/test_groups.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/test/test_groups.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -190,7 +190,9 @@
         newAssignments = {
             u"__wsanchez1__": (None, u"__top_group_1__")
         }
-        yield self.groupCacher.applyExternalAssignments(txn, newAssignments)
+        yield self.groupCacher.scheduleExternalAssignments(
+            txn, newAssignments, immediately=True
+        )
         oldExternalAssignments = (yield txn.externalDelegates())
         self.assertEquals(
             oldExternalAssignments,
@@ -215,7 +217,10 @@
                 u"__top_group_1__"
             ),
         }
-        yield self.groupCacher.applyExternalAssignments(txn, newAssignments)
+
+        yield self.groupCacher.scheduleExternalAssignments(
+            txn, newAssignments, immediately=True
+        )
         oldExternalAssignments = (yield txn.externalDelegates())
         self.assertEquals(
             oldExternalAssignments,
@@ -290,7 +295,9 @@
                 None
             ),
         }
-        yield self.groupCacher.applyExternalAssignments(txn, newAssignments)
+        yield self.groupCacher.scheduleExternalAssignments(
+            txn, newAssignments, immediately=True
+        )
         oldExternalAssignments = (yield txn.externalDelegates())
         self.assertEquals(
             oldExternalAssignments,

Modified: CalendarServer/trunk/txdav/who/util.py
===================================================================
--- CalendarServer/trunk/txdav/who/util.py	2014-08-12 19:57:18 UTC (rev 13867)
+++ CalendarServer/trunk/txdav/who/util.py	2014-08-12 20:45:40 UTC (rev 13868)
@@ -143,6 +143,9 @@
                     BaseFieldName.fullNames: mapping.fullNames,
                     BaseFieldName.emailAddresses: mapping.emailAddresses,
                     LDAPFieldName.memberDNs: mapping.memberDNs,
+                    CalFieldName.readOnlyProxy: mapping.readOnlyProxy,
+                    CalFieldName.readWriteProxy: mapping.readWriteProxy,
+                    CalFieldName.hasCalendars: mapping.hasCalendars,
                 }),
                 recordTypeSchemas=MappingProxyType({
                     RecordType.user: RecordTypeSchema(
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140812/77c0b80f/attachment-0001.html>


More information about the calendarserver-changes mailing list