[CalendarServer-changes] [6074] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Aug 11 19:36:29 PDT 2010


Revision: 6074
          http://trac.macosforge.org/projects/calendarserver/changeset/6074
Author:   glyph at apple.com
Date:     2010-08-11 19:36:27 -0700 (Wed, 11 Aug 2010)
Log Message:
-----------
merge sql-store branch

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/support/build.sh
    CalendarServer/trunk/twext/web2/dav/test/util.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
    CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
    CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py
    CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py
    CalendarServer/trunk/twistedcaldav/test/test_multiget.py
    CalendarServer/trunk/twistedcaldav/test/test_sharing.py
    CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
    CalendarServer/trunk/twistedcaldav/test/util.py
    CalendarServer/trunk/txcaldav/calendarstore/file.py
    CalendarServer/trunk/txcaldav/calendarstore/postgres.py
    CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql
    CalendarServer/trunk/txcaldav/calendarstore/scheduling.py
    CalendarServer/trunk/txcaldav/calendarstore/test/common.py
    CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py
    CalendarServer/trunk/txcaldav/icalendarstore.py
    CalendarServer/trunk/txcarddav/addressbookstore/file.py
    CalendarServer/trunk/txcarddav/addressbookstore/test/common.py
    CalendarServer/trunk/txcarddav/iaddressbookstore.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/inotifications.py
    CalendarServer/trunk/txdav/idav.py

Added Paths:
-----------
    CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py
    CalendarServer/trunk/txcaldav/calendarstore/util.py
    CalendarServer/trunk/txdav/datastore/subpostgres.py
    CalendarServer/trunk/txdav/datastore/test/
    CalendarServer/trunk/txdav/datastore/test/__init__.py
    CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py

Removed Paths:
-------------
    CalendarServer/trunk/txdav/datastore/test/__init__.py
    CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -28,7 +28,7 @@
 from time import time
 
 from subprocess import Popen, PIPE
-from pwd import getpwuid
+from pwd import getpwuid, getpwnam
 from grp import getgrnam
 from OpenSSL.SSL import Error as SSLError
 import OpenSSL
@@ -88,6 +88,10 @@
 from calendarserver.tap.util import getRootResource, computeProcessCount
 from calendarserver.tools.util import checkDirectory
 
+from txcaldav.calendarstore.postgres import v1_schema
+from txdav.datastore.subpostgres import PostgresService
+from twext.python.filepath import CachingFilePath
+
 log = Logger()
 
 
@@ -681,6 +685,7 @@
         logger = AMPLoggingFactory(
             RotatingFileAccessLoggingObserver(config.AccessLogFile)
         )
+
         if config.GroupName:
             try:
                 gid = getgrnam(config.GroupName).gr_gid
@@ -688,6 +693,15 @@
                 raise ConfigurationError("Invalid group name: %s" % (config.GroupName,))
         else:
             gid = os.getgid()
+
+        if config.UserName:
+            try:
+                uid = getpwnam(config.UserName).pw_uid
+            except KeyError, e:
+                raise ConfigurationError("Invalid user name: %s" % (config.UserName,))
+        else:
+            uid = os.getuid()
+
         if config.ControlSocket:
             loggingService = GroupOwnedUNIXServer(
                 gid, config.ControlSocket, logger, mode=0660
@@ -700,9 +714,30 @@
         loggingService.setServiceParent(s)
 
         monitor = DelayedStartupProcessMonitor()
-        monitor.setServiceParent(s)
         s.processMonitor = monitor
 
+        if config.UseDatabase:
+            # Postgres: delay spawning child processes until database is up
+
+            dbRoot = CachingFilePath(config.DatabaseRoot)
+
+            def subServiceFactory(connectionFactory):
+                return monitor
+
+            if os.getuid() == 0: # Only override if root
+                postgresUID = uid
+                postgresGID = gid
+            else:
+                postgresUID = None
+                postgresGID = None
+
+            PostgresService(dbRoot, subServiceFactory, v1_schema,
+                "caldav", logFile=config.PostgresLogFile,
+                uid=postgresUID, gid=postgresGID).setServiceParent(s)
+
+        else:
+            monitor.setServiceParent(s)
+
         parentEnv = {
             "PATH": os.environ.get("PATH", ""),
             "PYTHONPATH": os.environ.get("PYTHONPATH", ""),

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -68,7 +68,11 @@
 from calendarserver.webcal.resource import WebCalendarResource
 
 from txdav.common.datastore.file import CommonDataStore
+from txcaldav.calendarstore.postgres import PostgresStore, v1_schema
+from txdav.datastore.subpostgres import PostgresService
+from twext.python.filepath import CachingFilePath
 
+
 log = Logger()
 
 
@@ -286,11 +290,17 @@
     else:
         notifierFactory = None
 
+    if config.UseDatabase:
+        _dbRoot = CachingFilePath(config.DatabaseRoot)
+        _postgresService = PostgresService(_dbRoot, None, v1_schema, "caldav",
+            logFile=config.PostgresLogFile)
+        _newStore = PostgresStore(_postgresService.produceConnection,
+            notifierFactory, # config.EnableCalDAV, config.EnableCardDAV)
+            _dbRoot.child("attachments"))
+    else:
+        _newStore = CommonDataStore(FilePath(config.DocumentRoot),
+            notifierFactory, config.EnableCalDAV, config.EnableCardDAV) 
 
-    # Need a data store
-    _newStore = CommonDataStore(FilePath(config.DocumentRoot),
-        notifierFactory, config.EnableCalDAV, config.EnableCardDAV)
-
     if config.EnableCalDAV:
         log.info("Setting up calendar collection: %r" % (calendarResourceClass,))
         calendarCollection = calendarResourceClass(

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2010-08-12 02:36:27 UTC (rev 6074)
@@ -82,6 +82,10 @@
     <key>DataRoot</key>
     <string>Data</string>
 
+    <!-- Database root -->
+    <key>DatabaseRoot</key>
+    <string>Database</string>
+
     <!-- Document root -->
     <key>DocumentRoot</key>
     <string>Documents</string>

Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/support/build.sh	2010-08-12 02:36:27 UTC (rev 6074)
@@ -196,17 +196,17 @@
       cache_file="${cache_deps}/${name}-$(echo "${url}" | hash)-$(basename "${url}")";
 
       if [ ! -f "${cache_file}" ]; then
-	echo "Downloading ${name}...";
-	curl -L "${url}" -o "${cache_file}";
+        echo "Downloading ${name}...";
+        curl -L "${url}" -o "${cache_file}";
       fi;
 
       if [ -n "${md5}" ] && [ "${hash}" == "md5" ]; then
-	echo "Checking MD5 sum for ${name}...";
-	local sum="$(hash "${cache_file}" | perl -pe 's|^.*([0-9a-f]{32}).*$|\1|')";
-	if [ "${md5}" != "${sum}" ]; then
-	  echo "ERROR: MD5 sum for cache file ${cache_file} ${sum} != ${md5}. Corrupt file?";
-	  exit 1;
-	fi;
+        echo "Checking MD5 sum for ${name}...";
+        local sum="$(hash "${cache_file}" | perl -pe 's|^.*([0-9a-f]{32}).*$|\1|')";
+        if [ "${md5}" != "${sum}" ]; then
+          echo "ERROR: MD5 sum for cache file ${cache_file} ${sum} != ${md5}. Corrupt file?";
+          exit 1;
+        fi;
       fi;
 
       echo "Unpacking ${name} from cache...";
@@ -287,13 +287,13 @@
       mkdir -p "${cache_deps}";
 
       if [ -f "${cache_file}" ]; then
-	echo "Unpacking ${name} from cache...";
-	mkdir -p "${path}";
-	tar -C "${path}" -xvzf "${cache_file}";
+        echo "Unpacking ${name} from cache...";
+        mkdir -p "${path}";
+        tar -C "${path}" -xvzf "${cache_file}";
       else
-	checkout;
-	echo "Caching ${name}...";
-	tar -C "${path}" -cvzf "${cache_file}" .;
+        checkout;
+        echo "Caching ${name}...";
+        tar -C "${path}" -cvzf "${cache_file}" .;
       fi;
     else
       checkout;

Modified: CalendarServer/trunk/twext/web2/dav/test/util.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/util.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twext/web2/dav/test/util.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -200,7 +200,8 @@
 
         return (os.path.abspath(path), uri)
 
-    def send(self, request, callback):
+
+    def send(self, request, callback=None):
         log.msg("Sending %s request for URI %s" % (request.method, request.uri))
 
         d = request.locateResource(request.uri)

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -2257,7 +2257,7 @@
 
     def createNotificationsCollection(self):
         
-        txn = self._newStoreHome._transaction
+        txn = self._associatedTransaction
         notifications = txn.notificationsWithUID(self._newStoreHome.uid())
 
         from twistedcaldav.storebridge import StoreNotificationCollectionResource

Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -60,13 +60,17 @@
         return self._invitesDB
 
     def inviteProperty(self, request):
-        
-        # Build the CS:invite property from our DB
+        """
+        Calculate the customxml.Invite property (for readProperty) from the
+        invites database.
+        """
+
         def sharedOK(isShared):
             if config.Sharing.Enabled and isShared:
                 self.validateInvites()
                 return customxml.Invite(
-                    *[record.makePropertyElement() for record in self.invitesDB().allRecords()]
+                    *[record.makePropertyElement() for
+                        record in self.invitesDB().allRecords()]
                 )
             else:
                 return None
@@ -339,8 +343,9 @@
 
     def validUserIDForShare(self, userid):
         """
-        Test the user id to see if it is a valid identifier for sharing and return a "normalized"
-        form for our own use (e.g. convert mailto: to urn:uuid).
+        Test the user id to see if it is a valid identifier for sharing and
+        return a "normalized" form for our own use (e.g. convert mailto: to
+        urn:uuid).
 
         @param userid: the userid to test
         @type userid: C{str}
@@ -526,8 +531,10 @@
         # Locate notifications collection for user
         sharee = self.principalForCalendarUserAddress(record.userid)
         if sharee is None:
-            raise ValueError("sharee is None but userid was valid before")        
-        notifications = self._newStoreParentHome._transaction.notificationsWithUID(sharee.principalUID())
+            raise ValueError("sharee is None but userid was valid before")
+        notifications = self._associatedTransaction.notificationsWithUID(
+            sharee.principalUID()
+        )
         
         # Look for existing notification
         oldnotification = notifications.notificationObjectWithUID(record.inviteuid)
@@ -576,7 +583,7 @@
         def _handleErrorResponse(error):
             if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
                 return error.value.response
-            return Response(code=responsecode.BAD_REQUEST)
+            return error
 
         def _handleInvite(invitedoc):
             def _handleInviteSet(inviteset):
@@ -935,8 +942,8 @@
 
 class SharedHomeMixin(LinkFollowerMixIn):
     """
-    A mix-in for calendar/addressbook homes that defines the operations for manipulating a sharee's
-    set of shared calendars.
+    A mix-in for calendar/addressbook homes that defines the operations for
+    manipulating a sharee's set of shared calendars.
     """
     
     def sharesDB(self):
@@ -1053,7 +1060,9 @@
 
     @inlineCallbacks
     def _changeShare(self, request, state, hostUrl, replytoUID, displayname=None):
-        """ Accept an invite to a shared collection """
+        """
+        Accept or decline an invite to a shared collection.
+        """
         
         # Change state in sharer invite
         owner = (yield self.ownerPrincipal(request))
@@ -1074,7 +1083,6 @@
 
     @inlineCallbacks
     def sendReply(self, request, sharee, sharedCollection, state, hostUrl, replytoUID, displayname=None):
-        
 
         # Locate notifications collection for sharer
         sharer = (yield sharedCollection.ownerPrincipal(request))
@@ -1107,7 +1115,7 @@
         def _handleErrorResponse(error):
             if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
                 return error.value.response
-            return Response(code=responsecode.BAD_REQUEST)
+            return error
 
         def _handleInviteReply(invitereplydoc):
             """ Handle a user accepting or declining a sharing invite """

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -149,6 +149,8 @@
     "UseMetaFD": True,         # Use a 'meta' FD, i.e. an FD to transmit other
                                # FDs to slave processes.
 
+    "UseDatabase" : True,      # True: postgress; False: files
+
     #
     # Types of service provided
     #
@@ -160,6 +162,7 @@
     #
     "ServerRoot"              : "/Library/CalendarServer",
     "DataRoot"                : "Data",
+    "DatabaseRoot"            : "Database",
     "DocumentRoot"            : "Documents",
     "ConfigRoot"              : "/etc/caldavd",
     "LogRoot"                 : "/var/log/caldavd",
@@ -266,6 +269,7 @@
     "ErrorLogEnabled"   : True,       # True = use log file, False = stdout
     "ErrorLogRotateMB"  : 10,         # Rotate error log after so many megabytes
     "ErrorLogMaxRotatedFiles"  : 5,   # Retain this many error log files
+    "PostgresLogFile" : "postgres.log",  # Postgres log
     "PIDFile"        : "caldavd.pid",
     "RotateAccessLog"   : False,
     "EnableExtendedAccessLog": True,
@@ -627,9 +631,11 @@
                   ("ServerRoot", "ConfigRoot"),
                   ("ServerRoot", "LogRoot"),
                   ("ServerRoot", "RunRoot"),
+                  ("DataRoot", "DatabaseRoot"),
                   ("ConfigRoot", "SudoersFile"),
                   ("LogRoot", "AccessLogFile"),
                   ("LogRoot", "ErrorLogFile"),
+                  ("LogRoot", "PostgresLogFile"),
                   ("LogRoot", "AccountingLogRoot"),
                   ("RunRoot", "PIDFile"),
                   ("RunRoot", "GlobalStatsSocket"),

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -660,6 +660,12 @@
         self._initializeWithCalendar(calendar, home)
 
 
+    def __repr__(self):
+        return "<Calendar Collection Resource %r:%r>" % (
+            self._newStoreCalendar.ownerCalendarHome().uid(),
+            self._newStoreCalendar.name())
+
+
     def name(self):
         return self._newStoreCalendar.name()
 
@@ -944,16 +950,18 @@
         the given request's resource-lookup mapping, transaction, and re-look-
         up my calendar object in a new transaction.
 
-        Return the new transaction so it can be committed.
+        @return: the new transaction so it can be committed.
         """
         # FIXME: private names from 'file' implementation; maybe there should
         # be a public way to do this?  or maybe we should just have a real
         # queue.
         objectName = self._newStoreObject.name()
-        calendarName = self._newStoreObject._calendar.name()
-        homeUID = self._newStoreObject._calendar._calendarHome.uid()
-        store = self._newStoreObject._transaction.store()
-        txn = store.newTransaction("new transaction for " + self._newStoreObject.name())
+        calendar = self._newStoreObject.calendar()
+        calendarName = calendar.name()
+        ownerHome = calendar.ownerCalendarHome()
+        homeUID = ownerHome.uid()
+        txn = ownerHome.transaction().store().newTransaction(
+            "new transaction for " + self._newStoreObject.name())
         newObject = (txn.calendarHomeWithUID(homeUID)
                         .calendarWithName(calendarName)
                         .calendarObjectWithName(objectName))
@@ -1551,34 +1559,10 @@
         self._initializeWithObject(Object)
 
 
-    def inNewTransaction(self, request):
-        """
-        Implicit auto-replies need to span multiple transactions.  Clean out the
-        given request's resource-lookup mapping, transaction, and re-look-up my
-        addressbook object in a new transaction.
-
-        Return the new transaction so it can be committed.
-        """
-        # FIXME: private names from 'file' implementation; maybe there should be
-        # a public way to do this?  or maybe we should just have a real queue.
-        objectName = self._newStoreObject.name()
-        Name = self._newStoreObject._addressbook.name()
-        homeUID = self._newStoreObject._addressbook._addressbookHome.uid()
-        store = self._newStoreObject._transaction.store()
-        txn = store.newTransaction("new AB transaction for " + self._newStoreObject.name())
-        newObject = (txn.addressbookHomeWithUID(homeUID)
-                        .addressbookWithName(Name)
-                        .addressbookObjectWithName(objectName))
-        request._newStoreTransaction = txn
-        request._resourcesByURL.clear()
-        request._urlsByResource.clear()
-        self._initializeWithObject(newObject)
-        return txn
-
-
     def isCollection(self):
         return False
 
+
     def exists(self):
         # FIXME: Tests
         return True

Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -15,24 +15,59 @@
 ##
 
 import os
-import shutil
 
 from twisted.trial.unittest import SkipTest
+
+from twext.python.filepath import CachingFilePath as FilePath
+
 from twext.web2 import responsecode
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
 from twext.web2.dav import davxml
-from twext.web2.dav.fileop import rmdir
 from twext.web2.dav.util import davXMLFromStream
 from twext.web2.test.test_server import SimpleRequest
 
 from twistedcaldav import caldavxml
 from twistedcaldav import ical
-from twistedcaldav.index import db_basename
+
 from twistedcaldav.query import calendarqueryfilter
 from twistedcaldav.config import config
 from twistedcaldav.test.util import HomeTestCase
+from twisted.internet.defer import inlineCallbacks, returnValue
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory
 
+
+ at inlineCallbacks
+def addEventsDir(testCase, eventsDir, uri):
+    """
+    Add events to a L{HomeTestCase} from a directory.
+
+    @param testCase: The test case to add events to.
+    @type testCase: L{HomeTestCase}
+
+    @param eventsDir: A directory full of events.
+    @type eventsDir: L{FilePath}
+
+    @param uri: The URI-path of the calendar to insert events into.
+    @type uri: C{str}
+
+    @return: a L{Deferred} which fires with the number of added calendar object
+        resources.
+    """
+    count = 0
+    for child in eventsDir.children():
+        count += 1
+        if child.basename().split(".")[-1] != "ics":
+            continue
+        request = SimpleRequest(testCase.site, "PUT",
+                                uri + "/" + child.basename())
+        request.stream = MemoryStream(child.getContent())
+        yield testCase.send(request)
+    returnValue(count)
+
+
+
 class CalendarQuery (HomeTestCase):
     """
     calendar-query REPORT
@@ -180,7 +215,8 @@
         All events.
         (CalDAV-access-09, section 7.6.8)
         """
-        uids = [r[0] for r in (os.path.splitext(f) for f in os.listdir(self.holidays_dir)) if r[1] == ".ics"]
+        uids = [r[0] for r in (os.path.splitext(f) for f in
+                os.listdir(self.holidays_dir)) if r[1] == ".ics"]
 
         return self.simple_event_query("/calendar_query_events/", None, uids)
 
@@ -288,41 +324,42 @@
 
         return self.calendar_query(cal_uri, query, got_xml)
 
+
+    @inlineCallbacks
     def calendar_query(self, calendar_uri, query, got_xml):
-        calendar_path = os.path.join(self.docroot, calendar_uri[1:])
 
-        if os.path.exists(calendar_path): rmdir(calendar_path)
+        response = yield self.send(SimpleRequest(self.site, "MKCALENDAR", calendar_uri))
+        response = IResponse(response)
 
-        mkrequest = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
+        if response.code != responsecode.CREATED:
+            self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-        def do_report(response):
-            response = IResponse(response)
+        # Add holiday events to calendar
+        yield addEventsDir(self, FilePath(self.holidays_dir), calendar_uri)
 
-            if response.code != responsecode.CREATED:
-                self.fail("MKCALENDAR failed: %s" % (response.code,))
+        request = SimpleRequest(self.site, "REPORT", calendar_uri)
+        request.stream = MemoryStream(query.toxml())
+        response = yield self.send(request)
 
-            # Add holiday events to calendar
-            # We're cheating by simply copying the files in
-            for filename in os.listdir(self.holidays_dir):
-                if os.path.splitext(filename)[1] != ".ics": continue
-                path = os.path.join(self.holidays_dir, filename)
-                shutil.copy(path, calendar_path)
+        response = IResponse(response)
 
-            # Delete the index because we cheated
-            index_path = os.path.join(calendar_path, db_basename)
-            if os.path.isfile(index_path): os.remove(index_path)
+        if response.code != responsecode.MULTI_STATUS:
+            self.fail("REPORT failed: %s" % (response.code,))
 
-            request = SimpleRequest(self.site, "REPORT", calendar_uri)
-            request.stream = MemoryStream(query.toxml())
+        returnValue(
+            (yield davXMLFromStream(response.stream).addCallback(got_xml))
+        )
 
-            def do_test(response):
-                response = IResponse(response)
 
-                if response.code != responsecode.MULTI_STATUS:
-                    self.fail("REPORT failed: %s" % (response.code,))
+class DatabaseQueryTests(CalendarQuery):
 
-                return davXMLFromStream(response.stream).addCallback(got_xml)
+    @inlineCallbacks
+    def setUp(self):
+        self.calendarStore = yield buildStore(self, StubNotifierFactory())
+        yield super(DatabaseQueryTests, self).setUp()
 
-            return self.send(request, do_test)
 
-        return self.send(mkrequest, do_report)
+    def createDataStore(self):
+        return self.calendarStore
+
+

Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -14,7 +14,7 @@
 # limitations under the License.
 ##
 
-from twisted.internet.defer import DeferredList
+from twisted.internet.defer import inlineCallbacks
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.web2 import responsecode
 from twext.web2.iweb import IResponse
@@ -58,16 +58,14 @@
         # Tests in this suite assume that the root resource is a calendar home.
         # FIXME: there should be a centralized way of saying 'make this look
         # like a calendar home'
-        super(CollectionContents, self).setUp()
+        return super(CollectionContents, self).setUp()
 
 
     def test_collection_in_calendar(self):
         """
         Make (regular) collection in calendar
         """
-        calendar_path, calendar_uri = self.mkdtemp("collection_in_calendar")
-        calPath = FilePath(calendar_path)
-        calPath.remove()
+        calendar_uri = "/collection_in_calendar/"
 
         def mkcalendar_cb(response):
             response = IResponse(response)
@@ -166,40 +164,33 @@
         with the data from given stream and verifies that the response code from the
         PUT request matches the given response_code.
         """
-        calendar_path, calendar_uri = self.mkdtemp("calendar")
-        calPath = FilePath(calendar_path)
-        calPath.remove()
+        calendar_uri = "/testing_calendar/"
 
+
+        @inlineCallbacks
         def mkcalendar_cb(response):
             response = IResponse(response)
 
             if response.code != responsecode.CREATED:
                 self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-            if not calPath.isdir():
-                self.fail("MKCALENDAR did not create a collection")
-
-            ds = []
             c = 0
 
             for stream, response_code in work:
-                def put_cb(response, stream=stream, response_code=response_code):
-                    response = IResponse(response)
 
-                    if response.code != response_code:
-                        self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
-
                 dst_uri = "/".join([calendar_uri, "dst%d.ics" % (c,)])
                 request = SimpleRequest(self.site, "PUT", dst_uri)
                 request.headers.setHeader("if-none-match", "*")
                 request.headers.setHeader("content-type", MimeType("text", "calendar"))
                 request.stream = stream
-                ds.append(self.send(request, put_cb))
+                response = yield self.send(request)
+                response = IResponse(response)
 
+                if response.code != response_code:
+                    self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
+
                 c += 1
 
-            return DeferredList(ds)
-
         request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
         return self.send(request, mkcalendar_cb)
 
@@ -208,9 +199,7 @@
         """
         Make (regular) collection in calendar
         """
-        calendar_path, calendar_uri = self.mkdtemp("dot_file_in_calendar")
-        calPath = FilePath(calendar_path)
-        calPath.remove()
+        calendar_uri = "/dot_file_in_calendar/"
 
         def mkcalendar_cb(response):
             response = IResponse(response)
@@ -236,7 +225,7 @@
             request = SimpleRequest(self.site, "PUT", event_uri)
             request.headers.setHeader("content-type", MimeType("text", "calendar"))
             request.stream = MemoryStream(calendar)
-            self.send(request, put_cb)
+            return self.send(request, put_cb)
 
         request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
         return self.send(request, mkcalendar_cb)

Modified: CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_freebusyquery.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -18,18 +18,21 @@
 from twisted.trial.unittest import SkipTest
 
 import os
-import shutil
 
+from twext.python.filepath import CachingFilePath as FilePath
+
 from twext.web2 import responsecode
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
-from twext.web2.dav.fileop import rmdir
+
 from twext.web2.test.test_server import SimpleRequest
 
 import twistedcaldav.test.util
 from twistedcaldav import caldavxml
-from twistedcaldav.index import db_basename
 
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.test.test_calendarquery import addEventsDir
+
 class FreeBusyQuery (twistedcaldav.test.util.TestCase):
     """
     free-busy-query REPORT
@@ -73,47 +76,35 @@
         )
 
         query = caldavxml.FreeBusyQuery(query_timerange,)
-        
+
         def got_calendar(calendar):
             pass
 
         return self.free_busy_query(cal_uri, query, got_calendar)
 
+
+    @inlineCallbacks
     def free_busy_query(self, calendar_uri, query, got_calendar):
-        calendar_path = os.path.join(self.docroot, calendar_uri[1:])
 
-        if os.path.exists(calendar_path): rmdir(calendar_path)
+        request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
+        response = yield self.send(request)
+        response = IResponse(response)
 
-        def do_report(response):
-            response = IResponse(response)
+        if response.code != responsecode.CREATED:
+            self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-            if response.code != responsecode.CREATED:
-                self.fail("MKCALENDAR failed: %s" % (response.code,))
+        yield addEventsDir(self, FilePath(self.holidays_dir), calendar_uri)
 
-            # Add holiday events to calendar
-            # We're cheating by simply copying the files in
-            for filename in os.listdir(self.holidays_dir):
-                if os.path.splitext(filename)[1] != ".ics": continue
-                path = os.path.join(self.holidays_dir, filename)
-                shutil.copy(path, calendar_path)
+        request = SimpleRequest(self.site, "REPORT", calendar_uri)
+        request.stream = MemoryStream(query.toxml())
+        response = yield self.send(request)
+        response = IResponse(response)
 
-            # Delete the index because we cheated
-            index_path = os.path.join(calendar_path, db_basename)
-            if os.path.isfile(index_path): os.remove(index_path)
+        if response.code != responsecode.OK:
+            self.fail("REPORT failed: %s" % (response.code,))
 
-            request = SimpleRequest(self.site, "REPORT", calendar_uri)
-            request.stream = MemoryStream(query.toxml())
+        result = yield Component.fromIStream(response.stream).addCallback(
+            got_calendar
+        )
+        returnValue(result)
 
-            def do_test(response):
-                response = IResponse(response)
-
-                if response.code != responsecode.OK:
-                    self.fail("REPORT failed: %s" % (response.code,))
-
-                return Component.fromIStream(response.stream).addCallback(got_calendar)
-
-            return self.send(request, do_test, calendar_path)
-
-        request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
-
-        return self.send(request, do_report, calendar_path)

Modified: CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_mkcalendar.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -26,7 +26,7 @@
 from twext.web2.test.test_server import SimpleRequest
 
 from twistedcaldav import caldavxml
-from twistedcaldav.test.util import HomeTestCase, todo
+from twistedcaldav.test.util import HomeTestCase
 
 class MKCALENDAR (HomeTestCase):
     """
@@ -150,47 +150,7 @@
         request.stream = MemoryStream(mk.toxml())
         return self.send(request, do_test)
 
-    @todo("Remove: Does not make sense with new store")
-    def test_make_calendar_no_parent(self):
-        """
-        Make calendar with no parent
-        """
-        uri  = "/no/parent/for/calendar/"
 
-        def do_test(response):
-            response = IResponse(response)
-
-            if response.code != responsecode.CONFLICT:
-                self.fail("Incorrect response to MKCALENDAR with no parent: %s" % (response.code,))
-
-            # FIXME: Check for CalDAV:calendar-collection-location-ok element
-
-        request = SimpleRequest(self.site, "MKCALENDAR", uri)
-        return self.send(request, do_test)
-
-    @todo("Remove: Does not make sense with new store")
-    def test_make_calendar_on_resource(self):
-        """
-        Make calendar on existing resource
-        """
-        uri  = "/calendar_on_resource/"
-        path = os.path.join(self.docroot, uri[1:])
-
-        if not os.path.exists(path):
-            f = open(path[:-1], 'w')
-            f.close()
-
-        def do_test(response):
-            response = IResponse(response)
-
-            if response.code != responsecode.NOT_ALLOWED:
-                self.fail("Incorrect response to MKCALENDAR on existing resource: %s" % (response.code,))
-
-            # FIXME: Check for DAV:resource-must-be-null element
-
-        request = SimpleRequest(self.site, "MKCALENDAR", uri)
-        return self.send(request, do_test)
-
     def test_make_calendar_on_collection(self):
         """
         Make calendar on existing collection

Modified: CalendarServer/trunk/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_multiget.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_multiget.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,4 +1,3 @@
-##
 # Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,20 +14,18 @@
 ##
 
 import os
-import shutil
 
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.python.filepath import CachingFilePath as FilePath
 from twext.web2 import responsecode
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
 from twext.web2.dav import davxml
-from twext.web2.dav.fileop import rmdir
 from twext.web2.dav.util import davXMLFromStream
 from twext.web2.test.test_server import SimpleRequest
 
 from twistedcaldav import caldavxml
 from twistedcaldav import ical
-from twistedcaldav.index import db_basename
 from twistedcaldav.test.util import HomeTestCase, todo
 from twistedcaldav.config import config
 
@@ -247,52 +244,40 @@
 
         return self.calendar_query(cal_uri, query, got_xml, data, no_init)
 
+    @inlineCallbacks
     def calendar_query(self, calendar_uri, query, got_xml, data, no_init):
-        calendar_path = os.path.join(self.docroot, calendar_uri[1:])
 
-        if not no_init and os.path.exists(calendar_path): rmdir(calendar_path)
+        if not no_init:
+            response = yield self.send(SimpleRequest(self.site, "MKCALENDAR",
+                calendar_uri))
+            response = IResponse(response)
+            if response.code != responsecode.CREATED:
+                self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-        def do_report(response):
-            if not no_init:
-                response = IResponse(response)
-    
-                if response.code != responsecode.CREATED:
-                    self.fail("MKCALENDAR failed: %s" % (response.code,))
-    
-                if data:
-                    for filename, icaldata in data.iteritems():
-                        path = os.path.join(calendar_path, filename + ".ics")
-                        f = open(path, "w")
-                        f.write(icaldata)
-                        f.close()
-                else:
-                    # Add holiday events to calendar
-                    # We're cheating by simply copying the files in
-                    for filename in os.listdir(self.holidays_dir):
-                        if os.path.splitext(filename)[1] != ".ics": continue
-                        path = os.path.join(self.holidays_dir, filename)
-                        shutil.copy(path, calendar_path)
-    
-                # Delete the index because we cheated
-                index_path = os.path.join(calendar_path, db_basename)
-                if os.path.isfile(index_path): os.remove(index_path)
+            if data:
+                for filename, icaldata in data.iteritems():
+                    request = SimpleRequest(self.site, "PUT",
+                        calendar_uri + "/" + filename + ".ics")
+                    request.stream = MemoryStream(icaldata)
+                    yield self.send(request)
+            else:
+                # Add holiday events to calendar
+                for child in FilePath(self.holidays_dir).children():
+                    if os.path.splitext(child.basename())[1] != ".ics": continue
+                    request = SimpleRequest(self.site, "PUT",
+                        calendar_uri + "/" + child.basename())
+                    request.stream = MemoryStream(child.getContent())
+                    yield self.send(request)
 
-            request = SimpleRequest(self.site, "REPORT", calendar_uri)
-            request.stream = MemoryStream(query.toxml())
+        request = SimpleRequest(self.site, "REPORT", calendar_uri)
+        request.stream = MemoryStream(query.toxml())
+        response = yield self.send(request)
 
-            def do_test(response):
-                response = IResponse(response)
+        response = IResponse(response)
 
-                if response.code != responsecode.MULTI_STATUS:
-                    self.fail("REPORT failed: %s" % (response.code,))
+        if response.code != responsecode.MULTI_STATUS:
+            self.fail("REPORT failed: %s" % (response.code,))
 
-                return davXMLFromStream(response.stream).addCallback(got_xml)
-
-            return self.send(request, do_test)
-
-        if no_init:
-            return do_report(None)
-        else:
-            request = SimpleRequest(self.site, "MKCALENDAR", calendar_uri)
-    
-            return self.send(request, do_report)
+        returnValue(
+            (yield davXMLFromStream(response.stream).addCallback(got_xml))
+        )

Modified: CalendarServer/trunk/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -25,6 +25,8 @@
 from twistedcaldav.config import config
 from twistedcaldav.test.util import HomeTestCase, norequest
 from twistedcaldav.resource import CalDAVResource
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory
 
 
 class SharingTests(HomeTestCase):
@@ -54,8 +56,8 @@
         
         def displayName(self):
             return self.displayname
-            
 
+
     @inlineCallbacks
     def setUp(self):
         yield super(SharingTests, self).setUp()
@@ -63,11 +65,6 @@
         self.patch(config.Sharing, "Enabled", True)
         self.patch(config.Sharing.Calendars, "Enabled", True)
 
-        self.resource = (
-            yield self.site.resource.locateChild(norequest(), ["calendar"])
-        )[0]
-        self.site.resource.putChild("calendar", self.resource)
-
         CalDAVResource.validUserIDForShare = self._fakeValidUserID
         CalDAVResource.validUserIDWithCommonNameForShare = self._fakeValidUserID
         CalDAVResource.sendInvite = lambda self, record, request: succeed(True)
@@ -76,6 +73,17 @@
         CalDAVResource.principalForCalendarUserAddress = lambda self, cuaddr: SharingTests.FakePrincipal(cuaddr)
 
 
+    @inlineCallbacks
+    def _refreshRoot(self, request=None):
+        if request is None:
+            request = norequest()
+        result = yield super(SharingTests, self)._refreshRoot(request)
+        self.resource = (
+            yield self.site.resource.locateChild(request, ["calendar"])
+        )[0]
+        returnValue(result)
+
+
     def _fakeValidUserID(self, userid, *args):
         if userid.startswith("/principals/"):
             return userid
@@ -400,7 +408,7 @@
 
     @inlineCallbacks
     def test_POSTaddRemoveSameInvitee(self):
-        
+
         self.resource.upgradeToShare()
 
         yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
@@ -448,9 +456,9 @@
             ),
         ))
 
+
     @inlineCallbacks
     def test_POSTaddInvalidInvitee(self):
-        
         self.resource.upgradeToShare()
 
         response = (yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
@@ -464,7 +472,6 @@
 """,
             responsecode.MULTI_STATUS
         ))
-        
         self.assertEqual(
             str(response.stream.read()).replace("\r\n", "\n"),
             """<?xml version='1.0' encoding='UTF-8'?>
@@ -475,10 +482,13 @@
   </response>
 </multistatus>"""
         )
-
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
-        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
 
+        self.assertEquals(
+            self._clearUIDElementValue(propInvite), customxml.Invite()
+        )
+
+
     @inlineCallbacks
     def test_POSTremoveInvalidInvitee(self):
         
@@ -530,3 +540,18 @@
 
         propInvite = (yield self.resource.readProperty(customxml.Invite, None))
         self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+
+
+
+class DatabaseSharingTests(SharingTests):
+
+    @inlineCallbacks
+    def setUp(self):
+        self.calendarStore = yield buildStore(self, StubNotifierFactory())
+        yield super(DatabaseSharingTests, self).setUp()
+
+
+    def createDataStore(self):
+        return self.calendarStore
+
+

Modified: CalendarServer/trunk/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_wrapping.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/test_wrapping.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -19,10 +19,12 @@
 """
 
 
+from twisted.trial.unittest import SkipTest
+
 from twext.web2.server import Request
 from twext.web2.responsecode import UNAUTHORIZED
 from twext.web2.http_headers import Headers
-from txdav.idav import AlreadyFinishedError
+from txdav.idav import AlreadyFinishedError, IDataStore
 
 from twext.web2.dav import davxml
 from twistedcaldav.config import config
@@ -37,13 +39,19 @@
 
 from twistedcaldav.test.util import TestCase
 
-from txcaldav.calendarstore.file import CalendarStore, CalendarHome
 from txcaldav.calendarstore.test.test_file import event4_text
 
 from txcarddav.addressbookstore.file import AddressBookStore, AddressBookHome
 from txcarddav.addressbookstore.test.test_file import vcard4_text
 
+from txcaldav.calendarstore.test.test_postgres import buildStore
+from txcaldav.calendarstore.test.common import StubNotifierFactory, \
+    assertProvides
+from txcaldav.icalendarstore import ICalendarHome
+from txcarddav.iaddressbookstore import IAddressBookHome
 
+
+
 class FakeChanRequest(object):
     def writeHeaders(self, code, headers):
         self.code = code
@@ -65,7 +73,6 @@
 
 
 
-
 class WrappingTests(TestCase):
     """
     Tests for L{twistedcaldav.static.CalDAVFile} creating the appropriate type
@@ -188,6 +195,9 @@
         return req
 
 
+    pathTypes = ['calendar', 'addressbook']
+
+
     @inlineCallbacks
     def test_autoRevertUnCommitted(self):
         """
@@ -196,7 +206,7 @@
         commit them.  This can happen, for example, with resources that are
         children of non-existent (proto-)resources.
         """
-        for pathType in ['calendar', 'addressbook']:
+        for pathType in self.pathTypes:
             req = self.requestForPath('/%ss/users/wsanchez/%s/forget/it'
                                       % (pathType, pathType))
             yield req.process()
@@ -211,7 +221,7 @@
         Sanity check and integration test: an unauthorized request of calendar
         and addressbook resources results in an L{UNAUTHORIZED} response code.
         """
-        for pathType in ['calendar', 'addressbook']:
+        for pathType in self.pathTypes:
             req = self.requestForPath('/%ss/users/wsanchez/%s/'
                                       % (pathType, pathType))
             yield req.process()
@@ -223,7 +233,7 @@
         Creating a DirectoryCalendarHomeProvisioningResource will create a paired
         CalendarStore.
         """
-        self.assertIsInstance(self.calendarCollection._newStore, CalendarStore)
+        assertProvides(self, IDataStore, self.calendarCollection._newStore)
 
 
     @inlineCallbacks
@@ -235,7 +245,7 @@
         """
         calDavFile = yield self.getResource("calendars/users/wsanchez/")
         self.commit()
-        self.assertIsInstance(calDavFile._newStoreHome, CalendarHome)
+        assertProvides(self, ICalendarHome, calDavFile._newStoreHome)
 
 
     @inlineCallbacks
@@ -264,9 +274,9 @@
         C{CalendarHome.calendarWithName}.
         """
         calDavFile = yield self.getResource("calendars/users/wsanchez/calendar")
-        self.commit()
         self.assertEquals(calDavFile.resourceType(),
                           davxml.ResourceType.calendar)
+        self.commit()
 
 
     @inlineCallbacks
@@ -338,7 +348,7 @@
         Creating a AddressBookHomeProvisioningFile will create a paired
         AddressBookStore.
         """
-        self.assertIsInstance(self.addressbookCollection._newStore, AddressBookStore)
+        assertProvides(self, IDataStore, self.addressbookCollection._newStore)
 
 
     @inlineCallbacks
@@ -350,7 +360,7 @@
         """
         calDavFile = yield self.getResource("addressbooks/users/wsanchez/")
         self.commit()
-        self.assertIsInstance(calDavFile._newStoreHome, AddressBookHome)
+        assertProvides(self, IAddressBookHome, calDavFile._newStoreHome)
 
 
     @inlineCallbacks
@@ -412,3 +422,19 @@
         self.commit()
         self.assertEquals(calDavFileAddressBook._principalCollections,
                           frozenset([self.principalsResource]))
+
+
+
+class DatabaseWrappingTests(WrappingTests):
+
+    @inlineCallbacks
+    def setUp(self):
+        self.calendarStore = yield buildStore(self, StubNotifierFactory())
+        super(DatabaseWrappingTests, self).setUp()
+
+
+    def createDataStore(self):
+        return self.calendarStore
+
+
+

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -15,18 +15,12 @@
 ##
 
 from __future__ import with_statement
-from calendarserver.provision.root import RootResource
 
-__all__ = [
-    "featureUnimplemented",
-    "testUnimplemented",
-    "todo",
-    "TestCase",
-]
-
 import os
 import xattr
 
+from calendarserver.provision.root import RootResource
+
 from twisted.python.failure import Failure
 from twisted.internet.base import DelayedCall
 from twisted.internet.defer import succeed, fail, inlineCallbacks, returnValue
@@ -34,7 +28,7 @@
 from twisted.internet.protocol import ProcessProtocol
 
 from twext.python.memcacheclient import ClientFactory
-from twext.python.filepath import CachingFilePath as FilePath, CachingFilePath
+from twext.python.filepath import CachingFilePath as FilePath
 import twext.web2.dav.test.util
 from twext.web2.dav import davxml
 from twext.web2.http import HTTPError, StatusResponse
@@ -51,6 +45,13 @@
 
 from txdav.common.datastore.file import CommonDataStore
 
+
+__all__ = [
+    "featureUnimplemented",
+    "testUnimplemented",
+    "todo",
+    "TestCase",
+]
 DelayedCall.debug = True
 
 def _todo(f, why):
@@ -67,6 +68,8 @@
 augmentsFile = dirTest.child("augments.xml")
 proxiesFile = dirTest.child("proxies.xml")
 
+
+
 class TestCase(twext.web2.dav.test.util.TestCase):
     resource_class = RootResource
 
@@ -96,6 +99,15 @@
         )
 
 
+    def createDataStore(self):
+        """
+        Create an L{IDataStore} that can store calendars (but not
+        addressbooks.)  By default returns a L{CommonDataStore}, but this is a
+        hook for subclasses to override to provide different data stores.
+        """
+        return CommonDataStore(FilePath(config.DocumentRoot), None, True, False)
+
+
     def setupCalendars(self):
         """
         Set up the resource at /calendars (a L{DirectoryCalendarHomeProvisioningResource}),
@@ -103,7 +115,7 @@
         """
 
         # Need a data store
-        _newStore = CommonDataStore(CachingFilePath(config.DocumentRoot), None, True, False)
+        _newStore = self.createDataStore()
 
         self.calendarCollection = DirectoryCalendarHomeProvisioningResource(
             self.directoryService,
@@ -133,7 +145,7 @@
         config.ConfigRoot = "config"
         config.LogRoot = "logs"
         config.RunRoot = "logs"
-        
+
         if not os.path.exists(config.DataRoot):
             os.makedirs(config.DataRoot)
         if not os.path.exists(config.DocumentRoot):
@@ -150,6 +162,7 @@
 
         config.DirectoryAddressBook.Enabled = False
 
+
     def createHierarchy(self, structure, root=None):
         if root is None:
             root = self.mktemp()
@@ -295,6 +308,13 @@
     than a top-level resource hierarchy.
     """
 
+    def createDataStore(self):
+        # FIXME: AddressBookHomeTestCase needs the same treatment.
+        fp = FilePath(self.mktemp())
+        fp.createDirectory()
+        return CommonDataStore(fp, None, True, False)
+
+
     def setUp(self):
         """
         Replace self.site.resource with an appropriately provisioned
@@ -303,13 +323,10 @@
         """
         super(HomeTestCase, self).setUp()
 
-        fp = FilePath(self.mktemp())
-        fp.createDirectory()
-
         self.createStockDirectoryService()
 
         # Need a data store
-        _newStore = CommonDataStore(fp, None, True, False)
+        _newStore = self.createDataStore()
 
         self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
             self.directoryService, "/calendars/",
@@ -318,21 +335,39 @@
         
         def _defer(user):
             # Commit the transaction
-            self.site.resource._associatedTransaction.commit()
-            self.docroot = user._newStoreHome._path.path
+            self.addCleanup(self.noRenderCommit)
+            # FIXME: nothing should use docroot any more.
+            aPath = getattr(user._newStoreHome, "_path", None)
+            if aPath is not None:
+                self.docroot = aPath.path
 
         return self._refreshRoot().addCallback(_defer)
 
 
+    committed = True
+
+    def noRenderCommit(self):
+        """
+        A resource was retrieved but will not be rendered, so commit.
+        """
+        if not self.committed:
+            self.committed = True
+            self.site.resource._associatedTransaction.commit()
+
+
     @inlineCallbacks
-    def _refreshRoot(self):
+    def _refreshRoot(self, request=None):
         """
         Refresh the user resource positioned at the root of this site, to give
         it a new transaction.
         """
+        self.noRenderCommit()
+        if request is None:
+            request = norequest()
         users = self.homeProvisioner.getChild("users")
-        user, ignored = (yield users.locateChild(norequest(), ["wsanchez"]))
 
+        user, ignored = (yield users.locateChild(request, ["wsanchez"]))
+
         # Force the request to succeed regardless of the implementation of
         # accessControlList.
         user.accessControlList = lambda request, *a, **k: succeed(
@@ -342,19 +377,27 @@
         # Fix the site to point directly at the user's calendar home so that we
         # can focus on testing just that rather than hierarchy traversal..
         self.site.resource = user
+        self.committed = False
         returnValue(user)
 
 
     @inlineCallbacks
-    def send(self, request, callback):
+    def send(self, request, callback=None):
         """
         Override C{send} in order to refresh the 'user' resource each time, to
         get a new transaction to associate with the calendar home.
         """
+        self.noRenderCommit()
+        yield self._refreshRoot(request)
+        result = (yield super(HomeTestCase, self).send(request))
+        self.committed = True
         yield self._refreshRoot()
-        result = (yield super(HomeTestCase, self).send(request, callback))
+        if callback is not None:
+            result = yield callback(result)
         returnValue(result)
 
+
+
 class AddressBookHomeTestCase(TestCase):
     """
     Utility class for tests which wish to interact with a addressbook home rather
@@ -411,7 +454,7 @@
 
 
     @inlineCallbacks
-    def send(self, request, callback):
+    def send(self, request, callback=None):
         """
         Override C{send} in order to refresh the 'user' resource each time, to
         get a new transaction to associate with the calendar home.
@@ -422,7 +465,6 @@
 
 
 
-
 class InMemoryPropertyStore(object):
     def __init__(self):
         class _FauxPath(object):

Modified: CalendarServer/trunk/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/file.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/file.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -33,6 +33,7 @@
 
 from twisted.internet.interfaces import ITransport
 from twisted.python.failure import Failure
+
 from txdav.propertystore.xattr import PropertyStore
 
 from twext.python.vcomponent import InvalidICalendarDataError
@@ -41,7 +42,6 @@
 from twext.web2.dav.resource import TwistedGETContentMD5
 from twext.web2.http_headers import generateContentType
 
-
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
 from twistedcaldav.index import Index as OldIndex, IndexSchedule as OldInboxIndex
@@ -51,11 +51,17 @@
 from txcaldav.icalendarstore import ICalendar, ICalendarObject
 from txcaldav.icalendarstore import ICalendarHome
 
+from txcaldav.calendarstore.util import (
+    validateCalendarComponent, dropboxIDFromCalendarObject
+)
 
-from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction, \
-    CommonHome, CommonHomeChild, CommonObjectResource
-from txdav.common.icommondatastore import InvalidObjectResourceError, \
-    NoSuchObjectResourceError, InternalDataStoreError
+from txdav.common.datastore.file import (
+    CommonDataStore, CommonStoreTransaction, CommonHome, CommonHomeChild,
+    CommonObjectResource
+)
+
+from txdav.common.icommondatastore import (NoSuchObjectResourceError,
+    InternalDataStoreError)
 from txdav.datastore.file import writeOperation, hidden, FileMetaDataMixin
 from txdav.propertystore.base import PropertyName
 
@@ -196,12 +202,6 @@
             ),
         )
 
-    def _doValidate(self, component):
-        # FIXME: should be separate class, not separate case!
-        if self.name() == 'inbox':
-            component.validateComponentsForCalDAV(True)
-        else:
-            component.validateForCalDAV()
 
 
 class CalendarObject(CommonObjectResource):
@@ -222,26 +222,14 @@
         return self._parentCollection
 
 
+    def calendar(self):
+        return self._calendar
+
+
     @writeOperation
     def setComponent(self, component):
-        if not isinstance(component, VComponent):
-            raise TypeError(type(component))
+        validateCalendarComponent(self, self._calendar, component)
 
-        try:
-            if component.resourceUID() != self.uid():
-                raise InvalidObjectResourceError(
-                    "UID may not change (%s != %s)" % (
-                        component.resourceUID(), self.uid()
-                     )
-                )
-        except NoSuchObjectResourceError:
-            pass
-
-        try:
-            self._calendar._doValidate(component)
-        except InvalidICalendarDataError, e:
-            raise InvalidObjectResourceError(e)
-
         newRevision = self._calendar._updateSyncToken() # FIXME: test
         self._calendar.retrieveOldIndex().addResource(
             self.name(), component, newRevision
@@ -378,24 +366,9 @@
 
 
     def dropboxID(self):
-        # FIXME: direct tests
-        dropboxProperty = self.component().getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
-        if dropboxProperty is not None:
-            componentDropboxID = dropboxProperty.value().split("/")[-1]
-            return componentDropboxID
-        attachProperty = self.component().getFirstPropertyInAnyComponent("ATTACH")
-        if attachProperty is not None:
-            # Make sure the value type is URI
-            valueType = attachProperty.params().get("VALUE", ("TEXT",))
-            if valueType[0] == "URI":
-                # FIXME: more aggressive checking to see if this URI is really the
-                # 'right' URI.  Maybe needs to happen in the front end.
-                attachPath = attachProperty.value().split("/")[-2]
-                return attachPath
+        return dropboxIDFromCalendarObject(self)
 
-        return self.uid() + ".dropbox"
 
-
     def _dropboxPath(self):
         dropboxPath = self._parentCollection._home._path.child(
             "dropbox"
@@ -462,6 +435,7 @@
         props.flush()
 
 
+
 class Attachment(FileMetaDataMixin):
     """
     An L{Attachment} is a container for the data associated with a I{locally-

Modified: CalendarServer/trunk/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/postgres.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/postgres.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -20,13 +20,2082 @@
 """
 
 __all__ = [
-    "CalendarStore",
-    "CalendarHome",
-    "Calendar",
-    "CalendarObject",
+    "PostgresStore",
+    "PostgresCalendarHome",
+    "PostgresCalendar",
+    "PostgresCalendarObject",
+    "PostgresAddressBookHome",
+    "PostgresAddressBook",
+    "PostgresAddressBookObject",
 ]
 
+import StringIO
+
+from twisted.python import hashlib
+from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
+
+from inspect import getargspec
+from zope.interface.declarations import implements
+
 from twisted.python.modules import getModule
+from twisted.application.service import Service
+from twisted.internet.interfaces import ITransport
+from twisted.internet.error import ConnectionLost
+from twisted.python.failure import Failure
 
+from twext.web2.dav.element.rfc2518 import ResourceType
 
-v1_schema = getModule(__name__).filePath.sibling("postgres_schema_v1.sql").getContent()
+from txdav.idav import IDataStore, AlreadyFinishedError
+from txdav.common.inotifications import (INotificationCollection,
+    INotificationObject)
+
+from txdav.common.icommondatastore import (
+    ObjectResourceNameAlreadyExistsError, HomeChildNameAlreadyExistsError,
+    NoSuchHomeChildError, NoSuchObjectResourceError)
+from txcaldav.calendarstore.util import (validateCalendarComponent,
+    validateAddressBookComponent, dropboxIDFromCalendarObject, SyncTokenHelper)
+
+
+from txcaldav.icalendarstore import (ICalendarTransaction, ICalendarHome,
+    ICalendar, ICalendarObject, IAttachment)
+from txcarddav.iaddressbookstore import (IAddressBookTransaction,
+    IAddressBookHome, IAddressBook, IAddressBookObject)
+from txdav.propertystore.base import AbstractPropertyStore, PropertyName
+from txdav.propertystore.none import PropertyStore
+
+from twext.web2.http_headers import MimeType, generateContentType
+from twext.web2.dav.element.parser import WebDAVDocument
+
+from twext.python.vcomponent import VComponent
+from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.sharing import Invite
+from twistedcaldav.notifications import NotificationRecord
+from twistedcaldav.query.sqlgenerator import sqlgenerator
+from twistedcaldav.index import IndexedSearchException
+
+
+v1_schema = getModule(__name__).filePath.sibling(
+    "postgres_schema_v1.sql").getContent()
+
+
+# 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
+
+
+
+def _getarg(argname, argspec, args, kw):
+    """
+    Get an argument from some arguments.
+
+    @param argname: The name of the argument to retrieve.
+
+    @param argspec: The result of L{inspect.getargspec}.
+
+    @param args: positional arguments passed to the function specified by
+        argspec.
+
+    @param kw: keyword arguments passed to the function specified by
+        argspec.
+
+    @return: The value of the argument named by 'argname'.
+    """
+    argnames = argspec[0]
+    try:
+        argpos = argnames.index(argname)
+    except ValueError:
+        argpos = None
+    if argpos is not None:
+        if len(args) > argpos:
+            return args[argpos]
+    if argname in kw:
+        return kw[argname]
+    else:
+        raise TypeError("could not find key argument %r in %r/%r (%r)" %
+            (argname, args, kw, argpos)
+        )
+
+
+
+def memoized(keyArgument, memoAttribute):
+    """
+    Decorator which memoizes the result of a method on that method's instance.
+
+    @param keyArgument: The name of the 'key' argument.
+
+    @type keyArgument: C{str}
+
+    @param memoAttribute: The name of the attribute on the instance which
+        should be used for memoizing the result of this method; the attribute
+        itself must be a dictionary.
+
+    @type memoAttribute: C{str}
+    """
+    def decorate(thunk):
+        spec = getargspec(thunk)
+        def outer(*a, **kw):
+            self = a[0]
+            memo = getattr(self, memoAttribute)
+            key = _getarg(keyArgument, spec, a, kw)
+            if key in memo:
+                return memo[key]
+            result = thunk(*a, **kw)
+            if result is not None:
+                memo[key] = result
+            return result
+        return outer
+    return decorate
+
+
+
+class PropertyStore(AbstractPropertyStore):
+
+    def __init__(self, peruser, defaultuser, txn, resourceID):
+        super(PropertyStore, self).__init__(peruser, defaultuser)
+        self._txn = txn
+        self._resourceID = resourceID
+
+
+    def _getitem_uid(self, key, uid):
+        rows = self._txn.execSQL(
+            "select VALUE from RESOURCE_PROPERTY where "
+            "RESOURCE_ID = %s and NAME = %s and VIEWER_UID = %s",
+            [self._resourceID, key.toString(), uid])
+        if not rows:
+            raise KeyError(key)
+        return WebDAVDocument.fromString(rows[0][0]).root_element
+
+
+    def _setitem_uid(self, key, value, uid):
+        self._delitem_uid(key, uid)
+        self._txn.execSQL(
+            "insert into RESOURCE_PROPERTY "
+            "(RESOURCE_ID, NAME, VALUE, VIEWER_UID) values (%s, %s, %s, %s)",
+            [self._resourceID, key.toString(), value.toxml(), uid])
+
+
+    def _delitem_uid(self, key, uid):
+        self._txn.execSQL(
+            "delete from RESOURCE_PROPERTY where VIEWER_UID = %s"
+            "and RESOURCE_ID = %s AND NAME = %s",
+            [uid, self._resourceID, key.toString()])
+
+
+    def _keys_uid(self, uid):
+        rows = self._txn.execSQL(
+            "select NAME from RESOURCE_PROPERTY where "
+            "VIEWER_UID = %s and RESOURCE_ID = %s",
+            [uid, self._resourceID]
+        )
+        for row in rows:
+            yield PropertyName.fromString(row[0])
+
+
+
+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()
+
+
+    def properties(self):
+        return PropertyStore(
+            self.uid(),
+            self.uid(),
+            self._txn,
+            self._resourceID
+        )
+
+
+    def setComponent(self, component):
+        validateCalendarComponent(self, self._calendar, component)
+        calendarText = str(component)
+        self._txn.execSQL(
+            "update CALENDAR_OBJECT set ICALENDAR_TEXT = %s "
+            "where RESOURCE_ID = %s", [calendarText, self._resourceID]
+        )
+        self._calendarText = calendarText
+        self._calendar._updateSyncToken()
+
+        if self._calendar._notifier:
+            self._calendar._home._txn.postCommit(self._calendar._notifier.notify)
+
+
+    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")
+
+
+    def md5(self):
+        return None
+
+
+    def size(self):
+        return 0
+
+
+    def created(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+    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, MD5 from ATTACHMENT where PATH = %s
+            """, [self._pathValue()])
+        if not rows:
+            return False
+        self._contentType = MimeType.fromString(rows[0][0])
+        self._md5 = rows[0][1]
+        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 0
+
+
+    def created(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+    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, MD5 = %s "
+            "WHERE PATH = %s",
+            [contentTypeString, 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.SENDER_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__/" + 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.
+        principalUID = record.principalURL.split("/")[-1]
+        shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
+        rows = self._txn.execSQL(
+            "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where INVITE_UID = %s",
+            [record.inviteuid]
+        )
+        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, SENDER_ADDRESS = %s
+                where INVITE_UID = %s
+                """,
+                [record.name, record.userid, record.inviteuid]
+            )
+        else:
+            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
+                ])
+            self._txn.execSQL(
+                """
+                insert into INVITE (
+                    INVITE_UID, NAME,
+                    HOME_RESOURCE_ID, RESOURCE_ID,
+                    SENDER_ADDRESS
+                )
+                values (%s, %s, %s, %s, %s)
+                """,
+                [
+                    record.inviteuid, record.name,
+                    shareeHome._resourceID, self._calendar._resourceID,
+                    record.userid
+                ])
+
+
+    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):
+        return []
+#        c = self._home._txn._cursor
+#        c.execute(
+#            "select CALENDAR_RESOURCE_ID, CALENDAR_HOME_RESOURCE_ID from "
+#            "CALENDAR_BIND where CALENDAR_BIND"
+#            "",
+#            [self._home.uid()])
+#        ownedShares = c.fetchall()
+#        for row in rows:
+#            [calendarResourceID] = row
+#            shareuid = 
+#            yield SharedCollectionRecord(
+#                shareuid, sharetype, hosturl, localname, summary
+#            )
+
+
+    def recordForLocalName(self, localname):
+        return None
+#        c = self._home._txn.cursor()
+#        return SharedCollectionRecord(shareuid, sharetype, hosturl, localname, summary)
+
+
+    def recordForShareUID(self, shareUID):
+        pass
+
+
+    def addOrUpdateRecord(self, record):
+        print record
+
+#        self._db_execute("""insert or replace into SHARES (SHAREUID, SHARETYPE, HOSTURL, LOCALNAME, SUMMARY)
+#            values (:1, :2, :3, :4, :5)
+#            """, record.shareuid, record.sharetype, record.hosturl, record.localname, record.summary,
+#        )
+
+    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.)
+    """
+
+    def __init__(self, expr, calendar):
+        self.TIMESPANDB = "TIME_RANGE"
+        self.TIMESPANTEST = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
+        self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.END_DATE > %s))"
+        self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOAT == 'N' AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOAT == 'Y' AND TIME_RANGE.START_DATE < %s))"
+        self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID == CALENDAR_OBJECT.RESOURCE_ID"
+
+        super(postgresqlgenerator, self).__init__(expr)
+        self.calendar = calendar
+
+
+    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.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:
+            select += ", %s, %s" % (
+                self.TIMESPANDB)
+        select += self.sout.getvalue()
+        return select, self.arguments
+
+
+    def addArgument(self, arg):
+        self.arguments.append(arg)
+        self.sout.write("%s")
+
+
+    def setArgument(self, arg):
+        self.arguments.append(arg)
+        return "%s"
+
+
+
+class PostgresLegacyIndexEmulator(object):
+    """
+    Emulator for L{twistedcaldv.index.Index} and
+    L{twistedcaldv.index.IndexSchedule}.
+    """
+
+    def __init__(self, calendar):
+        self.calendar = calendar
+
+
+    @property
+    def _txn(self):
+        return self.calendar._txn
+
+
+    def reserveUID(self, uid):
+        pass
+
+
+    def isAllowedUID(self, uid, *names):
+        """
+        @see: L{twistedcaldav.index.Index.isAllowedUID}
+        """
+        return True
+
+
+    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 unreserveUID(self, uid):
+        pass
+
+
+    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 self._txn.execSQL(
+            "select RESOURCE_NAME from CALENDAR_OBJECT "
+            "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
+            [minDate, self.calendar._resourceID]
+        )
+
+
+    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, 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(SyncTokenHelper):
+
+    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 retrieveOldInvites(self):
+        return PostgresLegacyInvitesEmulator(self)
+
+    def retrieveOldIndex(self):
+        return PostgresLegacyIndexEmulator(self)
+
+
+    def notifierID(self, label="default"):
+        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
+
+
+    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)
+
+
+    def calendarObjectWithUID(self, uid):
+        rows = self._txn.execSQL(
+            "select RESOURCE_NAME from CALENDAR_OBJECT where "
+            "ICALENDAR_UID = %s",
+            [uid]
+        )
+        if not rows:
+            return None
+        name = rows[0][0]
+        return self.calendarObjectWithName(name)
+
+
+    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()
+
+        self._updateSyncToken()
+
+        calendarObject = PostgresCalendarObject(self, name, None)
+        calendarObject.component = lambda : component
+
+        validateCalendarComponent(calendarObject, self, component)
+
+        componentText = str(component)
+        self._txn.execSQL(
+            """
+            insert into CALENDAR_OBJECT
+            (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT,
+             ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE)
+             values
+            (%s, %s, %s, %s, %s, %s)
+            """,
+            # should really be filling out more fields: ORGANIZER,
+            # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
+            # DROPBOX
+            [self._resourceID, name, componentText, component.resourceUID(),
+            component.resourceType(), _ATTACHMENTS_MODE_WRITE]
+        )
+        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._updateSyncToken()
+
+        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._updateSyncToken()
+
+        if self._notifier:
+            self._home._txn.postCommit(self._notifier.notify)
+
+
+    def syncToken(self):
+        return self._txn.execSQL(
+            "select SYNC_TOKEN from CALENDAR where RESOURCE_ID = %s",
+            [self._resourceID])[0][0]
+
+
+    def calendarObjectsInTimeRange(self, start, end, timeZone):
+        raise NotImplementedError()
+
+
+    def calendarObjectsSinceToken(self, token):
+        raise NotImplementedError()
+
+
+    def properties(self):
+        ownerUID = self.ownerCalendarHome().uid()
+        return PropertyStore(
+            ownerUID,
+            ownerUID,
+            self._txn,
+            self._resourceID
+        )
+
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of Calendar objects is text/calendar.
+        """
+        return MimeType.fromString("text/calendar")
+
+
+    def md5(self):
+        return None
+
+
+    def size(self):
+        return 0
+
+
+    def created(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+
+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_STATUS != %s",
+            [self._resourceID, _BIND_STATUS_DECLINED]
+        )
+        names = [row[0] for row in rows]
+        return names
+
+
+    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",
+            [name]
+        )
+        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 (SYNC_TOKEN, RESOURCE_ID) values "
+            "(%s, %s)",
+            ['uninitialized', 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(True)
+
+        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)
+
+
+    def properties(self):
+        return PropertyStore(
+            self.uid(),
+            self.uid(),
+            self._txn,
+            self._resourceID
+        )
+
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of Calendar objects is text/calendar.
+        """
+        return MimeType.fromString("text/calendar")
+
+
+    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"):
+        return None
+
+
+
+class PostgresNotificationObject(object):
+    implements(INotificationObject)
+
+    def __init__(self, home, resourceID):
+        self._home = home
+        self._resourceID = resourceID
+
+
+    def name(self):
+        return self.uid() + ".xml"
+
+
+    @property
+    def _txn(self):
+        return self._home._txn
+
+
+    def setData(self, uid, xmltype, xmldata):
+        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")
+
+
+    def properties(self):
+        return PropertyStore(self._home.uid(),
+                             self._home.uid(),
+                             self._txn,
+                             self._resourceID)
+
+
+    def md5(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+    def created(self):
+        return None
+
+
+
+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 PostgresNotificationsCollection(object):
+
+    implements(INotificationCollection)
+
+    def __init__(self, txn, uid, resourceID):
+        self._txn = txn
+        self._uid = uid
+        self._resourceID = resourceID
+
+
+    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))
+
+
+    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):
+        xmltype = PropertyName.fromElement(xmltype).toString()
+        self._txn.execSQL(
+            "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
+            "values (%s, %s, %s, %s)", [self._resourceID, uid, xmltype, xmldata])
+
+
+    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)
+
+
+    def properties(self):
+        return PropertyStore(
+            self._uid, self._uid, self._txn, self._resourceID
+        )
+
+
+
+class PostgresTransaction(object):
+    """
+    Transaction implementation for postgres database.
+    """
+    implements(ICalendarTransaction, IAddressBookTransaction)
+
+    def __init__(self, store, connection, notifierFactory, label):
+        # print 'STARTING', label
+        self._store = store
+        self._connection = connection
+        self._cursor = connection.cursor()
+        self._completed = False
+        self._homes = {}
+        self._postCommitOperations = []
+        self._notifierFactory = notifierFactory
+        self._label = label
+
+
+    def store(self):
+        return self._store
+
+
+    def __repr__(self):
+        return 'PG-TXN<%s>' % (self._label,)
+
+
+    def execSQL(self, sql, args=[]):
+        # print 'EXECUTE %s: %s' % (self._label, sql)
+        self._cursor.execute(sql, args)
+        if self._cursor.description:
+            return self._cursor.fetchall()
+        else:
+            return None
+
+
+    def __del__(self):
+        if not self._completed:
+            self._connection.rollback()
+            self._connection.close()
+
+
+    @memoized('uid', '_homes')
+    def calendarHomeWithUID(self, uid, create=False):
+        data = self.execSQL(
+            "select RESOURCE_ID from CALENDAR_HOME where OWNER_UID = %s",
+            [uid]
+        )
+        if not data:
+            if not create:
+                return None
+            self.execSQL(
+                "insert into CALENDAR_HOME (OWNER_UID) values (%s)",
+                [uid]
+            )
+            home = self.calendarHomeWithUID(uid)
+            home.createCalendarWithName("calendar")
+            return home
+        resid = data[0][0]
+
+        if self._notifierFactory:
+            notifier = self._notifierFactory.newNotifier(id=uid)
+        else:
+            notifier = None
+
+        return PostgresCalendarHome(self, uid, resid, notifier)
+
+
+    @memoized('uid', '_homes')
+    def addressbookHomeWithUID(self, uid, create=False):
+        data = self.execSQL(
+            "select RESOURCE_ID from ADDRESSBOOK_HOME where OWNER_UID = %s",
+            [uid]
+        )
+        if not data:
+            if not create:
+                return None
+            self.execSQL(
+                "insert into ADDRESSBOOK_HOME (OWNER_UID) values (%s)",
+                [uid]
+            )
+            home = self.addressbookHomeWithUID(uid)
+            home.createAddressBookWithName("addressbook")
+            return home
+        resid = data[0][0]
+
+        if self._notifierFactory:
+            notifier = self._notifierFactory.newNotifier(id=uid)
+        else:
+            notifier = None
+
+        return PostgresAddressBookHome(self, uid, resid, notifier)
+
+
+    def notificationsWithUID(self, uid):
+        """
+        Implement notificationsWithUID.
+        """
+        rows = self.execSQL(
+            """
+            select RESOURCE_ID from NOTIFICATION_HOME where
+            OWNER_UID = %s
+            """, [uid])
+        if rows:
+            [[resourceID]] = rows
+        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 PostgresNotificationsCollection(self, uid, resourceID)
+
+
+    def abort(self):
+        if not self._completed:
+            # print 'ABORTING', self._label
+            self._completed = True
+            self._connection.rollback()
+            self._connection.close()
+        else:
+            raise AlreadyFinishedError()
+
+
+    def commit(self):
+        if not self._completed:
+            # print 'COMPLETING', self._label
+            self._completed = True
+            self._connection.commit()
+            self._connection.close()
+            for operation in self._postCommitOperations:
+                operation()
+        else:
+            raise AlreadyFinishedError()
+
+
+    def postCommit(self, operation):
+        """
+        Run things after 'commit.'
+        """
+        self._postCommitOperations.append(operation)
+        # FIXME: implement.
+
+# 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()
+
+
+    def properties(self):
+        return PropertyStore(
+            self.uid(),
+            self.uid(),
+            self._txn,
+            self._resourceID
+        )
+
+
+    def setComponent(self, component):
+        validateAddressBookComponent(self, self._addressbook, component)
+
+        vCardText = str(component)
+        self._txn.execSQL(
+            "update ADDRESSBOOK_OBJECT set VCARD_TEXT = %s "
+            "where RESOURCE_ID = %s", [vCardText, self._resourceID]
+        )
+        self._vCardText = vCardText
+        if self._addressbook._notifier:
+            self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
+
+
+
+    # IDataStoreResource
+    def contentType(self):
+        """
+        The content type of Addressbook objects is text/x-vcard.
+        """
+        return MimeType.fromString("text/x-vcard")
+
+
+    def md5(self):
+        return None
+
+
+    def size(self):
+        return 0
+
+
+    def created(self):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+
+class PostgresAddressBook(object):
+
+    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 retrieveOldInvites(self):
+        return PostgresLegacyInvitesEmulator(self)
+
+    def retrieveOldIndex(self):
+        return PostgresLegacyIndexEmulator(self)
+
+
+    def notifierID(self, label="default"):
+        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
+
+
+    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)
+
+
+    def addressbookObjectWithUID(self, uid):
+        rows = self._txn.execSQL(
+            "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+            "VCARD_UID = %s",
+            [uid]
+        )
+        if not rows:
+            return None
+        name = rows[0][0]
+        return self.addressbookObjectWithName(name)
+
+
+    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)
+
+        componentText = str(component)
+        self._txn.execSQL(
+            """
+            insert into ADDRESSBOOK_OBJECT
+            (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT,
+             VCARD_UID, VCARD_TYPE)
+             values
+            (%s, %s, %s, %s, %s)
+            """,
+            [self._resourceID, name, componentText, component.resourceUID(),
+            "VCARD"] # component.resourceType()]  FIXME: what value(s) here?
+        )
+        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)
+        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)
+        if self._notifier:
+            self._home._txn.postCommit(self._notifier.notify)
+
+
+    def syncToken(self):
+        return self._txn.execSQL(
+            "select SYNC_TOKEN from ADDRESSBOOK where RESOURCE_ID = %s",
+            [self._resourceID])[0][0]
+
+
+    def addressbookObjectsSinceToken(self, token):
+        raise NotImplementedError()
+
+
+    def properties(self):
+        ownerUID = self.ownerAddressBookHome().uid()
+        return PropertyStore(
+            ownerUID,
+            ownerUID,
+            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):
+        return None
+
+
+    def modified(self):
+        return None
+
+
+
+
+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 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 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 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",
+            [name]
+        )
+        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 (SYNC_TOKEN, RESOURCE_ID) values "
+            "(%s, %s)",
+            ['uninitialized', 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
+        self.addressbookWithName(name).properties()[
+            PropertyName.fromElement(ResourceType)] = addressbookType
+        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)
+
+
+    def properties(self):
+        return PropertyStore(
+            self.uid(),
+            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"):
+        return None
+
+
+#
+
+
+class PostgresStore(Service, object):
+
+    implements(IDataStore)
+
+    def __init__(self, connectionFactory, notifierFactory, attachmentsPath):
+        self.connectionFactory = connectionFactory
+        self.notifierFactory = notifierFactory
+        self.attachmentsPath = attachmentsPath
+
+
+    def newTransaction(self, label="unlabeled"):
+        return PostgresTransaction(
+            self,
+            self.connectionFactory(),
+            self.notifierFactory,
+            label
+        )
+

Modified: CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/postgres_schema_v1.sql	2010-08-12 02:36:27 UTC (rev 6074)
@@ -25,6 +25,40 @@
 );
 
 
+------------------------
+-- Sharing Invitation --
+------------------------
+
+create table INVITE (
+    INVITE_UID         varchar(255) not null,
+    NAME               varchar(255) not null,
+    SENDER_ADDRESS     varchar(255) not null,
+    HOME_RESOURCE_ID   varchar(255) not null,
+    RESOURCE_ID        varchar(255) not null
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+  RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID   varchar(255) not null unique
+);
+
+
+create table NOTIFICATION (
+  RESOURCE_ID varchar(255)
+        primary key default nextval('RESOURCE_ID_SEQ'),
+  NOTIFICATION_HOME_RESOURCE_ID
+        varchar(255) not null references NOTIFICATION_HOME,
+  NOTIFICATION_UID varchar(255) not null,
+  XML_TYPE varchar not null,
+  XML_DATA varchar not null
+);
+
+
 -------------------
 -- Calendar Bind --
 -------------------
@@ -34,12 +68,16 @@
 create table CALENDAR_BIND (
   CALENDAR_HOME_RESOURCE_ID varchar(255) not null references CALENDAR_HOME,
   CALENDAR_RESOURCE_ID      varchar(255) not null references CALENDAR,
-  CALENDAR_RESOURCE_NAME    varchar(255) not null,
+  
+  -- 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             bool         not null,
   SEEN_BY_SHAREE            bool         not null,
-  MESSAGE                   text,                  -- FIXME: xml?
+  MESSAGE                   text,
 
   primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
   unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
@@ -66,6 +104,7 @@
 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');
 
 
 ---------------------
@@ -82,9 +121,15 @@
   ATTACHMENTS_MODE     int          not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
   ORGANIZER            varchar(255),
   ORGANIZER_OBJECT     varchar(255) references CALENDAR_OBJECT,
+  RECURRANCE_MAX       date, -- maximum date that recurrences have been expanded to.
 
-  unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME),
-  unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+  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
@@ -131,11 +176,11 @@
 ----------------
 
 create table ATTACHMENT (
-  CALENDAR_OBJECT_RESOURCE_ID varchar(255) not null references CALENDAR_OBJECT,
-  CONTENT_TYPE                varchar(255) not null,
-  SIZE                        int          not null,
-  MD5                         char(32)     not null,
-  PATH                        varchar(255) not null unique
+  CALENDAR_OBJECT_RESOURCE_ID varchar(255)  not null references CALENDAR_OBJECT,
+  CONTENT_TYPE                varchar(255)  not null,
+  SIZE                        int           not null,
+  MD5                         char(32)      not null,
+  PATH                        varchar(1024) not null unique
 );
 
 
@@ -164,3 +209,57 @@
 
   primary key(RESOURCE_ID, NAME, VIEWER_UID)
 );
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+  RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+  OWNER_UID   varchar(255) not null unique
+);
+
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+  RESOURCE_ID varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+  SYNC_TOKEN  varchar(255)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+  ADDRESSBOOK_HOME_RESOURCE_ID varchar(255) not null references ADDRESSBOOK_HOME,
+  ADDRESSBOOK_RESOURCE_ID      varchar(255) not null references ADDRESSBOOK,
+  ADDRESSBOOK_RESOURCE_NAME    varchar(255) not null,
+  BIND_MODE                 integer      not null, -- enum CALENDAR_BIND_MODE
+  BIND_STATUS               integer      not null, -- enum CALENDAR_BIND_STATUS
+  SEEN_BY_OWNER             bool         not null,
+  SEEN_BY_SHAREE            bool         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          varchar(255) primary key default nextval('RESOURCE_ID_SEQ'),
+  ADDRESSBOOK_RESOURCE_ID varchar(255) not null references ADDRESSBOOK,
+  RESOURCE_NAME        varchar(255) not null,
+  VCARD_TEXT           text         not null,
+  VCARD_UID            varchar(255) not null,
+  VCARD_TYPE           varchar(255) not null,
+
+  unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
+  unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
+);

Modified: CalendarServer/trunk/txcaldav/calendarstore/scheduling.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/scheduling.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/scheduling.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -153,9 +153,9 @@
         self._calendarStore = calendarStore
 
 
-    def newTransaction(self):
+    def newTransaction(self, label="unlabeled"):
         """
         Wrap an underlying L{ITransaction}.
         """
         return ImplicitTransaction(
-                    self._calendarStore.newTransaction())
+                    self._calendarStore.newTransaction(label))

Modified: CalendarServer/trunk/txcaldav/calendarstore/test/common.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/common.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/common.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -144,6 +144,22 @@
 
 
 
+def assertProvides(testCase, interface, provider):
+    """
+    Verify that C{provider} properly provides C{interface}
+
+    @type interface: L{zope.interface.Interface}
+    @type provider: C{provider}
+    """
+    try:
+        verifyObject(interface, provider)
+    except BrokenMethodImplementation, e:
+        testCase.fail(e)
+    except DoesNotImplement, e:
+        testCase.fail("%r does not provide %s.%s" %
+                      (provider, interface.__module__, interface.getName()))
+
+
 class CommonTests(object):
     """
     Tests for common functionality of interfaces defined in
@@ -157,6 +173,7 @@
                 "2.ics": cal1Root.child("2.ics").getContent(),
                 "3.ics": cal1Root.child("3.ics").getContent()
             },
+            "calendar_2": {},
             "calendar_empty": {},
             "not_a_calendar": None
         },
@@ -188,7 +205,7 @@
             return self.lastTransaction
         if self.savedStore is None:
             self.savedStore = self.storeUnderTest()
-        txn = self.lastTransaction = self.savedStore.newTransaction()
+        txn = self.lastTransaction = self.savedStore.newTransaction(self.id())
         return txn
 
 
@@ -210,6 +227,10 @@
         self.lastTransaction = None
 
 
+    def setUp(self):
+        self.notifierFactory = StubNotifierFactory()
+
+
     def tearDown(self):
         if self.lastTransaction is not None:
             self.commit()
@@ -219,8 +240,7 @@
         """
         Get the calendar home detailed by C{requirements['home1']}.
         """
-        return self.transactionUnderTest().calendarHomeWithUID(
-            "home1")
+        return self.transactionUnderTest().calendarHomeWithUID("home1")
 
 
     def calendarUnderTest(self):
@@ -238,22 +258,8 @@
         return self.calendarUnderTest().calendarObjectWithName("1.ics")
 
 
-    def assertProvides(self, interface, provider):
-        """
-        Verify that C{provider} properly provides C{interface}
+    assertProvides = assertProvides
 
-        @type interface: L{zope.interface.Interface}
-        @type provider: C{provider}
-        """
-        try:
-            verifyObject(interface, provider)
-        except BrokenMethodImplementation, e:
-            self.fail(e)
-        except DoesNotImplement, e:
-            self.fail("%r does not provide %s.%s" %
-                (provider, interface.__module__, interface.getName()))
-
-
     def test_calendarStoreProvides(self):
         """
         The calendar store provides L{IDataStore} and its required attributes.
@@ -268,7 +274,7 @@
         L{ICommonStoreTransaction}, L{ICalendarTransaction}, and their
         respectively required attributes.
         """
-        txn = self.storeUnderTest().newTransaction()
+        txn = self.transactionUnderTest()
         self.assertProvides(ICommonTransaction, txn)
         self.assertProvides(ICalendarTransaction, txn)
 
@@ -303,9 +309,8 @@
         provides L{ICalendarHome} and has a C{uid()} method that returns the
         same value that was passed in.
         """
-        calendarHome = (self.storeUnderTest().newTransaction()
+        calendarHome = (self.transactionUnderTest()
                         .calendarHomeWithUID("home1"))
-
         self.assertEquals(calendarHome.uid(), "home1")
         self.assertProvides(ICalendarHome, calendarHome)
 
@@ -315,11 +320,8 @@
         L{ICommonStoreTransaction.calendarHomeWithUID} should return C{None}
         when asked for a non-existent calendar home.
         """
-        self.assertEquals(
-            self.storeUnderTest().newTransaction()
-            .calendarHomeWithUID("xyzzy"),
-            None
-        )
+        txn = self.transactionUnderTest()
+        self.assertEquals(txn.calendarHomeWithUID("xyzzy"), None)
 
 
     def test_calendarWithName_exists(self):
@@ -330,6 +332,8 @@
         home = self.homeUnderTest()
         for name in home1_calendarNames:
             calendar = home.calendarWithName(name)
+            if calendar is None:
+                self.fail("calendar %r didn't exist" % (name,))
             self.assertProvides(ICalendar, calendar)
             self.assertEquals(calendar.name(), name)
 
@@ -380,24 +384,27 @@
                 calendarProperties[
                     PropertyName.fromString(davxml.ResourceType.sname())
                 ],
-                davxml.ResourceType.calendar) #@UndefinedVariable
+                davxml.ResourceType.calendar
+            ) #@UndefinedVariable
         checkProperties()
 
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(self.notifierFactory.compare([("update", "home1")]))
+        self.assertEquals(self.notifierFactory.history, [("update", "home1")])
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = self.homeUnderTest()
         self.assertNotIdentical(home.calendarWithName(name), None)
-        home = self.calendarStore.newTransaction().calendarHomeWithUID(
-            "home1")
-        # Sanity check: are the properties actually persisted?
-        # FIXME: no independent testing of this right now
+
+        # Sanity check: are the properties actually persisted?  Check in
+        # subsequent transaction.
         checkProperties()
 
+        # FIXME: no independent testing of the property store's persistence
+        # right now
 
+
     def test_createCalendarWithName_exists(self):
         """
         L{ICalendarHome.createCalendarWithName} raises
@@ -427,8 +434,10 @@
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(self.notifierFactory.compare(
-            [("update", "home1"), ("update", "home1"), ("update", "home1")]))
+        self.assertEquals(
+            self.notifierFactory.history,
+            [("update", "home1"), ("update", "home1"), ("update", "home1")]
+        )
 
 
     def test_removeCalendarWithName_absent(self):
@@ -456,8 +465,8 @@
             )
 
         self.assertEquals(
-            list(o.name() for o in calendarObjects),
-            calendar1_objectNames
+            set(list(o.name() for o in calendarObjects)),
+            set(calendar1_objectNames)
         )
 
 
@@ -527,17 +536,16 @@
 
         # Make sure notifications are fired after commit
         self.commit()
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/calendar_1"),
-                    ("update", "home1"),
-                    ("update", "home1/calendar_1"),
-                    ("update", "home1"),
-                    ("update", "home1/calendar_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/calendar_1"),
+                ("update", "home1"),
+                ("update", "home1/calendar_1"),
+                ("update", "home1"),
+                ("update", "home1/calendar_1"),
+            ]
         )
 
     def test_removeCalendarObjectWithName_exists(self):
@@ -689,13 +697,12 @@
         self.commit()
 
         # Make sure notifications fire after commit
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/calendar_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/calendar_1"),
+            ]
         )
 
 
@@ -705,10 +712,12 @@
         L{CalendarObjectNameAlreadyExistsError} if a calendar object with the
         given name already exists in that calendar.
         """
+        cal = self.calendarUnderTest()
+        comp = VComponent.fromString(event4_text)
         self.assertRaises(
             ObjectResourceNameAlreadyExistsError,
-            self.calendarUnderTest().createCalendarObjectWithName,
-            "1.ics", VComponent.fromString(event4_text)
+            cal.createCalendarObjectWithName,
+            "1.ics", comp
         )
 
 
@@ -764,9 +773,13 @@
             create=True
         )
         def readOtherTxn():
-            return self.savedStore.newTransaction().calendarHomeWithUID(
-                noHomeUID)
+            otherTxn = self.savedStore.newTransaction(self.id() + "other txn")
+            self.addCleanup(otherTxn.commit)
+            return otherTxn.calendarHomeWithUID(noHomeUID)
         self.assertProvides(ICalendarHome, calendarHome)
+        # Default calendar should be automatically created.
+        self.assertProvides(ICalendar,
+                            calendarHome.calendarWithName("calendar"))
         # A concurrent transaction shouldn't be able to read it yet:
         self.assertIdentical(readOtherTxn(), None)
         self.commit()
@@ -795,13 +808,12 @@
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/calendar_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/calendar_1"),
+            ]
         )
 
 
@@ -930,7 +942,7 @@
     def test_dropboxID(self):
         """
         L{ICalendarObject.dropboxID} should synthesize its dropbox from the X
-        -APPLE-DROPBOX property.
+        -APPLE-DROPBOX property, if available.
         """
         cal = self.calendarUnderTest()
         cal.createCalendarObjectWithName("drop.ics", VComponent.fromString(
@@ -1087,10 +1099,7 @@
         return Notifier(self, label=label, id=id)
 
     def send(self, op, id):
-        self._history.append((op, id))
+        self.history.append((op, id))
 
     def reset(self):
-        self._history = []
-
-    def compare(self, expected):
-        return self._history == expected
+        self.history = []

Modified: CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/test_file.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -18,6 +18,10 @@
 File calendar store tests.
 """
 
+# FIXME: all test cases in this file aside from FileStorageTests should be
+# deleted and replaced with either implementation-specific methods on
+# FileStorageTests, or implementation-agnostic methods on CommonTests.
+
 from twext.python.filepath import CachingFilePath as FilePath
 from twisted.trial import unittest
 
@@ -33,7 +37,7 @@
 from txcaldav.calendarstore.file import Calendar, CalendarObject
 
 from txcaldav.calendarstore.test.common import (
-    CommonTests, event4_text, event1modified_text, StubNotifierFactory)
+    CommonTests, event4_text, event1modified_text)
 
 storePath = FilePath(__file__).parent().child("calendar_store")
 
@@ -58,7 +62,6 @@
     calendarPath.parent().makedirs()
     storePath.copyTo(calendarPath)
 
-    test.notifierFactory = StubNotifierFactory()
     test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory)
     test.txn = test.calendarStore.newTransaction()
     assert test.calendarStore is not None, "No calendar store?"
@@ -84,6 +87,8 @@
     Test cases for L{CalendarStore}.
     """
 
+    notifierFactory = None
+
     def setUp(self):
         setUpCalendarStore(self)
 
@@ -102,6 +107,7 @@
 
 class CalendarHomeTest(unittest.TestCase):
 
+    notifierFactory = None
     def setUp(self):
         setUpHome1(self)
 
@@ -158,6 +164,8 @@
 
 class CalendarTest(unittest.TestCase):
 
+    notifierFactory = None
+
     def setUp(self):
         setUpCalendar1(self)
 
@@ -400,6 +408,8 @@
 
 
 class CalendarObjectTest(unittest.TestCase):
+    notifierFactory = None
+
     def setUp(self):
         setUpCalendar1(self)
         self.object1 = self.calendar1.calendarObjectWithName("1.ics")

Copied: CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py)
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py	                        (rev 0)
+++ CalendarServer/trunk/txcaldav/calendarstore/test/test_postgres.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,242 @@
+##
+# 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, HomeChildNameAlreadyExistsError)
+
+from twisted.trial import unittest
+from txdav.datastore.subpostgres import (PostgresService,
+    DiagnosticConnectionWrapper)
+from txcaldav.calendarstore.postgres import PostgresStore, 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 = PostgresStore(
+                        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']
+        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 to appear unless it's
+                # explicitly listed.
+                try:
+                    home.removeCalendarWithName("calendar")
+                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/trunk/txcaldav/calendarstore/util.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py)
===================================================================
--- CalendarServer/trunk/txcaldav/calendarstore/util.py	                        (rev 0)
+++ CalendarServer/trunk/txcaldav/calendarstore/util.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,155 @@
+##
+# 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 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
+from twistedcaldav.customxml import GETCTag
+from uuid import uuid4
+from txdav.propertystore.base import PropertyName
+
+
+def validateCalendarComponent(calendarObject, calendar, component):
+    """
+    Validate a calendar component for a particular calendar.
+
+    @param calendarObject: The calendar object whose component will be replaced.
+    @type calendarObject: L{ICalendarObject}
+
+    @param calendar: The calendar which the L{ICalendarObject} is present in.
+    @type calendar: L{ICalendar}
+
+    @param component: The VComponent to be validated.
+    @type component: L{VComponent}
+    """
+
+    if not isinstance(component, VComponent):
+        raise TypeError(type(component))
+
+    try:
+        if component.resourceUID() != calendarObject.uid():
+            raise InvalidObjectResourceError(
+                "UID may not change (%s != %s)" % (
+                    component.resourceUID(), calendarObject.uid()
+                 )
+            )
+    except NoSuchObjectResourceError:
+        pass
+
+    try:
+        # FIXME: This is a bad way to do this test, there should be a
+        # Calendar-level API for it.
+        if calendar.name() == 'inbox':
+            component.validateComponentsForCalDAV(True)
+        else:
+            component.validateForCalDAV()
+    except InvalidICalendarDataError, e:
+        raise InvalidObjectResourceError(e)
+
+
+def dropboxIDFromCalendarObject(calendarObject):
+    """
+    Helper to implement L{ICalendarObject.dropboxID}.
+
+    @param calendarObject: The calendar object to retrieve a dropbox ID for.
+    @type calendarObject: L{ICalendarObject}
+    """
+    dropboxProperty = calendarObject.component(
+        ).getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
+    if dropboxProperty is not None:
+        componentDropboxID = dropboxProperty.value().split("/")[-1]
+        return componentDropboxID
+    attachProperty = calendarObject.component().getFirstPropertyInAnyComponent("ATTACH")
+    if attachProperty is not None:
+        # Make sure the value type is URI
+        valueType = attachProperty.params().get("VALUE", ("TEXT",))
+        if valueType[0] == "URI": 
+            # FIXME: more aggressive checking to see if this URI is really the
+            # 'right' URI.  Maybe needs to happen in the front end.
+            attachPath = attachProperty.value().split("/")[-2]
+            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 SyncTokenHelper(object):
+    """
+    Implement a basic _updateSyncToken in terms of an object with a property
+    store.  This is a mixin for use by data store implementations.
+    """
+
+    def _updateSyncToken(self, reset=False):
+        # FIXME: add locking a-la CalDAVResource.bumpSyncToken
+        # FIXME: tests for desired concurrency properties
+        ctag = PropertyName.fromString(GETCTag.sname())
+        props = self.properties()
+        token = props.get(ctag)
+        if token is None or reset:
+            tokenuuid = uuid4()
+            revision = 1
+        else:
+            # FIXME: no direct tests for update
+            token = str(token)
+            tokenuuid, revision = token.split("#", 1)
+            revision = int(revision) + 1
+        token = "%s#%d" % (tokenuuid, revision)
+        props[ctag] = GETCTag(token)
+        # FIXME: no direct tests for commit
+        return revision
+
+
+

Modified: CalendarServer/trunk/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/trunk/txcaldav/icalendarstore.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcaldav/icalendarstore.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -196,7 +196,7 @@
 
         @param name: a string.
         @param component: a C{VCALENDAR} L{Component}
-        @raise CalendarObjectNameAlreadyExistsError: if a calendar
+        @raise ObjectResourceNameAlreadyExistsError: if a calendar
             object with the given C{name} already exists.
         @raise CalendarObjectUIDAlreadyExistsError: if a calendar
             object with the same UID as the given C{component} already
@@ -265,6 +265,12 @@
     object.
     """
 
+    def calendar():
+        """
+        @return: The calendar which this calendar object is a part of.
+        @rtype: L{ICalendar}
+        """
+
     def setComponent(component):
         """
         Rewrite this calendar object to match the given C{component}.
@@ -375,7 +381,15 @@
         """
 
 
+    def attendeesCanManageAttachments():
+        """
+        Are attendees allowed to manage attachments?
 
+        @return: C{True} if they can, C{False} if they can't.
+        """
+
+
+
 class IAttachment(IDataStoreResource):
     """
     Information associated with an attachment to a calendar object.

Modified: CalendarServer/trunk/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/trunk/txcarddav/addressbookstore/file.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/addressbookstore/file.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -152,6 +152,11 @@
     def _addressbook(self):
         return self._parentCollection
 
+
+    def addressbook(self):
+        return self._addressbook
+
+
     @writeOperation
     def setComponent(self, component):
         if not isinstance(component, VComponent):

Modified: CalendarServer/trunk/txcarddav/addressbookstore/test/common.py
===================================================================
--- CalendarServer/trunk/txcarddav/addressbookstore/test/common.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/addressbookstore/test/common.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,4 +1,4 @@
-# -*- test-case-name: txcarddav.addressbookstore -*-
+# -*- test-case-name: txcarddav.addressbookstore,txcaldav.calendarstore.test.test_postgres.AddressBookSQLStorageTests -*-
 ##
 # Copyright (c) 2010 Apple Inc. All rights reserved.
 #
@@ -103,7 +103,23 @@
 )
 
 
+def assertProvides(testCase, interface, provider):
+    """
+    Verify that C{provider} properly provides C{interface}
 
+    @type interface: L{zope.interface.Interface}
+    @type provider: C{provider}
+    """
+    try:
+        verifyObject(interface, provider)
+    except BrokenMethodImplementation, e:
+        testCase.fail(e)
+    except DoesNotImplement, e:
+        testCase.fail("%r does not provide %s.%s" %
+                      (provider, interface.__module__, interface.getName()))
+
+
+
 class CommonTests(object):
     """
     Tests for common functionality of interfaces defined in
@@ -117,6 +133,7 @@
                 "2.vcf": adbk1Root.child("2.vcf").getContent(),
                 "3.vcf": adbk1Root.child("3.vcf").getContent()
             },
+            "addressbook_2": {},
             "addressbook_empty": {},
             "not_a_addressbook": None
         },
@@ -148,7 +165,7 @@
             return self.lastTransaction
         if self.savedStore is None:
             self.savedStore = self.storeUnderTest()
-        txn = self.lastTransaction = self.savedStore.newTransaction()
+        txn = self.lastTransaction = self.savedStore.newTransaction(self.id())
         return txn
 
 
@@ -169,7 +186,15 @@
         self.lastTransaction.abort()
         self.lastTransaction = None
 
+    def setUp(self):
+        self.notifierFactory = StubNotifierFactory()
 
+    def tearDown(self):
+        if self.lastTransaction is not None:
+            self.commit()
+
+
+
     def homeUnderTest(self):
         """
         Get the addressbook home detailed by C{requirements['home1']}.
@@ -192,22 +217,9 @@
         return self.addressbookUnderTest().addressbookObjectWithName("1.vcf")
 
 
-    def assertProvides(self, interface, provider):
-        """
-        Verify that C{provider} properly provides C{interface}
+    assertProvides = assertProvides
 
-        @type interface: L{zope.interface.Interface}
-        @type provider: C{provider}
-        """
-        try:
-            verifyObject(interface, provider)
-        except BrokenMethodImplementation, e:
-            self.fail(e)
-        except DoesNotImplement, e:
-            self.fail("%r does not provide %s.%s" %
-                (provider, interface.__module__, interface.getName()))
 
-
     def test_addressbookStoreProvides(self):
         """
         The addressbook store provides L{IAddressBookStore} and its required
@@ -222,7 +234,7 @@
         The transactions generated by the addressbook store provide
         L{IAddressBookStoreTransaction} and its required attributes.
         """
-        txn = self.storeUnderTest().newTransaction()
+        txn = self.transactionUnderTest()
         self.assertProvides(ICommonTransaction, txn)
         self.assertProvides(IAddressBookTransaction, txn)
 
@@ -257,9 +269,8 @@
         provides L{IAddressBookHome} and has a C{uid()} method that returns the
         same value that was passed in.
         """
-        addressbookHome = (self.storeUnderTest().newTransaction()
+        addressbookHome = (self.transactionUnderTest()
                         .addressbookHomeWithUID("home1"))
-
         self.assertEquals(addressbookHome.uid(), "home1")
         self.assertProvides(IAddressBookHome, addressbookHome)
 
@@ -269,11 +280,8 @@
         L{IAddressBookStoreTransaction.addressbookHomeWithUID} should return C{None}
         when asked for a non-existent addressbook home.
         """
-        self.assertEquals(
-            self.storeUnderTest().newTransaction()
-            .addressbookHomeWithUID("xyzzy"),
-            None
-        )
+        txn = self.transactionUnderTest()
+        self.assertEquals(txn.addressbookHomeWithUID("xyzzy"), None)
 
 
     def test_addressbookWithName_exists(self):
@@ -284,6 +292,8 @@
         home = self.homeUnderTest()
         for name in home1_addressbookNames:
             addressbook = home.addressbookWithName(name)
+            if addressbook is None:
+                self.fail("addressbook %r didn't exist" % (name,))
             self.assertProvides(IAddressBook, addressbook)
             self.assertEquals(addressbook.name(), name)
 
@@ -334,18 +344,22 @@
                 addressbookProperties[
                     PropertyName.fromString(davxml.ResourceType.sname())
                 ],
-                davxml.ResourceType.addressbook) #@UndefinedVariable
+                davxml.ResourceType.addressbook
+            ) #@UndefinedVariable
         checkProperties()
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(self.notifierFactory.compare([("update", "home1")]))
+        self.assertEquals(self.notifierFactory.history, [("update", "home1")])
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = self.homeUnderTest()
         self.assertNotIdentical(home.addressbookWithName(name), None)
-        home = self.addressbookStore.newTransaction().addressbookHomeWithUID(
-            "home1")
+
+        # FIXME: These two lines aren't in the calendar common tests:
+        # home = self.addressbookStore.newTransaction().addressbookHomeWithUID(
+        #     "home1")
+
         # Sanity check: are the properties actually persisted?
         # FIXME: no independent testing of this right now
         checkProperties()
@@ -379,8 +393,10 @@
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(self.notifierFactory.compare(
-            [("update", "home1"), ("update", "home1"), ("update", "home1")]))
+        self.assertEquals(
+            self.notifierFactory.history,
+            [("update", "home1"), ("update", "home1"), ("update", "home1")]
+        )
 
 
     def test_removeAddressBookWithName_absent(self):
@@ -408,8 +424,8 @@
             )
 
         self.assertEquals(
-            list(o.name() for o in addressbookObjects),
-            addressbook1_objectNames
+            set(o.name() for o in addressbookObjects),
+            set(addressbook1_objectNames)
         )
 
 
@@ -494,17 +510,16 @@
 
         # Make sure notifications are fired after commit
         self.commit()
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/addressbook_1"),
-                    ("update", "home1"),
-                    ("update", "home1/addressbook_1"),
-                    ("update", "home1"),
-                    ("update", "home1/addressbook_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/addressbook_1"),
+                ("update", "home1"),
+                ("update", "home1/addressbook_1"),
+                ("update", "home1"),
+                ("update", "home1/addressbook_1"),
+            ]
         )
 
 
@@ -595,8 +610,8 @@
                               home.addressbookWithName(addressbook.name()))
 
         self.assertEquals(
-            list(c.name() for c in addressbooks),
-            home1_addressbookNames
+            set(c.name() for c in addressbooks),
+            set(home1_addressbookNames)
         )
 
 
@@ -629,13 +644,12 @@
         self.commit()
 
         # Make sure notifications fire after commit
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/addressbook_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/addressbook_1"),
+            ]
         )
 
 
@@ -704,12 +718,13 @@
             create=True
         )
         def readOtherTxn():
-            return self.savedStore.newTransaction().addressbookHomeWithUID(
-                noHomeUID)
+            otherTxn = self.savedStore.newTransaction()
+            self.addCleanup(otherTxn.commit)
+            return otherTxn.addressbookHomeWithUID(noHomeUID)
         self.assertProvides(IAddressBookHome, addressbookHome)
         # A concurrent transaction shouldn't be able to read it yet:
         self.assertIdentical(readOtherTxn(), None)
-        txn.commit()
+        self.commit()
         # But once it's committed, other transactions should see it.
         self.assertProvides(IAddressBookHome, readOtherTxn())
 
@@ -735,13 +750,12 @@
         self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue(
-            self.notifierFactory.compare(
-                [
-                    ("update", "home1"),
-                    ("update", "home1/addressbook_1"),
-                ]
-            )
+        self.assertEquals(
+            self.notifierFactory.history,
+            [
+                ("update", "home1"),
+                ("update", "home1/addressbook_1"),
+            ]
         )
 
     def checkPropertiesMethod(self, thunk):
@@ -841,10 +855,7 @@
         return Notifier(self, label=label, id=id)
 
     def send(self, op, id):
-        self._history.append((op, id))
+        self.history.append((op, id))
 
     def reset(self):
-        self._history = []
-
-    def compare(self, expected):
-        return self._history == expected
+        self.history = []

Modified: CalendarServer/trunk/txcarddav/iaddressbookstore.py
===================================================================
--- CalendarServer/trunk/txcarddav/iaddressbookstore.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txcarddav/iaddressbookstore.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,3 +1,4 @@
+# -*- test-case-name: txcarddav.addressbookstore,txcaldav.calendarstore.test.test_postgres.AddressBookSQLStorageTests -*-
 ##
 # Copyright (c) 2010 Apple Inc. All rights reserved.
 #
@@ -222,6 +223,13 @@
     An addressbook object describes a contact (vCard).
     """
 
+    def addressbook():
+        """
+        @return: The address book which this address book object is a part of.
+        @rtype: L{IAddressBook}
+        """
+
+
     def setComponent(component):
         """
         Rewrite this addressbook object to match the given C{component}.

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -14,6 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from txcaldav.calendarstore.util import SyncTokenHelper
 
 """
 Common utility functions for a file based datastore.
@@ -26,7 +27,7 @@
 from twisted.python.util import FancyEqMixin
 
 from twistedcaldav import customxml
-from twistedcaldav.customxml import GETCTag, NotificationType
+from twistedcaldav.customxml import NotificationType
 from twistedcaldav.notifications import NotificationRecord
 from twistedcaldav.notifications import NotificationsDatabase as OldNotificationIndex
 from twistedcaldav.sharing import SharedCollectionsDatabase
@@ -44,7 +45,6 @@
 from txdav.propertystore.xattr import PropertyStore
 
 from errno import EEXIST, ENOENT
-from uuid import uuid4
 from zope.interface import implements, directlyProvides
 
 ECALENDARTYPE = 0
@@ -475,8 +475,10 @@
             return None
 
 
-class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin):
+class CommonHomeChild(FileMetaDataMixin, LoggingMixIn, FancyEqMixin,
+                      SyncTokenHelper):
     """
+    Common ancestor class of AddressBooks and Calendars.
     """
 
     compareAttributes = '_name _home _transaction'.split()
@@ -678,26 +680,6 @@
         raise NotImplementedError()
 
 
-    def _updateSyncToken(self, reset=False):
-        # FIXME: add locking a-la CalDAVResource.bumpSyncToken
-        # FIXME: tests for desired concurrency properties
-        ctag = PropertyName.fromString(GETCTag.sname())
-        props = self.properties()
-        token = props.get(ctag)
-        if token is None or reset:
-            tokenuuid = uuid4()
-            revision = 1
-        else:
-            # FIXME: no direct tests for update
-            token = str(token)
-            tokenuuid, revision = token.split("#", 1)
-            revision = int(revision) + 1
-        token = "%s#%d" % (tokenuuid, revision)
-        props[ctag] = GETCTag(token)
-        # FIXME: no direct tests for commit
-        return revision
-
-
     def objectResourcesSinceToken(self, token):
         raise NotImplementedError()
 

Modified: CalendarServer/trunk/txdav/common/inotifications.py
===================================================================
--- CalendarServer/trunk/txdav/common/inotifications.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/common/inotifications.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -78,9 +78,8 @@
         given C{xmltype}. Create or overwrite are OK.
 
         @param uid: a string.
-        @param xmltype: a string.
+        @param xmltype: a custom XML class (not an instance)
         @param xmldata: a string.
-        @param component: a C{VCARD} L{Component}
         """
 
     def removeNotificationObjectWithName(name):

Copied: CalendarServer/trunk/txdav/datastore/subpostgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/subpostgres.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/datastore/subpostgres.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,445 @@
+# -*- test-case-name: txdav.datastore.test.test_subpostgres -*-
+##
+# 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.
+##
+
+"""
+Run and manage PostgreSQL as a subprocess.
+"""
+import os
+import pwd
+from hashlib import md5
+
+from twisted.python.procutils import which
+from twisted.internet.protocol import ProcessProtocol
+from twisted.internet.error import ProcessDone
+from twisted.python.reflect import namedAny
+from twisted.python import log
+from twext.python.filepath import CachingFilePath
+
+
+pgdb = namedAny("pgdb")
+from pg import DatabaseError
+
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+
+from twisted.application.service import MultiService
+
+
+# This appears in the postgres log to indicate that it is accepting
+# connections.
+_MAGIC_READY_COOKIE = "database system is ready to accept connections"
+
+
+class DiagnosticCursorWrapper(object):
+    """
+    Diagnostic wrapper around a DB-API 2.0 cursor for debugging connection
+    status.
+    """
+
+    def __init__(self, realCursor, connectionWrapper):
+        self.realCursor = realCursor
+        self.connectionWrapper = connectionWrapper
+
+
+    @property
+    def rowcount(self):
+        return self.realCursor.rowcount
+
+
+    @property
+    def description(self):
+        return self.realCursor.description
+
+
+    def execute(self, sql, args=()):
+        self.connectionWrapper.state = 'executing %r' % (sql,)
+        self.realCursor.execute(sql, args)
+
+
+    def close(self):
+        self.realCursor.close()
+
+
+    def fetchall(self):
+        return self.realCursor.fetchall()
+
+
+
+class DiagnosticConnectionWrapper(object):
+    """
+    Diagnostic wrapper around a DB-API 2.0 connection for debugging connection
+    status.
+    """
+
+    def __init__(self, realConnection, label):
+        self.realConnection = realConnection
+        self.label = label
+        self.state = 'idle (start)'
+
+
+    def cursor(self):
+        return DiagnosticCursorWrapper(self.realConnection.cursor(), self)
+
+
+    def close(self):
+        self.realConnection.close()
+        self.state = 'closed'
+
+
+    def commit(self):
+        self.realConnection.commit()
+        self.state = 'idle (after commit)'
+
+
+    def rollback(self):
+        self.realConnection.rollback()
+        self.state = 'idle (after rollback)'
+
+
+
+class _PostgresMonitor(ProcessProtocol):
+    """
+    A monitoring protocol which watches the postgres subprocess.
+    """
+
+    def __init__(self, svc=None):
+        self.lineReceiver = LineReceiver()
+        self.lineReceiver.delimiter = '\n'
+        self.lineReceiver.lineReceived = self.lineReceived
+        self.svc = svc
+        self.isReady = False
+        self.completionDeferred = Deferred()
+
+
+    def lineReceived(self, line):
+        if self.svc is None:
+            return
+        if not self.isReady:
+            if _MAGIC_READY_COOKIE in line:
+                self.svc.ready()
+
+
+    disconnecting = False
+    def connectionMade(self):
+        self.lineReceiver.makeConnection(self)
+
+
+    def outReceived(self, out):
+        log.msg("received postgres stdout %r" % (out,))
+        # self.lineReceiver.dataReceived(out)
+
+
+    def errReceived(self, err):
+        log.msg("received postgres stderr %r" % (err,))
+        self.lineReceiver.dataReceived(err)
+
+
+    def processEnded(self, reason):
+        log.msg("postgres process ended %r" % (reason,))
+        self.lineReceiver.connectionLost(reason)
+        self.completionDeferred.callback(None)
+
+
+class ErrorOutput(Exception):
+    """
+    The process produced some error output and exited with a non-zero exit
+    code.
+    """
+
+
+class CapturingProcessProtocol(ProcessProtocol):
+    """
+    A L{ProcessProtocol} that captures its output and error.
+
+    @ivar output: a C{list} of all C{str}s received to stderr.
+
+    @ivar error: a C{list} of all C{str}s received to stderr.
+    """
+
+    def __init__(self, deferred, inputData):
+        """
+        Initialize a L{CapturingProcessProtocol}.
+
+        @param deferred: the L{Deferred} to fire when the process is complete.
+
+        @param inputData: a C{str} to feed to the subprocess's stdin.
+        """
+        self.deferred = deferred
+        self.input = inputData
+        self.output = []
+        self.error = []
+
+
+    def connectionMade(self):
+        """
+        The process started; feed its input on stdin.
+        """
+        if self.input is not None:
+            self.transport.write(self.input)
+            self.transport.closeStdin()
+
+
+    def outReceived(self, data):
+        """
+        Some output was received on stdout.
+        """
+        self.output.append(data)
+
+    def errReceived(self, data):
+        """
+        Some output was received on stderr.
+        """
+        self.output.append(data)
+
+
+    def processEnded(self, why):
+        """
+        The process is over, fire the Deferred with the output.
+        """
+        self.deferred.callback(''.join(self.output))
+
+
+class PostgresService(MultiService):
+
+    def __init__(self, dataStoreDirectory, subServiceFactory,
+                 schema, databaseName='subpostgres', resetSchema=False,
+                 logFile="postgres.log", testMode=False,
+                 uid=None, gid=None):
+        """
+        Initialize a L{PostgresService} pointed at a data store directory.
+
+        @param dataStoreDirectory: the directory to
+        @type dataStoreDirectory: L{twext.python.filepath.CachingFilePath}
+
+        @param subServiceFactory: a 1-arg callable that will be called with a
+            1-arg callable which returns a DB-API cursor.
+        @type subServiceFactory: C{callable}
+        """
+        MultiService.__init__(self)
+        self.subServiceFactory = subServiceFactory
+        self.dataStoreDirectory = dataStoreDirectory
+        self.resetSchema = resetSchema
+        self.socketDir = CachingFilePath("/tmp/ccs_postgres_%s/" %
+            (md5(dataStoreDirectory.path).hexdigest()))
+        self.databaseName = databaseName
+        self.logFile = logFile
+        self.uid = uid
+        self.gid = gid
+        self.schema = schema
+        self.monitor = None
+        self.openConnections = []
+
+        # FIXME: By default there is very little (4MB) shared memory available,
+        # so at the moment I am lowering these postgres config options to allow
+        # multiple servers to run.  We might want to look into raising
+        # kern.sysv.shmmax.
+        # See: http://www.postgresql.org/docs/8.4/static/kernel-resources.html
+        if testMode:
+            self.sharedBuffers = 16
+            self.maxConnections = 2
+        else:
+            self.sharedBuffers = 30
+            self.maxConnections = 20
+
+
+    def produceConnection(self, label="<unlabeled>", databaseName=None):
+        """
+        Produce a DB-API 2.0 connection pointed at this database.
+        """
+        if databaseName is None:
+            databaseName = self.databaseName
+
+        if self.uid is not None:
+            dsn = "%s:dbname=%s:%s" % (self.socketDir.path, databaseName,
+                pwd.getpwuid(self.uid).pw_name)
+        else:
+            dsn = "%s:dbname=%s" % (self.socketDir.path, databaseName)
+        connection = pgdb.connect(dsn)
+
+        w = DiagnosticConnectionWrapper(connection, label)
+        c = w.cursor()
+        # Turn on standard conforming strings.  This option is _required_ if
+        # you want to get correct behavior out of parameter-passing with the
+        # pgdb module.  If it is not set then the server is potentially
+        # vulnerable to certain types of SQL injection.
+        c.execute("set standard_conforming_strings=on")
+
+        # Abort any second that takes more than 2 seconds (2000ms) to execute.
+        # This is necessary as a temporary workaround since it's hypothetically
+        # possible that different database operations could block each other,
+        # while executing SQL in the same process (in the same thread, since
+        # SQL executes in the main thread now).  It's preferable to see some
+        # exceptions while we're in this state than to have the entire worker
+        # process hang.
+        c.execute("set statement_timeout=2000")
+        w.commit()
+        c.close()
+        return w
+
+
+    def ready(self):
+        """
+        Subprocess is ready.  Time to initialize the subservice.
+        """
+        createDatabaseConn = self.produceConnection(
+            'schema creation', 'postgres'
+        )
+        createDatabaseCursor = createDatabaseConn.cursor()
+        createDatabaseCursor.execute("commit")
+
+        if self.resetSchema:
+            try:
+                createDatabaseCursor.execute(
+                    "drop database %s" % (self.databaseName)
+                )
+            except DatabaseError:
+                pass
+
+        try:
+            createDatabaseCursor.execute(
+                "create database %s" % (self.databaseName)
+            )
+        except:
+            execSchema = False
+        else:
+            execSchema = True
+
+        createDatabaseCursor.close()
+        createDatabaseConn.close()
+
+        if execSchema:
+            connection = self.produceConnection()
+            cursor = connection.cursor()
+            cursor.execute(self.schema)
+            connection.commit()
+            connection.close()
+
+        connection = self.produceConnection()
+        cursor = connection.cursor()
+
+        self.subServiceFactory(self.produceConnection).setServiceParent(self)
+
+
+    def pauseMonitor(self):
+        """
+        Pause monitoring.  This is a testing hook for when (if) we are
+        continuously monitoring output from the 'postgres' process.
+        """
+#        for pipe in self.monitor.transport.pipes.values():
+#            pipe.stopReading()
+#            pipe.stopWriting()
+
+
+    def unpauseMonitor(self):
+        """
+        Unpause monitoring.
+        
+        @see: L{pauseMonitor} 
+        """
+#        for pipe in self.monitor.transport.pipes.values():
+#            pipe.startReading()
+#            pipe.startWriting()
+
+
+    def startDatabase(self):
+        """
+        Start the database and initialize the subservice.
+        """
+        monitor = _PostgresMonitor(self)
+        pg_ctl = which("pg_ctl")[0]
+        # check consistency of initdb and postgres?
+        reactor.spawnProcess(
+            monitor, pg_ctl,
+            [
+                pg_ctl,
+                "start",
+                "-l", self.logFile,
+                "-w",
+                # XXX what are the quoting rules for '-o'?  do I need to repr()
+                # the path here?
+                "-o", "-c listen_addresses='' -k '%s' -c standard_conforming_strings=on -c shared_buffers=%d -c max_connections=%d"
+                    % (self.socketDir.path, self.sharedBuffers, self.maxConnections),
+            ],
+            self.env,
+            uid=self.uid, gid=self.gid,
+        )
+        self.monitor = monitor
+        def gotReady(result):
+            self.ready()
+        def reportit(f):
+            log.err(f)
+        self.monitor.completionDeferred.addCallback(
+            gotReady).addErrback(reportit)
+
+
+    def startService(self):
+        MultiService.startService(self)
+        clusterDir = self.dataStoreDirectory.child("cluster")
+        workingDir = self.dataStoreDirectory.child("working")
+        env = self.env = os.environ.copy()
+        env.update(PGDATA=clusterDir.path,
+                   PGHOST=self.socketDir.path)
+        initdb = which("initdb")[0]
+        if not self.socketDir.isdir():
+            self.socketDir.createDirectory()
+        if self.uid and self.gid:
+            os.chown(self.socketDir.path, self.uid, self.gid)
+        if self.dataStoreDirectory.isdir():
+            self.startDatabase()
+        else:
+            self.dataStoreDirectory.createDirectory()
+            workingDir.createDirectory()
+            if self.uid and self.gid:
+                os.chown(self.dataStoreDirectory.path, self.uid, self.gid)
+                os.chown(workingDir.path, self.uid, self.gid)
+            dbInited = Deferred()
+            reactor.spawnProcess(
+                CapturingProcessProtocol(dbInited, None),
+                initdb, [initdb], env, workingDir.path,
+                uid=self.uid, gid=self.gid,
+            )
+            def doCreate(result):
+                self.startDatabase()
+            dbInited.addCallback(doCreate)
+
+
+    def stopService(self):
+        """
+        Stop all child services, then stop the subprocess, if it's running.
+        """
+        d = MultiService.stopService(self)
+        def superStopped(result):
+            # Probably want to stop and wait for startup if that hasn't
+            # completed yet...
+            monitor = _PostgresMonitor()
+            pg_ctl = which("pg_ctl")[0]
+            reactor.spawnProcess(monitor, pg_ctl,
+                [pg_ctl, '-l', 'logfile', 'stop'],
+                self.env,
+                uid=self.uid, gid=self.gid,
+            )
+            return monitor.completionDeferred
+        return d.addCallback(superStopped)
+
+#        def maybeStopSubprocess(result):
+#            if self.monitor is not None:
+#                self.monitor.transport.signalProcess("INT")
+#                return self.monitor.completionDeferred
+#            return result
+#        d.addCallback(maybeStopSubprocess)
+#        return d

Deleted: CalendarServer/trunk/txdav/datastore/test/__init__.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/datastore/test/__init__.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,16 +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.
-##
-

Copied: CalendarServer/trunk/txdav/datastore/test/__init__.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/datastore/test/__init__.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,16 @@
+##
+# 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.
+##
+

Deleted: CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -1,86 +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 txdav.datastore.subpostgres.
-"""
-
-from twisted.trial.unittest import TestCase
-
-from twext.python.filepath import CachingFilePath
-
-from txdav.datastore.subpostgres import PostgresService
-from twisted.internet.defer import inlineCallbacks, Deferred
-from twisted.application.service import Service
-
-class SubprocessStartup(TestCase):
-    """
-    Tests for starting and stopping the subprocess.
-    """
-
-    @inlineCallbacks
-    def test_startService(self):
-        """
-        Assuming a properly configured environment ($PATH points at an 'initdb'
-        and 'postgres', $PYTHONPATH includes pgdb), starting a
-        L{PostgresService} will start the service passed to it, after executing
-        the schema.
-        """
-
-        test = self
-        class SimpleService(Service):
-
-            instances = []
-            rows = []
-            ready = Deferred()
-
-            def __init__(self, connectionFactory):
-                self.connection = connectionFactory()
-                test.addCleanup(self.connection.close)
-                self.instances.append(self)
-
-
-            def startService(self):
-                cursor = self.connection.cursor()
-                try:
-                    cursor.execute(
-                        "insert into test_dummy_table values ('dummy')"
-                    )
-                except:
-                    self.ready.errback()
-                else:
-                    self.ready.callback(None)
-                finally:
-                    cursor.close()
-
-
-        dbPath = "../_postgres_test_db"
-        svc = PostgresService(
-            CachingFilePath(dbPath),
-            SimpleService,
-            "create table TEST_DUMMY_TABLE (stub varchar)",
-            "dummy_db",
-            testMode=True
-        )
-
-        svc.startService()
-        self.addCleanup(svc.stopService)
-        yield SimpleService.ready
-        connection = SimpleService.instances[0].connection
-        cursor = connection.cursor()
-        cursor.execute("select * from test_dummy_table")
-        values = cursor.fetchall()
-        self.assertEquals(values, [["dummy"]])

Copied: CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py (from rev 6073, CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py)
===================================================================
--- CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/datastore/test/test_subpostgres.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -0,0 +1,86 @@
+##
+# 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 txdav.datastore.subpostgres.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twext.python.filepath import CachingFilePath
+
+from txdav.datastore.subpostgres import PostgresService
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.application.service import Service
+
+class SubprocessStartup(TestCase):
+    """
+    Tests for starting and stopping the subprocess.
+    """
+
+    @inlineCallbacks
+    def test_startService(self):
+        """
+        Assuming a properly configured environment ($PATH points at an 'initdb'
+        and 'postgres', $PYTHONPATH includes pgdb), starting a
+        L{PostgresService} will start the service passed to it, after executing
+        the schema.
+        """
+
+        test = self
+        class SimpleService(Service):
+
+            instances = []
+            rows = []
+            ready = Deferred()
+
+            def __init__(self, connectionFactory):
+                self.connection = connectionFactory()
+                test.addCleanup(self.connection.close)
+                self.instances.append(self)
+
+
+            def startService(self):
+                cursor = self.connection.cursor()
+                try:
+                    cursor.execute(
+                        "insert into test_dummy_table values ('dummy')"
+                    )
+                except:
+                    self.ready.errback()
+                else:
+                    self.ready.callback(None)
+                finally:
+                    cursor.close()
+
+
+        dbPath = "../_postgres_test_db"
+        svc = PostgresService(
+            CachingFilePath(dbPath),
+            SimpleService,
+            "create table TEST_DUMMY_TABLE (stub varchar)",
+            "dummy_db",
+            testMode=True
+        )
+
+        svc.startService()
+        self.addCleanup(svc.stopService)
+        yield SimpleService.ready
+        connection = SimpleService.instances[0].connection
+        cursor = connection.cursor()
+        cursor.execute("select * from test_dummy_table")
+        values = cursor.fetchall()
+        self.assertEquals(values, [["dummy"]])

Modified: CalendarServer/trunk/txdav/idav.py
===================================================================
--- CalendarServer/trunk/txdav/idav.py	2010-08-12 02:22:44 UTC (rev 6073)
+++ CalendarServer/trunk/txdav/idav.py	2010-08-12 02:36:27 UTC (rev 6074)
@@ -68,7 +68,7 @@
     Property name.
     """
     namespace = Attribute("Namespace")
-    name      = Attribute("Name")
+    name = Attribute("Name")
 
     def toString():
         """
@@ -92,26 +92,20 @@
     # FIXME: the type for values isn't quite right, there should be some more
     # specific interface for that.
 
-    def flush():
-        """
-        Write out any pending changes.
-        """
 
-    def abort():
-        """
-        Abort any pending changes.
-        """
 
-
-
 class IDataStore(Interface):
     """
     An L{IDataStore} is a storage of some objects.
     """
 
-    def newTransaction():
+    def newTransaction(label=None):
         """
         Create a new transaction.
+        
+        @param label: A label to assign to this transaction for diagnostic
+            purposes.
+        @type label: C{str}
 
         @return: a new transaction which provides L{ITransaction}, as well as
             sub-interfaces to request appropriate data objects.
@@ -124,7 +118,7 @@
     """
     An L{IDataStoreResource} are the objects stored in an L{IDataStore}.
     """
-    
+
     def name():
         """
         Identify the name of the object
@@ -175,7 +169,9 @@
 
         @return: an L{IPropertyStore}.
         """
-    
+
+
+
 class ITransaction(Interface):
     """
     Transaction that can be aborted and either succeeds or fails in
@@ -211,6 +207,16 @@
         @param operation: a callable.
         """
 
+
+    def store():
+        """
+        The store that this transaction was initiated from.
+
+        @rtype: L{IDataStore}
+        """
+
+
+
 class INotifier(Interface):
     """
     Push notification interface
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100811/09001aa4/attachment-0001.html>


More information about the calendarserver-changes mailing list