[CalendarServer-changes] [7515] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri May 20 13:36:24 PDT 2011


Revision: 7515
          http://trac.macosforge.org/projects/calendarserver/changeset/7515
Author:   cdaboo at apple.com
Date:     2011-05-20 13:36:24 -0700 (Fri, 20 May 2011)
Log Message:
-----------
Make sure SQL DB queries are scoped to calendar and addressbook collections as appropriate. Fix up some
incorrect addressbook query behavior.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/query/addressbookquery.py
    CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py
    CalendarServer/trunk/twistedcaldav/query/expression.py
    CalendarServer/trunk/txdav/common/datastore/sql_legacy.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/query/test/test_expression.py

Modified: CalendarServer/trunk/twistedcaldav/query/addressbookquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	2011-05-20 20:27:41 UTC (rev 7514)
+++ CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	2011-05-20 20:36:24 UTC (rev 7515)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 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.
@@ -26,8 +26,7 @@
     "sqladdressbookquery",
 ]
 
-from twistedcaldav.query import expression, sqlgenerator
-from twistedcaldav import carddavxml
+from twistedcaldav.query import expression, sqlgenerator, addressbookqueryfilter
 
 # SQL Index column (field) names
 
@@ -76,33 +75,27 @@
         # Test for <<field>> != "*"
         return expression.isExpression(fields["UID"], "", True)
     
-    # Handle text-match
-    tm = None
-    if propfilter.qualifier and isinstance(propfilter.qualifier, carddavxml.TextMatch):
-        if propfilter.qualifier.negate:
-            tm = expression.notcontainsExpression(fields[propfilter.filter_name], str(propfilter.qualifier), propfilter.qualifier)
-        else:
-            tm = expression.containsExpression(fields[propfilter.filter_name], str(propfilter.qualifier), propfilter.qualifier)
-    
-    # Handle embedded parameters - we do not right now as our Index does not handle them
+    # Handle embedded parameters/text-match
     params = []
-    if len(propfilter.filters) > 0:
-        raise ValueError
+    for filter in propfilter.filters:
+        if isinstance(filter, addressbookqueryfilter.TextMatch):
+            if filter.negate:
+                params.append(expression.notcontainsExpression(fields[propfilter.filter_name], str(filter.text), True))
+            else:
+                params.append(expression.containsExpression(fields[propfilter.filter_name], str(filter.text), True))
+        else:
+            # No embedded parameters - not right now as our Index does not handle them
+            raise ValueError
+
+    # Now build return expression
     if len(params) > 1:
-        paramsExpression = expression.orExpression[params]
+        if propfilter.propfilter_test == "anyof":
+            return expression.orExpression[params]
+        else:
+            return expression.andExpression[params]
     elif len(params) == 1:
-        paramsExpression = params[0]
+        return params[0]
     else:
-        paramsExpression = None
-
-    # Now build return expression
-    if (tm is not None) and (paramsExpression is not None):
-        return expression.andExpression([tm, paramsExpression])
-    elif tm is not None:
-        return tm
-    elif paramsExpression is not None:
-        return paramsExpression
-    else:
         return None
 
 def sqladdressbookquery(filter, addressbookid=None, generator=sqlgenerator.sqlgenerator):
@@ -116,7 +109,7 @@
     """
     try:
         expression = addressbookquery(filter, generator.FIELDS)
-        sql = generator(expression, addressbookid)
+        sql = generator(expression, addressbookid, None)
         return sql.generate()
     except ValueError:
         return None

Modified: CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py	2011-05-20 20:27:41 UTC (rev 7514)
+++ CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py	2011-05-20 20:36:24 UTC (rev 7515)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2010 Apple Inc. All rights reserved.
+# Copyright (c) 2011 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.
@@ -69,7 +69,7 @@
         if len(self.children) > 0:
             allof = self.filter_test == "allof"
             for propfilter in self.children:
-                if allof != propfilter.test(vcard):
+                if allof != propfilter._match(vcard):
                     return not allof
             return allof
         else:
@@ -153,7 +153,7 @@
         if len(self.filters) > 0:
             allof = self.propfilter_test == "allof"
             for filter in self.filters:
-                if allof != filter.test(item):
+                if allof != filter._match(item):
                     return not allof
             return allof
         else:
@@ -164,7 +164,7 @@
     Limits a search to specific properties.
     """
 
-    def test(self, vcard):
+    def _match(self, vcard):
         # At least one property must match (or is-not-defined is set)
         for property in vcard.properties():
             if property.name().upper() == self.filter_name.upper() and self.match(property): break
@@ -188,7 +188,7 @@
     Limits a search to specific parameters.
     """
 
-    def test(self, property):
+    def _match(self, property):
 
         # At least one parameter must match (or is-not-defined is set)
         result = not self.defined
@@ -246,7 +246,7 @@
         else:
             self.match_type = "contains"
 
-    def test(self, item):
+    def _match(self, item):
         """
         Match the text for the item.
         If the item is a property, then match the property value,

Modified: CalendarServer/trunk/twistedcaldav/query/expression.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/expression.py	2011-05-20 20:27:41 UTC (rev 7514)
+++ CalendarServer/trunk/twistedcaldav/query/expression.py	2011-05-20 20:36:24 UTC (rev 7515)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2011 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.
@@ -55,7 +55,22 @@
         
         return False
     
-class allExpression(object):
+    def _collapsedExpression(self):
+        return self
+
+    def andWith(self, other):
+        if isinstance(other, andExpression):
+            return andExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return andExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+    def orWith(self, other):
+        if isinstance(other, orExpression):
+            return orExpression((self._collapsedExpression(),) + tuple(other.expressions))
+        else:
+            return orExpression((self._collapsedExpression(), other._collapsedExpression(),))
+
+class allExpression(baseExpression):
     """
     Match everything.
     """
@@ -99,6 +114,12 @@
         
         return True
 
+    def _collapsedExpression(self):
+        if self.multi() and len(self.expressions) == 1:
+            return self.expressions[0]._collapsedExpression()
+        else:
+            return self
+
 class notExpression(logicExpression):
     """
     Logical NOT operation.
@@ -135,6 +156,10 @@
     def operator(self):
         return "AND"
 
+    def andWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
 class orExpression(logicExpression):
     """
     Logical OR operation.
@@ -146,6 +171,10 @@
     def operator(self):
         return "OR"
 
+    def orWith(self, other):
+        self.expressions = tuple(self.expressions) + (other._collapsedExpression(),)
+        return self
+
 class timerangeExpression(baseExpression):
     """
     CalDAV time-range comparison expression.

Added: CalendarServer/trunk/twistedcaldav/query/test/test_expression.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/test/test_expression.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/test/test_expression.py	2011-05-20 20:36:24 UTC (rev 7515)
@@ -0,0 +1,166 @@
+##
+# Copyright (c) 2011 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 twistedcaldav.query import expression
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+    def test_andWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) AND (is(B, 2, True) OR is(C, 3, True)))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) AND is(B, 2, True) AND is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) AND is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) OR is(B, 2, True)) AND is(C, 3, True))"
+            ),
+        )
+        
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.andWith(expr2)), result, msg="Failed on %s" % (result,))
+
+    def test_orWith(self):
+
+        tests = (
+            (
+                expression.isExpression("A", "1", True),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.andExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR (is(B, 2, True) AND is(C, 3, True)))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.isExpression("A", "1", True),
+                expression.orExpression((
+                    expression.isExpression("B", "2", True),
+                    expression.isExpression("C", "3", True),
+                )),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.andExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "((is(A, 1, True) AND is(B, 2, True)) OR is(C, 3, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                )),
+                expression.isExpression("B", "2", True),
+                "(is(A, 1, True) OR is(B, 2, True))"
+            ),
+            (
+                expression.orExpression((
+                    expression.isExpression("A", "1", True),
+                    expression.isExpression("B", "2", True),
+                )),
+                expression.isExpression("C", "3", True),
+                "(is(A, 1, True) OR is(B, 2, True) OR is(C, 3, True))"
+            ),
+        )
+        
+        for expr1, expr2, result in tests:
+            self.assertEqual(str(expr1.orWith(expr2)), result, msg="Failed on %s" % (result,))

Modified: CalendarServer/trunk/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_legacy.py	2011-05-20 20:27:41 UTC (rev 7514)
+++ CalendarServer/trunk/txdav/common/datastore/sql_legacy.py	2011-05-20 20:36:24 UTC (rev 7515)
@@ -30,13 +30,13 @@
 from twext.python.clsprop import classproperty
 from twext.python.log import Logger, LoggingMixIn
 
-from twistedcaldav import carddavxml
 from twistedcaldav.config import config
 from twistedcaldav.dateops import normalizeForIndex, pyCalendarTodatetime
 from twistedcaldav.memcachepool import CachePoolUserMixIn
 from twistedcaldav.notifications import NotificationRecord
 from twistedcaldav.query import (
-    calendarqueryfilter, calendarquery, addressbookquery)
+    calendarqueryfilter, calendarquery, addressbookquery, expression,
+    addressbookqueryfilter)
 from twistedcaldav.query.sqlgenerator import sqlgenerator
 from twistedcaldav.sharing import Invite
 
@@ -891,6 +891,62 @@
         self.substitutions = []
         self.usedtimespan = False
 
+        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+        if self.calendarid:
+            
+            test = expression.isExpression("CALENDAR_OBJECT.CALENDAR_RESOURCE_ID", str(self.calendarid), True)
+
+            # Since timerange expression already have the calendar resource-id test in them, do not
+            # add the additional term to those. When the additional term is added, add it as the first
+            # component in the AND expression to hopefully get the DB to use its index first
+
+            # Top-level timerange expression already has calendar resource-id restriction in it
+            if isinstance(self.expression, expression.timerangeExpression):
+                pass
+            
+            # Top-level OR - check each component
+            elif isinstance(self.expression, expression.orExpression):
+                
+                def _hasTopLevelTimerange(testexpr):
+                    if isinstance(testexpr, expression.timerangeExpression):
+                        return True
+                    elif isinstance(testexpr, expression.andExpression):
+                        return any([isinstance(expr, expression.timerangeExpression) for expr in testexpr.expressions])
+                    else:
+                        return False
+                        
+                hasTimerange = any([_hasTopLevelTimerange(expr) for expr in self.expression.expressions])
+
+                if hasTimerange:
+                    # AND each of the non-timerange expressions
+                    trexpressions = []
+                    orexpressions = []
+                    for expr in self.expression.expressions:
+                        if _hasTopLevelTimerange(expr):
+                            trexpressions.append(expr)
+                        else:
+                            orexpressions.append(expr)
+                    
+                    if orexpressions:
+                        self.expression.expressions = tuple(trexpressions) + (
+                            test.andWith(expression.orExpression(orexpressions)),
+                        )
+                else:
+                    # AND the whole thing
+                    self.expression = test.andWith(self.expression)    
+
+            
+            # Top-level AND - only add additional expression if timerange not present
+            elif isinstance(self.expression, expression.andExpression):
+                hasTimerange = any([isinstance(expr, expression.timerangeExpression) for expr in self.expression.expressions])
+                if not hasTimerange:
+                    # AND the whole thing
+                    self.expression = test.andWith(self.expression)    
+            
+            # Just AND the entire thing
+            else:
+                self.expression = test.andWith(self.expression)
+
         # Generate ' where ...' partial statement
         self.sout.write(self.WHERE)
         self.generateExpression(self.expression)
@@ -1291,6 +1347,13 @@
         self.arguments = []
         self.substitutions = []
 
+        # For SQL data DB we need to restrict the query to just the targeted calendar resource-id if provided
+        if self.calendarid:
+            
+            # AND the whole thing
+            test = expression.isExpression("ADDRESSBOOK_OBJECT.ADDRESSBOOK_RESOURCE_ID", str(self.calendarid), True)
+            self.expression = test.andWith(self.expression)    
+
         # Generate ' where ...' partial statement
         self.sout.write(self.WHERE)
         self.generateExpression(self.expression)
@@ -1356,7 +1419,7 @@
 
 
     def searchValid(self, filter):
-        if isinstance(filter, carddavxml.Filter):
+        if isinstance(filter, addressbookqueryfilter.Filter):
             qualifiers = addressbookquery.sqladdressbookquery(filter)
         else:
             qualifiers = None
@@ -1379,7 +1442,7 @@
         else:
             generator = postgresqladbkgenerator
         # Make sure we have a proper Filter element and get the partial SQL statement to use.
-        if isinstance(filter, carddavxml.Filter):
+        if isinstance(filter, addressbookqueryfilter.Filter):
             qualifiers = addressbookquery.sqladdressbookquery(
                 filter, self.addressbook._resourceID, generator=generator)
         else:
@@ -1392,10 +1455,10 @@
             )
         else:
             rowiter = yield Select(
-                [schema.ADDRESSBOOK_OBJECT.RESOURCE_NAME,
-                 schema.ADDRESSBOOK_OBJECT.VCARD_UID],
-                From=schema.ADDRESSBOOK_OBJECT,
-                Where=schema.ADDRESSBOOK_OBJECT.ADDRESSBOOK_RESOURCE_ID ==
+                [self._objectSchema.RESOURCE_NAME,
+                 self._objectSchema.VCARD_UID],
+                From=self._objectSchema,
+                Where=self._objectSchema.ADDRESSBOOK_RESOURCE_ID ==
                 self.addressbook._resourceID
             ).on(self.addressbook._txn)
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110520/6526d98e/attachment-0001.html>


More information about the calendarserver-changes mailing list