diff --git a/product/ZSQLCatalog/ColumnMap.py b/product/ZSQLCatalog/ColumnMap.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd8fe52a41ded3d42139374b1fe47c57e8626c5b
--- /dev/null
+++ b/product/ZSQLCatalog/ColumnMap.py
@@ -0,0 +1,502 @@
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SARL and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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.
+#
+##############################################################################
+
+import sys
+from zLOG import LOG
+from Interface.IColumnMap import IColumnMap
+from Interface.Verify import verifyClass
+from SQLCatalog import profiler_decorator
+
+DEFAULT_GROUP_ID = None
+
+TRACE = False
+
+# TODO: handle left joins
+# TODO: handle straight joins
+# TODO: make it possible to do: query=ComplexQuery(Query(source_title='foo'), Query(source_title='bar')), sort_on=[('source_title_1', )]
+#       currently, it's not possible because related_key_dict is indexed by related key name, which makes 'source_title_1' lookup fail. It should be indexed by group (probably).
+# TODO: rename all "related_key" references into "virtual_column"
+
+class ColumnMap(object):
+
+  __implements__ = IColumnMap
+
+  @profiler_decorator
+  def __init__(self, catalog_table_name=None):
+    self.catalog_table_name = catalog_table_name
+    # Key: group
+    # Value: set of column names
+    self.registry = {}
+    # Key: group
+    # Value: dict
+    #  Key: column name
+    #  Value: set of SimpleQuery
+    self.simple_query_dict = {}
+    # Key: (group, column name)
+    # Value: table name
+    self.column_map = {}
+    # Key: (group, table name)
+    # Value: table alias
+    self.table_alias_dict = {}
+    # Key: related key name
+    # Value: (group, column name)
+    self.related_key_dict = {}
+    # Key: related_column
+    # Value: last used alias order
+    self.related_key_order_dict = {}
+    # Key: group
+    # Value: relate_key
+    self.related_group_dict = {}
+    # Key: table alias
+    # Value: table name
+    self.table_map = {}
+    # Key: raw column
+    # Value: (function, column)
+    self.raw_column_dict = {}
+    # Entries: column name
+    self.column_ignore_set = set()
+    self.join_table_set = set()
+    self.straight_join_table_list = []
+    self.left_join_table_list = []
+    self.join_query_list = []
+
+  @profiler_decorator
+  def registerColumn(self, raw_column, group=DEFAULT_GROUP_ID, simple_query=None):
+    assert ' as ' not in raw_column.lower()
+    # Sanitize input: extract column from raw column (might contain COUNT, ...).
+    if '(' in raw_column:
+      function, column = raw_column.split('(')
+      column = column.strip()
+      assert column[-1] == ')'
+      column = column[:-1].strip()
+    else:
+      function = None
+      column = raw_column
+    # Remove '`' from column.
+    column = column.replace('`', '')
+    # Extract table name from column, if any.
+    if '.' in column:
+      # Assuming the part before the dot is a real table name, not an alias.
+      table, column = column.split('.')
+      LOG('ColumnMap', 100, 'Column %r is pre-mapped. This use is strongly discouraged.' % (raw_column, ))
+    else:
+      table = None
+
+    self.raw_column_dict[raw_column] = (function, column)
+    self.registry.setdefault(group, set()).add(column)
+    self.simple_query_dict.setdefault(group, {}).setdefault(column, set()).add(simple_query)
+    if table is not None:
+      # Register table alias and mark column as resolved.
+      self.registerTable(table, alias=table, group=group)
+      self.resolveColumn(column, table, group=group)
+      if group is DEFAULT_GROUP_ID and table != self.catalog_table_name:
+        # When a column is registered  in default group and is explicitely
+        # mapped to a table, we must mark its table as requiring a join with
+        # catalog table (unless it's the catalog table, of course).
+        self._addJoinTable(table, group)
+
+  def ignoreColumn(self, column):
+    self.column_ignore_set.add(column)
+
+  @profiler_decorator
+  def registerRelatedKey(self, related_column, column):
+    # XXX: should we store the group, or directly the table on which the column is mapped ?
+    # The former avoids duplicating data, but requires one more lookup (group + column -> table)
+    # The latter makes it harder (?) to split the mapping in multiple queries (if splitting by groups turns out to be a good idea)
+    real_related_column = related_column
+    order = self.related_key_order_dict.get(real_related_column, 0) + 1
+    related_column = '%s_%s' % (related_column, order)
+    group = 'related_%s' % (related_column, )
+    assert group not in self.registry
+    assert group not in self.related_group_dict
+    self.related_key_order_dict[real_related_column] = order
+    self.related_key_dict[real_related_column] = (group, column)
+    self.registerColumn(column, group=group)
+    self.related_group_dict[group] = related_column
+    # XXX: hardcoded translation table column names: they are not present in sql_catalog.getColumnMap(), and this table cannot be joined by uid, forbidding implicit join.
+    if column in ('translated_message', 'language', 'message_context', 'original_message'):
+      self.registerTable('translation', alias='translation', group=group)
+      self.resolveColumn(column, 'translation', group=group)
+    # Likewise, for measure table. Moreover, there is a related key named the same way as a column of that table (designed to do the join).
+    elif column in ('metric_type_uid', ):
+      self.registerTable('measure', group=group)
+      self.resolveColumn(column, 'measure', group=group)
+    return group
+
+  @profiler_decorator
+  def registerCatalog(self):
+    assert self.catalog_table_name is not None
+    LOG('ColumnMap', 100, 'Registering implicit catalog. This use is strongly discouraged.')
+    self.registerTable(self.catalog_table_name)
+    self.resolveTable(self.catalog_table_name, self.catalog_table_name)
+
+  @profiler_decorator
+  def registerRelatedKeyColumn(self, related_column, position, group):
+    assert group in self.related_group_dict
+    group = self.getRelatedKeyGroup(position, group)
+    assert group not in self.related_group_dict
+    self.related_group_dict[group] = related_column
+    return group
+
+  def getRelatedKeyGroup(self, position, group):
+    return '%s_column_%s' % (group, position)
+
+  @profiler_decorator
+  def registerTable(self, table_name, alias=None, group=DEFAULT_GROUP_ID):
+    table_alias_dict = self.table_alias_dict
+    table_alias_key = (group, table_name)
+    existing_value = table_alias_dict.get(table_alias_key)
+    # alias = None, existing = None -> store
+    # alias = None, existing ! None -> skip
+    # alias ! None, existing = None -> store & resolve
+    # alias ! None, existing ! None -> skip if alias = existing, raise otherwise
+    if existing_value is None:
+      table_alias_dict[table_alias_key] = alias
+      if alias is not None:
+        self.resolveTable(table_name, alias, group=group)
+    elif alias is not None and alias != existing_value:
+      raise ValueError, 'Table %r for group %r is aliased as %r, can\'t alias it now as %r' % (table_name, group, existing_value, alias)
+
+  @profiler_decorator
+  def _mapColumns(self, column_table_map, table_usage_dict, column_name_set, group, vote_result_dict):
+    mapping_dict = {}
+    catalog_table_name = self.catalog_table_name
+
+    # Map all columns to tables decided by vote.
+    for column_name, candidate_dict in vote_result_dict.iteritems():
+      # candidate_dict is never empty
+      max_score = 0
+      for table_name, score in candidate_dict.iteritems():
+        if score > max_score:
+          max_score = score
+          best_count = 0
+          best_choice = table_name
+        elif score == max_score:
+          best_count += 1
+      if best_count:
+        LOG('ColumnMap', 100, 'Mapping vote led to a tie. Mapping to %r' % (best_choice, ))
+      if TRACE:
+        LOG('ColumnMap', 0, 'Mapping by vote %r to %r' % (column_name, best_choice))
+      mapping_dict[column_name] = best_choice
+      column_name_set.remove(column_name)
+      for table_name, column_set in table_usage_dict.iteritems():
+        if table_name != best_choice:
+          column_set.discard(column_name)
+
+    # Map all remaning columns.
+    def table_weight(a):
+      """
+        Compute each table weight.
+      """
+      if (group, a[0]) in self.table_alias_dict:
+        result = (2, )
+      elif a[0] == catalog_table_name:
+        result = (1, )
+      else:
+        result = (0, len(a[1]))
+      return result
+    # Sort table name list, first has the most required columns
+    weighted_table_list = sorted(table_usage_dict.iteritems(), key=table_weight)
+    while len(weighted_table_list):
+      table_name, column_set = weighted_table_list.pop()
+      if len(column_set):
+        common_column_set = column_name_set.intersection(column_set)
+        if len(common_column_set):
+          # Only allow usage of this table if any of those is true:
+          # - current table is the catalog  (if any catalog was provided)
+          # - there are column used on that table which are already mapped
+          #   (this does not include columns mapped by this code)
+          #   If columns are mapped to this table in current group, then using
+          #   it will not require a new join, so it should be allowed.
+          #   Note: it would be good to take indexes into account when there
+          #   are multiple candidate tables.
+          # - any of those columns belongs exclusively to this table
+          #   Although the list of tables those columns belong to is known
+          #   earlier (in "build"), mapping them here
+          #   - avoids code duplication (registerTable, resolveColumn,
+          #     _addJoinTable)
+          #   - offers user to vote for an unknown table, overriding this
+          #     forced mapping.
+          use_allowed = table_name == catalog_table_name or \
+                        len(common_column_set) < len(column_set)
+          if not use_allowed:
+            for column_name in column_set:
+              if len(column_table_map.get(column_name, [])) == 1:
+                # There is no alternative, mark as required
+                use_allowed = True
+                break
+          if use_allowed:
+            for column_name in common_column_set:
+              if TRACE:
+                LOG('ColumnMap', 0, 'Mapping by default %r to %r' % \
+                    (column_name, table_name))
+              mapping_dict[column_name] = table_name
+              # This column must not be resolved any longer
+              column_name_set.remove(column_name)
+              # Remove this column from sets containing it. This prevents from
+              # giving a high score to a table which columns would already have
+              # been mapped to another table.
+              for ignored, other_column_set in weighted_table_list:
+                other_column_set.discard(column_name)
+            weighted_table_list.sort(key=table_weight)
+          else:
+            # All column which are mappable on that table are to-be-mapped
+            # columns. This means that this table was not explicitely used, and
+            # as each table contain a different amount of lines, we should not
+            # join with any non-explicit table. Hence, we skip this mapping.
+            LOG('ColumnMap', 0, 'Skipping possible map of %r on %r as that table' \
+                ' is not explicitely used.' % (common_column_set, table_name))
+
+    # Detect incomplete mappings
+    if len(column_name_set):
+      raise ValueError, 'Could not map those columns: %r' % (column_name_set, )
+
+    # Do the actual mapping
+    for column_name, table_name in mapping_dict.iteritems():
+      # Mark this column as resolved
+      if TRACE:
+        LOG('ColumnMap', 0, 'Mapping column %s to table %s' % (column_name, table_name))
+      self.registerTable(table_name, group=group)
+      self.resolveColumn(column_name, table_name, group=group)
+      if table_name != catalog_table_name:
+        self._addJoinTable(table_name, group)
+
+  @profiler_decorator
+  def build(self, sql_catalog):
+    catalog_table_name = self.catalog_table_name
+    if catalog_table_name is None:
+      return
+
+    column_table_map = sql_catalog.getColumnMap()
+    table_vote_method_list = [getattr(sql_catalog, x) for x in sql_catalog.sql_catalog_table_vote_scripts]
+
+    # Generate missing joins from default group (this is required to allow using related keys outside of queries: order_by, sort_on, ...)
+    column_set = self.registry.get(DEFAULT_GROUP_ID, [])
+    for column_name in column_set:
+      if column_name not in column_table_map and column_name not in self.related_key_dict:
+        related_key_definition = sql_catalog.getRelatedKeyDefinition(column_name)
+        if related_key_definition is not None:
+          join_query = sql_catalog.getSearchKey(column_name, 'RelatedKey').buildQuery(sql_catalog=sql_catalog, related_key_definition=related_key_definition)
+          join_query.registerColumnMap(sql_catalog, self)
+          self._addJoinQuery(join_query)
+
+    # List all possible tables, with all used column for each
+    for group, column_set in self.registry.iteritems():
+      # unique needed column name set
+      column_name_set = set()
+      # table -> column_set, including alternatives
+      table_usage_dict = {}
+
+      for column_name in column_set:
+        if column_name == '*' or column_name in self.column_ignore_set:
+          continue
+        table_name_list = column_table_map.get(column_name, [])
+        if len(table_name_list) == 0:
+          if not(group is DEFAULT_GROUP_ID and column_name in self.related_key_dict):
+            LOG('ColumnMap', 100, 'Not a known column name: %r' % (column_name, ))
+          continue
+        column_map_key = (group, column_name)
+        if column_map_key in self.column_map:
+          # Column is already mapped, so we must count this column as being available only on that table. Its mapping will not change, and it will impact table schema choice.
+          table_name = self.column_map[column_map_key]
+          assert table_name in table_name_list, '%r not in %r' % (table_name, table_name_list)
+          table_name_list = [table_name]
+        else:
+          # Mark this column as requiring to be mapped.
+          column_name_set.add(column_name)
+        for table_name in table_name_list:
+          table_usage_dict.setdefault(table_name, set()).add(column_name)
+      # XXX: mutable datatypes are provided to vote method. if it modifies
+      # them, it can introduce mapping bugs. Copying them might be costly,
+      # especialy if done before each call, since they also contain mutable
+      # types.
+      # XXX: the API of vote methods is not stable yet. Parameters should
+      # always be passed and expected by name, to make it less painful to
+      # change API.
+      # XXX: there is no check that the table voted for contains mapped
+      # column. It is up to the user not to do stupid things.
+      vote_result_dict = {}
+      simple_query_dict = self.simple_query_dict[group]
+      for table_vote_method in table_vote_method_list:
+        vote_dict = table_vote_method(column_name_set=column_name_set,
+                                      simple_query_dict=simple_query_dict,
+                                      table_usage_dict=table_usage_dict,
+                                      group=group)
+        if isinstance(vote_dict, dict):
+          for column, table in vote_dict.iteritems():
+            if column in column_name_set:
+              column_vote_dict = vote_result_dict.setdefault(column, {})
+              column_vote_dict[table] = column_vote_dict.get(table, 0) + 1
+            else:
+              LOG('ColumnMap', 100, 'Vote script %r voted for a ' \
+                  'non-candidate column: %r, candidates are: %r. Ignored.' %
+                  (table_vote_method, column, column_name_set))
+        else:
+          LOG('ColumnMap', 100, 'Vote script %r returned invalid data: %r. ' \
+              'Ignored.' % (table_vote_method, vote_dict))
+      self._mapColumns(column_table_map, table_usage_dict, column_name_set, group, vote_result_dict)
+
+    table_alias_number_dict = {}
+
+    for (group, table_name), alias in self.table_alias_dict.iteritems():
+      if alias is None:
+        if group in self.related_group_dict:
+          alias_table_name = 'related_%s_%s' % (self.related_group_dict[group], table_name)
+        else:
+          alias_table_name = table_name
+        table_alias_number = table_alias_number_dict.get(alias_table_name, 0)
+        while True:
+          if table_alias_number == 0:
+            alias = alias_table_name
+          else:
+            alias = '%s_%s' % (alias_table_name, table_alias_number)
+          table_alias_number += 1
+          if alias not in self.table_map:
+            break
+        table_alias_number_dict[alias_table_name] = table_alias_number
+      self.resolveTable(table_name, alias, group=group)
+
+    if TRACE:
+      # Key: group
+      # Value: 2-tuple
+      #  dict
+      #   Key: column
+      #   Value: table name
+      #  dict
+      #   Key: table name
+      #   Value: table alias
+      summary_dict = {}
+      for (group, column), table_name in self.column_map.iteritems():
+        column_dict = summary_dict.setdefault(group, ({}, {}))[0]
+        assert column not in column_dict, '%r in %r' % (column, column_dict)
+        column_dict[column] = table_name
+      for (group, table_name), table_alias in self.table_alias_dict.iteritems():
+        table_dict = summary_dict.setdefault(group, ({}, {}))[1]
+        assert table_name not in table_dict, '%r in %r' % (table_name, table_dict)
+        table_dict[table_name] = table_alias
+      for group, (column_dict, table_dict) in summary_dict.iteritems():
+        LOG('ColumnMap', 0, 'Group %r:' % (group, ))
+        LOG('ColumnMap', 0, ' Columns:')
+        for column, table_name in column_dict.iteritems():
+          LOG('ColumnMap', 0, '  %r from table %r' % (column, table_name))
+        LOG('ColumnMap', 0, ' Tables:')
+        for table_name, table_alias in table_dict.iteritems():
+          LOG('ColumnMap', 0, '  %r as %r' % (table_name, table_alias))
+
+  def asSQLColumn(self, raw_column, group=DEFAULT_GROUP_ID):
+    if self.catalog_table_name is None or raw_column in self.column_ignore_set or \
+       '.' in raw_column or '*' in raw_column:
+      result = raw_column
+    else:
+      function, column = self.raw_column_dict.get(raw_column, (None, raw_column))
+      if group is DEFAULT_GROUP_ID:
+        group, column = self.related_key_dict.get(column, (group, raw_column))
+      alias = self.table_alias_dict[(group, self.column_map[(group, column)])]
+      result = '`%s`.`%s`' % (alias, column)
+      if function is not None:
+        result = '%s(%s)' % (function, result)
+    return result
+
+  def getCatalogTableAlias(self, group=DEFAULT_GROUP_ID):
+    return self.table_alias_dict[(group, self.catalog_table_name)]
+
+  def getTableAliasDict(self):
+    return self.table_map.copy()
+
+  @profiler_decorator
+  def resolveColumn(self, column, table_name, group=DEFAULT_GROUP_ID):
+    assert group in self.registry
+    assert column in self.registry[group]
+    column_map_key = (group, column)
+    column_map = self.column_map
+    assert (group, table_name) in self.table_alias_dict
+    previous_value = column_map.get(column_map_key)
+    if previous_value is None:
+      column_map[column_map_key] = table_name
+    elif previous_value != table_name:
+      if column == 'uid':
+        LOG('ColumnMap', 100, 'Attempt to remap uid from %r to %r ignored.' % (previous_value, table_name))
+      else:
+        raise ValueError, 'Cannot remap a column to another table. column_map[%r] = %r, new = %r' % (column_map_key, column_map.get(column_map_key), table_name)
+
+  @profiler_decorator
+  def resolveTable(self, table_name, alias, group=DEFAULT_GROUP_ID):
+    table_alias_key = (group, table_name)
+    assert table_alias_key in self.table_alias_dict
+    assert self.table_alias_dict[table_alias_key] in (None, alias)
+    self.table_alias_dict[table_alias_key] = alias
+    assert self.table_map.get(alias) in (None, table_name)
+    self.table_map[alias] = table_name
+
+  def getTableAlias(self, table_name, group=DEFAULT_GROUP_ID):
+    return self.table_alias_dict[(group, table_name)]
+
+  def _addJoinQuery(self, query):
+    self.join_query_list.append(query)
+
+  def addJoinQuery(self, query):
+    LOG('ColumnMap', 0, 'addJoinQuery use is discouraged')
+    self._addJoinQuery(query)
+
+  def iterJoinQueryList(self):
+    return iter(self.join_query_list)
+
+  @profiler_decorator
+  def _addJoinTable(self, table_name, group=DEFAULT_GROUP_ID):
+    """
+      Declare given table as requiring to be joined with catalog table.
+
+      table_name (string)
+        Table name.
+      group (string)
+        Group id of given table.
+    """
+    catalog_table = self.catalog_table_name
+    if catalog_table is not None:
+      # Only join tables when there is a catalog table
+      # Register unconditionaly catalog table
+      self.registerTable(catalog_table)
+      if 'uid' not in self.registry.get(DEFAULT_GROUP_ID, ()):
+        # Register uid column if it is not already
+        self.registerColumn('uid')
+        self.resolveColumn('uid', catalog_table)
+      self.join_table_set.add((group, table_name))
+
+  def getJoinTableAliasList(self):
+    return [self.getTableAlias(table_name, group=group)
+            for (group, table_name) in self.join_table_set]
+
+  def getStraightJoinTableList(self):
+    return self.straight_join_table_list[:]
+
+  def getLeftJoinTableList(self):
+    return self.left_join_table_list[:]
+
+verifyClass(IColumnMap, ColumnMap)
+
diff --git a/product/ZSQLCatalog/Interface/IAbstractSyntaxNode.py b/product/ZSQLCatalog/Interface/IAbstractSyntaxNode.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6310d48ee61e1c0653df44fe465536b5d361a54
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IAbstractSyntaxNode.py
@@ -0,0 +1,99 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class INode(Interface):
+  """
+    Any kind of node in an Abstract Syntax Tree.
+  """
+
+  def isLeaf():
+    """
+      Returns True if current node is a leaf in node tree.
+      Returns False otherwise.
+    """
+
+  def isColumn():
+    """
+      Returns True if current node is a column in node tree.
+      Returns False otherwise.
+    """
+
+class IValueNode(INode):
+  """
+    Value- and comparison-operator-containig node.
+    They are leaf nodes in the syntax tree.
+  """
+
+  def getValue():
+    """
+      Returns node's value.
+    """
+
+  def getComparisonOperator():
+    """
+      Returns node's comparison operator.
+    """
+
+class ILogicalNode(INode):
+  """
+    Logical-operator-containing node.
+    They are internal tree nodes.
+  """
+
+  def getLogicalOperator():
+    """
+      Returns node's logical operator.
+    """
+
+  def getNodeList():
+    """
+      Returns the list of subnodes.
+    """
+
+class IColumnNode(INode):
+  """
+    Column-name-containing node.
+    They are internal tree nodes.
+    Their value applies to any contained ValueNode, except if there is another
+    ColumnNode between current one and a ValueNode, for which the other
+    ColumnNode will take precedence.
+  """
+
+  def getColumnName():
+    """
+      Returns node's column name.
+    """
+
+  def getSubNode():
+    """
+      Returns node's (only) subnode.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/IColumnMap.py b/product/ZSQLCatalog/Interface/IColumnMap.py
new file mode 100644
index 0000000000000000000000000000000000000000..98a3e5991a117bd04a91bf5a6c633a555687fb42
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IColumnMap.py
@@ -0,0 +1,308 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class IColumnMap(Interface):
+  """
+    The role of the column mapper is to make possible to have a "flat"
+    overview of all columns used in a query, to choose how they will be
+    mapped to catalog tables, and how those tables will be aliased in the SQL
+    rendering of that query.
+
+    Typical usage:
+    - Instanciate (with or without a catalog table)
+    - Register all columns the query will use
+    - Build the column map
+    - Fetch SQL representation of each registered column.
+    - Fetch table alias mapping
+    - Fetch the list of table aliases which require a join with catalog
+    - Fetch the list of queries implied by columns. This happens when there
+      are virtual columns registered which were not expanded in the query
+      already. In that case, column map generate queries required to reach
+      the real column behind the virtual one.
+
+    Note that, although it's not enforced, it is meaningless to:
+    - call build more than once
+    - register columns after build happened
+    - fetch SQL representation before build happened (this will most probably
+      lead to KeyErrors anyway)
+
+    Groups.
+      All references to a given table withing a given group will cause all
+      columns referencing that table to be resolved to the same table alias
+      in resulting query.
+      Ex:
+        Note: instead of resolving columns by hand, it is also possible to
+        call "build", but this way it makes .
+        Registration:
+          registerColumn('bar')
+          registerColumn('baz')
+          resolveColumn('bar', 'foo')
+          resolveColumn('baz', 'foo')
+        Result:
+          asSQLColumn('bar') -> 'foo_alias.bar'
+          asSQLColumn('baz') -> 'foo_alias.baz'
+      Complementary, any reference to a given table from one group to
+      another will cause columns to be resolved to distinct table aliases
+      in resulting query.
+      Ex:
+        Registration:
+          registerColumn('bar')
+          registerColumn('baz', group='hoge')
+          resolveColumn('bar', 'foo')
+          resolveColumn('baz', 'foo', group='hoge')
+        Result:
+          asSQLColumn('bar')               -> 'foo_alias.bar'
+          asSQLColumn('baz', group='hoge') -> 'other_foo_alias.baz'
+  """
+
+  def __init__(catalog_table_name=None):
+    """
+      Create a column map.
+
+      Initialises internal data structures and set the table name to use as
+      catalog table.
+    """
+
+  def registerColumn(column, group=None, simple_query=None):
+    """
+      Register a column in given group on this column map.
+
+      column (string)
+        Contains the name of the column to register.
+
+        This name might contain a dot, in which case a table name is extracted
+        from it and that column is resolved to that table within its group.
+        Note that this practice is discouraged, but not deprecated.
+      group (string)
+        Group id of given column.
+      simple_query (SimpleQuery)
+        The SimpleQuery doing a comparison on given column. This can be
+        accessed from vote script at ColumnMap build time.
+    """
+
+  def ignoreColumn(column):
+    """
+      Act as if given column was valid and its mapping succeeded.
+      ie: supresses all warnings relative to that column, and all mapping
+      attemps.
+      It is supposed to be used when given value is an SQL alias for a
+      separately registered column.
+
+      column (string)
+        The value to ignore.
+    """
+
+  def registerRelatedKey(related_column, column):
+    """
+      Register a "virtual column".
+      This method is to be called when registering any virtual column.
+      A virtual column makes use internaly of multiple catalog columns.
+      A related key is an example of a virtual column.
+
+      This method internaly generates a new group id that caller must pass to
+      registerRelatedKeyColumn when registering those internal (and real)
+      columns. This is because the same virtual column can be used more than
+      once in a query without interferences from other uses, and hence must be
+      put in separate groups, without having to know that there are other uses
+      for the same virtual column.
+
+      virtual_column
+        Name of the virtual column to register.
+      column
+        Name of the "last" real column behind the virtual column. Comparisons
+        done on the virtual column will happen on that real column instead.
+        For example, it's "title" for "source_title" related key.
+    """
+
+  def registerCatalog():
+    """
+      Register catalog table as being used in default group.
+
+      This is for backward compatibility with text-only related keys, since
+      they often hardcode catalog table name.
+    """
+
+  def registerRelatedKeyColumn(related_column, position, group):
+    """
+      Register given column as part of already registered virtual column.
+
+      related_column (string)
+        Name of the virtual column registered column is a part of.
+      position (int)
+        Unique id of this column in the list of all columns used by the
+        virtual column it is a part of (it might use the same column name
+        while expecting uses to be mapped to different aliases or evne
+        different tables).
+        Typically, this is table position in the parameter list the related
+        key expects.
+      group (string)
+        Group id as returned by registerRelatedKey of given virtual column.
+    """
+
+  def getRelatedKeyGroup(position, group):
+    """
+      For given virtual key position and group, return a group.
+
+      This is here so that all group generation code is inside ColumnMap
+      class.
+    """
+
+  def registerTable(table_name, alias=None, group=None):
+    """
+      Register given table name as being used in given group.
+
+      This method should not be called outside of this class except for
+      backward compatibility purposes.
+
+      It is implicitely done most of the time, and should only be used only
+      when there is no control over chosen table alias or when table
+      registration cannot be done by a Query.
+      There are 2 cases where it cas required:
+      - pre-mapped columns which won't be actually used, but which force table
+        alias choice (see EntireQuery).
+      - related keys, where the Related Query cannot register columns it uses
+        by itself (since its content is not based on Queries but on raw SQL
+        text).
+    """
+
+  def build(sql_catalog):
+    """
+      Resolve all unresolved registered columns (ie, not mapped to tables).
+      Resolve all used tables to unique aliases.
+
+      Chosen table aliases are based on table name and virtual column name the
+      column comes from, if applicable. This is done to make final SQL more
+      reader-friendly.
+    """
+
+  def asSQLColumn(column, group=None):
+    """
+      Return an SQL rendering of given column, with the table alias it has
+      been mapped to.
+
+      column (string)
+        Column name. Can be the name of a virtual column.
+
+        This name might contain a dot, in which case a table name is extracted
+        from it and that column is resolved to that table within its group.
+        Note that this practice is discouraged, but not deprecated.
+        Note also that it does not apply to virtual columns (they cannot
+        contain a dot).
+      group (string)
+        Group id of given column.
+    """
+
+  def getCatalogTableAlias(group=None):
+    """
+      Return the alias of catalog table name given at instantiation.
+    """
+
+  def getTableAliasDict():
+    """
+      Return a copy of table mapping.
+
+      returned value (dict)
+        key (string)
+          Table alias.
+        value (string)
+          Table name.
+    """
+
+  def resolveColumn(column, table_name, group=None):
+    """
+      Map given column to given table within given group.
+
+      column (string)
+        Name of the column to map to a table. This cannot be a virtual column.
+      table_name (string)
+        Name of the table column must be mapped to. This is not an alias.
+        It must be already known to be used within given group.
+      group (string)
+        Name of the group column and table are in.
+    """
+
+  def resolveTable(table_name, alias, group=None):
+    """
+      Resolve given table of given group as given alias.
+
+      table_name (string)
+        Table to alias.
+      alias (string)
+        Table alias. It must be unique at query scope to work as intended.
+      gorup (string)
+        Group the table belongs to.
+    """
+
+  def getTableAlias(table_name, group=None):
+    """
+      Get table alias in given group.
+
+      table_name (string)
+        Table name.
+      group (string)
+        Group id of given table.
+    """
+
+  def addJoinQuery(query):
+    """
+      Adds a query to a list of top-level queries.
+      This method is only made available at interface level for backward
+      compatibility.
+    """
+
+  def iterJoinQueryList():
+    """
+      Get an iterator over queries internally generated when resolving column
+      map. Those queries are generated when a virtual column was registered as
+      a real one. Queries required to map virtual columns to real ones are
+      put in that list by build.
+    """
+
+  def getJoinTableAliasList():
+    """
+      Return a copy of the table alias list for tables requiring a join with
+      catalog table.
+    """
+
+  def getStraightJoinTableList():
+    """
+      Returns the list of tables used this search and which
+      need to be joined with the main table using explicit
+      indices.
+    """
+
+  def getLeftJoinTableList():
+    """
+      Returns the list of tables used this search and which
+      need to be LEFT joined with the main table using explicit
+      indices.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/IEntireQuery.py b/product/ZSQLCatalog/Interface/IEntireQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..146ab1da2774141008c5fa422ee74c4ae0454e55
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IEntireQuery.py
@@ -0,0 +1,101 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class IEntireQuery(Interface):
+  """
+    A EntireQuery represents an entire SQL expression, where a Query
+    represents only the "WHERE" part of that expression.
+    A EntireQuery contains:
+     - a Query instance
+     - a limit expression
+     - a group-by expression
+     - an order-by expression
+     - a select expression
+    It internaly uses a ColumnMap instance to resolve tables to use to
+    generate a "from" expression.
+  """
+
+  def __init__(query, order_by_list=None, group_by_list=None,
+    select_dict=None, limit=None, catalog_table_name=None,
+    extra_column_list=None, from_expression=None,
+    order_by_override_list=None):
+    """
+      query (Query instance)
+        The root of the Query tree this query will contain.
+      order_by_list (list of 1-tuple, 2-tuple or 3-tuple)
+        The list of columns which will be sorted by SQL.
+        Tuple values are:
+        - mandatory: column name
+        - optionnal: sort order (can be "ASC" or "DESC", "ASC" by default)
+        - optionnal: type cast (no cast by default, see "CAST" SQL method)
+      group_by_list (list of string)
+        The list of columns which will be groupped by value by SQL.
+      select_dict (dict, key: string, value: string, None)
+        Given values describe columns to make available in SQL result.
+        If column is aliased in result set, key is the alias and value is the
+        column.
+        Otherwise, key is the column, and value must be None.
+      limit
+        See SQLExpression.
+      catalog_table_name (string)
+        Name of the table to use as a catalog.
+
+      Deprecated parameters.
+      extra_column_list (list of string)
+        The list of columns to register to column map. They will not be used
+        in final rendering, but are hint on which table are supposed to be
+        used when mapping columns.
+      from_expression
+        See SQLExpression.
+      order_by_override_list (list of string)
+        If a column is in order_by_list, cannot be mapped to a table column
+        but is present in this list, it will be passed through to
+        SQLExpression.
+    """
+
+  def asSQLExpression(sql_catalog, only_group_columns):
+    """
+      Instantiate a column map, process parameters given at instantiation and
+      register them to column map.
+      Register query to column map.
+      Build column map.
+      Generate extra SQLExpressions from column map.
+      Generate SQLExpression instance and return it.
+    """
+
+  def asSearchTextExpression(sql_catalog):
+    """
+      This is just a passthrough to embeded Query's asSearchTextExpression
+      method.
+      This means that only the where expression can be represented as a
+      SearchText, but not sort, limits, ...
+    """
+
diff --git a/product/ZSQLCatalog/Interface/IOperator.py b/product/ZSQLCatalog/Interface/IOperator.py
new file mode 100644
index 0000000000000000000000000000000000000000..459d4f5c4bc77afb9f697437a16d0e9c4c6fa0cd
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IOperator.py
@@ -0,0 +1,71 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class IOperator(Interface):
+  """
+    An operator is responsible for rendering a value and a column name as SQL
+    or Search Text.
+
+    This class is designed to be used as a singleton-per-operator.
+  """
+
+  def __init__(operator):
+    """
+      operator (string)
+        Operator's text representation. It is used both for SQL and SearchText
+        rendering.
+    """
+
+  def asSearchText(value):
+    """
+      Render given value as Search Text
+
+      value (see _renderValue)
+        Value to render as a string for use in a Search Text expression.
+    """
+
+  def asSQLExpression(column, value_list, only_group_columns):
+    """
+      Construct a SQLExpression instance from given column and value, with
+      contained glue text.
+      value_list can be a non-list instance, which must be handled that same
+      way as a list of one item.
+      only_group_columns (bool)
+        If false, the operator can add group columns in the "select_dict" of
+        returned SLQExpression.
+        Otherwise, it must not (SQL would be invalid).
+    """
+
+  def getOperator():
+    """
+      Accessor for operator's text representation.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/IQuery.py b/product/ZSQLCatalog/Interface/IQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..d84f68226584150b6a5092d1d73612a2a2505cdc
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IQuery.py
@@ -0,0 +1,69 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class IQuery(Interface):
+  """
+    A Query contains:
+    - a value
+    - an operator
+    - a column
+
+    It is the python representation of a predicate, independently of its
+    rendering (SQL or SearchText).
+    For SQL rendering to be possible, it is necesary for some data to be
+    centralized in a data structure known at EntireQuery level (to be able to
+    generate unique table aliases, for exemple). This is the role of
+    ColumnMap, and registerColumnMap method on this interface.
+
+    This interface also offers various rendering methods, one per rendering
+    format.
+  """
+
+  def asSearchTextExpression(sql_catalog, column=None):
+    """
+      Render a query in a user-oriented SearchText.
+      Returns None if there is this query has no SearchText representation,
+      but is SearchText-aware.
+      If column is provided, it must be used instead of local knowledge of
+      column name. It is used to make queries inside a related key render
+      correctly.
+    """
+
+  def asSQLExpression(sql_catalog, column_map, only_group_columns):
+    """
+      Render a query as an SQLExpression instance.
+    """
+
+  def registerColumnMap(sql_catalog, column_map):
+    """
+      Register a query to given column_map.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/IQueryCatalog.py b/product/ZSQLCatalog/Interface/IQueryCatalog.py
new file mode 100644
index 0000000000000000000000000000000000000000..c569909c07b079a14277e179f336d42dfbe3d3ed
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/IQueryCatalog.py
@@ -0,0 +1,209 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class ISearchKeyCatalog(Interface):
+
+  def buildQuery(kw, ignore_empty_string=True, operator='and'):
+    """
+      Build a ComplexQuery from kw values.
+
+      kw (dict: string keys, any value)
+        A query will be emited based on its value. Depending on the type of
+        the value it is handled differently. Query values will be passed
+        through to result (key is ignored). For all other types, their key
+        must be either a known column of a sql_search_tables table, or a
+        related key name.
+        - String values will be parsed according to the default SearchKey of
+          their real column (even for related keys). If parsing was
+          successful, Queries will be generated from its output.
+          Otherwise, that value will be taken as such.
+        - Dictionary values can be composed of the following keys:
+          'query': Their payload value, considered as empty if not given.
+          'key': The SearchKey to use for this value, overriding default
+                 column configuration.
+          (for other possible keys, see SearchKeys)
+          They will be taken as such.
+        - All other types will be taken as such, and no "empty" check will be
+          performed on them.
+      ignore_empty_string (boolean)
+        If True, values from kw which are empty will be skipped.
+      operator (string)
+        Used to explicit the logical relation between kw entries.
+        It must be a valid ComplexQuery logical operator ('and', 'or').
+    """
+
+  def buildSQLQuery(query_table='catalog', REQUEST=None,
+                    ignore_empty_string=1, only_group_columns=False,
+                    limit=None, extra_column_list=None,
+                    **kw):
+    """
+      Construct and return an instance of EntireQuery class from given
+      parameters by calling buildQuery.
+
+      ignore_empty_string (boolean)
+        See buildQuery.
+      limit (1-tuple, 2-tuple)
+        If given, will emit SQL to limit the number of result lines.
+      group_by_list (list of strings)
+        If given, will emit SQL to group found lines on given parameter names
+        (their column if they are column names, corresponding virtual columns
+        otherwise - as for related keys).
+      select_dict (dict, key: string, value: string, None)
+        Given values describe columns to make available in SQL result.
+        If column is aliased in result set, key is the alias and value is the
+        column.
+        Otherwise, key is the column, and value can be None or the same as
+        key.
+      select_list (list of strings)
+        Same as providing select_dict with select_list items as keys, and None
+        values.
+      order_by_list (list of 1-, 2-, or 3-tuples of strings)
+        If given, will emit SQL to sort result lines.
+        Sort will happen with decreasing precedence in list order.
+        Given n-tuples can contain those values, always in this order:
+         - parameter name
+         - sort order (see SQL documentation of 'ORDER BY')
+         - type cast (see SQL documentation of 'CAST')
+        Sort will happen on given parameter name (its column if it's a column
+        name, corresponding virtual column otherwise - as for related keys).
+      Extra parameters are passed through to buildQuery.
+
+      Backward compatibility parameters:
+      Those parameters are deprecated and should not be used. They are present
+      to provide backward compatibility with former ZSQLCatalog version.
+      REQUEST
+        Ignored.
+      extra_column_list (list)
+      query_table (string, None)
+        The table to use as catalog table.
+        If given and None, not catalog table will be used. Use this when you
+        are using SQLCatalog to generate manualy a part of another query.
+        That table has a special position in returned query:
+         - all other tables are joined on this one (when it is required to use
+           other tables)
+         - it is expected to have some columns (uid, path)
+        It is strongly discouraged to use this parameter for any value other
+        than None.
+      group_by
+        Use group_by_list instead.
+      group_by_expression
+        Use group_by_list instead.
+      select_expression
+        Use select_list or select_dict instead.
+      sort_on
+        Use order_by_list instead.
+      sort_order
+        Use order_by_list instead.
+      from_expression
+        This value will be emited in SQL directly in addition to computed
+        value.
+        There is no replacement.
+      where_expression
+        This value will be emited in SQL directly in addition to computed
+        value.
+        Use Query instances instead.
+      select_expression_key
+        This prevents given column from being ignored even if they could not
+        be mapped.
+        There is no replacement.
+      only_group_columns
+        Replaces former stat__ parameter.
+        Used to globally disalow use of non-group columns in SQL.
+    """
+
+  def getSearchKey(column, search_key=None):
+    """
+      Returns the default SearchKey instance for the
+      requested column. There is one instance per
+      search_key (incl. virtual keys surch as
+      source_title) and we try to compute it once
+      only and cache it.
+      If search_key is provided, it is used as the
+      name of the search key class to return.
+    """
+
+  def getComparisonOperator(operator):
+    """
+      Return a comparison operator matching given string.
+      String must be a valid SQL comparison operator (=, LIKE, IN, ...).
+      String case does not matter.
+      There is one comparison operator instance per possible string value.
+    """
+
+  # TODO: add support for other operators (logical, ensemblist (?))
+
+  def searchResults(REQUEST=None, **kw):
+    """
+      Invokes queryResults with the appropriate
+      ZSQL Method to return a list of results
+    """
+
+  def countResults(REQUEST=None, **kw):
+    """
+      Invokes queryResults with the appropriate
+      ZSQL Method to return a statistics for
+      a list of results
+    """
+
+  def queryResults(sql_method, REQUEST=None, src__=0, build_sql_query_method=None, **kw):
+    """
+      Return the result of the given 'sql_method' ZSQL Method after
+      processing all parameters to build a Query object passed to
+      that method.
+
+      The implementation should do the following.
+
+      1- Use **kw parameters to build a Query object
+         by invoking buildQuery
+
+      2- Build a ColumnMap instance by invoking
+         the buildColumnMap on the Query. Some
+         optmisation may happen here to try
+         to build the best possible ColumnMap and
+         use the best possible indices for joining.
+         During the ColumnMap build process, the
+         Search Key associated to each Query node
+         in the Query tree registers the columns
+         which are used (ex. to search) or provided
+         (ex. MATCH value for full text search, 
+         interleave expression or parameter in a
+         UNION Query)
+
+      3- Render the query object as an SQLExpression
+         instance. This instance contains all necessary
+         parts to generate:
+           - where_expression
+           - sort_expression
+           - group_by_expression
+           - select_expression
+
+      4- Invoke sql_method
+    """
diff --git a/product/ZSQLCatalog/Interface/ISQLExpression.py b/product/ZSQLCatalog/Interface/ISQLExpression.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8b99877fd0baabc2eb0dd5cb69103f0a8a3c862
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/ISQLExpression.py
@@ -0,0 +1,119 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class ISQLExpression(Interface):
+  """
+    This is a container for chunks of SQL generated by a Query.
+    Each Query instance generates its own SQLExpression instance.
+    SQLExpressions can be nested (an SQLExpression can contain 0, 1 or more
+    SQLExpressions).
+
+    Chunks are:
+    - table_alias_dict
+    - order_by_list
+    - group_by_list
+    - select_dict
+    - limit
+    Mutualy exclusive chunks are:
+    - where expression
+    - sql_expression_list
+    Deprecated chunks are:
+    - from_expression
+
+    Providing sql_expression_list with more than one entry makes
+    where_expression_operator mandatory.
+  """
+
+  def __init__(query,
+               table_alias_dict=None,
+               order_by_list=None,
+               order_by_dict=None,
+               group_by_list=None,
+               where_expression=None,
+               where_expression_operator=None,
+               sql_expression_list=None,
+               select_dict=None,
+               limit=None,
+               from_expression=None):
+    """
+      Instantiate an SQLExpression object.
+
+      This method does consistency checks on received parameters, so that
+      failures are detected as early as possible.
+
+      Also, it casts most optional parameters into empty lists and empty dicts
+      to make code using those values simpler.
+
+      query (Query)
+        The Query instance which called this constructor.
+      table_alias_dict (dict, key: string, value: string)
+        Table alias dict as returned by ColumnMap.getTableAliasDict() .
+      order_by_list (list of strings)
+        List of result ordering, pre-rendered.
+      order_by_dict (dict, key: string, value: string)
+        Column rendering replacement specific to order_by.
+      group_by_list (list of strings)
+        List of column names on which result line list will be grouped.
+      where_expression (string)
+        Text representing a "where" expression of an SQL query.
+      where_expression_operator ("and", "or", "not", None)
+        Operator to apply on immediately contained SQLExpressions.
+        It must be "and" or "or" when there are multiple contained
+        SQLExpressions, it can be "not" if there is exactly one contained
+        SQLExpression, and must not be provided if there is no contained
+        SQLExpression.
+      sql_expression_list (list of SQLExpression)
+        List of immediately contained SQLExpressions.
+      select_dict (dict, key:string, value:string or Null)
+        Lists all columns to be part of select expression.
+        Key is column alias.
+        Value is column name, or Null. If it is Null, the alias will also be
+        used as column name.
+      limit (1-tuple, 2-tuple, other)
+        First item is the number of lines expected, second one if given is the
+        offset of limited result list within the unlimited result list.
+        If it is not a tuple it is treated as the value of a 1-tuple
+        parameter.
+      from_expression (string)
+        This parameter is deprecated.
+    """
+
+  def asSQLExpressionDict():
+    """
+      Returns a dictionnary usable as a **kw for a catalog sql method.
+
+      It renders aliases (see getTableAliasDict) as a list of strings and
+      "from" expression (see getFromExpression) as a list of strings.
+
+      See getWhereExpression, getOrderByExpression, getLimitExpression,
+      getSelectExpression, getGroupByExpression.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/ISearchKey.py b/product/ZSQLCatalog/Interface/ISearchKey.py
new file mode 100644
index 0000000000000000000000000000000000000000..9dc86d3cdb52ca30c782b36e8f35156360d13125
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/ISearchKey.py
@@ -0,0 +1,190 @@
+##############################################################################
+#
+# Copyright (c) 2002-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Interface import Interface
+
+class ISearchKey(Interface):
+  """
+    A SearchKey generates Query instances.
+    It is responsible for parsing (or not) input value and generating the
+    appropriate Query structure to match the intended search.
+
+    This class is designed as a "singleton per column".
+  """
+
+  def __init__(column):
+    """
+      column (string)
+        The column this SearchKey is instanciated for.
+    """
+
+  def getColumn():
+    """
+      Returns: string
+        The column this SearchKey is instanciated for.
+    """
+
+  def buildSQLExpression(operator, value, column_map, only_group_columns, group):
+    """
+      This method passes on SQLExpression creation to operator, giving it the
+      resolved column of this RelatedKey.
+
+      operator (Operator)
+        A comparison operator. It is used to render value, column and itself
+        as valid SQL.
+      value (anything)
+      column_map (ColumnMap)
+        The (built) column map to render our column.
+      group (string)
+        The group our column was part of.
+
+      Returns: SQLExpression
+        SQLExpression build by operator for current Query.
+    """
+
+  def buildSearchTextExpression(operator, value, column=None):
+    """
+      Render comparison using operator between value and our column as
+      SearchText.
+
+      operator (Operator)
+        A comparison operator. It is used to render value as value SearchText.
+      value (anything)
+      column (string)
+        If given, it overrides our column in SearchText rendering. This is
+        useful when SearchText rendering is done for a virtual column,
+        because the name of the virtual column must appear when rendered as
+        SearchText.
+    """
+
+  def registerColumnMap(column_map, group, simple_query):
+    """
+      Register the column of this SearchKey to given column map.
+
+      column_map (ColumnMap)
+        The ColumnMap instance to register to.
+      group (string)
+        The group registration will be part of.
+      simple_query (SimpleQuery)
+        The SimpleQuery being registered.
+
+      Returns: string
+        The group assigned to caller. See ColumnMap for methods redefining
+        caller's group.
+    """
+
+  def buildQuery(search_value, group=None, logical_operator=None, comparison_operator=None):
+    """
+      Generate queries from given search_value.
+      logical_operator ('and', 'or', 'not', None)
+        If no logical operator can be found in search_value, it will use given
+        default operator.
+      comparison_operator (string, None)
+        If given, expresses the comparison between column and value.
+    """
+
+  def parseSearchText(value):
+    """
+      Parse given value to generate an Abstract Syntax Tree representing its
+      logical structure, or None if there is no obvious structure in given
+      value.
+      See SearchText for parsing code.
+
+      value (string)
+        The string to parse.
+
+      Returns: (None, AbstratSyntaxNode)
+        AbstratSyntaxNode complies with the IAbstractSyntaxNode interface.
+    """
+
+class IRelatedKey(ISearchKey):
+  """
+    A RelatedKey is a special variation of a SearchKey.
+    Only a small set of methods differ. They are defined in this class.
+  """
+
+  def registerColumnMap(column_map, table_alias_list=None):
+    """
+      This is slightly different for regular registerColumnMap in that it must
+      register multiple tables (and not columns, since RelatedKeys do not
+      provide this information).
+      Also, it must register namely "catalog" table and resolve its alias,
+      angain since it's hardcoded in RelatedKey ZSQLMethods and not provided
+      by their definitions.
+
+      column_map (ColumnMap)
+
+      Deprecated:
+        table_alias_list (None, list of 2-tuples of strings)
+          This list must have the exact same length as the list of tables 
+    """
+
+  def buildSQLExpression(sql_catalog, column_map, only_group_columns, group):
+    """
+      operator and value parameters are useless, since this type of SearhKey
+      does not compare a value to any column, but uses a ZSQLMethod.
+      To reach that ZSQLMethod, it also required a new sql_catalog parameter.
+
+      sql_catalog (SQLCatalog)
+        Used to retrieve related key's ZSQLMethod.
+    """
+
+  def buildQuery(sql_catalog, related_key_definition, search_value=None, search_key_name=None, logical_operator=None, comparison_operator=None):
+    """
+      group is useless here, since group is determined by ColumnMap at
+      registration time. search_value becomes optional.
+
+      sql_catalog (SQLCatalog)
+        Used to retrieve real column's SearchKey. For example, a RelatedKey
+        used to compare with a "title" column will retrieve title's default
+        SearchKey (which should be a KeywordKey).
+      related_key_definition (string)
+        Describes parameters of a RelatedKey. It is composed of 3 mains parts,
+        separated by '/':
+        - a list of table names
+          Table names are separated by ','
+        - a column name
+        - the name of the related key ZSQLMethod
+      search_value (anything)
+        If given, a condition on real column will be generated.
+        Otherwise, only the SQL required to reach that column will be
+        generated. This is usefull when sorting on a virtual column, for
+        example.
+      search_key_name (string, None)
+        If given, it overrides real column's default SearchKey.
+      logical_operator (string, None)
+        If given, expresses the default logical link between operands.
+        It must be one of None, 'or' and 'and'.
+        It is overriden by operator present in search_value if it is a dict
+        and contains an 'operator' key.
+        'or' is assumed if not given or given with a None value.
+      comparison_operator (string, None)
+        If given, expresses the comparison between column and value.
+    """
+
diff --git a/product/ZSQLCatalog/Interface/Verify.py b/product/ZSQLCatalog/Interface/Verify.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c13e2d9b38a9a36ee63fa88c9708a6cfc51b348
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/Verify.py
@@ -0,0 +1,3 @@
+# This imports from Zope's products, which would otherwise be unreachable from parent folder.
+from Interface.Verify import verifyClass
+
diff --git a/product/ZSQLCatalog/Interface/__init__.py b/product/ZSQLCatalog/Interface/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/product/ZSQLCatalog/Interface/__init__.py
@@ -0,0 +1 @@
+
diff --git a/product/ZSQLCatalog/Operator/ComparisonOperator.py b/product/ZSQLCatalog/Operator/ComparisonOperator.py
new file mode 100644
index 0000000000000000000000000000000000000000..d990fadb92cd4534499e79594005fc2ddf9002dc
--- /dev/null
+++ b/product/ZSQLCatalog/Operator/ComparisonOperator.py
@@ -0,0 +1,147 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 OperatorBase import OperatorBase
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.Interface.IOperator import IOperator
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+class ComparisonOperatorBase(OperatorBase):
+  @profiler_decorator
+  def asSQLExpression(self, column, value_list, only_group_columns):
+    """
+      In a Comparison Operator, rendering order is:
+        <column> <operator> <value_list>
+    """
+    column, value_list = self.render(column, value_list)
+    return SQLExpression(self, where_expression='%s %s %s' % (column, self.getOperator().upper(), value_list))
+
+  def render(self, column, value_list):
+    raise NotImplementedError, 'This method must be overloaded by a subclass.'
+
+  def renderValue(self, value_list):
+    raise NotImplementedError, 'This method must be overloaded by a subclass.'
+
+verifyClass(IOperator, ComparisonOperatorBase)
+
+class MonovaluedComparisonOperator(ComparisonOperatorBase):
+  @profiler_decorator
+  def renderValue(self, value_list):
+    """
+      value_list must either be a non-list or a single-value list.
+    """
+    if isinstance(value_list, (tuple, list)):
+      if len(value_list) > 1:
+        raise ValueError, '%r: value_list must not contain more than one item. Got %r' % (self, value_list)
+      value_list = value_list[0]
+    return self._renderValue(value_list)
+
+  @profiler_decorator
+  def render(self, column, value_list):
+    """
+      value_list must either be a non-list or a single-value list.
+    """
+    if isinstance(value_list, (tuple, list)):
+      if len(value_list) > 1:
+        raise ValueError, '%r: value_list must not contain more than one item. Got %r' % (self, value_list)
+      value_list = value_list[0]
+    return self._render(column, value_list)
+
+verifyClass(IOperator, MonovaluedComparisonOperator)
+
+class MultivaluedComparisonOperator(ComparisonOperatorBase):
+  @profiler_decorator
+  def renderValue(self, value_list):
+    """
+      value_list must be a multi-value list (more than one item).
+    """
+    if not isinstance(value_list, (tuple, list)) or len(value_list) < 2:
+      raise ValueError, '%r: value_list must be a list of more than one item. Got %r' % (self, value_list)
+    return '(%s)' % (', '.join([self._renderValue(x) for x in value_list]), )
+
+  @profiler_decorator
+  def render(self, column, value_list):
+    """
+      value_list must be a multi-value list (more than one item).
+    """
+    if not isinstance(value_list, (tuple, list)) or len(value_list) < 2:
+      raise ValueError, '%r: value_list must be a list of more than one item. Got %r' % (self, value_list)
+    return column, '(%s)' % (', '.join([self._renderValue(x) for x in value_list]), )
+
+verifyClass(IOperator, MultivaluedComparisonOperator)
+
+class MatchComparisonOperator(MonovaluedComparisonOperator):
+  def __init__(self, operator, mode=''):
+    MonovaluedComparisonOperator.__init__(self, operator)
+    self.where_expression_format_string = 'MATCH (%%(column)s) AGAINST (%%(value_list)s%s)' % (mode, )
+
+  @profiler_decorator
+  def asSQLExpression(self, column, value_list, only_group_columns):
+    """
+      This operator can emit a select expression, so it overrides
+      asSQLExpression inseatd of just defining a render method.
+    """
+    match_string = self.where_expression_format_string % {
+      'column': column,
+      'value_list': self.renderValue(value_list),
+    }
+    select_dict = {}
+    if not only_group_columns:
+      select_dict[column.replace('`', '').split('.')[-1]] = match_string
+    # Sort on this column uses relevance.
+    # TODO: Add a way to allow sorting by raw column value.
+    order_by_dict = {
+      column: self.where_expression_format_string,
+    }
+    return SQLExpression(
+      self,
+      select_dict=select_dict,
+      where_expression=match_string,
+      order_by_dict=order_by_dict,
+    )
+
+verifyClass(IOperator, MatchComparisonOperator)
+
+operator_dict = {
+  '=': MonovaluedComparisonOperator('='),
+  '!=': MonovaluedComparisonOperator('!='),
+  '>': MonovaluedComparisonOperator('>'),
+  '<': MonovaluedComparisonOperator('<'),
+  '<=': MonovaluedComparisonOperator('<='),
+  '>=': MonovaluedComparisonOperator('>='),
+  'like': MonovaluedComparisonOperator('like'),
+  'match': MatchComparisonOperator('match'),
+  'match_boolean': MatchComparisonOperator('match_boolean', mode=' IN BOOLEAN MODE'),
+  'match_expansion': MatchComparisonOperator('match_expansion', mode=' WITH QUERY EXPANSION'),
+  'in': MultivaluedComparisonOperator('in'),
+  'is': MonovaluedComparisonOperator('is'),
+}
+
diff --git a/product/ZSQLCatalog/Operator/OperatorBase.py b/product/ZSQLCatalog/Operator/OperatorBase.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e5e8c561ff98f109aebe0e21ab1db624e9ed76c
--- /dev/null
+++ b/product/ZSQLCatalog/Operator/OperatorBase.py
@@ -0,0 +1,155 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 zLOG import LOG
+from Products.ZSQLCatalog.Interface.IOperator import IOperator
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+@profiler_decorator
+def escapeString(value):
+  # Inspired from ERP5Type/Utils:sqlquote, but this product must not depend on it.
+  return "'" + value.replace('\\', '\\\\').replace("'", "''") + "'"
+
+@profiler_decorator
+def valueFloatRenderer(value):
+  if isinstance(value, basestring):
+    value = float(value.replace(' ', ''))
+  return repr(value)
+
+@profiler_decorator
+def valueDateTimeRenderer(value):
+  return '"%s"' % (value.toZone('UTC').ISO(), )
+
+@profiler_decorator
+def valueDefaultRenderer(value):
+  LOG('OperatorBase', 0, 'Unhandled value class: %s (%r). Converted to string and escaped.' % (value.__class__.__name__, value))
+  return escapeString(str(value))
+
+@profiler_decorator
+def valueNoneRenderer(value):
+  return 'NULL'
+
+value_renderer = {
+  'int': str,
+  'long': str,
+  'float': valueFloatRenderer,
+  'DateTime': valueDateTimeRenderer,
+  'NoneType': valueNoneRenderer,
+}
+
+value_search_text_renderer = {
+  'DateTime': str,
+}
+
+@profiler_decorator
+def valueDefaultSearchTextRenderer(value):
+  """
+    This is just repr, but always surrounding text strings with doublequotes.
+  """
+  if isinstance(value, basestring):
+    result = '"%s"' % (value.replace('\\', '\\\\').replace('"', '\\"'), )
+  else:
+    result = repr(value)
+  return result
+
+@profiler_decorator
+def columnFloatRenderer(column, format=None):
+  if format is not None:
+    if '.' in format:
+      format = format.replace(' ', '')
+      column = "TRUNCATE(%s, %s)" % (column, len(format.split('.')[-1]))
+  return column
+
+@profiler_decorator
+def columnDefaultRenderer(column, format=None):
+  return column
+
+column_renderer = {
+  'float': columnFloatRenderer
+}
+
+class OperatorBase(object):
+
+  __implements__ = IOperator
+
+  def __init__(self, operator):
+    self.operator = operator
+
+  def getOperator(self):
+    return self.operator
+
+  @profiler_decorator
+  def _render(self, column, value):
+    """
+      Render given column and value for use in SQL.
+      Value is rendered to convert it to SQL-friendly value.
+      Column is rendered to include possible cast code.
+
+      column (string)
+        Column on which the value will be matched
+      value (see _renderValue)
+        Value to render.
+    """
+    if isinstance(value, dict):
+      type = value['type']
+      column = column_renderer.get(type, columnDefaultRenderer)(column, format=value['format'])
+      value = value_renderer.get(type, valueDefaultRenderer)(value['query'])
+    else:
+      value = self._renderValue(value)
+    return column, value
+
+  @profiler_decorator
+  def _renderValue(self, value):
+    """
+      Render given value as string.
+
+      value (int, float, long, DateTime, string, None)
+        Value to render as a string for use in SQL (quoted, escaped).
+    """
+    if isinstance(value, basestring):
+      value = escapeString(value)
+    else:
+      value = value_renderer.get(value.__class__.__name__, valueDefaultRenderer)(value)
+    return value
+
+  @profiler_decorator
+  def asSearchText(self, value):
+    return value_search_text_renderer.get(value.__class__.__name__, valueDefaultSearchTextRenderer)(value)
+
+  def asSQLExpression(self, column, value_list, only_group_columns):
+    raise NotImplementedError, 'This method must be overloaded by a subclass ' \
+      'to be able to get an SQL representation of this operator.'
+
+  def __repr__(self):
+    return '<%s(%r) at %s>' % (self.__class__.__name__, self.getOperator(), id(self))
+
+verifyClass(IOperator, OperatorBase)
+
diff --git a/product/ZSQLCatalog/Operator/__init__.py b/product/ZSQLCatalog/Operator/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4710ca60bafa16f16dbbe710e18cb8213558f741
--- /dev/null
+++ b/product/ZSQLCatalog/Operator/__init__.py
@@ -0,0 +1,2 @@
+from ComparisonOperator import operator_dict
+
diff --git a/product/ZSQLCatalog/Query/AutoQuery.py b/product/ZSQLCatalog/Query/AutoQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d8331ff4a7c678a4edc45869e99d5de5b0d1b64
--- /dev/null
+++ b/product/ZSQLCatalog/Query/AutoQuery.py
@@ -0,0 +1,139 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Query import Query
+from zLOG import LOG
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+class AutoQuery(Query):
+  """
+    An AutoQuery is a compatibility layer for former Query class.
+    It passes parameters given at instantiation time to SQLCatalog's
+    buildQuery or buildSingleQuery, and wraps resulting Query instance (proxy
+    behaviour).
+
+    This is only here for backward compatibility, and use is strongly
+    discouraged. Use SQLCatalog API instead.
+  """
+  wrapped_query = None
+
+  @profiler_decorator
+  def __init__(self, *args, **kw):
+    """
+      Note: "operator" might contain a logical or a comparison operator.
+    """
+    if len(args):
+      LOG('AutoQuery', 100, 'Got extra positional parameters (will be ignored): %r' % (args, ))
+    self.table_alias_list = kw.pop('table_alias_list', None)
+    self.kw = kw
+    operator = kw.pop('operator', None)
+    if isinstance(operator, basestring):
+      operator = operator.lower()
+    self.operator = operator
+    self.ignore_empty_string = kw.pop('ignore_empty_string', True)
+    if 'key' in kw and len(kw) > 2:
+      raise ValueError, '"key" parameter cannot be used when more than one column is given. key=%r' % (self.search_key, )
+    self.search_key = kw.pop('key', None)
+
+  @profiler_decorator
+  def _createWrappedQuery(self, sql_catalog):
+    """
+      Create wrapped query. This requires being able to reach catalog, since
+      we use it as a query producer.
+    """
+    kw = self.kw
+    operator = self.operator
+    if 'range' in kw:
+      # If we received a range parameter we are building a single query.
+      # Recreate value as a dict and pass it to buildSingleQuery.
+      range = kw.pop('range')
+      assert len(kw) == 1, repr(kw)
+      key, value = kw.items()[0]
+      query = sql_catalog.buildSingleQuery(key, {'query': value,
+                                                 'range': range})
+    elif operator == 'in':
+      # 'in' is a *comparison* operator, not a logical operator.
+      # Transform kw into the proper form.
+      assert len(kw) == 1, repr(kw)
+      key, value = kw.items()[0]
+      query = sql_catalog.buildSingleQuery(key, {'query': value,
+                                                 'operator': operator})
+    elif len(kw) == 1 and isinstance(kw.values()[0], (tuple, list)) and \
+       operator in ('and', 'or'):
+      # If there is only one parameter, and operator was given and is a
+      # known logical operator, then operator will apply to it.
+      # For example (from testDomainTool):
+      #  kw = {'portal_type': ['!=a', '!=b'], 'operator': 'AND'}
+      #  In such case, expected result is
+      #  "portal_type!='a' AND portal_type!='b'"
+      key, value = kw.items()[0]
+      query = sql_catalog.buildSingleQuery(key, value, logical_operator=operator)
+    else:
+      # Otherwise, the operator will apply to the relationship between
+      # parameters.
+      if operator is None:
+        operator = 'and'
+      if self.search_key is not None:
+        key, value = kw.items()[0]
+        kw = {key: {'query': value, 'key': self.search_key}}
+      query = sql_catalog.buildQuery(kw, operator=operator, ignore_empty_string=self.ignore_empty_string)
+    if self.table_alias_list is not None:
+      query.setTableAliasList(self.table_alias_list)
+    self.wrapped_query = query
+
+  @profiler_decorator
+  def asSearchTextExpression(self, sql_catalog, column=None):
+    if self.wrapped_query is None:
+      self._createWrappedQuery(sql_catalog)
+    return self.wrapped_query.asSearchTextExpression(sql_catalog, column=column)
+
+  @profiler_decorator
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
+    if self.wrapped_query is None:
+      self._createWrappedQuery(sql_catalog)
+    return self.wrapped_query.asSQLExpression(sql_catalog, column_map, only_group_columns=only_group_columns)
+
+  @profiler_decorator
+  def registerColumnMap(self, sql_catalog, column_map):
+    if self.wrapped_query is None:
+      self._createWrappedQuery(sql_catalog)
+    return self.wrapped_query.registerColumnMap(sql_catalog, column_map)
+
+  def __repr__(self):
+    if self.wrapped_query is None:
+      result = '<%s(**%r) at %s>' % (self.__class__.__name__, self.kw, id(self))
+    else:
+      result = '<%s %r>' % (self.__class__.__name__, self.wrapped_query)
+    return result
+
+verifyClass(IQuery, AutoQuery)
+
diff --git a/product/ZSQLCatalog/Query/ComplexQuery.py b/product/ZSQLCatalog/Query/ComplexQuery.py
index 16919945a734b9ff410463d81e7419ca389de68d..d817be737d3c31e927ec7b101bdda4b6165aeda0 100644
--- a/product/ZSQLCatalog/Query/ComplexQuery.py
+++ b/product/ZSQLCatalog/Query/ComplexQuery.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,68 +28,127 @@
 #
 ##############################################################################
 
-from Products.PythonScripts.Utility import allow_class
-from Query import QueryMixin
+from Query import Query
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from SQLQuery import SQLQuery
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
 
-class ComplexQuery(QueryMixin):
+class ComplexQuery(Query):
   """
-  Used in order to concatenate many queries
+    A ComplexQuery represents logical operations between Query instances.
   """
-
+  @profiler_decorator
   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
+      *args (tuple of Query or of list of Query)
+        list-type entry will extend subquery list, other entries will be
+        appended.
+      logical_operator ('and', 'or', 'not')
+        Logical operator.
+        Default: 'and'
+
+      Deprecated 
+        operator ('and', 'or', 'not')
+          See logical_operator.
+          logical_operator takes precedence if given.
+        unknown_column_dict (dict)
+          Only one key of this dictionnary is used here:
+            key: 'from_expression'
+            value: string
+            This value will be passed through to SQLExpression. If it is
+            provided, this ComplexQuery must have no subquery (regular
+            SQLExpression limitation)
+        implicit_table_list (list of strings)
+          Each entry in this list will be registered to column map. This is
+          used to make column mapper choose tables differently.
     """
-    sql_expression_list = []
-    select_expression_list = []
-    for query in self.getQueryList():
-      if isinstance(query, basestring):
-        sql_expression_list.append(query)
+    self.logical_operator = kw.pop('logical_operator', kw.pop('operator', 'and')).lower()
+    assert self.logical_operator in ('and', 'or', 'not'), self.logical_operator
+    unknown_column_dict = kw.pop('unknown_column_dict', {})
+    self.from_expression = unknown_column_dict.pop('from_expression', None)
+    self.implicit_table_list = kw.pop('implicit_table_list', [])
+    query_list = []
+    append = query_list.append
+    extend = query_list.extend
+    # Flaten the first level of list-type arguments
+    for arg in args:
+      if isinstance(arg, (list, tuple)):
+        extend(arg)
       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}
+        append(arg)
+    new_query_list = []
+    append = new_query_list.append
+    # Iterate over the flaten argument list to cast each into a query type.
+    for query in query_list:
+      if not isinstance(query, Query):
+        query = SQLQuery(query)
+      append(query)
+    self.query_list = new_query_list
+
+  @profiler_decorator
+  def asSearchTextExpression(self, sql_catalog, column=None):
+    if column in (None, ''):
+      query_column = column
+    else:
+      query_column = ''
+    search_text_list = [y for y in [x.asSearchTextExpression(sql_catalog, column=query_column) for x in self.query_list] if y is not None]
+    if len(search_text_list) == 0:
+      result = ''
+    else:
+      if self.logical_operator in ('and', 'or'):
+        if len(search_text_list) == 1:
+          result = search_text_list[0]
+        else:
+          logical_operator = ' %s ' % (self.logical_operator.upper(), )
+          result = '(%s)' % (logical_operator.join(search_text_list), )
+      elif self.logical_operator == 'not':
+        assert len(search_text_list) == 1
+        result = '(NOT %s)' % (search_text_list[0], )
+      else:
+        raise ValueError, 'Unknown operator %r' % (self.logical_operator, )
+      if column not in (None, ''):
+        result = '%s:%s' % (column, result)
     return result
 
-  def getSQLKeyList(self):
+  @profiler_decorator
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
+    sql_expression_list = [x.asSQLExpression(sql_catalog, column_map, only_group_columns)
+                           for x in self.query_list]
+    if len(sql_expression_list) == 0:
+      sql_expression_list = [SQLExpression(self, where_expression='1')]
+    return SQLExpression(self,
+      sql_expression_list=sql_expression_list,
+      where_expression_operator=self.logical_operator,
+      from_expression=self.from_expression)
+
+  @profiler_decorator
+  def registerColumnMap(self, sql_catalog, column_map):
+    for implicit_table_column in self.implicit_table_list:
+      column_map.registerColumn(implicit_table_column)
+    for query in self.query_list:
+      query.registerColumnMap(sql_catalog, column_map)
+
+  def __repr__(self):
+    return '<%s of %r.join(%r)>' % (self.__class__.__name__, self.logical_operator, self.query_list)
+
+  @profiler_decorator
+  def setTableAliasList(self, table_alias_list):
     """
-    Returns the list of keys used by this
-    instance
+      This function is here for backward compatibility.
+      This can only be used when there is one and only one subquery which
+      defines a setTableAliasList method.
+      
+      See RelatedQuery.
     """
-    key_list=[]
-    for query in self.getQueryList():
-      if not(isinstance(query, basestring)):
-        key_list.extend(query.getSQLKeyList())
-    return key_list
+    assert len(self.query_list) == 1
+    self.query_list[0].setTableAliasList(table_alias_list)
+
+  @profiler_decorator
+  def setGroup(self, group):
+    for query in self.query_list:
+      query.setGroup(group)
+
+verifyClass(IQuery, ComplexQuery)
 
-allow_class(ComplexQuery)
diff --git a/product/ZSQLCatalog/Query/EntireQuery.py b/product/ZSQLCatalog/Query/EntireQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..37eee743dd70fdafbf4d8b05e42deb07cd2f42da
--- /dev/null
+++ b/product/ZSQLCatalog/Query/EntireQuery.py
@@ -0,0 +1,182 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.ColumnMap import ColumnMap
+from zLOG import LOG
+from Products.ZSQLCatalog.Interface.IEntireQuery import IEntireQuery
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+class EntireQuery(object):
+  """
+    This is not a Query subclass, since it does not define a
+    registerColumnMap method, and instead does the ColumnMap handling
+    internaly.
+  """
+
+  __implements__ = IEntireQuery
+
+  column_map = None
+
+  @profiler_decorator
+  def __init__(self, query, order_by_list=None, group_by_list=None,
+               select_dict=None, limit=None, catalog_table_name=None,
+               extra_column_list=None, from_expression=None,
+               order_by_override_list=None):
+    def default(value):
+      if value is None:
+        return []
+      assert isinstance(value, (tuple, list))
+      return value
+    def defaultDict(value):
+      if value is None:
+        return {}
+      assert isinstance(value, dict)
+      return value
+    self.query = query
+    self.order_by_list = default(order_by_list)
+    self.order_by_override_set = frozenset(default(order_by_override_list))
+    self.group_by_list = default(group_by_list)
+    self.select_dict = defaultDict(select_dict)
+    self.limit = limit
+    self.catalog_table_name = catalog_table_name
+    self.extra_column_list = default(extra_column_list)
+    self.from_expression = from_expression
+
+  def asSearchTextExpression(self, sql_catalog):
+    return query.asSearchTextExpression(sql_catalog)
+
+  @profiler_decorator
+  def asSQLExpression(self, sql_catalog, only_group_columns):
+    column_map = self.column_map
+    if column_map is None:
+      # XXX: should we provide a way to register column map as a separate mathod or do it here ?
+      # Column Map was not built yet, do it.
+      self.column_map = column_map = ColumnMap(catalog_table_name=self.catalog_table_name)
+      for extra_column in self.extra_column_list:
+        table, column = extra_column.replace('`', '').split('.')
+        if table != self.catalog_table_name:
+          raise ValueError, 'Extra columns must be catalog columns. %r does not follow this rule (catalog=%r, extra_column_list=%r)' % (extra_column, self.catalog_table_name, self.extra_column_list)
+        column_map.registerColumn(extra_column)
+      for column in self.group_by_list:
+        column_map.registerColumn(column)
+      for alias, column in self.select_dict.iteritems():
+        if column is None:
+          column = alias
+        else:
+          column_map.ignoreColumn(alias)
+        column_map.registerColumn(column)
+      for override in self.order_by_override_set:
+        column_map.ignoreColumn(override)
+      for order_by in self.order_by_list:
+        assert isinstance(order_by, (tuple, list))
+        assert len(order_by)
+        column_map.registerColumn(order_by[0])
+      self.query.registerColumnMap(sql_catalog, column_map)
+      column_map.build(sql_catalog)
+      # Replace given group_by_list entries by their mapped representations.
+      new_column_list = []
+      append = new_column_list.append
+      for column in self.group_by_list:
+        try:
+          append(column_map.asSQLColumn(column))
+        except KeyError:
+          LOG('EntireQuery', 100, 'Group-by column %r could not be mapped, but is passed through. This use is strongly discouraged.' % (column, ))
+          append(column)
+      self.group_by_list = new_column_list
+      # Build a dictionnary from select_dict aliasing their mapped representations
+      self.final_select_dict = select_dict = {}
+      for alias, raw_column in self.select_dict.iteritems():
+        if raw_column is None:
+          column = alias
+        else:
+          column = raw_column
+        try:
+          rendered = column_map.asSQLColumn(column)
+        except KeyError:
+          LOG('EntireQuery', 100, 'Select column %r could not be mapped, but is passed through. This use is strongly discouraged.' % (column, ))
+          rendered = column
+        select_dict[alias] = rendered
+      # Replace given order_by_list entries by their mapped representations.
+      new_order_by_list = []
+      append = new_order_by_list.append
+      for order_by in self.order_by_list:
+        column = order_by[0]
+        if column in self.order_by_override_set:
+          LOG('EntireQuery', 100, 'Order-by column %r is forcibly accepted. This use is strongly discouraged.' % (column, ))
+          rendered = column
+        else:
+          try:
+            rendered = column_map.asSQLColumn(column)
+          except KeyError:
+            LOG('SQLCatalog', 100, 'Order by %r ignored: it could not be mapped to a known column.' % (order_by, ))
+            rendered = None
+        if rendered is not None:
+          if len(order_by) > 1:
+            if len(order_by) > 2 and order_by[2] not in (None, ''):
+              rendered = 'CAST(%s AS %s)' % (rendered, order_by[2])
+            rendered = '%s %s' % (rendered, order_by[1])
+          append(rendered)
+      self.order_by_list = new_order_by_list
+      # generate SQLExpression from query
+      sql_expression_list = [self.query.asSQLExpression(sql_catalog, column_map, only_group_columns)]
+      # generate join expression based on column_map.getJoinTableAliasList
+      append = sql_expression_list.append
+      for join_query in column_map.iterJoinQueryList():
+        append(join_query.asSQLExpression(sql_catalog, column_map, only_group_columns))
+      join_table_list = column_map.getJoinTableAliasList()
+      if len(join_table_list):
+        # XXX: Is there any special rule to observe when joining tables ?
+        # Maybe we could check which column is a primary key instead of
+        # hardcoding "uid".
+        where_pattern = '`%s`.`uid` = `%%s`.`uid`' % \
+          (column_map.getCatalogTableAlias(), )
+        # XXX: It would cleaner from completeness point of view to use column
+        # mapper to render column, but makes code much more complex to just do
+        # a simple text rendering. If there is any reason why we should have
+        # those column in the mapper, then we should use the clean way.
+        append(SQLExpression(self, where_expression=' AND '.join(
+          where_pattern % (x, ) for x in join_table_list
+        )))
+      self.sql_expression_list = sql_expression_list
+    return SQLExpression(
+      self,
+      table_alias_dict=column_map.getTableAliasDict(),
+      from_expression=self.from_expression,
+      order_by_list=self.order_by_list,
+      group_by_list=self.group_by_list,
+      select_dict=self.final_select_dict,
+      limit=self.limit,
+      where_expression_operator='and',
+      sql_expression_list=self.sql_expression_list)
+
+verifyClass(IEntireQuery, EntireQuery)
+
diff --git a/product/ZSQLCatalog/Query/Query.py b/product/ZSQLCatalog/Query/Query.py
index 2d159bca490e64bacabc47d65847dcdd90717403..5e3689062b72aa1d32cac4e525fdb0f81cbfdc8c 100644
--- a/product/ZSQLCatalog/Query/Query.py
+++ b/product/ZSQLCatalog/Query/Query.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,117 +28,35 @@
 #
 ##############################################################################
 
-from DocumentTemplate.DT_Var import sql_quote
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
 
-class QueryMixin:
+class Query(object):
   """
-    Mixing class which implements methods which are
-    common to all kinds of Queries
+    This is the base class of all kind of queries. Its only purpose is to be
+    able to distinguish any kind of value from a query.
   """
-  operator = None
-  format = None
-  type = None
 
-  def __call__(self, **kw):
-    return self.asSQLExpression(**kw)  
+  __implements__ = IQuery
+  __allow_access_to_unprotected_subobjects__ = 1
 
-  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
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
     """
-    return self.search_mode
-    
-  def getSearchKey(self):
-    """Search mode used for Full Text search
+      To enable SQL rendering, overload this method in a subclass.
     """
-    return self.search_key
-  
-  def getKey(self):
-    return self.key
+    raise TypeError, 'A %s cannot be rendered as an SQL expression.' % (self.__class__.__name__, )
 
-  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):
+  def asSearchTextExpression(self, sql_catalog, column=None):
     """
-      Return a dictionnary containing the keys and value types:
-        'where_expression': string
-        'select_expression_list': string
+      To enable Search Text rendering, overload this method in a subclass.
     """
-    raise NotImplementedError
+    raise TypeError, 'A %s cannot be rendered as a SearchText expression.' % (self.__class__.__name__, )
 
-  def getSQLKeyList(self):
-    """
-      Return a list of keys used by this query and its subqueries.
-    """
-    raise NotImplementedError
-  
-  def getRelatedTableMapDict(self):
+  def registerColumnMap(self, sql_catalog, column_map):
     """
-      Return for each key used by this query (plus ones used by its
-      subqueries) the table alias mapping.
+      This method must always be overloaded by subclasses.
     """
-    raise NotImplementedError
+    raise NotImplementedError, '%s is incompeltely implemented.' % (self.__class__.__name__, )
 
-  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
+verifyClass(IQuery, Query)
 
-  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    
diff --git a/product/ZSQLCatalog/Query/RelatedQuery.py b/product/ZSQLCatalog/Query/RelatedQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccfa4cf05d17c1a23eb4850ad822e3cd7f07a45e
--- /dev/null
+++ b/product/ZSQLCatalog/Query/RelatedQuery.py
@@ -0,0 +1,99 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Query import Query
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+class RelatedQuery(Query):
+  """
+    A RelatedQuery represents the is a container for a join condition.
+  """
+  @profiler_decorator
+  def __init__(self, search_key, join_condition=None, table_alias_list=None):
+    """
+      search_key (SearchKey)
+      join_condition (Query)
+        If given, it will be registered and rendered by this query.
+
+      Deprecated
+        table_alias_list (list of 2-tuple of strings)
+          See setTableAliasList.
+    """
+    self.search_key = search_key
+    self.join_condition = join_condition
+    self.table_alias_list = table_alias_list
+
+  @profiler_decorator
+  def setTableAliasList(self, table_alias_list):
+    """
+      This function is here for backward compatibility.
+
+      table_alias_list (list of 2-tuples of strings)
+        Each 2-tuple contains the name of a related key parameter and the
+        table alias it must be mapped on (respectively).
+    """
+    self.table_alias_list = table_alias_list
+
+  @profiler_decorator
+  def asSearchTextExpression(self, sql_catalog, column=None):
+    assert column is None
+    join_condition = self.join_condition
+    if join_condition is None:
+      result = None
+    else:
+      result = join_condition.asSearchTextExpression(sql_catalog, column=self.search_key.getColumn())
+    return result
+
+  @profiler_decorator
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
+    sql_expression_list = [self.search_key.buildSQLExpression(sql_catalog, column_map, only_group_columns, self.group)]
+    join_condition = self.join_condition
+    if join_condition is not None:
+      sql_expression_list.append(join_condition.asSQLExpression(sql_catalog, column_map, only_group_columns))
+    return SQLExpression(self, sql_expression_list=sql_expression_list, where_expression_operator='and')
+
+  @profiler_decorator
+  def registerColumnMap(self, sql_catalog, column_map):
+    self.group = self.search_key.registerColumnMap(column_map, table_alias_list=self.table_alias_list)
+    join_condition = self.join_condition
+    if join_condition is not None:
+      # Update its group
+      join_condition.setGroup(self.group)
+      # Propagate registration to embeded query
+      join_condition.registerColumnMap(sql_catalog, column_map)
+
+  def __repr__(self):
+    return '<%s on %r with %r>' % (self.__class__.__name__, self.search_key.getColumn(), self.join_condition)
+
+verifyClass(IQuery, RelatedQuery)
+
diff --git a/product/ZSQLCatalog/Query/SQLQuery.py b/product/ZSQLCatalog/Query/SQLQuery.py
new file mode 100644
index 0000000000000000000000000000000000000000..40546105037091c289b81ecaa75088817bd003fc
--- /dev/null
+++ b/product/ZSQLCatalog/Query/SQLQuery.py
@@ -0,0 +1,68 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 Query import Query
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
+
+class SQLQuery(Query):
+  """
+    This Query subclass is used to wrap raw SQL text.
+    Use of this class is strongly discouraged, and it is only here for
+    backward compatibility.
+  """
+  def __init__(self, payload):
+    """
+      payload (string)
+        Raw SQL text.
+    """
+    if not isinstance(payload, basestring):
+      raise TypeError, 'Payload must be a string, got a %r: %r' % (type(payload), payload)
+    assert len(payload)
+    self.payload = '(' + payload + ')'
+
+  def asSearchText(self, sql_catalog):
+    return None
+
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
+    return SQLExpression(self, where_expression=self.payload)
+
+  def registerColumnMap(self, sql_catalog, column_map):
+    """
+      There is nothing to register for this type of Query subclass.
+    """
+    pass
+
+  def __repr__(self):
+    return '<%s (%r)>' % (self.__class__.__name__, self.payload)
+
+verifyClass(IQuery, SQLQuery)
+
diff --git a/product/ZSQLCatalog/Query/SimpleQuery.py b/product/ZSQLCatalog/Query/SimpleQuery.py
index 39966a317424a73934244bed00b00bc5fcbeacaf..21849e246005360da39bad17a7cf6026811d1a1e 100644
--- a/product/ZSQLCatalog/Query/SimpleQuery.py
+++ b/product/ZSQLCatalog/Query/SimpleQuery.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,282 +28,104 @@
 #
 ##############################################################################
 
-from Products.PythonScripts.Utility import allow_class
-from DateTime import DateTime
-from Query import QueryMixin
-from pprint import pprint
+from Query import Query
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.Interface.IQuery import IQuery
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
 
-# valid search modes for queries
-FULL_TEXT_SEARCH_MODE = 'FullText'
-EXACT_MATCH_SEARCH_MODE = 'ExactMatch'
-KEYWORD_SEARCH_MODE = 'Keyword'
-DATETIME_SEARCH_MODE = 'DateTime'
-
-def isSimpleType(value):
-  return isinstance(value, basestring) or \
-         isinstance(value, int) or \
-         isinstance(value, long) or \
-         isinstance(value, float)
-
-# XXX Bad name JPS - NotQuery or NegativeQuery is better NegationQuery
-class NegatedQuery(QueryMixin):
+class SimpleQuery(Query):
   """
-    Do a boolean negation of given query.
+    A SimpleQuery represents a single comparison between a single column and
+    one or more values.
   """
+  @profiler_decorator
+  def __init__(self, search_key=None, operator='=', group=None, **kw):
+    """
+      search_key (None, SearchKey instance)
+        If given, the instance of SearchKey which is responsible for column
+        map registration and rendering (SQL and SearchText).
+      operator (string)
+        The comparison operator which will be applied between column and
+        values.
+        See Operator/ComparisonOperator.py for possible values.
+      group
+        See ColumnMap.
+      **kw
+        Must contain exactly one item.
+        item key (string)
+          column name
+        item value
+          one or more values
+    """
+    self.search_key = search_key
+    if len(kw) != 1:
+      raise ValueError, 'SimpleQuery can support one and one only column. Got %r.' % (kw, )
+    self.column, value = kw.popitem()
+    # Backward compatibility code (those changes should not be needed when
+    # this Query is instanciated by a SearchKey, as operator should be correct
+    # already).
+    operator = operator.lower()
+    if operator == 'in':
+      if isinstance(value, (list, tuple)):
+        if len(value) == 0:
+          raise ValueError, 'Empty lists are not allowed.'
+        elif len(value) == 1:
+          value = value[0]
+          operator = '='
+      else:
+        operator = '='
+    elif operator == '=':
+      if isinstance(value, (list, tuple)):
+        if len(value) == 0:
+          raise ValueError, 'Empty lists are not allowed.'
+        elif len(value) == 1:
+          value = value[0]
+        else:
+          operator = 'in'
+    self.value = value
+    self.operator = operator
+    self.group = group
 
-  def __init__(self, query):
-    self._query = query
-
-  def asSQLExpression(self, *args, **kw):
-    sql_expression_dict = self._query.asSQLExpression(*args, **kw)
-    sql_expression_dict['where_expression'] = '(NOT (%s))' % \
-      (sql_expression_dict['where_expression'], )
-    return sql_expression_dict
-
-  def getSQLKeyList(self, *args, **kw):
-    return self._query.getSQLKeyList(*args, **kw)
-
-  def getRelatedTableMapDict(self, *args, **kw):
-    return self._query.getRelatedTableMapDict(*args, **kw)
-
-allow_class(NegatedQuery)
+  @profiler_decorator
+  def asSearchTextExpression(self, sql_catalog, column=None):
+    return self.getSearchKey(sql_catalog).buildSearchTextExpression(self.getOperator(sql_catalog), self.getValue(), column=column)
 
-class SimpleQuery(QueryMixin):
-  """
-  This allow to define constraints on a sql column
+  @profiler_decorator
+  def asSQLExpression(self, sql_catalog, column_map, only_group_columns):
+    return self.getSearchKey(sql_catalog).buildSQLExpression(
+      self.getOperator(sql_catalog), self.getValue(),
+      column_map, only_group_columns, group=self.group)
 
-  format - type date : %d/%m/%Y
-           type float : 1 234.12
-  """
-  
-  def __init__(self, format=None, operator=None, range=None, key=None,
-                     search_mode=None, table_alias_list=None, type=None, **kw):
-    self.format = format
-    if operator is None:
-      operator = 'OR'
-    self.operator = operator
-    self.range = range
-    self.search_mode = search_mode
-    self.table_alias_list = table_alias_list
-    key_list = kw.keys()
-    if len(key_list) != 1:
-      raise KeyError, 'Query must have only one key'
-    self.key = key_list[0]
-    self.value = kw[self.key]
-    self.type = type
-    self.search_key = key
+  @profiler_decorator
+  def registerColumnMap(self, sql_catalog, column_map):
+    self.group = self.getSearchKey(sql_catalog).registerColumnMap(column_map, group=self.group, simple_query=self)
 
-  def getRelatedTableMapDict(self):
-    result = {}
-    table_alias_list = self.getTableAliasList()
-    if table_alias_list is not None:
-      result[self.getKey()] = table_alias_list
-    return result
-    
-  def getSQLKeyList(self):
+  def getOperator(self, sql_catalog):
     """
-    Returns the list of keys used by this
-    instance
+      Return an instance of OperatorBase class.
     """
-    return [self.getKey()]
-    
-  def asSearchTextExpression(self):
-    # This will be the standard way to represent
-    # complex values in listbox. Some fixed
-    # point must be garanteed
-    value = self.getValue()
-    if isSimpleType(value) or isinstance(value, DateTime):
-      return str(value)
-    elif isinstance(value, (list, tuple)):
-      value = map(lambda x:str(x), value)
-      return (' %s ' % self.operator).join(value)
-        
-  def _getSearchKeyClassByType(self, type, search_key_class = None):
-    """ Return search key class based on type of value. """
-    name_search_key_map = {'keyword': KeyWordKey,
-                           'default': DefaultKey,
-                           'fulltext': FullTextKey,
-                           'date': DateTimeKey,
-                           'float': FloatKey,
-                           'int': DefaultKey,}
-    return name_search_key_map.get(type, search_key_class)
-    
-  def _getSearchKeyClassByValue(self, value, search_key_class = None):
-    """ Return search key class based on type of value. """
-    if isinstance(value, basestring):
-      if value.find('%')!=-1:
-        # it's likely a KeyWordKey
-        search_key_class = KeyWordKey
-      else:
-        search_key_class = DefaultKey
-    elif isinstance(value, DateTime):
-      search_key_class = DateTimeKey
-    elif isinstance(value, (int, long,)):
-      search_key_class = DefaultKey
-    elif isinstance(value, float):
-      search_key_class = FloatKey
-    elif value is None:
-      return RawKey
-    return search_key_class
+    return sql_catalog.getComparisonOperator(self.operator)
 
-  def _asSQLExpression(self, search_key_class, key, value, format=None, mode=None, range_value=None, stat__=None):
-    """ Generate SQL expressions based on respective search_key passed. """
-    lexer = getSearchKeyInstance(search_key_class)
-    where_expression, select_expression_list = \
-             lexer.buildSQLExpression(key, value, format, mode, range_value, stat__)
-    sql_expressions = {'where_expression': where_expression,
-                       'select_expression_list': select_expression_list,}
-    return sql_expressions
-    
-  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):
+  def getSearchKey(self, sql_catalog):
     """
-    Build the sql expressions string
+      Return an instance of SearchKey class.
     """
-    search_key_class = None
-    value = self.getValue()
-    key = self.getKey()
-    operator = self.getOperator()
-    type = self.getType()
-    format = self.getFormat()
-    search_mode = self.getSearchMode()
-    range_value = self.getRange()
-    search_key = self.getSearchKey()
+    if self.search_key is None:
+      self.search_key = sql_catalog.getSearchKey(self.getColumn())
+    return self.search_key
 
-    if keyword_search_keys is None:
-      keyword_search_keys = []
-    if datetime_search_keys is None:
-      datetime_search_keys = []
-    if full_text_search_keys is None:
-      full_text_search_keys = []
+  def getColumn(self):
+    return self.column
 
-    # key can have an alias definition which we should acquire
-    if key_alias_dict is not None:
-      key = key_alias_dict.get(key, None)
+  def getValue(self):
+    return self.value
 
-    search_key_class = None
-    where_expression_list = []
-    select_expression_list = []
-    sql_expressions = {'where_expression': '1',
-                       'select_expression_list': []}
-    
-    # try to get search key type by the key definitions passed
-    if search_key_class is None:
-      if search_key == EXACT_MATCH_SEARCH_MODE:
-        search_key_class =  RawKey
-      elif search_key == KEYWORD_SEARCH_MODE or \
-          (key in keyword_search_keys):
-        search_key_class =  KeyWordKey
-      elif search_key == DATETIME_SEARCH_MODE or \
-        (key in datetime_search_keys):
-        search_key_class =  DateTimeKey
-      elif search_key == FULL_TEXT_SEARCH_MODE or \
-        (key in full_text_search_keys):
-        search_key_class =  FullTextKey
-    
-    # get search class based on explicitly passed key type
-    if search_key_class is None:
-      search_key_class = self._getSearchKeyClassByType(type)
-    
-    # some use cases where we can just return SQL without grammar staff
-    if key is None or (ignore_empty_string and \
-                       isinstance(value, basestring) and \
-                       value.strip() == ''):
-      # do not further generate sql expressions because
-      # we ignore empty strings by default
-      return sql_expressions
-    elif (not ignore_empty_string) \
-        and isinstance(value, basestring) \
-        and value.strip() == '':
-      # explicitly requested not to ignore empty strings
-      sql_expressions = {'where_expression': "%s = ''"  % key,
-                         'select_expression_list': []}
-      return sql_expressions
-    else:
-      # search for 'NULL' values 
-      if value is None:
-        sql_expressions = {'where_expression':  "%s is NULL" % (key),
-                           'select_expression_list': [],}
-        return sql_expressions
-            
-      # we have a list of values and respective operator defined
-      if isinstance(value, (tuple, list)):
-        if range_value is None:
-          # use operators to build sql expressions
-          if operator in ('IN',):
-            # values in list are not treated as searchable strings but 
-            # they should be SQL quoted at least
-            if len(value) > 1:
-              if search_key_class is None:
-                # no explicitly defined, try to find by value
-                search_key_class = self._getSearchKeyClassByValue(value[0])
-              search_key_instance = getSearchKeyInstance(search_key_class)
-              escaped_value_list = [search_key_instance.quoteSQLString(x, format) for x in value]
-              escaped_value_string = ', '.join(escaped_value_list)
-              where_expression_list.append("%s IN (%s)" % (key, escaped_value_string))
-            elif len(value) == 1:
-              if search_key_class is None:
-                # no explicitly defined, try to find by value            
-                search_key_class = self._getSearchKeyClassByValue(value[0])
-              search_key_instance = getSearchKeyInstance(search_key_class)
-              where_expression_list.append("%s = %s" 
-                                           %(key, search_key_instance.quoteSQLString(value[0], format)))
-            else:
-              # empty list
-              where_expression_list.append("0")
-          elif operator in ('OR', 'AND',):
-            # each of the list elements can be treated as a Key, so 
-            # leave SQL generation to Key itself
-            if len(value) > 1:
-              sql_logical_sub_expressions = []
-              if search_key_class is None:
-                # no explicitly defined, try to find by value
-                search_key_class = self._getSearchKeyClassByValue(value[0])
-              for item in value:
-                list_item_sql_expressions = self._asSQLExpression(search_key_class, key, \
-                                                                   item, format, search_mode, range_value, stat__)
-                sql_logical_sub_expressions.append('%s' %list_item_sql_expressions['where_expression'])
-              # join list items (now sql logical expressions) using respective operator
-              where_expression = (' %s ' %operator).join(sql_logical_sub_expressions)
-              where_expression_list.append("(%s)" % (where_expression))
-            elif len(value) == 1:
-              if search_key_class is None:
-                # no explicitly defined, try to find by value
-                search_key_class = self._getSearchKeyClassByValue(value[0])
-              item_sql_expressions = self._asSQLExpression(search_key_class, key, \
-                                                            value[0], format, search_mode, range_value, stat__)
-              where_expression_list.append(item_sql_expressions['where_expression'])
-          # join where expressions list
-          where_expression = ' '.join(where_expression_list)
-          sql_expressions = {'where_expression': where_expression,
-                             'select_expression_list': [],}
-          return sql_expressions
-        else:
-          # we can have range specified
-          if search_key_class is None:
-            # try to guess by type of first_element in list
-            search_key_class = self._getSearchKeyClassByValue(value[0])
-      
-      # get search class based on value of value
-      if search_key_class is None:
-        search_key_class = self._getSearchKeyClassByValue(value)
-      
-      # last fallback case
-      if search_key_class is None:
-        search_key_class = DefaultKey
-        
-      # use respective search key.
-      sql_expressions = self._asSQLExpression(search_key_class, key,
-                                                value, format, search_mode, range_value, stat__)
-      return sql_expressions
+  def __repr__(self):
+    return '<%s %r %s %r>' % (self.__class__.__name__, self.getColumn(), self.operator, self.getValue())
+
+  def setGroup(self, group):
+    self.group = group
 
-allow_class(SimpleQuery)
+verifyClass(IQuery, SimpleQuery)
 
-from Products.ZSQLCatalog.SearchKey.DefaultKey import DefaultKey
-from Products.ZSQLCatalog.SearchKey.RawKey import RawKey
-from Products.ZSQLCatalog.SearchKey.KeyWordKey import KeyWordKey
-from Products.ZSQLCatalog.SearchKey.DateTimeKey import DateTimeKey
-from Products.ZSQLCatalog.SearchKey.FullTextKey import FullTextKey
-from Products.ZSQLCatalog.SearchKey.FloatKey import FloatKey
-from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
diff --git a/product/ZSQLCatalog/SQLCatalog.py b/product/ZSQLCatalog/SQLCatalog.py
index 009b0391874033f79cd2d3a4e629d383057e0828..d7006f05e1a27de807debfb26261a01672a4958b 100644
--- a/product/ZSQLCatalog/SQLCatalog.py
+++ b/product/ZSQLCatalog/SQLCatalog.py
@@ -1,6 +1,6 @@
 ##############################################################################
 #
-# Copyright (c) 2002 Nexedi SARL. All Rights Reserved.
+# Copyright (c) 2002-2009 Nexedi SARL. All Rights Reserved.
 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
 #
 # This software is subject to the provisions of the Zope Public License,
@@ -45,7 +45,16 @@ from xml.dom.minidom import parse
 from xml.sax.saxutils import escape, quoteattr
 import os
 import md5
-from sets import ImmutableSet
+
+from Interface.IQueryCatalog import ISearchKeyCatalog
+from Interface.Verify import verifyClass
+
+PROFILING_ENABLED = False
+if PROFILING_ENABLED:
+  from tiny_profiler import profiler_decorator, profiler_report, profiler_reset
+else:
+  def profiler_decorator(func):
+    return func
 
 try:
   from Products.CMFCore.Expression import Expression
@@ -64,6 +73,7 @@ try:
   from Products.ERP5Type.Cache import enableReadOnlyTransactionCache
   from Products.ERP5Type.Cache import disableReadOnlyTransactionCache, CachingMethod
 except ImportError:
+  LOG('SQLCatalog', 100, 'Count not import CachingMethod, expect slowness.')
   def doNothing(context):
     pass
   class CachingMethod:
@@ -76,15 +86,53 @@ except ImportError:
       return self.function(*opts, **kw)
   enableReadOnlyTransactionCache = doNothing
   disableReadOnlyTransactionCache = doNothing
- 
+
+class caching_class_method_decorator:
+  def __init__(self, *args, **kw):
+    self.args = args
+    self.kw = kw
+
+  def __call__(self, method):
+    caching_method = CachingMethod(method, *self.args, **self.kw)
+    return lambda *args, **kw: caching_method(*args, **kw)
+    #def wrapper(wrapped_self):
+    #  LOG('caching_class_method_decorator', 0, 'lookup')
+    #  return caching_method(wrapped_self)
+    #return wrapper
+
+try:
+  from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
+except ImportError:
+  LOG('SQLCatalog', 100, 'Count not import getTransactionalVariable, expect slowness.')
+  def getTransactionalVariable(context):
+    return {}
+
+class transactional_cache_decorator:
+  """
+    Implements singleton-style caching.
+    Wrapped method must have no parameters (besides "self").
+  """
+  def __init__(self, cache_id):
+    self.cache_id = cache_id
+
+  def __call__(self, method):
+    def wrapper(wrapped_self):
+      transactional_cache = getTransactionalVariable(None)
+      try:
+        return transactional_cache[self.cache_id]
+      except KeyError:
+        result = transactional_cache[self.cache_id] = method(wrapped_self)
+        return result
+    return wrapper
+
 try:
   from ZPublisher.HTTPRequest import record
 except ImportError:
   dict_type_list = (dict, )
 else:
   dict_type_list = (dict, record)
-   
-     
+
+
 UID_BUFFER_SIZE = 300
 OBJECT_LIST_SIZE = 300
 MAX_PATH_LEN = 255
@@ -184,6 +232,10 @@ class UidBuffer(TM):
     tid = get_ident()
     self.temporary_buffer.setdefault(tid, []).extend(iterable)
 
+DEBUG = False
+
+related_key_definition_cache = {}
+
 class Catalog(Folder,
               Persistent,
               Acquisition.Implicit,
@@ -219,6 +271,9 @@ class Catalog(Folder,
     - optmization: indexing objects should be deferred
       until timeout value or end of transaction
   """
+
+  __implements__ = ISearchKeyCatalog
+
   meta_type = "SQLCatalog"
   icon = 'misc_/ZCatalog/ZCatalog.gif' # FIXME: use a different icon
   security = ClassSecurityInfo()
@@ -463,6 +518,13 @@ class Catalog(Folder,
                       'a monovalued local role',
       'type': 'lines',
       'mode': 'w' },
+    { 'id': 'sql_catalog_table_vote_scripts',
+      'title': 'Table vote scripts',
+      'description': 'Scripts helping column mapping resolution',
+      'type': 'multiple selection',
+      'select_variable' : 'getPythonMethodIds',
+      'mode': 'w' },
+
   )
 
   sql_catalog_produce_reserved = ''
@@ -500,6 +562,7 @@ class Catalog(Folder,
   sql_catalog_scriptable_keys = ()
   sql_catalog_role_keys = ()
   sql_catalog_local_role_keys = ()
+  sql_catalog_table_vote_scripts = ()
 
   # These are ZODB variables, so shared by multiple Zope instances.
   # This is set to the last logical time when clearReserved is called.
@@ -887,26 +950,24 @@ class Catalog(Folder,
       keys = keys.keys()
       keys.sort()
       return keys
-    return CachingMethod(_getColumnIds, id='SQLCatalog.getColumnIds', cache_factory='erp5_content_long')()
+    return CachingMethod(_getColumnIds, id='SQLCatalog.getColumnIds', cache_factory='erp5_content_long')()[:]
 
+  @profiler_decorator
+  @transactional_cache_decorator('SQLCatalog.getColumnMap')
+  @profiler_decorator
+  @caching_class_method_decorator(id='SQLCatalog.getColumnMap', cache_factory='erp5_content_long')
+  @profiler_decorator
   def getColumnMap(self):
     """
     Calls the show column method and returns dictionnary of
     Field Ids
     """
-    def _getColumnMap():
-      keys = {}
-      for table in self.getCatalogSearchTableIds():
-        field_list = self._getCatalogSchema(table=table)
-        for field in field_list:
-          key = field
-          if not keys.has_key(key): keys[key] = []
-          keys[key].append(table)
-          key = '%s.%s' % (table, key)
-          if not keys.has_key(key): keys[key] = []
-          keys[key].append(table) # Is this inconsistent ?
-      return keys
-    return CachingMethod(_getColumnMap, id='SQLCatalog.getColumnMap', cache_factory='erp5_content_long')()
+    result = {}
+    for table in self.getCatalogSearchTableIds():
+      for field in self._getCatalogSchema(table=table):
+        result.setdefault(field, []).append(table)
+        result.setdefault('%s.%s' % (table, field), []).append(table) # Is this inconsistent ?
+    return result
 
   def getResultColumnIds(self):
     """
@@ -1332,7 +1393,7 @@ class Catalog(Folder,
         if self.isMethodFiltered(method_name):
           catalogged_object_list = []
           filter = self.filter_dict[method_name]
-          type_set = ImmutableSet(filter['type']) or None
+          type_set = frozenset(filter['type']) or None
           expression = filter['expression_instance']
           expression_cache_key_list = filter.get('expression_cache_key', '').split()
           for object in object_list:
@@ -1633,7 +1694,8 @@ class Catalog(Folder,
     """ Accesses a single record for a given path """
     return self.getMetadataForPath(path)
 
-  def getCatalogMethodIds(self):
+  def getCatalogMethodIds(self,
+      valid_method_meta_type_list=valid_method_meta_type_list):
     """Find Z SQL methods in the current folder and above
     This function return a list of ids.
     """
@@ -1658,6 +1720,26 @@ class Catalog(Folder,
     ids.sort()
     return ids
 
+  def getPythonMethodIds(self):
+    """
+      Returns a list of all python scripts available in
+      current sql catalog.
+    """
+    return self.getCatalogMethodIds(valid_method_meta_type_list=('Script (Python)', ))
+
+  @profiler_decorator
+  @transactional_cache_decorator('SQLCatalog._getSQLCatalogRelatedKeyList')
+  @profiler_decorator
+  def _getSQLCatalogRelatedKeySet(self):
+    column_map = self.getColumnMap()
+    column_set = set(column_map)
+    for related_key in self.sql_catalog_related_keys:
+      related_key_id = related_key.split(' | ')[0].strip()
+      if related_key_id in column_set:
+        LOG('SQLCatalog', 100, 'Related key %r has the same name as an existing column on tables %r' % (related_key_id, column_map[related_key_id]))
+      column_set.add(related_key_id)
+    return column_set
+
   def getSQLCatalogRelatedKeyList(self, key_list=None):
     """
     Return the list of related keys.
@@ -1666,10 +1748,9 @@ class Catalog(Folder,
     """
     if key_list is None:
       key_list = []
+    column_map = self._getSQLCatalogRelatedKeySet()
     # Do not generate dynamic related key for acceptable_keys
-    dynamic_key_list = [k for k in key_list \
-        if k not in self.getColumnMap().keys()]
-
+    dynamic_key_list = [k for k in key_list if k not in column_map]
     dynamic_list = self.getDynamicRelatedKeyList(dynamic_key_list)
     full_list = list(dynamic_list) + list(self.sql_catalog_related_keys)
     return full_list
@@ -1702,486 +1783,412 @@ class Catalog(Folder,
           %(table_index, table))
       return table_index
     return CachingMethod(_getTableIndex, id='SQLCatalog.getTableIndex', \
-                         cache_factory='erp5_content_long')(table=table)
-
-
-  def getIndex(self, table, column_list, all_column_list):
-    """
-    Return possible index for a column list in a given table
-    """
-    def _getIndex(table, column_list, all_column_list):
-      index_dict = self.getTableIndex(table)
-      if isinstance(column_list, str):
-        column_list = [column_list,]
-      # Get possible that can be used
-      possible_index = []
-      for index in index_dict.keys():
-        index_columns = index_dict[index]
-        for column in index_columns:
-          if column in column_list:
-            if index not in possible_index:
-              possible_index.append(index)
-      if len(possible_index) == 0:
-        return []
-      # Get the most suitable index
-      for index in possible_index:
-        # Make sure all column in index are used by the query
-        index_column = index_dict[index]
-        for column in index_column:
-          if column in column_list or column in all_column_list:
-            continue
-          else:
-            possible_index.remove(index)
-      LOG("SQLCatalog.getIndex", INFO, "index = %s for table %s and columns %s" \
-          %(possible_index, table, column_list))
-      return possible_index
-    return CachingMethod(_getIndex, id='SQLCatalog.getIndex', cache_factory='erp5_content_long')\
-          (table=table, column_list=column_list, all_column_list=all_column_list)
+                         cache_factory='erp5_content_long')(table=table).copy()
 
+  @profiler_decorator
+  def getRelatedKeyDefinition(self, key):
+    """
+      Returns the definition of given related key name if found, None
+      otherwise.
+    """
+    try:
+      result = related_key_definition_cache[key]
+    except KeyError:
+      result = None
+      for entire_definition in self.getSQLCatalogRelatedKeyList([key]):
+        name, definition = entire_definition.split(' | ')
+        if name == key:
+          result = definition
+          break
+      if result is not None:
+        related_key_definition_cache[key] = result
+    return result
 
-  def buildSQLQuery(self, query_table='catalog', REQUEST=None,
-                          ignore_empty_string=1, query=None, stat__=0, **kw):
-    """ Builds a complex SQL query to simulate ZCatalog behaviour """
-    # Get search arguments:
-    if REQUEST is None and (kw is None or kw == {}):
-      # We try to get the REQUEST parameter
-      # since we have nothing handy
-      try: REQUEST=self.REQUEST
-      except AttributeError: pass
-
-    #LOG('SQLCatalog.buildSQLQuery, kw',0,kw)
-    # If kw and query are not set, then use REQUEST instead
-    if query is None and (kw is None or kw == {}):
-      kw = REQUEST
-
-    acceptable_key_map = self.getColumnMap()
-    full_text_search_keys = list(self.sql_catalog_full_text_search_keys)
-    keyword_search_keys = list(self.sql_catalog_keyword_search_keys)
-    datetime_search_keys = list(self.sql_catalog_datetime_search_keys)
-    topic_search_keys = self.sql_catalog_topic_search_keys
-    multivalue_keys = self.sql_catalog_multivalue_keys
-
-
-    # Compute "sort_index", which is a sort index, or none:
-    if kw.has_key('sort-on'):
-      sort_index=kw['sort-on']
-    elif hasattr(self, 'sort-on'):
-      sort_index=getattr(self, 'sort-on')
-    elif kw.has_key('sort_on'):
-      sort_index=kw['sort_on']
-    else: sort_index=None
-
-    # Compute the sort order
-    if kw.has_key('sort-order'):
-      so=kw['sort-order']
-    elif hasattr(self, 'sort-order'):
-      so=getattr(self, 'sort-order')
-    elif kw.has_key('sort_order'):
-      so=kw['sort_order']
-    else: so=None
-
-    # We must now turn sort_index into
-    # a dict with keys as sort keys and values as sort order
-    if isinstance(sort_index, basestring):
-      sort_index = [(sort_index, so)]
-    elif not isinstance(sort_index, (list, tuple)):
-      sort_index = None
-
-    # Rebuild keywords to behave as new style query (_usage='toto:titi' becomes {'toto':'titi'})
-    new_kw = {}
-    usage_len = len('_usage')
-    for k, v in kw.items():
-      if k.endswith('_usage'):
-        new_k = k[0:-usage_len]
-        if not new_kw.has_key(new_k):
-          new_kw[new_k] = {}
-        if not isinstance(new_kw[new_k], dict_type_list):
-          new_kw[new_k] = {'query': new_kw[new_k]}
-        split_v = v.split(':')
-        new_kw[new_k] = {split_v[0]: split_v[1]}
+  @profiler_decorator
+  def getColumnSearchKey(self, key, search_key_name=None):
+    """
+      Return a SearchKey instance for given key, using search_key_name
+      as a SearchKey name if given, otherwise guessing from catalog
+      configuration. If there is no search_key_name given and no
+      SearchKey can be found, return None.
+
+      Also return a related key definition string with following rules:
+       - If returned SearchKey is a RelatedKey, value is its definition
+       - Otherwise, value is None
+    """
+    # Is key a "real" column or some related key ?
+    related_key_definition = None
+    if key in self.getColumnMap():
+      search_key = self.getSearchKey(key, search_key_name)
+    else:
+      # Maybe a related key...
+      related_key_definition = self.getRelatedKeyDefinition(key)
+      if related_key_definition is None:
+        # Unknown
+        search_key = None
       else:
-        if not new_kw.has_key(k):
-          new_kw[k] = v
-        else:
-          new_kw[k]['query'] = v
-    kw = new_kw
-
-    # Initialise Scriptable Dict
-    scriptable_key_dict = {}
-    for t in self.sql_catalog_scriptable_keys:
-      t = t.split('|')
-      key = t[0].strip()
-      if len(t)>1:
-        # method defined that will generate a ComplexQuery
-        method_id = t[1].strip()
+        # It's a related key
+        search_key = self.getSearchKey(key, 'RelatedKey')
+    return search_key, related_key_definition
+
+  @profiler_decorator
+  def getColumnDefaultSearchKey(self, key):
+    """
+      Return a SearchKey instance which would ultimately receive the value
+      associated with given key.
+    """
+    search_key, related_key_definition = self.getColumnSearchKey(key)
+    if search_key is None:
+      result = None
+    else:
+      if related_key_definition is not None:
+        search_key = search_key.getSearchKey(self, related_key_definition)
+    return search_key
+
+  @profiler_decorator
+  def buildSingleQuery(self, key, value, search_key_name=None, logical_operator=None, comparison_operator=None):
+    """
+      From key and value, determine the SearchKey to use and generate a Query
+      from it.
+    """
+    search_key, related_key_definition = self.getColumnSearchKey(key, search_key_name)
+    if search_key is None:
+      result = None
+    else:
+      if related_key_definition is None:
+        result = search_key.buildQuery(value, logical_operator=logical_operator, comparison_operator=comparison_operator)
       else:
-        # no method define, let ScriptableKey generate a ComplexQuery
-        method_id = None
-      scriptable_key_dict[key] = method_id
-
-    # Build the list of Queries and ComplexQueries
-    query_dict = {}
-    key_list = [] # the list of column keys
-    key_alias_dict = {}
-    query_group_by_list = None # Useful to keep a default group_by passed by scriptable keys
-    query_related_table_map_dict = {}
-    if query is not None:
-      kw ['query'] = query
-    for key in kw.keys():
-      if key not in RESERVED_KEY_LIST:
-        value = kw[key]
-        current_query = None
-        new_query_dict = {}
-        if isinstance(value, (Query, ComplexQuery)):
-          current_query = value
-        elif scriptable_key_dict.has_key(key):
-          if scriptable_key_dict[key] is not None:
-            # Turn this key into a query by invoking a script          
-            method = getattr(self, scriptable_key_dict[key])
-            current_query = method(value) # May return None            
-          else:
-            # let default implementation of ScriptableKey generate ComplexQuery
-            search_key_instance = getSearchKeyInstance(ScriptableKey)
-            current_query = search_key_instance.buildQuery('', value)
-          if hasattr(current_query, 'order_by'): query_group_by_list = current_query.order_by
+        result = search_key.buildQuery(search_value=value, sql_catalog=self, search_key_name=search_key_name, related_key_definition=related_key_definition, logical_operator=logical_operator, comparison_operator=comparison_operator)
+    return result
+
+  @profiler_decorator
+  def buildQueryFromAbstractSyntaxTreeNode(self, node, key):
+    """
+      Build a query from given Abstract Syntax Tree (AST) node by recursing in
+      its childs.
+      This method calls itself recursively when walking the tree.
+
+      node
+        AST node being treated.
+      key
+        Default column (used when there is no explicit column in an AST leaf).
+
+      Expected node API is described in Interface/IAbstractSyntaxNode.py .
+    """
+    if node.isLeaf():
+      result = self.buildSingleQuery(key, node.getValue(), comparison_operator=node.getComparisonOperator())
+      if result is None:
+        # Unknown, skip loudly
+        LOG('SQLCatalog', 100, 'Unknown column %r, skipped.' % (key, ))
+    elif node.isColumn():
+      result = self.buildQueryFromAbstractSyntaxTreeNode(node.getSubNode(), node.getColumnName())
+    else:
+      query_list = []
+      value_dict = {}
+      append = query_list.append
+      for subnode in node.getNodeList():
+        if subnode.isLeaf():
+          value_dict.setdefault(subnode.getComparisonOperator(), []).append(subnode.getValue())
         else:
-          if isinstance(value, dict_type_list):
-            new_query_dict = value.copy()
-            if 'query' in new_query_dict:
-              new_query_dict[key] = new_query_dict.pop('query')
-          else:
-            new_query_dict[key] = value
-          current_query = Query(**new_query_dict)
-        if current_query is not None:
-          query_dict[key] = current_query
-          key_list.extend(current_query.getSQLKeyList())
-          query_related_table_map_dict.update(current_query.getRelatedTableMapDict())
-
-    # if we have a sort index, we must take it into account to get related
-    # keys.
-    sort_key_dict = dict()
-    if sort_index:
-      for sort_info in sort_index:
-        sort_key = sort_info[0]
-        if sort_key not in key_list:
-          key_list.append(sort_key)
-        sort_key_dict[sort_key] = 1
-
-    related_tuples = self.getSQLCatalogRelatedKeyList(key_list=key_list)
-    
-    # Define related maps
-    # each tuple from `related_tuples` has the form (key,
-    # 'table1,table2,table3/column/where_expression')
-    related_keys = {}
-    related_method = {}
-    related_table_map = {}
-    related_column = {}
-    related_table_list = {}
-    table_rename_index = 0
-    related_methods = {} # related methods which need to be used
-    for t in related_tuples:
-      t_tuple = t.split('|')
-      key = t_tuple[0].strip()
-      if key in key_list:
-        if ignore_empty_string \
-            and kw.get(key, None) in ('', [], ())\
-            and key not in sort_key_dict:
-              # We don't ignore 0 and None, but if the key is used for sorting,
-              # we should not discard this key
-          continue
-        join_tuple = t_tuple[1].strip().split('/')
-        related_keys[key] = None
-        method_id = join_tuple[2]
-        table_list = tuple(join_tuple[0].split(','))
-        related_method[key] = method_id
-        related_table_list[key] = table_list
-        related_column[key] = join_tuple[1]
-        # Check if some aliases where specified in queries
-        map_list = query_related_table_map_dict.get(key,None)
-        # Rename tables to prevent conflicts
-        if not related_table_map.has_key((table_list,method_id)):
-          if map_list is None:
-            map_list = []
-            for table_id in table_list:
-              map_list.append((table_id,
-                 "related_%s_%s" % (table_id, table_rename_index))) # We add an index in order to alias tables in the join
-              table_rename_index += 1 # and prevent name conflicts
-          related_table_map[(table_list,method_id)] = map_list
-
-    # We take additional parameters from the REQUEST
-    # and give priority to the REQUEST
-    if REQUEST is not None:
-      for key in acceptable_key_map.iterkeys():
-        if REQUEST.has_key(key):
-          # Only copy a few keys from the REQUEST
-          if key in self.sql_catalog_request_keys:
-            kw[key] = REQUEST[key]
-
-    def getNewKeyAndUpdateVariables(key):
-      new_key = None
-      if query_table:
-        key_is_acceptable = key in acceptable_key_map # Only calculate once
-        key_is_related = key in related_keys
-        if key_is_acceptable or key_is_related:
-          if key_is_related: # relation system has priority (ex. security_uid)
-            # We must rename the key
-            method_id = related_method[key]
-            table_list = related_table_list[key]
-            if not related_methods.has_key((table_list,method_id)):
-              related_methods[(table_list,method_id)] = 1
-            # Prepend renamed table name
-            new_key = "%s.%s" % (related_table_map[(table_list,method_id)][-1][-1],
-                                 related_column[key])
-          elif key_is_acceptable:
-            if key.find('.') < 0:
-              # if the key is only used by one table, just append its name
-              if len(acceptable_key_map[key]) == 1 :
-                new_key = '%s.%s' % (acceptable_key_map[key][0], key)
-              # query_table specifies what table name should be used by default
-              elif '%s.%s' % (query_table, key) in acceptable_key_map:
-                new_key = '%s.%s' % (query_table, key)
-              elif key == 'uid':
-                # uid is always ambiguous so we can only change it here
-                new_key = 'catalog.uid'
-              else:
-                LOG('SQLCatalog', WARNING, 'buildSQLQuery this key is too ambiguous : %s' % key)
-            else:
-              new_key = key
-            if new_key is not None:
-              # Add table to table dict, we use catalog by default
-              from_table_dict[acceptable_key_map[new_key][0]] = acceptable_key_map[new_key][0]
+          subquery = self.buildQueryFromAbstractSyntaxTreeNode(subnode, key)
+          if subquery is not None:
+            append(subquery)
+      for comparison_operator, value_list in value_dict.iteritems():
+        subquery = self.buildSingleQuery(key, value_list, comparison_operator=comparison_operator)
+        if subquery is None:
+          LOG('SQLCatalog', 100, 'Unknown column %r, skipped.' % (key, ))
+        else:
+          append(subquery)
+      operator = node.getLogicalOperator()
+      if operator == 'not' or len(query_list) > 1:
+        result = ComplexQuery(query_list, operator=operator)
+      elif len(query_list) == 1:
+        result = query_list[0]
       else:
-        new_key = key
-      key_alias_dict[key] = new_key
-      return new_key
-
-    where_expression_list = []
-    select_expression_list = []
-    group_by_expression_list = []
-    where_expression = ''
-    select_expression = ''
-    group_by_expression = ''
-    select_expression_key = ''
-    
-    from_table_dict = {'catalog' : 'catalog'} # Always include catalog table
-    if len(kw):
-      if kw.has_key('select_expression_key'):
-        select_expression_key = kw['select_expression_key']
-        if type(select_expression_key) is type('a'):
-          select_expression_key = [select_expression_key]
-      if kw.has_key('select_expression'):
-        select_expression_list.append(kw['select_expression'])
-      if kw.has_key('group_by_expression'):
-        group_by_expression_list.append(kw['group_by_expression'])
-      # Grouping
-      group_by_list = kw.get('group_by', query_group_by_list)
-      if type(group_by_list) is type('a'): group_by_list = [group_by_list]
-      if group_by_list is not None:
-        try:
-          for key in group_by_list:
-            new_key = getNewKeyAndUpdateVariables(key)
-            group_by_expression_list.append(new_key)
-        except ConflictError:
-          raise
-        except:
-          LOG('SQLCatalog', WARNING, 'buildSQLQuery could not build the new group by expression', error=sys.exc_info())
-          group_by_expression = ''
-      if len(group_by_expression_list)>0:
-        group_by_expression = ','.join(group_by_expression_list)
-        group_by_expression = str(group_by_expression)
-
-    sort_on = None
-    sort_key_list = []
-    if sort_index is not None:
-      new_sort_index = []
-      for sort in sort_index:
-        if len(sort) == 2:
-          # Try to analyse expressions of the form "title AS unsigned"
-          sort_key_list = sort[0].split()
-          if len(sort_key_list) == 3:
-            sort_key = sort_key_list[0]
-            sort_type = sort_key_list[2]
-          elif len(sort_key_list):
-            sort_key = sort_key_list[0]
-            sort_type = None
-          else:
-            sort_key = sort[0]
-            sort_type = None
-          new_sort_index.append((sort_key, sort[1], sort_type))
-        elif len(sort) == 3:
-          new_sort_index.append(sort)
-      sort_index = new_sort_index
-      try:
-        new_sort_index = []
-        for (original_key, so, as_type) in sort_index:
-          key = getNewKeyAndUpdateVariables(original_key)
-          if key is None:
-            if original_key in select_expression_key:
-              key = original_key
-          if key is not None:
-            sort_key_list.append(key)
-            if as_type == 'int':
-              key = 'CAST(%s AS SIGNED)' % key
-            elif as_type:
-              key = 'CAST(%s AS %s)' % (key, as_type) # Different casts are possible
-            if so in ('descending', 'reverse', 'DESC'):
-              new_sort_index.append('%s DESC' % key)
+        result = None
+    return result
+
+  @profiler_decorator
+  def buildQuery(self, kw, ignore_empty_string=True, operator='and'):
+    query_list = []
+    append = query_list.append
+    # unknown_column_dict: contains all (key, value) pairs which could not be
+    # changed into queries. This is here for backward compatibility, because
+    # scripts can invoke this method and expect extra parameters (such as
+    # from_expression) to be handled. As they are normaly handled at
+    # buildSQLQuery level, we must store them into final ComplexQuery, which
+    # will handle them.
+    unknown_column_dict = {}
+    # implicit_table_list: contains all tables explicitely given as par of
+    # column names with empty values. This is for backward compatibility. See
+    # comment about empty values.
+    implicit_table_list = []
+    for key, value in kw.iteritems():
+      result = None
+      if isinstance(value, dict_type_list):
+        # Cast dict-ish types into plain dicts.
+        value = dict(value)
+      if ignore_empty_string and (
+          value == ''
+          or (isinstance(value, (list, tuple)) and len(value) == 0)
+          or (isinstance(value, dict) and (
+            'query' not in value
+            or value['query'] == ''
+            or (isinstance(value['query'], (list, tuple))
+              and len(value['query']) == 0)))):
+        # We have an empty value:
+        # - do not create a query from it
+        # - if key has a dot, add its left part to the list of "hint" tables
+        #   This is for backward compatibility, when giving a mapped column
+        #   with an empty value caused a join with catalog to appear in
+        #   resulting where-expression)
+        if '.' in key:
+          implicit_table_list.append(key)
+        LOG('buildQuery', WARNING, 'Discarding empty value for key %r: %r' % (key, value))
+      else:
+        if isinstance(value, _Query):
+          # Query instance: use as such, ignore key.
+          result = value
+        elif isinstance(value, basestring):
+          # String: parse using key's default search key.
+          search_key = self.getColumnDefaultSearchKey(key)
+          if search_key is not None:
+            abstract_syntax_tree = search_key.parseSearchText(value)
+            if abstract_syntax_tree is None:
+              # Parsing failed, create a query from the bare string.
+              result = self.buildSingleQuery(key, value)
             else:
-              new_sort_index.append('%s' % key)
-          else:
-            LOG('SQLCatalog', WARNING, 'buildSQLQuery could not build sort '
-                'index (%s -> %s)' % (original_key, key))
-        sort_index = join(new_sort_index,',')
-        sort_on = str(sort_index)
-      except ConflictError:
-        raise
-      except:
-        LOG('SQLCatalog', WARNING, 'buildSQLQuery could not build the new sort index', error=sys.exc_info())
-        sort_on = ''
-        sort_key_list = []
-
-    for key in key_list:
-      if not key_alias_dict.has_key(key):
-        getNewKeyAndUpdateVariables(key)
-    if len(query_dict):
-      for key, query in query_dict.items():
-        query_result = query.asSQLExpression(key_alias_dict=key_alias_dict,
-                                    full_text_search_keys=full_text_search_keys,
-                                    keyword_search_keys=keyword_search_keys,
-                                    datetime_search_keys=datetime_search_keys,
-                                    ignore_empty_string=ignore_empty_string,
-                                    stat__=stat__)
-        if query_result['where_expression'] not in ('',None):
-          where_expression_list.append(query_result['where_expression'])
-        select_expression_list.extend(query_result['select_expression_list'])
-
-    # Calculate extra where_expression based on required joins
-    if query_table:
-      for k, tid in from_table_dict.items():
-        if k != query_table:
-          where_expression_list.append('%s.uid = %s.uid' % (query_table, tid))
-    # Calculate extra where_expressions based on related definition
-    for (table_list, method_id) in related_methods.keys():
-      related_method = getattr(self, method_id, None)
-      if related_method is not None:
-        table_id = {'src__' : 1} # Return query source, do not evaluate
-        table_id['query_table'] = query_table
-        table_index = 0
-        for t_tuple in related_table_map[(table_list,method_id)]:
-          table_id['table_%s' % table_index] = t_tuple[1] # table_X is set to mapped id
-          from_table_dict[t_tuple[1]] = t_tuple[0]
-          table_index += 1
-        where_expression_list.append(related_method(**table_id))
-    # Concatenate expressions
-    if kw.get('where_expression',None) not in (None,''):
-      where_expression_list.append(kw['where_expression'])
-    if len(where_expression_list)>1:
-      where_expression_list = ['(%s)' % x for x in where_expression_list]
-    where_expression = join(where_expression_list, ' AND ')
-    select_expression= join(select_expression_list,',')
-
-    limit_expression = kw.get('limit', None)
-    if isinstance(limit_expression, (list, tuple)):
-      limit_expression = '%s,%s' % (limit_expression[0], limit_expression[1])
-    elif limit_expression is not None:
-      limit_expression = str(limit_expression)
-
-    # force index if exists when doing sort as mysql doesn't manage them efficiently
-    if len(sort_key_list) > 0:
-      index_from_table = {}
-      # first group columns from a same table
-      for key in sort_key_list:
-        try:
-          related_table, column = key.split('.')
-        except ValueError:
-          # key is not of the form table.column
-          # so get table from dict
-          if len(from_table_dict) != 1:
-            continue
-          column = key
-          related_table = from_table_dict.keys()[0]
-
-        table = from_table_dict[related_table]
-        # Check if it's a column for which we want to specify index
-        index_columns = getattr(self, 'sql_catalog_index_on_order_keys', [])
-        sort_column = '%s.%s' %(table, column)
-        if not sort_column in index_columns:
-          continue
-        # Group columns
-        if not index_from_table.has_key(table):
-          index_from_table[table] = [column,]
+              if DEBUG:
+                LOG('SQLCatalog', 0, 'Building queries from abstract syntax tree: %r' % (abstract_syntax_tree, ))
+              result = self.buildQueryFromAbstractSyntaxTreeNode(abstract_syntax_tree, key)
+        elif isinstance(value, dict):
+          # Dictionnary: might contain the search key to use.
+          search_key_name = value.get('key')
+          # Backward compatibility: former "Keyword" key is now named
+          # "KeywordKey".
+          if search_key_name == 'Keyword':
+            search_key_name = value['key'] = 'KeywordKey'
+          result = self.buildSingleQuery(key, value, search_key_name)
+        else:
+          # Any other type, just create a query. (can be a DateTime, ...)
+          result = self.buildSingleQuery(key, value)
+        if result is None:
+          # No query could be created, emit a log, add to unknown column dict.
+          unknown_column_dict[key] = value
+        else:
+          append(result)
+    if len(unknown_column_dict):
+      LOG('SQLCatalog', 100, 'Unknown columns %r, skipped.' % (unknown_column_dict.keys(), ))
+    return ComplexQuery(query_list, operator=operator, unknown_column_dict=unknown_column_dict, implicit_table_list=implicit_table_list)
+
+  @profiler_decorator
+  def buildOrderByList(self, sort_on=None, sort_order=None, order_by_expression=None):
+    """
+      Internal method. Should not be used by code outside buildSQLQuery.
+
+      It is in a separate method because this code is here to keep backward
+      compatibility with an ambiguous API, and as such is ugly. So it's better
+      to conceal it to its own method.
+
+      It does not preserve backward compatibility for:
+        'sort-on' parameter
+        'sort-on' property
+        'sort-order' parameter
+        'sort-order' property
+    """
+    order_by_list = []
+    append = order_by_list.append
+    if sort_on is not None:
+      if order_by_expression is not None:
+        LOG('SQLCatalog', 0, 'order_by_expression (%r) and sort_on (%r) were given. Ignoring order_by_expression.' % (order_by_expression, sort_on))
+      if not isinstance(sort_on, (tuple, list)):
+        sort_on = [[sort_on]]
+      for item in sort_on:
+        if isinstance(item, (tuple, list)):
+          item = list(item)
         else:
-          index_from_table[table].append(column)
-      # second ask index
-      for table in index_from_table.keys():
-        available_index_list = self.getIndex(table, index_from_table[table], key_list)
-        if len(available_index_list) > 0:
-          # Always give MySQL a chance to use PRIMARY key. It is much faster if
-          # current table is used in a join on primary key than forcing it to
-          # use another index.
-          # Note: due to a bug (?) in MySQL (at least 5.0.45 community), it is
-          # a syntax error to put "PRIMARY" keyword anywere besides at first
-          # position. Hence the "insert(0".
-          if 'PRIMARY' not in available_index_list:
-            available_index_list.insert(0, 'PRIMARY')
-          # tell mysql to use these index
-          table = from_table_dict.pop(related_table)
-          index_list_string = ', '.join(available_index_list)
-          table_with_index =  "%s use index(%s)"  %(related_table, index_list_string)
-          from_table_dict[table_with_index] = table
-
-    from_expression = kw.get('from_expression', None)
-    if from_expression is not None:
-      final_from_expression = ', '.join(
-        [from_expression.get(table, '`%s` AS `%s`' % (table, alias))
-         for alias, table in from_table_dict.iteritems()])
+          item = [item]
+        if sort_order is not None and len(item) == 1:
+          item.append(sort_order)
+        if len(item) > 1:
+          if item[1] in ('descending', 'reverse', 'DESC'):
+            item[1] = 'DESC'
+          else:
+            item[1] = 'ASC'
+          if len(item) > 2:
+            if item[2] == 'int':
+              item[2] = 'SIGNED'
+        append(item)
+    elif order_by_expression is not None:
+      if not isinstance(order_by_expression, basestring):
+        raise TypeError, 'order_by_expression must be a basestring instance. Got %r.' % (order_by_expression, )
+      order_by_list = [[x.strip()] for x in order_by_expression.split(',')]
+    return order_by_list
+
+  @profiler_decorator
+  def buildSQLQuery(self, query_table='catalog', REQUEST=None,
+                          ignore_empty_string=1, only_group_columns=False,
+                          limit=None, extra_column_list=None,
+                          **kw):
+#    from traceback import format_list, extract_stack
+#    LOG('buildSQLQuery', 0, ''.join(format_list(extract_stack())))
+    if DEBUG:
+      LOG('buildSQLQuery', 0, repr(kw))
+    group_by_list = kw.pop('group_by_list', kw.pop('group_by', kw.pop('group_by_expression', None)))
+    if isinstance(group_by_list, basestring):
+      group_by_list = [x.strip() for x in group_by_list.split(',')]
+    select_dict = kw.pop('select_dict', kw.pop('select_list', kw.pop('select_expression', None)))
+    if isinstance(select_dict, basestring):
+      if len(select_dict):
+        real_select_dict = {}
+        for column in select_dict.split(','):
+          index = column.lower().find(' as ')
+          if index != -1:
+            real_select_dict[column[index + 4:].strip()] = column[:index].strip()
+          else:
+            real_select_dict[column.strip()] = None
+        select_dict = real_select_dict
+      else:
+        select_dict = None
+    elif isinstance(select_dict, (list, tuple)):
+      select_dict = dict([(x, None) for x in select_dict])
+    # Handle order_by_list
+    order_by_list = kw.pop('order_by_list', None)
+    sort_on = kw.pop('sort_on', None)
+    sort_order = kw.pop('sort_order', None)
+    order_by_expression = kw.pop('order_by_expression', None)
+    if order_by_list is None:
+      order_by_list = self.buildOrderByList(
+        sort_on=sort_on,
+        sort_order=sort_order,
+        order_by_expression=order_by_expression
+      )
     else:
-      final_from_expression = None
-    # Use a dictionary at the moment.
-    return { 'from_table_list' : from_table_dict.items(),
-             'from_expression' : final_from_expression,
-             'order_by_expression' : sort_on,
-             'where_expression' : where_expression,
-             'limit_expression' : limit_expression,
-             'select_expression': select_expression,
-             'group_by_expression' : group_by_expression}
+      if sort_on is not None:
+        LOG('SQLCatalog', 0, 'order_by_list and sort_on were given, ignoring sort_on.')
+      if sort_order is not None:
+        LOG('SQLCatalog', 0, 'order_by_list and sort_order were given, ignoring sort_order.')
+      if order_by_expression is not None:
+        LOG('SQLCatalog', 0, 'order_by_list and order_by_expression were given, ignoring order_by_expression.')
+    # Handle from_expression
+    from_expression = kw.pop('from_expression', None)
+    # Handle where_expression
+    where_expression = kw.get('where_expression', None)
+    if isinstance(where_expression, basestring) and len(where_expression):
+      LOG('SQLCatalog', 100, 'Giving where_expression a string value is deprecated.')
+      # Transform given where_expression into a query, and update kw.
+      kw['where_expression'] = SQLQuery(where_expression)
+    # Handle select_expression_key
+    # It is required to support select_expression_key parameter for backward
+    # compatiblity, but I'm not sure if there can be a serious use for it in
+    # new API.
+    order_by_override_list = kw.pop('select_expression_key', None)
+    query = EntireQuery(
+      query=self.buildQuery(kw, ignore_empty_string=ignore_empty_string),
+      order_by_list=order_by_list,
+      order_by_override_list=order_by_override_list,
+      group_by_list=group_by_list,
+      select_dict=select_dict,
+      limit=limit,
+      catalog_table_name=query_table,
+      extra_column_list=extra_column_list,
+      from_expression=from_expression)
+    result = query.asSQLExpression(self, only_group_columns).asSQLExpressionDict()
+    if DEBUG:
+      LOG('buildSQLQuery', 0, repr(result))
+    return result
 
   # Compatibililty SQL Sql
   buildSqlQuery = buildSQLQuery
 
-  def queryResults(self, sql_method, REQUEST=None, used=None, src__=0, build_sql_query_method=None, **kw):
+  @profiler_decorator
+  @transactional_cache_decorator('SQLCatalog._getSearchKeyDict')
+  @profiler_decorator
+  @caching_class_method_decorator(id='SQLCatalog._getSearchKeyDict', cache_factory='erp5_content_long')
+  @profiler_decorator
+  def _getSearchKeyDict(self):
+    result = {}
+    search_key_column_dict = {
+      'KeywordKey': self.sql_catalog_keyword_search_keys,
+      'FullTextKey': self.sql_catalog_full_text_search_keys,
+      'DateTimeKey': self.sql_catalog_datetime_search_keys,
+    }
+    for key, column_list in search_key_column_dict.iteritems():
+      for column in column_list:
+        if column in result:
+          LOG('SQLCatalog', 100, 'Ambiguous configuration: column %r is set to use %r key, but also to use %r key. Former takes precedence.' % (column, result[column], key))
+        else:
+          result[column] = key
+    return result
+
+  @profiler_decorator
+  def getSearchKey(self, column, search_key=None):
+    """
+      Return an instance of a SearchKey class.
+
+      column (string)
+        The column for which the search key will be returned.
+      search_key (string)
+        If given, must be the name of a SearchKey class to be returned.
+        Returned value will be an instance of that class, even if column has
+        been configured to use a different one.
+    """
+    if search_key is None:
+      search_key = self._getSearchKeyDict().get(column, 'DefaultKey')
+    return getSearchKeyInstance(search_key, column)
+
+  def getComparisonOperator(self, operator):
+    """
+      Return an instance of an Operator class.
+
+      operator (string)
+        String defining the expected operator class.
+        See Operator module to have a list of available operators.
+    """
+    return getComparisonOperatorInstance(operator)
+
+  @profiler_decorator
+  def _queryResults(self, REQUEST=None, build_sql_query_method=None, **kw):
     """ Returns a list of brains from a set of constraints on variables """
     if build_sql_query_method is None:
       build_sql_query_method = self.buildSQLQuery
     query = build_sql_query_method(REQUEST=REQUEST, **kw)
+    # XXX: decide if this should be made normal
+    ENFORCE_SEPARATION = True
+    if ENFORCE_SEPARATION:
+      new_kw = {}
+      # Some parameters must be propagated:
+      for parameter_id in ('selection_domain', 'selection_report'):
+        if parameter_id in kw:
+          new_kw[parameter_id] = kw[parameter_id]
+      kw = new_kw
     kw['where_expression'] = query['where_expression']
     kw['sort_on'] = query['order_by_expression']
     kw['from_table_list'] = query['from_table_list']
-    kw['from_expression'] = query.get('from_expression')
+    kw['from_expression'] = query['from_expression']
     kw['limit_expression'] = query['limit_expression']
     kw['select_expression'] = query['select_expression']
     kw['group_by_expression'] = query['group_by_expression']
-    # Return the result
+    return kw
 
-    #LOG('acceptable_keys',0,'acceptable_keys: %s' % str(acceptable_keys))
-    #LOG('acceptable_key_map',0,'acceptable_key_map: %s' % str(acceptable_key_map))
-    #LOG('queryResults',0,'kw: %s' % str(kw))
-    #LOG('queryResults',0,'from_table_list: %s' % str(query['from_table_list']))
-    return sql_method(src__=src__, **kw)
+  def queryResults(self, sql_method, REQUEST=None, src__=0, build_sql_query_method=None, **kw):
+    sql_kw = self._queryResults(REQUEST=REQUEST, build_sql_query_method=build_sql_query_method, **kw)
+    if DEBUG and not src__:
+      LOG('queryResults', 0, sql_method(src__=1, **sql_kw))
+    return sql_method(src__=src__, **sql_kw)
       
-  def searchResults(self, REQUEST=None, used=None, **kw):
+  def searchResults(self, REQUEST=None, **kw):
     """ Returns a list of brains from a set of constraints on variables """
-    # The used argument is deprecated and is ignored
     method = getattr(self, self.sql_search_results)
-    return self.queryResults(method, REQUEST=REQUEST, used=used, **kw)
+    return self.queryResults(method, REQUEST=REQUEST, extra_column_list=self.getCatalogSearchResultKeys(), **kw)
 
   __call__ = searchResults
 
-  def countResults(self, REQUEST=None, used=None, stat__=1, **kw):
+  def countResults(self, REQUEST=None, **kw):
     """ Returns the number of items which satisfy the where_expression """
     # Get the search method
     method = getattr(self, self.sql_count_results)
-    return self.queryResults(method, REQUEST=REQUEST, used=used, stat__=stat__, **kw)
+    return self.queryResults(method, REQUEST=REQUEST, extra_column_list=self.getCatalogSearchResultKeys(), only_group_columns=True, **kw)
 
   def recordObjectList(self, path_list, catalog=1):
     """
@@ -2394,40 +2401,56 @@ Globals.default__class_init__(Catalog)
 
 class CatalogError(Exception): pass
 
-# pool of global preinitialized search keys instances
-SEARCH_KEY_INSTANCE_POOL = threading.local()
-# hook search keys and Query implementation 
-def getSearchKeyInstance(search_key_class):
-  """ Return instance of respective search_key class.
-      We should have them initialized only once."""
-  global SEARCH_KEY_INSTANCE_POOL
-  if not hasattr(SEARCH_KEY_INSTANCE_POOL, 'pool'):
-    pool = dict()
-    for klass in (DefaultKey, RawKey, KeyWordKey, DateTimeKey,
-                             FullTextKey, FloatKey, ScriptableKey, KeyMappingKey):
-      search_key_instance = klass()
-      search_key_instance.build()
-      pool[klass] = search_key_instance
-    SEARCH_KEY_INSTANCE_POOL.pool = pool
-
-  return SEARCH_KEY_INSTANCE_POOL.pool[search_key_class]
-  
-from Query.Query import QueryMixin
-from Query.SimpleQuery import NegatedQuery, SimpleQuery
+from Query.Query import Query as _Query
+from Query.SimpleQuery import SimpleQuery
 from Query.ComplexQuery import ComplexQuery
-
-# for of backwards compatability  
-QueryMixin = QueryMixin
-Query = SimpleQuery
-NegatedQuery = NegatedQuery
-ComplexQuery = ComplexQuery
- 
-from Products.ZSQLCatalog.SearchKey.DefaultKey import DefaultKey
-from Products.ZSQLCatalog.SearchKey.RawKey import RawKey
-from Products.ZSQLCatalog.SearchKey.KeyWordKey import KeyWordKey
-from Products.ZSQLCatalog.SearchKey.DateTimeKey import DateTimeKey
-from Products.ZSQLCatalog.SearchKey.FullTextKey import FullTextKey
-from Products.ZSQLCatalog.SearchKey.FloatKey import FloatKey
-from Products.ZSQLCatalog.SearchKey.ScriptableKey import ScriptableKey, KeyMappingKey
-
+from Query.AutoQuery import AutoQuery as Query
+
+def NegatedQuery(query):
+  return ComplexQuery(query, operator='not')
+
+allow_class(SimpleQuery)
+allow_class(ComplexQuery)
+
+import SearchKey
+SEARCH_KEY_INSTANCE_POOL = {}
+SEARCH_KEY_CLASS_CACHE = {}
+
+@profiler_decorator
+def getSearchKeyInstance(search_key_class_name, column):
+  assert isinstance(search_key_class_name, basestring)
+  try:
+    search_key_class = SEARCH_KEY_CLASS_CACHE[search_key_class_name]
+  except KeyError:
+    search_key_class = getattr(getattr(SearchKey, search_key_class_name),
+                               search_key_class_name)
+    SEARCH_KEY_CLASS_CACHE[search_key_class_name] = search_key_class
+  try:
+    instance_dict = SEARCH_KEY_INSTANCE_POOL[search_key_class]
+  except KeyError:
+    instance_dict = SEARCH_KEY_INSTANCE_POOL[search_key_class] = {}
+  try:
+    result = instance_dict[column]
+  except KeyError:
+    result = instance_dict[column] = search_key_class(column)
+  return result
+
+from Operator import operator_dict
+def getComparisonOperatorInstance(operator):
+  return operator_dict[operator]
+
+from Query.EntireQuery import EntireQuery
+from Query.SQLQuery import SQLQuery
+
+verifyClass(ISearchKeyCatalog, Catalog)
+
+if PROFILING_ENABLED:
+  def Catalog_dumpProfilerData(self):
+    return profiler_report()
+
+  def Catalog_resetProfilerData(self):
+    profiler_reset()
+
+  Catalog.dumpProfilerData = Catalog_dumpProfilerData
+  Catalog.resetProfilerData = Catalog_resetProfilerData
 
diff --git a/product/ZSQLCatalog/SQLExpression.py b/product/ZSQLCatalog/SQLExpression.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a8bdb47710d9fc188f38242347e5067ded3e556
--- /dev/null
+++ b/product/ZSQLCatalog/SQLExpression.py
@@ -0,0 +1,349 @@
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SARL and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 zLOG import LOG
+from Interface.ISQLExpression import ISQLExpression
+from Interface.Verify import verifyClass
+from types import NoneType
+from SQLCatalog import profiler_decorator
+
+SQL_LIST_SEPARATOR = ', '
+SQL_TABLE_FORMAT = '%s' # XXX: should be changed to '`%s`', but this breaks some ZSQLMethods.
+SQL_SELECT_ALIAS_FORMAT = '%s AS `%s`'
+
+"""
+  TODO:
+    - change table_alias_dict in internals to represent computed tables:
+       ie: '(SELECT * FROM `bar` WHERE `baz` = "hoge") AS `foo`'
+           '`foo` LEFT JOIN `bar` WHERE (`baz` = "hoge")'
+"""
+
+# Set to true to keep a reference to the query which created us.
+# Set to false to avoid keeping a reference to an object.
+DEBUG = True
+
+def defaultDict(value):
+  if value is None:
+    return {}
+  assert isinstance(value, dict)
+  return value.copy()
+
+class SQLExpression(object):
+
+  __implements__ = ISQLExpression
+
+  @profiler_decorator
+  def __init__(self,
+               query,
+               table_alias_dict=None,
+               order_by_list=(),
+               order_by_dict=None,
+               group_by_list=(),
+               where_expression=None,
+               where_expression_operator=None,
+               sql_expression_list=(),
+               select_dict=None,
+               limit=None,
+               from_expression=None):
+    if DEBUG:
+      self.query = query
+    self.table_alias_dict = defaultDict(table_alias_dict)
+    self.order_by_list = list(order_by_list)
+    self.group_by_list = list(group_by_list)
+    self.order_by_dict = defaultDict(order_by_dict)
+    # Only one of (where_expression, where_expression_operator) must be given (never both)
+    assert None in (where_expression, where_expression_operator)
+    # Exactly one of (where_expression, where_expression_operator) must be given, except if sql_expression_list is given and contains exactly one entry
+    assert where_expression is not None or where_expression_operator is not None or (sql_expression_list is not None and len(sql_expression_list) == 1)
+    # where_expression must be a basestring instance if given
+    assert isinstance(where_expression, (NoneType, basestring))
+    # where_expression_operator must be 'and', 'or' or 'not' (if given)
+    assert where_expression_operator in (None, 'and', 'or', 'not'), where_expression_operator
+    self.where_expression = where_expression
+    self.where_expression_operator = where_expression_operator
+    # Exactly one of (where_expression, sql_expression_list) must be given (XXX: duplicate of previous conditions ?)
+    assert where_expression is not None or sql_expression_list is not None
+    if isinstance(sql_expression_list, (list, tuple)):
+      sql_expression_list = [x for x in sql_expression_list if x is not None]
+    self.sql_expression_list = list(sql_expression_list)
+    self.select_dict = defaultDict(select_dict)
+    if limit is None:
+      self.limit = ()
+    elif isinstance(limit, (list, tuple)):
+      if len(limit) < 3:
+        self.limit = limit
+      else:
+        raise ValueError, 'Unrecognized "limit" value: %r' % (limit, )
+    else:
+      self.limit = (limit, )
+    if from_expression is not None:
+      LOG('SQLExpression', 0, 'Providing a from_expression is deprecated.')
+    self.from_expression = from_expression
+
+  @profiler_decorator
+  def getTableAliasDict(self):
+    """
+      Returns a dictionary:
+        key: table alias (string)
+        value: table name (string)
+
+      If there are nested SQLExpressions, it aggregates their mappings and
+      checks that they don't alias different table with the same name. If they
+      do, it raises a ValueError.
+    """
+    result = self.table_alias_dict.copy()
+    for sql_expression in self.sql_expression_list:
+      for alias, table_name in sql_expression.getTableAliasDict().iteritems():
+        existing_value = result.get(alias)
+        if existing_value not in (None, table_name):
+          message = '%r is a known alias for table %r, can\'t alias it now to table %r' % (alias, existing_value, table_name)
+          if DEBUG:
+            message = message + '. I was created by %r, and I am working on %r (%r) out of [%s]' % (
+              self.query,
+              sql_expression,
+              sql_expression.getQuery(),
+              ', '.join('%r (%r)' % (x, x.getQuery()) for x in self.sql_expression_list))
+          raise ValueError, message
+        result[alias] = table_name
+    return result
+
+  @profiler_decorator
+  def getFromExpression(self):
+    """
+      Returns a string.
+
+      If there are nested SQLExpression, it checks that they either don't
+      define any from_expression or the exact same from_expression. Otherwise,
+      it raises a ValueError.
+    """
+    result = self.from_expression
+    for sql_expression in self.sql_expression_list:
+      from_expression = sql_expression.getFromExpression()
+      if None not in (result, from_expression):
+        message = 'I don\'t know how to merge from_expressions'
+        if DEBUG:
+          message = message + '. I was created by %r, and I am working on %r (%r) out of [%s]' % (
+            self.query,
+            sql_expression,
+            sql_expression.getQuery(),
+            ', '.join('%r (%r)' % (x, x.getQuery()) for x in self.sql_expression_list))
+        raise ValueError, message
+    return result
+
+  @profiler_decorator
+  def getOrderByList(self):
+    """
+      Returns a list of strings.
+
+      If there are nested SQLExpression, it checks that they don't define
+      sorts for columns which are already sorted. If they do, it raises a
+      ValueError.
+    """
+    result = self.order_by_list[:]
+    known_column_set = set([x[0] for x in result])
+    for sql_expression in self.sql_expression_list:
+      for order_by in sql_expression.getOrderByList():
+        if order_by[0] in known_column_set:
+          raise ValueError, 'I don\'t know how to merge order_by yet'
+        else:
+          result.append(order_by)
+          known_column_set.add(order_by[0])
+    return result
+
+  @profiler_decorator
+  def getOrderByDict(self):
+    result_dict = self.order_by_dict.copy()
+    for sql_expression in self.sql_expression_list:
+      order_by_dict = sql_expression.getOrderByDict()
+      for key, value in order_by_dict.iteritems():
+        if key in result_dict and value != result_dict[key]:
+          message = 'I don\'t know how to merge order_by_dict with ' \
+                    'conflicting entries for key %r: %r vs. %r' % (key, result_dict[key], value)
+          if DEBUG:
+            message = message + '. I was created by %r, and I am working on %r (%r) out of [%s]' % (
+              self.query,
+              sql_expression,
+              sql_expression.getQuery(),
+              ', '.join('%r (%r)' % (x, x.getQuery()) for x in self.sql_expression_list))
+          raise ValueError, message
+      result_dict.update(order_by_dict)
+    return result_dict
+
+  @profiler_decorator
+  def getOrderByExpression(self):
+    """
+      Returns a string.
+
+      Returns a rendered "order by" expression. See getOrderByList.
+    """
+    order_by_dict = self.getOrderByDict()
+    get = order_by_dict.get
+    return SQL_LIST_SEPARATOR.join(get(x, str(x)) \
+                                   for x in self.getOrderByList())
+
+  @profiler_decorator
+  def getWhereExpression(self):
+    """
+      Returns a string.
+
+      Returns a rendered "where" expression.
+    """
+    if self.where_expression is not None:
+      result = self.where_expression
+    else:
+      if self.where_expression_operator == 'not':
+        assert len(self.sql_expression_list) == 1
+        result = '(NOT %s)' % (self.sql_expression_list[0].getWhereExpression())
+      elif len(self.sql_expression_list) == 1:
+        result = self.sql_expression_list[0].getWhereExpression()
+      elif len(self.sql_expression_list) == 0:
+        result = '(1)'
+      else:
+        operator = '\n  ' + self.where_expression_operator.upper() + ' '
+        result = '(%s)' % (operator.join(x.getWhereExpression() for x in self.sql_expression_list), )
+    return result
+
+  @profiler_decorator
+  def getLimit(self):
+    """
+      Returns a list of 1 or 2 items (int or string).
+
+      If there are nested SQLExpression, it checks that they either don't
+      define any limit or the exact same limit. Otherwise it raises a
+      ValueError.
+    """
+    result = list(self.limit)
+    for sql_expression in self.sql_expression_list:
+      other_limit = sql_expression.getLimit()
+      if other_limit not in ([], result):
+        message = 'I don\'t know how to merge limits yet'
+        if DEBUG:
+          message = message + '. I was created by %r, and I am working on %r (%r) out of [%s]' % (
+            self.query,
+            sql_expression,
+            sql_expression.getQuery(),
+            ', '.join('%r (%r)' % (x, x.getQuery()) for x in self.sql_expression_list))
+        raise ValueError, message
+    return result
+
+  @profiler_decorator
+  def getLimitExpression(self):
+    """
+      Returns a string.
+      
+      Returns a rendered "limit" expression. See getLimit.
+    """
+    return SQL_LIST_SEPARATOR.join(str(x) for x in self.getLimit())
+
+  @profiler_decorator
+  def getGroupByset(self):
+    """
+      Returns a set of strings.
+
+      If there are nested SQLExpression, it merges (union of sets) them with
+      local value.
+    """
+    result = set(self.group_by_list)
+    for sql_expression in self.sql_expression_list:
+      result.update(sql_expression.getGroupByset())
+    return result
+
+  @profiler_decorator
+  def getGroupByExpression(self):
+    """
+      Returns a string.
+
+      Returns a rendered "group by" expression. See getGroupBySet.
+    """
+    return SQL_LIST_SEPARATOR.join(self.getGroupByset())
+
+  @profiler_decorator
+  def getSelectDict(self):
+    """
+      Returns a dict:
+        key: alias (string)
+        value: column (string) or None
+
+      If there are nested SQLExpression, it aggregates their mappings and
+      checks that they don't alias different columns with the same name. If
+      they do, it raises a ValueError.
+    """
+    result = self.select_dict.copy()
+    for sql_expression in self.sql_expression_list:
+      for alias, column in sql_expression.getSelectDict().iteritems():
+        existing_value = result.get(alias)
+        if existing_value not in (None, column):
+          message = '%r is a known alias for column %r, can\'t alias it now to column %r' % (alias, existing_value, column)
+          if DEBUG:
+            message = message + '. I was created by %r, and I am working on %r (%r) out of [%s]' % (
+              self.query,
+              sql_expression,
+              sql_expression.getQuery(),
+              ', '.join('%r (%r)' % (x, x.getQuery()) for x in self.sql_expression_list))
+          raise ValueError, message
+        result[alias] = column
+    return result
+
+  @profiler_decorator
+  def getSelectExpression(self):
+    """
+      Returns a string.
+
+      Returns a rendered "select" expression. See getSelectDict.
+    """
+    return SQL_LIST_SEPARATOR.join(
+      SQL_SELECT_ALIAS_FORMAT % (column, alias)
+      for alias, column in self.getSelectDict().iteritems())
+
+  @profiler_decorator
+  def asSQLExpressionDict(self):
+    table_alias_dict = self.getTableAliasDict()
+    from_table_list = []
+    append = from_table_list.append
+    for alias, table in table_alias_dict.iteritems():
+      append((SQL_TABLE_FORMAT % (alias, ), SQL_TABLE_FORMAT % (table, )))
+    from_expression_dict = self.getFromExpression()
+    if from_expression_dict is not None:
+      from_expression = SQL_LIST_SEPARATOR.join(
+        from_expression_dict.get(table, '`%s` AS `%s`' % (table, alias))
+        for alias, table in table_alias_dict.iteritems())
+    else:
+      from_expression = None
+    return {
+      'where_expression': self.getWhereExpression(),
+      'order_by_expression': self.getOrderByExpression(),
+      'from_table_list': from_table_list,
+      'from_expression': from_expression,
+      'limit_expression': self.getLimitExpression(),
+      'select_expression': self.getSelectExpression(),
+      'group_by_expression': self.getGroupByExpression()
+    }
+
+verifyClass(ISQLExpression, SQLExpression)
+
diff --git a/product/ZSQLCatalog/SearchKey/DateTimeKey.py b/product/ZSQLCatalog/SearchKey/DateTimeKey.py
index 893bc5ffc494b01d7685d5fc99a42a197943175a..9b69572aa7ff4cec1be7d9afecbcdb8283c58170 100644
--- a/product/ZSQLCatalog/SearchKey/DateTimeKey.py
+++ b/product/ZSQLCatalog/SearchKey/DateTimeKey.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,171 +28,241 @@
 #
 ##############################################################################
 
-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
+import sys
 from SearchKey import SearchKey
-from pprint import pprint
+from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery
+from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
+#from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from zLOG import LOG
+from DateTime.DateTime import DateTime, DateTimeError, _cache
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
 
+MARKER = []
 
-class DateTimeKey(SearchKey):
-  """ 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'].
+timezone_dict = _cache._zmap
+
+date_completion_format_dict = {
+  None: ['01/01/%s', '01/%s'],
+  'international': ['%s/01/01', '%s/01']
+}
+
+@profiler_decorator
+def _DateTime(*args, **kw):
+  return DateTime(*args, **kw)
+
+@profiler_decorator
+def castDate(value):
+  date_kw = {'datefmt': 'international'}
+  if isinstance(value, dict):
+    # Convert value into a DateTime, and guess desired delta from what user
+    # input.
+    assert value['type'] == 'date'
+    format = value.get('format')
+    value = value['query']
+    if format == '%m/%d/%Y':
+      date_kw.pop('datefmt')
+  if isinstance(value, DateTime):
+    pass
+  elif isinstance(value, basestring):
+    try:
+      value = _DateTime(value, **date_kw)
+    except DateTimeError:
+      delimiter_count = countDelimiters(value)
+      if delimiter_count < 3:
+        split_value = value.split()
+        if split_value[-1].lower() in timezone_dict:
+          value = '%s %s' % (date_completion_format_dict[date_kw.get('datefmt')][delimiter_count] % (' '.join(split_value[:-1]), ), split_value[-1])
+        else:
+          value = date_completion_format_dict[date_kw.get('datefmt')][delimiter_count] % (value, )
+        value = _DateTime(value, **date_kw)
+      else:
+        raise
+  else:
+    raise TypeError, 'Unknown date type: %r' % (value)
+  return value.toZone('UTC')
+
+# (strongly) inspired from DateTime.DateTime.py
+delimiter_list = ' -/.:,+'
+
+def getMonthLen(datetime):
+  return datetime._month_len[datetime.isLeapYear()][datetime.month()]
+
+def getYearLen(datetime):
+  return 365 + datetime.isLeapYear()
+
+delta_list = [getYearLen, getMonthLen, 1, 1.0 / 24, 1.0 / (24 * 60), 1.0 / (24 * 60 * 60)]
+
+@profiler_decorator
+def countDelimiters(value):
+  assert isinstance(value, basestring)
+  # Detect if timezone was provided, to avoid counting it as in precision computation.
+  split_value = value.split()
+  if split_value[-1].lower() in timezone_dict:
+    value = ' '.join(split_value[:-1])
+  # Count delimiters
+  delimiter_count = 0
+  for char in value:
+    if char in delimiter_list:
+      delimiter_count += 1
+  return delimiter_count
+
+@profiler_decorator
+def getPeriodBoundaries(value):
+  first_date = castDate(value)
+  if isinstance(value, dict):
+    value = value['query']
+  # Try to guess how much was given in query.
+  if isinstance(value, basestring):
+    delimiter_count = countDelimiters(value)
+  elif isinstance(value, DateTime):
+    raise TypeError, 'Impossible to guess a precision from a DateTime type.'
+  else:
+    raise TypeError, 'Unknown date type: %r' % (value)
+  delta = delta_list[delimiter_count]
+  if callable(delta):
+    delta = delta(first_date)
+  return first_date, first_date + delta
+
+@profiler_decorator
+def wholePeriod(search_key, group, column, value_list, exclude=False):
+  if exclude:
+    first_operator = '<'
+    second_operator = '>='
+    logical_operator = 'or'
+  else:
+    first_operator = '>='
+    second_operator = '<'
+    logical_operator = 'and'
+  query_list = []
+  append = query_list.append
+  for value in value_list:
+    first_date, second_date = getPeriodBoundaries(value)
+    append(ComplexQuery([SimpleQuery(search_key=search_key, operator=first_operator, group=group, **{column: first_date}),
+                         SimpleQuery(search_key=search_key, operator=second_operator, group=group, **{column: second_date})],
+                        operator=logical_operator))
+  return query_list
+
+def matchWholePeriod(search_key, group, column, value_list, *ignored):
+  return wholePeriod(search_key, group, column, value_list)
 
-      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!
+def matchNotWholePeriod(search_key, group, column, value_list, *ignored):
+  return wholePeriod(search_key, group, column, value_list, exclude=True)
 
-      Examples (GMT+02, Bulgaria/Sofia for 'delivery.start_date'):
+@profiler_decorator
+def matchExact(search_key, group, column, value_list, comparison_operator, logical_operator):
+  if comparison_operator is None:
+    comparison_operator = '='
+  value_list = [castDate(x) for x in value_list]
+  if logical_operator == 'or' and comparison_operator == '=':
+    query_list = [SimpleQuery(search_key=search_key, operator='in', group=group, **{column: value_list})]
+  else:
+    query_list = [SimpleQuery(search_key=search_key, operator=comparison_operator, group=group, **{column: x}) for x in value_list]
+  return query_list
 
-        * '15/01/2008' --> "delivery.start_date = '2008-01-14 22:00'"
+def getNextPeriod(value):
+  return getPeriodBoundaries(value)[1]
 
-        * '>=15/01/2008' --> "delivery.start_date >= '2008-01-14 22:00'"     
+@profiler_decorator
+def matchBeforeNextPeriod(search_key, group, column, value_list, comparison_operator, logical_operator):
+  return matchExact(search_key, group, column, [getNextPeriod(x) for x in value_list], '<', logical_operator)
 
-        * '>=15/01/2008 or <=20/01/2008'
-          --> "delivery.start_date >= '2008-01-14 22:00' or delivery.start_date<='2008-01-19 22:00'"
+@profiler_decorator
+def matchAfterPeriod(search_key, group, column, value_list, comparison_operator, logical_operator):
+  return matchExact(search_key, group, column, [getNextPeriod(x) for x in value_list], '>=', logical_operator)
 
-        * '>=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'
-          "
+operator_matcher_dict = {
+  None: matchWholePeriod,
+  '=': matchWholePeriod,
+  '!=': matchNotWholePeriod,
+  '<': matchExact,
+  '>=': matchExact,
+  '<=': matchBeforeNextPeriod,
+  '>': matchAfterPeriod,
+}
+
+# Behaviour of date time operators
+# Objects:
+#   2005/03/14 23:59:59
+#   2005/03/15 00:00:00
+#   2005/03/15 00:00:01
+#   2005/03/15 23:59:59
+#   2005/03/16 00:00:00
+#   2005/03/16 00:00:01
+#
+# Searches:
+#   "2005/03/15" (operator = None)
+#     Implicitely matches the whole period.
+#     2005/03/15 00:00:00
+#     2005/03/15 00:00:01
+#     2005/03/15 23:59:59
+#
+#   "=2005/03/15" (operator = '=')
+#     Behaves the same way as None operator.
+#     2005/03/15 00:00:00
+#     2005/03/15 00:00:01
+#     2005/03/15 23:59:59
+#
+#   "!=2005/03/15" (operator = '!=')
+#     Complementary of '=' operator.
+#     2005/03/14 23:59:59
+#     2005/03/16 00:00:00
+#     2005/03/16 00:00:01
+#
+#   "<2005/03/15" (operator = '<')
+#     Non-ambiguous (no difference wether time is considered as a period or a single point in time).
+#     2005/03/14 23:59:59
+#
+#   ">=2005/03/15" (operator = '>=')
+#     Complementary of '<' operator, and also non-ambiguous.
+#     2005/03/15 00:00:00
+#     2005/03/15 00:00:01
+#     2005/03/15 23:59:59
+#     2005/03/16 00:00:00
+#     2005/03/16 00:00:01
+#
+#   "<=2005/03/15" (operator = '<=')
+#     Union of results from '=' and '<' operators.
+#     2005/03/14 23:59:59
+#     2005/03/15 00:00:00
+#     2005/03/15 00:00:01
+#     2005/03/15 23:59:59
+#
+#   ">2005/03/15" (operator = '>')
+#     Complementary of '<=' operator.
+#     2005/03/16 00:00:00
+#     2005/03/16 00:00:01
+
+class DateTimeKey(SearchKey):
   """
+    This SearchKey allows generating date ranges from single, user-input dates.
+  """
+
+  default_comparison_operator = None
+  get_operator_from_value = True
 
-  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|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|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|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 """
+  def _renderValueAsSearchText(self, value, operator):
+    return '"%s"' % (DateTime(value).ISO(), )
+
+  @profiler_decorator
+  def _buildQuery(self, operator_value_dict, logical_operator, parsed, group):
+    column = self.getColumn()
     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
+    extend = query_list.extend
+    for comparison_operator, value_list in operator_value_dict.iteritems():
       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'}
+        if parsed:
+          subquery_list = operator_matcher_dict[comparison_operator](
+                   self, group, column, value_list, comparison_operator,
+                   logical_operator)
         else:
-          query_kw = {key: date_value + days_offset,
-                      'range': sub_operator}  
-        query_kw['type'] = 'date'
+          subquery_list = matchExact(self, group, column, value_list, comparison_operator, logical_operator)
+      except DateTimeError:
+        LOG('DateTimeKey', 100, 'Got an exception while generating a query for %r %r.' % (comparison_operator, value_list), error=sys.exc_info())
       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, []
+        extend(subquery_list)
+    return query_list
+
+verifyClass(ISearchKey, DateTimeKey)
+
diff --git a/product/ZSQLCatalog/SearchKey/DefaultKey.py b/product/ZSQLCatalog/SearchKey/DefaultKey.py
index ab097b1c6cc5eb2cb1d2e75d0f93aaee9940f810..f1bc2d19c4a29299c0efcf745c6d7c7c128e78a2 100644
--- a/product/ZSQLCatalog/SearchKey/DefaultKey.py
+++ b/product/ZSQLCatalog/SearchKey/DefaultKey.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -26,124 +28,36 @@
 #
 ##############################################################################
 
-from DocumentTemplate.DT_Var import sql_quote
 from SearchKey import SearchKey
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
 
 class DefaultKey(SearchKey):
-  """ 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|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|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|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'([^"\s<>!][\S\n]*|!([^=\s][\S\n]*)?)'
-    # newlines are allowed, because variations are delimited by newlines.
-    # 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()
-    if value[0] == '=':
-      value = value[1:]
-    t.value = value
-    return t
-
-  def t_WORDSET(self, t):
-    r'"[^"]*"'
-    #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 = 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'" % sql_quote(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   
+    This SearchKey behaves like an ExactMatch SearchKey, except if value is a
+    string and contains a '%' sign, in which case it behaves like a
+    KeywordKey.
+  """
+  default_comparison_operator = '='
+  get_operator_from_value = True
+
+  def _guessComparisonOperator(self, value):
+    if isinstance(value, basestring) and '%' in value:
+      operator = 'like'
+    else:
+      operator = SearchKey._guessComparisonOperator(self, value)
+    return operator
+
+  def buildSearchTextExpression(self, operator, value, column=None):
+    operator_text = operator.getOperator()
+    if column is None:
+      column = self.getColumn()
+    if operator_text == 'like':
+      assert isinstance(value, basestring)
+      assert '%' in value
+      result = '%s:%s' % (column, value)
+    else:
+      result = SearchKey.buildSearchTextExpression(self, operator, value, column=column)
+    return result
+
+verifyClass(ISearchKey, DefaultKey)
diff --git a/product/ZSQLCatalog/SearchKey/FloatKey.py b/product/ZSQLCatalog/SearchKey/FloatKey.py
deleted file mode 100644
index 5cb458322938b6a53025654875e4d928fc35c42a..0000000000000000000000000000000000000000
--- a/product/ZSQLCatalog/SearchKey/FloatKey.py
+++ /dev/null
@@ -1,92 +0,0 @@
-##############################################################################
-#
-# 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 SearchKey import SearchKey
-
-class FloatKey(SearchKey):
-  """ 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|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|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|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 = 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    
diff --git a/product/ZSQLCatalog/SearchKey/FullTextKey.py b/product/ZSQLCatalog/SearchKey/FullTextKey.py
index 0a56558426efaae40b6cef10d7f45ddf26f2c4a7..2feeb2a382de99a330ff811b08b77188a37393ac 100644
--- a/product/ZSQLCatalog/SearchKey/FullTextKey.py
+++ b/product/ZSQLCatalog/SearchKey/FullTextKey.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -27,77 +29,19 @@
 ##############################################################################
 
 from SearchKey import SearchKey
-import re
-
-SEARCH_MODE_MAPPING = {'in_boolean_mode': 'IN BOOLEAN MODE',
-                       'with_query_expansion': 'WITH QUERY EXPANSION'}
+from Products.ZSQLCatalog.SearchText import parse
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
 
 class FullTextKey(SearchKey):
-  """ 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
   """
+    This SearchKey generates SQL fulltext comparisons.
+  """
+  default_comparison_operator = 'match'
+  get_operator_from_value = False
 
-  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 parseSearchText(self, value):
+    return parse(value)
 
-  def t_WORD(self, t):
-    r'[^\+\-<>\(\)\~\*\"\s]$|[^\+\-<>\(\)\~\*\"\s]+[^\*\"\s\)]'
-    #r'[^\+\-<>\(\)\~\*\"\s]\S*'
-    #r'[\x7F-\xFF\w\d][\x7F-\xFF\w\d]*'
-    # WORD may contain arbitrary letters and numbers without white space
-    return t
+verifyClass(ISearchKey, FullTextKey)
 
-  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
-      if mode == '' and len(tokens) > 1:
-        value = ' '.join(['+%s' %  x.value for x in tokens])
-        mode = SEARCH_MODE_MAPPING['in_boolean_mode']
-    # 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,
-                            self.quoteSQLString(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,
-                    self.quoteSQLString(value, ''), mode, relevance_key1),]
-      if relevance_key2 is not None:
-        select_expression_list.append(self.select_match_against_as % (
-          key, self.quoteSQLString(value, ''), mode, relevance_key2))
-    return where_expression, select_expression_list
diff --git a/product/ZSQLCatalog/SearchKey/KeyWordKey.py b/product/ZSQLCatalog/SearchKey/KeyWordKey.py
deleted file mode 100644
index 9dbea72ed027bd5d970343f7ed489757d98eb02e..0000000000000000000000000000000000000000
--- a/product/ZSQLCatalog/SearchKey/KeyWordKey.py
+++ /dev/null
@@ -1,234 +0,0 @@
-##############################################################################
-#
-# 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 SearchKey import SearchKey
-from pprint import pprint
-
-class KeyWordKey(SearchKey):
-  """ KeyWordKey key is an ERP5 portal_catalog search key which is used to render
-      SQL expression that will try to match all possible values in a greedy manner.
-      It supports following special operator ['=', '%', '>' , '>=', '<', '<='] in
-      addition to main logical operators like ['OR', 'or', 'AND', 'and'].
-
-      Examples for title column: 
-        * 'foo or bar'  --> "title LIKE '%foo%' OR title LIKE '%bar%'"
-        * 'foo or =bar'  --> "title LIKE '%foo%' OR title = 'bar'"
-        * 'Organisation Module' -->  "title LIKE '%Organisation Module%'"
-        * '"Organisation Module"' --> "title LIKE '%Organisation Module%'"
-        * '="Organisation Module"' --> "title  = 'Organisation Module'"
-
-  """
-
-  tokens =  ('OR', 'AND', 'NOT', 
-             'KEYWORD', 'WORDSET', 'WORD', 'EXPLICITEQUALLITYWORD',
-             'GREATERTHAN', 'GREATERTHANEQUAL', 
-             'LESSTHAN', 'LESSTHANEQUAL')
-
-  sub_operators = ('GREATERTHAN', 'GREATERTHANEQUAL', 
-                    'LESSTHAN', 'LESSTHANEQUAL', 'NOT')
-
-  # this is the default operator
-  default_operator = 'like'
-  
-  # if token's list starts with left sided operator
-  # use this map to transfer it to range operator
-  token_operator_range_map = {'like': 'like',
-                              '!=': 'not_like',
-                              '=': '=',}
-                    
-  # Note: Order of placing rules (t_WORD for example) is very important
-  def t_OR(self, t):
-    r'\s+(OR|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|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|not)\s+|!=)'
-    # operator must have leading and trailing ONLY one white space character
-    # otherwise it's treated as a WORD
-    t.value = t.value.upper().strip()
-    return t
-
-  t_GREATERTHANEQUAL = r'>='
-  t_LESSTHANEQUAL = r'<='
-  t_GREATERTHAN = r'>'
-  t_LESSTHAN = r'<'
-
-  def t_EXPLICITEQUALLITYWORD(self, t):
-    r'=\S*'
-    # EXPLICITEQUALLITYWORD may contain arbitrary letters and numbers without white space
-    # EXPLICITEQUALLITYWORD must contain '=' at the beginning
-    value = t.value.strip()
-    # get rid of leading '='
-    t.value = value[1:]
-    return t
-
-  def t_KEYWORD(self, t):
-    r'(%\S*|[^!<>=\s%]*%)(?!\S)'
-    # KEYWORD must start and/or end with '%'.
-    # It may contain arbitrary letters and numbers without white space
-    value = t.value.strip()
-    t.value = value
-    return t    
-    
-  def t_WORD(self, t):
-    r'([^"\s<>!=%]([^ \t\r\f\v]*[^ \t\r\f\v%])?|!([^= \t\r\f\v%]|[^= \t\r\f\v][\S\n]*[^ \t\r\f\v%])?)'
-    # 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 t_WORDSET(self, t):
-    r'=?"[^"]*"'
-    # WORDSET is a combination of WORDs separated by white space
-    # and starting/ending with " (optionally with '=')
-    value = t.value.replace('"', '')
-    if value[0] == '=':
-      value = value[1:]
-    t.value = value
-    return t
-  
-  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:]
-    elif token.type == 'EXPLICITEQUALLITYWORD':
-      # even though it's keyword key we can still explicitly define 
-      # that we want equality
-      return '=', tokens
-    else:
-      return self.default_operator, tokens  
-  
-  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)
-      first_token = sub_tokens[0]
-      range = self.token_operator_range_map.get(sub_operator)
-      
-      sub_tokens_values = [x.value for x in sub_tokens]
-      right_side_expression = ' '.join(sub_tokens_values)
-      if first_token.type == 'WORDSET' and first_token.value.startswith('='):
-        range = '='
-        right_side_expression = first_token.value[1:]
-      elif first_token.type in ('WORDSET', 'WORD',) and range == 'like':
-        if '%' not in right_side_expression:
-          # If the search string doesn't already contain '%', add trailing and
-          # leading '%' to get more results
-          right_side_expression = '%%%s%%' % right_side_expression
-      query_kw = {key: right_side_expression,
-                  'range': range}
-      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 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 ('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 LIKE '%%%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
-##          where_expressions.append('%s' %operator.value)
-##        if len(tokens):
-##          # no it's not a stand alone expression, 
-##          # determine it from list of tokens
-##          sub_where_expression = ''
-##          tokens_number = len(tokens)
-##          if tokens_number == 1:
-##            # no left sided operator (<, >, >=, <=) found
-##            token = tokens[0]
-##            if token.type == 'WORD':
-##              sub_where_expression = "LIKE '%%%s%%'" %token.value
-##            elif token.type == 'KEYWORD':
-##              sub_where_expression = "LIKE '%s'" %token.value
-##            elif token.type == 'EXPLICITEQUALLITYWORD':
-##             sub_where_expression = "= '%s'" %token.value
-##            elif token.type == 'WORDSET' and token.value.startswith('='):
-##              # if WORDSET starts with '=' it's an equality
-##              sub_where_expression = " = '%s'" %token.value[1:]
-##            else:
-##              sub_where_expression = "LIKE '%%%s%%'" %token.value
-##          else:
-##            # we have two or more tokens, by definition first one should be 
-##            # logical operator like (<, >, >=, <=)
-##            operator = tokens[0]
-##            operator_value = operator.value
-##            if operator.type in ('KEYWORD', 'WORDSET', 'WORD'):
-##              # no operator for this token list, assume it's 'LIKE'
-##              sub_where_expression = "LIKE '%s'" %' '.join([x.value for x in tokens])
-##            else:
-##              # we have operator and by convention if operator is used it's applyied to one token only
-##              sub_where_expression = "%s'%s'" %(operator_value, tokens[1].value)
-##          where_expressions.append('%s %s' %(key, sub_where_expression))
-##    return where_expressions, select_expressions
diff --git a/product/ZSQLCatalog/SearchKey/KeywordKey.py b/product/ZSQLCatalog/SearchKey/KeywordKey.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d9aa8e14c2409fded027bed5a6da52041c400be
--- /dev/null
+++ b/product/ZSQLCatalog/SearchKey/KeywordKey.py
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 SearchKey import SearchKey
+from Products.ZSQLCatalog.SearchText import parse
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
+
+class KeywordKey(SearchKey):
+  """
+    This SearchKey generates matching comparison Queries suited for strings
+    with wilcards.
+  """
+  default_comparison_operator = 'like'
+  get_operator_from_value = True
+ 
+  def parseSearchText(self, value):
+    return parse(value)
+
+verifyClass(ISearchKey, KeywordKey)
+
diff --git a/product/ZSQLCatalog/SearchKey/RawKey.py b/product/ZSQLCatalog/SearchKey/RawKey.py
index 718a352e0eab9b6a7a07cc77b6612f53d6ee39b7..7f34e032ac862e6a821cf6d79f60be27ddc0a487 100644
--- a/product/ZSQLCatalog/SearchKey/RawKey.py
+++ b/product/ZSQLCatalog/SearchKey/RawKey.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -27,23 +29,15 @@
 ##############################################################################
 
 from SearchKey import SearchKey
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
 
 class RawKey(SearchKey):
-  """ 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 ."""
+  """
+    This SearchKey does not do any parsing of given value.
+  """
+  default_comparison_operator = '='
+  get_operator_from_value = False
 
-  def build(self, **kwargs):
-    # this key doesn't require parsing
-    # It's required to implement it as it's used ONLY for ExactMath
-    pass
+verifyClass(ISearchKey, RawKey)
 
-  def buildSQLExpression(self, key, value, 
-                         format=None, mode=None, range_value=None, stat__=None):
-
-    if value is not None:
-      value = self.quoteSQLString(value, format)
-      key = self.quoteSQLKey(key, format)
-      where_expression = "%s = %s" % (key, value)
-    else:
-      where_expression = "%s is NULL" % (key)
-    return where_expression, []
diff --git a/product/ZSQLCatalog/SearchKey/RelatedKey.py b/product/ZSQLCatalog/SearchKey/RelatedKey.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c092d2783343d93e6332e9182b383cf32036620
--- /dev/null
+++ b/product/ZSQLCatalog/SearchKey/RelatedKey.py
@@ -0,0 +1,206 @@
+##############################################################################
+#
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@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 SearchKey import SearchKey
+from Products.ZSQLCatalog.Query.Query import Query
+from Products.ZSQLCatalog.Query.RelatedQuery import RelatedQuery
+from Products.ZSQLCatalog.Query.SQLQuery import SQLQuery
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from zLOG import LOG
+from Products.ZSQLCatalog.Interface.ISearchKey import IRelatedKey
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+MARKER = []
+BACKWARD_COMPATIBILITY = True
+
+class RelatedKey(SearchKey):
+  """
+    This SearchKey handles searched on virtual columns of RelatedKey type.
+    It generates joins required by the virtual column to reach the actual
+    column to compare, plus a regular query on that column if needed.
+  """
+
+  __implements__ = IRelatedKey
+
+  related_key_definition = None
+
+  @profiler_decorator
+  def _buildRelatedKey(self, related_key_definition):
+    """
+      Extract RelatedKey parameters from its definition, and cache this
+      result. If related_key_definition changes since last computation, cached
+      values will be refreshed.
+
+      related_key_definition (string)
+        Describes parameters of a RelatedKey. It is composed of 3 mains parts,
+        separated by '/':
+        - a list of table names
+          Table names are separated by ','
+        - a column name
+        - the name of the related key ZSQLMethod
+    """
+    assert related_key_definition is not None
+    if self.related_key_definition != related_key_definition:
+      self.related_key_definition = related_key_definition
+      # Extract related_key_id, column_id and table_list from related_key_definition
+      table_list, self.real_column, self.related_key_id = related_key_definition.split('/')
+      self.table_list = table_list.split(',')
+
+  @profiler_decorator
+  def _getSearchKey(self, sql_catalog, search_key_name):
+    """
+      Get search key relevant to the actual column.
+
+      sql_catalog (SQLCatalog)
+        Used to access SearchKey provider.
+      search_key_name (string, None)
+        See SQLCatalog.getSearchKey.
+    """
+    return sql_catalog.getSearchKey(self.real_column, search_key_name)
+
+  @profiler_decorator
+  def getSearchKey(self, sql_catalog, related_key_definition, search_key_name=None):
+    """
+      Get search key relevant to the actual column, extracting information
+      about that column first if needed.
+
+      sql_catalog (SQLCatalog)
+        Used to access SearchKey provider.
+      related_key_definition (string)
+        See _buildRelatedKey.
+      search_key_name (string, None)
+        See SQLCatalog.getSearchKey.
+    """
+    self._buildRelatedKey(related_key_definition)
+    return self._getSearchKey(sql_catalog, search_key_name)
+
+  @profiler_decorator
+  def buildQuery(self, sql_catalog, related_key_definition,
+                 search_value=MARKER, search_key_name=None,
+                 logical_operator=None, comparison_operator=None):
+    self._buildRelatedKey(related_key_definition)
+    if search_value is MARKER:
+      join_condition = None
+    else:
+      join_condition = self._getSearchKey(sql_catalog, search_key_name).buildQuery(
+        search_value, group=self.getColumn(),
+        logical_operator=logical_operator,
+        comparison_operator=comparison_operator)
+    return RelatedQuery(search_key=self,
+                        join_condition=join_condition)
+
+  @profiler_decorator
+  def registerColumnMap(self, column_map, table_alias_list=None):
+    related_column = self.getColumn()
+    group = column_map.registerRelatedKey(related_column, self.real_column)
+    # Each table except last one must be registered to their own group, so that
+    # the same table can be used multiple time (and aliased multiple times)
+    # in the same related key. The last one must be register to related key
+    # "main" group (ie, the value of the "group" variable) to be the same as
+    # the ta ble used in join_condition.
+    if table_alias_list is not None:
+      assert len(self.table_list) == len(table_alias_list)
+    for table_position in xrange(len(self.table_list) - 1):
+      table_name = self.table_list[table_position]
+      local_group = column_map.registerRelatedKeyColumn(related_column, table_position, group)
+      column_map.registerTable(table_name, group=local_group)
+      if table_alias_list is not None:
+        # Pre-resolve all tables with given aliases
+        given_name, given_alias = table_alias_list[table_position]
+        assert table_name == given_name
+        column_map.resolveTable(table_name, given_alias, group=local_group)
+    table_name = self.table_list[-1]
+    column_map.registerTable(table_name, group=group)
+    if table_alias_list is not None:
+      given_name, given_alias = table_alias_list[-1]
+      assert table_name == given_name
+      column_map.resolveTable(table_name, given_alias, group=group)
+    # Resolve (and register) related key column in related key group with its last table.
+    column_map.registerColumn(self.real_column, group=group)
+    column_map.resolveColumn(self.real_column, table_name, group=group)
+    # Always register catalog, since it is always the "base" table of
+    # RelatedKeys.
+    column_map.registerCatalog()
+    return group
+
+  @profiler_decorator
+  def buildSQLExpression(self, sql_catalog, column_map, only_group_columns, group):
+    """
+      Render RelatedKey's ZSQLMethod by providing it table aliases from
+      ColumnMap.
+
+      sql_catalog (SQLCatalog)
+      column_map (ColumnMap)
+      group (string)
+      only_group_columns (bool)
+        Ignored.
+    """
+    related_key = getattr(sql_catalog, self.related_key_id)
+    table_alias_dict = dict(
+      [('table_%s' % (x, ), column_map.getTableAlias(self.table_list[x], group=column_map.getRelatedKeyGroup(x, group)))
+       for x in xrange(len(self.table_list) - 1)])
+    x = len(table_alias_dict)
+    assert x == len(self.table_list) - 1
+    table_alias_dict['table_%s' % (x, )] = column_map.getTableAlias(self.table_list[x], group=group)
+    rendered_related_key = related_key(
+      query_table=column_map.getCatalogTableAlias(),
+      src__=1,
+      **table_alias_dict)
+    # Important:
+    # Former catalog separated join condition from related query.
+    # Example:
+    #   ComplexQuery(Query(title="foo"),
+    #                Query(subordination_title="bar")
+    #                , operator='OR')
+    # Former catalog rendering (truncated where-expression):
+    #   AND ((catalog.title LIKE '%bil%') OR
+    #        (related_catalog_1.title = 'My Organisation'))
+    #   AND (related_catalog_1.uid = related_category_0.category_uid AND
+    #        related_category_0.base_category_uid = 873 AND
+    #        related_category_0.uid = catalog.uid)
+    # As you can see, the part of the query joining the tables is *out* of the
+    # OR expression, and therefor applies to the entire query.
+    # This was done on purpose, because doing otherwise gives very poor
+    # performances (on a simple data set, similar query can take *minutes* to
+    # execute - as of MySQL 5.x).
+    # Doing the same way as the former catalog is required for backward
+    # compatibility, until a decent alternative is found (like spliting the
+    # "OR" expression into ensemblist operations at query level).
+    # Note that doing this has a side effect on result list, as objects
+    # lacking a relation will never appear in the result.
+    if BACKWARD_COMPATIBILITY:
+      column_map.addJoinQuery(SQLQuery(rendered_related_key))
+      return None
+    else:
+      return SQLExpression(self, where_expression=rendered_related_key)
+
+verifyClass(IRelatedKey, RelatedKey)
+
diff --git a/product/ZSQLCatalog/SearchKey/ScriptableKey.py b/product/ZSQLCatalog/SearchKey/ScriptableKey.py
deleted file mode 100644
index 60868f8689617af1e6e5780078e94847c425ed46..0000000000000000000000000000000000000000
--- a/product/ZSQLCatalog/SearchKey/ScriptableKey.py
+++ /dev/null
@@ -1,193 +0,0 @@
-##############################################################################
-#
-# 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 SearchKey import SearchKey
-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(SearchKey):
-  """ 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(SearchKey):
-  """ 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|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|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'[^<>=:\s]+\s*(>|<|<=|>=|:)\s*\S+'
-    # 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'[^<>=\s:]+'
-    # 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)
diff --git a/product/ZSQLCatalog/SearchKey/SearchKey.py b/product/ZSQLCatalog/SearchKey/SearchKey.py
index 1babb855460e041ee14f13324548705627ec33f3..64bd8a78cfbcaea99bd27801f3cb338716ee1877 100644
--- a/product/ZSQLCatalog/SearchKey/SearchKey.py
+++ b/product/ZSQLCatalog/SearchKey/SearchKey.py
@@ -1,7 +1,9 @@
 ##############################################################################
 #
-# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
-#                     Ivan Tyagov <ivan@nexedi.com>
+# Copyright (c) 2002-2006 Nexedi SARL and Contributors. All Rights Reserved.
+# Copyright (c) 2007-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
+#                    Vincent Pelletier <vincent@nexedi.com>
 #
 # WARNING: This program as such is intended to be used by professional
 # programmers who take the whole responsability of assessing all potential
@@ -13,7 +15,7 @@
 # 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.
+# 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
@@ -26,220 +28,322 @@
 #
 ##############################################################################
 
-from DocumentTemplate.DT_Var import sql_quote
-from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery as Query
+from zLOG import LOG
+from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery
 from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
-from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
-
-import ply.yacc as yacc
-import ply.lex as lex
-
-class SearchKey:
-  """ 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 grammar 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) 
+from Products.ZSQLCatalog.SQLExpression import SQLExpression
+from Products.ZSQLCatalog.Interface.ISearchKey import ISearchKey
+from Interface.Verify import verifyClass
+from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+
+single_operator_dict = {
+  'min': '>=',
+  'max': '<',
+  'ngt': '<=',
+  'nlt': '>'
+}
+
+dual_operator_dict = {
+  'minmax': ('>=', '<'),
+  'minngt': ('>=', '<=')
+}
+
+# List of operators searched for at value's begnining when it's a basestring.
+# Order is important: an operator whose left part would be matching another operator of lower index would never be used.
+operator_list = ('>=', '<=', '>', '<', '=', '!=')
+
+def preprocessLikeValue(value):
+  if '%' not in value:
+    value = '%%%s%%' % (value, )
+  return value
+
+operator_value_preprocessor_dict = {
+  'like': preprocessLikeValue
+}
+
+def deprocessLikeValue(value):
+  assert isinstance(value, basestring)
+  if len(value) >= 2 and value[0] == '%' and value[-1] == '%':
+    value = value.strip('%')
+  return value
+
+operator_value_deprocessor_dict = {
+  'like': deprocessLikeValue
+}
+
+class SearchKey(object):
+
+  __implements__ = ISearchKey
+
+  # Comparison operator to use when parsing a string value and no operator is
+  # found.
+  # Note: for non-string values, "=" is always used by default.
+  default_comparison_operator = '='
+  # Wether or not to allow a basestring value to be searched for a comparison
+  # operator.
+  get_operator_from_value = True
+
+  def __init__(self, column):
+    self.column = column
+
+  def getColumn(self):
+    return self.column
+
+  @profiler_decorator
+  def buildSQLExpression(self, operator, value, column_map, only_group_columns, group):
+    column_name = self.getColumn()
+    rendered_column = column_map.asSQLColumn(column_name, group=group)
+    return operator.asSQLExpression(rendered_column, value, only_group_columns)
+
+  @profiler_decorator
+  def _renderValueAsSearchText(self, value, operator):
+    """
+      Render a single value as valid SearchText using provided operator.
+      This is also responsible for undoing any formatting the value received
+      from the SearchKey.
+
+      value (anything)
+      operator (Operator)
+        The operator used to render value.
+    """
+    operator_value_deprocessor = operator_value_deprocessor_dict.get(operator.getOperator())
+    if operator_value_deprocessor is not None:
+      value = operator_value_deprocessor(value)
+    return operator.asSearchText(value)
+
+  @profiler_decorator
+  def buildSearchTextExpression(self, operator, value, column=None):
+    operator_text = operator.getOperator()
+    if column is None:
+      column = self.getColumn()
+    if isinstance(value, (list, tuple)):
+      assert operator_text == 'in'
+      assert len(value)
+      value = [self._renderValueAsSearchText(x, operator) for x in value]
+      if self.default_comparison_operator != '=':
+        value = ['=%s' % (x, ) for x in value]
+      # XXX: operator used to join value elements should be reused from parser data (?)
+      result = '(%s)' % (' OR '.join(value), )
+    else:
+      result = self._renderValueAsSearchText(value, operator)
+      if operator_text != self.default_comparison_operator:
+        result = '%s%s' % (operator_text, result)
+    if len(column):
+      result = '%s:%s' % (column, result)
     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:]
+  @profiler_decorator
+  def registerColumnMap(self, column_map, group, simple_query):
+    column_map.registerColumn(self.getColumn(), group=group, simple_query=simple_query)
+    return group
+
+  @profiler_decorator
+  def _getComparisonOperator(self, value):
+    """
+      From a basestring instance, return a contained operator and value without
+      that operator.
+
+      value (string)
+      
+      Returns: 2-tuple of strings
+        First element is the operator. None if there was no operator in value.
+        Second element is the value without the operator.
+    """
+    startswith = value.startswith
+    for operator in operator_list:
+      if startswith(operator):
+        value = value[len(operator):].lstrip()
+        break
     else:
-      return self.default_operator, tokens    
+      operator = self._guessComparisonOperator(value)
+    return operator, value
 
-  def groupByLogicalOperator(self, tokens, logical_operator ='OR'):
-    """ Split tokens list into one or many OR concatanated tokens list 
+  @profiler_decorator
+  def _guessComparisonOperator(self, value):
     """
-    sub_tokens_or_groups = []
-    tmp_token_list = []
-    for token in tokens:
-      if token.type != logical_operator:
-        tmp_token_list.append(token)
+      From a basestring instance, return a contained operator.
+      Value cannot be altered in the process.
+
+      value (string)
+      
+      Returns: 2-tuple of strings
+        First element is the operator. None if there was no operator in value.
+        Second element is the value without the operator.
+    """
+    return self.default_comparison_operator
+
+  @profiler_decorator
+  def _preprocessValue(self, value, operator):
+    operator_value_preprocessor = operator_value_preprocessor_dict.get(operator)
+    if operator_value_preprocessor is not None:
+      value = operator_value_preprocessor(value)
+    return value
+
+  @profiler_decorator
+  def _processSearchValue(self, search_value, default_logical_operator, comparison_operator):
+    """
+      Change search_value into a list of values, one or more logical operators,
+      and a comparison operator. If no default_logical_operator is given, 
+      'or' is used.
+
+      search_value
+        basestring
+        int
+        dict
+        list or tuple
+          Non-empty
+          Composed of homogeneous items
+      Returns: 3-tuple
+        dict:
+          key (string)
+            Comparison operator
+          value (list of anything)
+            List of values applying to this operator.
+        string:
+          Logical operator applied to all elements of returned dict.
+        bool:
+          True if logical operators were searched for in values, False
+          otherwise. Useful to give different meanings to in-value operators
+          and others.
+    """
+    if comparison_operator == '':
+      comparison_operator = None
+      get_operator_from_value = False
+    else:
+      get_operator_from_value = self.get_operator_from_value
+    logical_operator = None
+    if default_logical_operator is None:
+      default_logical_operator = 'or'
+    parsed = False
+    if isinstance(search_value, dict):
+      # comparison_operator parameter collides with dict's 'operator' key.
+      # Fail loudly.
+      assert comparison_operator is None
+      actual_value = search_value['query']
+      if search_value.get('key') not in (None, self.__class__.__name__):
+        LOG(self.__class__.__name__, 100, '"key" dict entry does not match current class: %r' % (search_value, ))
+      if 'type' in search_value:
+        assert 'operator' not in search_value, search_value
+        assert 'range' not in search_value, search_value
       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'" % sql_quote(str(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__) 
+        value_operator = search_value.get('operator')
+        value_range = search_value.get('range')
+        if value_range is not None:
+          if value_operator is not None:
+            LOG('SearchKey', 100, '"range" and "operator" are mutualy exclusive, ignoring operator: %r' % (search_value, ))
+          if value_range in single_operator_dict:
+            comparison_operator = single_operator_dict[value_range]
+          elif value_range in dual_operator_dict:
+            if not isinstance(actual_value, (tuple, list)):
+              raise TypeError, 'Operator %r requires value to be a tuple/list. (%r)' % (value_range, search_value)
+            if len(actual_value) != 2:
+              raise TypeError, 'Operator %r requires value to have a length of 2. len(%r) = %i (%r)' % (value_range, actual_value, len(actual_value), search_value)
+            comparison_operator = dual_operator_dict[value_range]
+            logical_operator = 'and'
+          else:
+            raise ValueError, 'Unknown "range" value in %r' % (search_value, )
+        if value_operator is not None:
+          if not isinstance(value_operator, basestring):
+            raise TypeError, 'Operator must be of a string type. Got a %r' % (type(value_operator), )
+          value_operator = value_operator.lower()
+          if not isinstance(actual_value, (tuple, list)):
+            raise TypeError, 'When specifying an operator, query must be a list.'
+          if value_operator == 'in':
+            comparison_operator = '='
+            logical_operator = 'or'
+          else:
+            logical_operator = value_operator
+        search_value = actual_value
+    # Cast to list
+    if isinstance(search_value, (tuple, list)):
+      # Check list content (not empty, homogenous)
+      search_value_len = len(search_value)
+      if search_value_len == 0:
+        raise ValueError, 'Value cannot be an empty list/tuple: %r' % (search_value, )
+      reference_class = search_value[0].__class__
+      for x in search_value[1:]:
+        if x.__class__ != reference_class:
+          raise TypeError, 'List elements must be of the same class: %r' % (search_value, )
     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': []}
+      assert logical_operator is None
+      if isinstance(search_value, dict):
+        reference_class = search_value['query'].__class__
+      else:
+        reference_class = search_value.__class__
+      search_value = [search_value]
+    if logical_operator is None:
+      logical_operator = default_logical_operator
+    operator_value_dict = {}
+    if None in search_value:
+      if comparison_operator not in (None, 'is'):
+        LOG('KeywordKey', 100, 'None value requires an "is" comparison operator. Fixed.')
+      operator_value_dict['is'] = search_value
+    elif comparison_operator is None:
+      if issubclass(reference_class, basestring):
+        if get_operator_from_value:
+          parsed = True
+          for value in search_value:
+            if isinstance(value, dict):
+              operator, value['query'] = self._getComparisonOperator(value['query'])
+            else:
+              operator, value = self._getComparisonOperator(value)
+            operator_value_dict.setdefault(operator, []).append(self._preprocessValue(value, operator))
+        else:
+          for value in search_value:
+            if isinstance(value, dict):
+              operator = self._guessComparisonOperator(value['query'])
+            else:
+              operator = self._guessComparisonOperator(value)
+            operator_value_dict.setdefault(operator, []).append(self._preprocessValue(value, operator))
+      else:
+        # XXX: comparison operator is hardcoded for non-strings.
+        operator_value_dict['='] = search_value
+    elif isinstance(comparison_operator, (tuple, list)):
+      assert len(comparison_operator) == len(search_value)
+      for operator, value in zip(comparison_operator, search_value):
+        operator_value_dict.setdefault(operator, []).append(value)
     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']
+      operator_value_dict[comparison_operator] = search_value
+    return operator_value_dict, logical_operator, parsed
 
-  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)
+  @profiler_decorator
+  def _buildQuery(self, operator_value_dict, logical_operator, parsed, group):
+    """
+      Create Queries from values, logical and comparison operators.
 
-    # split tokens list into one or more 'OR' tokens lists
-    tokens_or_groups = self.groupByLogicalOperator(tokens, 'OR')
+      operator_value_dict (dict)
+        See _processSearchValue.
+      logical_operator (string)
+        See _processSearchValue.
+      parsed (bool)
+        See _processSearchValue.
+      group (string)
+        The gorup all queries will belong to.
+    """
+    column = self.getColumn()
+    query_list = []
+    append = query_list.append
+    if logical_operator == 'or' and '=' in operator_value_dict:
+      # Special case for equality with an 'or' logical operator: use SQL 'in'.
+      append(SimpleQuery(search_key=self, operator='in', group=group, **{column: operator_value_dict.pop('=')}))
+    for comparison_operator, value_list in operator_value_dict.iteritems():
+      for value in value_list:
+        append(SimpleQuery(search_key=self, operator=comparison_operator, group=group, **{column: value}))
+    return query_list
 
-    # remove empty tokens lists
-    tokens_or_groups = filter(lambda x: len(x), tokens_or_groups)
+  @profiler_decorator
+  def buildQuery(self, search_value, group=None, logical_operator=None, comparison_operator=None):
+    assert logical_operator in (None, 'and', 'or'), repr(logical_operator)
+    operator_value_dict, logical_operator, parsed = self._processSearchValue(search_value, logical_operator, comparison_operator)
+    query_list = self._buildQuery(operator_value_dict, logical_operator, parsed, group)
+    if len(query_list) == 1:
+      query = query_list[0]
+    else:
+      query = ComplexQuery(query_list, operator=logical_operator)
+    return query
 
-    # 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)
+  def parseSearchText(self, value):
+    return None
 
-    if len(query_list):
-      # join query list in one really big ComplexQuery
-      return ComplexQuery(*query_list,
-                          **{'operator':'OR'})
+verifyClass(ISearchKey, SearchKey)
 
-  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      
diff --git a/product/ZSQLCatalog/SearchKey/__init__.py b/product/ZSQLCatalog/SearchKey/__init__.py
index 8b137891791fe96927ad78e64b0aad7bded08bdc..40a84b841184bd87b4e76ab74796569bd3ee00c3 100644
--- a/product/ZSQLCatalog/SearchKey/__init__.py
+++ b/product/ZSQLCatalog/SearchKey/__init__.py
@@ -1 +1,13 @@
+# Automaticaly import all SearchKeys
+import os
+module_path = os.path.dirname(os.path.abspath(__file__))
+global_dict = globals()
+__relative_file__ = os.path.basename(__file__)
+for filename in os.listdir(module_path):
+  if filename.endswith('.py') and filename != __relative_file__:
+    modulename = filename[:-3]
+    try:
+      module = __import__(modulename, global_dict, None, [])
+    except ImportError:
+      continue
 
diff --git a/product/ZSQLCatalog/SearchText/AdvancedSearchTextDetector.py b/product/ZSQLCatalog/SearchText/AdvancedSearchTextDetector.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bcfe712c62d3e36e8f6c9855a04356fb92430ba
--- /dev/null
+++ b/product/ZSQLCatalog/SearchText/AdvancedSearchTextDetector.py
@@ -0,0 +1,110 @@
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Vincent Pelletier <vincent@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 lexer import lexer, update_docstrings
+
+# TODO: remove the special OPERATOR case: it does not work when there are both a valid and an invalid operator
+
+class AdvancedSearchTextDetector(lexer):
+
+  def t_OPERATOR(self, t):
+    r'(>=?|<=?|!?=)[ ]*'
+    return t
+
+  def t_LEFT_PARENTHESE(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def t_STRING(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def t_COLUMN(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def t_OR(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def t_AND(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def t_NOT(self, t):
+    self.found = True
+    t.type = 'WORD'
+    return t
+
+  def p_search_text(self, p):
+    '''search_text : value
+                   | value search_text'''
+    if len(p) == 2:
+      p[0] = p[1]
+    else:
+      p[0] = p[1] or p[2]
+
+  def p_value(self, p):
+    '''value : WORD
+             | OPERATOR WORD'''
+    p[0] = len(p) == 3 and ' ' not in p[1]
+
+  tokens = (
+    'WORD',
+    'OPERATOR')
+
+  def real_token(self):
+    return lexer.token(self)
+
+  def token(self):
+    return self.token_list.pop(0)
+
+  def __call__(self, input):
+    self.found = False
+    check_grammar = False
+    self.token_list = token_list = []
+    append = token_list.append
+    self.input(input)
+    while not self.found:
+      token = self.real_token()
+      append(token)
+      if token is None:
+        break
+      if token.type == 'OPERATOR':
+        check_grammar = True
+    if not self.found and check_grammar:
+      self.found = self.parse()
+    return self.found
+
+update_docstrings(AdvancedSearchTextDetector)
+
diff --git a/product/ZSQLCatalog/SearchText/AdvancedSearchTextParser.py b/product/ZSQLCatalog/SearchText/AdvancedSearchTextParser.py
new file mode 100644
index 0000000000000000000000000000000000000000..0192514d5822fc08bc391d27612d6c95ae16d76d
--- /dev/null
+++ b/product/ZSQLCatalog/SearchText/AdvancedSearchTextParser.py
@@ -0,0 +1,229 @@
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Vincent Pelletier <vincent@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 lexer import lexer, update_docstrings
+try:
+  from Products.ZSQLCatalog.Interface.IAbstractSyntaxNode import INode, IValueNode, ILogicalNode, IColumnNode
+  from Interface.Verify import verifyClass
+except ImportError:
+  INode = None
+  IValueNode = None
+  ILogicalNode = None
+  IColumnNode = None
+  def verifyClass(*args, **kw):
+    pass
+
+class Node(object):
+
+  __implements__ = INode
+
+  def isLeaf(self):
+    return False
+
+  def isColumn(self):
+    return False
+
+  def push(self, logical_operator, node):
+    return LogicalNode(logical_operator, self, node)
+
+verifyClass(INode, Node)
+
+class ValueNode(Node):
+
+  __implements__ = IValueNode
+
+  def __init__(self, value, comparison_operator=''):
+    self.value = value
+    self.comparison_operator = comparison_operator
+
+  def isLeaf(self):
+    return True
+
+  def getComparisonOperator(self):
+    return self.comparison_operator
+
+  def getValue(self):
+    return self.value
+
+  def __repr__(self):
+    return '<%s %r %r>' % (self.__class__.__name__, self.comparison_operator, self.value)
+
+verifyClass(INode, ValueNode)
+verifyClass(IValueNode, ValueNode)
+
+class NotNode(Node):
+
+  __implements__ = ILogicalNode
+
+  def __init__(self, node):
+    self.node = node
+
+  def getLogicalOperator(self):
+    return 'not'
+
+  def getNodeList(self):
+    return [self.node]
+
+  def __repr__(self):
+    return '<%s %r>' % (self.__class__.__name__, self.node)
+
+verifyClass(INode, NotNode)
+verifyClass(ILogicalNode, NotNode)
+
+class LogicalNode(Node):
+
+  __implements__ = ILogicalNode
+
+  def __init__(self, logical_operator, node, other):
+    self.logical_operator = logical_operator
+    self.node_list = []
+    self._push(node)
+    self._push(other)
+
+  def getLogicalOperator(self):
+    return self.logical_operator
+
+  def getNodeList(self):
+    return self.node_list
+
+  def _push(self, node):
+    if isinstance(node, LogicalNode) and node.logical_operator == self.logical_operator:
+      self.node_list.extend(node.node_list)
+    else:
+      self.node_list.append(node)
+
+  def __repr__(self):
+    return '<%s %r %r>' % (self.__class__.__name__, self.logical_operator, self.node_list)
+
+verifyClass(INode, LogicalNode)
+verifyClass(ILogicalNode, LogicalNode)
+
+class ColumnNode(Node):
+
+  __implements__ = IColumnNode
+
+  def __init__(self, column_name, node):
+    self.column_name = column_name
+    self.node = node
+
+  def isColumn(self):
+    return True
+
+  def getColumnName(self):
+    return self.column_name
+
+  def getSubNode(self):
+    return self.node
+
+  def __repr__(self):
+    return '<%s %r %r>' % (self.__class__.__name__, self.column_name, self.node)
+
+verifyClass(INode, ColumnNode)
+verifyClass(IColumnNode, ColumnNode)
+
+class AdvancedSearchTextParser(lexer):
+
+  def p_seach_text(self, p):
+    '''search_text : and_expression
+                   | and_expression OR search_text
+                   | and_expression search_text'''
+    if len(p) == 2:
+      p[0] = p[1]
+    elif len(p) == 3:
+      p[0] = p[1].push('or', p[2])
+    else:
+      p[0] = p[1].push('or', p[3])
+
+  def p_and_expression(self, p):
+    '''and_expression : boolean_expression
+                      | boolean_expression AND and_expression'''
+    if len(p) == 2:
+      p[0] = p[1]
+    else:
+      p[0] = p[1].push('and', p[3])
+
+  def p_boolean_expression(self, p):
+    '''boolean_expression : NOT expression
+                          | expression'''
+    if len(p) == 3:
+      p[0] = NotNode(p[2])
+    else:
+      p[0] = p[1]
+
+  def p_expression(self, p):
+    '''expression : LEFT_PARENTHESE search_text RIGHT_PARENTHESE
+                  | column
+                  | value'''
+    if len(p) == 2:
+      p[0] = p[1]
+    else:
+      p[0] = p[2]
+
+  def p_column(self, p):
+    '''column : COLUMN value_expression'''
+    p[0] = ColumnNode(p[1], p[2])
+
+  def p_value_expression(self, p):
+    '''value_expression : LEFT_PARENTHESE value_or_expression RIGHT_PARENTHESE
+                        | value'''
+    if len(p) == 2:
+      p[0] = p[1]
+    else:
+      p[0] = p[2]
+
+  def p_value_or_expression(self, p):
+    '''value_or_expression : value_and_expression
+                           | value_and_expression value_or_expression
+                           | value_and_expression OR value_or_expression'''
+    if len(p) == 2:
+      p[0] = p[1]
+    elif len(p) == 3:
+      p[0] = p[1].push('or', p[2])
+    else:
+      p[0] = p[1].push('or', p[3])
+
+  def p_value_and_expression(self, p):
+    '''value_and_expression : value_expression
+                            | value_expression AND value_and_expression'''
+    if len(p) == 2:
+      p[0] = p[1]
+    else:
+      p[0] = p[1].push('and', p[3])
+
+  def p_value(self, p):
+    '''value : OPERATOR STRING
+             | OPERATOR WORD
+             | STRING
+             | WORD'''
+    if len(p) == 2:
+      p[0] = ValueNode(p[1])
+    else:
+      p[0] = ValueNode(p[2], comparison_operator=p[1])
+
+update_docstrings(AdvancedSearchTextParser)
+
diff --git a/product/ZSQLCatalog/SearchText/SearchTextParser.py b/product/ZSQLCatalog/SearchText/SearchTextParser.py
new file mode 100644
index 0000000000000000000000000000000000000000..a86942abb2d87eb3a7831aadb845ff14c2b7953f
--- /dev/null
+++ b/product/ZSQLCatalog/SearchText/SearchTextParser.py
@@ -0,0 +1,333 @@
+#!/usr/bin/python
+# -*- coding: utf8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Vincent Pelletier <vincent@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.
+#
+##############################################################################
+
+import threading
+from AdvancedSearchTextDetector import AdvancedSearchTextDetector
+from AdvancedSearchTextParser import AdvancedSearchTextParser
+from lexer import ParserOrLexerError
+try:
+  from Products.ZSQLCatalog.SQLCatalog import profiler_decorator
+except ImportError:
+  def profiler_decorator(funct):
+    return funct
+
+if __name__ == '__main__':
+  DEBUG = 1
+else:
+  DEBUG = 0
+
+parser_pool = threading.local()
+
+@profiler_decorator
+def getAdvancedSearchTextDetector():
+  try:
+    return parser_pool.advanced_search_text_detector
+  except AttributeError:
+    advanced_search_text_detector = AdvancedSearchTextDetector()
+    advanced_search_text_detector.init(debug=DEBUG)
+    parser_pool.advanced_search_text_detector = advanced_search_text_detector
+    return advanced_search_text_detector
+
+@profiler_decorator
+def getAdvancedSearchTextParser():
+  try:
+    return parser_pool.parser
+  except AttributeError:
+    parser = AdvancedSearchTextParser()
+    parser.init(debug=DEBUG)
+    parser_pool.parser = parser
+    return parser
+
+@profiler_decorator
+def _parse(input, *args, **kw):
+  if getAdvancedSearchTextDetector()(input):
+    result = getAdvancedSearchTextParser()(input, *args, **kw)
+  else:
+    result = None
+  return result
+
+@profiler_decorator
+def parse(*args, **kw):
+  try:
+    result = _parse(*args, **kw)
+  except (KeyboardInterrupt, SystemExit):
+    raise
+  except:
+    result = None
+  return result
+
+if __name__ == '__main__':
+  class Query:
+    def __init__(self, column, value, comparison_operator='='):
+      self.column = column
+      self.comparison_operator = comparison_operator
+      if isinstance(value, (list, tuple)):
+        value = ''.join(value)
+      self.value = value
+
+    def asTuple(self):
+      return (self.column, self.value, self.comparison_operator)
+
+    def __repr__(self):
+      value = self.value
+      if len(value) == 1:
+        value = value[0]
+      return 'Query(%r, %r, %r)' % (self.column, value, self.comparison_operator)
+
+    def __eq__(self, other):
+      if isinstance(other, Query):
+        return self.asTuple() == other.asTuple()
+      else:
+        return False
+
+    def __ne__(self, other):
+      return not (self == other)
+
+  class ComplexQuery:
+    def __init__(self, query_list, operator):
+      self.operator = operator
+      self.query_list = query_list
+
+    def __repr__(self):
+      return 'ComplexQuery(%r, operator=%r)' % (self.query_list, self.operator)
+
+    def __eq__(self, other):
+      if isinstance(other, ComplexQuery):
+        if self.operator != other.operator:
+          return False
+        other_query_list = other.query_list[:]
+        for my_query in self.query_list:
+          for other_index in xrange(len(other_query_list)):
+            other_query = other_query_list[other_index]
+            if my_query == other_query:
+              other_query_list.pop(other_index)
+              break
+          else:
+            return False
+        return len(other_query_list) == 0
+      else:
+        return False
+
+    def __ne__(self, other):
+      return not (self == other)
+
+  check_list = [
+    ('foo', None),
+    ('foo bar', None),
+    ('foo   bar', None),
+    ('foo%', None),
+    ('%foo', None),
+    ('%foo%', None),
+    ('foo%bar', None),
+    ('foo% bar', None),
+    ('foo %bar', None),
+    ('foo and bar', None),
+    ('foo or bar', None),
+    ('foo - bar', None),
+    ('foo- bar', None),
+    ('!-1', None),
+    ('->1', None),
+    ('+=1', None),
+    ('jean-paul', None),
+    ('JeanAndPaul', None),
+    ('totoORtata', None),
+    ('NORD', None),
+    ('OR ARGENT', None),
+    ('CUIVRE OR ARGENT', None), # XXX
+    ('title :foo', None),
+    ('-foo', None),
+    ('foo -bar', None),
+    ('+foo -bar', None),
+    ('+1', None),
+    ('-1', None),
+
+    ('foo OR "-" OR bar OR -baz', ComplexQuery([Query(None, 'foo'), Query(None, '-'), Query(None, 'bar'), Query(None, '-baz')], operator='or')),
+    ('foo "-" bar -baz',          ComplexQuery([Query(None, 'foo'), Query(None, '-'), Query(None, 'bar'), Query(None, '-baz')], operator='or')),
+    ('title:foo',                 Query('title', 'foo')),
+    ('title: foo',                Query('title', 'foo')),
+    ('title:foo bar',             ComplexQuery([Query('title', 'foo'), Query(None, 'bar')], operator='or')),
+    ('title:"foo bar"',           Query('title', 'foo bar')),
+    ('"title:foo bar"',           Query(None, 'title:foo bar')),
+    ('"foo bar"',                 Query(None, 'foo bar')),
+    ('"foo   bar"',               Query(None, 'foo   bar')),
+    ('foo AND bar',               ComplexQuery([Query(None, 'foo'), Query(None, 'bar')], operator='and')),
+    ('foo OR bar',                ComplexQuery([Query(None, 'foo'), Query(None, 'bar')], operator='or')),
+    ('"foo AND bar"',             Query(None, 'foo AND bar')),
+    ('"foo and bar"',             Query(None, 'foo and bar')),
+    ('"foo OR bar"',              Query(None, 'foo OR bar')),
+    ('"foo or bar"',              Query(None, 'foo or bar')),
+    ('"foo% bar"',                Query(None, 'foo% bar')),
+    ('"foo %bar"',                Query(None, 'foo %bar')),
+    ('>1',                        Query(None, '1', '>')),
+    ('">1"',                      Query(None, '>1')),
+    ('>a',                        Query(None, 'a', '>')),
+    ('">a"',                      Query(None, '>a')),
+    ('>1 0',                      ComplexQuery([Query(None, '1', '>'), Query(None, '0')], operator='or')),
+    ('>=1',                       Query(None, '1', '>=')),
+    ('>"=1"',                     Query(None, '=1', '>')),
+    ('-"1"',                      ComplexQuery([Query(None, '-'), Query(None, '1')], operator='or')),
+    ('"!-1"',                     Query(None, '!-1')),
+#    (r"a:'tu:\'tu\''",            ['a', "tu:'tu'"]),
+    (r'''b:"tu:\'tu\'"''',        Query('b', "tu:\\'tu\\'")),
+    (r'''c:"tu:'tu'"''',          Query('c', "tu:'tu'")),
+    (r'd:"tu:\"tu\""',            Query('d', 'tu:"tu"')),
+    ('toto: tutu tutu',           ComplexQuery([Query('toto', 'tutu'), Query(None, 'tutu')], operator='or')),
+    ('(tutu) (toto:tata)',        ComplexQuery([Query(None, 'tutu'), Query('toto', 'tata')], operator='or')),
+    ('(tutu) (toto:"tata")',      ComplexQuery([Query(None, 'tutu'), Query('toto', 'tata')], operator='or')),
+#    ('toto:',                     ['toto', '']),
+    ('toto:""',                   Query('toto', '')),
+#    ("''",                        ''),
+    ('""',                        Query(None, '')),
+    (r'"\""',                     Query(None, '"')),
+    (r'"\n"',                     Query(None, '\\n')),
+#こんにちは
+    (u'ã‚“',                       None),
+    (u'(toto:ã‚“) OR (titi:ã‚“)',   ComplexQuery([Query('toto', u'ã‚“'), Query('titi', u'ã‚“')], operator='or')),
+    ('ã‚“',                        None),
+    ('(toto:ã‚“) OR (titi:ã‚“)',    ComplexQuery([Query('toto', 'ã‚“'), Query('titi', 'ã‚“')], operator='or')),
+    ('(foo)',                     Query(None, 'foo')),
+    ('toto:(foo)',                Query('toto', 'foo')),
+    ('(foo OR bar)',              ComplexQuery([Query(None, 'foo'), Query(None, 'bar')], operator='or')),
+    ('(a AND b) OR (c AND (d OR e))',
+                                  ComplexQuery([ComplexQuery([Query(None, 'a'), Query(None, 'b')], operator='and'), ComplexQuery([Query(None, 'c'), ComplexQuery([Query(None, 'd'), Query(None, 'e')], operator='or')], operator='and')], operator='or')),
+    ('(foo:"") (bar:baz)',        ComplexQuery([Query('foo', ''), Query('bar', 'baz')], operator='or')),
+    ('(foo:"") (OR:bar)',         ComplexQuery([Query('foo', ''), Query('OR', 'bar')], operator='or')),
+#    ('foo: OR',                   ['foo', 'or']),
+#    ('foo: OR ',                  ['foo', 'or']),
+#    ('(foo:)',                    ['foo', '']),
+    ('(foo: bar)',                Query('foo', 'bar')),
+    ('(a:b) AND (c:d)',           ComplexQuery([Query('a', 'b'), Query('c', 'd')], operator='and')),
+    ('a:(b c)',                   ComplexQuery([Query('a', 'b'), Query('a', 'c')], operator='or')),
+    ('a:(b OR c)',                ComplexQuery([Query('a', 'b'), Query('a', 'c')], operator='or')),
+    ('a:(b c d)',                 ComplexQuery([Query('a', 'b'), Query('a', 'c'), Query('a', 'd')], operator='or')),
+    ('a:(b (c d))',               ComplexQuery([Query('a', 'b'), Query('a', 'c'), Query('a', 'd')], operator='or')),
+    ('a:(b OR (c d))',            ComplexQuery([Query('a', 'b'), Query('a', 'c'), Query('a', 'd')], operator='or')),
+    ('"JeanANDPaul"',             Query(None, 'JeanANDPaul')),
+    ('"Jean" AND "Paul"',         ComplexQuery([Query(None, 'Jean'), Query(None, 'Paul')], operator='and')),
+    ('"jean paul" OR "thierry"',  ComplexQuery([Query(None, 'jean paul'), Query(None, 'thierry')], operator='or')),
+    ('title:Paul Jean Lili',      ComplexQuery([Query('title', 'Paul'), Query(None, 'Jean'), Query(None, 'Lili')], operator='or')),
+    ('toto AND titi OR tutu AND tata OR toto',
+                                  ComplexQuery([ComplexQuery([Query(None, 'toto'), Query(None, 'titi')], operator='and'), ComplexQuery([Query(None, 'tutu'), Query(None, 'tata')], operator='and'), Query(None, 'toto')], operator='or')),
+    ('toto AND (titi OR tutu) AND tata OR toto',
+                                  ComplexQuery([ComplexQuery([Query(None, 'toto'), ComplexQuery([Query(None, 'titi'), Query(None, 'tutu')], operator='or'), Query(None, 'tata')], operator='and'), Query(None, 'toto')], operator='or')),
+    ('"OR ARGENT"',               Query(None, 'OR ARGENT')),
+    ('1 AND 2 OR 3',              ComplexQuery([ComplexQuery([Query(None, '1'), Query(None, '2')], operator='and'), Query(None, '3')], operator='or')),
+    ('1 OR 2 AND 3',              ComplexQuery([Query(None, '1'), ComplexQuery([Query(None, '2'), Query(None, '3')], operator='and')], operator='or')),
+    ('1 AND 2 3',                 ComplexQuery([ComplexQuery([Query(None, '1'), Query(None, '2')], operator='and'), Query(None, '3')], operator='or')),
+    ('1 2 AND 3',                 ComplexQuery([Query(None, '1'), ComplexQuery([Query(None, '2'), Query(None, '3')], operator='and')], operator='or')),
+    ('10 11 OR 12 13',            ComplexQuery([Query(None, '10'), Query(None, '11'), Query(None, '12'), Query(None, '13')], operator='or')),
+    ('((1 AND 2 OR 3) OR (4 AND 5 6) OR (7 8 AND 9) OR (10 11 OR 12 13))',
+                                  ComplexQuery([ComplexQuery([Query(None, '1'), Query(None, '2')], operator='and'), Query(None, '3'), ComplexQuery([Query(None, '4'), Query(None, '5')], operator='and'), Query(None, '6'), Query(None, '7'), ComplexQuery([Query(None, '8'), Query(None, '9')], operator='and'), Query(None, '10'), Query(None, '11'), Query(None, '12'), Query(None, '13')], operator='or')),
+    ('((titi:foo) AND (toto:bar)) OR ((titi:bar) AND (toto:foo))',
+                                  ComplexQuery([ComplexQuery([Query('titi', 'foo'), Query('toto', 'bar')], operator='and'), ComplexQuery([Query('titi', 'bar'), Query('toto', 'foo')], operator='and')], operator='or')),
+    ('title:(Paul Jean OR Lili)', ComplexQuery([Query('title', 'Paul'), Query('title', 'Jean'), Query('title', 'Lili')], operator='or')),
+    ('title:Paul Jean OR Lili',   ComplexQuery([Query('title', 'Paul'), Query(None, 'Jean'), Query(None, 'Lili')], operator='or')),
+  ]
+
+  def walk(node, key=None):
+    """
+      Recusrively walk given AST and build ComplexQuery & Query instances for each node.
+    """
+    if node.isLeaf():
+      comparison_operator = node.getComparisonOperator()
+      if comparison_operator == '':
+        comparison_operator = '='
+      result = Query(key, node.getValue(), comparison_operator=comparison_operator)
+    elif node.isColumn():
+      result = walk(node.getSubNode(), node.getColumnName())
+    else:
+      query_list = [walk(x, key) for x in node.getNodeList()]
+      operator = node.getLogicalOperator()
+      if operator == 'not' or len(query_list) > 1:
+        result = ComplexQuery(query_list, operator=operator)
+      elif len(query_list) == 1:
+        result = query_list[0]
+      else:
+        result = None
+    return result
+
+  original_parse = _parse
+
+  def parse(input, *args, **kw):
+    """
+      Parse input and walk generated AST.
+    """
+    result = original_parse(input, *args, **kw)
+    if result is not None:
+      #print repr(result)
+      result = walk(result)
+    return result
+
+  success_count = 0
+  for input, expected in check_list:
+    try:
+      result = parse(input)
+    except ParserOrLexerError, message:
+      print "ERROR when checking %r" % (input, )
+      print " crashed with: %s" % (message, )
+      print " instead of producing %r" % (expected, )
+    else:
+      if result != expected:
+        print "ERROR when checking %r:" % (input, )
+        print " produced   %r" % (result, )
+        print " instead of %r" % (expected, )
+      else:
+        success_count += 1
+  print '%i/%i checks succeeded.' % (success_count, len(check_list))
+  while 1:
+    try:
+      input = raw_input('catalog> ')
+    except (EOFError, KeyboardInterrupt):
+      break
+    print repr(input)
+    try:
+      try:
+        detector_result = getAdvancedSearchTextDetector()(input)
+      except ParserOrLexerError, message:
+        print '  Detector raise: %r' % (message, )
+        detector_result = False
+      else:
+        print '  Detector: %r' % (detector_result, )
+      if detector_result:
+        print '  LEX:'
+        lexer = getAdvancedSearchTextParser().lexer
+        lexer.input(input)
+        while 1:
+          tok = lexer.token()
+          if not tok: break      # No more input
+          print '    %s' % (tok, )
+        print '  YACC:'
+        print '    %r' % (parse(input, debug=2), )
+      else:
+        print '    %r' % (input, )
+    except ParserOrLexerError, message:
+      print message
+  print
+
diff --git a/product/ZSQLCatalog/SearchText/__init__.py b/product/ZSQLCatalog/SearchText/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e45dba662a6d824b94d6e02c9e09defd383bf248
--- /dev/null
+++ b/product/ZSQLCatalog/SearchText/__init__.py
@@ -0,0 +1,2 @@
+from SearchTextParser import parse
+
diff --git a/product/ZSQLCatalog/SearchText/lexer.py b/product/ZSQLCatalog/SearchText/lexer.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad6ac0c1268bd7d7321014ef8fcf5993cba6494d
--- /dev/null
+++ b/product/ZSQLCatalog/SearchText/lexer.py
@@ -0,0 +1,160 @@
+##############################################################################
+#
+# Copyright (c) 2008-2009 Nexedi SA and Contributors. All Rights Reserved.
+#                    Vincent Pelletier <vincent@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 ply import lex, yacc
+import os
+import sys
+from cStringIO import StringIO
+
+try:
+  from zLOG import LOG
+except ImportError:
+  def LOG(channel, level, message):
+    print >>sys.stderr, message
+
+module_path = os.path.dirname(os.path.abspath(__file__))
+
+class ParserOrLexerError(Exception):
+  pass
+
+class LexerError(ParserOrLexerError):
+  pass
+
+class ParserError(ParserOrLexerError):
+  pass
+
+class lexer(object):
+  def init(self, **kw):
+    debug = kw.pop('debug', False)
+    # Catch all logs with a cStringIO
+    output = sys.stdout = sys.stderr = StringIO()
+    self.lexer = lex.lex(object=self, **kw)
+    self.parser = yacc.yacc(module=self, debug=debug,
+                            debugfile="%s.out" % (self.__class__.__name__, ),
+                            tabmodule="%s_parsetab" % (self.__class__.__name__, ),
+                            outputdir=module_path)
+    sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__
+    # Emit all logs with regular Zope logging
+    for line in output.getvalue().split('\n'):
+      if len(line):
+        LOG('lexer', 0, line)
+
+  def t_error(self, t):
+    raise LexerError, 'ERROR: Illegal character %r' % (t.value[0], )
+
+  def p_error(self, p):
+    raise ParserError, 'Syntax error in input: %r' % (p, )
+
+  def input(self, string):
+    self.lexer.input(string)
+
+  def token(self):
+    return self.lexer.token()
+
+  tokens = (
+    'OR',
+    'AND',
+    'NOT',
+    'COLUMN',
+    'STRING',
+    'WORD',
+    'OPERATOR',
+    'LEFT_PARENTHESE',
+    'RIGHT_PARENTHESE')
+
+  t_ignore = ' '
+
+  def t_LEFT_PARENTHESE(self, t):
+    r'\('
+    return t
+
+  def t_RIGHT_PARENTHESE(self, t):
+    r'\)'
+    return t
+
+  def t_OPERATOR(self, t):
+    r'(>=?|<=?|!?=)'
+    return t
+
+  def t_STRING(self, t):
+    r'"(\\.|[^\\"])*"'
+    # Unescape value and strip surrounding quotes
+    value_list = []
+    append = value_list.append
+    escaped = False
+    for char in t.value[1:-1]:
+      if escaped:
+        escaped = False
+        if char != '"':
+          append('\\')
+      else:
+        if char == '\\':
+          escaped = True
+          continue
+      append(char)
+    assert not escaped
+    t.value = ''.join(value_list)
+    return t
+
+  def t_COLUMN(self, t):
+    r'[^><= :\(\)"][^ :\(\)"]*:'
+    t.value = t.value[:-1]
+    return t
+
+  def t_OR(self, t):
+    r'OR'
+    return t
+
+  def t_AND(self, t):
+    r'AND'
+    return t
+
+  def t_NOT(self, t):
+    r'NOT'
+    return t
+
+  def t_WORD(self, t):
+    r'[^><= :\(\)"][^ :\(\)"]*'
+    return t
+
+  def parse(self, *args, **kw):
+    kw['lexer'] = self
+    return self.parser.parse(*args, **kw)
+
+  __call__ = parse
+
+def update_docstrings(klass):
+  for property in dir(klass):
+    if property.startswith('t_'):
+      source = getattr(lexer, property, None)
+      if callable(source):
+        destination = getattr(klass, property)
+        assert callable(destination)
+        if destination.__doc__ is None:
+          destination.im_func.__doc__ = source.__doc__
+
diff --git a/product/ZSQLCatalog/ZSQLCatalog.py b/product/ZSQLCatalog/ZSQLCatalog.py
index ec63a320557c89d1283b7bb7bf8ee2cd5a4f87de..54cb28916b9ea7082cead717cfd226623c367641 100644
--- a/product/ZSQLCatalog/ZSQLCatalog.py
+++ b/product/ZSQLCatalog/ZSQLCatalog.py
@@ -790,8 +790,9 @@ class ZCatalog(Folder, Persistent, Implicit):
         except ConflictError:
           raise
         except:
+          raise
           LOG('WARNING ZSQLCatalog', 0, 'wrapObject failed on the object %r' % (obj,), error=sys.exc_info())
-          failed_object_list.append(obj)
+          failed_object_list.append(obj) # XXX Strange JPS - why LOG and keep on ?? wrap_obj not defined
 
     # run activity or execute for each archive depending on priority
     if len(catalog_dict):
@@ -1043,7 +1044,7 @@ class ZCatalog(Folder, Persistent, Implicit):
   security.declarePublic('buildSqlQuery')
   buildSqlQuery = buildSQLQuery
 
-  def searchResults(self, REQUEST=None, used=None, sql_catalog_id=None, **kw):
+  def searchResults(self, REQUEST=None, sql_catalog_id=None, **kw):
     """
     Search the catalog according to the ZTables search interface.
     Search terms can be passed in the REQUEST or as keyword
@@ -1051,18 +1052,18 @@ class ZCatalog(Folder, Persistent, Implicit):
     """
     catalog = self.getSQLCatalog(sql_catalog_id)
     if catalog is not None:
-      return apply(catalog.searchResults, (REQUEST,used), kw)
+      return apply(catalog.searchResults, (REQUEST, ), kw)
     return []
 
   __call__=searchResults
 
-  def countResults(self, REQUEST=None, used=None, sql_catalog_id=None, **kw):
+  def countResults(self, REQUEST=None, sql_catalog_id=None, **kw):
     """
     Counts the number of items which satisfy the query defined in kw.
     """
     catalog = self.getSQLCatalog(sql_catalog_id)
     if catalog is not None:
-      return apply(catalog.countResults, (REQUEST,used), kw)
+      return apply(catalog.countResults, (REQUEST, ), kw)
     return []
 
 ## this stuff is so the find machinery works
diff --git a/product/ZSQLCatalog/__init__.py b/product/ZSQLCatalog/__init__.py
index 8926814e06ed98e7cdfe9f009d226d701ed71e4a..703d9f14c064bf4dea7854e4eeba99434e679a41 100644
--- a/product/ZSQLCatalog/__init__.py
+++ b/product/ZSQLCatalog/__init__.py
@@ -44,3 +44,6 @@ def initialize(context):
 from AccessControl import ModuleSecurityInfo, ClassSecurityInfo
 ModuleSecurityInfo('Products.ZSQLCatalog.SQLCatalog').declarePublic(
             'ComplexQuery', 'Query', 'NegatedQuery',)
+
+from Query import Query, SimpleQuery
+from SearchKey import SearchKey
\ No newline at end of file
diff --git a/product/ZSQLCatalog/tests/testSQLCatalog.py b/product/ZSQLCatalog/tests/testSQLCatalog.py
new file mode 100644
index 0000000000000000000000000000000000000000..51ab9bc2e6436885e28e140726dd9c3a3e2aa2e1
--- /dev/null
+++ b/product/ZSQLCatalog/tests/testSQLCatalog.py
@@ -0,0 +1,301 @@
+##############################################################################
+#
+# Copyright (c) 2006-2009 Nexedi SA and Contributors. All Rights Reserved.
+#          Jerome Perrin <jerome@nexedi.com>
+#          Vincent Pelletier <vincent@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.
+#
+##############################################################################
+
+import unittest
+import sys
+
+from DateTime import DateTime
+from Products.ZSQLMethods.SQL import SQL as ZSQLMethod
+from Products.CMFCore.Expression import Expression
+
+from Products.ZSQLCatalog.SQLCatalog import Catalog as SQLCatalog
+from Products.ZSQLCatalog.ZSQLCatalog import ZCatalog as ZSQLCatalog
+from Products.ZSQLCatalog.SQLCatalog import Query
+from Products.ZSQLCatalog.SQLCatalog import ComplexQuery
+from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
+from Products.ZSQLCatalog.SQLCatalog import NegatedQuery
+from Products.ZSQLCatalog.Query.Query import Query as _Query
+from Products.ZSQLCatalog.Query.EntireQuery import EntireQuery
+from Products.ZSQLCatalog.Query.RelatedQuery import RelatedQuery
+from DateTime import DateTime
+
+class ReferenceQuery:
+  operator = None
+  column = None
+  value = None
+
+  def __init__(self, *args, **kw):
+    self.operator = kw.pop('operator', None)
+    assert len(args) == 0 or len(kw) == 0
+    self.args = []
+    for arg in args:
+      if isinstance(arg, (tuple, list)):
+        self.args.extend(arg)
+      else:
+        self.args.append(arg)
+    if len(kw) == 1:
+      self.column, self.value = kw.items()[0]
+    elif len(kw) > 1:
+      raise ValueError, 'kw must not have more than one item: %r' % (kw, )
+
+  def __eq__(self, other):
+    if isinstance(other, SimpleQuery):
+      return self.column is not None and \
+             other.getColumn() == self.column and \
+             other.getValue() == self.value and \
+             other.operator == self.operator
+    elif isinstance(other, ComplexQuery):
+      if not (len(other.query_list) == len(self.args) and \
+              other.logical_operator == self.operator):
+        return False
+      other_query_list = other.query_list[:]
+      for subquery in self.args:
+        for other_query_id in xrange(len(other_query_list)):
+          other_query = other_query_list[other_query_id]
+          if subquery == other_query:
+            other_query_list.pop(other_query_id)
+            break
+        else:
+          return False
+      return len(other_query_list) == 0
+    elif isinstance(other, EntireQuery):
+      return len(self.args) == 1 and \
+             self.args[0] == other.query
+    elif isinstance(other, RelatedQuery):
+      return self == other.join_condition
+    elif isinstance(other, Query):
+      return self == other.wrapped_query
+    else:
+      raise TypeError, 'Compared value is not a (known) Query instance: (%s) %r' % (other.__class__.__name__, other)
+
+  def __repr__(self):
+    return '<%s column=%r operator=%r value=%r args=%r>' % \
+      (self.__class__.__name__, self.column, self.operator, self.value, self.args)
+
+class RelatedReferenceQuery:
+  def __init__(self, reference_subquery):
+    self.subquery = reference_subquery
+
+  def __eq__(self, other):
+    return isinstance(other, RelatedQuery) and \
+           self.subquery == other.join_condition
+
+  def __repr__(self):
+    return '<%s %r>' % (self.__class__.__name__, self.subquery)
+
+class DummyCatalog(SQLCatalog):
+  """
+    Mimic a table stucture.
+  """
+
+  sql_catalog_keyword_search_keys = ('keyword', )
+  sql_catalog_datetime_search_keys = ('date', )
+
+  def getColumnMap(self):
+    return {
+      'uid': ['foo', 'bar'],
+      'default': ['foo', ],
+      'keyword': ['foo', ],
+      'date': ['foo', ],
+      'other_uid': ['bar', ]
+    }
+
+  def getSQLCatalogRelatedKeyList(self, key_list):
+    return [
+      'related_default | bar,foo/default/z_related_table',
+      'related_keyword | bar,foo/keyword/z_related_table',
+      'related_date | bar,foo/date/z_related_table'
+    ]
+
+  def z_related_table(self, *args, **kw):
+    assert kw.get('src__', False)
+    assert 'query_table' in kw
+    assert 'table_0' in kw
+    assert 'table_1' in kw
+    assert len(kw) == 4
+    return '%(table_0)s.uid = %(query_table)s.uid AND %(table_0)s.other_uid = %(table_1)s' % kw
+
+class TestSQLCatalog(unittest.TestCase):
+  def setUp(self):
+    self._catalog = DummyCatalog('dummy_catalog')
+
+  def assertCatalogRaises(self, exception, kw):
+    self.assertRaises(exception, self._catalog, src__=1, query_table='foo', **kw)
+
+  def catalog(self, reference_tree, kw, check_search_text=True):
+    reference_param_dict = self._catalog._queryResults(query_table='foo', **kw)
+    query = self._catalog.buildQuery(kw)
+    self.assertEqual(reference_tree, query)
+    search_text = query.asSearchTextExpression(self._catalog)
+    if check_search_text:
+      # XXX: sould "keyword" be always used for search text searches ?
+      search_text_param_dict = self._catalog._queryResults(query_table='foo', keyword=search_text)
+      self.assertEqual(reference_param_dict, search_text_param_dict,
+          'Query: %r\nSearchText: %r\nReference: %r\nSecond rendering: %r' % \
+                       (query, search_text, reference_param_dict, search_text_param_dict))
+
+  def test_001_DefaultKey(self):
+    for column in ('default', 'related_default'):
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='=', default='a'), operator='and'),
+                   {column: 'a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='like', default='%a'), operator='and'),
+                   {column: '%a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='<', default='a'), operator='and'),
+                   {column: '<a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='<', default='path'), operator='and'),
+                   {column: {'query': 'path', 'range': 'max'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='in', default=['a', 'b']), operator='and'),
+                   {column: ['a', 'b']})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='in', default=['a', 'b']), operator='and'),
+                   {column: ['=a', '=b']})
+
+  def test_002_keyOverride(self):
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='=', default='%a'), operator='and'),
+                 {'default': {'query': '%a', 'key': 'ExactMatch'}},
+                 check_search_text=False)
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='=', default='<a'), operator='and'),
+                 {'default': {'query': '<a', 'key': 'ExactMatch'}},
+                 check_search_text=False)
+
+  def test_003_DateTimeKey(self):
+    for column in ('date', 'related_date'):
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='>=', date=DateTime('2008/10/01 12:10:21')), operator='and'),
+                   {column: {'query': '>2008/10/01 12:10:20', 'format': '%y/%m/%d'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='>=', date=DateTime('2008/10/01 12:10:21 CEST')), operator='and'),
+                   {column: {'query': '>2008/10/01 12:10:20 CEST', 'format': '%y/%m/%d'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='>=', date=DateTime('2008/10/01 12:10:21 CET')), operator='and'),
+                   {column: {'query': '>2008/10/01 12:10:20 CET', 'format': '%y/%m/%d'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(operator='>=', date=DateTime('2008/10/01 UTC')),
+                     ReferenceQuery(operator='<', date=DateTime('2008/10/02 UTC'))
+                   , operator='and'), operator='and'),
+                   {column: '2008/10/01 UTC'})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(operator='>=', date=DateTime('2008/01/01 UTC')),
+                     ReferenceQuery(operator='<', date=DateTime('2009/01/01 UTC'))
+                   , operator='and'), operator='and'),
+                   {column: '2008 UTC'})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(operator='>=', date=DateTime('2008/01/01 UTC')),
+                     ReferenceQuery(operator='<', date=DateTime('2008/02/01 UTC'))
+                   , operator='and'), operator='and'),
+                   {column: '2008/01 UTC'})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(operator='>=', date=DateTime('2008/10/01 UTC')),
+                     ReferenceQuery(operator='<', date=DateTime('2008/10/02 UTC'))
+                   , operator='and'), operator='and'),
+                   {column: {'type': 'date', 'query': '10/01/2008 UTC', 'format': '%m/%d/%Y'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(operator='>=', date=DateTime('2008/10/01 UTC')),
+                     ReferenceQuery(operator='<', date=DateTime('2008/10/02 UTC'))
+                   , operator='and'), operator='and'),
+                   {column: {'type': 'date', 'query': '01/10/2008 UTC', 'format': '%d/%m/%Y'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='in', date=[DateTime('2008/01/10 UTC'), DateTime('2008/01/09 UTC')]), operator='and'),
+                   {column: {'query': ['2008/01/10 UTC', '2008/01/09 UTC'], 'operator': 'in'}},
+                   check_search_text=False)
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='>', date=DateTime('2008/01/10 UTC')), operator='and'),
+                   {column: {'query': '2008/01/10 UTC', 'range': 'nlt'}},
+                   check_search_text=False)
+
+  def test_004_KeywordKey(self):
+    for column in ('keyword', 'related_keyword'):
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%a%'), operator='and'),
+                   {column: 'a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%a'), operator='and'),
+                   {column: '%a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='<', keyword='a'), operator='and'),
+                   {column: '<a'})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='<', keyword='path'), operator='and'),
+                   {column: {'query': 'path', 'range': 'max'}})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                       ReferenceQuery(operator='like', keyword='%a%'),
+                       ReferenceQuery(operator='like', keyword='%b%')
+                     , operator='or'), operator='and'),
+                   {column: ['a', 'b']})
+      self.catalog(ReferenceQuery(ReferenceQuery(operator='in', keyword=['a', 'b']), operator='and'),
+                   {column: ['=a', '=b']})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                       ReferenceQuery(operator='like', keyword='%a%'),
+                       ReferenceQuery(operator='<', keyword='b')
+                     , operator='or'), operator='and'),
+                   {column: ['a', '<b']})
+      self.catalog(ReferenceQuery(ReferenceQuery(
+                       ReferenceQuery(operator='like', keyword='%a%'),
+                       ReferenceQuery(operator='like', keyword='%b')
+                     , operator='or'), operator='and'),
+                   {column: ['a', '%b']})
+
+  def test_005_SearchText(self):
+    self.catalog(ReferenceQuery(ReferenceQuery(ReferenceQuery(operator='like', keyword='%=a%'), ReferenceQuery(operator='like', keyword='%=b%'), operator='or'), operator='and'),
+                 {'keyword': '"=a" OR "=b"'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='in', keyword=['a', 'b']), operator='and'),
+                 {'keyword': '="a" OR ="b"'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='in', keyword=['a', 'b']), operator='and'),
+                 {'keyword': '=a OR =b'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='in', keyword=['a', 'b', 'c']), operator='and'),
+                 {'keyword': '=a OR =b OR =c'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%a%'), operator='and'),
+                 {'keyword': 'keyword:a'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='=', default='a'), operator='and'),
+                 {'keyword': 'default:a'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%a b%'), operator='and'),
+                 {'keyword': 'a b'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%=a OR =b%'), operator='and'),
+                 {'keyword': '"=a OR =b"'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='=', keyword='=a OR =b'), operator='and'),
+                 {'keyword': '="=a OR =b"'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='<', keyword='=a OR =b'), operator='and'),
+                 {'keyword': '<"=a OR =b"'})
+    self.catalog(ReferenceQuery(ReferenceQuery(operator='like', keyword='%"a" OR "b"%'), operator='and'),
+                 {'keyword': '"\\"a\\" OR \\"b\\""'})
+
+  def test_006_testRelatedKey_with_multiple_join(self):
+    # The name of catalog parameter does not matter at all
+    # ComplexQuery(ComplexQuery(AutoQuery(RelatedQuery(SimpleQuery())), AutoQuery(RelatedQuery(SimpleQuery()))))
+    # 'AutoQuery' doesn't need any ReferenceQuery equivalent.
+    self.catalog(ReferenceQuery(ReferenceQuery(
+                     ReferenceQuery(RelatedReferenceQuery(ReferenceQuery(operator='=', default='a')), operator='and'),
+                     ReferenceQuery(RelatedReferenceQuery(ReferenceQuery(operator='=', default='b')), operator='and')
+                   , operator='and'), operator='and'),
+                 {'query': ComplexQuery(Query(related_default='a'), Query(related_default='b'))})
+
+##return catalog(title=Query(title='a', operator='not'))
+#return catalog(title={'query': 'a', 'operator': 'not'})
+#return catalog(title={'query': ['a', 'b'], 'operator': 'not'})
+#return context.portal_catalog(source_title="toto", source_description="tutu", src__=1)
+#print catalog(query=ComplexQuery(Query(title='1'), ComplexQuery(Query(portal_type='Foo') ,Query(portal_type='Bar'), operator='or'), operator='and'))
+#print catalog(title={'query': ('path', 2), 'operator': 'and'}, exception=TypeError)
+#print catalog(sort_on=[('source_title', )], check_search_text=False)
+#print catalog(query=ComplexQuery(Query(source_title='foo'), Query(source_title='bar')), sort_on=[('source_title', ), ('source_title_1', )], check_search_text=False)
+
+def test_suite():
+  suite = unittest.TestSuite()
+  suite.addTest(unittest.makeSuite(TestSQLCatalog))
+  return suite
+
diff --git a/product/ZSQLCatalog/tests/testSearchKeys.py b/product/ZSQLCatalog/tests/testSearchKeys.py
index e75ff300ad4733c596f1278135216807833cc5e5..c5048f2ccb742dc6a502fe3262e1202b108e9be9 100644
--- a/product/ZSQLCatalog/tests/testSearchKeys.py
+++ b/product/ZSQLCatalog/tests/testSearchKeys.py
@@ -28,6 +28,7 @@
 
 import unittest
 from DateTime import DateTime
+from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
 from Products.ZSQLCatalog.SearchKey.DefaultKey import DefaultKey
 from Products.ZSQLCatalog.SearchKey.RawKey import RawKey
 from Products.ZSQLCatalog.SearchKey.KeyWordKey import KeyWordKey
@@ -37,19 +38,20 @@ from Products.ZSQLCatalog.SearchKey.FloatKey import FloatKey
 from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance
 from Products.ZSQLCatalog.SearchKey.ScriptableKey import ScriptableKey, KeyMappingKey
 
-class TestSearchKeyLexer(unittest.TestCase):
+class TestSearchKeyLexer(ERP5TypeTestCase):
   """Test search keys
   """
   run_all_test = 1
   quiet = 0
-
+  
   def compare(self, search_key_class, search_value, expected_token_types):
     """ """
     key = getSearchKeyInstance(search_key_class)
-    tokens = key.tokenize(search_value)
+    tokens = key.tokenize(search_value)  
     token_types = [x.type for x in tokens]
-    self.assertEqual(token_types, list(expected_token_types))
+    self.assertSameSet(token_types, expected_token_types)  
 
+  
   def test_01ProperPoolInitialization(self, quiet=quiet, run=run_all_test):
     """ Check that search key pool is properly initialized """
     if not run: return
@@ -82,7 +84,6 @@ size/Child/34"""
     self.compare(DefaultKey, 
                 'S\xc3\xa9bastien or !="Doe John1" and Doe', 
                 ('WORD', 'OR', 'NOT', 'WORDSET', 'AND', 'WORD',))  
-    self.compare(DefaultKey, '.', ('WORD',))
                              
   def test_03KeyWordKey(self, quiet=quiet, run=run_all_test):
     """ Check lexer for KeyWordKey."""
@@ -101,37 +102,29 @@ size/Child/34"""
     self.compare(KeyWordKey, '<=John% and >="JOHN John"', 
                             ('LESSTHANEQUAL', 'KEYWORD', 'AND', 
                              'GREATERTHANEQUAL', 'WORDSET',))                             
-    self.compare(KeyWordKey, '=John% and >="JOHN John"',
-                            ('EXPLICITEQUALLITYWORD', 'AND',
+    self.compare(KeyWordKey, '=John% and >="JOHN John"', 
+                            ('EXPLICITEQUALLITYWORD', 'KEYWORD', 'AND', 
                              'GREATERTHANEQUAL', 'WORDSET',))
-    self.compare(KeyWordKey, '.', ('WORD',))
                              
   def test_04DateTimeKey(self, quiet=quiet, run=run_all_test):
     """ Check lexer for DateTimeKey."""
     if not run: return
-    self.compare(DateTimeKey, '2007.12.23', ('DATE',))
-    self.compare(DateTimeKey,
+    self.compare(DateTimeKey, '2007.12.23', ('DATE',))     
+    self.compare(DateTimeKey, 
                  '=2007.12.23 22:00:00 Universal or =23/12/2007 10:10 and !=2009-12-12',
                  ('EQUAL', 'DATE', 'OR', 'EQUAL', 'DATE', 'AND', 'NOT', 'DATE',))   
-    self.compare(DateTimeKey,
+    self.compare(DateTimeKey, 
                  '>=2007.12.23 22:00:00 GMT+02 or <=23/12/2007 and >2009/12/12 and <2009-11-11',
                  ('GREATERTHANEQUAL', 'DATE', 'OR', 'LESSTHANEQUAL', 'DATE', 
                    'AND', 'GREATERTHAN', 'DATE', 'AND', 'LESSTHAN', 'DATE'))
-
+                     
   def test_05FullTextKey(self, quiet=quiet, run=run_all_test):
-    """ Check lexer for FullTextKey."""
+    """ Check lexer for FullTextKey."""  
     if not run: return
-    self.compare(FullTextKey, 'John', ('WORD',))
-    self.compare(FullTextKey, 'John Doe', ('WORD', 'WORD',))
-    self.compare(FullTextKey, '+John -Doe',
-                 ('PLUS', 'WORD', 'MINUS', 'WORD',))
-    self.compare(FullTextKey, 'John*', ('WORD', 'ASTERISK'))
-    self.compare(FullTextKey, '+John*', ('PLUS', 'WORD', 'ASTERISK'))
-    self.compare(FullTextKey, '.', ('WORD',))
-    self.compare(FullTextKey, '"John Doe"', ('DOUBLEQUOTE', 'WORD', 'WORD', 'DOUBLEQUOTE'))
-    self.compare(FullTextKey, '+apple +(>turnover <strudel)',
-                 ('PLUS', 'WORD', 'PLUS', 'LEFTPARENTHES', 'GREATERTHAN', 'WORD',
-                  'LESSTHAN', 'WORD', 'RIGHTPARENTHES',))
+    self.compare(FullTextKey, 'John Doe', 
+                 ('WORD', 'WORD',)) 
+    self.compare(FullTextKey, '+John -Doe', 
+                 ('PLUS', 'WORD', 'MINUS', 'WORD',)) 
 
   def test_06ScriptableKey(self, quiet=quiet, run=run_all_test):
     """ Check lexer for ScriptableKey."""  
@@ -143,7 +136,7 @@ size/Child/34"""
                 'John Doe OR creation_date>=2005/12/12',
                 ('WORD', 'WORD', 'OR', 'KEYMAPPING',))                
                    
-class TestSearchKeyQuery(unittest.TestCase):
+class TestSearchKeyQuery(ERP5TypeTestCase):
   """Test search keys query generation
   """
   run_all_test = 1
@@ -206,12 +199,6 @@ class TestSearchKeyQuery(unittest.TestCase):
                 '%John and !=Doe%', 
                 "((((title = '%John') AND (title != 'Doe%'))))",
                 [])
-    # special chars
-    self.compare(DefaultKey,
-                 'title',
-                 '.',
-                 "((((title = '.'))))",
-                 [])
                 
   def test_02KeyWordKey(self, quiet=quiet, run=run_all_test):
     """ Check DefaultKey query generation"""
@@ -241,12 +228,6 @@ class TestSearchKeyQuery(unittest.TestCase):
                 '%John Doe% or =Doe John', 
                 "((((title LIKE '%John Doe%'))) OR (((title = 'Doe John'))))",
                 [])       
-    # special chars
-    self.compare(KeyWordKey,
-                 'title',
-                 '.',
-                 "((((title LIKE '%.%'))))",
-                 [])
                 
   def test_03DateTimeKey(self, quiet=quiet, run=run_all_test):
     """ Check DefaultKey query generation"""
@@ -368,18 +349,7 @@ class TestSearchKeyQuery(unittest.TestCase):
                 '1ab521ty', 
                 "delivery.stock = '1ab521ty'",
                 [])    
-    # special chars
-    self.compare(RawKey,
-                 'delivery.stock',
-                 '.',
-                 "delivery.stock = '.'",
-                 [])
-    #None values
-    self.compare(RawKey,
-                 'title',
-                 None,
-                 "title is NULL",
-                 [])
+                
   def test_06FullTextKey(self, quiet=quiet, run=run_all_test):
     """ Check FullTextKey query generation"""
     if not run: return
@@ -389,28 +359,6 @@ class TestSearchKeyQuery(unittest.TestCase):
                 "MATCH full_text.SearchableText AGAINST ('john' )",
                 ["MATCH full_text.SearchableText AGAINST ('john' ) AS full_text_SearchableText_relevance", 
                 "MATCH full_text.SearchableText AGAINST ('john' ) AS SearchableText_relevance"])    
-
-    # special chars
-    self.compare(FullTextKey,
-                'full_text.SearchableText',
-                '.',
-                "MATCH full_text.SearchableText AGAINST ('.' )",
-                ["MATCH full_text.SearchableText AGAINST ('.' ) AS full_text_SearchableText_relevance",
-                "MATCH full_text.SearchableText AGAINST ('.' ) AS SearchableText_relevance"])
-    #Boolean Mode
-    self.compare(FullTextKey,
-                'full_text.SearchableText',
-                'john stuart mill',
-                "MATCH full_text.SearchableText AGAINST ('+john +stuart +mill' IN BOOLEAN MODE)",
-                ["MATCH full_text.SearchableText AGAINST ('+john +stuart +mill' IN BOOLEAN MODE) AS full_text_SearchableText_relevance",
-                "MATCH full_text.SearchableText AGAINST ('+john +stuart +mill' IN BOOLEAN MODE) AS SearchableText_relevance"])
-
-    self.compare(FullTextKey,
-                'full_text.SearchableText',
-                'John*',
-                "MATCH full_text.SearchableText AGAINST ('John*' IN BOOLEAN MODE)",
-                ["MATCH full_text.SearchableText AGAINST ('John*' IN BOOLEAN MODE) AS full_text_SearchableText_relevance",
-                "MATCH full_text.SearchableText AGAINST ('John*' IN BOOLEAN MODE) AS SearchableText_relevance"])
     
   def test_07ScriptableKey(self, quiet=quiet, run=run_all_test):
     """ Check ScriptableKey query generation"""
diff --git a/product/ZSQLCatalog/tests/testZSQLCatalog.py b/product/ZSQLCatalog/tests/testZSQLCatalog.py
index d534c8d6cc3082fd70c1ba10d487221e11605bdb..8619e4d20a9d1a714a607a9edca152f5e9111f6a 100644
--- a/product/ZSQLCatalog/tests/testZSQLCatalog.py
+++ b/product/ZSQLCatalog/tests/testZSQLCatalog.py
@@ -95,369 +95,9 @@ class TestSQLCatalog(unittest.TestCase):
     self.assertFalse(
         self._catalog.isPortalTypeSelected('not_exists', 'Selected'))
 
-
-class TestQuery(unittest.TestCase):
-  """Test SQL bits generated from Queries
-  """
-  def testSimpleQuery(self):
-    q = Query(title='Foo')
-    self.assertEquals(
-          dict(where_expression="((((title = 'Foo'))))",
-               select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=[],
-                            datetime_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):
-    q = Query(title=None)
-    self.assertEquals(
-          dict(where_expression="title is NULL",
-               select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=[], 
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  def testEmptyQueryNotIgnoreEmptyString(self):
-    q = Query(title='')
-    # if you want to search with an empty string, pass ignore_empty_string=0 to
-    # asSQLExpression. XXX not to __init__ ?
-    self.assertEquals(
-          dict(where_expression="title = ''",
-               select_expression_list=[]),
-          q.asSQLExpression(ignore_empty_string=0,
-                            keyword_search_keys=[],
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  def testEmptyQuery(self):
-    q = Query(title='')
-    # query are true by default
-    self.assertEquals(
-          dict(where_expression="1",
-               select_expression_list=[]),
-          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')))))",
-               select_expression_list=[]),
-          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=[], 
-                            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=[],
-                            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=[], 
-                            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=[], 
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  # format
-  def testDateFormat(self):
-    date = DateTime(2001, 02, 03)
-    q = Query(date=date, format='%Y/%m/%d', type='date')
-    self.assertEquals(
-          dict(where_expression=
-            "((((date >= '%s' AND date < '%s'))))" \
-                 %(date.toZone('UTC').ISO(), (date + 1).toZone('UTC').ISO()),
-               select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=[], 
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-  
-  # full text
-  def testSimpleQueryFullText(self):
-    q = Query(title='Foo')
-    self.assertEquals(dict(where_expression="MATCH title AGAINST ('Foo' )",
-                           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):
-    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=[],
-                            datetime_search_keys = [],
-                            full_text_search_keys=['title']))
-  
-  def testSimpleQueryFullAutomaticTextSearchMode(self):
-    q = Query(title='Foo*',)
-    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(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=[],
-                            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%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  def testQueryKeywordSearchKeyWithPercent(self):
-    q = Query(title='Fo%oo')
-    self.assertEquals(dict(where_expression="((((title LIKE 'Fo%oo'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-
-  def testQueryKeywordSearchKeyWithPercentAndOnlyOneLetter(self):
-    q = Query(title='F%o')
-    self.assertEquals(dict(where_expression="((((title LIKE 'F%o'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title']))
-
-  def testQueryKeywordSearchKeyWithPercentOnly(self):
-    q = Query(title='%')
-    self.assertEquals(dict(where_expression="((((title LIKE '%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-
-  def testQueryKeywordSearchKeyWithMinus(self):
-    q = Query(title='F-o')
-    self.assertEquals(dict(where_expression="((((title LIKE '%F-o%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  def testQueryKeywordSearchKeyWithSpace(self):
-    q = Query(title='F o')
-    self.assertEquals(dict(where_expression="((((title LIKE '%F o%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  def testQueryKeywordSearchKeyWithPercentAtTheEnd(self):
-    q = Query(title='F%')
-    self.assertEquals(dict(where_expression="((((title LIKE 'F%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-    q = Query(title='Fo%')
-    self.assertEquals(dict(where_expression="((((title LIKE 'Fo%'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-
-  def testQueryKeywordSearchKeyWithPercentAtTheBeginning(self):
-    q = Query(title='%o')
-    self.assertEquals(dict(where_expression="((((title LIKE '%o'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-    q = Query(title='%oo')
-    self.assertEquals(dict(where_expression="((((title LIKE '%oo'))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=['title'],))
-
-  def testNegatedQuery(self):
-    q1 = Query(title='Foo')
-    q = NegatedQuery(q1)
-    self.assertEquals(
-        dict(where_expression="(NOT (((((title = 'Foo'))))))",
-                           select_expression_list=[]),
-          q.asSQLExpression(keyword_search_keys=[],
-                            datetime_search_keys = [],
-                            full_text_search_keys=[]))
-
-  # complex queries
-  def testSimpleComplexQuery(self):
-    q1 = Query(title='Foo')
-    q2 = Query(reference='Bar')
-    q = ComplexQuery(q1, q2)
-    self.assertEquals(
-        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):
-    q1 = Query(title='Foo')
-    q2 = Query(reference='Bar')
-    q3 = ComplexQuery(q1, q2)
-    q = NegatedQuery(q3)
-    self.assertEquals(
-      # maybe too many parents here
-     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%'))))",
-          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'])
-
-  def testQuotedStringDefaultKey(self):
-    q = Query(title='Foo d\'Ba')
-    self.assertEquals(
-              dict(where_expression="((((title = 'Foo d''Ba'))))",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringKeywordKey(self):
-    q = Query(title='Foo d\'Ba', key='Keyword')
-    self.assertEquals(
-              dict(where_expression="((((title LIKE '%Foo d''Ba%'))))",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringExactMatch(self):
-    q = Query(title='Foo d\'Ba', key='ExactMatch')
-    self.assertEquals(
-              dict(where_expression="title = 'Foo d''Ba'",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringFullTextKey(self):
-    q = Query(title='Foo d\'Ba', type='fulltext')
-    self.assertEquals(
-        dict(where_expression="MATCH title AGAINST ('+Foo +d''Ba' IN BOOLEAN MODE)",
-             select_expression_list=["MATCH title AGAINST ('+Foo +d''Ba' IN BOOLEAN MODE)"
-                                     " AS title_relevance"]),
-          q.asSQLExpression())
-
-  def testQuotedStringListKeywordKey(self):
-    q = Query(title=('Foo d\'Ba',), key='Keyword')
-    self.assertEquals(
-              dict(where_expression="((((title LIKE '%Foo d''Ba%'))))",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringListExactMatch(self):
-    q = Query(title=('Foo d\'Ba',), key='ExactMatch')
-    self.assertEquals(
-              dict(where_expression="title = 'Foo d''Ba'",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-
-  def testQuotedStringDateKey(self):
-    q = Query(title='Foo d\'Ba', type='date')
-    self.assertEquals(
-        # I don't know exactly what we should expect here.
-              dict(where_expression="1",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringFloatKey(self):
-    q = Query(title='Foo d\'Ba', type='float')
-    self.assertEquals(
-        # I don't know exactly what we should expect here.
-        # At least it's safe.
-              dict(where_expression="1",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testQuotedStringIntKey(self):
-    q = Query(title='Foo d\'Ba', type='int')
-    self.assertEquals(
-              dict(where_expression="((((title = 'Foo d''Ba'))))",
-                   select_expression_list=[]),
-                q.asSQLExpression())
-
-  def testListValuesInQuery(self):
-    q = Query(title=('Foo', 'Bar'))
-    self.assertEquals(
-      dict(where_expression="(((((title = 'Foo')))) OR ((((title = 'Bar')))))",
-           select_expression_list=[]),
-        q.asSQLExpression())
-
-  def testListValuesInQueryWithKey(self):
-    q = Query(title=('Foo', 'Bar'), key='Keyword')
-    self.assertEquals(
-      dict(where_expression=
-             "(((((title LIKE '%Foo%')))) OR ((((title LIKE '%Bar%')))))",
-           select_expression_list=[]),
-        q.asSQLExpression())
-
-
 def test_suite():
   suite = unittest.TestSuite()
   suite.addTest(unittest.makeSuite(TestSQLCatalog))
   suite.addTest(unittest.makeSuite(TestZSQLCatalog))
-  suite.addTest(unittest.makeSuite(TestQuery))
   return suite