Commit 3aeb0ab9 authored by Ivan Tyagov's avatar Ivan Tyagov

Refactoring of ZSQLCatalog.

Introduce search key which now are reposnsible for Query generation.
Improve parsing of search strings (you must install "ply":http://www.dabeaz.com/ply/).



git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@19152 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 8d482a11
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Products.PythonScripts.Utility import allow_class
from Query import QueryMixin
class ComplexQuery(QueryMixin):
"""
Used in order to concatenate many queries
"""
def __init__(self, *args, **kw):
self.query_list = args
self.operator = kw.pop('operator', 'AND')
# XXX: What is that used for ?! It's utterly dangerous.
#self.__dict__.update(kw)
def getQueryList(self):
return self.query_list
def getRelatedTableMapDict(self):
result = {}
for query in self.getQueryList():
if not(isinstance(query, basestring)):
result.update(query.getRelatedTableMapDict())
return result
def asSQLExpression(self, key_alias_dict=None,
ignore_empty_string=1,
keyword_search_keys=None,
datetime_search_keys=None,
full_text_search_keys=None,
stat__=0):
"""
Build the sql string
"""
sql_expression_list = []
select_expression_list = []
for query in self.getQueryList():
if isinstance(query, basestring):
sql_expression_list.append(query)
else:
query_result = query.asSQLExpression(key_alias_dict=key_alias_dict,
ignore_empty_string=ignore_empty_string,
keyword_search_keys=keyword_search_keys,
datetime_search_keys=datetime_search_keys,
full_text_search_keys=full_text_search_keys,
stat__=stat__)
sql_expression_list.append(query_result['where_expression'])
select_expression_list.extend(query_result['select_expression_list'])
operator = self.getOperator()
result = {'where_expression':('(%s)' % \
(' %s ' % operator).join(['(%s)' % x for x in sql_expression_list])),
'select_expression_list':select_expression_list}
return result
def getSQLKeyList(self):
"""
Returns the list of keys used by this
instance
"""
key_list=[]
for query in self.getQueryList():
if not(isinstance(query, basestring)):
key_list.extend(query.getSQLKeyList())
return key_list
allow_class(ComplexQuery)
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
class QueryMixin:
"""
Mixing class which implements methods which are
common to all kinds of Queries
"""
operator = None
format = None
type = None
def __call__(self, **kw):
return self.asSQLExpression(**kw)
def getOperator(self):
return self.operator
def getFormat(self):
return self.format
def getType(self):
return self.type
def getRange(self):
return self.range
def getTableAliasList(self):
return self.table_alias_list
def getSearchMode(self):
"""Search mode used for Full Text search
"""
return self.search_mode
def getSearchKey(self):
"""Search mode used for Full Text search
"""
return self.search_key
def getKey(self):
return self.key
def getValue(self):
return self.value
def getOperator(self):
return self.operator.upper().strip()
def asSearchTextExpression(self):
raise NotImplementedError
def asSQLExpression(self, key_alias_dict=None,
keyword_search_keys=None,
datetime_search_keys=None,
full_text_search_keys=None,
ignore_empty_string=1, stat__=0):
"""
Return a dictionnary containing the keys and value types:
'where_expression': string
'select_expression_list': string
"""
raise NotImplementedError
def getSQLKeyList(self):
"""
Return a list of keys used by this query and its subqueries.
"""
raise NotImplementedError
def getRelatedTableMapDict(self):
"""
Return for each key used by this query (plus ones used by its
subqueries) the table alias mapping.
"""
raise NotImplementedError
def _quoteSQLString(self, value):
"""Return a quoted string of the value.
XXX: Left for backwards compatability!
"""
format = self.getFormat()
type = self.getType()
if format is not None and type is not None:
if type == 'date':
if hasattr(value, 'strftime'):
value = value.strftime(format)
if isinstance(value, basestring):
value = "STR_TO_DATE('%s','%s')" % (value, format)
if type == 'float':
# Make sure there is no space in float values
value = value.replace(' ','')
value = "'%s'" % value
else:
if getattr(value, 'ISO', None) is not None:
value = "'%s'" % value.toZone('UTC').ISO()
else:
value = "'%s'" % sql_quote(str(value))
return value
def _quoteSQLKey(self, key):
"""Return a quoted string of the value.
XXX: Left for backwards compatability!
"""
format = self.getFormat()
type = self.getType()
if format is not None and type is not None:
if type == 'date':
key = "STR_TO_DATE(DATE_FORMAT(%s,'%s'),'%s')" % (key, format, format)
if type == 'float':
float_format = format.replace(' ','')
if float_format.find('.') >= 0:
precision = len(float_format.split('.')[1])
key = "TRUNCATE(%s,%s)" % (key, precision)
return key
This diff is collapsed.
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery as Query
from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
from DateTime import DateTime
from Key import BaseKey
from pprint import pprint
class DateTimeKey(BaseKey):
""" DateTimeKey key is an ERP5 portal_catalog search key which is used to render
SQL expression that will try to match values in DateTime MySQL columns.
It supports following special operator ['=', '%', '>' , '>=', '<', '<='] in
addition to main logical operators like ['OR', 'or', 'AND', 'and'].
Note: because all ERP5 datetime values are indexed in MySQL in 'UTC'
the respective passed date will be first converted to 'UTC' before inserted into
respective SQL query!
Examples (GMT+02, Bulgaria/Sofia for 'delivery.start_date'):
* '15/01/2008' --> "delivery.start_date = '2008-01-14 22:00'"
* '>=15/01/2008' --> "delivery.start_date >= '2008-01-14 22:00'"
* '>=15/01/2008 or <=20/01/2008'
--> "delivery.start_date >= '2008-01-14 22:00' or delivery.start_date<='2008-01-19 22:00'"
* '>=15/01/2008 10:00 GMT+02 OR <=20/01/2008 05:12 Universal'
-->
"delivery.start_date >= '2008-01-15 08:00 Universal'
OR
delivery.start_date <= '2008-01-20 05:12 Universal'
"
"""
tokens = ('DATE', 'OR', 'AND', 'NOT', 'EQUAL',
'GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL')
sub_operators = ('GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL', 'NOT', 'EQUAL',)
def t_OR(self, t):
r'(\s+OR\s+|\s+or\s+)'
# operator has leading and trailing ONLY one white space character
t.value = 'OR'
return t
def t_AND(self, t):
r'(\s+AND\s+|\s+and\s+)'
# operator has leading and trailing ONLY one white space character
t.value = 'AND'
return t
def t_NOT(self, t):
r'(\s+NOT\s+|\s+not\s+|!=)'
# operator has leading and trailing ONLY one white space character
t.value = t.value.upper().strip()
return t
t_GREATERTHANEQUAL = r'>='
t_LESSTHANEQUAL = r'<='
t_GREATERTHAN = r'>'
t_LESSTHAN = r'<'
t_EQUAL = r'='
t_DATE = r'\d{1,4}[(/|\.|\-) /.]\d{1,4}[(/|\.|\-) /.]\d{1,4}((\s.)*\d{0,2}:\d{0,2}(:\d{0,2})?)?(\sUniversal|\sGMT\+\d\d)?|\d\d\d\d%?'
def quoteSQLString(self, value, format):
""" Return a quoted string of the value.
Make sure to convert it to UTC first."""
if getattr(value, 'ISO', None) is not None:
value = "'%s'" % value.toZone('UTC').ISO()
else:
value = "'%s'" %DateTime(value).toZone('UTC').ISO()
return value
def buildQueryForTokenList(self, tokens, key, value, format):
""" Build a ComplexQuery for a token list """
query_list = []
for group_tokens in self.groupByLogicalOperator(tokens, 'AND'):
token_values = [x.value for x in group_tokens]
sub_operator, sub_tokens = self.getOperatorForTokenList(group_tokens)
date_value = sub_tokens[0].value
days_offset = 0
# some format require special handling
if format != '%Y':
# full format (Year/Month/Day)
if sub_operator in ('=',):
# 2007/01/01 00:00 <= date < 2007/01/02
days_offset = 1
elif format == '%Y':
# incomplete format only Year because DateTime can not handle
# extend format and value by assumption that start of year is ment
# add days ofset accordingly
format = '%%%s/%%m/%%d' %format
date_value = '%s/01/01' %date_value
days_offset_map = {'=' : 366, '>' : 366,
'>=' : 366, '<': -366, '<=':-366}
days_offset = days_offset_map[sub_operator]
# convert to UTC in given format
is_valid_date = 1
try:
if format != '%m/%d/%Y':
# treat ambigious dates as "days before month before year"
date_value = DateTime(date_value, datefmt="international").toZone('UTC')
else:
# US style "month before day before year"
date_value = DateTime(date_value).toZone('UTC')
except:
is_valid_date = 0
query_kw = None
if is_valid_date:
if sub_operator == '=':
# transform to range 'key >= date AND date < key'
query_kw = {key: (date_value, date_value + days_offset,),
'range': 'minmax'}
else:
query_kw = {key: date_value + days_offset,
'range': sub_operator}
query_kw['type'] = 'date'
else:
# not a valid date, try to get an year range
is_year = 1
date_value = date_value.replace('%', '')
try: date_value = int(date_value)
except: is_year = 0
if is_year:
date_value = '%s/01/01' % date_value
date_value = DateTime(date_value).toZone('UTC')
query_kw = {key: (date_value, date_value + 366,),
'type': 'date',
'range': 'minmax'}
# append only if it was possible to generate query
if query_kw is not None:
query_list.append(Query(**query_kw))
# join query list in one really big ComplexQuery
if len(query_list):
complex_query = ComplexQuery(*query_list,
**{'operator': 'AND'})
return complex_query
## def buildSQLExpressionFromSearchString(self, key, value, format, mode, range_value, stat__):
## """ Tokenize/analyze passed string value and generate SQL query expressions. """
## where_expression = ''
## key = self.quoteSQLKey(key, format)
## tokens = self.tokenize(value)
## operators_mapping_list = self.groupByOperator(tokens)
## # new one
## for item in operators_mapping_list:
## row_tokens_values = []
## tokens = item['tokens']
## operator = item['operator']
## operator_value = None
## if operator is not None:
## # operator is standalone expression
## operator_value = operator.value
## where_expressions.append('%s' %operator_value)
## if len(tokens):
## # no it's not a stand alone expression,
## # determine it from list of tokens
## operator_value, sub_tokens = self.getOperatorForTokenList(tokens)
## row_tokens_values = [self.quoteSQLString(x.value, format) for x in sub_tokens]
## where_expression = "%s %s %s" %(key, operator_value, ' '.join(row_tokens_values))
## return where_expression, []
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Key import BaseKey
from pprint import pprint
class DefaultKey(BaseKey):
""" DefaultKey key is an ERP5 portal_catalog search key which is used to render
SQL expression that will try to exactly one value.
It supports following special operator ['=', '%', '>' , '>=', '<', '<='] in
addition to main logical operators like ['OR', 'or', 'AND', 'and'].
Examples for title column:
* 'foo or bar' --> "title = 'foo' OR title = 'bar'"
* 'foo or =bar' --> "title = 'foo' OR title = 'bar'"
* '%foo% or bar' --> "title = '%foo%' OR title = 'bar'"
* 'Organisation Module' --> "title = 'Organisation Module'"
* '"Organisation Module"' --> "title = 'Organisation Module'"
* '="Organisation Module"' --> "title = 'Organisation Module'"
"""
# default type of sub Queries to be generated out fo a search string
default_key_type = 'default'
tokens = ('OR', 'AND', 'NOT', 'WORDSET', 'WORD',
'GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL')
sub_operators = ('GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL', 'NOT')
# Note: Order of placing rules (t_WORD for example) is very important
def t_OR(self, t):
r'(\s+OR\s+|\s+or\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'OR'
return t
def t_AND(self, t):
r'(\s+AND\s+|\s+and\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'AND'
return t
def t_NOT(self, t):
r'(\s+NOT\s+|\s+not\s+|!=)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = '!='
return t
t_GREATERTHANEQUAL = r'>='
t_LESSTHANEQUAL = r'<='
t_GREATERTHAN = r'>'
t_LESSTHAN = r'<'
def t_WORD(self, t):
r'[\x7F-\xFF\w\d\/~!@#$%^&*()_+\n][\x7F-\xFF\w\d\/~!@#$%^&*()_+\n]*'
#r'[\x7F-\xFF\w\d\/%][\x7F-\xFF\w\d\/%]*'
# WORD may contain arbitrary letters and numbers without white space
# WORD may contain '%' but not at the beginning or end (otherwise it's KEYWORD)
value = t.value.strip()
t.value = "%s" %value
return t
def t_WORDSET(self, t):
r'"[\x7F-\xFF\w\d\s\/~!@#$%^&*()_+][\x7F-\xFF\w\d\s\/~!@#$%^&*()_+]*"'
#r'"[\x7F-\xFF\w\d\s/%][\x7F-\xFF\w\d\s/%]*"'
# WORDSET is a combination of WORDs separated by white space
# and starting/ending with "
value = t.value.replace('"', '').strip()
t.value = "%s" %value
return t
def quoteSQLString(self, value, format):
""" Return a quoted string of the value. """
if isinstance(value, (int, long,)):
return str(value)
return "'%s'" %value
## def buildSQLExpressionFromSearchString(self, key, value, format, mode, range_value, stat__):
## """ Tokenize/analyze passed string value and generate SQL query expressions. """
## where_expressions = []
## select_expressions = []
## tokens = self.tokenize(value)
## operators_mapping_list = self.groupByOperator(tokens)
##
## # find if any logical operator exists
## tokens_values = []
## logical_operator_found = 0
## for token in tokens:
## if token.type not in ('WORDSET', 'WORD',):
## logical_operator_found = 1
## break
## tokens_values.append(token.value.replace("'", ""))
##
## # build expressions
## if not logical_operator_found:
## # no logical operator found so we assume that we search for a combination of words
## where_expressions.append("%s = '%s'" %(key, ' '.join(tokens_values)))
## else:
## # in the search string we have explicitly defined an operator
## for item in operators_mapping_list:
## row_tokens_values = []
## tokens = item['tokens']
## operator = item['operator']
## operator_value = None
## if operator is not None:
## # operator is standalone expression
## operator_value = operator.value
## where_expressions.append('%s' %operator_value)
## if len(tokens):
## # no it's not a stand alone expression,
## # determine it from list of tokens
## operator_value, sub_tokens = self.getOperatorForTokenList(tokens)
## row_tokens_values = [x.value for x in sub_tokens]
## where_expressions.append("%s %s '%s'" %(key, operator_value, ' '.join(row_tokens_values)))
## return where_expressions, select_expressions
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Key import BaseKey
class FloatKey(BaseKey):
""" FloatKey key is an ERP5 portal_catalog search key which is used to render
float like SQL expression.
"""
# default type of sub Queries to be generated out fo a search string
default_key_type = 'float'
tokens = ('OR', 'AND', 'NOT', 'FLOAT',
'GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL')
sub_operators = ('GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL', 'NOT')
# Note: Order of placing rules (t_WORD for example) is very important
def t_OR(self, t):
r'(\s+OR\s+|\s+or\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'OR'
return t
def t_AND(self, t):
r'(\s+AND\s+|\s+and\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'AND'
return t
def t_NOT(self, t):
r'(\s+NOT\s+|\s+not\s+|!=)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = '!='
return t
t_GREATERTHANEQUAL = r'>='
t_LESSTHANEQUAL = r'<='
t_GREATERTHAN = r'>'
t_LESSTHAN = r'<'
def t_FLOAT(self, t):
r'[\d.][\d.]*'
# FLOAT is a float number
value = t.value.replace('"', '').strip()
t.value = "%s" %value
return t
def quoteSQLString(self, value, format):
""" Return a quoted string of the value. """
# Make sure there is no space in float values
return "'%s'" %str(value).replace(' ', '')
def quoteSQLKey(self, key, format):
""" Return a quoted string of the value. """
if format is not None:
float_format = format.replace(' ', '')
if float_format.find('.') >= 0:
precision = len(float_format.split('.')[1])
key = "TRUNCATE(%s,%s)" % (key, precision)
return key
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Key import BaseKey
SEARCH_MODE_MAPPING = {'in_boolean_mode': 'IN BOOLEAN MODE',
'with_query_expansion': 'WITH QUERY EXPANSION'}
class FullTextKey(BaseKey):
""" FullTextKey key is an ERP5 portal_catalog search key which is used to render
SQL expression that will try match all possible values using
MySQL's fulltext search support.
See syntax see MySQL's FullText search reference:
http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html
"""
tokens = ('PLUS', 'MINUS', 'WORD', 'GREATERTHAN', 'LESSTHAN', 'LEFTPARENTHES',
'RIGHTPARENTHES', 'TILDE', 'ASTERISK', 'DOUBLEQUOTE',)
# SQL expressions patterns
relevance = '%s_relevance'
where_match_against = "MATCH %s AGAINST ('%s' %s)"
select_match_against_as = "MATCH %s AGAINST ('%s' %s) AS %s"
t_PLUS = r'(\+)'
t_MINUS = r'(\-)'
t_GREATERTHAN = r'(\>)'
t_LESSTHAN = r'(\<)'
t_LEFTPARENTHES = r'(\()'
t_RIGHTPARENTHES = r'(\))'
t_TILDE = r'(\~)'
t_ASTERISK = r'(\*)'
t_DOUBLEQUOTE = r'(\")'
def t_WORD(self, t):
r'[\x7F-\xFF\w\d\/!@#$%^&_][\x7F-\xFF\w\d\/!@#$%^&_]*'
#r'[\x7F-\xFF\w\d][\x7F-\xFF\w\d]*'
# WORD may contain arbitrary letters and numbers without white space
word_value = t.value
t.value = "'%s'" %word_value
return t
def buildSQLExpression(self, key, value,
format=None, mode=None, range_value=None, stat__=None):
""" Analize token list and generate SQL expressions."""
tokens = self.tokenize(value)
# based on type tokens we may switch to different search mode
mode = SEARCH_MODE_MAPPING.get(mode, '')
if mode == '':
# determine it based on list of tokens i.e if we have only words
# leave as its but if we have '-' or '+' use boolean mode
for token in tokens:
if token.type != 'WORD':
mode = SEARCH_MODE_MAPPING['in_boolean_mode']
break
# split (if possible) to column.key
if key.find('.') != -1:
table, column = key.split('.')
relevance_key1 = self.relevance %key.replace('.', '_')
relevance_key2 = self.relevance %column
else:
relevance_key1 = self.relevance %key
relevance_key2 = None
select_expression_list = []
where_expression = self.where_match_against %(key, value, mode)
if not stat__:
# stat__ is an internal implementation artifact to prevent adding
# select_expression for countFolder
select_expression_list = [self.select_match_against_as %(key, value, mode, relevance_key1),]
if relevance_key2 is not None:
select_expression_list.append(self.select_match_against_as %(key, value, mode, relevance_key2))
return where_expression, select_expression_list
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery as Query
from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
import ply.yacc as yacc
import ply.lex as lex
class BaseKey:
""" BaseKey is a base class that implements a parser of
search grammar used in ERP5. It also implements all generic
search key class methods."""
# main logical operators
operators = ('OR', 'AND',)
default_operator = '='
# in ERP5 search grammer white space is extremely important
# so we can not ignore it.
#t_ignore = ' \t'
# no need to rack down line numbers
#def t_newline(self, t):
# r'\n+'
# #t.lexer.lineno += len(t.value)
def t_error(self, t):
#print "Illegal character '%s'" % t.value[0]
t.lexer.skip(1)
def p_error(self, p):
pass
def build(self, **kwargs):
""" This method will initialize respective search key class with
tokens' definitions. """
self.lexer = lex.lex(object = self, **kwargs)
def tokenize(self, data):
""" Return list of tokens according to respective
search key tokens' definitions. """
result = []
self.lexer.input(data)
while 1:
tok = self.lexer.token()
if not tok:
break
result.append(tok)
return result
# Grouping of tokens
def getOperatorForTokenList(self, tokens):
""" Generic implementation that will return respective
operator for a token list. The first found occurence wins."""
token = tokens[0]
token_type = token.type
if token_type in self.sub_operators:
return token.value, tokens[1:]
else:
return self.default_operator, tokens
def groupByLogicalOperator(self, tokens, logical_operator ='OR'):
""" Split tokens list into one or many OR concatanated tokens list
"""
sub_tokens_or_groups = []
tmp_token_list = []
for token in tokens:
if token.type != logical_operator:
tmp_token_list.append(token)
else:
sub_tokens_or_groups.append(tmp_token_list)
tmp_token_list = []
# append remainig last tokens
sub_tokens_or_groups.append(tmp_token_list)
return sub_tokens_or_groups
# SQL quoting (each search key should override them it if needed)
def quoteSQLKey(self, key, format):
""" Return a quoted string of the value. """
return key
def quoteSQLString(self, value, format):
""" Return a quoted string of the value. """
return "'%s'" %value
# SQL generation
def buildSQLExpression(self, key, value,
format = None, mode = None, range_value = None, stat__=0):
""" Generic implementation. Leave details to respective key. """
if range_value is not None:
# if range_value we handle directly (i.e no parsing of search string)
where_expressions, select_expressions = \
self.buildSQLExpressionFromRange(key, value,
format, mode, range_value, stat__)
else:
# search string parsing is needed
where_expressions, select_expressions = \
self.buildSQLExpressionFromSearchString(key, str(value),
format, mode, range_value, stat__)
return where_expressions, select_expressions
def buildSQLExpressionFromSearchString(self, key, value, format, mode, range_value, stat__):
complex_query = self.buildQuery(key, value, format, mode, range_value, stat__)
if complex_query is None:
# Query could not be generated from search string
sql_expression = {'where_expression': '1',
'select_expression_list': []}
else:
sql_expression = complex_query(keyword_search_keys = [],
datetime_search_keys = [],
full_text_search_keys = [])
return sql_expression['where_expression'], sql_expression['select_expression_list']
def buildQuery(self, key, value, format, mode, range_value, stat__):
""" Build Query """
query_list = []
# tokenize searchs string into tokens for Search Key
tokens = self.tokenize(value)
# split tokens list into one or more 'OR' tokens lists
tokens_or_groups = self.groupByLogicalOperator(tokens, 'OR')
# remove empty tokens lists
tokens_or_groups = filter(lambda x: len(x), tokens_or_groups)
# get a ComplexQuery for a sub token list
for tokens_or_group in tokens_or_groups:
query = self.buildQueryForTokenList(tokens_or_group, key, value, format)
if query is not None:
# query could be generated for token list
query_list.append(query)
if len(query_list):
# join query list in one really big ComplexQuery
return ComplexQuery(*query_list,
**{'operator':'OR'})
def buildQueryForTokenList(self, tokens, key, value, format):
""" Build a ComplexQuery for a token list """
query_list = []
logical_groups = self.groupByLogicalOperator(tokens, 'AND')
for group_tokens in logical_groups:
token_values = [x.value for x in group_tokens]
sub_operator, sub_tokens = self.getOperatorForTokenList(group_tokens)
sub_tokens_values = [x.value for x in sub_tokens]
query_kw = {key: ' '.join(sub_tokens_values),
'type': self.default_key_type,
'format': format,
'range': sub_operator}
query_list.append(Query(**query_kw))
# join query list in one really big ComplexQuery
complex_query = ComplexQuery(*query_list,
**{'operator': 'AND'})
return complex_query
def buildSQLExpressionFromRange(self, key, value, format, mode, range_value, stat__):
""" This method will generate SQL expressions
from explicitly passed list of values and
range_value in ('min', 'max', ..)"""
key = self.quoteSQLKey(key, format)
where_expression = ''
select_expressions = []
if isinstance(value, (list, tuple)):
if len(value) > 1:
# value should contain at least two items
query_min = self.quoteSQLString(value[0], format)
query_max = self.quoteSQLString(value[1], format)
else:
# value contains only one item
query_min = query_max = self.quoteSQLString(value[0], format)
else:
query_min = query_max = self.quoteSQLString(value, format)
if range_value == 'min':
where_expression = "%s >= %s" % (key, query_min)
elif range_value == 'max':
where_expression = "%s < %s" % (key, query_max)
elif range_value == 'minmax' :
where_expression = "%s >= %s AND %s < %s" % (key, query_min, key, query_max)
elif range_value == 'minngt' :
where_expression = "%s >= %s AND %s <= %s" % (key, query_min, key, query_max)
elif range_value == 'ngt':
where_expression = "%s <= %s" % (key, query_max)
elif range_value == 'nlt':
where_expression = "%s > %s" % (key, query_max)
elif range_value == 'like':
where_expression = "%s LIKE %s" % (key, query_max)
elif range_value == 'not_like':
where_expression = "%s NOT LIKE %s" % (key, query_max)
elif range_value in ('=', '>', '<', '>=', '<=','!=',):
where_expression = "%s %s %s" % (key, range_value, query_max)
return where_expression, select_expressions
## def groupByOperator(self, tokens, group_by_operators_list = operators):
## """ Generic implementation of splitting tokens into logical
## groups defided by respective list of logical operator
## defined for respective search key. """
## items = []
## last_operator = None
## operators_mapping_list = []
## last_operator = {'operator': None,
## 'tokens': []}
## for token in tokens:
## token_type = token.type
## token_value = token.value
## if token_type in group_by_operators_list:
## # (re) init it
## last_operator = {'operator': token,
## 'tokens': []}
## operators_mapping_list.append(last_operator)
## else:
## # not an operator just a value token
## last_operator['tokens'].append(token)
## if last_operator not in operators_mapping_list:
## operators_mapping_list.append(last_operator)
## return operators_mapping_list
This diff is collapsed.
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
class RawKey:
""" RawKey key is an ERP5 portal_catalog search key which is used to render
SQL expression that will match exactly what's passed to it using equality ."""
def build(self, **kwargs):
# this key doesn't require parsing
# It's required to implement it as it's used ONLY for ExactMath
pass
def buildSQLExpression(self, key, value,
format=None, mode=None, range_value=None, stat__=None):
where_expression = "%s = '%s'" %(key, value)
return where_expression, []
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
# Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# 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.
#
##############################################################################
from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery as Query
from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
from Products.PythonScripts.Utility import allow_class
from Key import BaseKey
from pprint import pprint
# these keys are used to build query in case for ScriptableKey
# when no key was specified in fornt of value
DEFAULT_SEARCH_KEYS = ('SearchableText', 'reference', 'title',)
class KeyMappingKey(BaseKey):
""" Usable lexer class used (internally) by ScriptableKey lexer than can parse following:
VALUE OPERATOR VALUE
Examples:
* "portal_type : Person"
* "creation_date > 2007-01-01"
"""
tokens = ('OPERATOR', 'COLONOPERATOR', 'VALUE',)
t_OPERATOR = r'>=|<=|>|<'
t_VALUE = r'[\x7F-\xFF\w\d\/~!@#$^&*()_+-][\x7F-\xFF\w\d\/~!@#$^&*()_+-]*'
def t_COLONOPERATOR(self, t):
r':'
# ':' is the same as '=' (equality)
t.value = '='
return t
class ScriptableKey(BaseKey):
""" KeyWordKey key is an ERP5 portal_catalog search key which is used to generate a
ComplexQuery instance out of an arbitrary search string.
Examples:
* "John Doe AND portal_type:Person AND creation_date > 2007-01-01"
would be turned into following ComplexQuery:
* ComplexQuery(Query(portal_type='Person'),
Query(creation_date='2007-01-01', operator='>'),
ComplexQuery(Query(searchable_text='John Doe'),
Query(title='John Doe'),
Query(reference='John Doe'),
operator='OR')
operator='AND'))
"""
sub_operators = ('GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL',)
tokens = ('OR', 'AND',
'DATE', 'WORD', 'KEYMAPPING',
'GREATERTHAN', 'GREATERTHANEQUAL',
'LESSTHAN', 'LESSTHANEQUAL', 'EQUAL')
t_GREATERTHANEQUAL = r'>='
t_LESSTHANEQUAL = r'<='
t_GREATERTHAN = r'>'
t_LESSTHAN = r'<'
t_EQUAL = r'='
# Note: Order of placing rules (t_WORD for example) is very important
def t_OR(self, t):
r'(\s+OR\s+|\s+or\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'OR'
return t
def t_AND(self, t):
r'(\s+AND\s+|\s+and\s+)'
# operator must have leading and trailing ONLY one white space character
# otherwise it's treated as a WORD
t.value = 'AND'
return t
def t_KEYMAPPING(self, t):
r'[\x7F-\xFF\w\d\/~!@#$^&*()_+-][\x7F-\xFF\w\d\/~!@#$^&*()_+-]*\s*(>|<|<=|>=|:)\s*[\x7F-\xFF\w\d\/~!@#$^&*()_+-][\x7F-\xFF\w\d\/~!@#$^&*()_+-]*'
# KEYMAPPING has following format: KEY OPERATOR VALUE
# where OPERATOR in ['<', '>', '<=', '>=', ':']
# example: 'creation_date < 2007-12-12'
value = t.value.strip()
t.value = value
return t
def t_WORD(self, t):
r'[\x7F-\xFF\w\d\/~!@#$^&*()_+][\x7F-\xFF\w\d\/~!@#$^&*()_+]*'
# WORD may contain arbitrary letters and numbers without white space
# WORD may contain '%' but not at the beginning or end (otherwise it's KEYWORD)
value = t.value.strip()
t.value = value
return t
def buildQueryForTokenList(self, tokens):
""" Build a ComplexQuery for a token list """
query_list = []
for group in self.groupByLogicalOperator(tokens, 'AND'):
group_tokens = group
first_group_token = group_tokens[0]
if first_group_token.type == 'KEYMAPPING':
# user specified a full sub query definition following this format:
# 'key operator value'
sub_search_string = group_tokens[0].value
keymapping_lexer = getSearchKeyInstance(KeyMappingKey)
sub_tokens = keymapping_lexer.tokenize(sub_search_string)
sub_tokens_values = [x.value for x in sub_tokens]
search_key, search_operator, search_value = sub_tokens_values
query_kw = {search_key: search_value,
'range' : search_operator,}
query_list.append(Query( **query_kw))
elif first_group_token.type in self.sub_operators:
# user specified a incomplete sub query definition following this format:
# 'operator value'. Assume that he ment to search for 'title' and
# use supplied 'operator'
search_operator = first_group_token.value
simple_query_value = ' '.join([x.value for x in group_tokens[1:]])
query_kw = {'title': simple_query_value,
'range' : search_operator,}
query_list.append(Query( **query_kw))
else:
# user specified a VERY incomplete sub query definition following this format:
# 'value'. Let's search against most common search_keys and assume operator
# is '=' (by default) and try to get as much possible results
simple_query_value = ' '.join([x.value for x in group_tokens])
sub_query_list = []
for default_key in DEFAULT_SEARCH_KEYS:
query_kw = {default_key: simple_query_value}
sub_query_list.append(Query(**query_kw))
query_list.append(ComplexQuery(*sub_query_list,
**{'operator':'OR'}))
# join query list in one really big ComplexQuery
complex_query = ComplexQuery(*query_list,
**{'operator':'AND'})
return complex_query
def buildQuery(self, key, value,
format=None, mode=None, range_value=None, stat__=None):
""" Build ComplexQuery from passed search string value.
When grouping expressions we use the following assumptions
that 'OR' operator has higher priority in a sense:
* "John Doe AND portal_type:Person OR creation_date>=2005/12/12"
is considered as:
* (John Doe AND portal_type:Person) OR (creation_date>=2005/12/12)"
"""
query_list = []
tokens = self.tokenize(value)
# split tokens list into one or many OR concatanated expressions
sub_tokens_or_groups = self.groupByLogicalOperator(tokens, 'OR')
# get a ComplexQuery for a sub token list
for tokens_or_group in sub_tokens_or_groups:
query_list.append(self.buildQueryForTokenList(tokens_or_group))
# join query list in one really big ComplexQuery
complex_query = ComplexQuery(*query_list,
**{'operator':'OR'})
return complex_query
allow_class(ScriptableKey)
......@@ -43,5 +43,4 @@ def initialize(context):
from AccessControl import ModuleSecurityInfo, ClassSecurityInfo
ModuleSecurityInfo('Products.ZSQLCatalog.SQLCatalog').declarePublic(
'ComplexQuery', 'Query', 'NegatedQuery')
'ComplexQuery', 'Query', 'NegatedQuery',)
......@@ -102,9 +102,11 @@ class TestQuery(unittest.TestCase):
def testSimpleQuery(self):
q = Query(title='Foo')
self.assertEquals(
dict(where_expression="title = 'Foo'",
dict(where_expression="((((title = 'Foo'))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testQueryMultipleKeys(self):
# using multiple keys is invalid and raises
......@@ -116,7 +118,9 @@ class TestQuery(unittest.TestCase):
self.assertEquals(
dict(where_expression="title is NULL",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testEmptyQueryNotIgnoreEmptyString(self):
q = Query(title='')
......@@ -127,6 +131,7 @@ class TestQuery(unittest.TestCase):
select_expression_list=[]),
q.asSQLExpression(ignore_empty_string=0,
keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testEmptyQuery(self):
......@@ -135,52 +140,67 @@ class TestQuery(unittest.TestCase):
self.assertEquals(
dict(where_expression="1",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testMultiValuedQuery(self):
q = Query(title=['Foo', 'Bar'])
self.assertEquals(
dict(where_expression="(title = 'Foo' OR title = 'Bar')",
dict(where_expression="(((((title = 'Foo')))) OR ((((title = 'Bar')))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testINQuery(self):
q = Query(title=['Foo', 'Bar'], operator='IN')
self.assertEquals(
dict(where_expression="title IN ('Foo', 'Bar')",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testEmptyINQuery(self):
q = Query(title=[], operator='IN')
self.assertEquals(
dict(where_expression="0",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testMinQuery(self):
q = Query(title='Foo', range='min')
self.assertEquals(
dict(where_expression="title >= 'Foo'",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testMaxQuery(self):
q = Query(title='Foo', range='max')
self.assertEquals(
dict(where_expression="title < 'Foo'",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
# format
def testDateFormat(self):
q = Query(date=DateTime(2001, 02, 03), format='%Y/%m/%d', type='date')
date = DateTime(2001, 02, 03)
q = Query(date=date, format='%Y/%m/%d', type='date')
self.assertEquals(
dict(where_expression=
"STR_TO_DATE(DATE_FORMAT(date,'%Y/%m/%d'),'%Y/%m/%d')"
" = STR_TO_DATE('2001/02/03','%Y/%m/%d')",
"((((date >= '%s' AND date < '%s'))))" \
%(date.toZone('UTC').ISO(), (date + 1).toZone('UTC').ISO()),
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[], full_text_search_keys=[]))
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
# full text
def testSimpleQueryFullText(self):
......@@ -189,6 +209,7 @@ class TestQuery(unittest.TestCase):
select_expression_list=
["MATCH title AGAINST ('Foo' ) AS title_relevance"]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=['title']))
def testSimpleQueryFullTextSearchMode(self):
......@@ -199,6 +220,7 @@ class TestQuery(unittest.TestCase):
select_expression_list=
["MATCH title AGAINST ('Foo' IN BOOLEAN MODE) AS title_relevance"]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=['title']))
def testSimpleQueryFullTextStat__(self):
......@@ -209,23 +231,26 @@ class TestQuery(unittest.TestCase):
where_expression="MATCH title AGAINST ('Foo' )",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=['title'],
stat__=1))
def testSimpleQueryKeywordSearchKey(self):
q = Query(title='Foo')
self.assertEquals(dict(where_expression="title LIKE '%Foo%'",
self.assertEquals(dict(where_expression="((((title LIKE '%Foo%'))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=['title'],
datetime_search_keys = [],
full_text_search_keys=[]))
def testNegatedQuery(self):
q1 = Query(title='Foo')
q = NegatedQuery(q1)
self.assertEquals(
dict(where_expression="(NOT (title = 'Foo'))",
dict(where_expression="(NOT (((((title = 'Foo'))))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
# complex queries
......@@ -234,9 +259,10 @@ class TestQuery(unittest.TestCase):
q2 = Query(reference='Bar')
q = ComplexQuery(q1, q2)
self.assertEquals(
dict(where_expression="((title = 'Foo') AND (reference = 'Bar'))",
dict(where_expression="((((((title = 'Foo'))))) AND (((((reference = 'Bar'))))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
def testNegatedComplexQuery(self):
......@@ -246,35 +272,40 @@ class TestQuery(unittest.TestCase):
q = NegatedQuery(q3)
self.assertEquals(
# maybe too many parents here
dict(where_expression="(NOT (((title = 'Foo') AND (reference = 'Bar'))))",
dict(where_expression="(NOT (((((((title = 'Foo'))))) AND (((((reference = 'Bar'))))))))",
select_expression_list=[]),
q.asSQLExpression(keyword_search_keys=[],
datetime_search_keys = [],
full_text_search_keys=[]))
# forced keys
def testSimpleQueryForcedKeywordSearchKey(self):
q = Query(title='Foo', key='Keyword')
self.assertEquals("title LIKE '%Foo%'",
self.assertEquals("((((title LIKE '%Foo%'))))",
q.asSQLExpression(keyword_search_keys=[],
datetime_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=[],
datetime_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'],
datetime_search_keys = [],
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'],
datetime_search_keys = [],
full_text_search_keys=[])['where_expression'])
......
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