Index: /main/waeup.sirp/branches/unique-index/README.txt
===================================================================
--- /main/waeup.sirp/branches/unique-index/README.txt	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/README.txt	(revision 6780)
@@ -0,0 +1,206 @@
+Field Indexes
+=============
+
+Field indexes index orderable values.  Note that they don't check for
+orderability. That is, all of the values added to the index must be
+orderable together. It is up to applications to provide only mutually
+orderable values.
+
+    >>> from zope.index.field import FieldIndex
+
+    >>> index = FieldIndex()
+    >>> index.index_doc(0, 6)
+    >>> index.index_doc(1, 26)
+    >>> index.index_doc(2, 94)
+    >>> index.index_doc(3, 68)
+    >>> index.index_doc(4, 30)
+    >>> index.index_doc(5, 68)
+    >>> index.index_doc(6, 82)
+    >>> index.index_doc(7, 30)
+    >>> index.index_doc(8, 43)
+    >>> index.index_doc(9, 15)
+
+Field indexes are searched with apply.  The argument is a tuple
+with a minimum and maximum value:
+
+    >>> index.apply((30, 70))
+    IFSet([3, 4, 5, 7, 8])
+
+A common mistake is to pass a single value.  If anything other than a
+two-tuple is passed, a type error is raised:
+
+    >>> index.apply('hi')
+    Traceback (most recent call last):
+    ...
+    TypeError: ('two-length tuple expected', 'hi')
+
+
+Open-ended ranges can be provided by provinding None as an end point:
+
+    >>> index.apply((30, None))
+    IFSet([2, 3, 4, 5, 6, 7, 8])
+
+    >>> index.apply((None, 70))
+    IFSet([0, 1, 3, 4, 5, 7, 8, 9])
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+To do an exact value search, supply equal minimum and maximum values:
+
+    >>> index.apply((30, 30))
+    IFSet([4, 7])
+
+    >>> index.apply((70, 70))
+    IFSet([])
+
+Field indexes support basic statistics:
+
+    >>> index.documentCount()
+    10
+    >>> index.wordCount()
+    8
+
+Documents can be reindexed:
+
+    >>> index.apply((15, 15))
+    IFSet([9])
+    >>> index.index_doc(9, 14)
+
+    >>> index.apply((15, 15))
+    IFSet([])
+    >>> index.apply((14, 14))
+    IFSet([9])
+
+Documents can be unindexed:
+
+    >>> index.unindex_doc(7)
+    >>> index.documentCount()
+    9
+    >>> index.wordCount()
+    8
+    >>> index.unindex_doc(8)
+    >>> index.documentCount()
+    8
+    >>> index.wordCount()
+    7
+
+    >>> index.apply((30, 70))
+    IFSet([3, 4, 5])
+
+Unindexing a document id that isn't present is ignored:
+
+    >>> index.unindex_doc(8)
+    >>> index.unindex_doc(80)
+    >>> index.documentCount()
+    8
+    >>> index.wordCount()
+    7
+
+We can also clear the index entirely:
+
+    >>> index.clear()
+    >>> index.documentCount()
+    0
+    >>> index.wordCount()
+    0
+
+    >>> index.apply((30, 70))
+    IFSet([])
+
+Sorting
+-------
+
+Field indexes also implement IIndexSort interface that
+provides a method for sorting document ids by their indexed
+values.
+
+    >>> index.index_doc(1, 9)
+    >>> index.index_doc(2, 8)
+    >>> index.index_doc(3, 7)
+    >>> index.index_doc(4, 6)
+    >>> index.index_doc(5, 5)
+    >>> index.index_doc(6, 4)
+    >>> index.index_doc(7, 3)
+    >>> index.index_doc(8, 2)
+    >>> index.index_doc(9, 1)
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5]))
+    [9, 7, 5, 4, 3, 2, 1]
+
+We can also specify the ``reverse`` argument to reverse results:
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], reverse=True))
+    [1, 2, 3, 4, 5, 7, 9]
+
+And as per IIndexSort, we can limit results by specifying the ``limit``
+argument:
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], limit=3))
+    [9, 7, 5]
+
+If we pass an id that is not indexed by this index, it won't be included
+in the result.
+
+    >>> list(index.sort([2, 10]))
+    [2]
+
+    >>> index.clear()
+
+Bugfix testing:
+---------------
+Happened at least once that the value dropped out of the forward index,
+but the index still contains the object, the unindex broke
+
+    >>> index.index_doc(0, 6)
+    >>> index.index_doc(1, 26)
+    >>> index.index_doc(2, 94)
+    >>> index.index_doc(3, 68)
+    >>> index.index_doc(4, 30)
+    >>> index.index_doc(5, 68)
+    >>> index.index_doc(6, 82)
+    >>> index.index_doc(7, 30)
+    >>> index.index_doc(8, 43)
+    >>> index.index_doc(9, 15)
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+Here is the damage:
+
+    >>> del index._fwd_index[68]
+
+Unindex should succeed:
+
+    >>> index.unindex_doc(5)
+    >>> index.unindex_doc(3)
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 4, 6, 7, 8, 9])
+
+
+Optimizations
+-------------
+
+There is an optimization which makes sure that nothing is changed in the
+internal data structures if the value of the ducument was not changed.
+
+To test this optimization we patch the index instance to make sure unindex_doc
+is not called.
+
+    >>> def unindex_doc(doc_id):
+    ...     raise KeyError
+    >>> index.unindex_doc = unindex_doc
+
+Now we get a KeyError if we try to change the value.
+
+    >>> index.index_doc(9, 14)
+    Traceback (most recent call last):
+    ...
+    KeyError
+
+Leaving the value unchange doesn't call unindex_doc.
+
+    >>> index.index_doc(9, 15)
+    >>> index.apply((15, 15))
+    IFSet([9])
Index: /main/waeup.sirp/branches/unique-index/__init__.py
===================================================================
--- /main/waeup.sirp/branches/unique-index/__init__.py	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/__init__.py	(revision 6780)
@@ -0,0 +1,1 @@
+from uniquefield import UniqueFieldIndex, UniqueField
Index: /main/waeup.sirp/branches/unique-index/configure.zcml
===================================================================
--- /main/waeup.sirp/branches/unique-index/configure.zcml	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/configure.zcml	(revision 6780)
@@ -0,0 +1,10 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <class class=".uniquefield.UniqueFieldIndex">
+    <require
+        permission="zope.ManageServices"
+        interface="zope.catalog.interfaces.IAttributeIndex
+                   zope.index.interfaces.IStatistics"
+        set_schema="zope.catalog.interfaces.IAttributeIndex"
+        />
+  </class>
+</configure>
Index: /main/waeup.sirp/branches/unique-index/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/unique-index/interfaces.py	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/interfaces.py	(revision 6780)
@@ -0,0 +1,54 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat May 28 13:36:51 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Interfaces for :mod:`waeup.sirp.index`.
+"""
+from zope.index.interfaces import IInjection
+from zope.catalog.field import IFieldIndex
+
+class IUnique(IInjection):
+    def index_doc(docid, value):
+        """Add a document to the index.
+
+        `docid`: int, identifying the document
+
+        `value`: the value to be indexed
+
+        return: None
+
+        This can also be used to reindex documents.
+
+        If the same value is already stored under a different `docid`,
+        a :exc:`KeyError` is raised.
+        """
+
+
+class IUniqueFieldIndex(IFieldIndex):
+    """A field index that only accepts unique values.
+    """
+
+    def index_doc(docid, value):
+        """Index `value` under the given `docid`.
+
+        Raises :exc:`KeyError` if the value is already stored for a
+        different docid (unique-constraint violation).
+        """
Index: /main/waeup.sirp/branches/unique-index/tests.py
===================================================================
--- /main/waeup.sirp/branches/unique-index/tests.py	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/tests.py	(revision 6780)
@@ -0,0 +1,387 @@
+"""Test unique field index
+"""
+import unittest
+from zope.catalog.catalog import Catalog
+from waeup.sirp.index.uniquefield import UniqueFieldIndex as FieldIndex
+from zope.catalog.tests import (
+    PlacelessSetup, IntIdsStub, stoopid, stoopidCallable,)
+from zope.component import provideUtility
+from zope.intid.interfaces import IIntIds
+
+_marker = object()
+
+class RawUniqueFieldIndexTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from waeup.sirp.index.uniquefield import RawUniqueFieldIndex
+        return RawUniqueFieldIndex
+
+    def _makeOne(self, family=_marker):
+        if family is _marker:
+            return self._getTargetClass()()
+        return self._getTargetClass()(family)
+
+    def _populateIndex(self, index):
+        index.index_doc(5, 1) # docid, obj
+        index.index_doc(2, 2)
+        index.index_doc(1, 3)
+        index.index_doc(3, 4)
+        index.index_doc(4, 5)
+        index.index_doc(8, 6)
+        index.index_doc(9, 7)
+        index.index_doc(7, 8)
+        index.index_doc(6, 9)
+        index.index_doc(11, 10)
+        index.index_doc(10, 11)
+
+    def test_class_conforms_to_IInjection(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IInjection
+        verifyClass(IInjection, self._getTargetClass())
+
+    def test_instance_conforms_to_IInjection(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IInjection
+        verifyObject(IInjection, self._makeOne())
+
+    def test_class_conforms_to_IUniqueInjection(self):
+        from zope.interface.verify import verifyClass
+        from waeup.sirp.index.interfaces import IUnique
+        verifyClass(IUnique, self._getTargetClass())
+
+    def test_instance_conforms_to_IUniqueInjection(self):
+        from zope.interface.verify import verifyObject
+        from waeup.sirp.index.interfaces import IUnique
+        verifyObject(IUnique, self._makeOne())
+
+    def test_class_conforms_to_IIndexSearch(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IIndexSearch
+        verifyClass(IIndexSearch, self._getTargetClass())
+
+    def test_instance_conforms_to_IIndexSearch(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IIndexSearch
+        verifyObject(IIndexSearch, self._makeOne())
+
+    def test_class_conforms_to_IStatistics(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IStatistics
+        verifyClass(IStatistics, self._getTargetClass())
+
+    def test_instance_conforms_to_IStatistics(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IStatistics
+        verifyObject(IStatistics, self._makeOne())
+
+    def test_ctor_defaults(self):
+        import BTrees
+        index = self._makeOne()
+        self.failUnless(index.family is BTrees.family32)
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+
+    def test_ctor_explicit_family(self):
+        import BTrees
+        index = self._makeOne(BTrees.family64)
+        self.failUnless(index.family is BTrees.family64)
+
+    def test_index_doc_new(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failUnless('value' in index._fwd_index)
+
+    def test_index_doc_existing_same_value(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.index_doc(1, 'value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failUnless('value' in index._fwd_index)
+        self.assertEqual(list(index._fwd_index['value']), [1])
+
+    def test_index_doc_existing_new_value(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.index_doc(1, 'new_value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+        self.failUnless('new_value' in index._fwd_index)
+        self.assertEqual(list(index._fwd_index['new_value']), [1])
+
+    def test_unindex_doc_nonesuch(self):
+        index = self._makeOne()
+        index.unindex_doc(1) # doesn't raise
+
+    def test_unindex_doc_no_residual_fwd_values(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.unindex_doc(1) # doesn't raise
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+        self.failIf(1 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+
+    def test_unindex_doc_w_residual_fwd_values(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        self.assertRaises(
+            KeyError,
+            index.index_doc, 2, 'value')
+        index.unindex_doc(1) # doesn't raise
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+        self.failIf(1 in index._rev_index)
+        self.failIf(2 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+        self.assertRaises(
+            KeyError,
+            index._fwd_index.__getitem__, 'value')
+        self.assertEqual(index._fwd_index.get('value'), None)
+
+    def test_apply_non_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ['a', 'b'])
+
+    def test_apply_empty_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a',))
+
+    def test_apply_one_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a',))
+
+    def test_apply_three_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a', 'b', 'c'))
+
+    def test_apply_two_tuple_miss(self):
+        index = self._makeOne()
+        self.assertEqual(list(index.apply(('a', 'b'))), [])
+
+    def test_apply_two_tuple_hit(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.apply(('a', 'b'))), [1])
+
+    def test_sort_w_limit_lt_1(self):
+        index = self._makeOne()
+        self.assertRaises(ValueError,
+                          lambda: list(index.sort([1, 2, 3], limit=0)))
+
+    def test_sort_w_empty_index(self):
+        index = self._makeOne()
+        self.assertEqual(list(index.sort([1, 2, 3])), [])
+
+    def test_sort_w_empty_docids(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([])), [])
+
+    def test_sort_w_missing_docids(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_force_nbest_w_missing_docids(self):
+        index = self._makeOne()
+        index._use_nbest = True
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_force_lazy_w_missing_docids(self):
+        index = self._makeOne()
+        index._use_lazy = True
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_lazy_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_lazy = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4])
+
+    def test_sort_lazy_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_lazy = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nonlazy_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4])
+
+    def test_sort_nonlazy_missingdocid(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4]) # 99 not present
+
+    def test_sort_nonlazy_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nonlazy_reverse_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True)
+        self.assertEqual(list(result), [4, 3, 1, 2, 5])
+
+    def test_sort_nonlazy_reverse_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nbest(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nbest_reverse(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nbest_missing(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nbest_missing_reverse(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nodocs(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [])
+
+    def test_sort_nodocids(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet()
+        result = index.sort(c1)
+        self.assertEqual(list(result), [])
+
+    def test_sort_badlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=0)
+        self.assertRaises(ValueError, list, result)
+
+
+class TestCatalogBugs(PlacelessSetup, unittest.TestCase):
+    """I found that z.a.catalog, AttributeIndex failed to remove the previous
+    value/object from the index IF the NEW value is None.
+    """
+
+    def test_updateIndexWithNone(self):
+        uidutil = IntIdsStub()
+        provideUtility(uidutil, IIntIds)
+
+        catalog = Catalog()
+        index = FieldIndex('author', None)
+        catalog['author'] = index
+
+        ob1 = stoopid(author = "joe")
+        ob1id = uidutil.register(ob1)
+        catalog.index_doc(ob1id, ob1)
+
+        res = catalog.searchResults(author=('joe','joe'))
+        names = [x.author for x in res]
+        names.sort()
+        self.assertEqual(len(names), 1)
+        self.assertEqual(names, ['joe'])
+
+        ob1.author = None
+        catalog.index_doc(ob1id, ob1)
+
+        #the index must be empty now because None values are never indexed
+        res = catalog.searchResults(author=(None, None))
+        self.assertEqual(len(res), 0)
+
+    def test_updateIndexFromCallableWithNone(self):
+        uidutil = IntIdsStub()
+        provideUtility(uidutil, IIntIds)
+
+        catalog = Catalog()
+        index = FieldIndex('getAuthor', None, field_callable=True)
+        catalog['author'] = index
+
+        ob1 = stoopidCallable(author = "joe")
+
+        ob1id = uidutil.register(ob1)
+        catalog.index_doc(ob1id, ob1)
+
+        res = catalog.searchResults(author=('joe','joe'))
+        names = [x.author for x in res]
+        names.sort()
+        self.assertEqual(len(names), 1)
+        self.assertEqual(names, ['joe'])
+
+        ob1.author = None
+        catalog.index_doc(ob1id, ob1)
+
+        #the index must be empty now because None values are never indexed
+        res = catalog.searchResults(author=(None, None))
+        self.assertEqual(len(res), 0)
+
+def test_suite():
+    from zope.testing import doctest
+    return unittest.TestSuite((
+        doctest.DocFileSuite('README.txt', optionflags=doctest.ELLIPSIS),
+        unittest.makeSuite(RawUniqueFieldIndexTests),
+        unittest.makeSuite(TestCatalogBugs),
+        ))
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: /main/waeup.sirp/branches/unique-index/uniquefield.py
===================================================================
--- /main/waeup.sirp/branches/unique-index/uniquefield.py	(revision 6780)
+++ /main/waeup.sirp/branches/unique-index/uniquefield.py	(revision 6780)
@@ -0,0 +1,57 @@
+##
+## uniquefield.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat May 28 13:39:15 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Unique field indexes.
+"""
+from grok.index import IndexDefinition
+from zope.catalog.field import FieldIndex
+from zope.index.field import FieldIndex as RawFieldIndex
+from zope.interface import implements
+from waeup.sirp.index.interfaces import IUniqueFieldIndex, IUnique
+
+class RawUniqueFieldIndex(RawFieldIndex):
+    """Unique field index.
+    """
+    implements(IUnique)
+
+    def index_doc(self, docid, value):
+        """Index some doc.
+        """
+        entries = self._fwd_index.get(value, ())
+        if len(entries) > 0 and docid not in entries:
+            # Accept already existing entries if docid matches
+            # ('reindex' use case). If docid is not contained already,
+            # we have a unique-constraint violation.
+            raise KeyError(
+                'The value already exists in catalog: %s' % value,)
+        return super(RawUniqueFieldIndex, self).index_doc(docid, value)
+
+class UniqueFieldIndex(FieldIndex):
+    implements(IUniqueFieldIndex)
+
+class UniqueField(IndexDefinition):
+    """A :class:`grok.Indexes` index that matches against an entire field.
+
+    Contrary to :class:`grok.index.Field` this index only accepts
+    unique values.
+    """
+    index_class = UniqueFieldIndex
