[CalendarServer-changes] [5930] CalendarServer/branches/users/glyph/sql-store

source_changes at macosforge.org source_changes at macosforge.org
Fri Jul 23 12:15:12 PDT 2010


Revision: 5930
          http://trac.macosforge.org/projects/calendarserver/changeset/5930
Author:   glyph at apple.com
Date:     2010-07-23 12:15:12 -0700 (Fri, 23 Jul 2010)
Log Message:
-----------
checkpoint; enough test scaffolding to see that it doesn't quite work yet

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/sql-store/support/build.sh
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/common.py
    CalendarServer/branches/users/glyph/sql-store/txcaldav/icalendarstore.py
    CalendarServer/branches/users/glyph/sql-store/txdav/idav.py

Added Paths:
-----------
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py
    CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py
    CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/
    CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py
    CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py

Modified: CalendarServer/branches/users/glyph/sql-store/support/build.sh
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/support/build.sh	2010-07-23 19:11:12 UTC (rev 5929)
+++ CalendarServer/branches/users/glyph/sql-store/support/build.sh	2010-07-23 19:15:12 UTC (rev 5930)
@@ -193,17 +193,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}" ]; then
-	echo "Checking MD5 sum for ${name}...";
-	local sum="$(md5 "${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="$(md5 "${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...";
@@ -284,13 +284,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/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py	2010-07-23 19:11:12 UTC (rev 5929)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -27,6 +27,412 @@
 ]
 
 from twisted.python.modules import getModule
+from twisted.application.service import Service
+from txdav.idav import IDataStore
+from zope.interface.declarations import implements
+from txcaldav.icalendarstore import ICalendarTransaction, ICalendarHome, \
+    ICalendar, ICalendarObject
+from txdav.propertystore.base import AbstractPropertyStore, PropertyName
+from twext.web2.dav.element.parser import WebDAVDocument
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError
+from txdav.propertystore.none import PropertyStore
 
+from twext.python.vcomponent import VComponent
 
-v1_schema = getModule(__name__).filePath.sibling("postgres_schema_v1.sql").getContent()
+
+v1_schema = getModule(__name__).filePath.sibling(
+    "postgres_schema_v1.sql").getContent()
+
+
+# these are in the schema, and should probably be discovered from there
+# somehow.
+
+_BIND_STATUS_ACCEPTED = 1
+_BIND_STATUS_DECLINED = 2
+
+_ATTACHMENTS_MODE_WRITE = 1
+
+_BIND_MODE_OWN = 0
+
+
+class PropertyStore(AbstractPropertyStore):
+    """
+    
+    """
+
+    def __init__(self, cursor, connection, resourceID):
+        self._cursor = cursor
+        self._connection = connection
+        self._resourceID = resourceID
+
+    def _getitem_uid(self, key, uid):
+        self._cursor.execute(
+            "select VALUE from RESOURCE_PROPERTY where "
+            "NAME = %s and VIEWER_UID = %s",
+            [key.toString(), uid])
+        rows = self._cursor.fetchall()
+        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._cursor.execute(
+            "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._cursor.execute(
+            "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):
+        self._cursor.execute(
+            "select NAME from RESOURCE_PROPERTY where "
+            "VIEWER_UID = %s and RESOURCE_ID = %s",
+            [uid, self._resourceID]
+        )
+        for row in self._cursor.fetchall():
+            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
+
+
+    def uid(self):
+        return self.component().resourceUID()
+    
+    
+    def dropboxID(self):
+        return self.uid() + ".dropbox"
+
+
+    def name(self):
+        return self._name
+
+
+    def iCalendarText(self):
+        c = self._calendar._cursor()
+        c.execute("select ICALENDAR_TEXT from CALENDAR_OBJECT where "
+                  "RESOURCE_ID = %s", [self._resourceID])
+        return c.fetchall()[0][0]
+
+
+    def component(self):
+        return VComponent.fromString(self.iCalendarText())
+
+
+    def componentType(self):
+        return self.component().mainType()
+
+
+    def properties(self):
+        return PropertyStore(self._calendar._cursor(),
+            self._calendar._home._txn._connection,
+            self._resourceID)
+
+
+    def setComponent(self, component):
+        self._calendar._cursor().execute(
+            "update CALENDAR_OBJECT set ICALENDAR_TEXT = %s "
+            "where RESOURCE_ID = %s", [str(component), self._resourceID]
+        )
+
+
+    def createAttachmentWithName(self, name, contentType):
+        pass
+
+
+    def attachments(self):
+        return []
+
+
+    def attachmentWithName(self, name):
+        return None
+
+
+    def removeAttachmentWithName(self, name):
+        pass
+
+
+
+class PostgresCalendar(object):
+    """
+    
+    """
+
+    implements(ICalendar)
+
+
+    def __init__(self, home, name, resourceID):
+        self._home = home
+        self._name = name
+        self._resourceID = resourceID
+
+
+    def _cursor(self):
+        return self._home._txn._cursor
+
+
+    def name(self):
+        return self._name
+
+    def rename(self, name):
+        raise NotImplementedError()
+
+    def ownerCalendarHome(self):
+        return self._home
+
+
+    def calendarObjects(self):
+        c = self._cursor()
+        c.execute(
+            "select RESOURCE_NAME from "
+            "CALENDAR_OBJECT where "
+            "CALENDAR_RESOURCE_ID = %s",
+            [self._resourceID])
+        for row in c.fetchall():
+            name = row[0]
+            yield self.calendarObjectWithName(name)
+
+
+    def calendarObjectWithName(self, name):
+        c = self._cursor()
+        c.execute("select RESOURCE_ID from CALENDAR_OBJECT where "
+                  "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
+                  [name, self._resourceID])
+        rows = c.fetchall()
+        if not rows:
+            return None
+        resid = rows[0][0]
+        return PostgresCalendarObject(self, name, resid)
+
+
+    def calendarObjectWithUID(self, uid):
+        c = self._cursor()
+        c.execute("select RESOURCE_NAME from CALENDAR_OBJECT where "
+                  "ICALENDAR_UID = %s",
+                  [uid])
+        rows = c.fetchall()
+        if not rows:
+            return None
+        name = rows[0][0]
+        return self.calendarObjectWithName(name)
+
+
+    def createCalendarObjectWithName(self, name, component):
+        str(component)
+        c = self._cursor()
+        c.execute(
+"""
+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, str(component), component.resourceUID(),
+component.resourceType(), _ATTACHMENTS_MODE_WRITE])
+
+
+    def removeCalendarObjectWithName(self, name):
+        c = self._cursor()
+        c.execute("delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and ",
+                  "CALENDAR_RESOURCE_ID = %s",
+                  [name, self._resourceID])
+
+
+    def removeCalendarObjectWithUID(self, uid):
+        c = self._cursor()
+        c.execute("delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and ",
+                  "CALENDAR_RESOURCE_ID = %s",
+                  [uid, self._resourceID])
+
+
+    def syncToken(self):
+        c = self._cursor()
+        c.execute("select SYNC_TOKEN from CALENDAR where RESOURCE_ID = %s",
+                  [self._resourceID])
+        return c.fetchall()[0][0]
+
+
+    def calendarObjectsInTimeRange(self, start, end, timeZone):
+        raise NotImplementedError()
+
+
+    def calendarObjectsSinceToken(self, token):
+        raise NotImplementedError()
+
+
+    def properties(self):
+        return PropertyStore(self._cursor(), self._home._txn._connection,
+                             self._resourceID)
+
+
+
+class PostgresCalendarHome(object):
+    implements(ICalendarHome)
+    def __init__(self, transaction, ownerUID, resourceID):
+        self._txn = transaction
+        self._ownerUID = ownerUID
+        self._resourceID = resourceID
+
+
+    def uid(self):
+        """
+        Retrieve the unique identifier for this calendar home.
+
+        @return: a string.
+        """
+        return self._ownerUID
+
+
+    def calendars(self):
+        """
+        Retrieve calendars contained in this calendar home.
+
+        @return: an iterable of L{ICalendar}s.
+        """
+        c = self._txn._cursor
+        c.execute(
+            "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
+            "CALENDAR_HOME_RESOURCE_ID = %s "
+            "AND STATUS != %s",
+            [self._resourceID,
+            _BIND_STATUS_DECLINED, ]
+        )
+        names = c.fetchall()
+        for name in names:
+            yield self.calendarWithName(name)
+
+
+    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.
+        """
+        c = self._txn._cursor
+        c.execute("select CALENDAR_RESOURCE_ID from CALENDAR_BIND where "
+                  "CALENDAR_RESOURCE_NAME = %s",
+                  [name])
+        data = c.fetchall()
+        if not data:
+            return None
+        resourceID = data[0][0]
+        return PostgresCalendar(self, name, resourceID)
+
+
+    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):
+        c = self._txn._cursor
+        c.execute("select nextval('RESOURCE_ID_SEQ')")
+        resourceID = c.fetchall()[0][0]
+        c.execute("insert into CALENDAR (SYNC_TOKEN, RESOURCE_ID) values "
+                  "(%s, %s)",
+                  ['uninitialized', resourceID])
+
+        c.execute("""
+        insert into CALENDAR_BIND (
+            CALENDAR_HOME_RESOURCE_ID,
+            CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, CALENDAR_MODE,
+            SEEN_BY_OWNER, SEEN_BY_SHAREE, STATUS) values (
+        %s, %s, %s, %s, %s, %s, %s)
+        """,
+        [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+        _BIND_STATUS_ACCEPTED])
+
+
+    def removeCalendarWithName(self, name):
+        c = self._txn._cursor
+        c.execute(
+            "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s and "
+            "CALENDAR_HOME_RESOURCE_ID = %s",
+            [name, self._resourceID])
+        # FIXME: the schema should probably cascade the delete when the last
+        # bind is deleted.
+
+
+    def properties(self):
+        return PropertyStore(self._txn._cursor, self._txn._connection,
+                             self._resourceID)
+
+
+
+class PostgresCalendarTransaction(object):
+    """
+    Transaction implementation for postgres database.
+    """
+    implements(ICalendarTransaction)
+
+    def __init__(self, connection):
+        self._connection = connection
+        self._cursor = connection.cursor()
+
+
+    def calendarHomeWithUID(self, uid, create=False):
+        self._cursor.execute(
+            "select RESOURCE_ID from CALENDAR_HOME where OWNER_UID = %s",
+            [uid]
+        )
+        data = self._cursor.fetchall()
+        if not data:
+            if not create:
+                return None
+            self._cursor.execute(
+                "insert into CALENDAR_HOME (OWNER_UID) values (%s)",
+                [uid]
+            )
+            return self.calendarHomeWithUID(uid)
+        resid = data[0][0]
+        return PostgresCalendarHome(self, uid, resid)
+
+
+    def abort(self):
+        self._connection.rollback()
+        self._connection.close()
+
+
+    def commit(self):
+        self._connection.commit()
+        self._connection.close()
+
+
+class PostgresStore(Service, object):
+
+    implements(IDataStore)
+
+    def __init__(self, connectionFactory):
+        self.connectionFactory = connectionFactory
+
+
+    def newTransaction(self):
+        return PostgresCalendarTransaction(self.connectionFactory())
+

Modified: CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/common.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/common.py	2010-07-23 19:11:12 UTC (rev 5929)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/common.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -667,11 +667,17 @@
         L{CalendarObjectNameAlreadyExistsError} if a calendar object with the
         given name already exists in that calendar.
         """
+        print 'getting calendar under test'
+        cal = self.calendarUnderTest()
+        print 'parsing component'
+        comp = VComponent.fromString(event4_text)
+        print 'checking raise'
         self.assertRaises(
             ObjectResourceNameAlreadyExistsError,
-            self.calendarUnderTest().createCalendarObjectWithName,
-            "1.ics", VComponent.fromString(event4_text)
+            cal.createCalendarObjectWithName,
+            "1.ics", comp
         )
+        print 'done'
 
 
     def test_createCalendarObjectWithName_invalid(self):

Added: CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/test/test_postgres.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -0,0 +1,111 @@
+##
+# 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.
+##
+
+from txcaldav.calendarstore.test.common import CommonTests
+
+from twisted.trial import unittest
+from txdav.datastore.subpostgres import PostgresService
+from txcaldav.calendarstore.postgres import PostgresStore, v1_schema
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+from twext.python.filepath import CachingFilePath
+from twext.python.vcomponent import VComponent
+from twisted.internet.task import deferLater
+
+
+
+sharedService = None
+
+class SQLStorageTests(CommonTests, unittest.TestCase):
+    """
+    File storage tests.
+    """
+
+    def setUp(self):
+        global sharedService
+        if sharedService is None:
+            ready = Deferred()
+            def getReady(connectionFactory):
+                global calendarStore
+                calendarStore = PostgresStore(connectionFactory)
+                self.populate()
+                ready.callback(None)
+                return calendarStore
+            sharedService = PostgresService(
+                CachingFilePath("pg"), getReady, v1_schema, "caldav"
+            )
+            sharedService.startService()
+            def startStopping():
+                for pipe in sharedService.monitor.transport.pipes.values():
+                    pipe.startReading()
+                    pipe.startWriting()
+                sharedService.stopService()
+            reactor.addSystemEventTrigger(
+                "before", "shutdown", startStopping)
+            return ready
+        else:
+            cleanupConn = calendarStore.connectionFactory()
+            cursor = cleanupConn.cursor()
+            cursor.execute("delete from RESOURCE_PROPERTY")
+            cursor.execute("delete from ATTACHMENT")
+            cursor.execute("delete from CALENDAR_OBJECT")
+            cursor.execute("delete from CALENDAR_BIND")
+            cursor.execute("delete from CALENDAR")
+            cursor.execute("delete from CALENDAR_HOME")
+            cleanupConn.commit()
+            cleanupConn.close()
+            self.populate()
+            for pipe in sharedService.monitor.transport.pipes.values():
+                pipe.startReading()
+                pipe.startWriting()
+            # I need to allow the log buffer to unspool.
+            return deferLater(reactor, 0.1, lambda : None)
+
+
+    def tearDown(self):
+        def stopit():
+            for pipe in sharedService.monitor.transport.pipes.values():
+                pipe.stopReading()
+                pipe.stopWriting()
+        return deferLater(reactor, 0.1, stopit)
+
+
+
+    def populate(self):
+        populateTxn = calendarStore.newTransaction()
+        for homeUID in self.requirements:
+            calendars = self.requirements[homeUID]
+            if calendars is not None:
+                home = populateTxn.calendarHomeWithUID(homeUID, True)
+                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()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return calendarStore
+

Modified: CalendarServer/branches/users/glyph/sql-store/txcaldav/icalendarstore.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/icalendarstore.py	2010-07-23 19:11:12 UTC (rev 5929)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/icalendarstore.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -207,7 +207,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

Added: CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sql-store/txdav/datastore/subpostgres.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -0,0 +1,203 @@
+# -*- 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 pgdb
+
+from twisted.python.procutils import which
+from twisted.internet.utils import getProcessOutput
+from twisted.internet.protocol import ProcessProtocol
+
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, succeed
+
+from twisted.application.service import MultiService
+
+
+_MAGIC_READY_COOKIE = "database system is ready to accept connections"
+
+
+class _PostgresMonitor(ProcessProtocol):
+    """
+    A monitoring protocol which watches the postgres subprocess.
+    """
+
+    def __init__(self, svc):
+        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 not self.isReady:
+            if _MAGIC_READY_COOKIE in line:
+                self.svc.ready()
+        print 'log output:', repr(line)
+
+
+    disconnecting = False
+    def connectionMade(self):
+        self.lineReceiver.makeConnection(self)
+
+
+    def outReceived(self, out):
+        print 'received postgres output', out
+        # self.lineReceiver.dataReceived(out)
+
+
+    def errReceived(self, err):
+        print 'postgress err received', repr(err)
+        self.lineReceiver.dataReceived(err)
+
+
+    def processEnded(self, reason):
+        self.lineReceiver.connectionLost(reason)
+        self.completionDeferred.callback(None)
+
+
+
+class PostgresService(MultiService):
+
+    def __init__(self, dataStoreDirectory, subServiceFactory,
+                 schema, databaseName='subpostgres'):
+        """
+        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.databaseName = databaseName
+        self.schema = schema
+        self.monitor = None
+
+
+    def produceConnection(self):
+        """
+        Produce a DB-API 2.0 connection pointed at this database.
+        """
+        return pgdb.connect(
+            "%s:dbname=%s" % (
+                self.socketDir.path,
+                self.databaseName
+            )
+        )
+
+
+    def ready(self):
+        """
+        Subprocess is ready.  Time to initialize the subservice.
+        """
+        if self.firstTime:
+            createDatabaseConn = pgdb.connect(
+                self.socketDir.path + ":dbname=template1"
+            )
+            createDatabaseCursor = createDatabaseConn.cursor()
+            createDatabaseCursor.execute("commit")
+            createDatabaseCursor.execute(
+                "create database %s" % (self.databaseName)
+            )
+            createDatabaseCursor.close()
+            createDatabaseConn.close()
+            print 'executing schema', repr(self.schema)
+            connection = self.produceConnection()
+            cursor = connection.cursor()
+            cursor.execute(self.schema)
+            connection.commit()
+            connection.close()
+        print 'creating subservice'
+        self.subServiceFactory(self.produceConnection).setServiceParent(self)
+        print 'subservice created'
+
+
+    def startDatabase(self):
+        """
+        Start the database and initialize the subservice.
+        """
+        monitor = _PostgresMonitor(self)
+        postgres = which("postgres")[0]
+        # check consistency of initdb and postgres?
+        reactor.spawnProcess(
+            monitor, postgres,
+            [
+                postgres,
+                "-k", self.socketDir.path,
+                # "-N", "5000",
+            ],
+            self.env
+        )
+        self.monitor = monitor
+
+
+    def startService(self):
+        MultiService.startService(self)
+        self.dataStoreDirectory.createDirectory()
+        clusterDir = self.dataStoreDirectory.child("cluster")
+        self.socketDir = self.dataStoreDirectory.child("socket")
+        self.socketDir.createDirectory()
+        workingDir = self.dataStoreDirectory.child("working")
+        env = self.env = os.environ.copy()
+        env.update(PGDATA=clusterDir.path)
+        initdb = which("initdb")[0]
+        if clusterDir.isdir():
+            self.firstTime = False
+            self.startDatabase()
+        else:
+            workingDir.createDirectory()
+            self.firstTime = True
+            print 'Creating database'
+            dbInited = getProcessOutput(
+                initdb, [], env, workingDir.path, errortoo=True
+            )
+            def doCreate(result):
+                print '--- initdb ---'
+                print result
+                print '/// initdb ///'
+                self.startDatabase()
+            dbInited.addCallback(
+                doCreate
+            )
+            def showme(result):
+                print 'SHOW ME:', result.getTraceback()
+            dbInited.addErrback(showme)
+
+
+    def stopService(self):
+        """
+        Stop all child services, then stop the subprocess, if it's running.
+        """
+        d = MultiService.stopService(self)
+        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

Added: CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/__init__.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -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.
+##
+

Added: CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sql-store/txdav/datastore/test/test_subpostgres.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -0,0 +1,76 @@
+##
+# 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.
+##
+from txdav.datastore.subpostgres import PostgresService
+from twisted.internet.defer import inlineCallbacks, Deferred
+from twisted.application.service import Service
+
+"""
+Tests for txdav.datastore.subpostgres.
+"""
+
+from twext.python.filepath import CachingFilePath
+from twisted.trial.unittest import TestCase
+
+
+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)
+                print 'CREATING simpleservice'
+                self.instances.append(self)
+
+            def startService(self):
+                print 'STARTING simpleservice'
+                cursor = self.connection.cursor()
+                cursor.execute(
+                    "insert into test_dummy_table values ('dummy')"
+                )
+                cursor.close()
+                self.ready.callback(None)
+
+        svc = PostgresService(
+            CachingFilePath("database"),
+            SimpleService,
+            "create table TEST_DUMMY_TABLE (stub varchar)",
+            "dummy_db"
+        )
+
+        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 = list(cursor)
+        self.assertEquals(values, [["dummy"]])

Modified: CalendarServer/branches/users/glyph/sql-store/txdav/idav.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txdav/idav.py	2010-07-23 19:11:12 UTC (rev 5929)
+++ CalendarServer/branches/users/glyph/sql-store/txdav/idav.py	2010-07-23 19:15:12 UTC (rev 5930)
@@ -91,18 +91,8 @@
     # 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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100723/b5931a9c/attachment-0001.html>


More information about the calendarserver-changes mailing list