Commit 1c143ee4 authored by Jérome Perrin's avatar Jérome Perrin

Allow to force the key behaviour of by passing a key= argument to a Query constructor



git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@17167 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 1c626480
...@@ -2017,6 +2017,20 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor): ...@@ -2017,6 +2017,20 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
result = sql_connection.manage_test(sql % sub_obj.getUid()) result = sql_connection.manage_test(sql % sub_obj.getUid())
self.assertSameSet(['little_owner'], [x.owner for x in result]) self.assertSameSet(['little_owner'], [x.owner for x in result])
def test_ExactMatchSearch(self):
# test exact match search with queries
doc = self._makeOrganisation(title='Foo%')
other_doc = self._makeOrganisation(title='FooBar')
ctool = self.getCatalogTool()
# by default, % in catalog search is a wildcard:
self.assertEquals(sorted([doc, other_doc]),
sorted([x.getObject() for x in
ctool(portal_type='Organisation', title='Foo%')]))
# ... but you can force searches with an exact match key
self.assertEquals([doc], [x.getObject() for x in
ctool(portal_type='Organisation', title=dict(query='Foo%',
key='ExactMatch'))])
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
......
...@@ -181,6 +181,13 @@ class UidBuffer(TM): ...@@ -181,6 +181,13 @@ class UidBuffer(TM):
tid = get_ident() tid = get_ident()
self.temporary_buffer.setdefault(tid, []).extend(iterable) self.temporary_buffer.setdefault(tid, []).extend(iterable)
# valid search modes for queries
FULL_TEXT_SEARCH_MODE = 'FullText'
EXACT_MATCH_SEARCH_MODE = 'ExactMatch'
KEYWORD_SEARCH_MODE = 'Keyword'
class QueryMixin: class QueryMixin:
operator = None operator = None
...@@ -289,7 +296,7 @@ class Query(QueryMixin): ...@@ -289,7 +296,7 @@ class Query(QueryMixin):
format - type date : %d/%m/%Y format - type date : %d/%m/%Y
type float : 1 234.12 type float : 1 234.12
""" """
def __init__(self, format=None, operator=None, range=None, def __init__(self, format=None, operator=None, range=None, key=None,
search_mode=None, table_alias_list=None, type=None, **kw): search_mode=None, table_alias_list=None, type=None, **kw):
self.format = format self.format = format
if operator is None: if operator is None:
...@@ -304,6 +311,7 @@ class Query(QueryMixin): ...@@ -304,6 +311,7 @@ class Query(QueryMixin):
self.key = key_list[0] self.key = key_list[0]
self.value = kw[self.key] self.value = kw[self.key]
self.type = type self.type = type
self.search_key = key
def __call__(self): def __call__(self):
self.asSQLExpression() self.asSQLExpression()
...@@ -322,6 +330,8 @@ class Query(QueryMixin): ...@@ -322,6 +330,8 @@ class Query(QueryMixin):
return result return result
def getSearchMode(self): def getSearchMode(self):
"""Search mode used for Full Text search
"""
return self.search_mode return self.search_mode
def asSearchTextExpression(self): def asSearchTextExpression(self):
...@@ -345,7 +355,8 @@ class Query(QueryMixin): ...@@ -345,7 +355,8 @@ class Query(QueryMixin):
sql_expression = '' sql_expression = ''
value = self.getValue() value = self.getValue()
key = self.getKey() key = self.getKey()
ignore_key=0 search_key = self.search_key
ignore_key = 0
if key_alias_dict is not None: if key_alias_dict is not None:
# Try to find the alias # Try to find the alias
if key not in key_alias_dict: if key not in key_alias_dict:
...@@ -387,10 +398,10 @@ class Query(QueryMixin): ...@@ -387,10 +398,10 @@ class Query(QueryMixin):
where_expression.append("%s > %s" % (key, query_max)) where_expression.append("%s > %s" % (key, query_max))
elif isSimpleType(value) or isinstance(value, DateTime) \ elif isSimpleType(value) or isinstance(value, DateTime) \
or (isinstance(value, (list, tuple)) and self.operator.upper() != 'IN'): or (isinstance(value, (list, tuple)) and self.operator.upper() != 'IN'):
# Convert into lists any value which contain a ; # Convert into lists any value which contain 'OR'
# Refer to _listGlobalActions DCWorkflow patch # Refer to _listGlobalActions DCWorkflow patch for example of use
# for example of use if isinstance(value, basestring) \
if isinstance(value, basestring): and search_key != EXACT_MATCH_SEARCH_MODE:
value = value.split(' OR ') value = value.split(' OR ')
value = map(lambda x:x.strip(), value) value = map(lambda x:x.strip(), value)
value_list = value value_list = value
...@@ -401,7 +412,7 @@ class Query(QueryMixin): ...@@ -401,7 +412,7 @@ class Query(QueryMixin):
comparison_operator = None comparison_operator = None
if (value != '' or not ignore_empty_string) \ if (value != '' or not ignore_empty_string) \
and isinstance(value, basestring): and isinstance(value, basestring):
if '%' in value: if '%' in value and search_key != EXACT_MATCH_SEARCH_MODE:
comparison_operator = 'LIKE' comparison_operator = 'LIKE'
elif len(value) >= 1 and value[0:2] in ('<=','!=','>='): elif len(value) >= 1 and value[0:2] in ('<=','!=','>='):
comparison_operator = value[0:2] comparison_operator = value[0:2]
...@@ -409,11 +420,15 @@ class Query(QueryMixin): ...@@ -409,11 +420,15 @@ class Query(QueryMixin):
elif len(value) >= 1 and value[0] in ('=','>','<'): elif len(value) >= 1 and value[0] in ('=','>','<'):
comparison_operator = value[0] comparison_operator = value[0]
value = value[1:] value = value[1:]
elif key in keyword_search_keys: elif search_key == KEYWORD_SEARCH_MODE or (
key in keyword_search_keys and
search_key != EXACT_MATCH_SEARCH_MODE):
# We must add % in the request to simulate the catalog # We must add % in the request to simulate the catalog
comparison_operator = 'LIKE' comparison_operator = 'LIKE'
value = '%%%s%%' % value value = '%%%s%%' % value
elif key in full_text_search_keys: elif search_key == FULL_TEXT_SEARCH_MODE or (
key in full_text_search_keys
and search_key != EXACT_MATCH_SEARCH_MODE):
# We must add % in the request to simulate the catalog # We must add % in the request to simulate the catalog
# we first check if there is a special search_mode for this key # we first check if there is a special search_mode for this key
# incl. table name, or for all keys of that name, # incl. table name, or for all keys of that name,
......
...@@ -39,16 +39,13 @@ from Products.ZSQLCatalog.SQLCatalog import Query ...@@ -39,16 +39,13 @@ from Products.ZSQLCatalog.SQLCatalog import Query
from Products.ZSQLCatalog.SQLCatalog import ComplexQuery from Products.ZSQLCatalog.SQLCatalog import ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import NegatedQuery from Products.ZSQLCatalog.SQLCatalog import NegatedQuery
try:
from transaction import get as get_transaction
except ImportError:
pass
class TestZSQLCatalog(unittest.TestCase): class TestZSQLCatalog(unittest.TestCase):
"""Tests for ZSQL Catalog. """Tests for ZSQL Catalog.
""" """
def setUp(self): def setUp(self):
self._catalog = ZSQLCatalog() self._catalog = ZSQLCatalog()
# TODO ?
class TestSQLCatalog(unittest.TestCase): class TestSQLCatalog(unittest.TestCase):
...@@ -100,6 +97,8 @@ class TestSQLCatalog(unittest.TestCase): ...@@ -100,6 +97,8 @@ class TestSQLCatalog(unittest.TestCase):
class TestQuery(unittest.TestCase): class TestQuery(unittest.TestCase):
"""Test SQL bits generated from Queries
"""
def testSimpleQuery(self): def testSimpleQuery(self):
q = Query(title='Foo') q = Query(title='Foo')
self.assertEquals( self.assertEquals(
...@@ -107,6 +106,11 @@ class TestQuery(unittest.TestCase): ...@@ -107,6 +106,11 @@ class TestQuery(unittest.TestCase):
select_expression_list=[]), select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[])) q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
def testQueryMultipleKeys(self):
# using multiple keys is invalid and raises
# KeyError: 'Query must have only one key'
self.assertRaises(KeyError, Query, title='Foo', reference='bar')
def testNoneQuery(self): def testNoneQuery(self):
q = Query(title=None) q = Query(title=None)
self.assertEquals( self.assertEquals(
...@@ -168,6 +172,7 @@ class TestQuery(unittest.TestCase): ...@@ -168,6 +172,7 @@ class TestQuery(unittest.TestCase):
select_expression_list=[]), select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[])) q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
# format
def testDateFormat(self): def testDateFormat(self):
q = Query(date=DateTime(2001, 02, 03), format='%Y/%m/%d', type='date') q = Query(date=DateTime(2001, 02, 03), format='%Y/%m/%d', type='date')
self.assertEquals( self.assertEquals(
...@@ -177,6 +182,7 @@ class TestQuery(unittest.TestCase): ...@@ -177,6 +182,7 @@ class TestQuery(unittest.TestCase):
select_expression_list=[]), select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[])) q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
# full text
def testSimpleQueryFullText(self): def testSimpleQueryFullText(self):
q = Query(title='Foo') q = Query(title='Foo')
self.assertEquals(dict(where_expression="MATCH title AGAINST ('Foo' )", self.assertEquals(dict(where_expression="MATCH title AGAINST ('Foo' )",
...@@ -185,7 +191,28 @@ class TestQuery(unittest.TestCase): ...@@ -185,7 +191,28 @@ class TestQuery(unittest.TestCase):
q.asSQLExpression(keyword_search_keys=[], q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=['title'])) full_text_search_keys=['title']))
def testSimpleQuerySearchKey(self): def testSimpleQueryFullTextSearchMode(self):
q = Query(title='Foo',
search_mode='in_boolean_mode')
self.assertEquals(dict(
where_expression="MATCH title AGAINST ('Foo' IN BOOLEAN MODE)",
select_expression_list=
["MATCH title AGAINST ('Foo' IN BOOLEAN MODE) AS title_relevance"]),
q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=['title']))
def testSimpleQueryFullTextStat__(self):
# stat__ is an internal implementation artifact to prevent adding
# select_expression for countFolder
q = Query(title='Foo')
self.assertEquals(dict(
where_expression="MATCH title AGAINST ('Foo' )",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=['title'],
stat__=1))
def testSimpleQueryKeywordSearchKey(self):
q = Query(title='Foo') q = Query(title='Foo')
self.assertEquals(dict(where_expression="title LIKE '%Foo%'", self.assertEquals(dict(where_expression="title LIKE '%Foo%'",
select_expression_list=[]), select_expression_list=[]),
...@@ -201,6 +228,7 @@ class TestQuery(unittest.TestCase): ...@@ -201,6 +228,7 @@ class TestQuery(unittest.TestCase):
q.asSQLExpression(keyword_search_keys=[], q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=[])) full_text_search_keys=[]))
# complex queries
def testSimpleComplexQuery(self): def testSimpleComplexQuery(self):
q1 = Query(title='Foo') q1 = Query(title='Foo')
q2 = Query(reference='Bar') q2 = Query(reference='Bar')
...@@ -223,6 +251,33 @@ class TestQuery(unittest.TestCase): ...@@ -223,6 +251,33 @@ class TestQuery(unittest.TestCase):
q.asSQLExpression(keyword_search_keys=[], q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=[])) full_text_search_keys=[]))
# forced keys
def testSimpleQueryForcedKeywordSearchKey(self):
q = Query(title='Foo', key='Keyword')
self.assertEquals("title LIKE '%Foo%'",
q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=[])['where_expression'])
def testSimpleQueryForcedFullText(self):
q = Query(title='Foo', key='FullText')
self.assertEquals("MATCH title AGAINST ('Foo' )",
q.asSQLExpression(keyword_search_keys=[],
full_text_search_keys=[])['where_expression'])
def testSimpleQueryForcedExactMatch(self):
q = Query(title='Foo', key='ExactMatch')
self.assertEquals("title = 'Foo'",
q.asSQLExpression(keyword_search_keys=['title'],
full_text_search_keys=[])['where_expression'])
def testSimpleQueryForcedExactMatchOR(self):
q = Query(title='Foo% OR %?ar', key='ExactMatch')
self.assertEquals("title = 'Foo% OR %?ar'",
q.asSQLExpression(keyword_search_keys=['title'],
full_text_search_keys=[])['where_expression'])
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestSQLCatalog)) suite.addTest(unittest.makeSuite(TestSQLCatalog))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment