This patch adds selectOne and selectOneBy methods to SQLObject, and simple
test cases for them.
ForeignKey columns and alternateID columns are great, but don't satisfy all
use-cases. There are times when you need to use select or selectBy, but you
also know that your query can only ever return one (or zero) rows. An
example is any time you have an UNIQUE constraint on multiple columns, and
you do a selectBy on the same set of columns.
selectOne and selectOneBy are designed to make this situation as easy to
deal with as possible.
At Canonical, we've played with this for some time and found it to be a very
valuable addition to SQLObject, eliminating a lot of boilerplate code.
Please apply, or let me know what you think!
Index: SQLObject/sqlobject/main.py
===================================================================
--- SQLObject.orig/sqlobject/main.py 2005-06-30 12:26:44.000000000 -0300
+++ SQLObject/sqlobject/main.py 2005-06-30 12:26:46.000000000 -0300
@@ -49,6 +49,7 @@
class SQLObjectNotFound(LookupError): pass
class SQLObjectIntegrityError(Exception): pass
+class SQLObjectMoreThanOneResultError(Exception): pass
True, False = 1==1, 0==1
@@ -1186,6 +1187,46 @@
selectBy = classmethod(selectBy)
+ def selectOne(cls, clause=None, clauseTables=None, lazyColumns=False,
+ connection=None):
+ """A variant of select to return a single result.
+
+ If clause finds no results, this returns None. If it finds one result,
+ it returns it. If it finds more than one result, it raises a
+ SQLObjectMoreThanOneResultError.
+ """
+ results = list(SelectResults(cls, clause, clauseTables=clauseTables,
+ lazyColumns=lazyColumns,
+ connection=connection))
+ if len(results) == 0:
+ return None
+ elif len(results) == 1:
+ return results[0]
+ else:
+ raise SQLObjectMoreThanOneResultError(
+ "%d rows retrieved by selectOne" % len(results))
+ selectOne = classmethod(selectOne)
+
+ def selectOneBy(cls, lazyColumns=False, connection=None, **kw):
+ """A variant of selectBy to return a single result.
+
+ If it finds no results, this returns None. If it finds one result,
+ it returns it. If it finds more than one result, it raises a
+ SQLObjectMoreThanOneResultError.
+ """
+ results = list(SelectResults(cls,
+ cls._connection._SO_columnClause(cls, kw),
+ lazyColumns=lazyColumns,
+ connection=connection))
+ if len(results) == 0:
+ return None
+ elif len(results) == 1:
+ return results[0]
+ else:
+ raise SQLObjectMoreThanOneResultError(
+ "%d rows retrieved by selectOne" % len(results))
+ selectOneBy = classmethod(selectOneBy)
+
def dropTable(cls, ifExists=False, dropJoinTables=True, cascade=False,
connection=None):
conn = connection or cls._connection
@@ -1433,4 +1474,5 @@
__all__ = ['NoDefault', 'SQLObject', 'sqlmeta',
'getID', 'getObject',
- 'SQLObjectNotFound', 'sqlhub']
+ 'SQLObjectNotFound', 'SQLObjectMoreThanOneResultError',
+ 'sqlhub']
Index: SQLObject/sqlobject/tests/test_select.py
===================================================================
--- SQLObject.orig/sqlobject/tests/test_select.py 2005-06-30 12:26:44.000000000 -0300
+++ SQLObject/sqlobject/tests/test_select.py 2005-06-30 12:27:18.000000000 -0300
@@ -1,3 +1,5 @@
+from py.test import raises
+
from sqlobject import *
from sqlobject.tests.dbtest import *
@@ -112,3 +114,17 @@
# Ensure that the orderBy argument to selectBy works
assert ([c.n2 for c in Counter2.selectBy(n1=5, orderBy='-n2')] ==
range(10)[::-1])
+
+ def testSelectOne(self):
+ # Test the behaviour of selectOne when it gets 0 rows, 1 row, and >1
+ # rows.
+ assert Counter2.selectOne('n1=99') == None
+ raises(SQLObjectMoreThanOneResultError, Counter2.selectOne, 'n1=1')
+ assert Counter2.selectOne('n1=1 AND n2=2').n1 == 1
+
+ # Test the behaviour of selectOneBy when it gets 0 rows, 1 row, and >1
+ # rows.
+ assert Counter2.selectOneBy(n1=99) == None
+ raises(SQLObjectMoreThanOneResultError, Counter2.selectOneBy, n1=1)
+ assert Counter2.selectOneBy(n1=1, n2=2).n1 == 1
+
-Andrew.
|