[CalendarServer-changes] [6184] CalendarServer/branches/generic-sqlstore

source_changes at macosforge.org source_changes at macosforge.org
Wed Aug 25 17:52:31 PDT 2010


Revision: 6184
          http://trac.macosforge.org/projects/calendarserver/changeset/6184
Author:   cdaboo at apple.com
Date:     2010-08-25 17:52:30 -0700 (Wed, 25 Aug 2010)
Log Message:
-----------
Major re-factor of classes out of postgres.py into common and separate modules.

Modified Paths:
--------------
    CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py
    CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py
    CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py
    CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py
    CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py

Added Paths:
-----------
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py
    CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py
    CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py
    CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py
    CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py
    CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py

Removed Paths:
-------------
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql
    CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py

Modified: CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -88,7 +88,7 @@
 from calendarserver.tap.util import getRootResource, computeProcessCount
 from calendarserver.tools.util import checkDirectory
 
-from txcaldav.calendarstore.postgres import v1_schema
+from txdav.common.datastore.sql import v1_schema
 from txdav.datastore.subpostgres import PostgresService
 from twext.python.filepath import CachingFilePath
 

Modified: CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -69,7 +69,7 @@
 
 from txdav.common.datastore.sql import CommonDataStore as CommonSQLDataStore
 from txdav.common.datastore.file import CommonDataStore as CommonFileDataStore
-from txcaldav.calendarstore.postgres import v1_schema
+from txdav.common.datastore.sql import v1_schema
 from txdav.datastore.subpostgres import PostgresService
 from twext.python.filepath import CachingFilePath
 

Modified: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -227,8 +227,8 @@
 
 
     @writeOperation
-    def setComponent(self, component):
-        validateCalendarComponent(self, self._calendar, component)
+    def setComponent(self, component, inserting=False):
+        validateCalendarComponent(self, self._calendar, component, inserting)
 
         self._calendar.retrieveOldIndex().addResource(
             self.name(), component

Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,3013 +0,0 @@
-# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
-##
-# Copyright (c) 2010 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.
-##
-
-"""
-PostgreSQL data store.
-"""
-
-__all__ = [
-    "PostgresCalendarHome",
-    "PostgresCalendar",
-    "PostgresCalendarObject",
-    "PostgresAddressBookHome",
-    "PostgresAddressBook",
-    "PostgresAddressBookObject",
-]
-
-import datetime
-import StringIO
-
-from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
-
-from zope.interface.declarations import implements
-
-from twisted.internet.error import ConnectionLost
-from twisted.internet.interfaces import ITransport
-from twisted.python import hashlib
-from twisted.python.failure import Failure
-from twisted.internet.defer import succeed
-from twisted.python.modules import getModule
-
-from twext.web2.dav.element.rfc2518 import ResourceType
-
-from txdav.common.inotifications import (INotificationCollection,
-    INotificationObject)
-
-from txdav.common.icommondatastore import (
-    ObjectResourceNameAlreadyExistsError, HomeChildNameAlreadyExistsError,
-    NoSuchHomeChildError, NoSuchObjectResourceError)
-from txcaldav.calendarstore.util import (validateCalendarComponent,
-    validateAddressBookComponent, dropboxIDFromCalendarObject, CalendarSyncTokenHelper,
-    AddressbookSyncTokenHelper)
-from txdav.datastore.file import cached
-from txdav.datastore.sql import memoized
-
-from txcaldav.icalendarstore import (ICalendarHome, ICalendar, ICalendarObject, IAttachment)
-from txcarddav.iaddressbookstore import (IAddressBookHome, IAddressBook, IAddressBookObject)
-from txdav.propertystore.base import PropertyName
-from txdav.propertystore.sql import PropertyStore
-
-from twext.web2.http_headers import MimeType, generateContentType
-
-from twext.python.log import Logger, LoggingMixIn
-from twext.python.vcomponent import VComponent
-
-from twistedcaldav import carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
-from twistedcaldav.customxml import NotificationType
-from twistedcaldav.dateops import normalizeForIndex
-from twistedcaldav.index import IndexedSearchException, ReservationError,\
-    SyncTokenValidException
-from twistedcaldav.instance import InvalidOverriddenInstanceError
-from twistedcaldav.memcachepool import CachePoolUserMixIn
-from twistedcaldav.notifications import NotificationRecord
-from twistedcaldav.query import calendarqueryfilter, calendarquery, \
-    addressbookquery
-from twistedcaldav.query.sqlgenerator import sqlgenerator
-from twistedcaldav.sharing import Invite
-from twistedcaldav.vcard import Component as VCard
-
-from vobject.icalendar import utc
-
-v1_schema = getModule(__name__).filePath.sibling(
-    "postgres_schema_v1.sql").getContent()
-
-log = Logger()
-
-# FIXME: these constants are in the schema, and should probably be discovered
-# from there somehow.
-
-_BIND_STATUS_INVITED = 0
-_BIND_STATUS_ACCEPTED = 1
-_BIND_STATUS_DECLINED = 2
-_BIND_STATUS_INVALID = 3
-
-_ATTACHMENTS_MODE_WRITE = 1
-
-_BIND_MODE_OWN = 0
-_BIND_MODE_READ = 1
-_BIND_MODE_WRITE = 2
-
-
-#
-# Duration into the future through which recurrences are expanded in the index
-# by default.  This is a caching parameter which affects the size of the index;
-# it does not affect search results beyond this period, but it may affect
-# performance of such a search.
-#
-default_future_expansion_duration = datetime.timedelta(days=365 * 1)
-
-#
-# Maximum duration into the future through which recurrences are expanded in the
-# index.  This is a caching parameter which affects the size of the index; it
-# does not affect search results beyond this period, but it may affect
-# performance of such a search.
-#
-# When a search is performed on a time span that goes beyond that which is
-# expanded in the index, we have to open each resource which may have data in
-# that time period.  In order to avoid doing that multiple times, we want to
-# cache those results.  However, we don't necessarily want to cache all
-# occurrences into some obscenely far-in-the-future date, so we cap the caching
-# period.  Searches beyond this period will always be relatively expensive for
-# resources with occurrences beyond this period.
-#
-maximum_future_expansion_duration = datetime.timedelta(days=365 * 5)
-
-icalfbtype_to_indexfbtype = {
-    "UNKNOWN"         : 0,
-    "FREE"            : 1,
-    "BUSY"            : 2,
-    "BUSY-UNAVAILABLE": 3,
-    "BUSY-TENTATIVE"  : 4,
-}
-
-indexfbtype_to_icalfbtype = {
-    0: '?',
-    1: 'F',
-    2: 'B',
-    3: 'U',
-    4: 'T',
-}
-
-
-class PostgresCalendarObject(object):
-    implements(ICalendarObject)
-
-    def __init__(self, calendar, name, resid):
-        self._calendar = calendar
-        self._name = name
-        self._resourceID = resid
-        self._calendarText = None
-
-
-    @property
-    def _txn(self):
-        return self._calendar._txn
-
-
-    def uid(self):
-        return self.component().resourceUID()
-
-
-    def organizer(self):
-        return self.component().getOrganizer()
-
-
-    def dropboxID(self):
-        return dropboxIDFromCalendarObject(self)
-
-
-    def name(self):
-        return self._name
-
-
-    def calendar(self):
-        return self._calendar
-
-
-    def iCalendarText(self):
-        if self._calendarText is None:
-            text = self._txn.execSQL(
-                "select ICALENDAR_TEXT from CALENDAR_OBJECT where "
-                "RESOURCE_ID = %s", [self._resourceID]
-            )[0][0]
-            self._calendarText = text
-            return text
-        else:
-            return self._calendarText
-
-
-    def component(self):
-        return VComponent.fromString(self.iCalendarText())
-
-
-    def componentType(self):
-        return self.component().mainType()
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    def setComponent(self, component):
-        validateCalendarComponent(self, self._calendar, component)
-
-        self.updateDatabase(component)
-        self._calendar._updateRevision(self._name)
-
-        if self._calendar._notifier:
-            self._calendar._home._txn.postCommit(self._calendar._notifier.notify)
-
-    def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
-        """
-        Update the database tables for the new data being written.
-
-        @param component: calendar data to store
-        @type component: L{Component}
-        """
-
-        # Decide how far to expand based on the component
-        master = component.masterComponent()
-        if master is None or not component.isRecurring() and not component.isRecurringUnbounded():
-            # When there is no master we have a set of overridden components - index them all.
-            # When there is one instance - index it.
-            # When bounded - index all.
-            expand = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-        else:
-            if expand_until:
-                expand = expand_until
-            else:
-                expand = datetime.date.today() + default_future_expansion_duration
-
-            if expand > (datetime.date.today() + maximum_future_expansion_duration):
-                raise IndexedSearchException
-
-        try:
-            instances = component.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
-        except InvalidOverriddenInstanceError, e:
-            log.err("Invalid instance %s when indexing %s in %s" % (e.rid, self._name, self.resource,))
-            raise
-
-        componentText = str(component)
-        self._calendarText = componentText
-        organizer = component.getOrganizer()
-        if not organizer:
-            organizer = ""
-
-        # CALENDAR_OBJECT table update
-        if inserting:
-            self._resourceID = self._txn.execSQL(
-                """
-                insert into CALENDAR_OBJECT
-                (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX)
-                 values
-                (%s, %s, %s, %s, %s, %s, %s, %s)
-                 returning RESOURCE_ID
-                """,
-                # FIXME: correct ATTACHMENTS_MODE based on X-APPLE-
-                # DROPBOX
-                [
-                    self._calendar._resourceID,
-                    self._name,
-                    componentText,
-                    component.resourceUID(),
-                    component.resourceType(),
-                    _ATTACHMENTS_MODE_WRITE,
-                    organizer,
-                    normalizeForIndex(instances.limit) if instances.limit else None,
-                ]
-            )[0][0]
-        else:
-            self._txn.execSQL(
-                """
-                update CALENDAR_OBJECT set
-                (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MODIFIED)
-                 =
-                (%s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP))
-                 where RESOURCE_ID = %s
-                """,
-                # should really be filling out more fields: ORGANIZER,
-                # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
-                # DROPBOX
-                [
-                    componentText,
-                    component.resourceUID(),
-                    component.resourceType(),
-                    _ATTACHMENTS_MODE_WRITE,
-                    organizer,
-                    normalizeForIndex(instances.limit) if instances.limit else None,
-                    self._resourceID
-                ]
-            )
-
-            # Need to wipe the existing time-range for this and rebuild
-            self._txn.execSQL(
-                """
-                delete from TIME_RANGE where CALENDAR_OBJECT_RESOURCE_ID = %s
-                """,
-                [
-                    self._resourceID,
-                ],
-            )
-
-
-        # CALENDAR_OBJECT table update
-        for key in instances:
-            instance = instances[key]
-            start = instance.start.replace(tzinfo=utc)
-            end = instance.end.replace(tzinfo=utc)
-            float = instance.start.tzinfo is None
-            transp = instance.component.propertyValue("TRANSP") == "TRANSPARENT"
-            instanceid = self._txn.execSQL(
-                """
-                insert into TIME_RANGE
-                (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
-                 values
-                (%s, %s, %s, %s, %s, %s, %s)
-                 returning
-                INSTANCE_ID
-                """,
-                [
-                    self._calendar._resourceID,
-                    self._resourceID,
-                    float,
-                    start,
-                    end,
-                    icalfbtype_to_indexfbtype.get(instance.component.getFBType(), icalfbtype_to_indexfbtype["FREE"]),
-                    transp,
-                ],
-            )[0][0]
-            peruserdata = component.perUserTransparency(instance.rid)
-            for useruid, transp in peruserdata:
-                self._txn.execSQL(
-                    """
-                    insert into TRANSPARENCY
-                    (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
-                     values
-                    (%s, %s, %s)
-                    """,
-                    [
-                        instanceid,
-                        useruid,
-                        transp,
-                    ],
-                )
-
-        # Special - for unbounded recurrence we insert a value for "infinity"
-        # that will allow an open-ended time-range to always match it.
-        if component.isRecurringUnbounded():
-            start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-            end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
-            float = False
-            instanceid = self._txn.execSQL(
-                """
-                insert into TIME_RANGE
-                (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
-                 values
-                (%s, %s, %s, %s, %s, %s, %s)
-                 returning
-                INSTANCE_ID
-                """,
-                [
-                    self._calendar._resourceID,
-                    self._resourceID,
-                    float,
-                    start,
-                    end,
-                    icalfbtype_to_indexfbtype["UNKNOWN"],
-                    True,
-                ],
-            )[0][0]
-            peruserdata = component.perUserTransparency(None)
-            for useruid, transp in peruserdata:
-                self._txn.execSQL(
-                    """
-                    insert into TRANSPARENCY
-                    (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
-                     values
-                    (%s, %s, %s)
-                    """,
-                    [
-                        instanceid,
-                        useruid,
-                        transp,
-                    ],
-                )
-
-    def _attachmentPath(self, name):
-        attachmentRoot = self._calendar._home._txn._store.attachmentsPath
-        try:
-            attachmentRoot.createDirectory()
-        except:
-            pass
-        return attachmentRoot.child(
-            "%s-%s-%s-%s.attachment" % (
-                self._calendar._home.uid(), self._calendar.name(),
-                self.name(), name
-            )
-        )
-
-
-    def createAttachmentWithName(self, name, contentType):
-        path = self._attachmentPath(name)
-        attachment = PostgresAttachment(self, path)
-        self._txn.execSQL("""
-            insert into ATTACHMENT (CALENDAR_OBJECT_RESOURCE_ID, CONTENT_TYPE,
-            SIZE, MD5, PATH)
-            values (%s, %s, %s, %s, %s)
-            """,
-            [
-                self._resourceID, generateContentType(contentType), 0, "",
-                attachment._pathValue()
-            ]
-        )
-        return attachment.store(contentType)
-
-
-    def attachments(self):
-        rows = self._txn.execSQL("""
-        select PATH from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s 
-        """, [self._resourceID])
-        for row in rows:
-            demangledName = _pathToName(row[0])
-            yield self.attachmentWithName(demangledName)
-
-
-    def attachmentWithName(self, name):
-        attachment = PostgresAttachment(self, self._attachmentPath(name))
-        if attachment._populate():
-            return attachment
-        else:
-            return None
-
-
-    def removeAttachmentWithName(self, name):
-        attachment = PostgresAttachment(self, self._attachmentPath(name))
-        self._calendar._home._txn.postCommit(attachment._path.remove)
-        self._txn.execSQL("""
-        delete from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s AND
-        PATH = %s
-        """, [self._resourceID, attachment._pathValue()])
-
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Calendar objects is text/calendar.
-        """
-        return MimeType.fromString("text/calendar; charset=utf-8")
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        size = self._txn.execSQL(
-            "select character_length(ICALENDAR_TEXT) from CALENDAR_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return size
-
-
-    def created(self):
-        created = self._txn.execSQL(
-            "select extract(EPOCH from CREATED) from CALENDAR_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(created)
-
-    def modified(self):
-        modified = self._txn.execSQL(
-            "select extract(EPOCH from MODIFIED) from CALENDAR_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(modified)
-
-
-    def attendeesCanManageAttachments(self):
-        return self.component().hasPropertyInAnyComponent("X-APPLE-DROPBOX")
-
-
-
-def _pathToName(path):
-    return path.rsplit(".", 1)[0].split("-", 3)[-1]
-
-
-
-class PostgresAttachment(object):
-
-    implements(IAttachment)
-
-    def __init__(self, calendarObject, path):
-        self._calendarObject = calendarObject
-        self._path = path
-
-
-    @property
-    def _txn(self):
-        return self._calendarObject._txn
-
-
-    def _populate(self):
-        """
-        Execute necessary SQL queries to retrieve attributes.
-
-        @return: C{True} if this attachment exists, C{False} otherwise.
-        """
-        rows = self._txn.execSQL(
-            """
-            select CONTENT_TYPE, SIZE, MD5, extract(EPOCH from CREATED), extract(EPOCH from MODIFIED) from ATTACHMENT where PATH = %s
-            """, [self._pathValue()])
-        if not rows:
-            return False
-        self._contentType = MimeType.fromString(rows[0][0])
-        self._size = rows[0][1]
-        self._md5 = rows[0][2]
-        self._created = int(rows[0][3])
-        self._modified = int(rows[0][4])
-        return True
-
-
-    def store(self, contentType):
-        return PostgresAttachmentStorageTransport(self, contentType)
-
-
-    def retrieve(self, protocol):
-        protocol.dataReceived(self._path.getContent())
-        protocol.connectionLost(Failure(ConnectionLost()))
-
-
-    def properties(self):
-        pass # stub
-
-
-    # IDataStoreResource
-    def contentType(self):
-        return self._contentType
-
-
-    def md5(self):
-        return self._md5
-
-
-    def size(self):
-        return self._size
-
-
-    def created(self):
-        return self._created
-
-    def modified(self):
-        return self._modified
-
-
-    def name(self):
-        return _pathToName(self._pathValue())
-
-
-    def _pathValue(self):
-        """
-        Compute the value which should go into the 'path' column for this
-        attachment.
-        """
-        root = self._calendarObject._calendar._home._txn._store.attachmentsPath
-        return '/'.join(self._path.segmentsFrom(root))
-
-
-
-class PostgresAttachmentStorageTransport(object):
-
-    implements(ITransport)
-
-    def __init__(self, attachment, contentType):
-        self.attachment = attachment
-        self.contentType = contentType
-        self.buf = ''
-        self.hash = hashlib.md5()
-
-
-    @property
-    def _txn(self):
-        return self.attachment._txn
-
-
-    def write(self, data):
-        self.buf += data
-        self.hash.update(data)
-
-
-    def loseConnection(self):
-        self.attachment._path.setContent(self.buf)
-        pathValue = self.attachment._pathValue()
-        contentTypeString = generateContentType(self.contentType)
-        self._txn.execSQL(
-            "update ATTACHMENT set CONTENT_TYPE = %s, SIZE = %s, MD5 = %s, MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) "
-            "WHERE PATH = %s",
-            [contentTypeString, len(self.buf), self.hash.hexdigest(), pathValue]
-        )
-
-
-
-class PostgresLegacyInvitesEmulator(object):
-    """
-    Emulator for the implicit interface specified by
-    L{twistedcaldav.sharing.InvitesDatabase}.
-    """
-
-
-    def __init__(self, calendar):
-        self._calendar = calendar
-
-
-    @property
-    def _txn(self):
-        return self._calendar._txn
-
-
-    def create(self):
-        "No-op, because the index implicitly always exists in the database."
-
-
-    def remove(self):
-        "No-op, because the index implicitly always exists in the database."
-
-
-    def allRecords(self):
-        for row in self._txn.execSQL(
-                """
-                select
-                    INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
-                    CALENDAR_HOME.OWNER_UID, CALENDAR_BIND.BIND_MODE,
-                    CALENDAR_BIND.BIND_STATUS, CALENDAR_BIND.MESSAGE
-                from
-                    INVITE, CALENDAR_HOME, CALENDAR_BIND
-                where
-                    INVITE.RESOURCE_ID = %s and
-                    INVITE.HOME_RESOURCE_ID = 
-                        CALENDAR_HOME.RESOURCE_ID and
-                    CALENDAR_BIND.CALENDAR_RESOURCE_ID =
-                        INVITE.RESOURCE_ID and
-                    CALENDAR_BIND.CALENDAR_HOME_RESOURCE_ID =
-                        INVITE.HOME_RESOURCE_ID
-                order by
-                    INVITE.NAME asc
-                """, [self._calendar._resourceID]):
-            [inviteuid, common_name, userid, ownerUID,
-                bindMode, bindStatus, summary] = row
-            # FIXME: this is really the responsibility of the protocol layer.
-            state = {
-                _BIND_STATUS_INVITED: "NEEDS-ACTION",
-                _BIND_STATUS_ACCEPTED: "ACCEPTED",
-                _BIND_STATUS_DECLINED: "DECLINED",
-                _BIND_STATUS_INVALID: "INVALID",
-            }[bindStatus]
-            access = {
-                _BIND_MODE_READ: "read-only",
-                _BIND_MODE_WRITE: "read-write"
-            }[bindMode]
-            principalURL = "/principals/__uids__/%s/" % (ownerUID,)
-            yield Invite(
-                inviteuid, userid, principalURL, common_name,
-                access, state, summary
-            )
-
-
-    def recordForUserID(self, userid):
-        for record in self.allRecords():
-            if record.userid == userid:
-                return record
-
-
-    def recordForPrincipalURL(self, principalURL):
-        for record in self.allRecords():
-            if record.principalURL == principalURL:
-                return record
-
-
-    def recordForInviteUID(self, inviteUID):
-        for record in self.allRecords():
-            if record.inviteuid == inviteUID:
-                return record
-
-
-    def addOrUpdateRecord(self, record):
-        bindMode = {'read-only': _BIND_MODE_READ,
-                    'read-write': _BIND_MODE_WRITE}[record.access]
-        bindStatus = {
-            "NEEDS-ACTION": _BIND_STATUS_INVITED,
-            "ACCEPTED": _BIND_STATUS_ACCEPTED,
-            "DECLINED": _BIND_STATUS_DECLINED,
-            "INVALID": _BIND_STATUS_INVALID,
-        }[record.state]
-        # principalURL is derived from a directory record's principalURL() so
-        # it will always contain the UID.  The form is '/principals/__uids__/x'
-        # (and may contain a trailing slash).
-        principalUID = record.principalURL.split("/")[3]
-        shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
-            [record.userid]
-        )
-        if rows:
-            [[resourceID, homeResourceID]] = rows
-            # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
-            self._txn.execSQL("""
-                update CALENDAR_BIND set BIND_MODE = %s,
-                BIND_STATUS = %s, MESSAGE = %s
-                where
-                    CALENDAR_RESOURCE_ID = %s and
-                    CALENDAR_HOME_RESOURCE_ID = %s
-            """, [bindMode, bindStatus, record.summary,
-                resourceID, homeResourceID])
-            self._txn.execSQL("""
-                update INVITE set NAME = %s, INVITE_UID = %s
-                where RECIPIENT_ADDRESS = %s
-                """,
-                [record.name, record.inviteuid, record.userid]
-            )
-        else:
-            self._txn.execSQL(
-                """
-                insert into INVITE (
-                    INVITE_UID, NAME,
-                    HOME_RESOURCE_ID, RESOURCE_ID,
-                    RECIPIENT_ADDRESS
-                )
-                values (%s, %s, %s, %s, %s)
-                """,
-                [
-                    record.inviteuid, record.name,
-                    shareeHome._resourceID, self._calendar._resourceID,
-                    record.userid
-                ])
-            self._txn.execSQL(
-                """
-                insert into CALENDAR_BIND
-                (
-                    CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID, 
-                    CALENDAR_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
-                    SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
-                )
-                values (%s, %s, %s, %s, %s, %s, %s, %s)
-                """,
-                [
-                    shareeHome._resourceID,
-                    self._calendar._resourceID,
-                    None, # this is NULL because it is not bound yet, let's be
-                          # explicit about that.
-                    bindMode,
-                    bindStatus,
-                    False,
-                    False,
-                    record.summary
-                ])
-
-
-    def removeRecordForUserID(self, userid):
-        rec = self.recordForUserID(userid)
-        self.removeRecordForInviteUID(rec.inviteuid)
-
-
-    def removeRecordForPrincipalURL(self, principalURL):
-        raise NotImplementedError("removeRecordForPrincipalURL")
-
-
-    def removeRecordForInviteUID(self, inviteUID):
-        rows = self._txn.execSQL("""
-                select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
-                INVITE_UID = %s
-            """, [inviteUID])
-        if rows:
-            [[homeID, resourceID]] = rows
-            self._txn.execSQL(
-                "delete from CALENDAR_BIND where "
-                "CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s",
-                [homeID, resourceID])
-            self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
-                [inviteUID])
-
-
-
-class PostgresLegacySharesEmulator(object):
-
-    def __init__(self, home):
-        self._home = home
-
-
-    @property
-    def _txn(self):
-        return self._home._txn
-
-
-    def create(self):
-        pass
-
-
-    def remove(self):
-        pass
-
-
-    def allRecords(self):
-        # This should have been a smart join that got all these columns at
-        # once, but let's not bother to fix it, since the actual query we
-        # _want_ to do (just look for calendar binds in a particular homes) is
-        # much simpler anyway; we should just do that.
-        shareRows = self._txn.execSQL(
-            """
-            select CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, MESSAGE
-            from CALENDAR_BIND
-                where CALENDAR_HOME_RESOURCE_ID = %s and
-                BIND_MODE != %s and
-                CALENDAR_RESOURCE_NAME is not null
-            """, [self._home._resourceID, _BIND_MODE_OWN])
-        for resourceID, resourceName, summary in shareRows:
-            [[shareuid]] = self._txn.execSQL(
-                """
-                select INVITE_UID
-                from INVITE
-                where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
-                """, [resourceID, self._home._resourceID])
-            sharetype = 'I'
-            [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
-                """
-                select CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME
-                from CALENDAR_BIND
-                where CALENDAR_RESOURCE_ID = %s and
-                    BIND_MODE = %s
-                """, [resourceID, _BIND_MODE_OWN]
-                )
-            [[ownerUID]] = self._txn.execSQL(
-                "select OWNER_UID from CALENDAR_HOME where RESOURCE_ID = %s",
-                [ownerHomeID])
-            hosturl = '/calendars/__uids__/%s/%s' % (
-                ownerUID, ownerResourceName
-            )
-            localname = resourceName
-            record = SharedCollectionRecord(
-                shareuid, sharetype, hosturl, localname, summary
-            )
-            yield record
-
-
-    def _search(self, **kw):
-        [[key, value]] = kw.items()
-        for record in self.allRecords():
-            if getattr(record, key) == value:
-                return record
-
-    def recordForLocalName(self, localname):
-        return self._search(localname=localname)
-
-    def recordForShareUID(self, shareUID):
-        return self._search(shareuid=shareUID)
-
-
-    def addOrUpdateRecord(self, record):
-#        print '*** SHARING***: Adding or updating this record:'
-#        import pprint
-#        pprint.pprint(record.__dict__)
-        # record.hosturl -> /calendars/__uids__/<uid>/<calendarname>
-        splithost = record.hosturl.split('/')
-        ownerUID = splithost[3]
-        ownerCalendarName = splithost[4]
-        ownerHome = self._txn.calendarHomeWithUID(ownerUID)
-        ownerCalendar = ownerHome.calendarWithName(ownerCalendarName)
-        calendarResourceID = ownerCalendar._resourceID
-
-        # There needs to be a bind already, one that corresponds to the
-        # invitation.  The invitation's UID is the same as the share UID.  I
-        # just need to update its 'localname', i.e.
-        # CALENDAR_BIND.CALENDAR_RESOURCE_NAME.
-
-        self._txn.execSQL(
-            """
-            update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s
-            where CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s
-            """,
-            [record.localname, self._home._resourceID, calendarResourceID]
-        )
-
-
-    def removeRecordForLocalName(self, localname):
-        self._txn.execSQL(
-            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
-            "and CALENDAR_HOME_RESOURCE_ID = %s",
-            [localname, self._home._resourceID]
-        )
-
-
-    def removeRecordForShareUID(self, shareUID):
-        pass
-#        c = self._home._cursor()
-#        c.execute(
-#            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
-#            "and CALENDAR_HOME_RESOURCE_ID = %s",
-#            [self._home._resourceID]
-#        )
-
-
-
-class postgresqlgenerator(sqlgenerator):
-    """
-    Query generator for postgreSQL indexed searches.  (Currently unused: work
-    in progress.)
-    """
-
-    ISOP = " = "
-    CONTAINSOP = " LIKE "
-    NOTCONTAINSOP = " NOT LIKE "
-    FIELDS = {
-        "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
-        "UID":  "CALENDAR_OBJECT.ICALENDAR_UID",
-    }
-
-    def __init__(self, expr, calendarid, userid):
-        self.RESOURCEDB = "CALENDAR_OBJECT"
-        self.TIMESPANDB = "TIME_RANGE"
-        self.TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
-        self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
-        self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
-        self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND CALENDAR_OBJECT.CALENDAR_RESOURCE_ID = %s"
-        self.TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
-
-        super(postgresqlgenerator, self).__init__(expr, calendarid, userid)
-
-
-    def generate(self):
-        """
-        Generate the actual SQL 'where ...' expression from the passed in
-        expression tree.
-        
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
-            partial SQL statement, and the C{list} is the list of argument
-            substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.sout = StringIO.StringIO()
-        self.arguments = []
-        self.substitutions = []
-        self.usedtimespan = False
-
-        # Generate ' where ...' partial statement
-        self.sout.write(self.WHERE)
-        self.generateExpression(self.expression)
-
-        # Prefix with ' from ...' partial statement
-        select = self.FROM + self.RESOURCEDB
-        if self.usedtimespan:
-            self.frontArgument(self.userid)
-            select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
-                self.TIMESPANDB,
-                self.TRANSPARENCYDB,
-                self.TIMESPANTEST_JOIN_ON_PIECE
-            )
-        select += self.sout.getvalue()
-
-        select = select % tuple(self.substitutions)
-
-        return select, self.arguments
-
-
-    def addArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-        self.sout.write("%s")
-
-    def setArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-
-    def frontArgument(self, arg):
-        self.arguments.insert(0, arg)
-        self.substitutions.insert(0, "%s")
-
-    def containsArgument(self, arg):
-        return "%%%s%%" % (arg,)
-
-
-class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
-    def __init__(self, index, cachePool=None):
-        self.index = index
-        self._cachePool = cachePool
-
-    def _key(self, uid):
-        return 'reservation:%s' % (
-            hashlib.md5('%s:%s' % (uid,
-                                   self.index.resource._resourceID)).hexdigest())
-
-    def reserveUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Reserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _handleFalse(result):
-            if result is False:
-                raise ReservationError(
-                    "UID %s already reserved for calendar collection %s."
-                    % (uid, self.index.resource._name)
-                    )
-
-        d = self.getCachePool().add(self._key(uid),
-                                    'reserved',
-                                    expireTime=config.UIDReservationTimeOut)
-        d.addCallback(_handleFalse)
-        return d
-
-
-    def unreserveUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Unreserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _handleFalse(result):
-            if result is False:
-                raise ReservationError(
-                    "UID %s is not reserved for calendar collection %s."
-                    % (uid, self.index.resource._resourceID)
-                    )
-
-        d = self.getCachePool().delete(self._key(uid))
-        d.addCallback(_handleFalse)
-        return d
-
-
-    def isReservedUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Is reserved UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        def _checkValue((flags, value)):
-            if value is None:
-                return False
-            else:
-                return True
-
-        d = self.getCachePool().get(self._key(uid))
-        d.addCallback(_checkValue)
-        return d
-
-class DummyUIDReserver(LoggingMixIn):
-
-    def __init__(self, index):
-        self.index = index
-        self.reservations = set()
-
-    def _key(self, uid):
-        return 'reservation:%s' % (
-            hashlib.md5('%s:%s' % (uid,
-                                   self.index.resource._resourceID)).hexdigest())
-
-    def reserveUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Reserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        key = self._key(uid)
-        if key in self.reservations:
-            raise ReservationError(
-                "UID %s already reserved for calendar collection %s."
-                % (uid, self.index.resource._name)
-                )
-        self.reservations.add(key)
-        return succeed(None)
-
-
-    def unreserveUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Unreserving UID %r @ %r" % (
-                uid,
-                self.index.resource))
-
-        key = self._key(uid)
-        if key in self.reservations:
-            self.reservations.remove(key)
-        return succeed(None)
-
-
-    def isReservedUID(self, uid):
-        uid = uid.encode('utf-8')
-        self.log_debug("Is reserved UID %r @ %r" % (
-                uid,
-                self.index.resource))
-        key = self._key(uid)
-        return succeed(key in self.reservations)
-
-class PostgresLegacyIndexEmulator(LoggingMixIn):
-    """
-    Emulator for L{twistedcaldv.index.Index} and
-    L{twistedcaldv.index.IndexSchedule}.
-    """
-
-    def __init__(self, calendar):
-        self.resource = self.calendar = calendar
-        if (
-            hasattr(config, "Memcached") and
-            config.Memcached.Pools.Default.ClientEnabled
-        ):
-            self.reserver = MemcachedUIDReserver(self)
-        else:
-            # This is only used with unit tests
-            self.reserver = DummyUIDReserver(self)
-
-    @property
-    def _txn(self):
-        return self.calendar._txn
-
-
-    def reserveUID(self, uid):
-        if self.calendar._name == "inbox":
-            return succeed(None)
-        else:
-            return self.reserver.reserveUID(uid)
-
-
-    def unreserveUID(self, uid):
-        if self.calendar._name == "inbox":
-            return succeed(None)
-        else:
-            return self.reserver.unreserveUID(uid)
-
-
-    def isReservedUID(self, uid):
-        if self.calendar._name == "inbox":
-            return succeed(False)
-        else:
-            return self.reserver.isReservedUID(uid)
-
-
-    def isAllowedUID(self, uid, *names):
-        """
-        Checks to see whether to allow an operation which would add the
-        specified UID to the index.  Specifically, the operation may not
-        violate the constraint that UIDs must be unique.
-        @param uid: the UID to check
-        @param names: the names of resources being replaced or deleted by the
-            operation; UIDs associated with these resources are not checked.
-        @return: True if the UID is not in the index and is not reserved,
-            False otherwise.
-        """
-        if self.calendar._name == "inbox":
-            return True
-        else:
-            rname = self.resourceNameForUID(uid)
-            return (rname is None or rname in names)
-
-    def resourceUIDForName(self, name):
-        obj = self.calendar.calendarObjectWithName(name)
-        if obj is None:
-            return None
-        return obj.uid()
-
-
-    def resourceNameForUID(self, uid):
-        obj = self.calendar.calendarObjectWithUID(uid)
-        if obj is None:
-            return None
-        return obj.name()
-
-
-    def notExpandedBeyond(self, minDate):
-        """
-        Gives all resources which have not been expanded beyond a given date
-        in the database.  (Unused; see above L{postgresqlgenerator}.
-        """
-        return [row[0] for row in self._txn.execSQL(
-            "select RESOURCE_NAME from CALENDAR_OBJECT "
-            "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
-            [normalizeForIndex(minDate), self.calendar._resourceID]
-        )]
-
-
-    def reExpandResource(self, name, expand_until):
-        """
-        Given a resource name, remove it from the database and re-add it
-        with a longer expansion.
-        """
-        obj = self.calendar.calendarObjectWithName(name)
-        obj.updateDatabase(obj.component(), expand_until=expand_until, reCreate=True)
-
-    def testAndUpdateIndex(self, minDate):
-        # Find out if the index is expanded far enough
-        names = self.notExpandedBeyond(minDate)
-
-        # Actually expand recurrence max
-        for name in names:
-            self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
-            self.reExpandResource(name, minDate)
-
-    def whatchanged(self, revision):
-
-        results = [
-            (name.encode("utf-8"), deleted)
-            for name, deleted in
-            self._txn.execSQL(
-                """select RESOURCE_NAME, DELETED from CALENDAR_OBJECT_REVISIONS
-                   where REVISION > %s and CALENDAR_RESOURCE_ID = %s""",
-                [revision, self.calendar._resourceID],
-            )
-        ]
-        results.sort(key=lambda x:x[1])
-        
-        changed = []
-        deleted = []
-        for name, wasdeleted in results:
-            if name:
-                if wasdeleted:
-                    if revision:
-                        deleted.append(name)
-                else:
-                    changed.append(name)
-            else:
-                raise SyncTokenValidException
-        
-        return changed, deleted,
-
-    def indexedSearch(self, filter, useruid='', fbtype=False):
-        """
-        Finds resources matching the given qualifiers.
-        @param filter: the L{Filter} for the calendar-query to execute.
-        @return: an iterable of tuples for each resource matching the
-            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
-            C{name} is the resource name, C{uid} is the resource UID, and
-            C{type} is the resource iCalendar component type.x
-        """
-
-        # Make sure we have a proper Filter element and get the partial SQL
-        # statement to use.
-        if isinstance(filter, calendarqueryfilter.Filter):
-            qualifiers = calendarquery.sqlcalendarquery(filter, self.calendar._resourceID, useruid, generator=postgresqlgenerator)
-            if qualifiers is not None:
-                # Determine how far we need to extend the current expansion of
-                # events. If we have an open-ended time-range we will expand one
-                # year past the start. That should catch bounded recurrences - unbounded
-                # will have been indexed with an "infinite" value always included.
-                maxDate, isStartDate = filter.getmaxtimerange()
-                if maxDate:
-                    maxDate = maxDate.date()
-                    if isStartDate:
-                        maxDate += datetime.timedelta(days=365)
-                    self.testAndUpdateIndex(maxDate)
-            else:
-                # We cannot handler this filter in an indexed search
-                raise IndexedSearchException()
-
-        else:
-            qualifiers = None
-
-        # Perform the search
-        if qualifiers is None:
-            rowiter = self._txn.execSQL(
-                "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
-                [self.calendar._resourceID, ],
-            )
-        else:
-            if fbtype:
-                # For a free-busy time-range query we return all instances
-                rowiter = self._txn.execSQL(
-                    """select DISTINCT
-                        CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE, CALENDAR_OBJECT.ORGANIZER,
-                        TIME_RANGE.FLOATING, TIME_RANGE.START_DATE, TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT""" +
-                    qualifiers[0],
-                    qualifiers[1]
-                )
-            else:
-                rowiter = self._txn.execSQL(
-                    "select DISTINCT CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE" +
-                    qualifiers[0],
-                    qualifiers[1]
-                )
-
-        # Check result for missing resources
-
-        for row in rowiter:
-            if fbtype:
-                row = list(row)
-                row[4] = 'Y' if row[4] else 'N'
-                row[7] = indexfbtype_to_icalfbtype[row[7]]
-                row[8] = 'T' if row[9] else 'F'
-                del row[9]
-            yield row
-
-
-    def bruteForceSearch(self):
-        return self._txn.execSQL(
-            "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from "
-            "CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
-            [self.calendar._resourceID]
-        )
-
-
-    def resourcesExist(self, names):
-        return list(set(names).intersection(
-            set(self.calendar.listCalendarObjects())))
-
-
-    def resourceExists(self, name):
-        return bool(
-            self._txn.execSQL(
-                "select RESOURCE_NAME from CALENDAR_OBJECT where "
-                "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
-                [name, self.calendar._resourceID]
-            )
-        )
-
-
-
-class PostgresCalendar(CalendarSyncTokenHelper):
-
-    implements(ICalendar)
-
-    def __init__(self, home, name, resourceID, notifier):
-        self._home = home
-        self._name = name
-        self._resourceID = resourceID
-        self._objects = {}
-        self._notifier = notifier
-
-
-    @property
-    def _txn(self):
-        return self._home._txn
-
-
-    def setSharingUID(self, uid):
-        self.properties()._setPerUserUID(uid)
-
-
-    def retrieveOldInvites(self):
-        return PostgresLegacyInvitesEmulator(self)
-
-    def retrieveOldIndex(self):
-        return PostgresLegacyIndexEmulator(self)
-
-
-    def notifierID(self, label="default"):
-        if self._notifier:
-            return self._notifier.getID(label)
-        else:
-            return None
-
-
-    def name(self):
-        return self._name
-
-
-    def rename(self, name):
-        oldName = self._name
-        self._txn.execSQL(
-            "update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s "
-            "where CALENDAR_RESOURCE_ID = %s AND "
-            "CALENDAR_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID, self._home._resourceID]
-        )
-        self._name = name
-        # update memos
-        del self._home._calendars[oldName]
-        self._home._calendars[name] = self
-        self._updateSyncToken()
-
-
-    def ownerCalendarHome(self):
-        return self._home
-
-
-    def listCalendarObjects(self):
-        # FIXME: see listChildren
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from "
-            "CALENDAR_OBJECT where "
-            "CALENDAR_RESOURCE_ID = %s",
-            [self._resourceID])
-        return [row[0] for row in rows]
-
-
-    def calendarObjects(self):
-        for name in self.listCalendarObjects():
-            yield self.calendarObjectWithName(name)
-
-
-    @memoized('name', '_objects')
-    def calendarObjectWithName(self, name):
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID from CALENDAR_OBJECT where "
-            "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if not rows:
-            return None
-        resid = rows[0][0]
-        return PostgresCalendarObject(self, name, resid)
-
-
-    @memoized('uid', '_objects')
-    def calendarObjectWithUID(self, uid):
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID, RESOURCE_NAME from CALENDAR_OBJECT where "
-            "ICALENDAR_UID = %s and CALENDAR_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        if not rows:
-            return None
-        resid = rows[0][0]
-        name = rows[0][1]
-        return PostgresCalendarObject(self, name, resid)
-
-
-    def createCalendarObjectWithName(self, name, component):
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from CALENDAR_OBJECT where "
-            " RESOURCE_NAME = %s AND CALENDAR_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if rows:
-            raise ObjectResourceNameAlreadyExistsError()
-
-        calendarObject = PostgresCalendarObject(self, name, None)
-        calendarObject.component = lambda : component
-
-        validateCalendarComponent(calendarObject, self, component)
-
-        calendarObject.updateDatabase(component, inserting=True)
-        self._insertRevision(name)
-
-        if self._notifier:
-            self._home._txn.postCommit(self._notifier.notify)
-
-
-    def removeCalendarObjectWithName(self, name):
-        self._txn.execSQL(
-            "delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and "
-            "CALENDAR_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if self._txn._cursor.rowcount == 0:
-            raise NoSuchObjectResourceError()
-        self._objects.pop(name, None)
-        self._deleteRevision(name)
-
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    def removeCalendarObjectWithUID(self, uid):
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from CALENDAR_OBJECT where "
-            "ICALENDAR_UID = %s AND CALENDAR_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        if not rows:
-            raise NoSuchObjectResourceError()
-        name = rows[0][0]
-        self._txn.execSQL(
-            "delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and "
-            "CALENDAR_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        self._objects.pop(name, None)
-        self._objects.pop(uid, None)
-        self._deleteRevision(name)
-
-        if self._notifier:
-            self._home._txn.postCommit(self._notifier.notify)
-
-
-    def calendarObjectsInTimeRange(self, start, end, timeZone):
-        raise NotImplementedError()
-
-
-    def calendarObjectsSinceToken(self, token):
-        raise NotImplementedError()
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.ownerCalendarHome().uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Calendar objects is text/calendar.
-        """
-        return MimeType.fromString("text/calendar; charset=utf-8")
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        return 0
-
-
-    def created(self):
-        created = self._txn.execSQL(
-            "select extract(EPOCH from CREATED) from CALENDAR where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(created)
-
-    def modified(self):
-        modified = self._txn.execSQL(
-            "select extract(EPOCH from MODIFIED) from CALENDAR where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(modified)
-
-
-class PostgresCalendarHome(object):
-
-    implements(ICalendarHome)
-
-    def __init__(self, transaction, ownerUID, resourceID, notifier):
-        self._txn = transaction
-        self._ownerUID = ownerUID
-        self._resourceID = resourceID
-        self._calendars = {}
-        self._notifier = notifier
-
-
-    def retrieveOldShares(self):
-        return PostgresLegacySharesEmulator(self)
-
-
-    def uid(self):
-        """
-        Retrieve the unique identifier for this calendar home.
-
-        @return: a string.
-        """
-        return self._ownerUID
-
-
-    def name(self):
-        """
-        Implement L{IDataStoreResource.name} to return the uid.
-        """
-        return self.uid()
-
-
-    def transaction(self):
-        return self._txn
-
-
-    def listChildren(self):
-        """
-        Retrieve the names of the children in this calendar home.
-
-        @return: an iterable of C{str}s.
-        """
-        # FIXME: not specified on the interface or exercised by the tests, but
-        # required by clients of the implementation!
-        rows = self._txn.execSQL(
-            "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
-            "CALENDAR_HOME_RESOURCE_ID = %s "
-            "and BIND_MODE = %s ",
-            # Right now, we only show owned calendars.
-            [self._resourceID, _BIND_MODE_OWN]
-        )
-        names = [row[0] for row in rows]
-        return names
-
-
-    def createdHome(self):
-        self.createCalendarWithName("calendar")
-        defaultCal = self.calendarWithName("calendar")
-        props = defaultCal.properties()
-        props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(
-            Opaque())
-        self.createCalendarWithName("inbox")
-
-    def calendars(self):
-        """
-        Retrieve calendars contained in this calendar home.
-
-        @return: an iterable of L{ICalendar}s.
-        """
-        names = self.listChildren()
-        for name in names:
-            yield self.calendarWithName(name)
-
-
-    @memoized('name', '_calendars')
-    def calendarWithName(self, name):
-        """
-        Retrieve the calendar with the given C{name} contained in this
-        calendar home.
-
-        @param name: a string.
-        @return: an L{ICalendar} or C{None} if no such calendar
-            exists.
-        """
-        data = self._txn.execSQL(
-            "select CALENDAR_RESOURCE_ID from CALENDAR_BIND where "
-            "CALENDAR_RESOURCE_NAME = %s and CALENDAR_HOME_RESOURCE_ID = %s "
-            "and BIND_MODE = %s",
-            [name, self._resourceID, _BIND_MODE_OWN]
-        )
-        if not data:
-            return None
-        resourceID = data[0][0]
-        if self._notifier:
-            childID = "%s/%s" % (self.uid(), name)
-            notifier = self._notifier.clone(label="collection", id=childID)
-        else:
-            notifier = None
-        return PostgresCalendar(self, name, resourceID, notifier)
-
-
-    def calendarObjectWithDropboxID(self, dropboxID):
-        """
-        Implement lookup with brute-force scanning.
-        """
-        for calendar in self.calendars():
-            for calendarObject in calendar.calendarObjects():
-                if dropboxID == calendarObject.dropboxID():
-                    return calendarObject
-
-
-    def createCalendarWithName(self, name):
-        rows = self._txn.execSQL(
-            "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
-            "CALENDAR_RESOURCE_NAME = %s AND "
-            "CALENDAR_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if rows:
-            raise HomeChildNameAlreadyExistsError()
-        rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
-        resourceID = rows[0][0]
-        self._txn.execSQL(
-            "insert into CALENDAR (RESOURCE_ID) values "
-            "(%s)",
-            [resourceID])
-
-        self._txn.execSQL("""
-            insert into CALENDAR_BIND (
-                CALENDAR_HOME_RESOURCE_ID,
-                CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, BIND_MODE,
-                SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
-            %s, %s, %s, %s, %s, %s, %s)
-            """,
-            [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
-             _BIND_STATUS_ACCEPTED]
-        )
-
-        calendarType = ResourceType.calendar #@UndefinedVariable
-        newCalendar = self.calendarWithName(name)
-        newCalendar.properties()[
-            PropertyName.fromElement(ResourceType)] = calendarType
-        newCalendar._updateSyncToken()
-
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    def removeCalendarWithName(self, name):
-        self._txn.execSQL(
-            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s and "
-            "CALENDAR_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        self._calendars.pop(name, None)
-        if self._txn._cursor.rowcount == 0:
-            raise NoSuchHomeChildError()
-        # FIXME: the schema should probably cascade the calendar delete when
-        # the last bind is deleted.
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Calendar objects is text/calendar.
-        """
-        return None
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        return 0
-
-
-    def created(self):
-        return None
-
-
-    def modified(self):
-        return None
-
-
-    def notifierID(self, label="default"):
-        if self._notifier:
-            return self._notifier.getID(label)
-        else:
-            return None
-
-
-
-class PostgresNotificationObject(object):
-    implements(INotificationObject)
-
-    def __init__(self, home, resourceID):
-        self._home = home
-        self._resourceID = resourceID
-
-
-    def notificationCollection(self):
-        return self._home
-
-
-    def name(self):
-        return self.uid() + ".xml"
-
-
-    def contentType(self):
-        """
-        The content type of NotificationObjects is text/xml.
-        """
-        return MimeType.fromString("text/xml")
-
-
-    @property
-    def _txn(self):
-        return self._home._txn
-
-
-    def setData(self, uid, xmltype, xmldata):
-        self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
-        return self._txn.execSQL(
-            """
-            update NOTIFICATION set NOTIFICATION_UID = %s, XML_TYPE = %s,
-            XML_DATA = %s where RESOURCE_ID = %s
-            """,
-            [uid, xmltype, xmldata, self._resourceID]
-        )
-
-
-    def _fieldQuery(self, field):
-        [[data]] = self._txn.execSQL(
-            "select " + field + " from NOTIFICATION where "
-            "RESOURCE_ID = %s",
-            [self._resourceID])
-        return data
-
-
-    def xmldata(self):
-        return self._fieldQuery("XML_DATA")
-
-
-    def uid(self):
-        return self._fieldQuery("NOTIFICATION_UID")
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self._home.uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    def md5(self):
-        return hashlib.md5(self.xmldata()).hexdigest()
-
-
-    def modified(self):
-        modified = self._txn.execSQL(
-            "select extract(EPOCH from MODIFIED) from NOTIFICATION where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(modified)
-
-
-    def created(self):
-        return None
-
-
-    def size(self):
-        return len(self.xmldata())
-
-
-
-class PostgresLegacyNotificationsEmulator(object):
-    def __init__(self, notificationsCollection):
-        self._collection = notificationsCollection
-
-
-    def _recordForObject(self, notificationObject):
-        return NotificationRecord(
-            notificationObject.uid(),
-            notificationObject.name(),
-            notificationObject._fieldQuery("XML_TYPE"))
-
-
-    def recordForName(self, name):
-        return self._recordForObject(
-            self._collection.notificationObjectWithName(name)
-        )
-
-
-    def recordForUID(self, uid):
-        return self._recordForObject(
-            self._collection.notificationObjectWithUID(uid)
-        )
-
-
-    def removeRecordForUID(self, uid):
-        self._collection.removeNotificationObjectWithUID(uid)
-
-
-    def removeRecordForName(self, name):
-        self._collection.removeNotificationObjectWithName(name)
-
-
-
-class PostgresNotificationCollection(object):
-
-    implements(INotificationCollection)
-
-    def __init__(self, txn, uid, resourceID):
-        self._txn = txn
-        self._uid = uid
-        self._resourceID = resourceID
-        self._notifications = {}
-
-
-    def retrieveOldIndex(self):
-        return PostgresLegacyNotificationsEmulator(self)
-
-
-    def name(self):
-        return 'notification'
-
-
-    def uid(self):
-        return self._uid
-
-
-    def notificationObjects(self):
-        for [uid] in self._txn.execSQL(
-                "select (NOTIFICATION_UID) "
-                "from NOTIFICATION "
-                "where NOTIFICATION_HOME_RESOURCE_ID = %s",
-                [self._resourceID]):
-            yield self.notificationObjectWithUID(uid)
-
-
-    def _nameToUID(self, name):
-        """
-        Based on the file-backed implementation, the 'name' is just uid +
-        ".xml".
-        """
-        return name.rsplit(".", 1)[0]
-
-
-    def notificationObjectWithName(self, name):
-        return self.notificationObjectWithUID(self._nameToUID(name))
-
-
-    @memoized('uid', '_notifications')
-    def notificationObjectWithUID(self, uid):
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID from NOTIFICATION where NOTIFICATION_UID = %s"
-            " and NOTIFICATION_HOME_RESOURCE_ID = %s",
-            [uid, self._resourceID])
-        if rows:
-            [[resourceID]] = rows
-            return PostgresNotificationObject(self, resourceID)
-        else:
-            return None
-
-
-    def writeNotificationObject(self, uid, xmltype, xmldata):
-        xmltypeString = xmltype.toxml()
-        existing = self._txn.execSQL("select NOTIFICATION_UID from NOTIFICATION where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
-            [self._resourceID, uid])
-        if existing:
-            self._txn.execSQL(
-                "update NOTIFICATION set XML_TYPE = %s, XML_DATA = %s where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
-                [xmltypeString, xmldata, self._resourceID, uid])
-        else:
-            self._txn.execSQL(
-                "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
-                "values (%s, %s, %s, %s)", [self._resourceID, uid, xmltypeString, xmldata])
-        notificationObject = self.notificationObjectWithUID(uid)
-        notificationObject.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
-
-
-    def removeNotificationObjectWithName(self, name):
-        self.removeNotificationObjectWithUID(self._nameToUID(name))
-
-
-    def removeNotificationObjectWithUID(self, uid):
-        self._txn.execSQL(
-            "delete from NOTIFICATION where NOTIFICATION_UID = %s and "
-            "NOTIFICATION_HOME_RESOURCE_ID = %s",
-            [uid, self._resourceID])
-
-
-    def syncToken(self):
-        return 'dummy-sync-token'
-
-
-    def notificationObjectsSinceToken(self, token):
-        changed = []
-        removed = []
-        token = self.syncToken()
-        return (changed, removed, token)
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self._uid,
-            self._txn,
-            self._resourceID
-        )
-
-
-
-# CARDDAV
-
-class PostgresAddressBookObject(object):
-
-    implements(IAddressBookObject)
-
-    def __init__(self, addressbook, name, resid):
-        self._addressbook = addressbook
-        self._name = name
-        self._resourceID = resid
-        self._vCardText = None
-
-
-    @property
-    def _txn(self):
-        return self._addressbook._txn
-
-
-    def uid(self):
-        return self.component().resourceUID()
-
-
-    def name(self):
-        return self._name
-
-
-    def addressbook(self):
-        return self._addressbook
-
-
-    def vCardText(self):
-        if self._vCardText is None:
-            text = self._txn.execSQL(
-                "select VCARD_TEXT from ADDRESSBOOK_OBJECT where "
-                "RESOURCE_ID = %s", [self._resourceID]
-            )[0][0]
-            self._vCardText = text
-            return text
-        else:
-            return self._vCardText
-
-
-    def component(self):
-        return VCard.fromString(self.vCardText())
-
-
-    def componentType(self):
-        return self.component().mainType()
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    def setComponent(self, component):
-        validateAddressBookComponent(self, self._addressbook, component)
-
-        self.updateDatabase(component)
-        self._addressbook._updateRevision(self._name)
-
-        if self._addressbook._notifier:
-            self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
-
-    def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
-        """
-        Update the database tables for the new data being written.
-
-        @param component: calendar data to store
-        @type component: L{Component}
-        """
-
-        componentText = str(component)
-        self._vCardText = componentText
-
-        # CALENDAR_OBJECT table update
-        if inserting:
-            self._resourceID = self._txn.execSQL(
-                """
-                insert into ADDRESSBOOK_OBJECT
-                (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID)
-                 values
-                (%s, %s, %s, %s)
-                 returning RESOURCE_ID
-                """,
-                [
-                    self._addressbook._resourceID,
-                    self._name,
-                    componentText,
-                    component.resourceUID(),
-                ]
-            )[0][0]
-        else:
-            self._txn.execSQL(
-                """
-                update ADDRESSBOOK_OBJECT set
-                (VCARD_TEXT, VCARD_UID, MODIFIED)
-                 =
-                (%s, %s, timezone('UTC', CURRENT_TIMESTAMP))
-                 where RESOURCE_ID = %s
-                """,
-                [
-                    componentText,
-                    component.resourceUID(),
-                    self._resourceID
-                ]
-            )
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Addressbook objects is text/x-vcard.
-        """
-        return MimeType.fromString("text/vcard; charset=utf-8")
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        size = self._txn.execSQL(
-            "select character_length(VCARD_TEXT) from ADDRESSBOOK_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return size
-
-
-    def created(self):
-        created = self._txn.execSQL(
-            "select extract(EPOCH from CREATED) from ADDRESSBOOK_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(created)
-
-
-    def modified(self):
-        modified = self._txn.execSQL(
-            "select extract(EPOCH from MODIFIED) from ADDRESSBOOK_OBJECT where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(modified)
-
-
-
-class postgresqladbkgenerator(sqlgenerator):
-    """
-    Query generator for postgreSQL indexed searches.  (Currently unused: work
-    in progress.)
-    """
-
-    ISOP = " = "
-    CONTAINSOP = " LIKE "
-    NOTCONTAINSOP = " NOT LIKE "
-    FIELDS = {
-        "UID":  "ADDRESSBOOK_OBJECT.VCARD_UID",
-    }
-
-    def __init__(self, expr, addressbookid):
-        self.RESOURCEDB = "ADDRESSBOOK_OBJECT"
-
-        super(postgresqladbkgenerator, self).__init__(expr, addressbookid)
-
-
-    def generate(self):
-        """
-        Generate the actual SQL 'where ...' expression from the passed in
-        expression tree.
-        
-        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
-            partial SQL statement, and the C{list} is the list of argument
-            substitutions to use with the SQL API execute method.
-        """
-
-        # Init state
-        self.sout = StringIO.StringIO()
-        self.arguments = []
-        self.substitutions = []
-
-        # Generate ' where ...' partial statement
-        self.sout.write(self.WHERE)
-        self.generateExpression(self.expression)
-
-        # Prefix with ' from ...' partial statement
-        select = self.FROM + self.RESOURCEDB
-        select += self.sout.getvalue()
-
-        select = select % tuple(self.substitutions)
-
-        return select, self.arguments
-
-
-    def addArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-        self.sout.write("%s")
-
-    def setArgument(self, arg):
-        self.arguments.append(arg)
-        self.substitutions.append("%s")
-
-    def frontArgument(self, arg):
-        self.arguments.insert(0, arg)
-        self.substitutions.insert(0, "%s")
-
-    def containsArgument(self, arg):
-        return "%%%s%%" % (arg,)
-
-
-class PostgresLegacyABIndexEmulator(object):
-    """
-    Emulator for L{twistedcaldv.index.Index} and
-    L{twistedcaldv.index.IndexSchedule}.
-    """
-
-    def __init__(self, addressbook):
-        self.resource = self.addressbook = addressbook
-        if (
-            hasattr(config, "Memcached") and
-            config.Memcached.Pools.Default.ClientEnabled
-        ):
-            self.reserver = MemcachedUIDReserver(self)
-        else:
-            # This is only used with unit tests
-            self.reserver = DummyUIDReserver(self)
-
-
-    @property
-    def _txn(self):
-        return self.addressbook._txn
-
-
-    def reserveUID(self, uid):
-        return self.reserver.reserveUID(uid)
-
-
-    def unreserveUID(self, uid):
-        return self.reserver.unreserveUID(uid)
-
-
-    def isReservedUID(self, uid):
-        return self.reserver.isReservedUID(uid)
-
-
-    def isAllowedUID(self, uid, *names):
-        """
-        Checks to see whether to allow an operation which would add the
-        specified UID to the index.  Specifically, the operation may not
-        violate the constraint that UIDs must be unique.
-        @param uid: the UID to check
-        @param names: the names of resources being replaced or deleted by the
-            operation; UIDs associated with these resources are not checked.
-        @return: True if the UID is not in the index and is not reserved,
-            False otherwise.
-        """
-        rname = self.resourceNameForUID(uid)
-        return (rname is None or rname in names)
-
-
-    def resourceUIDForName(self, name):
-        obj = self.addressbook.addressbookObjectWithName(name)
-        if obj is None:
-            return None
-        return obj.uid()
-
-
-    def resourceNameForUID(self, uid):
-        obj = self.addressbook.addressbookObjectWithUID(uid)
-        if obj is None:
-            return None
-        return obj.name()
-
-
-    def whatchanged(self, revision):
-
-        results = [
-            (name.encode("utf-8"), deleted)
-            for name, deleted in
-            self._txn.execSQL(
-                """select RESOURCE_NAME, DELETED from ADDRESSBOOK_OBJECT_REVISIONS
-                   where REVISION > %s and ADDRESSBOOK_RESOURCE_ID = %s""",
-                [revision, self.addressbook._resourceID],
-            )
-        ]
-        results.sort(key=lambda x:x[1])
-        
-        changed = []
-        deleted = []
-        for name, wasdeleted in results:
-            if name:
-                if wasdeleted:
-                    if revision:
-                        deleted.append(name)
-                else:
-                    changed.append(name)
-            else:
-                raise SyncTokenValidException
-        
-        return changed, deleted,
-
-    def searchValid(self, filter):
-        if isinstance(filter, carddavxml.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(filter)
-        else:
-            qualifiers = None
-
-        return qualifiers is not None
-
-    def search(self, filter):
-        """
-        Finds resources matching the given qualifiers.
-        @param filter: the L{Filter} for the addressbook-query to execute.
-        @return: an iterable of tuples for each resource matching the
-            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
-            C{name} is the resource name, C{uid} is the resource UID, and
-            C{type} is the resource iCalendar component type.x
-        """
-
-        # Make sure we have a proper Filter element and get the partial SQL statement to use.
-        if isinstance(filter, carddavxml.Filter):
-            qualifiers = addressbookquery.sqladdressbookquery(filter, self.addressbook._resourceID, generator=postgresqladbkgenerator)
-        else:
-            qualifiers = None
-        if qualifiers is not None:
-            rowiter = self._txn.execSQL(
-                "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
-                qualifiers[0],
-                qualifiers[1]
-            )
-        else:
-            rowiter = self._txn.execSQL(
-                "select RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
-                [self.addressbook._resourceID, ],
-            )
-
-        for row in rowiter:
-            yield row
-
-    def indexedSearch(self, filter, useruid='', fbtype=False):
-        """
-        Always raise L{IndexedSearchException}, since these indexes are not
-        fully implemented yet.
-        """
-        raise IndexedSearchException()
-
-
-    def bruteForceSearch(self):
-        return self._txn.execSQL(
-            "select RESOURCE_NAME, VCARD_UID from "
-            "ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
-            [self.addressbook._resourceID]
-        )
-
-
-    def resourcesExist(self, names):
-        return list(set(names).intersection(
-            set(self.addressbook.listAddressbookObjects())))
-
-
-    def resourceExists(self, name):
-        return bool(
-            self._txn.execSQL(
-                "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
-                "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
-                [name, self.addressbook._resourceID]
-            )
-        )
-
-
-class PostgresLegacyABInvitesEmulator(object):
-    """
-    Emulator for the implicit interface specified by
-    L{twistedcaldav.sharing.InvitesDatabase}.
-    """
-
-
-    def __init__(self, addressbook):
-        self._addressbook = addressbook
-
-
-    @property
-    def _txn(self):
-        return self._addressbook._txn
-
-
-    def create(self):
-        "No-op, because the index implicitly always exists in the database."
-
-
-    def remove(self):
-        "No-op, because the index implicitly always exists in the database."
-
-
-    def allRecords(self):
-        for row in self._txn.execSQL(
-                """
-                select
-                    INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
-                    ADDRESSBOOK_HOME.OWNER_UID, ADDRESSBOOK_BIND.BIND_MODE,
-                    ADDRESSBOOK_BIND.BIND_STATUS, ADDRESSBOOK_BIND.MESSAGE
-                from
-                    INVITE, ADDRESSBOOK_HOME, ADDRESSBOOK_BIND
-                where
-                    INVITE.RESOURCE_ID = %s and
-                    INVITE.HOME_RESOURCE_ID = 
-                        ADDRESSBOOK_HOME.RESOURCE_ID and
-                    ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_ID =
-                        INVITE.RESOURCE_ID and
-                    ADDRESSBOOK_BIND.ADDRESSBOOK_HOME_RESOURCE_ID =
-                        INVITE.HOME_RESOURCE_ID
-                order by
-                    INVITE.NAME asc
-                """, [self._addressbook._resourceID]):
-            [inviteuid, common_name, userid, ownerUID,
-                bindMode, bindStatus, summary] = row
-            # FIXME: this is really the responsibility of the protocol layer.
-            state = {
-                _BIND_STATUS_INVITED: "NEEDS-ACTION",
-                _BIND_STATUS_ACCEPTED: "ACCEPTED",
-                _BIND_STATUS_DECLINED: "DECLINED",
-                _BIND_STATUS_INVALID: "INVALID",
-            }[bindStatus]
-            access = {
-                _BIND_MODE_READ: "read-only",
-                _BIND_MODE_WRITE: "read-write"
-            }[bindMode]
-            principalURL = "/principals/__uids__/%s/" % (ownerUID,)
-            yield Invite(
-                inviteuid, userid, principalURL, common_name,
-                access, state, summary
-            )
-
-
-    def recordForUserID(self, userid):
-        for record in self.allRecords():
-            if record.userid == userid:
-                return record
-
-
-    def recordForPrincipalURL(self, principalURL):
-        for record in self.allRecords():
-            if record.principalURL == principalURL:
-                return record
-
-
-    def recordForInviteUID(self, inviteUID):
-        for record in self.allRecords():
-            if record.inviteuid == inviteUID:
-                return record
-
-
-    def addOrUpdateRecord(self, record):
-        bindMode = {'read-only': _BIND_MODE_READ,
-                    'read-write': _BIND_MODE_WRITE}[record.access]
-        bindStatus = {
-            "NEEDS-ACTION": _BIND_STATUS_INVITED,
-            "ACCEPTED": _BIND_STATUS_ACCEPTED,
-            "DECLINED": _BIND_STATUS_DECLINED,
-            "INVALID": _BIND_STATUS_INVALID,
-        }[record.state]
-        # principalURL is derived from a directory record's principalURL() so
-        # it will always contain the UID.  The form is '/principals/__uids__/x'
-        # (and may contain a trailing slash).
-        principalUID = record.principalURL.split("/")[3]
-        shareeHome = self._txn.addressbookHomeWithUID(principalUID, create=True)
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
-            [record.userid]
-        )
-        if rows:
-            [[resourceID, homeResourceID]] = rows
-            # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
-            self._txn.execSQL("""
-                update ADDRESSBOOK_BIND set BIND_MODE = %s,
-                BIND_STATUS = %s, MESSAGE = %s
-                where
-                    ADDRESSBOOK_RESOURCE_ID = %s and
-                    ADDRESSBOOK_HOME_RESOURCE_ID = %s
-            """, [bindMode, bindStatus, record.summary,
-                resourceID, homeResourceID])
-            self._txn.execSQL("""
-                update INVITE set NAME = %s, INVITE_UID = %s
-                where RECIPIENT_ADDRESS = %s
-                """,
-                [record.name, record.inviteuid, record.userid]
-            )
-        else:
-            self._txn.execSQL(
-                """
-                insert into INVITE (
-                    INVITE_UID, NAME,
-                    HOME_RESOURCE_ID, RESOURCE_ID,
-                    RECIPIENT_ADDRESS
-                )
-                values (%s, %s, %s, %s, %s)
-                """,
-                [
-                    record.inviteuid, record.name,
-                    shareeHome._resourceID, self._addressbook._resourceID,
-                    record.userid
-                ])
-            self._txn.execSQL(
-                """
-                insert into ADDRESSBOOK_BIND
-                (
-                    ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID, 
-                    ADDRESSBOOK_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
-                    SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
-                )
-                values (%s, %s, %s, %s, %s, %s, %s, %s)
-                """,
-                [
-                    shareeHome._resourceID,
-                    self._addressbook._resourceID,
-                    None, # this is NULL because it is not bound yet, let's be
-                          # explicit about that.
-                    bindMode,
-                    bindStatus,
-                    False,
-                    False,
-                    record.summary
-                ])
-
-
-    def removeRecordForUserID(self, userid):
-        rec = self.recordForUserID(userid)
-        self.removeRecordForInviteUID(rec.inviteuid)
-
-
-    def removeRecordForPrincipalURL(self, principalURL):
-        raise NotImplementedError("removeRecordForPrincipalURL")
-
-
-    def removeRecordForInviteUID(self, inviteUID):
-        rows = self._txn.execSQL("""
-                select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
-                INVITE_UID = %s
-            """, [inviteUID])
-        if rows:
-            [[homeID, resourceID]] = rows
-            self._txn.execSQL(
-                "delete from ADDRESSBOOK_BIND where "
-                "ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
-                [homeID, resourceID])
-            self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
-                [inviteUID])
-
-
-
-class PostgresLegacyABSharesEmulator(object):
-
-    def __init__(self, home):
-        self._home = home
-
-
-    @property
-    def _txn(self):
-        return self._home._txn
-
-
-    def create(self):
-        pass
-
-
-    def remove(self):
-        pass
-
-
-    def allRecords(self):
-        # This should have been a smart join that got all these columns at
-        # once, but let's not bother to fix it, since the actual query we
-        # _want_ to do (just look for addressbook binds in a particular homes) is
-        # much simpler anyway; we should just do that.
-        shareRows = self._txn.execSQL(
-            """
-            select ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, MESSAGE
-            from ADDRESSBOOK_BIND
-                where ADDRESSBOOK_HOME_RESOURCE_ID = %s and
-                BIND_MODE != %s and
-                ADDRESSBOOK_RESOURCE_NAME is not null
-            """, [self._home._resourceID, _BIND_MODE_OWN])
-        for resourceID, resourceName, summary in shareRows:
-            [[shareuid]] = self._txn.execSQL(
-                """
-                select INVITE_UID
-                from INVITE
-                where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
-                """, [resourceID, self._home._resourceID])
-            sharetype = 'I'
-            [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
-                """
-                select ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME
-                from ADDRESSBOOK_BIND
-                where ADDRESSBOOK_RESOURCE_ID = %s and
-                    BIND_MODE = %s
-                """, [resourceID, _BIND_MODE_OWN]
-                )
-            [[ownerUID]] = self._txn.execSQL(
-                "select OWNER_UID from ADDRESSBOOK_HOME where RESOURCE_ID = %s",
-                [ownerHomeID])
-            hosturl = '/addressbooks/__uids__/%s/%s' % (
-                ownerUID, ownerResourceName
-            )
-            localname = resourceName
-            record = SharedCollectionRecord(
-                shareuid, sharetype, hosturl, localname, summary
-            )
-            yield record
-
-
-    def _search(self, **kw):
-        [[key, value]] = kw.items()
-        for record in self.allRecords():
-            if getattr(record, key) == value:
-                return record
-
-    def recordForLocalName(self, localname):
-        return self._search(localname=localname)
-
-    def recordForShareUID(self, shareUID):
-        return self._search(shareuid=shareUID)
-
-
-    def addOrUpdateRecord(self, record):
-#        print '*** SHARING***: Adding or updating this record:'
-#        import pprint
-#        pprint.pprint(record.__dict__)
-        # record.hosturl -> /addressbooks/__uids__/<uid>/<addressbookname>
-        splithost = record.hosturl.split('/')
-        ownerUID = splithost[3]
-        ownerAddressBookName = splithost[4]
-        ownerHome = self._txn.addressbookHomeWithUID(ownerUID)
-        ownerAddressBook = ownerHome.addressbookWithName(ownerAddressBookName)
-        addressbookResourceID = ownerAddressBook._resourceID
-
-        # There needs to be a bind already, one that corresponds to the
-        # invitation.  The invitation's UID is the same as the share UID.  I
-        # just need to update its 'localname', i.e.
-        # ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_NAME.
-
-        self._txn.execSQL(
-            """
-            update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s
-            where ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s
-            """,
-            [record.localname, self._home._resourceID, addressbookResourceID]
-        )
-
-
-    def removeRecordForLocalName(self, localname):
-        self._txn.execSQL(
-            "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
-            "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-            [localname, self._home._resourceID]
-        )
-
-
-    def removeRecordForShareUID(self, shareUID):
-        pass
-#        c = self._home._cursor()
-#        c.execute(
-#            "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
-#            "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-#            [self._home._resourceID]
-#        )
-
-
-
-
-class PostgresAddressBook(AddressbookSyncTokenHelper):
-
-    implements(IAddressBook)
-
-    def __init__(self, home, name, resourceID, notifier):
-        self._home = home
-        self._name = name
-        self._resourceID = resourceID
-        self._objects = {}
-        self._notifier = notifier
-
-
-    @property
-    def _txn(self):
-        return self._home._txn
-
-
-    def setSharingUID(self, uid):
-        self.properties()._setPerUserUID(uid)
-
-
-    def retrieveOldInvites(self):
-        return PostgresLegacyABInvitesEmulator(self)
-
-    def retrieveOldIndex(self):
-        return PostgresLegacyABIndexEmulator(self)
-
-
-    def notifierID(self, label="default"):
-        if self._notifier:
-            return self._notifier.getID(label)
-        else:
-            return None
-
-
-    def name(self):
-        return self._name
-
-
-    def rename(self, name):
-        oldName = self._name
-        self._txn.execSQL(
-            "update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s "
-            "where ADDRESSBOOK_RESOURCE_ID = %s AND "
-            "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID, self._home._resourceID]
-        )
-        self._name = name
-        # update memos
-        del self._home._addressbooks[oldName]
-        self._home._addressbooks[name] = self
-        self._updateSyncToken()
-
-
-    def ownerAddressBookHome(self):
-        return self._home
-
-
-    def listAddressbookObjects(self):
-        # FIXME: see listChildren
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from "
-            "ADDRESSBOOK_OBJECT where "
-            "ADDRESSBOOK_RESOURCE_ID = %s",
-            [self._resourceID])
-        return [row[0] for row in rows]
-
-
-    def addressbookObjects(self):
-        for name in self.listAddressbookObjects():
-            yield self.addressbookObjectWithName(name)
-
-
-    @memoized('name', '_objects')
-    def addressbookObjectWithName(self, name):
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID from ADDRESSBOOK_OBJECT where "
-            "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if not rows:
-            return None
-        resid = rows[0][0]
-        return PostgresAddressBookObject(self, name, resid)
-
-
-    @memoized('uid', '_objects')
-    def addressbookObjectWithUID(self, uid):
-        rows = self._txn.execSQL(
-            "select RESOURCE_ID, RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
-            "VCARD_UID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        if not rows:
-            return None
-        resid = rows[0][0]
-        name = rows[0][1]
-        return PostgresAddressBookObject(self, name, resid)
-
-
-    def createAddressBookObjectWithName(self, name, component):
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
-            " RESOURCE_NAME = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if rows:
-            raise ObjectResourceNameAlreadyExistsError()
-
-        addressbookObject = PostgresAddressBookObject(self, name, None)
-        addressbookObject.component = lambda : component
-
-        validateAddressBookComponent(addressbookObject, self, component)
-
-        addressbookObject.updateDatabase(component, inserting=True)
-        self._insertRevision(name)
-
-        if self._notifier:
-            self._home._txn.postCommit(self._notifier.notify)
-
-
-    def removeAddressBookObjectWithName(self, name):
-        self._txn.execSQL(
-            "delete from ADDRESSBOOK_OBJECT where RESOURCE_NAME = %s and "
-            "ADDRESSBOOK_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if self._txn._cursor.rowcount == 0:
-            raise NoSuchObjectResourceError()
-        self._objects.pop(name, None)
-        self._deleteRevision(name)
-
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    def removeAddressBookObjectWithUID(self, uid):
-        rows = self._txn.execSQL(
-            "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
-            "VCARD_UID = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        if not rows:
-            raise NoSuchObjectResourceError()
-        name = rows[0][0]
-        self._txn.execSQL(
-            "delete from ADDRESSBOOK_OBJECT where VCARD_UID = %s and "
-            "ADDRESSBOOK_RESOURCE_ID = %s",
-            [uid, self._resourceID]
-        )
-        self._objects.pop(name, None)
-        self._objects.pop(uid, None)
-        self._deleteRevision(name)
-
-        if self._notifier:
-            self._home._txn.postCommit(self._notifier.notify)
-
-
-    def addressbookObjectsSinceToken(self, token):
-        raise NotImplementedError()
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.ownerAddressBookHome().uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Addressbook objects is ???
-        """
-        return None # FIXME: verify
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        return 0
-
-
-    def created(self):
-        created = self._txn.execSQL(
-            "select extract(EPOCH from CREATED) from ADDRESSBOOK where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(created)
-
-
-    def modified(self):
-        modified = self._txn.execSQL(
-            "select extract(EPOCH from MODIFIED) from ADDRESSBOOK where "
-            "RESOURCE_ID = %s", [self._resourceID]
-        )[0][0]
-        return int(modified)
-
-
-
-
-class PostgresAddressBookHome(object):
-
-    implements(IAddressBookHome)
-
-    def __init__(self, transaction, ownerUID, resourceID, notifier):
-        self._txn = transaction
-        self._ownerUID = ownerUID
-        self._resourceID = resourceID
-        self._addressbooks = {}
-        self._notifier = notifier
-
-
-    def retrieveOldShares(self):
-        return PostgresLegacyABSharesEmulator(self)
-
-
-    def uid(self):
-        """
-        Retrieve the unique identifier for this calendar home.
-
-        @return: a string.
-        """
-        return self._ownerUID
-
-
-    def name(self):
-        """
-        Implement L{IDataStoreResource.name} to return the uid.
-        """
-        return self.uid()
-
-
-    def listChildren(self):
-        """
-        Retrieve the names of the children in this addressbook home.
-
-        @return: an iterable of C{str}s.
-        """
-        # FIXME: not specified on the interface or exercised by the tests, but
-        # required by clients of the implementation!
-        rows = self._txn.execSQL(
-            "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
-            "ADDRESSBOOK_HOME_RESOURCE_ID = %s "
-            "AND BIND_STATUS != %s",
-            [self._resourceID, _BIND_STATUS_DECLINED]
-        )
-        names = [row[0] for row in rows]
-        return names
-
-
-    def createdHome(self):
-        self.createAddressBookWithName("addressbook")
-
-    def addressbooks(self):
-        """
-        Retrieve addressbooks contained in this addressbook home.
-
-        @return: an iterable of L{IAddressBook}s.
-        """
-        names = self.listChildren()
-        for name in names:
-            yield self.addressbookWithName(name)
-
-
-    @memoized('name', '_addressbooks')
-    def addressbookWithName(self, name):
-        """
-        Retrieve the addressbook with the given C{name} contained in this
-        addressbook home.
-
-        @param name: a string.
-        @return: an L{IAddressBook} or C{None} if no such addressbook
-            exists.
-        """
-        data = self._txn.execSQL(
-            "select ADDRESSBOOK_RESOURCE_ID from ADDRESSBOOK_BIND where "
-            "ADDRESSBOOK_RESOURCE_NAME = %s and "
-            "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if not data:
-            return None
-        resourceID = data[0][0]
-        if self._notifier:
-            childID = "%s/%s" % (self.uid(), name)
-            notifier = self._notifier.clone(label="collection", id=childID)
-        else:
-            notifier = None
-        return PostgresAddressBook(self, name, resourceID, notifier)
-
-
-    def createAddressBookWithName(self, name):
-        rows = self._txn.execSQL(
-            "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
-            "ADDRESSBOOK_RESOURCE_NAME = %s AND "
-            "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        if rows:
-            raise HomeChildNameAlreadyExistsError()
-        rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
-        resourceID = rows[0][0]
-        self._txn.execSQL(
-            "insert into ADDRESSBOOK (RESOURCE_ID) values "
-            "(%s)",
-            [resourceID])
-
-        self._txn.execSQL("""
-            insert into ADDRESSBOOK_BIND (
-                ADDRESSBOOK_HOME_RESOURCE_ID,
-                ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, BIND_MODE,
-                SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
-            %s, %s, %s, %s, %s, %s, %s)
-            """,
-            [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
-             _BIND_STATUS_ACCEPTED]
-        )
-
-        addressbookType = ResourceType.addressbook #@UndefinedVariable
-        newAddressbook = self.addressbookWithName(name)
-        newAddressbook.properties()[
-            PropertyName.fromElement(ResourceType)] = addressbookType
-        newAddressbook._updateSyncToken()
-
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    def removeAddressBookWithName(self, name):
-        self._txn.execSQL(
-            "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s and "
-            "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID]
-        )
-        self._addressbooks.pop(name, None)
-        if self._txn._cursor.rowcount == 0:
-            raise NoSuchHomeChildError()
-        # FIXME: the schema should probably cascade the addressbook delete when
-        # the last bind is deleted.
-        if self._notifier:
-            self._txn.postCommit(self._notifier.notify)
-
-
-    @cached
-    def properties(self):
-        return PropertyStore(
-            self.uid(),
-            self._txn,
-            self._resourceID
-        )
-
-
-    # IDataStoreResource
-    def contentType(self):
-        """
-        The content type of Addressbook home objects is ???
-        """
-        return None # FIXME: verify
-
-
-    def md5(self):
-        return None
-
-
-    def size(self):
-        return 0
-
-
-    def created(self):
-        return None
-
-
-    def modified(self):
-        return None
-
-
-    def notifierID(self, label="default"):
-        if self._notifier:
-            return self._notifier.getID(label)
-        else:
-            return None

Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql	2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,342 +0,0 @@
------------------
--- Resource ID --
------------------
-
-create sequence RESOURCE_ID_SEQ;
-
-
--------------------
--- Calendar Home --
--------------------
-
-create table CALENDAR_HOME (
-  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  OWNER_UID   varchar(255) not null unique
-);
-
-
---------------
--- Calendar --
---------------
-
-create table CALENDAR (
-  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ'),
-  REVISION    integer   default 0,
-  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
-);
-
-
-------------------------
--- Sharing Invitation --
-------------------------
-
-create table INVITE (
-    INVITE_UID         varchar(255) not null,
-    NAME               varchar(255) not null,
-    RECIPIENT_ADDRESS  varchar(255) not null,
-    HOME_RESOURCE_ID   integer      not null,
-    RESOURCE_ID        integer      not null
-);
-
-
----------------------------
--- Sharing Notifications --
----------------------------
-
-create table NOTIFICATION_HOME (
-  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  OWNER_UID   varchar(255) not null unique
-);
-
-
-create table NOTIFICATION (
-  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
-  NOTIFICATION_UID              varchar(255) not null,
-  XML_TYPE                      varchar      not null,
-  XML_DATA                      varchar      not null,
-  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-
-  unique(NOTIFICATION_UID, 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,
-  
-  -- An invitation which hasn't been accepted yet will not yet have a resource
-  -- name, so this field may be null.
-  
-  CALENDAR_RESOURCE_NAME    varchar(255),
-  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
-  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
-  SEEN_BY_OWNER             boolean      not null,
-  SEEN_BY_SHAREE            boolean      not null,
-  MESSAGE                   text,
-
-  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
-  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
-);
-
--- 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');
-
--- 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');
-
-
----------------------
--- Calendar Object --
----------------------
-
-create table CALENDAR_OBJECT (
-  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  CALENDAR_RESOURCE_ID integer      not null references CALENDAR,
-  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      not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
-  ORGANIZER            varchar(255),
-  ORGANIZER_OBJECT     integer      references CALENDAR_OBJECT,
-  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
-  CREATED              timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED             timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-
-  unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
-
-  -- 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)
-);
-
--- Enumeration of attachment modes
-
-create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
-  ID          integer     primary key,
-  DESCRIPTION varchar(16) not null unique
-);
-
-insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'read' );
-insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'write');
-
-
------------------
--- Instance ID --
------------------
-
-create sequence INSTANCE_ID_SEQ;
-
-
-----------------
--- Time Range --
-----------------
-
-create table TIME_RANGE (
-  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'),
-  CALENDAR_RESOURCE_ID        integer        not null references CALENDAR,
-  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
-);
-
--- Enumeration of free/busy types
-
-create table FREE_BUSY_TYPE (
-  ID          integer     primary key,
-  DESCRIPTION varchar(16) not null unique
-);
-
-insert into FREE_BUSY_TYPE values (0, 'unknown'         );
-insert into FREE_BUSY_TYPE values (1, 'free'            );
-insert into FREE_BUSY_TYPE values (2, 'busy'            );
-insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
-insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
-
-
-------------------
--- Transparency --
-------------------
-
-create table TRANSPARENCY (
-  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
-  USER_ID                     varchar(255) not null,
-  TRANSPARENT                 boolean      not null
-);
-
-
-----------------
--- Attachment --
-----------------
-
-create table ATTACHMENT (
-  CALENDAR_OBJECT_RESOURCE_ID integer       not null references CALENDAR_OBJECT on delete cascade,
-  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 unique
-);
-
-
-------------------------------
--- Calendar Object Revision --
-------------------------------
-
-create sequence CALENDAR_OBJECT_REVISION_SEQ;
-
-
--------------------------------
--- Calendar Object Revisions --
--------------------------------
-
-create table CALENDAR_OBJECT_REVISIONS (
-  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
-  RESOURCE_NAME        varchar(255) not null,
-  REVISION             integer      not null,
-  DELETED              boolean      not null,
-
-  unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
-);
-
-
-------------------
--- iTIP Message --
-------------------
-
-create table ITIP_MESSAGE (
-  CALENDAR_RESOURCE_ID integer      not null references CALENDAR,
-  ICALENDAR_TEXT       text         not null,
-  ICALENDAR_UID        varchar(255) not null,
-  MD5                  char(32)     not null,
-  CHANGES              text         not null
-);
-
-
------------------------
--- 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)
-);
-
-
-----------------------
--- AddressBook Home --
-----------------------
-
-create table ADDRESSBOOK_HOME (
-  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  OWNER_UID   varchar(255) not null unique
-);
-
-
------------------
--- AddressBook --
------------------
-
-create table ADDRESSBOOK (
-  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ'),
-  REVISION    integer   default 0,
-  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
-);
-
-
-----------------------
--- AddressBook Bind --
-----------------------
-
--- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
-
-create table ADDRESSBOOK_BIND (
-  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
-  ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK,
-
-  -- An invitation which hasn't been accepted yet will not yet have a resource
-  -- name, so this field may be null.
-
-  ADDRESSBOOK_RESOURCE_NAME    varchar(255),
-  BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
-  BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
-  SEEN_BY_OWNER                boolean      not null,
-  SEEN_BY_SHAREE               boolean      not null,
-  MESSAGE                      text,                  -- FIXME: xml?
-
-  primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID),
-  unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)
-);
-
-
-create table ADDRESSBOOK_OBJECT (
-  RESOURCE_ID             integer      primary key default nextval('RESOURCE_ID_SEQ'),
-  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK,
-  RESOURCE_NAME           varchar(255) not null,
-  VCARD_TEXT              text         not null,
-  VCARD_UID               varchar(255) not null,
-  CREATED                 timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
-  MODIFIED                timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
-
-  unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
-  unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
-);
-
-------------------------------
--- AddressBook Object Revision --
-------------------------------
-
-create sequence ADDRESSBOOK_OBJECT_REVISION_SEQ;
-
-
--------------------------------
--- AddressBook Object Revisions --
--------------------------------
-
-create table ADDRESSBOOK_OBJECT_REVISIONS (
-  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
-  RESOURCE_NAME           varchar(255) not null,
-  REVISION                integer      not null,
-  DELETED                 boolean      not null,
-
-  unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME)
-);
-
-

Added: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,634 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_sql -*-
+##
+# Copyright (c) 2010 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.
+##
+
+__all__ = [
+    "CalendarHome",
+    "Calendar",
+    "CalendarObject",
+]
+
+from twext.python.vcomponent import VComponent
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType, generateContentType
+
+from twisted.internet.error import ConnectionLost
+from twisted.internet.interfaces import ITransport
+from twisted.python import hashlib
+from twisted.python.failure import Failure
+
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
+from twistedcaldav.dateops import normalizeForIndex
+from twistedcaldav.index import IndexedSearchException
+from twistedcaldav.instance import InvalidOverriddenInstanceError
+
+from txcaldav.calendarstore.util import validateCalendarComponent,\
+    dropboxIDFromCalendarObject
+from txcaldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject,\
+    IAttachment
+
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
+    CommonObjectResource
+from txdav.common.datastore.sql_legacy import \
+    PostgresLegacyIndexEmulator, PostgresLegacyInvitesEmulator,\
+    PostgresLegacySharesEmulator
+from txdav.common.datastore.sql_tables import CALENDAR_TABLE,\
+    CALENDAR_BIND_TABLE, CALENDAR_OBJECT_REVISIONS_TABLE, CALENDAR_OBJECT_TABLE,\
+    _ATTACHMENTS_MODE_WRITE
+from txdav.propertystore.base import PropertyName
+
+from vobject.icalendar import utc
+
+import datetime
+
+from zope.interface.declarations import implements
+
+class CalendarHome(CommonHome):
+
+    implements(ICalendarHome)
+
+    def __init__(self, transaction, ownerUID, resourceID, notifier):
+        super(CalendarHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+
+        self._shares = PostgresLegacySharesEmulator(self)
+        self._childClass = Calendar
+        self._childTable = CALENDAR_TABLE
+        self._bindTable = CALENDAR_BIND_TABLE
+
+    createCalendarWithName = CommonHome.createChildWithName
+    removeCalendarWithName = CommonHome.removeChildWithName
+    calendarWithName = CommonHome.childWithName
+    calendars = CommonHome.children
+    listCalendars = CommonHome.listChildren
+
+    def calendarObjectWithDropboxID(self, dropboxID):
+        """
+        Implement lookup with brute-force scanning.
+        """
+        for calendar in self.calendars():
+            for calendarObject in calendar.calendarObjects():
+                if dropboxID == calendarObject.dropboxID():
+                    return calendarObject
+
+
+    def createdHome(self):
+        self.createCalendarWithName("calendar")
+        defaultCal = self.calendarWithName("calendar")
+        props = defaultCal.properties()
+        props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(
+            Opaque())
+        self.createCalendarWithName("inbox")
+
+class Calendar(CommonHomeChild):
+    """
+    File-based implementation of L{ICalendar}.
+    """
+    implements(ICalendar)
+
+    def __init__(self, home, name, resourceID, notifier):
+        """
+        Initialize a calendar pointing at a path on disk.
+
+        @param name: the subdirectory of calendarHome where this calendar
+            resides.
+        @type name: C{str}
+
+        @param calendarHome: the home containing this calendar.
+        @type calendarHome: L{CalendarHome}
+
+        @param realName: If this calendar was just created, the name which it
+        will eventually have on disk.
+        @type realName: C{str}
+        """
+        super(Calendar, self).__init__(home, name, resourceID, notifier)
+
+        self._index = PostgresLegacyIndexEmulator(self)
+        self._invites = PostgresLegacyInvitesEmulator(self)
+        self._objectResourceClass = CalendarObject
+        self._bindTable = CALENDAR_BIND_TABLE
+        self._homeChildTable = CALENDAR_TABLE
+        self._revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE
+        self._objectTable = CALENDAR_OBJECT_TABLE
+
+
+    @property
+    def _calendarHome(self):
+        return self._home
+
+
+    def resourceType(self):
+        return ResourceType.calendar #@UndefinedVariable
+
+
+    ownerCalendarHome = CommonHomeChild.ownerHome
+    calendarObjects = CommonHomeChild.objectResources
+    listCalendarObjects = CommonHomeChild.listObjectResources
+    calendarObjectWithName = CommonHomeChild.objectResourceWithName
+    calendarObjectWithUID = CommonHomeChild.objectResourceWithUID
+    createCalendarObjectWithName = CommonHomeChild.createObjectResourceWithName
+    removeCalendarObjectWithName = CommonHomeChild.removeObjectResourceWithName
+    removeCalendarObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+    calendarObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
+
+
+    def calendarObjectsInTimeRange(self, start, end, timeZone):
+        raise NotImplementedError()
+
+
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+                PropertyName.fromElement(caldavxml.CalendarDescription),
+                PropertyName.fromElement(caldavxml.CalendarTimeZone),
+            ),
+            (
+                PropertyName.fromElement(customxml.GETCTag),
+                PropertyName.fromElement(caldavxml.SupportedCalendarComponentSet),
+                PropertyName.fromElement(caldavxml.ScheduleCalendarTransp),
+            ),
+        )
+
+    def contentType(self):
+        """
+        The content type of Calendar objects is text/calendar.
+        """
+        return MimeType.fromString("text/calendar; charset=utf-8")
+
+#
+# Duration into the future through which recurrences are expanded in the index
+# by default.  This is a caching parameter which affects the size of the index;
+# it does not affect search results beyond this period, but it may affect
+# performance of such a search.
+#
+default_future_expansion_duration = datetime.timedelta(days=365 * 1)
+
+#
+# Maximum duration into the future through which recurrences are expanded in the
+# index.  This is a caching parameter which affects the size of the index; it
+# does not affect search results beyond this period, but it may affect
+# performance of such a search.
+#
+# When a search is performed on a time span that goes beyond that which is
+# expanded in the index, we have to open each resource which may have data in
+# that time period.  In order to avoid doing that multiple times, we want to
+# cache those results.  However, we don't necessarily want to cache all
+# occurrences into some obscenely far-in-the-future date, so we cap the caching
+# period.  Searches beyond this period will always be relatively expensive for
+# resources with occurrences beyond this period.
+#
+maximum_future_expansion_duration = datetime.timedelta(days=365 * 5)
+
+icalfbtype_to_indexfbtype = {
+    "UNKNOWN"         : 0,
+    "FREE"            : 1,
+    "BUSY"            : 2,
+    "BUSY-UNAVAILABLE": 3,
+    "BUSY-TENTATIVE"  : 4,
+}
+
+indexfbtype_to_icalfbtype = {
+    0: '?',
+    1: 'F',
+    2: 'B',
+    3: 'U',
+    4: 'T',
+}
+
+def _pathToName(path):
+    return path.rsplit(".", 1)[0].split("-", 3)[-1]
+
+class CalendarObject(CommonObjectResource):
+    implements(ICalendarObject)
+
+    def __init__(self, name, calendar, resid):
+        super(CalendarObject, self).__init__(name, calendar, resid)
+
+        self._objectTable = CALENDAR_OBJECT_TABLE
+
+    @property
+    def _calendar(self):
+        return self._parentCollection
+
+    def calendar(self):
+        return self._calendar
+
+    def setComponent(self, component, inserting=False):
+        validateCalendarComponent(self, self._calendar, component, inserting)
+
+        self.updateDatabase(component, inserting=inserting)
+        if inserting:
+            self._calendar._insertRevision(self._name)
+        else:
+            self._calendar._updateRevision(self._name)
+
+        if self._calendar._notifier:
+            self._txn.postCommit(self._calendar._notifier.notify)
+
+    def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
+        """
+        Update the database tables for the new data being written.
+
+        @param component: calendar data to store
+        @type component: L{Component}
+        """
+
+        # Decide how far to expand based on the component
+        master = component.masterComponent()
+        if master is None or not component.isRecurring() and not component.isRecurringUnbounded():
+            # When there is no master we have a set of overridden components - index them all.
+            # When there is one instance - index it.
+            # When bounded - index all.
+            expand = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+        else:
+            if expand_until:
+                expand = expand_until
+            else:
+                expand = datetime.date.today() + default_future_expansion_duration
+
+            if expand > (datetime.date.today() + maximum_future_expansion_duration):
+                raise IndexedSearchException
+
+        try:
+            instances = component.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
+        except InvalidOverriddenInstanceError, e:
+            self.log_err("Invalid instance %s when indexing %s in %s" % (e.rid, self._name, self.resource,))
+            raise
+
+        componentText = str(component)
+        self._objectText = componentText
+        organizer = component.getOrganizer()
+        if not organizer:
+            organizer = ""
+
+        # CALENDAR_OBJECT table update
+        if inserting:
+            self._resourceID = self._txn.execSQL(
+                """
+                insert into CALENDAR_OBJECT
+                (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX)
+                 values
+                (%s, %s, %s, %s, %s, %s, %s, %s)
+                 returning RESOURCE_ID
+                """,
+                # FIXME: correct ATTACHMENTS_MODE based on X-APPLE-
+                # DROPBOX
+                [
+                    self._calendar._resourceID,
+                    self._name,
+                    componentText,
+                    component.resourceUID(),
+                    component.resourceType(),
+                    _ATTACHMENTS_MODE_WRITE,
+                    organizer,
+                    normalizeForIndex(instances.limit) if instances.limit else None,
+                ]
+            )[0][0]
+        else:
+            self._txn.execSQL(
+                """
+                update CALENDAR_OBJECT set
+                (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MODIFIED)
+                 =
+                (%s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP))
+                 where RESOURCE_ID = %s
+                """,
+                # should really be filling out more fields: ORGANIZER,
+                # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
+                # DROPBOX
+                [
+                    componentText,
+                    component.resourceUID(),
+                    component.resourceType(),
+                    _ATTACHMENTS_MODE_WRITE,
+                    organizer,
+                    normalizeForIndex(instances.limit) if instances.limit else None,
+                    self._resourceID
+                ]
+            )
+
+            # Need to wipe the existing time-range for this and rebuild
+            self._txn.execSQL(
+                """
+                delete from TIME_RANGE where CALENDAR_OBJECT_RESOURCE_ID = %s
+                """,
+                [
+                    self._resourceID,
+                ],
+            )
+
+
+        # CALENDAR_OBJECT table update
+        for key in instances:
+            instance = instances[key]
+            start = instance.start.replace(tzinfo=utc)
+            end = instance.end.replace(tzinfo=utc)
+            float = instance.start.tzinfo is None
+            transp = instance.component.propertyValue("TRANSP") == "TRANSPARENT"
+            instanceid = self._txn.execSQL(
+                """
+                insert into TIME_RANGE
+                (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
+                 values
+                (%s, %s, %s, %s, %s, %s, %s)
+                 returning
+                INSTANCE_ID
+                """,
+                [
+                    self._calendar._resourceID,
+                    self._resourceID,
+                    float,
+                    start,
+                    end,
+                    icalfbtype_to_indexfbtype.get(instance.component.getFBType(), icalfbtype_to_indexfbtype["FREE"]),
+                    transp,
+                ],
+            )[0][0]
+            peruserdata = component.perUserTransparency(instance.rid)
+            for useruid, transp in peruserdata:
+                self._txn.execSQL(
+                    """
+                    insert into TRANSPARENCY
+                    (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
+                     values
+                    (%s, %s, %s)
+                    """,
+                    [
+                        instanceid,
+                        useruid,
+                        transp,
+                    ],
+                )
+
+        # Special - for unbounded recurrence we insert a value for "infinity"
+        # that will allow an open-ended time-range to always match it.
+        if component.isRecurringUnbounded():
+            start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+            end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
+            float = False
+            instanceid = self._txn.execSQL(
+                """
+                insert into TIME_RANGE
+                (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
+                 values
+                (%s, %s, %s, %s, %s, %s, %s)
+                 returning
+                INSTANCE_ID
+                """,
+                [
+                    self._calendar._resourceID,
+                    self._resourceID,
+                    float,
+                    start,
+                    end,
+                    icalfbtype_to_indexfbtype["UNKNOWN"],
+                    True,
+                ],
+            )[0][0]
+            peruserdata = component.perUserTransparency(None)
+            for useruid, transp in peruserdata:
+                self._txn.execSQL(
+                    """
+                    insert into TRANSPARENCY
+                    (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
+                     values
+                    (%s, %s, %s)
+                    """,
+                    [
+                        instanceid,
+                        useruid,
+                        transp,
+                    ],
+                )
+
+    def component(self):
+        return VComponent.fromString(self.iCalendarText())
+
+    def text(self):
+        if self._objectText is None:
+            text = self._txn.execSQL(
+                "select ICALENDAR_TEXT from CALENDAR_OBJECT where "
+                "RESOURCE_ID = %s", [self._resourceID]
+            )[0][0]
+            self._objectText = text
+            return text
+        else:
+            return self._objectText
+
+    iCalendarText = text
+
+    def uid(self):
+        return self.component().resourceUID()
+
+    def name(self):
+        return self._name
+
+    def componentType(self):
+        return self.component().mainType()
+
+    def organizer(self):
+        return self.component().getOrganizer()
+
+    def createAttachmentWithName(self, name, contentType):
+        path = self._attachmentPath(name)
+        attachment = Attachment(self, path)
+        self._txn.execSQL("""
+            insert into ATTACHMENT (CALENDAR_OBJECT_RESOURCE_ID, CONTENT_TYPE,
+            SIZE, MD5, PATH)
+            values (%s, %s, %s, %s, %s)
+            """,
+            [
+                self._resourceID, generateContentType(contentType), 0, "",
+                attachment._pathValue()
+            ]
+        )
+        return attachment.store(contentType)
+
+    def removeAttachmentWithName(self, name):
+        attachment = Attachment(self, self._attachmentPath(name))
+        self._txn.postCommit(attachment._path.remove)
+        self._txn.execSQL("""
+        delete from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s AND
+        PATH = %s
+        """, [self._resourceID, attachment._pathValue()])
+
+    def attachmentWithName(self, name):
+        attachment = Attachment(self, self._attachmentPath(name))
+        if attachment._populate():
+            return attachment
+        else:
+            return None
+
+    def attendeesCanManageAttachments(self):
+        return self.component().hasPropertyInAnyComponent("X-APPLE-DROPBOX")
+
+    def dropboxID(self):
+        return dropboxIDFromCalendarObject(self)
+
+    def _attachmentPath(self, name):
+        attachmentRoot = self._txn._store.attachmentsPath
+        try:
+            attachmentRoot.createDirectory()
+        except:
+            pass
+        return attachmentRoot.child(
+            "%s-%s-%s-%s.attachment" % (
+                self._calendar._home.uid(), self._calendar.name(),
+                self.name(), name
+            )
+        )
+
+    def attachments(self):
+        rows = self._txn.execSQL("""
+        select PATH from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s 
+        """, [self._resourceID])
+        for row in rows:
+            demangledName = _pathToName(row[0])
+            yield self.attachmentWithName(demangledName)
+
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+            ),
+            (
+                PropertyName.fromElement(customxml.TwistedCalendarAccessProperty),
+                PropertyName.fromElement(customxml.TwistedSchedulingObjectResource),
+                PropertyName.fromElement(caldavxml.ScheduleTag),
+                PropertyName.fromElement(customxml.TwistedScheduleMatchETags),
+                PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty),
+                PropertyName.fromElement(caldavxml.Originator),
+                PropertyName.fromElement(caldavxml.Recipient),
+                PropertyName.fromElement(customxml.ScheduleChanges),
+            ),
+        )
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of Calendar objects is text/calendar.
+        """
+        return MimeType.fromString("text/calendar; charset=utf-8")
+
+class AttachmentStorageTransport(object):
+
+    implements(ITransport)
+
+    def __init__(self, attachment, contentType):
+        self.attachment = attachment
+        self.contentType = contentType
+        self.buf = ''
+        self.hash = hashlib.md5()
+
+
+    @property
+    def _txn(self):
+        return self.attachment._txn
+
+
+    def write(self, data):
+        self.buf += data
+        self.hash.update(data)
+
+
+    def loseConnection(self):
+        self.attachment._path.setContent(self.buf)
+        pathValue = self.attachment._pathValue()
+        contentTypeString = generateContentType(self.contentType)
+        self._txn.execSQL(
+            "update ATTACHMENT set CONTENT_TYPE = %s, SIZE = %s, MD5 = %s, MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) "
+            "WHERE PATH = %s",
+            [contentTypeString, len(self.buf), self.hash.hexdigest(), pathValue]
+        )
+
+class Attachment(object):
+
+    implements(IAttachment)
+
+    def __init__(self, calendarObject, path):
+        self._calendarObject = calendarObject
+        self._path = path
+
+
+    @property
+    def _txn(self):
+        return self._calendarObject._txn
+
+
+    def _populate(self):
+        """
+        Execute necessary SQL queries to retrieve attributes.
+
+        @return: C{True} if this attachment exists, C{False} otherwise.
+        """
+        rows = self._txn.execSQL(
+            """
+            select CONTENT_TYPE, SIZE, MD5, extract(EPOCH from CREATED), extract(EPOCH from MODIFIED) from ATTACHMENT where PATH = %s
+            """, [self._pathValue()])
+        if not rows:
+            return False
+        self._contentType = MimeType.fromString(rows[0][0])
+        self._size = rows[0][1]
+        self._md5 = rows[0][2]
+        self._created = int(rows[0][3])
+        self._modified = int(rows[0][4])
+        return True
+
+
+    def name(self):
+        return _pathToName(self._pathValue())
+
+
+    def _pathValue(self):
+        """
+        Compute the value which should go into the 'path' column for this
+        attachment.
+        """
+        root = self._txn._store.attachmentsPath
+        return '/'.join(self._path.segmentsFrom(root))
+
+    def properties(self):
+        pass # stub
+
+
+    def store(self, contentType):
+        return AttachmentStorageTransport(self, contentType)
+
+
+    def retrieve(self, protocol):
+        protocol.dataReceived(self._path.getContent())
+        protocol.connectionLost(Failure(ConnectionLost()))
+
+
+    # IDataStoreResource
+    def contentType(self):
+        return self._contentType
+
+
+    def md5(self):
+        return self._md5
+
+
+    def size(self):
+        return self._size
+
+
+    def created(self):
+        return self._created
+
+    def modified(self):
+        return self._modified

Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,245 +0,0 @@
-##
-# Copyright (c) 2010 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.
-##
-
-"""
-Tests for txcaldav.calendarstore.postgres, mostly based on
-L{txcaldav.calendarstore.test.common}.
-"""
-
-import gc
-
-from txcaldav.calendarstore.test.common import CommonTests as CalendarCommonTests
-from txcarddav.addressbookstore.test.common import CommonTests as AddressBookCommonTests
-from txdav.common.icommondatastore import NoSuchHomeChildError
-from txdav.common.datastore.sql import CommonDataStore
-
-from twisted.trial import unittest
-from txdav.datastore.subpostgres import (PostgresService,
-    DiagnosticConnectionWrapper)
-from txcaldav.calendarstore.postgres import v1_schema
-from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet import reactor
-from twext.python.filepath import CachingFilePath
-from twext.python.vcomponent import VComponent
-from twistedcaldav.vcard import Component as VCard
-from twisted.internet.task import deferLater
-from twisted.python import log
-
-
-
-def allInstancesOf(cls):
-    for o in gc.get_referrers(cls):
-        if isinstance(o, cls):
-            yield o
-
-
-
-def dumpConnectionStatus():
-    print '+++ ALL CONNECTIONS +++'
-    for connection in allInstancesOf(DiagnosticConnectionWrapper):
-        print connection.label, connection.state
-    print '--- CONNECTIONS END ---'
-
-
-
-class StoreBuilder(object):
-    """
-    Test-fixture-builder which can construct a PostgresStore.
-    """
-    sharedService = None
-    currentTestID = None
-
-    SHARED_DB_PATH = "../_test_postgres_db"
-
-    def buildStore(self, testCase, notifierFactory):
-        """
-        Do the necessary work to build a store for a particular test case.
-
-        @return: a L{Deferred} which fires with an L{IDataStore}.
-        """
-        currentTestID = testCase.id()
-        dbRoot = CachingFilePath(self.SHARED_DB_PATH)
-        if self.sharedService is None:
-            ready = Deferred()
-            def getReady(connectionFactory):
-                attachmentRoot = dbRoot.child("attachments")
-                try:
-                    attachmentRoot.createDirectory()
-                except OSError:
-                    pass
-                try:
-                    self.store = CommonDataStore(
-                        lambda label=None: connectionFactory(
-                            label or currentTestID
-                        ),
-                        notifierFactory,
-                        attachmentRoot
-                    )
-                except:
-                    ready.errback()
-                    raise
-                else:
-                    self.cleanDatabase(testCase)
-                    ready.callback(self.store)
-                return self.store
-            self.sharedService = PostgresService(
-                dbRoot, getReady, v1_schema, "caldav", resetSchema=True,
-                testMode=True
-            )
-            self.sharedService.startService()
-            def startStopping():
-                log.msg("Starting stopping.")
-                self.sharedService.unpauseMonitor()
-                return self.sharedService.stopService()
-            reactor.addSystemEventTrigger(#@UndefinedVariable
-                "before", "shutdown", startStopping)
-            result = ready
-        else:
-            self.store.notifierFactory = notifierFactory
-            self.cleanDatabase(testCase)
-            result = succeed(self.store)
-
-        def cleanUp():
-            # FIXME: clean up any leaked connections and report them with an
-            # immediate test failure.
-            def stopit():
-                self.sharedService.pauseMonitor()
-            return deferLater(reactor, 0.1, stopit)
-        testCase.addCleanup(cleanUp)
-        return result
-
-
-    def cleanDatabase(self, testCase):
-        cleanupConn = self.store.connectionFactory(
-            "%s schema-cleanup" % (testCase.id(),)
-        )
-        cursor = cleanupConn.cursor()
-        tables = ['INVITE',
-                  'RESOURCE_PROPERTY',
-                  'ATTACHMENT',
-                  'ADDRESSBOOK_OBJECT',
-                  'CALENDAR_OBJECT',
-                  'CALENDAR_BIND',
-                  'ADDRESSBOOK_BIND',
-                  'CALENDAR',
-                  'ADDRESSBOOK',
-                  'CALENDAR_HOME',
-                  'ADDRESSBOOK_HOME',
-                  'NOTIFICATION',
-                  'NOTIFICATION_HOME']
-        for table in tables:
-            try:
-                cursor.execute("delete from "+table)
-            except:
-                log.err()
-        cleanupConn.commit()
-        cleanupConn.close()
-
-
-
-theStoreBuilder = StoreBuilder()
-buildStore = theStoreBuilder.buildStore
-
-
-
-class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
-    """
-    Calendar SQL storage tests.
-    """
-
-    @inlineCallbacks
-    def setUp(self):
-        super(CalendarSQLStorageTests, self).setUp()
-        self.calendarStore = yield buildStore(self, self.notifierFactory)
-        self.populate()
-
-
-    def populate(self):
-        populateTxn = self.calendarStore.newTransaction()
-        for homeUID in self.requirements:
-            calendars = self.requirements[homeUID]
-            if calendars is not None:
-                home = populateTxn.calendarHomeWithUID(homeUID, True)
-                # We don't want the default calendar or inbox to appear unless it's
-                # explicitly listed.
-                try:
-                    home.removeCalendarWithName("calendar")
-                    home.removeCalendarWithName("inbox")
-                except NoSuchHomeChildError:
-                    pass
-                for calendarName in calendars:
-                    calendarObjNames = calendars[calendarName]
-                    if calendarObjNames is not None:
-                        home.createCalendarWithName(calendarName)
-                        calendar = home.calendarWithName(calendarName)
-                        for objectName in calendarObjNames:
-                            objData = calendarObjNames[objectName]
-                            calendar.createCalendarObjectWithName(
-                                objectName, VComponent.fromString(objData)
-                            )
-        populateTxn.commit()
-        self.notifierFactory.reset()
-
-
-    def storeUnderTest(self):
-        """
-        Create and return a L{CalendarStore} for testing.
-        """
-        return self.calendarStore
-
-
-class AddressBookSQLStorageTests(AddressBookCommonTests, unittest.TestCase):
-    """
-    AddressBook SQL storage tests.
-    """
-
-    @inlineCallbacks
-    def setUp(self):
-        super(AddressBookSQLStorageTests, self).setUp()
-        self.addressbookStore = yield buildStore(self, self.notifierFactory)
-        self.populate()
-
-    def populate(self):
-        populateTxn = self.addressbookStore.newTransaction()
-        for homeUID in self.requirements:
-            addressbooks = self.requirements[homeUID]
-            if addressbooks is not None:
-                home = populateTxn.addressbookHomeWithUID(homeUID, True)
-                # We don't want the default addressbook to appear unless it's
-                # explicitly listed.
-                home.removeAddressBookWithName("addressbook")
-                for addressbookName in addressbooks:
-                    addressbookObjNames = addressbooks[addressbookName]
-                    if addressbookObjNames is not None:
-                        home.createAddressBookWithName(addressbookName)
-                        addressbook = home.addressbookWithName(addressbookName)
-                        for objectName in addressbookObjNames:
-                            objData = addressbookObjNames[objectName]
-                            addressbook.createAddressBookObjectWithName(
-                                objectName, VCard.fromString(objData)
-                            )
-
-        populateTxn.commit()
-        self.notifierFactory.reset()
-
-
-
-    def storeUnderTest(self):
-        """
-        Create and return a L{AddressBookStore} for testing.
-        """
-        return self.addressbookStore
-

Copied: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py (from rev 6168, CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py)
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,78 @@
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Tests for txcaldav.calendarstore.postgres, mostly based on
+L{txcaldav.calendarstore.test.common}.
+"""
+
+from txcaldav.calendarstore.test.common import CommonTests as CalendarCommonTests
+
+from txdav.common.datastore.test.util import SQLStoreBuilder
+from txdav.common.icommondatastore import NoSuchHomeChildError
+
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twext.python.vcomponent import VComponent
+
+
+theStoreBuilder = SQLStoreBuilder()
+buildStore = theStoreBuilder.buildStore
+
+class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
+    """
+    Calendar SQL storage tests.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        super(CalendarSQLStorageTests, self).setUp()
+        self.calendarStore = yield buildStore(self, self.notifierFactory)
+        self.populate()
+
+
+    def populate(self):
+        populateTxn = self.calendarStore.newTransaction()
+        for homeUID in self.requirements:
+            calendars = self.requirements[homeUID]
+            if calendars is not None:
+                home = populateTxn.calendarHomeWithUID(homeUID, True)
+                # We don't want the default calendar or inbox to appear unless it's
+                # explicitly listed.
+                try:
+                    home.removeCalendarWithName("calendar")
+                    home.removeCalendarWithName("inbox")
+                except NoSuchHomeChildError:
+                    pass
+                for calendarName in calendars:
+                    calendarObjNames = calendars[calendarName]
+                    if calendarObjNames is not None:
+                        home.createCalendarWithName(calendarName)
+                        calendar = home.calendarWithName(calendarName)
+                        for objectName in calendarObjNames:
+                            objData = calendarObjNames[objectName]
+                            calendar.createCalendarObjectWithName(
+                                objectName, VComponent.fromString(objData)
+                            )
+        populateTxn.commit()
+        self.notifierFactory.reset()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self.calendarStore

Modified: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -19,14 +19,12 @@
 
 from twext.python.vcomponent import InvalidICalendarDataError
 from twext.python.vcomponent import VComponent
-from twistedcaldav.vcard import Component as VCard
-from twistedcaldav.vcard import InvalidVCardDataError
 
 from txdav.common.icommondatastore import InvalidObjectResourceError,\
     NoSuchObjectResourceError
 
 
-def validateCalendarComponent(calendarObject, calendar, component):
+def validateCalendarComponent(calendarObject, calendar, component, inserting):
     """
     Validate a calendar component for a particular calendar.
 
@@ -44,7 +42,7 @@
         raise TypeError(type(component))
 
     try:
-        if component.resourceUID() != calendarObject.uid():
+        if not inserting and component.resourceUID() != calendarObject.uid():
             raise InvalidObjectResourceError(
                 "UID may not change (%s != %s)" % (
                     component.resourceUID(), calendarObject.uid()
@@ -87,219 +85,4 @@
             return attachPath
     
     return calendarObject.uid() + ".dropbox"
-
-
-def validateAddressBookComponent(addressbookObject, vcard, component):
-    """
-    Validate an addressbook component for a particular addressbook.
-
-    @param addressbookObject: The addressbook object whose component will be replaced.
-    @type addressbookObject: L{IAddressBookObject}
-
-    @param addressbook: The addressbook which the L{IAddressBookObject} is present in.
-    @type addressbook: L{IAddressBook}
-
-    @param component: The VComponent to be validated.
-    @type component: L{VComponent}
-    """
-
-    if not isinstance(component, VCard):
-        raise TypeError(type(component))
-
-    try:
-        if component.resourceUID() != addressbookObject.uid():
-            raise InvalidObjectResourceError(
-                "UID may not change (%s != %s)" % (
-                    component.resourceUID(), addressbookObject.uid()
-                 )
-            )
-    except NoSuchObjectResourceError:
-        pass
-
-    try:
-        component.validForCardDAV()
-    except InvalidVCardDataError, e:
-        raise InvalidObjectResourceError(e)
-
-
-
-class CalendarSyncTokenHelper(object):
-    """
-    This is a mixin for use by data store implementations.
-    """
-
-    def syncToken(self):
-        revision = self._txn.execSQL(
-            "select REVISION from CALENDAR where RESOURCE_ID = %s",
-            [self._resourceID])[0][0]
-        return "%s#%s" % (self._resourceID, revision,)
-
-    def _updateSyncToken(self):
-        
-        self._txn.execSQL("""
-            update CALENDAR
-            set (REVISION)
-            = (nextval('CALENDAR_OBJECT_REVISION_SEQ'))
-            where RESOURCE_ID = %s
-            """,
-            [self._resourceID]
-        )
-
-    def _insertRevision(self, name):
-        self._changeRevision("insert", name)
-
-    def _updateRevision(self, name):
-        self._changeRevision("update", name)
-
-    def _deleteRevision(self, name):
-        self._changeRevision("delete", name)
-
-    def _changeRevision(self, action, name):
-        
-        nextrevision = self._txn.execSQL("""
-            select nextval('CALENDAR_OBJECT_REVISION_SEQ')
-            """
-        )
-
-        if action == "delete":
-            self._txn.execSQL("""
-                update CALENDAR_OBJECT_REVISIONS
-                set (REVISION, DELETED) = (%s, TRUE)
-                where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [nextrevision, self._resourceID, name]
-            )
-            self._txn.execSQL("""    
-                update CALENDAR
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
-        elif action == "update":
-            self._txn.execSQL("""
-                update CALENDAR_OBJECT_REVISIONS
-                set (REVISION) = (%s)
-                where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [nextrevision, self._resourceID, name]
-            )
-            self._txn.execSQL("""    
-                update CALENDAR
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
-        elif action == "insert":
-            self._txn.execSQL("""
-                delete from CALENDAR_OBJECT_REVISIONS
-                where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [self._resourceID, name,]
-            )
-            self._txn.execSQL("""
-                insert into CALENDAR_OBJECT_REVISIONS
-                (CALENDAR_RESOURCE_ID, RESOURCE_NAME, REVISION, DELETED)
-                values (%s, %s, %s, FALSE)
-                """,
-                [self._resourceID, name, nextrevision]
-            )
-            self._txn.execSQL("""    
-                update CALENDAR
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
-
-class AddressbookSyncTokenHelper(object):
-    """
-    This is a mixin for use by data store implementations.
-    """
-
-    def syncToken(self):
-        revision = self._txn.execSQL(
-            "select REVISION from ADDRESSBOOK where RESOURCE_ID = %s",
-            [self._resourceID])[0][0]
-        return "%s#%s" % (self._resourceID, revision,)
-
-    def _updateSyncToken(self):
-        
-        self._txn.execSQL("""
-            update ADDRESSBOOK
-            set (REVISION)
-            = (nextval('ADDRESSBOOK_OBJECT_REVISION_SEQ'))
-            where RESOURCE_ID = %s
-            """,
-            [self._resourceID]
-        )
-
-    def _insertRevision(self, name):
-        self._changeRevision("insert", name)
-
-    def _updateRevision(self, name):
-        self._changeRevision("update", name)
-
-    def _deleteRevision(self, name):
-        self._changeRevision("delete", name)
-
-    def _changeRevision(self, action, name):
-        
-        nextrevision = self._txn.execSQL("""
-            select nextval('ADDRESSBOOK_OBJECT_REVISION_SEQ')
-            """
-        )
-
-        if action == "delete":
-            self._txn.execSQL("""
-                update ADDRESSBOOK_OBJECT_REVISIONS
-                set (REVISION, DELETED) = (%s, TRUE)
-                where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [nextrevision, self._resourceID, name]
-            )
-            self._txn.execSQL("""    
-                update ADDRESSBOOK
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
-        elif action == "update":
-            self._txn.execSQL("""
-                update ADDRESSBOOK_OBJECT_REVISIONS
-                set (REVISION) = (%s)
-                where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [nextrevision, self._resourceID, name]
-            )
-            self._txn.execSQL("""    
-                update ADDRESSBOOK
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
-        elif action == "insert":
-            self._txn.execSQL("""
-                delete from ADDRESSBOOK_OBJECT_REVISIONS
-                where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
-                """,
-                [self._resourceID, name,]
-            )
-            self._txn.execSQL("""
-                insert into ADDRESSBOOK_OBJECT_REVISIONS
-                (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, REVISION, DELETED)
-                values (%s, %s, %s, FALSE)
-                """,
-                [self._resourceID, name, nextrevision]
-            )
-            self._txn.execSQL("""    
-                update ADDRESSBOOK
-                set (REVISION) = (%s)
-                where RESOURCE_ID = %s
-                """,
-                [nextrevision, self._resourceID]
-            )
         
\ No newline at end of file

Modified: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -35,14 +35,14 @@
 from twistedcaldav.vcard import Component as VComponent, InvalidVCardDataError
 from twistedcaldav.vcardindex import AddressBookIndex as OldIndex
 
+from txcarddav.addressbookstore.util import validateAddressBookComponent
 from txcarddav.iaddressbookstore import IAddressBook, IAddressBookObject
 from txcarddav.iaddressbookstore import IAddressBookHome
 
 from txdav.common.datastore.file import CommonDataStore, CommonHome,\
     CommonStoreTransaction, CommonHomeChild, CommonObjectResource,\
     CommonStubResource
-from txdav.common.icommondatastore import InvalidObjectResourceError,\
-    NoSuchObjectResourceError, InternalDataStoreError
+from txdav.common.icommondatastore import NoSuchObjectResourceError, InternalDataStoreError
 from txdav.datastore.file import hidden, writeOperation
 from txdav.propertystore.base import PropertyName
 
@@ -110,7 +110,7 @@
         return self._home
 
     def resourceType(self):
-        return ResourceType.addressbook
+        return ResourceType.addressbook #@UndefinedVariable
 
     ownerAddressBookHome = CommonHomeChild.ownerHome
     addressbookObjects = CommonHomeChild.objectResources
@@ -158,25 +158,9 @@
 
 
     @writeOperation
-    def setComponent(self, component):
-        if not isinstance(component, VComponent):
-            raise TypeError(type(component))
+    def setComponent(self, component, inserting=False):
+        validateAddressBookComponent(self, self._addressbook, component, inserting)
 
-        try:
-            if component.resourceUID() != self.uid():
-                raise InvalidObjectResourceError(
-                    "UID may not change (%s != %s)" % (
-                        component.resourceUID(), self.uid()
-                     )
-                )
-        except NoSuchObjectResourceError:
-            pass
-
-        try:
-            self._addressbook._doValidate(component)
-        except InvalidVCardDataError, e:
-            raise InvalidObjectResourceError(e)
-
         self._addressbook.retrieveOldIndex().addResource(
             self.name(), component
         )

Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,241 @@
+# -*- test-case-name: txcarddav.addressbookstore.test.test_sql -*-
+##
+# Copyright (c) 2010 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.
+##
+
+__all__ = [
+    "AddressBookHome",
+    "AddressBook",
+    "AddressBookObject",
+]
+
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType
+
+from twistedcaldav import carddavxml, customxml
+from twistedcaldav.vcard import Component as VCard
+
+from txdav.common.datastore.sql_legacy import \
+    PostgresLegacyABIndexEmulator, PostgresLegacyABInvitesEmulator,\
+    PostgresLegacyABSharesEmulator
+
+from txcarddav.addressbookstore.util import validateAddressBookComponent
+from txcarddav.iaddressbookstore import IAddressBookHome, IAddressBook,\
+    IAddressBookObject
+
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
+    CommonObjectResource
+from txdav.common.datastore.sql_tables import ADDRESSBOOK_TABLE,\
+    ADDRESSBOOK_BIND_TABLE, ADDRESSBOOK_OBJECT_REVISIONS_TABLE,\
+    ADDRESSBOOK_OBJECT_TABLE
+from txdav.propertystore.base import PropertyName
+
+from zope.interface.declarations import implements
+
+class AddressBookHome(CommonHome):
+
+    implements(IAddressBookHome)
+
+    def __init__(self, transaction, ownerUID, resourceID, notifier):
+        super(AddressBookHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+
+        self._shares = PostgresLegacyABSharesEmulator(self)
+        self._childClass = AddressBook
+        self._childTable = ADDRESSBOOK_TABLE
+        self._bindTable = ADDRESSBOOK_BIND_TABLE
+
+    addressbooks = CommonHome.children
+    listAddressbooks = CommonHome.listChildren
+    addressbookWithName = CommonHome.childWithName
+    createAddressBookWithName = CommonHome.createChildWithName
+    removeAddressBookWithName = CommonHome.removeChildWithName
+
+    def createdHome(self):
+        self.createAddressBookWithName("addressbook")
+
+class AddressBook(CommonHomeChild):
+    """
+    File-based implementation of L{IAddressBook}.
+    """
+    implements(IAddressBook)
+
+    def __init__(self, home, name, resourceID, notifier):
+        """
+        Initialize an addressbook pointing at a path on disk.
+
+        @param name: the subdirectory of addressbookHome where this addressbook
+            resides.
+        @type name: C{str}
+
+        @param addressbookHome: the home containing this addressbook.
+        @type addressbookHome: L{AddressBookHome}
+
+        @param realName: If this addressbook was just created, the name which it
+        will eventually have on disk.
+        @type realName: C{str}
+        """
+        
+        super(AddressBook, self).__init__(home, name, resourceID, notifier)
+
+        self._index = PostgresLegacyABIndexEmulator(self)
+        self._invites = PostgresLegacyABInvitesEmulator(self)
+        self._objectResourceClass = AddressBookObject
+        self._bindTable = ADDRESSBOOK_BIND_TABLE
+        self._homeChildTable = ADDRESSBOOK_TABLE
+        self._revisionsTable = ADDRESSBOOK_OBJECT_REVISIONS_TABLE
+        self._objectTable = ADDRESSBOOK_OBJECT_TABLE
+
+    @property
+    def _addressbookHome(self):
+        return self._home
+
+    def resourceType(self):
+        return ResourceType.addressbook #@UndefinedVariable
+
+    ownerAddressBookHome = CommonHomeChild.ownerHome
+    addressbookObjects = CommonHomeChild.objectResources
+    listAddressbookObjects = CommonHomeChild.listObjectResources
+    addressbookObjectWithName = CommonHomeChild.objectResourceWithName
+    addressbookObjectWithUID = CommonHomeChild.objectResourceWithUID
+    createAddressBookObjectWithName = CommonHomeChild.createObjectResourceWithName
+    removeAddressBookObjectWithName = CommonHomeChild.removeObjectResourceWithName
+    removeAddressBookObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+    addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
+
+
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+                PropertyName.fromElement(carddavxml.AddressBookDescription),
+            ),
+            (
+                PropertyName.fromElement(customxml.GETCTag),
+            ),
+        )
+
+    def _doValidate(self, component):
+        component.validForCardDAV()
+
+    def contentType(self):
+        """
+        The content type of Addresbook objects is text/vcard.
+        """
+        return MimeType.fromString("text/vcard; charset=utf-8")
+
+class AddressBookObject(CommonObjectResource):
+
+    implements(IAddressBookObject)
+
+    def __init__(self, name, addressbook, resid):
+
+        super(AddressBookObject, self).__init__(name, addressbook, resid)
+
+        self._objectTable = ADDRESSBOOK_OBJECT_TABLE
+
+    @property
+    def _addressbook(self):
+        return self._parentCollection
+
+    def addressbook(self):
+        return self._addressbook
+
+    def setComponent(self, component, inserting=False):
+        validateAddressBookComponent(self, self._addressbook, component, inserting)
+
+        self.updateDatabase(component, inserting=inserting)
+        if inserting:
+            self._addressbook._insertRevision(self._name)
+        else:
+            self._addressbook._updateRevision(self._name)
+
+        if self._addressbook._notifier:
+            self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
+
+    def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
+        """
+        Update the database tables for the new data being written.
+
+        @param component: addressbook data to store
+        @type component: L{Component}
+        """
+
+        componentText = str(component)
+        self._objectText = componentText
+
+        # ADDRESSBOOK_OBJECT table update
+        if inserting:
+            self._resourceID = self._txn.execSQL(
+                """
+                insert into ADDRESSBOOK_OBJECT
+                (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID)
+                 values
+                (%s, %s, %s, %s)
+                 returning RESOURCE_ID
+                """,
+                [
+                    self._addressbook._resourceID,
+                    self._name,
+                    componentText,
+                    component.resourceUID(),
+                ]
+            )[0][0]
+        else:
+            self._txn.execSQL(
+                """
+                update ADDRESSBOOK_OBJECT set
+                (VCARD_TEXT, VCARD_UID, MODIFIED)
+                 =
+                (%s, %s, timezone('UTC', CURRENT_TIMESTAMP))
+                 where RESOURCE_ID = %s
+                """,
+                [
+                    componentText,
+                    component.resourceUID(),
+                    self._resourceID
+                ]
+            )
+
+    def component(self):
+        return VCard.fromString(self.vCardText())
+
+    def text(self):
+        if self._objectText is None:
+            text = self._txn.execSQL(
+                "select VCARD_TEXT from ADDRESSBOOK_OBJECT where "
+                "RESOURCE_ID = %s", [self._resourceID]
+            )[0][0]
+            self._objectText = text
+            return text
+        else:
+            return self._objectText
+
+    vCardText = text
+
+    def uid(self):
+        return self.component().resourceUID()
+
+    def name(self):
+        return self._name
+
+    def componentType(self):
+        return self.component().mainType()
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of Addressbook objects is text/x-vcard.
+        """
+        return MimeType.fromString("text/vcard; charset=utf-8")

Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,75 @@
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Tests for txcaldav.calendarstore.postgres, mostly based on
+L{txcaldav.calendarstore.test.common}.
+"""
+
+from txcarddav.addressbookstore.test.common import CommonTests as AddressBookCommonTests
+
+from txdav.common.datastore.test.util import SQLStoreBuilder
+
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.vcard import Component as VCard
+
+
+theStoreBuilder = SQLStoreBuilder()
+buildStore = theStoreBuilder.buildStore
+
+class AddressBookSQLStorageTests(AddressBookCommonTests, unittest.TestCase):
+    """
+    AddressBook SQL storage tests.
+    """
+
+    @inlineCallbacks
+    def setUp(self):
+        super(AddressBookSQLStorageTests, self).setUp()
+        self.addressbookStore = yield buildStore(self, self.notifierFactory)
+        self.populate()
+
+    def populate(self):
+        populateTxn = self.addressbookStore.newTransaction()
+        for homeUID in self.requirements:
+            addressbooks = self.requirements[homeUID]
+            if addressbooks is not None:
+                home = populateTxn.addressbookHomeWithUID(homeUID, True)
+                # We don't want the default addressbook to appear unless it's
+                # explicitly listed.
+                home.removeAddressBookWithName("addressbook")
+                for addressbookName in addressbooks:
+                    addressbookObjNames = addressbooks[addressbookName]
+                    if addressbookObjNames is not None:
+                        home.createAddressBookWithName(addressbookName)
+                        addressbook = home.addressbookWithName(addressbookName)
+                        for objectName in addressbookObjNames:
+                            objData = addressbookObjNames[objectName]
+                            addressbook.createAddressBookObjectWithName(
+                                objectName, VCard.fromString(objData)
+                            )
+
+        populateTxn.commit()
+        self.notifierFactory.reset()
+
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{AddressBookStore} for testing.
+        """
+        return self.addressbookStore
+

Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,58 @@
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Utility logic common to multiple backend implementations.
+"""
+
+from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import InvalidVCardDataError
+
+from txdav.common.icommondatastore import InvalidObjectResourceError,\
+    NoSuchObjectResourceError
+
+def validateAddressBookComponent(addressbookObject, vcard, component, inserting):
+    """
+    Validate an addressbook component for a particular addressbook.
+
+    @param addressbookObject: The addressbook object whose component will be replaced.
+    @type addressbookObject: L{IAddressBookObject}
+
+    @param addressbook: The addressbook which the L{IAddressBookObject} is present in.
+    @type addressbook: L{IAddressBook}
+
+    @param component: The VComponent to be validated.
+    @type component: L{VComponent}
+    """
+
+    if not isinstance(component, VCard):
+        raise TypeError(type(component))
+
+    try:
+        if not inserting and component.resourceUID() != addressbookObject.uid():
+            raise InvalidObjectResourceError(
+                "UID may not change (%s != %s)" % (
+                    component.resourceUID(), addressbookObject.uid()
+                 )
+            )
+    except NoSuchObjectResourceError:
+        pass
+
+    try:
+        component.validForCardDAV()
+    except InvalidVCardDataError, e:
+        raise InvalidObjectResourceError(e)
+        
\ No newline at end of file

Modified: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -39,7 +39,8 @@
 from txdav.common.inotifications import INotificationCollection, \
     INotificationObject
 from txdav.datastore.file import DataStoreTransaction, DataStore, writeOperation, \
-    hidden, isValidName, cached, FileMetaDataMixin
+    hidden, isValidName, FileMetaDataMixin
+from txdav.datastore.util import cached
 from txdav.idav import IDataStore
 from txdav.propertystore.base import PropertyName
 from txdav.propertystore.xattr import PropertyStore
@@ -635,7 +636,7 @@
             raise ObjectResourceNameAlreadyExistsError(name)
 
         objectResource = self._objectResourceClass(name, self)
-        objectResource.setComponent(component)
+        objectResource.setComponent(component, inserting=True)
         self._cachedObjectResources[name] = objectResource
 
         # Note: setComponent triggers a notification, so we don't need to
@@ -704,8 +705,8 @@
 
         @param props: the L{PropertyStore} from C{properties()}.
         """
+        pass
 
-
     def _doValidate(self, component):
         raise NotImplementedError
 
@@ -742,7 +743,7 @@
 
 
     @writeOperation
-    def setComponent(self, component):
+    def setComponent(self, component, inserting=False):
         raise NotImplementedError
 
 
@@ -761,9 +762,19 @@
     def properties(self):
         uid = self._parentCollection._home.uid()
         props = PropertyStore(uid, lambda : self._path)
+        self.initPropertyStore(props)
         self._transaction.addOperation(props.flush, "object properties flush")
         return props
 
+    def initPropertyStore(self, props):
+        """
+        A hook for subclasses to override in order to set up their property
+        store after it's been created.
+
+        @param props: the L{PropertyStore} from C{properties()}.
+        """
+        pass
+
 class CommonStubResource(object):
     """
     Just enough resource to keep the collection sql DB classes going.
@@ -877,7 +888,7 @@
 
 
     @writeOperation
-    def setData(self, uid, xmltype, xmldata):
+    def setData(self, uid, xmltype, xmldata, inserting=False):
 
         rname = uid + ".xml"
         self._parentCollection.retrieveOldIndex().addOrUpdateRecord(

Modified: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,4 +1,3 @@
-# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
 ##
 # Copyright (c) 2010 Apple Inc. All rights reserved.
 #
@@ -14,10 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twext.python.log import Logger
-from txdav.datastore.sql import memoized
-from txcaldav.calendarstore.postgres import PostgresCalendarHome,\
-    PostgresAddressBookHome, PostgresNotificationCollection
 
 """
 SQL data store.
@@ -26,20 +21,45 @@
 __all__ = [
     "CommonDataStore",
     "CommonStoreTransaction",
+    "CommonHome",
 ]
 
-from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
+from twext.python.log import Logger, LoggingMixIn
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType
 
-from zope.interface.declarations import implements, directlyProvides
-
 from twisted.application.service import Service
+from twisted.python import hashlib
+from twisted.python.modules import getModule
+from twisted.python.util import FancyEqMixin
 
-from txdav.idav import IDataStore, AlreadyFinishedError
+from twistedcaldav.customxml import NotificationType
 
+from txdav.common.datastore.sql_legacy import PostgresLegacyNotificationsEmulator
 from txcaldav.icalendarstore import ICalendarTransaction
+
 from txcarddav.iaddressbookstore import IAddressBookTransaction
 
+from txdav.common.datastore.sql_tables import CALENDAR_HOME_TABLE,\
+    ADDRESSBOOK_HOME_TABLE, NOTIFICATION_HOME_TABLE, _BIND_MODE_OWN,\
+    _BIND_STATUS_ACCEPTED
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError,\
+    HomeChildNameAlreadyExistsError, NoSuchHomeChildError,\
+    ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError,\
+    NoSuchObjectResourceError
+from txdav.common.inotifications import INotificationCollection,\
+    INotificationObject
+from txdav.datastore.sql import memoized
+from txdav.datastore.util import cached
+from txdav.idav import IDataStore, AlreadyFinishedError
+from txdav.propertystore.base import PropertyName
+from txdav.propertystore.sql import PropertyStore
 
+from zope.interface.declarations import implements, directlyProvides
+
+v1_schema = getModule(__name__).filePath.sibling(
+    "sql_schema_v1.sql").getContent()
+
 log = Logger()
 
 ECALENDARTYPE = 0
@@ -97,8 +117,10 @@
             extraInterfaces.append(IAddressBookTransaction)
         directlyProvides(self, *extraInterfaces)
 
-        CommonStoreTransaction._homeClass[ECALENDARTYPE] = PostgresCalendarHome
-        CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = PostgresAddressBookHome
+        from txcaldav.calendarstore.sql import CalendarHome
+        from txcarddav.addressbookstore.sql import AddressBookHome
+        CommonStoreTransaction._homeClass[ECALENDARTYPE] = CalendarHome
+        CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = AddressBookHome
 
     def store(self):
         return self._store
@@ -136,19 +158,19 @@
     def homeWithUID(self, storeType, uid, create=False):
         
         if storeType == ECALENDARTYPE:
-            homeTable = "CALENDAR_HOME"
+            homeTable = CALENDAR_HOME_TABLE
         elif storeType == EADDRESSBOOKTYPE:
-            homeTable = "ADDRESSBOOK_HOME"
+            homeTable = ADDRESSBOOK_HOME_TABLE
 
         data = self.execSQL(
-            "select RESOURCE_ID from %s where OWNER_UID = %%s" % (homeTable,),
+            "select %(column_RESOURCE_ID)s from %(name)s where %(column_OWNER_UID)s = %%s" % homeTable,
             [uid]
         )
         if not data:
             if not create:
                 return None
             self.execSQL(
-                "insert into %s (OWNER_UID) values (%%s)" % (homeTable,),
+                "insert into %(name)s (%(column_OWNER_UID)s) values (%%s)" % homeTable,
                 [uid]
             )
             home = self.homeWithUID(storeType, uid)
@@ -171,18 +193,18 @@
         """
         rows = self.execSQL(
             """
-            select RESOURCE_ID from NOTIFICATION_HOME where
-            OWNER_UID = %s
-            """, [uid])
+            select %(column_RESOURCE_ID)s from %(name)s where
+            %(column_OWNER_UID)s = %%s
+            """ % NOTIFICATION_HOME_TABLE, [uid]
+        )
         if rows:
-            [[resourceID]] = rows
+            resourceID = rows[0][0]
         else:
-            [[resourceID]] = self.execSQL("select nextval('RESOURCE_ID_SEQ')")
-            resourceID = str(resourceID)
-            self.execSQL(
-                "insert into NOTIFICATION_HOME (RESOURCE_ID, OWNER_UID) "
-                "values (%s, %s)", [resourceID, uid])
-        return PostgresNotificationCollection(self, uid, resourceID)
+            resourceID = str(self.execSQL(
+                "insert into %(name)s (%(column_OWNER_UID)s) values (%%s) returning %(column_RESOURCE_ID)s" % NOTIFICATION_HOME_TABLE,
+                [uid]
+            )[0][0])
+        return NotificationCollection(self, uid, resourceID)
 
 
     def abort(self):
@@ -214,3 +236,836 @@
         self._postCommitOperations.append(operation)
         # FIXME: implement.
 
+class CommonHome(LoggingMixIn):
+
+    _childClass = None
+    _childTable = None
+    _bindTable = None
+
+    def __init__(self, transaction, ownerUID, resourceID, notifier):
+        self._txn = transaction
+        self._ownerUID = ownerUID
+        self._resourceID = resourceID
+        self._shares = None
+        self._children = {}
+        self._notifier = notifier
+
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+    def uid(self):
+        """
+        Retrieve the unique identifier for this home.
+
+        @return: a string.
+        """
+        return self._ownerUID
+
+
+    def transaction(self):
+        return self._txn
+
+
+    def retrieveOldShares(self):
+        return self._shares
+
+
+    def name(self):
+        """
+        Implement L{IDataStoreResource.name} to return the uid.
+        """
+        return self.uid()
+
+
+    def children(self):
+        """
+        Retrieve children contained in this home.
+        """
+        names = self.listChildren()
+        for name in names:
+            yield self.childWithName(name)
+
+
+    def listChildren(self):
+        """
+        Retrieve the names of the children in this home.
+
+        @return: an iterable of C{str}s.
+        """
+        # FIXME: not specified on the interface or exercised by the tests, but
+        # required by clients of the implementation!
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_NAME)s from %(name)s where "
+            "%(column_HOME_RESOURCE_ID)s = %%s "
+            "and %(column_BIND_MODE)s = %%s " % self._bindTable,
+            # Right now, we only show owned calendars.
+            [self._resourceID, _BIND_MODE_OWN]
+        )
+        names = [row[0] for row in rows]
+        return names
+
+
+    @memoized('name', '_children')
+    def childWithName(self, name):
+        """
+        Retrieve the child with the given C{name} contained in this
+        home.
+
+        @param name: a string.
+        @return: an L{ICalendar} or C{None} if no such child
+            exists.
+        """
+        data = self._txn.execSQL(
+            "select %(column_RESOURCE_ID)s from %(name)s where "
+            "%(column_RESOURCE_NAME)s = %%s and %(column_HOME_RESOURCE_ID)s = %%s "
+            "and %(column_BIND_MODE)s = %%s" % self._bindTable,
+            [name, self._resourceID, _BIND_MODE_OWN]
+        )
+        if not data:
+            return None
+        resourceID = data[0][0]
+        if self._notifier:
+            childID = "%s/%s" % (self.uid(), name)
+            notifier = self._notifier.clone(label="collection", id=childID)
+        else:
+            notifier = None
+        return self._childClass(self, name, resourceID, notifier)
+
+
+    def createChildWithName(self, name):
+        if name.startswith("."):
+            raise HomeChildNameNotAllowedError(name)
+
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_NAME)s from %(name)s where "
+            "%(column_RESOURCE_NAME)s = %%s AND "
+            "%(column_HOME_RESOURCE_ID)s = %%s" % self._bindTable,
+            [name, self._resourceID]
+        )
+        if rows:
+            raise HomeChildNameAlreadyExistsError()
+
+        rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
+        resourceID = rows[0][0]
+        self._txn.execSQL(
+            "insert into %(name)s (%(column_RESOURCE_ID)s) values "
+            "(%%s)" % self._childTable,
+            [resourceID])
+
+        self._txn.execSQL("""
+            insert into %(name)s (
+                %(column_HOME_RESOURCE_ID)s,
+                %(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_BIND_MODE)s,
+                %(column_SEEN_BY_OWNER)s, %(column_SEEN_BY_SHAREE)s, %(column_BIND_STATUS)s) values (
+            %%s, %%s, %%s, %%s, %%s, %%s, %%s)
+            """ % self._bindTable,
+            [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+             _BIND_STATUS_ACCEPTED]
+        )
+
+        newChild = self.childWithName(name)
+        newChild.properties()[
+            PropertyName.fromElement(ResourceType)] = newChild.resourceType()
+        newChild._updateSyncToken()
+        self.createdChild(newChild)
+
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
+    def createdChild(self, child):
+        pass
+
+
+    def removeChildWithName(self, name):
+        rows = self._txn.execSQL(
+            """select %(column_RESOURCE_ID)s from %(name)s
+               where %(column_RESOURCE_NAME)s = %%s and %(column_HOME_RESOURCE_ID)s = %%s""" % self._bindTable,
+            [name, self._resourceID]
+        )
+        if not rows:
+            raise NoSuchHomeChildError()
+        resourceID = rows[0][0]
+
+        self._txn.execSQL(
+            "delete from %(name)s where %(column_RESOURCE_ID)s = %%s" % self._childTable,
+            [resourceID]
+        )
+        self._children.pop(name, None)
+        if self._txn._cursor.rowcount == 0:
+            raise NoSuchHomeChildError()
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
+    @cached
+    def properties(self):
+        return PropertyStore(
+            self.uid(),
+            self._txn,
+            self._resourceID
+        )
+
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of objects
+        """
+        return None
+
+
+    def md5(self):
+        return None
+
+
+    def size(self):
+        return 0
+
+
+    def created(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+    def notifierID(self, label="default"):
+        if self._notifier:
+            return self._notifier.getID(label)
+        else:
+            return None
+
+class CommonHomeChild(LoggingMixIn, FancyEqMixin):
+    """
+    Common ancestor class of AddressBooks and Calendars.
+    """
+
+    compareAttributes = '_name _home _resourceID'.split()
+
+    _objectResourceClass = None
+    _bindTable = None
+    _homeChildTable = None
+    _revisionsTable = None
+    _objectTable = None
+
+    def __init__(self, home, name, resourceID, notifier):
+        self._home = home
+        self._name = name
+        self._resourceID = resourceID
+        self._objects = {}
+        self._notifier = notifier
+
+        self._index = None  # Derived classes need to set this
+        self._invites = None # Derived classes need to set this
+
+
+    @property
+    def _txn(self):
+        return self._home._txn
+
+
+    def resourceType(self):
+        return NotImplementedError
+
+
+    def retrieveOldIndex(self):
+        return self._index
+
+
+    def retrieveOldInvites(self):
+        return self._invites
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+    def name(self):
+        return self._name
+
+
+    def rename(self, name):
+        oldName = self._name
+        self._txn.execSQL(
+            "update %(name)s set %(column_RESOURCE_NAME)s = %%s "
+            "where %(column_RESOURCE_ID)s = %%s AND "
+            "%(column_HOME_RESOURCE_ID)s = %%s" % self._bindTable,
+            [name, self._resourceID, self._home._resourceID]
+        )
+        self._name = name
+        # update memos
+        del self._home._children[oldName]
+        self._home._children[name] = self
+        self._updateSyncToken()
+
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
+    def ownerHome(self):
+        return self._home
+
+
+    def setSharingUID(self, uid):
+        self.properties()._setPerUserUID(uid)
+
+
+    def objectResources(self):
+        for name in self.listObjectResources():
+            yield self.objectResourceWithName(name)
+
+
+    def listObjectResources(self):
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_NAME)s from %(name)s "
+            "where %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+            [self._resourceID])
+        return sorted([row[0] for row in rows])
+
+
+    @memoized('name', '_objects')
+    def objectResourceWithName(self, name):
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_ID)s from %(name)s "
+            "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+            [name, self._resourceID]
+        )
+        if not rows:
+            return None
+        resid = rows[0][0]
+        return self._objectResourceClass(name, self, resid)
+
+
+    @memoized('uid', '_objects')
+    def objectResourceWithUID(self, uid):
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s from %(name)s "
+            "where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+            [uid, self._resourceID]
+        )
+        if not rows:
+            return None
+        resid = rows[0][0]
+        name = rows[0][1]
+        return self._objectResourceClass(name, self, resid)
+
+
+    def createObjectResourceWithName(self, name, component):
+        if name.startswith("."):
+            raise ObjectResourceNameNotAllowedError(name)
+
+        rows = self._txn.execSQL(
+            "select %(column_RESOURCE_ID)s from %(name)s "
+            "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+            [name, self._resourceID]
+        )
+        if rows:
+            raise ObjectResourceNameAlreadyExistsError()
+
+        objectResource = self._objectResourceClass(name, self, None)
+        objectResource.setComponent(component, inserting=True)
+
+        # Note: setComponent triggers a notification, so we don't need to
+        # call notify( ) here like we do for object removal.
+
+
+    def removeObjectResourceWithName(self, name):
+        rows = self._txn.execSQL(
+            "delete from %(name)s "
+            "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
+            "returning %(column_UID)s" % self._objectTable,
+            [name, self._resourceID],
+            raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
+        )
+        uid = rows[0][0]
+        self._objects.pop(name, None)
+        self._objects.pop(uid, None)
+        self._deleteRevision(name)
+
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
+    def removeObjectResourceWithUID(self, uid):
+        rows = self._txn.execSQL(
+            "delete from %(name)s "
+            "where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
+            "returning %(column_RESOURCE_NAME)s" % self._objectTable,
+            [uid, self._resourceID],
+            raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
+        )
+        name = rows[0][0]
+        self._objects.pop(name, None)
+        self._objects.pop(uid, None)
+        self._deleteRevision(name)
+
+        if self._notifier:
+            self._txn.postCommit(self._notifier.notify)
+
+
+    def syncToken(self):
+        revision = self._txn.execSQL(
+            "select %(column_REVISION)s from %(name)s where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+            [self._resourceID])[0][0]
+        return "%s#%s" % (self._resourceID, revision,)
+
+    def objectResourcesSinceToken(self, token):
+        raise NotImplementedError()
+
+
+    def _updateSyncToken(self):
+        
+        self._txn.execSQL("""
+            update %(name)s
+            set (%(column_REVISION)s) = (nextval('%(sequence)s'))
+            where %(column_RESOURCE_ID)s = %%s
+            """ % self._homeChildTable,
+            [self._resourceID]
+        )
+
+    def _insertRevision(self, name):
+        self._changeRevision("insert", name)
+
+    def _updateRevision(self, name):
+        self._changeRevision("update", name)
+
+    def _deleteRevision(self, name):
+        self._changeRevision("delete", name)
+
+    def _changeRevision(self, action, name):
+        
+        nextrevision = self._txn.execSQL("""
+            select nextval('%(sequence)s')
+            """ % self._homeChildTable
+        )
+
+        if action == "delete":
+            self._txn.execSQL("""
+                update %(name)s
+                set (%(column_REVISION)s, %(column_DELETED)s) = (%%s, TRUE)
+                where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                """ % self._revisionsTable,
+                [nextrevision, self._resourceID, name]
+            )
+            self._txn.execSQL("""    
+                update %(name)s
+                set (%(column_REVISION)s) = (%%s)
+                where %(column_RESOURCE_ID)s = %%s
+                """ % self._homeChildTable,
+                [nextrevision, self._resourceID]
+            )
+        elif action == "update":
+            self._txn.execSQL("""
+                update %(name)s
+                set (%(column_REVISION)s) = (%%s)
+                where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                """ % self._revisionsTable,
+                [nextrevision, self._resourceID, name]
+            )
+            self._txn.execSQL("""    
+                update %(name)s
+                set (%(column_REVISION)s) = (%%s)
+                where %(column_RESOURCE_ID)s = %%s
+                """ % self._homeChildTable,
+                [nextrevision, self._resourceID]
+            )
+        elif action == "insert":
+            self._txn.execSQL("""
+                delete from %(name)s
+                where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+                """ % self._revisionsTable,
+                [self._resourceID, name,]
+            )
+            self._txn.execSQL("""
+                insert into %(name)s
+                (%(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_REVISION)s, %(column_DELETED)s)
+                values (%%s, %%s, %%s, FALSE)
+                """ % self._revisionsTable,
+                [self._resourceID, name, nextrevision]
+            )
+            self._txn.execSQL("""    
+                update %(name)s
+                set (%(column_REVISION)s) = (%%s)
+                where %(column_RESOURCE_ID)s = %%s
+                """ % self._homeChildTable,
+                [nextrevision, self._resourceID]
+            )
+    @cached
+    def properties(self):
+        props = PropertyStore(
+            self.ownerHome().uid(),
+            self._txn,
+            self._resourceID
+        )
+        self.initPropertyStore(props)
+        return props
+
+    def initPropertyStore(self, props):
+        """
+        A hook for subclasses to override in order to set up their property
+        store after it's been created.
+
+        @param props: the L{PropertyStore} from C{properties()}.
+        """
+        pass
+
+    def _doValidate(self, component):
+        raise NotImplementedError
+
+    def notifierID(self, label="default"):
+        if self._notifier:
+            return self._notifier.getID(label)
+        else:
+            return None
+
+
+
+    # IDataStoreResource
+    def contentType(self):
+        raise NotImplementedError()
+
+
+    def md5(self):
+        return None
+
+
+    def size(self):
+        return 0
+
+
+    def created(self):
+        created = self._txn.execSQL(
+            "select extract(EPOCH from %(column_CREATED)s) from %(name)s "
+            "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+            [self._resourceID]
+        )[0][0]
+        return int(created)
+
+    def modified(self):
+        modified = self._txn.execSQL(
+            "select extract(EPOCH from %(column_MODIFIED)s) from %(name)s "
+            "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+            [self._resourceID]
+        )[0][0]
+        return int(modified)
+
+class CommonObjectResource(LoggingMixIn, FancyEqMixin):
+    """
+    @ivar _path: The path of the file on disk
+
+    @type _path: L{FilePath}
+    """
+
+    compareAttributes = '_name _parentCollection'.split()
+    
+    _objectTable = None
+
+    def __init__(self, name, parent, resid):
+        self._name = name
+        self._parentCollection = parent
+        self._resourceID = resid
+        self._objectText = None
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+    @property
+    def _txn(self):
+        return self._parentCollection._txn
+
+    def setComponent(self, component, inserting=False):
+        raise NotImplementedError
+
+
+    def component(self):
+        raise NotImplementedError
+
+
+    def text(self):
+        raise NotImplementedError
+
+
+    def uid(self):
+        raise NotImplementedError
+
+    @cached
+    def properties(self):
+        props = PropertyStore(
+            self.uid(),
+            self._txn,
+            self._resourceID
+        )
+        self.initPropertyStore(props)
+        return props
+
+    def initPropertyStore(self, props):
+        """
+        A hook for subclasses to override in order to set up their property
+        store after it's been created.
+
+        @param props: the L{PropertyStore} from C{properties()}.
+        """
+        pass
+
+    # IDataStoreResource
+    def contentType(self):
+        raise NotImplementedError()
+
+    def md5(self):
+        return None
+
+    def size(self):
+        size = self._txn.execSQL(
+            "select character_length(%(column_TEXT)s) from %(name)s "
+            "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+            [self._resourceID]
+        )[0][0]
+        return size
+
+
+    def created(self):
+        created = self._txn.execSQL(
+            "select extract(EPOCH from %(column_CREATED)s) from %(name)s "
+            "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+            [self._resourceID]
+        )[0][0]
+        return int(created)
+
+    def modified(self):
+        modified = self._txn.execSQL(
+            "select extract(EPOCH from %(column_MODIFIED)s) from %(name)s "
+            "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+            [self._resourceID]
+        )[0][0]
+        return int(modified)
+
+class NotificationCollection(LoggingMixIn, FancyEqMixin):
+
+    implements(INotificationCollection)
+
+    compareAttributes = '_uid _resourceID'.split()
+
+    _objectResourceClass = None
+    _bindTable = None
+    _homeChildTable = None
+    _revisionsTable = None
+    _objectTable = None
+
+    def __init__(self, txn, uid, resourceID):
+
+        self._txn = txn
+        self._uid = uid
+        self._resourceID = resourceID
+        self._notifications = {}
+
+
+    def resourceType(self):
+        return ResourceType.notification #@UndefinedVariable
+
+    def retrieveOldIndex(self):
+        return PostgresLegacyNotificationsEmulator(self)
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+    def name(self):
+        return 'notification'
+
+    def uid(self):
+        return self._uid
+
+    def notificationObjects(self):
+        for name in self.listNotificationObjects():
+            yield self.notificationObjectWithName(name)
+
+    def listNotificationObjects(self):
+        rows = self._txn.execSQL(
+            "select (NOTIFICATION_UID) from NOTIFICATION "
+            "where NOTIFICATION_HOME_RESOURCE_ID = %s",
+            [self._resourceID])
+        return sorted(["%s.xml" % row[0] for row in rows])
+
+    def _nameToUID(self, name):
+        """
+        Based on the file-backed implementation, the 'name' is just uid +
+        ".xml".
+        """
+        return name.rsplit(".", 1)[0]
+
+
+    def notificationObjectWithName(self, name):
+        return self.notificationObjectWithUID(self._nameToUID(name))
+
+    @memoized('uid', '_notifications')
+    def notificationObjectWithUID(self, uid):
+        rows = self._txn.execSQL(
+            "select RESOURCE_ID from NOTIFICATION "
+            "where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s",
+            [uid, self._resourceID])
+        if rows:
+            resourceID = rows[0][0]
+            return NotificationObject(self, resourceID)
+        else:
+            return None
+
+
+    def writeNotificationObject(self, uid, xmltype, xmldata):
+
+        inserting = False
+        notificationObject = self.notificationObjectWithUID(uid)
+        if notificationObject is None:
+            notificationObject = NotificationObject(self, None)
+            inserting = True
+        notificationObject.setData(uid, xmltype, xmldata, inserting=inserting)
+
+
+    def removeNotificationObjectWithName(self, name):
+        self.removeNotificationObjectWithUID(self._nameToUID(name))
+
+
+    def removeNotificationObjectWithUID(self, uid):
+        self._txn.execSQL(
+            "delete from NOTIFICATION "
+            "where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s",
+            [uid, self._resourceID]
+        )
+        self._notifications.pop(uid, None)
+
+
+    def syncToken(self):
+        return 'dummy-sync-token'
+
+
+    def notificationObjectsSinceToken(self, token):
+        changed = []
+        removed = []
+        token = self.syncToken()
+        return (changed, removed, token)
+
+
+    @cached
+    def properties(self):
+        return PropertyStore(
+            self._uid,
+            self._txn,
+            self._resourceID
+        )
+
+class NotificationObject(LoggingMixIn, FancyEqMixin):
+    implements(INotificationObject)
+
+    compareAttributes = '_resourceID _home'.split()
+
+    def __init__(self, home, resourceID):
+        self._home = home
+        self._resourceID = resourceID
+
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+    
+    @property
+    def _txn(self):
+        return self._home._txn
+
+
+    def notificationCollection(self):
+        return self._home
+
+
+    def name(self):
+        return self.uid() + ".xml"
+
+
+    def setData(self, uid, xmltype, xmldata, inserting=False):
+
+        xmltypeString = xmltype.toxml()
+        if inserting:
+            rows = self._txn.execSQL(
+                "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
+                "values (%s, %s, %s, %s) returning RESOURCE_ID",
+                [self._home._resourceID, uid, xmltypeString, xmldata]
+            )
+            self._resourceID = rows[0][0]
+        else:
+            self._txn.execSQL(
+                "update NOTIFICATION set XML_TYPE = %s, XML_DATA = %s "
+                "where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
+                [xmltypeString, xmldata, self._home._resourceID, uid])
+
+        self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
+
+
+    def _fieldQuery(self, field):
+        data = self._txn.execSQL(
+            "select " + field + " from NOTIFICATION "
+            "where RESOURCE_ID = %s",
+            [self._resourceID]
+        )
+        return data[0][0]
+
+
+    def xmldata(self):
+        return self._fieldQuery("XML_DATA")
+
+
+    def uid(self):
+        return self._fieldQuery("NOTIFICATION_UID")
+
+
+    @cached
+    def properties(self):
+        props = PropertyStore(
+            self._home.uid(),
+            self._txn,
+            self._resourceID
+        )
+        self.initPropertyStore(props)
+        return props
+
+    def initPropertyStore(self, props):
+        # Setup peruser special properties
+        props.setSpecialProperties(
+            (
+            ),
+            (
+                PropertyName.fromElement(NotificationType),
+            ),
+        )
+
+    def contentType(self):
+        """
+        The content type of NotificationObjects is text/xml.
+        """
+        return MimeType.fromString("text/xml")
+
+
+    def md5(self):
+        return hashlib.md5(self.xmldata()).hexdigest()
+
+
+    def size(self):
+        size = self._txn.execSQL(
+            "select character_length(XML_DATA) from NOTIFICATION "
+            "where RESOURCE_ID = %s",
+            [self._resourceID]
+        )[0][0]
+        return size
+
+
+    def created(self):
+        modified = self._txn.execSQL(
+            "select extract(EPOCH from CREATED) from NOTIFICATION "
+            "where RESOURCE_ID = %s",
+            [self._resourceID]
+        )[0][0]
+        return int(modified)
+
+    def modified(self):
+        modified = self._txn.execSQL(
+            "select extract(EPOCH from MODIFIED) from NOTIFICATION "
+            "where RESOURCE_ID = %s", [self._resourceID]
+        )[0][0]
+        return int(modified)

Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,1325 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
+##
+# Copyright (c) 2010 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.
+##
+
+
+"""
+PostgreSQL data store.
+"""
+
+import datetime
+import StringIO
+
+from twistedcaldav.sharing import SharedCollectionRecord
+
+from twisted.python import hashlib
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger, LoggingMixIn
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.dateops import normalizeForIndex
+from twistedcaldav.index import IndexedSearchException, ReservationError,\
+    SyncTokenValidException
+from twistedcaldav.memcachepool import CachePoolUserMixIn
+from twistedcaldav.notifications import NotificationRecord
+from twistedcaldav.query import calendarqueryfilter, calendarquery, \
+    addressbookquery
+from twistedcaldav.query.sqlgenerator import sqlgenerator
+from twistedcaldav.sharing import Invite
+
+from txdav.common.datastore.sql_tables import \
+    _BIND_MODE_OWN, _BIND_MODE_READ, _BIND_MODE_WRITE, _BIND_STATUS_INVITED,\
+    _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, _BIND_STATUS_INVALID
+
+log = Logger()
+
+indexfbtype_to_icalfbtype = {
+    0: '?',
+    1: 'F',
+    2: 'B',
+    3: 'U',
+    4: 'T',
+}
+
+class PostgresLegacyInvitesEmulator(object):
+    """
+    Emulator for the implicit interface specified by
+    L{twistedcaldav.sharing.InvitesDatabase}.
+    """
+
+
+    def __init__(self, calendar):
+        self._calendar = calendar
+
+
+    @property
+    def _txn(self):
+        return self._calendar._txn
+
+
+    def create(self):
+        "No-op, because the index implicitly always exists in the database."
+
+
+    def remove(self):
+        "No-op, because the index implicitly always exists in the database."
+
+
+    def allRecords(self):
+        for row in self._txn.execSQL(
+                """
+                select
+                    INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
+                    CALENDAR_HOME.OWNER_UID, CALENDAR_BIND.BIND_MODE,
+                    CALENDAR_BIND.BIND_STATUS, CALENDAR_BIND.MESSAGE
+                from
+                    INVITE, CALENDAR_HOME, CALENDAR_BIND
+                where
+                    INVITE.RESOURCE_ID = %s and
+                    INVITE.HOME_RESOURCE_ID = 
+                        CALENDAR_HOME.RESOURCE_ID and
+                    CALENDAR_BIND.CALENDAR_RESOURCE_ID =
+                        INVITE.RESOURCE_ID and
+                    CALENDAR_BIND.CALENDAR_HOME_RESOURCE_ID =
+                        INVITE.HOME_RESOURCE_ID
+                order by
+                    INVITE.NAME asc
+                """, [self._calendar._resourceID]):
+            [inviteuid, common_name, userid, ownerUID,
+                bindMode, bindStatus, summary] = row
+            # FIXME: this is really the responsibility of the protocol layer.
+            state = {
+                _BIND_STATUS_INVITED: "NEEDS-ACTION",
+                _BIND_STATUS_ACCEPTED: "ACCEPTED",
+                _BIND_STATUS_DECLINED: "DECLINED",
+                _BIND_STATUS_INVALID: "INVALID",
+            }[bindStatus]
+            access = {
+                _BIND_MODE_READ: "read-only",
+                _BIND_MODE_WRITE: "read-write"
+            }[bindMode]
+            principalURL = "/principals/__uids__/%s/" % (ownerUID,)
+            yield Invite(
+                inviteuid, userid, principalURL, common_name,
+                access, state, summary
+            )
+
+
+    def recordForUserID(self, userid):
+        for record in self.allRecords():
+            if record.userid == userid:
+                return record
+
+
+    def recordForPrincipalURL(self, principalURL):
+        for record in self.allRecords():
+            if record.principalURL == principalURL:
+                return record
+
+
+    def recordForInviteUID(self, inviteUID):
+        for record in self.allRecords():
+            if record.inviteuid == inviteUID:
+                return record
+
+
+    def addOrUpdateRecord(self, record):
+        bindMode = {'read-only': _BIND_MODE_READ,
+                    'read-write': _BIND_MODE_WRITE}[record.access]
+        bindStatus = {
+            "NEEDS-ACTION": _BIND_STATUS_INVITED,
+            "ACCEPTED": _BIND_STATUS_ACCEPTED,
+            "DECLINED": _BIND_STATUS_DECLINED,
+            "INVALID": _BIND_STATUS_INVALID,
+        }[record.state]
+        # principalURL is derived from a directory record's principalURL() so
+        # it will always contain the UID.  The form is '/principals/__uids__/x'
+        # (and may contain a trailing slash).
+        principalUID = record.principalURL.split("/")[3]
+        shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
+        rows = self._txn.execSQL(
+            "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
+            [record.userid]
+        )
+        if rows:
+            [[resourceID, homeResourceID]] = rows
+            # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
+            self._txn.execSQL("""
+                update CALENDAR_BIND set BIND_MODE = %s,
+                BIND_STATUS = %s, MESSAGE = %s
+                where
+                    CALENDAR_RESOURCE_ID = %s and
+                    CALENDAR_HOME_RESOURCE_ID = %s
+            """, [bindMode, bindStatus, record.summary,
+                resourceID, homeResourceID])
+            self._txn.execSQL("""
+                update INVITE set NAME = %s, INVITE_UID = %s
+                where RECIPIENT_ADDRESS = %s
+                """,
+                [record.name, record.inviteuid, record.userid]
+            )
+        else:
+            self._txn.execSQL(
+                """
+                insert into INVITE (
+                    INVITE_UID, NAME,
+                    HOME_RESOURCE_ID, RESOURCE_ID,
+                    RECIPIENT_ADDRESS
+                )
+                values (%s, %s, %s, %s, %s)
+                """,
+                [
+                    record.inviteuid, record.name,
+                    shareeHome._resourceID, self._calendar._resourceID,
+                    record.userid
+                ])
+            self._txn.execSQL(
+                """
+                insert into CALENDAR_BIND
+                (
+                    CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID, 
+                    CALENDAR_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
+                    SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
+                )
+                values (%s, %s, %s, %s, %s, %s, %s, %s)
+                """,
+                [
+                    shareeHome._resourceID,
+                    self._calendar._resourceID,
+                    None, # this is NULL because it is not bound yet, let's be
+                          # explicit about that.
+                    bindMode,
+                    bindStatus,
+                    False,
+                    False,
+                    record.summary
+                ])
+
+
+    def removeRecordForUserID(self, userid):
+        rec = self.recordForUserID(userid)
+        self.removeRecordForInviteUID(rec.inviteuid)
+
+
+    def removeRecordForPrincipalURL(self, principalURL):
+        raise NotImplementedError("removeRecordForPrincipalURL")
+
+
+    def removeRecordForInviteUID(self, inviteUID):
+        rows = self._txn.execSQL("""
+                select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
+                INVITE_UID = %s
+            """, [inviteUID])
+        if rows:
+            [[homeID, resourceID]] = rows
+            self._txn.execSQL(
+                "delete from CALENDAR_BIND where "
+                "CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s",
+                [homeID, resourceID])
+            self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
+                [inviteUID])
+
+
+
+class PostgresLegacySharesEmulator(object):
+
+    def __init__(self, home):
+        self._home = home
+
+
+    @property
+    def _txn(self):
+        return self._home._txn
+
+
+    def create(self):
+        pass
+
+
+    def remove(self):
+        pass
+
+
+    def allRecords(self):
+        # This should have been a smart join that got all these columns at
+        # once, but let's not bother to fix it, since the actual query we
+        # _want_ to do (just look for calendar binds in a particular homes) is
+        # much simpler anyway; we should just do that.
+        shareRows = self._txn.execSQL(
+            """
+            select CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, MESSAGE
+            from CALENDAR_BIND
+                where CALENDAR_HOME_RESOURCE_ID = %s and
+                BIND_MODE != %s and
+                CALENDAR_RESOURCE_NAME is not null
+            """, [self._home._resourceID, _BIND_MODE_OWN])
+        for resourceID, resourceName, summary in shareRows:
+            [[shareuid]] = self._txn.execSQL(
+                """
+                select INVITE_UID
+                from INVITE
+                where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
+                """, [resourceID, self._home._resourceID])
+            sharetype = 'I'
+            [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
+                """
+                select CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME
+                from CALENDAR_BIND
+                where CALENDAR_RESOURCE_ID = %s and
+                    BIND_MODE = %s
+                """, [resourceID, _BIND_MODE_OWN]
+                )
+            [[ownerUID]] = self._txn.execSQL(
+                "select OWNER_UID from CALENDAR_HOME where RESOURCE_ID = %s",
+                [ownerHomeID])
+            hosturl = '/calendars/__uids__/%s/%s' % (
+                ownerUID, ownerResourceName
+            )
+            localname = resourceName
+            record = SharedCollectionRecord(
+                shareuid, sharetype, hosturl, localname, summary
+            )
+            yield record
+
+
+    def _search(self, **kw):
+        [[key, value]] = kw.items()
+        for record in self.allRecords():
+            if getattr(record, key) == value:
+                return record
+
+    def recordForLocalName(self, localname):
+        return self._search(localname=localname)
+
+    def recordForShareUID(self, shareUID):
+        return self._search(shareuid=shareUID)
+
+
+    def addOrUpdateRecord(self, record):
+#        print '*** SHARING***: Adding or updating this record:'
+#        import pprint
+#        pprint.pprint(record.__dict__)
+        # record.hosturl -> /calendars/__uids__/<uid>/<calendarname>
+        splithost = record.hosturl.split('/')
+        ownerUID = splithost[3]
+        ownerCalendarName = splithost[4]
+        ownerHome = self._txn.calendarHomeWithUID(ownerUID)
+        ownerCalendar = ownerHome.calendarWithName(ownerCalendarName)
+        calendarResourceID = ownerCalendar._resourceID
+
+        # There needs to be a bind already, one that corresponds to the
+        # invitation.  The invitation's UID is the same as the share UID.  I
+        # just need to update its 'localname', i.e.
+        # CALENDAR_BIND.CALENDAR_RESOURCE_NAME.
+
+        self._txn.execSQL(
+            """
+            update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s
+            where CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s
+            """,
+            [record.localname, self._home._resourceID, calendarResourceID]
+        )
+
+
+    def removeRecordForLocalName(self, localname):
+        self._txn.execSQL(
+            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+            "and CALENDAR_HOME_RESOURCE_ID = %s",
+            [localname, self._home._resourceID]
+        )
+
+
+    def removeRecordForShareUID(self, shareUID):
+        pass
+#        c = self._home._cursor()
+#        c.execute(
+#            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+#            "and CALENDAR_HOME_RESOURCE_ID = %s",
+#            [self._home._resourceID]
+#        )
+
+
+
+class postgresqlgenerator(sqlgenerator):
+    """
+    Query generator for postgreSQL indexed searches.  (Currently unused: work
+    in progress.)
+    """
+
+    ISOP = " = "
+    CONTAINSOP = " LIKE "
+    NOTCONTAINSOP = " NOT LIKE "
+    FIELDS = {
+        "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
+        "UID":  "CALENDAR_OBJECT.ICALENDAR_UID",
+    }
+
+    def __init__(self, expr, calendarid, userid):
+        self.RESOURCEDB = "CALENDAR_OBJECT"
+        self.TIMESPANDB = "TIME_RANGE"
+        self.TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
+        self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
+        self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
+        self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND CALENDAR_OBJECT.CALENDAR_RESOURCE_ID = %s"
+        self.TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
+
+        super(postgresqlgenerator, self).__init__(expr, calendarid, userid)
+
+
+    def generate(self):
+        """
+        Generate the actual SQL 'where ...' expression from the passed in
+        expression tree.
+        
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
+            partial SQL statement, and the C{list} is the list of argument
+            substitutions to use with the SQL API execute method.
+        """
+
+        # Init state
+        self.sout = StringIO.StringIO()
+        self.arguments = []
+        self.substitutions = []
+        self.usedtimespan = False
+
+        # Generate ' where ...' partial statement
+        self.sout.write(self.WHERE)
+        self.generateExpression(self.expression)
+
+        # Prefix with ' from ...' partial statement
+        select = self.FROM + self.RESOURCEDB
+        if self.usedtimespan:
+            self.frontArgument(self.userid)
+            select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
+                self.TIMESPANDB,
+                self.TRANSPARENCYDB,
+                self.TIMESPANTEST_JOIN_ON_PIECE
+            )
+        select += self.sout.getvalue()
+
+        select = select % tuple(self.substitutions)
+
+        return select, self.arguments
+
+
+    def addArgument(self, arg):
+        self.arguments.append(arg)
+        self.substitutions.append("%s")
+        self.sout.write("%s")
+
+    def setArgument(self, arg):
+        self.arguments.append(arg)
+        self.substitutions.append("%s")
+
+    def frontArgument(self, arg):
+        self.arguments.insert(0, arg)
+        self.substitutions.insert(0, "%s")
+
+    def containsArgument(self, arg):
+        return "%%%s%%" % (arg,)
+
+
+class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
+    def __init__(self, index, cachePool=None):
+        self.index = index
+        self._cachePool = cachePool
+
+    def _key(self, uid):
+        return 'reservation:%s' % (
+            hashlib.md5('%s:%s' % (uid,
+                                   self.index.resource._resourceID)).hexdigest())
+
+    def reserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Reserving UID %r @ %r" % (
+                uid,
+                self.index.resource))
+
+        def _handleFalse(result):
+            if result is False:
+                raise ReservationError(
+                    "UID %s already reserved for calendar collection %s."
+                    % (uid, self.index.resource._name)
+                    )
+
+        d = self.getCachePool().add(self._key(uid),
+                                    'reserved',
+                                    expireTime=config.UIDReservationTimeOut)
+        d.addCallback(_handleFalse)
+        return d
+
+
+    def unreserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Unreserving UID %r @ %r" % (
+                uid,
+                self.index.resource))
+
+        def _handleFalse(result):
+            if result is False:
+                raise ReservationError(
+                    "UID %s is not reserved for calendar collection %s."
+                    % (uid, self.index.resource._resourceID)
+                    )
+
+        d = self.getCachePool().delete(self._key(uid))
+        d.addCallback(_handleFalse)
+        return d
+
+
+    def isReservedUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Is reserved UID %r @ %r" % (
+                uid,
+                self.index.resource))
+
+        def _checkValue((flags, value)):
+            if value is None:
+                return False
+            else:
+                return True
+
+        d = self.getCachePool().get(self._key(uid))
+        d.addCallback(_checkValue)
+        return d
+
+class DummyUIDReserver(LoggingMixIn):
+
+    def __init__(self, index):
+        self.index = index
+        self.reservations = set()
+
+    def _key(self, uid):
+        return 'reservation:%s' % (
+            hashlib.md5('%s:%s' % (uid,
+                                   self.index.resource._resourceID)).hexdigest())
+
+    def reserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Reserving UID %r @ %r" % (
+                uid,
+                self.index.resource))
+
+        key = self._key(uid)
+        if key in self.reservations:
+            raise ReservationError(
+                "UID %s already reserved for calendar collection %s."
+                % (uid, self.index.resource._name)
+                )
+        self.reservations.add(key)
+        return succeed(None)
+
+
+    def unreserveUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Unreserving UID %r @ %r" % (
+                uid,
+                self.index.resource))
+
+        key = self._key(uid)
+        if key in self.reservations:
+            self.reservations.remove(key)
+        return succeed(None)
+
+
+    def isReservedUID(self, uid):
+        uid = uid.encode('utf-8')
+        self.log_debug("Is reserved UID %r @ %r" % (
+                uid,
+                self.index.resource))
+        key = self._key(uid)
+        return succeed(key in self.reservations)
+
+class PostgresLegacyIndexEmulator(LoggingMixIn):
+    """
+    Emulator for L{twistedcaldv.index.Index} and
+    L{twistedcaldv.index.IndexSchedule}.
+    """
+
+    def __init__(self, calendar):
+        self.resource = self.calendar = calendar
+        if (
+            hasattr(config, "Memcached") and
+            config.Memcached.Pools.Default.ClientEnabled
+        ):
+            self.reserver = MemcachedUIDReserver(self)
+        else:
+            # This is only used with unit tests
+            self.reserver = DummyUIDReserver(self)
+
+    @property
+    def _txn(self):
+        return self.calendar._txn
+
+
+    def reserveUID(self, uid):
+        if self.calendar._name == "inbox":
+            return succeed(None)
+        else:
+            return self.reserver.reserveUID(uid)
+
+
+    def unreserveUID(self, uid):
+        if self.calendar._name == "inbox":
+            return succeed(None)
+        else:
+            return self.reserver.unreserveUID(uid)
+
+
+    def isReservedUID(self, uid):
+        if self.calendar._name == "inbox":
+            return succeed(False)
+        else:
+            return self.reserver.isReservedUID(uid)
+
+
+    def isAllowedUID(self, uid, *names):
+        """
+        Checks to see whether to allow an operation which would add the
+        specified UID to the index.  Specifically, the operation may not
+        violate the constraint that UIDs must be unique.
+        @param uid: the UID to check
+        @param names: the names of resources being replaced or deleted by the
+            operation; UIDs associated with these resources are not checked.
+        @return: True if the UID is not in the index and is not reserved,
+            False otherwise.
+        """
+        if self.calendar._name == "inbox":
+            return True
+        else:
+            rname = self.resourceNameForUID(uid)
+            return (rname is None or rname in names)
+
+    def resourceUIDForName(self, name):
+        obj = self.calendar.calendarObjectWithName(name)
+        if obj is None:
+            return None
+        return obj.uid()
+
+
+    def resourceNameForUID(self, uid):
+        obj = self.calendar.calendarObjectWithUID(uid)
+        if obj is None:
+            return None
+        return obj.name()
+
+
+    def notExpandedBeyond(self, minDate):
+        """
+        Gives all resources which have not been expanded beyond a given date
+        in the database.  (Unused; see above L{postgresqlgenerator}.
+        """
+        return [row[0] for row in self._txn.execSQL(
+            "select RESOURCE_NAME from CALENDAR_OBJECT "
+            "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
+            [normalizeForIndex(minDate), self.calendar._resourceID]
+        )]
+
+
+    def reExpandResource(self, name, expand_until):
+        """
+        Given a resource name, remove it from the database and re-add it
+        with a longer expansion.
+        """
+        obj = self.calendar.calendarObjectWithName(name)
+        obj.updateDatabase(obj.component(), expand_until=expand_until, reCreate=True)
+
+    def testAndUpdateIndex(self, minDate):
+        # Find out if the index is expanded far enough
+        names = self.notExpandedBeyond(minDate)
+
+        # Actually expand recurrence max
+        for name in names:
+            self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
+            self.reExpandResource(name, minDate)
+
+    def whatchanged(self, revision):
+
+        results = [
+            (name.encode("utf-8"), deleted)
+            for name, deleted in
+            self._txn.execSQL(
+                """select RESOURCE_NAME, DELETED from CALENDAR_OBJECT_REVISIONS
+                   where REVISION > %s and CALENDAR_RESOURCE_ID = %s""",
+                [revision, self.calendar._resourceID],
+            )
+        ]
+        results.sort(key=lambda x:x[1])
+        
+        changed = []
+        deleted = []
+        for name, wasdeleted in results:
+            if name:
+                if wasdeleted:
+                    if revision:
+                        deleted.append(name)
+                else:
+                    changed.append(name)
+            else:
+                raise SyncTokenValidException
+        
+        return changed, deleted,
+
+    def indexedSearch(self, filter, useruid='', fbtype=False):
+        """
+        Finds resources matching the given qualifiers.
+        @param filter: the L{Filter} for the calendar-query to execute.
+        @return: an iterable of tuples for each resource matching the
+            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
+            C{name} is the resource name, C{uid} is the resource UID, and
+            C{type} is the resource iCalendar component type.x
+        """
+
+        # Make sure we have a proper Filter element and get the partial SQL
+        # statement to use.
+        if isinstance(filter, calendarqueryfilter.Filter):
+            qualifiers = calendarquery.sqlcalendarquery(filter, self.calendar._resourceID, useruid, generator=postgresqlgenerator)
+            if qualifiers is not None:
+                # Determine how far we need to extend the current expansion of
+                # events. If we have an open-ended time-range we will expand one
+                # year past the start. That should catch bounded recurrences - unbounded
+                # will have been indexed with an "infinite" value always included.
+                maxDate, isStartDate = filter.getmaxtimerange()
+                if maxDate:
+                    maxDate = maxDate.date()
+                    if isStartDate:
+                        maxDate += datetime.timedelta(days=365)
+                    self.testAndUpdateIndex(maxDate)
+            else:
+                # We cannot handler this filter in an indexed search
+                raise IndexedSearchException()
+
+        else:
+            qualifiers = None
+
+        # Perform the search
+        if qualifiers is None:
+            rowiter = self._txn.execSQL(
+                "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
+                [self.calendar._resourceID, ],
+            )
+        else:
+            if fbtype:
+                # For a free-busy time-range query we return all instances
+                rowiter = self._txn.execSQL(
+                    """select DISTINCT
+                        CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE, CALENDAR_OBJECT.ORGANIZER,
+                        TIME_RANGE.FLOATING, TIME_RANGE.START_DATE, TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT""" +
+                    qualifiers[0],
+                    qualifiers[1]
+                )
+            else:
+                rowiter = self._txn.execSQL(
+                    "select DISTINCT CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE" +
+                    qualifiers[0],
+                    qualifiers[1]
+                )
+
+        # Check result for missing resources
+
+        for row in rowiter:
+            if fbtype:
+                row = list(row)
+                row[4] = 'Y' if row[4] else 'N'
+                row[7] = indexfbtype_to_icalfbtype[row[7]]
+                row[8] = 'T' if row[9] else 'F'
+                del row[9]
+            yield row
+
+
+    def bruteForceSearch(self):
+        return self._txn.execSQL(
+            "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from "
+            "CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
+            [self.calendar._resourceID]
+        )
+
+
+    def resourcesExist(self, names):
+        return list(set(names).intersection(
+            set(self.calendar.listCalendarObjects())))
+
+
+    def resourceExists(self, name):
+        return bool(
+            self._txn.execSQL(
+                "select RESOURCE_NAME from CALENDAR_OBJECT where "
+                "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
+                [name, self.calendar._resourceID]
+            )
+        )
+
+
+
+
+class PostgresLegacyNotificationsEmulator(object):
+    def __init__(self, notificationsCollection):
+        self._collection = notificationsCollection
+
+
+    def _recordForObject(self, notificationObject):
+        return NotificationRecord(
+            notificationObject.uid(),
+            notificationObject.name(),
+            notificationObject._fieldQuery("XML_TYPE"))
+
+
+    def recordForName(self, name):
+        return self._recordForObject(
+            self._collection.notificationObjectWithName(name)
+        )
+
+
+    def recordForUID(self, uid):
+        return self._recordForObject(
+            self._collection.notificationObjectWithUID(uid)
+        )
+
+
+    def removeRecordForUID(self, uid):
+        self._collection.removeNotificationObjectWithUID(uid)
+
+
+    def removeRecordForName(self, name):
+        self._collection.removeNotificationObjectWithName(name)
+
+
+
+
+# CARDDAV
+
+class postgresqladbkgenerator(sqlgenerator):
+    """
+    Query generator for postgreSQL indexed searches.  (Currently unused: work
+    in progress.)
+    """
+
+    ISOP = " = "
+    CONTAINSOP = " LIKE "
+    NOTCONTAINSOP = " NOT LIKE "
+    FIELDS = {
+        "UID":  "ADDRESSBOOK_OBJECT.VCARD_UID",
+    }
+
+    def __init__(self, expr, addressbookid):
+        self.RESOURCEDB = "ADDRESSBOOK_OBJECT"
+
+        super(postgresqladbkgenerator, self).__init__(expr, addressbookid)
+
+
+    def generate(self):
+        """
+        Generate the actual SQL 'where ...' expression from the passed in
+        expression tree.
+        
+        @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
+            partial SQL statement, and the C{list} is the list of argument
+            substitutions to use with the SQL API execute method.
+        """
+
+        # Init state
+        self.sout = StringIO.StringIO()
+        self.arguments = []
+        self.substitutions = []
+
+        # Generate ' where ...' partial statement
+        self.sout.write(self.WHERE)
+        self.generateExpression(self.expression)
+
+        # Prefix with ' from ...' partial statement
+        select = self.FROM + self.RESOURCEDB
+        select += self.sout.getvalue()
+
+        select = select % tuple(self.substitutions)
+
+        return select, self.arguments
+
+
+    def addArgument(self, arg):
+        self.arguments.append(arg)
+        self.substitutions.append("%s")
+        self.sout.write("%s")
+
+    def setArgument(self, arg):
+        self.arguments.append(arg)
+        self.substitutions.append("%s")
+
+    def frontArgument(self, arg):
+        self.arguments.insert(0, arg)
+        self.substitutions.insert(0, "%s")
+
+    def containsArgument(self, arg):
+        return "%%%s%%" % (arg,)
+
+
+class PostgresLegacyABIndexEmulator(object):
+    """
+    Emulator for L{twistedcaldv.index.Index} and
+    L{twistedcaldv.index.IndexSchedule}.
+    """
+
+    def __init__(self, addressbook):
+        self.resource = self.addressbook = addressbook
+        if (
+            hasattr(config, "Memcached") and
+            config.Memcached.Pools.Default.ClientEnabled
+        ):
+            self.reserver = MemcachedUIDReserver(self)
+        else:
+            # This is only used with unit tests
+            self.reserver = DummyUIDReserver(self)
+
+
+    @property
+    def _txn(self):
+        return self.addressbook._txn
+
+
+    def reserveUID(self, uid):
+        return self.reserver.reserveUID(uid)
+
+
+    def unreserveUID(self, uid):
+        return self.reserver.unreserveUID(uid)
+
+
+    def isReservedUID(self, uid):
+        return self.reserver.isReservedUID(uid)
+
+
+    def isAllowedUID(self, uid, *names):
+        """
+        Checks to see whether to allow an operation which would add the
+        specified UID to the index.  Specifically, the operation may not
+        violate the constraint that UIDs must be unique.
+        @param uid: the UID to check
+        @param names: the names of resources being replaced or deleted by the
+            operation; UIDs associated with these resources are not checked.
+        @return: True if the UID is not in the index and is not reserved,
+            False otherwise.
+        """
+        rname = self.resourceNameForUID(uid)
+        return (rname is None or rname in names)
+
+
+    def resourceUIDForName(self, name):
+        obj = self.addressbook.addressbookObjectWithName(name)
+        if obj is None:
+            return None
+        return obj.uid()
+
+
+    def resourceNameForUID(self, uid):
+        obj = self.addressbook.addressbookObjectWithUID(uid)
+        if obj is None:
+            return None
+        return obj.name()
+
+
+    def whatchanged(self, revision):
+
+        results = [
+            (name.encode("utf-8"), deleted)
+            for name, deleted in
+            self._txn.execSQL(
+                """select RESOURCE_NAME, DELETED from ADDRESSBOOK_OBJECT_REVISIONS
+                   where REVISION > %s and ADDRESSBOOK_RESOURCE_ID = %s""",
+                [revision, self.addressbook._resourceID],
+            )
+        ]
+        results.sort(key=lambda x:x[1])
+        
+        changed = []
+        deleted = []
+        for name, wasdeleted in results:
+            if name:
+                if wasdeleted:
+                    if revision:
+                        deleted.append(name)
+                else:
+                    changed.append(name)
+            else:
+                raise SyncTokenValidException
+        
+        return changed, deleted,
+
+    def searchValid(self, filter):
+        if isinstance(filter, carddavxml.Filter):
+            qualifiers = addressbookquery.sqladdressbookquery(filter)
+        else:
+            qualifiers = None
+
+        return qualifiers is not None
+
+    def search(self, filter):
+        """
+        Finds resources matching the given qualifiers.
+        @param filter: the L{Filter} for the addressbook-query to execute.
+        @return: an iterable of tuples for each resource matching the
+            given C{qualifiers}. The tuples are C{(name, uid, type)}, where
+            C{name} is the resource name, C{uid} is the resource UID, and
+            C{type} is the resource iCalendar component type.x
+        """
+
+        # Make sure we have a proper Filter element and get the partial SQL statement to use.
+        if isinstance(filter, carddavxml.Filter):
+            qualifiers = addressbookquery.sqladdressbookquery(filter, self.addressbook._resourceID, generator=postgresqladbkgenerator)
+        else:
+            qualifiers = None
+        if qualifiers is not None:
+            rowiter = self._txn.execSQL(
+                "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
+                qualifiers[0],
+                qualifiers[1]
+            )
+        else:
+            rowiter = self._txn.execSQL(
+                "select RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
+                [self.addressbook._resourceID, ],
+            )
+
+        for row in rowiter:
+            yield row
+
+    def indexedSearch(self, filter, useruid='', fbtype=False):
+        """
+        Always raise L{IndexedSearchException}, since these indexes are not
+        fully implemented yet.
+        """
+        raise IndexedSearchException()
+
+
+    def bruteForceSearch(self):
+        return self._txn.execSQL(
+            "select RESOURCE_NAME, VCARD_UID from "
+            "ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
+            [self.addressbook._resourceID]
+        )
+
+
+    def resourcesExist(self, names):
+        return list(set(names).intersection(
+            set(self.addressbook.listAddressbookObjects())))
+
+
+    def resourceExists(self, name):
+        return bool(
+            self._txn.execSQL(
+                "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+                "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
+                [name, self.addressbook._resourceID]
+            )
+        )
+
+
+class PostgresLegacyABInvitesEmulator(object):
+    """
+    Emulator for the implicit interface specified by
+    L{twistedcaldav.sharing.InvitesDatabase}.
+    """
+
+
+    def __init__(self, addressbook):
+        self._addressbook = addressbook
+
+
+    @property
+    def _txn(self):
+        return self._addressbook._txn
+
+
+    def create(self):
+        "No-op, because the index implicitly always exists in the database."
+
+
+    def remove(self):
+        "No-op, because the index implicitly always exists in the database."
+
+
+    def allRecords(self):
+        for row in self._txn.execSQL(
+                """
+                select
+                    INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
+                    ADDRESSBOOK_HOME.OWNER_UID, ADDRESSBOOK_BIND.BIND_MODE,
+                    ADDRESSBOOK_BIND.BIND_STATUS, ADDRESSBOOK_BIND.MESSAGE
+                from
+                    INVITE, ADDRESSBOOK_HOME, ADDRESSBOOK_BIND
+                where
+                    INVITE.RESOURCE_ID = %s and
+                    INVITE.HOME_RESOURCE_ID = 
+                        ADDRESSBOOK_HOME.RESOURCE_ID and
+                    ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_ID =
+                        INVITE.RESOURCE_ID and
+                    ADDRESSBOOK_BIND.ADDRESSBOOK_HOME_RESOURCE_ID =
+                        INVITE.HOME_RESOURCE_ID
+                order by
+                    INVITE.NAME asc
+                """, [self._addressbook._resourceID]):
+            [inviteuid, common_name, userid, ownerUID,
+                bindMode, bindStatus, summary] = row
+            # FIXME: this is really the responsibility of the protocol layer.
+            state = {
+                _BIND_STATUS_INVITED: "NEEDS-ACTION",
+                _BIND_STATUS_ACCEPTED: "ACCEPTED",
+                _BIND_STATUS_DECLINED: "DECLINED",
+                _BIND_STATUS_INVALID: "INVALID",
+            }[bindStatus]
+            access = {
+                _BIND_MODE_READ: "read-only",
+                _BIND_MODE_WRITE: "read-write"
+            }[bindMode]
+            principalURL = "/principals/__uids__/%s/" % (ownerUID,)
+            yield Invite(
+                inviteuid, userid, principalURL, common_name,
+                access, state, summary
+            )
+
+
+    def recordForUserID(self, userid):
+        for record in self.allRecords():
+            if record.userid == userid:
+                return record
+
+
+    def recordForPrincipalURL(self, principalURL):
+        for record in self.allRecords():
+            if record.principalURL == principalURL:
+                return record
+
+
+    def recordForInviteUID(self, inviteUID):
+        for record in self.allRecords():
+            if record.inviteuid == inviteUID:
+                return record
+
+
+    def addOrUpdateRecord(self, record):
+        bindMode = {'read-only': _BIND_MODE_READ,
+                    'read-write': _BIND_MODE_WRITE}[record.access]
+        bindStatus = {
+            "NEEDS-ACTION": _BIND_STATUS_INVITED,
+            "ACCEPTED": _BIND_STATUS_ACCEPTED,
+            "DECLINED": _BIND_STATUS_DECLINED,
+            "INVALID": _BIND_STATUS_INVALID,
+        }[record.state]
+        # principalURL is derived from a directory record's principalURL() so
+        # it will always contain the UID.  The form is '/principals/__uids__/x'
+        # (and may contain a trailing slash).
+        principalUID = record.principalURL.split("/")[3]
+        shareeHome = self._txn.addressbookHomeWithUID(principalUID, create=True)
+        rows = self._txn.execSQL(
+            "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
+            [record.userid]
+        )
+        if rows:
+            [[resourceID, homeResourceID]] = rows
+            # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
+            self._txn.execSQL("""
+                update ADDRESSBOOK_BIND set BIND_MODE = %s,
+                BIND_STATUS = %s, MESSAGE = %s
+                where
+                    ADDRESSBOOK_RESOURCE_ID = %s and
+                    ADDRESSBOOK_HOME_RESOURCE_ID = %s
+            """, [bindMode, bindStatus, record.summary,
+                resourceID, homeResourceID])
+            self._txn.execSQL("""
+                update INVITE set NAME = %s, INVITE_UID = %s
+                where RECIPIENT_ADDRESS = %s
+                """,
+                [record.name, record.inviteuid, record.userid]
+            )
+        else:
+            self._txn.execSQL(
+                """
+                insert into INVITE (
+                    INVITE_UID, NAME,
+                    HOME_RESOURCE_ID, RESOURCE_ID,
+                    RECIPIENT_ADDRESS
+                )
+                values (%s, %s, %s, %s, %s)
+                """,
+                [
+                    record.inviteuid, record.name,
+                    shareeHome._resourceID, self._addressbook._resourceID,
+                    record.userid
+                ])
+            self._txn.execSQL(
+                """
+                insert into ADDRESSBOOK_BIND
+                (
+                    ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID, 
+                    ADDRESSBOOK_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
+                    SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
+                )
+                values (%s, %s, %s, %s, %s, %s, %s, %s)
+                """,
+                [
+                    shareeHome._resourceID,
+                    self._addressbook._resourceID,
+                    None, # this is NULL because it is not bound yet, let's be
+                          # explicit about that.
+                    bindMode,
+                    bindStatus,
+                    False,
+                    False,
+                    record.summary
+                ])
+
+
+    def removeRecordForUserID(self, userid):
+        rec = self.recordForUserID(userid)
+        self.removeRecordForInviteUID(rec.inviteuid)
+
+
+    def removeRecordForPrincipalURL(self, principalURL):
+        raise NotImplementedError("removeRecordForPrincipalURL")
+
+
+    def removeRecordForInviteUID(self, inviteUID):
+        rows = self._txn.execSQL("""
+                select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
+                INVITE_UID = %s
+            """, [inviteUID])
+        if rows:
+            [[homeID, resourceID]] = rows
+            self._txn.execSQL(
+                "delete from ADDRESSBOOK_BIND where "
+                "ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
+                [homeID, resourceID])
+            self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
+                [inviteUID])
+
+
+
+class PostgresLegacyABSharesEmulator(object):
+
+    def __init__(self, home):
+        self._home = home
+
+
+    @property
+    def _txn(self):
+        return self._home._txn
+
+
+    def create(self):
+        pass
+
+
+    def remove(self):
+        pass
+
+
+    def allRecords(self):
+        # This should have been a smart join that got all these columns at
+        # once, but let's not bother to fix it, since the actual query we
+        # _want_ to do (just look for addressbook binds in a particular homes) is
+        # much simpler anyway; we should just do that.
+        shareRows = self._txn.execSQL(
+            """
+            select ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, MESSAGE
+            from ADDRESSBOOK_BIND
+                where ADDRESSBOOK_HOME_RESOURCE_ID = %s and
+                BIND_MODE != %s and
+                ADDRESSBOOK_RESOURCE_NAME is not null
+            """, [self._home._resourceID, _BIND_MODE_OWN])
+        for resourceID, resourceName, summary in shareRows:
+            [[shareuid]] = self._txn.execSQL(
+                """
+                select INVITE_UID
+                from INVITE
+                where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
+                """, [resourceID, self._home._resourceID])
+            sharetype = 'I'
+            [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
+                """
+                select ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME
+                from ADDRESSBOOK_BIND
+                where ADDRESSBOOK_RESOURCE_ID = %s and
+                    BIND_MODE = %s
+                """, [resourceID, _BIND_MODE_OWN]
+                )
+            [[ownerUID]] = self._txn.execSQL(
+                "select OWNER_UID from ADDRESSBOOK_HOME where RESOURCE_ID = %s",
+                [ownerHomeID])
+            hosturl = '/addressbooks/__uids__/%s/%s' % (
+                ownerUID, ownerResourceName
+            )
+            localname = resourceName
+            record = SharedCollectionRecord(
+                shareuid, sharetype, hosturl, localname, summary
+            )
+            yield record
+
+
+    def _search(self, **kw):
+        [[key, value]] = kw.items()
+        for record in self.allRecords():
+            if getattr(record, key) == value:
+                return record
+
+    def recordForLocalName(self, localname):
+        return self._search(localname=localname)
+
+    def recordForShareUID(self, shareUID):
+        return self._search(shareuid=shareUID)
+
+
+    def addOrUpdateRecord(self, record):
+#        print '*** SHARING***: Adding or updating this record:'
+#        import pprint
+#        pprint.pprint(record.__dict__)
+        # record.hosturl -> /addressbooks/__uids__/<uid>/<addressbookname>
+        splithost = record.hosturl.split('/')
+        ownerUID = splithost[3]
+        ownerAddressBookName = splithost[4]
+        ownerHome = self._txn.addressbookHomeWithUID(ownerUID)
+        ownerAddressBook = ownerHome.addressbookWithName(ownerAddressBookName)
+        addressbookResourceID = ownerAddressBook._resourceID
+
+        # There needs to be a bind already, one that corresponds to the
+        # invitation.  The invitation's UID is the same as the share UID.  I
+        # just need to update its 'localname', i.e.
+        # ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_NAME.
+
+        self._txn.execSQL(
+            """
+            update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s
+            where ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s
+            """,
+            [record.localname, self._home._resourceID, addressbookResourceID]
+        )
+
+
+    def removeRecordForLocalName(self, localname):
+        self._txn.execSQL(
+            "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
+            "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+            [localname, self._home._resourceID]
+        )
+
+
+    def removeRecordForShareUID(self, shareUID):
+        pass
+#        c = self._home._cursor()
+#        c.execute(
+#            "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
+#            "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+#            [self._home._resourceID]
+#        )

Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,342 @@
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID   varchar(255) not null unique
+);
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ'),
+  REVISION    integer   default 0,
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+------------------------
+-- Sharing Invitation --
+------------------------
+
+create table INVITE (
+    INVITE_UID         varchar(255) not null,
+    NAME               varchar(255) not null,
+    RECIPIENT_ADDRESS  varchar(255) not null,
+    HOME_RESOURCE_ID   integer      not null,
+    RESOURCE_ID        integer      not null
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID   varchar(255) not null unique
+);
+
+
+create table NOTIFICATION (
+  RESOURCE_ID                   integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  NOTIFICATION_HOME_RESOURCE_ID integer      not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID              varchar(255) not null,
+  XML_TYPE                      varchar      not null,
+  XML_DATA                      varchar      not null,
+  CREATED                       timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                      timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(NOTIFICATION_UID, 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,
+  
+  -- An invitation which hasn't been accepted yet will not yet have a resource
+  -- name, so this field may be null.
+  
+  CALENDAR_RESOURCE_NAME    varchar(255),
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  SEEN_BY_OWNER             boolean      not null,
+  SEEN_BY_SHAREE            boolean      not null,
+  MESSAGE                   text,
+
+  primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
+  unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
+);
+
+-- 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');
+
+-- 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');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+  RESOURCE_ID          integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  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      not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+  ORGANIZER            varchar(255),
+  ORGANIZER_OBJECT     integer      references CALENDAR_OBJECT,
+  RECURRANCE_MAX       date,        -- maximum date that recurrences have been expanded to.
+  CREATED              timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED             timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
+
+  -- 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)
+);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'write');
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+  INSTANCE_ID                 integer        primary key default nextval('INSTANCE_ID_SEQ'),
+  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
+);
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+  ID          integer     primary key,
+  DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown'         );
+insert into FREE_BUSY_TYPE values (1, 'free'            );
+insert into FREE_BUSY_TYPE values (2, 'busy'            );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative'  );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+  TIME_RANGE_INSTANCE_ID      integer      not null references TIME_RANGE on delete cascade,
+  USER_ID                     varchar(255) not null,
+  TRANSPARENT                 boolean      not null
+);
+
+
+----------------
+-- Attachment --
+----------------
+
+create table ATTACHMENT (
+  CALENDAR_OBJECT_RESOURCE_ID integer       not null references CALENDAR_OBJECT on delete cascade,
+  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 unique
+);
+
+
+------------------------------
+-- Calendar Object Revision --
+------------------------------
+
+create sequence CALENDAR_OBJECT_REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR on delete cascade,
+  RESOURCE_NAME        varchar(255) not null,
+  REVISION             integer      not null,
+  DELETED              boolean      not null,
+
+  unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
+);
+
+
+------------------
+-- iTIP Message --
+------------------
+
+create table ITIP_MESSAGE (
+  CALENDAR_RESOURCE_ID integer      not null references CALENDAR,
+  ICALENDAR_TEXT       text         not null,
+  ICALENDAR_UID        varchar(255) not null,
+  MD5                  char(32)     not null,
+  CHANGES              text         not null
+);
+
+
+-----------------------
+-- 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)
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID   varchar(255) not null unique
+);
+
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ'),
+  REVISION    integer   default 0,
+  CREATED     timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED    timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID integer      not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      integer      not null references ADDRESSBOOK on delete cascade,
+
+  -- An invitation which hasn't been accepted yet will not yet have a resource
+  -- name, so this field may be null.
+
+  ADDRESSBOOK_RESOURCE_NAME    varchar(255),
+  BIND_MODE                    integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS                  integer      not null, -- enum CALENDAR_BIND_STATUS
+  SEEN_BY_OWNER                boolean      not null,
+  SEEN_BY_SHAREE               boolean      not null,
+  MESSAGE                      text,                  -- FIXME: xml?
+
+  primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID),
+  unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)
+);
+
+
+create table ADDRESSBOOK_OBJECT (
+  RESOURCE_ID             integer      primary key default nextval('RESOURCE_ID_SEQ'),
+  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
+  RESOURCE_NAME           varchar(255) not null,
+  VCARD_TEXT              text         not null,
+  VCARD_UID               varchar(255) not null,
+  CREATED                 timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+  MODIFIED                timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
+
+  unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
+  unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
+);
+
+------------------------------
+-- AddressBook Object Revision --
+------------------------------
+
+create sequence ADDRESSBOOK_OBJECT_REVISION_SEQ;
+
+
+-------------------------------
+-- AddressBook Object Revisions --
+-------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+  ADDRESSBOOK_RESOURCE_ID integer      not null references ADDRESSBOOK on delete cascade,
+  RESOURCE_NAME           varchar(255) not null,
+  REVISION                integer      not null,
+  DELETED                 boolean      not null,
+
+  unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME)
+);
+
+

Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,132 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+SQL Table definitions.
+"""
+
+CALENDAR_HOME_TABLE = {
+    "name"               : "CALENDAR_HOME",
+    "column_RESOURCE_ID" : "RESOURCE_ID",
+    "column_OWNER_UID"   : "OWNER_UID",
+}
+
+ADDRESSBOOK_HOME_TABLE = {
+    "name"               : "ADDRESSBOOK_HOME",
+    "column_RESOURCE_ID" : "RESOURCE_ID",
+    "column_OWNER_UID"   : "OWNER_UID",
+}
+
+NOTIFICATION_HOME_TABLE = {
+    "name"               : "NOTIFICATION_HOME",
+    "column_RESOURCE_ID" : "RESOURCE_ID",
+    "column_OWNER_UID"   : "OWNER_UID",
+}
+
+CALENDAR_TABLE = {
+    "name"               : "CALENDAR",
+    "column_RESOURCE_ID" : "RESOURCE_ID",
+    "column_REVISION"    : "REVISION",
+    "column_CREATED"     : "CREATED",
+    "column_MODIFIED"    : "MODIFIED",
+    "sequence"           : "CALENDAR_OBJECT_REVISION_SEQ",
+}
+
+ADDRESSBOOK_TABLE = {
+    "name"               : "ADDRESSBOOK",
+    "column_RESOURCE_ID" : "RESOURCE_ID",
+    "column_REVISION"    : "REVISION",
+    "column_CREATED"     : "CREATED",
+    "column_MODIFIED"    : "MODIFIED",
+    "sequence"           : "ADDRESSBOOK_OBJECT_REVISION_SEQ",
+}
+
+CALENDAR_BIND_TABLE = {
+    "name"                    : "CALENDAR_BIND",
+    "column_HOME_RESOURCE_ID" : "CALENDAR_HOME_RESOURCE_ID",
+    "column_RESOURCE_ID"      : "CALENDAR_RESOURCE_ID",
+    "column_RESOURCE_NAME"    : "CALENDAR_RESOURCE_NAME",
+    "column_BIND_MODE"        : "BIND_MODE",
+    "column_BIND_STATUS"      : "BIND_STATUS",
+    "column_SEEN_BY_OWNER"    : "SEEN_BY_OWNER",
+    "column_SEEN_BY_SHAREE"   : "SEEN_BY_SHAREE",
+    "column_MESSAGE"          : "MESSAGE",
+}
+
+ADDRESSBOOK_BIND_TABLE = {
+    "name"                    : "ADDRESSBOOK_BIND",
+    "column_HOME_RESOURCE_ID" : "ADDRESSBOOK_HOME_RESOURCE_ID",
+    "column_RESOURCE_ID"      : "ADDRESSBOOK_RESOURCE_ID",
+    "column_RESOURCE_NAME"    : "ADDRESSBOOK_RESOURCE_NAME",
+    "column_BIND_MODE"        : "BIND_MODE",
+    "column_BIND_STATUS"      : "BIND_STATUS",
+    "column_SEEN_BY_OWNER"    : "SEEN_BY_OWNER",
+    "column_SEEN_BY_SHAREE"   : "SEEN_BY_SHAREE",
+    "column_MESSAGE"          : "MESSAGE",
+}
+
+CALENDAR_OBJECT_REVISIONS_TABLE = {
+    "name"                    : "CALENDAR_OBJECT_REVISIONS",
+    "column_RESOURCE_ID"      : "CALENDAR_RESOURCE_ID",
+    "column_RESOURCE_NAME"    : "RESOURCE_NAME",
+    "column_REVISION"         : "REVISION",
+    "column_DELETED"          : "DELETED",
+}
+
+ADDRESSBOOK_OBJECT_REVISIONS_TABLE = {
+    "name"                    : "ADDRESSBOOK_OBJECT_REVISIONS",
+    "column_RESOURCE_ID"      : "ADDRESSBOOK_RESOURCE_ID",
+    "column_RESOURCE_NAME"    : "RESOURCE_NAME",
+    "column_REVISION"         : "REVISION",
+    "column_DELETED"          : "DELETED",
+}
+
+CALENDAR_OBJECT_TABLE = {
+    "name"                      : "CALENDAR_OBJECT",
+    "column_RESOURCE_ID"        : "RESOURCE_ID",
+    "column_PARENT_RESOURCE_ID" : "CALENDAR_RESOURCE_ID",
+    "column_RESOURCE_NAME"      : "RESOURCE_NAME",
+    "column_TEXT"               : "ICALENDAR_TEXT",
+    "column_UID"                : "ICALENDAR_UID",
+    "column_CREATED"            : "CREATED",
+    "column_MODIFIED"           : "MODIFIED",
+}
+
+ADDRESSBOOK_OBJECT_TABLE = {
+    "name"                      : "ADDRESSBOOK_OBJECT",
+    "column_RESOURCE_ID"        : "RESOURCE_ID",
+    "column_PARENT_RESOURCE_ID" : "ADDRESSBOOK_RESOURCE_ID",
+    "column_RESOURCE_NAME"      : "RESOURCE_NAME",
+    "column_TEXT"               : "VCARD_TEXT",
+    "column_UID"                : "VCARD_UID",
+    "column_CREATED"            : "CREATED",
+    "column_MODIFIED"           : "MODIFIED",
+}
+
+
+# Various constants
+
+_BIND_STATUS_INVITED = 0
+_BIND_STATUS_ACCEPTED = 1
+_BIND_STATUS_DECLINED = 2
+_BIND_STATUS_INVALID = 3
+
+_ATTACHMENTS_MODE_WRITE = 1
+
+_BIND_MODE_OWN = 0
+_BIND_MODE_READ = 1
+_BIND_MODE_WRITE = 2

Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,20 @@
+# -*- test-case-name: txcarddav.addressbookstore.test -*-
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Store tests
+"""

Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,139 @@
+# -*- test-case-name: txcarddav.addressbookstore.test -*-
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Store test utility functions
+"""
+
+import gc
+
+from twext.python.filepath import CachingFilePath
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, succeed
+from twisted.internet.task import deferLater
+from twisted.python import log
+
+from txdav.common.datastore.sql import CommonDataStore, v1_schema
+from txdav.datastore.subpostgres import PostgresService,\
+    DiagnosticConnectionWrapper
+
+def allInstancesOf(cls):
+    for o in gc.get_referrers(cls):
+        if isinstance(o, cls):
+            yield o
+
+
+
+def dumpConnectionStatus():
+    print '+++ ALL CONNECTIONS +++'
+    for connection in allInstancesOf(DiagnosticConnectionWrapper):
+        print connection.label, connection.state
+    print '--- CONNECTIONS END ---'
+
+class SQLStoreBuilder(object):
+    """
+    Test-fixture-builder which can construct a PostgresStore.
+    """
+    sharedService = None
+    currentTestID = None
+
+    SHARED_DB_PATH = "../_test_postgres_db"
+
+    def buildStore(self, testCase, notifierFactory):
+        """
+        Do the necessary work to build a store for a particular test case.
+
+        @return: a L{Deferred} which fires with an L{IDataStore}.
+        """
+        currentTestID = testCase.id()
+        dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+        if self.sharedService is None:
+            ready = Deferred()
+            def getReady(connectionFactory):
+                attachmentRoot = dbRoot.child("attachments")
+                try:
+                    attachmentRoot.createDirectory()
+                except OSError:
+                    pass
+                try:
+                    self.store = CommonDataStore(
+                        lambda label=None: connectionFactory(
+                            label or currentTestID
+                        ),
+                        notifierFactory,
+                        attachmentRoot
+                    )
+                except:
+                    ready.errback()
+                    raise
+                else:
+                    self.cleanDatabase(testCase)
+                    ready.callback(self.store)
+                return self.store
+            self.sharedService = PostgresService(
+                dbRoot, getReady, v1_schema, "caldav", resetSchema=True,
+                testMode=True
+            )
+            self.sharedService.startService()
+            def startStopping():
+                log.msg("Starting stopping.")
+                self.sharedService.unpauseMonitor()
+                return self.sharedService.stopService()
+            reactor.addSystemEventTrigger(#@UndefinedVariable
+                "before", "shutdown", startStopping)
+            result = ready
+        else:
+            self.store.notifierFactory = notifierFactory
+            self.cleanDatabase(testCase)
+            result = succeed(self.store)
+
+        def cleanUp():
+            # FIXME: clean up any leaked connections and report them with an
+            # immediate test failure.
+            def stopit():
+                self.sharedService.pauseMonitor()
+            return deferLater(reactor, 0.1, stopit)
+        testCase.addCleanup(cleanUp)
+        return result
+
+
+    def cleanDatabase(self, testCase):
+        cleanupConn = self.store.connectionFactory(
+            "%s schema-cleanup" % (testCase.id(),)
+        )
+        cursor = cleanupConn.cursor()
+        tables = ['INVITE',
+                  'RESOURCE_PROPERTY',
+                  'ATTACHMENT',
+                  'ADDRESSBOOK_OBJECT',
+                  'CALENDAR_OBJECT',
+                  'CALENDAR_BIND',
+                  'ADDRESSBOOK_BIND',
+                  'CALENDAR',
+                  'ADDRESSBOOK',
+                  'CALENDAR_HOME',
+                  'ADDRESSBOOK_HOME',
+                  'NOTIFICATION',
+                  'NOTIFICATION_HOME']
+        for table in tables:
+            try:
+                cursor.execute("delete from "+table)
+            except:
+                log.err()
+        cleanupConn.commit()
+        cleanupConn.close()

Modified: CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -152,7 +152,7 @@
     An notification object describes an XML notification.
     """
 
-    def setData(uid, xmltype, xmldata):
+    def setData(uid, xmltype, xmldata, inserting=False):
         """
         Rewrite this notification object to match the given C{xmltype} and
         C{xmldata}. C{xmldata} must have the same UID as this notification object.

Modified: CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py	2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -46,35 +46,6 @@
     return path.sibling('.' + path.basename())
 
 
-_unset = object()
-
-class cached(object):
-    """
-    This object is a decorator for a 0-argument method which should be called
-    only once, and its result cached so that future invocations just return the
-    same result without calling the underlying method again.
-
-    @ivar thunk: the function to call to generate a cached value.
-    """
-
-    def __init__(self, thunk):
-        self.thunk = thunk
-
-
-    def __get__(self, oself, owner):
-        def inner():
-            cacheKey = "_" + self.thunk.__name__ + "_cached"
-            cached = getattr(oself, cacheKey, _unset)
-            if cached is _unset:
-                value = self.thunk(oself)
-                setattr(oself, cacheKey, value)
-                return value
-            else:
-                return cached
-        return inner
-
-
-
 def writeOperation(thunk):
     # FIXME: tests
     def inner(self, *a, **kw):

Added: CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py	                        (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py	2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,47 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_file -*-
+##
+# Copyright (c) 2010 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.
+##
+
+"""
+Common utility functions for a datastores.
+"""
+
+_unset = object()
+
+class cached(object):
+    """
+    This object is a decorator for a 0-argument method which should be called
+    only once, and its result cached so that future invocations just return the
+    same result without calling the underlying method again.
+
+    @ivar thunk: the function to call to generate a cached value.
+    """
+
+    def __init__(self, thunk):
+        self.thunk = thunk
+
+
+    def __get__(self, oself, owner):
+        def inner():
+            cacheKey = "_" + self.thunk.__name__ + "_cached"
+            cached = getattr(oself, cacheKey, _unset)
+            if cached is _unset:
+                value = self.thunk(oself)
+                setattr(oself, cacheKey, value)
+                return value
+            else:
+                return cached
+        return inner
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100825/1c6664f5/attachment-0001.html>


More information about the calendarserver-changes mailing list