[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