[CalendarServer-changes] [6933] CalendarServer/branches/users/glyph/dalify

source_changes at macosforge.org source_changes at macosforge.org
Wed Feb 16 06:26:04 PST 2011


Revision: 6933
          http://trac.macosforge.org/projects/calendarserver/changeset/6933
Author:   glyph at apple.com
Date:     2011-02-16 06:26:04 -0800 (Wed, 16 Feb 2011)
Log Message:
-----------
enough fixes to get all of CardDAV's custom queries using the DAL.

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/model.py
    CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/syntax.py
    CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_parseschema.py
    CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_sqlsyntax.py
    CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/sql.py

Modified: CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/model.py
===================================================================
--- CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/model.py	2011-02-16 14:23:04 UTC (rev 6932)
+++ CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/model.py	2011-02-16 14:26:04 UTC (rev 6933)
@@ -112,6 +112,7 @@
     """
 
 
+
 def _checkstr(x):
     """
     Verify that C{x} is a C{str}.  Raise a L{ValueError} if not.  This is to
@@ -121,6 +122,7 @@
         raise ValueError("%r is not a str." % (x,))
 
 
+
 class Column(object):
     """
     A column from a table.
@@ -184,7 +186,8 @@
 
         @rtype: C{bool}
         """
-        return self.canBeNull() or (self.default is not None)
+        return not (self.canBeNull() or
+                    (self.default not in (None, NO_DEFAULT)))
 
 
     def doesReferenceName(self, name):

Modified: CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/syntax.py
===================================================================
--- CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/syntax.py	2011-02-16 14:23:04 UTC (rev 6932)
+++ CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/syntax.py	2011-02-16 14:26:04 UTC (rev 6933)
@@ -92,7 +92,7 @@
         if isinstance(other, ColumnSyntax):
             return ColumnComparison(self, comparator, other)
         else:
-            return ConstantComparison(self, comparator, other)
+            return CompoundComparison(self, comparator, Constant(other))
     return __
 
 
@@ -123,24 +123,59 @@
 
 
 class FunctionInvocation(ExpressionSyntax):
-    def __init__(self, name, arg):
+    def __init__(self, name, *args):
         self.name = name
-        self.arg = arg
+        self.args = args
 
 
     def allColumns(self):
-        return self.arg.allColumns()
+        """
+        All of the columns in all of the arguments' columns.
+        """
+        def ac():
+            for arg in self.args:
+                for column in arg.allColumns():
+                    yield column
+        return list(ac())
 
 
     def subSQL(self, placeholder, quote, allTables):
         result = SQLFragment(self.name)
-        result.text += "("
-        result.append(self.arg.subSQL(placeholder, quote, allTables))
-        result.text += ")"
+        result.append(_inParens(
+            _commaJoined(_convert(arg).subSQL(placeholder, quote, allTables)
+                         for arg in self.args)))
         return result
 
 
 
+class Constant(ExpressionSyntax):
+    def __init__(self, value):
+        self.value = value
+
+
+    def allColumns(self):
+        return []
+
+
+    def subSQL(self, placeholder, quote, allTables):
+        return SQLFragment(placeholder, [self.value])
+
+
+
+class NamedValue(ExpressionSyntax):
+    """
+    A constant within the database; something pre-defined, such as
+    CURRENT_TIMESTAMP.
+    """
+    def __init__(self, name):
+        self.name = name
+
+
+    def subSQL(self, placeholder, quote, allTables):
+        return SQLFragment(self.name)
+
+
+
 class Function(object):
     """
     An L{Function} is a representation of an SQL Function function.
@@ -150,12 +185,13 @@
         self.name = name
 
 
-    def __call__(self, arg):
+    def __call__(self, *args):
         """
         Produce an L{FunctionInvocation}
         """
-        return FunctionInvocation(self.name, arg)
+        return FunctionInvocation(self.name, *args)
 
+
 Max = Function("max")
 Len = Function("character_length")
 
@@ -334,21 +370,6 @@
 
 
 
-class ConstantComparison(Comparison):
-
-    def allColumns(self):
-        return self.a.allColumns()
-
-
-    def subSQL(self, placeholder, quote, allTables):
-        sqls = SQLFragment()
-        sqls.append(self._subexpression(self.a, placeholder, quote, allTables))
-        sqls.append(SQLFragment(' ' + ' '.join([self.op, placeholder]),
-                                 [self.b]))
-        return sqls
-
-
-
 class CompoundComparison(Comparison):
     """
     A compound comparison; two or more constraints, joined by an operation
@@ -470,8 +491,8 @@
 
         if self.Limit is not None:
             stmt.text += quote(" limit ")
-            stmt.text += placeholder
-            stmt.parameters.append(self.Limit)
+            stmt.append(Constant(self.Limit).subSQL(placeholder, quote,
+                                                    allTables))
         return stmt
 
 
@@ -525,13 +546,26 @@
 
 
 
-class Insert(object):
+class _CommaList(object):
+    def __init__(self, subfragments):
+        self.subfragments = subfragments
+
+
+    def subSQL(self, placeholder, quote, allTables):
+        return _commaJoined(f.subSQL(placeholder, quote, allTables)
+                            for f in self.subfragments)
+
+
+
+class Insert(_Statement):
     """
     'insert' statement.
     """
 
     def __init__(self, columnMap, Return=None):
         self.columnMap = columnMap
+        if isinstance(Return, (tuple, list)):
+            Return = _CommaList(Return)
         self.Return = Return
         columns = _modelsFromMap(columnMap)
         table = _fromSameTable(columns)
@@ -563,7 +597,8 @@
              sortedColumns])))
         stmt.append(SQLFragment(" values "))
         stmt.append(_inParens(_commaJoined(
-            [SQLFragment(placeholder, [v]) for (c, v) in sortedColumns])))
+            [_convert(v).subSQL(placeholder, quote, allTables)
+             for (c, v) in sortedColumns])))
         if self.Return is not None:
             stmt.text += ' returning '
             stmt.append(self.Return.subSQL(placeholder, quote, allTables))
@@ -571,8 +606,20 @@
 
 
 
-class Update(object):
+def _convert(x):
     """
+    Convert a value to an appropriate SQL AST node.  (Currently a simple
+    isinstance, could be promoted to use adaptation if we want to get fancy.)
+    """
+    if isinstance(x, ExpressionSyntax):
+        return x
+    else:
+        return Constant(x)
+
+
+
+class Update(_Statement):
+    """
     'update' statement
     """
 
@@ -602,7 +649,8 @@
         result.append(
             _commaJoined(
                 [c.subSQL(placeholder, quote, allTables).append(
-                    SQLFragment(" = " + placeholder, [v]))
+                    SQLFragment(" = ").subSQL(placeholder, quote, allTables)
+                ).append(_convert(v).subSQL(placeholder, quote, allTables))
                     for (c, v) in sortedColumns]
             )
         )

Modified: CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_parseschema.py
===================================================================
--- CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_parseschema.py	2011-02-16 14:23:04 UTC (rev 6932)
+++ CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_parseschema.py	2011-02-16 14:26:04 UTC (rev 6933)
@@ -125,6 +125,32 @@
         self.assertEquals(table.columnNamed("f").default, None)
 
 
+    def test_needsValue(self):
+        """
+        Columns with defaults, or with a 'not null' constraint don't need a
+        value; columns without one don't.
+        """
+        s = Schema()
+        addSQLToSchema(s,
+                       """
+                       create table a (
+                        b integer default 4321 not null,
+                        c boolean default false,
+                        d integer not null,
+                        e integer
+                       )
+                       """)
+        table = s.tableNamed("a")
+        # Has a default, NOT NULL.
+        self.assertEquals(table.columnNamed("b").needsValue(), False)
+        # Has a default _and_ nullable.
+        self.assertEquals(table.columnNamed("c").needsValue(), False)
+        # No default, not nullable.
+        self.assertEquals(table.columnNamed("d").needsValue(), True)
+        # Just nullable.
+        self.assertEquals(table.columnNamed("e").needsValue(), False)
+
+
     def test_notNull(self):
         """
         A column with a NOT NULL constraint in SQL will be parsed as a

Modified: CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_sqlsyntax.py
===================================================================
--- CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_sqlsyntax.py	2011-02-16 14:23:04 UTC (rev 6932)
+++ CalendarServer/branches/users/glyph/dalify/twext/enterprise/dal/test/test_sqlsyntax.py	2011-02-16 14:26:04 UTC (rev 6933)
@@ -25,6 +25,7 @@
     TableMismatch, Parameter, Max, Len, NotEnoughValues
 )
 
+from twext.enterprise.dal.syntax import FunctionInvocation
 from twisted.trial.unittest import TestCase
 
 class GenerationTests(TestCase):
@@ -367,6 +368,21 @@
         )
 
 
+    def test_insertMultiReturn(self):
+        """
+        L{Insert}'s C{Return} argument can also be a C{tuple}, which will insert
+        an SQL 'returning' clause with multiple columns.
+        """
+        self.assertEquals(
+            Insert({self.schema.FOO.BAR: 23,
+                    self.schema.FOO.BAZ: 9},
+                   Return=(self.schema.FOO.BAR, self.schema.FOO.BAZ)).toSQL(),
+            SQLFragment(
+                "insert into FOO (BAR, BAZ) values (?, ?) returning BAR, BAZ",
+                [23, 9])
+        )
+
+
     def test_insertMismatch(self):
         """
         L{Insert} raises L{TableMismatch} if the columns specified aren't all
@@ -408,6 +424,37 @@
         )
 
 
+    def test_updateFunction(self):
+        """
+        L{Update} values may be L{FunctionInvocation}s, to update to computed
+        values in the database.
+        """
+        self.assertEquals(
+            Update(
+                {self.schema.FOO.BAR: 23,
+                 self.schema.FOO.BAZ: FunctionInvocation("hello")},
+                Where=self.schema.FOO.BAZ == 9
+            ).toSQL(),
+            SQLFragment("update FOO set BAR = ?, BAZ = hello() "
+                        "where BAZ = ?", [23, 9])
+        )
+
+
+    def test_insertFunction(self):
+        """
+        L{Update} values may be L{FunctionInvocation}s, to update to computed
+        values in the database.
+        """
+        self.assertEquals(
+            Insert(
+                {self.schema.FOO.BAR: 23,
+                 self.schema.FOO.BAZ: FunctionInvocation("hello")},
+            ).toSQL(),
+            SQLFragment("insert into FOO (BAR, BAZ) "
+                        "values (?, hello())", [23])
+        )
+
+
     def test_deleteReturning(self):
         """
         L{Delete}'s C{Return} argument will delete an SQL 'returning' clause.

Modified: CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/sql.py	2011-02-16 14:23:04 UTC (rev 6932)
+++ CalendarServer/branches/users/glyph/dalify/txdav/carddav/datastore/sql.py	2011-02-16 14:26:04 UTC (rev 6933)
@@ -47,6 +47,10 @@
 
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
     CommonObjectResource
+from twext.enterprise.dal.syntax import Insert
+from twext.enterprise.dal.syntax import Update
+from twext.enterprise.dal.syntax import NamedValue
+from twext.enterprise.dal.syntax import Function
 from txdav.common.datastore.sql_tables import ADDRESSBOOK_TABLE,\
     ADDRESSBOOK_BIND_TABLE, ADDRESSBOOK_OBJECT_REVISIONS_TABLE,\
     ADDRESSBOOK_OBJECT_TABLE, ADDRESSBOOK_HOME_TABLE,\
@@ -55,6 +59,7 @@
     ADDRESSBOOK_OBJECT_REVISIONS_AND_BIND_TABLE, schema
 from txdav.base.propertystore.base import PropertyName
 
+dbCurrentTime = Function('timezone')('UTC', NamedValue('CURRENT_TIMESTAMP'))
 
 
 class AddressBookHome(CommonHome):
@@ -207,7 +212,8 @@
 
 
     @inlineCallbacks
-    def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
+    def updateDatabase(self, component, expand_until=None, reCreate=False,
+                       inserting=False):
         """
         Update the database tables for the new data being written.
 
@@ -215,6 +221,8 @@
         @type component: L{Component}
         """
 
+        ao = schema.ADDRESSBOOK_OBJECT
+
         componentText = str(component)
         self._objectText = componentText
 
@@ -223,42 +231,24 @@
         self._size = len(componentText)
         if inserting:
             self._resourceID, self._created, self._modified = (
-                yield self._txn.execSQL(
-                """
-                insert into ADDRESSBOOK_OBJECT
-                (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID, MD5)
-                 values
-                (%s, %s, %s, %s, %s)
-                returning
-                 RESOURCE_ID,
-                 CREATED,
-                 MODIFIED
-                """,
-                [
-                    self._addressbook._resourceID,
-                    self._name,
-                    componentText,
-                    component.resourceUID(),
-                    self._md5,
-                ]
-            ))[0]
+                yield Insert(
+                    {ao.ADDRESSBOOK_RESOURCE_ID: self._addressbook._resourceID,
+                     ao.RESOURCE_NAME: self._name,
+                     ao.VCARD_TEXT: componentText,
+                     ao.VCARD_UID: component.resourceUID(),
+                     ao.MD5: self._md5},
+                    Return=(ao.RESOURCE_ID,
+                            ao.CREATED,
+                            ao.MODIFIED)
+                ).on(self._txn))[0]
         else:
-            yield self._txn.execSQL(
-                """
-                update ADDRESSBOOK_OBJECT set
-                (VCARD_TEXT, VCARD_UID, MD5, MODIFIED)
-                 =
-                (%s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP))
-                where RESOURCE_ID = %s
-                returning MODIFIED
-                """,
-                [
-                    componentText,
-                    component.resourceUID(),
-                    self._md5,
-                    self._resourceID,
-                ]
-            )
+            self._modified = (yield Update(
+                {ao.VCARD_TEXT: componentText,
+                 ao.VCARD_UID: component.resourceUID(),
+                 ao.MD5: self._md5,
+                 ao.MODIFIED: dbCurrentTime},
+                Where=ao.RESOURCE_ID == self._resourceID,
+                Return=ao.MODIFIED).on(self._txn))[0][0]
 
 
     @inlineCallbacks
@@ -275,3 +265,6 @@
         The content type of Addressbook objects is text/x-vcard.
         """
         return MimeType.fromString("text/vcard; charset=utf-8")
+
+
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110216/e7cebfac/attachment-0001.html>


More information about the calendarserver-changes mailing list