This patch adds a “compiler” for query filters, converting them to a
callable function used while preparing the query result. In addition, a
hints call allows some analysis to be done on the query (e.g. referenced
names), making data collection more efficient.

The depth of filters is limited to avoid exceeding the runtime's maximum
recursion depth.

More operators and other improvements can be implemented using this
base. Extensive unittests are provided.
---
 lib/query.py                  |  397 +++++++++++++++++++++++++++++++++++++++-
 test/ganeti.query_unittest.py |  340 +++++++++++++++++++++++++++++++++++
 2 files changed, 727 insertions(+), 10 deletions(-)

diff --git a/lib/query.py b/lib/query.py
index aa94616..49cc157 100644
--- a/lib/query.py
+++ b/lib/query.py
@@ -63,6 +63,7 @@ from ganeti import utils
 from ganeti import compat
 from ganeti import objects
 from ganeti import ht
+from ganeti import qlang
 
 from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER,
                               QFT_UNIT, QFT_TIMESTAMP, QFT_OTHER,
@@ -176,8 +177,330 @@ def GetAllFields(fielddefs):
   return [fdef for (fdef, _, _, _) in fielddefs]
 
 
+class _FilterHints:
+  def __init__(self, namefield):
+    """Initializes this class.
+
+    @type namefield: string
+    @param namefield: Field caller is interested in
+
+    """
+    self._namefield = namefield
+    self._allnames = False
+    self._names = None
+    self._datakinds = set()
+
+  def RequestedNames(self):
+    """Returns all requested values.
+
+    Returns C{None} if list of values can't be determined (e.g. encountered
+    non-equality operators).
+
+    @rtype: list
+
+    """
+    if self._allnames or self._names is None:
+      return None
+
+    return utils.UniqueSequence(self._names)
+
+  def ReferencedData(self):
+    """Returns all kinds of data referenced by the filter.
+
+    """
+    return frozenset(self._datakinds)
+
+  def _NeedAllNames(self):
+    """Changes internal state to request all names.
+
+    """
+    self._allnames = True
+    self._names = None
+
+  def NoteLogicOp(self, op):
+    """Called when handling a logic operation.
+
+    @type op: string
+    @param op: Operator
+
+    """
+    if op != qlang.OP_OR:
+      self._NeedAllNames()
+
+  def NoteUnaryOp(self, op): # pylint: disable-msg=W0613
+    """Called when handling an unary operation.
+
+    @type op: string
+    @param op: Operator
+
+    """
+    self._NeedAllNames()
+
+  def NoteBinaryOp(self, op, datakind, name, value):
+    """Called when handling a binary operation.
+
+    @type op: string
+    @param op: Operator
+    @type name: string
+    @param name: Left-hand side of operator (field name)
+    @param value: Right-hand side of operator
+
+    """
+    if datakind is not None:
+      self._datakinds.add(datakind)
+
+    if self._allnames:
+      return
+
+    # If any operator other than equality was used, all items need to be
+    # retrieved
+    if op == qlang.OP_EQUAL and name == self._namefield:
+      if self._names is None:
+        self._names = []
+      self._names.append(value)
+    else:
+      self._NeedAllNames()
+
+
+def _WrapLogicOp(op_fn, sentences, ctx, item):
+  """Wrapper for logic operator functions.
+
+  """
+  return op_fn(fn(ctx, item) for fn in sentences)
+
+
+def _WrapUnaryOp(op_fn, inner, ctx, item):
+  """Wrapper for unary operator functions.
+
+  """
+  return op_fn(inner(ctx, item))
+
+
+def _WrapBinaryOp(op_fn, retrieval_fn, value, ctx, item):
+  """Wrapper for binary operator functions.
+
+  """
+  return op_fn(retrieval_fn(ctx, item), value)
+
+
+def _WrapNot(fn, lhs, rhs):
+  """Negates the result of a wrapped function.
+
+  """
+  return not fn(lhs, rhs)
+
+
+class _FilterCompiler:
+  """Converts a query filter to a callable usable for filtering.
+
+  """
+  #: How deep filters can be nested
+  _LEVELS_MAX = 10
+
+  (_OPTYPE_LOGIC,
+   _OPTYPE_UNARY,
+   _OPTYPE_BINARY) = range(1, 4)
+
+  _EQUALITY_CHECKS = [
+    (QFF_HOSTNAME,
+     lambda lhs, rhs: utils.MatchNameComponent(rhs, [lhs],
+                                               case_sensitive=False)),
+    (None, operator.eq),
+    ]
+
+  _OPS = {
+    # Logic operators
+    qlang.OP_OR: (_OPTYPE_LOGIC, compat.any),
+    qlang.OP_AND: (_OPTYPE_LOGIC, compat.all),
+
+    # Unary operators
+    qlang.OP_NOT: (_OPTYPE_UNARY, operator.not_),
+
+    # Binary operators
+    qlang.OP_EQUAL: (_OPTYPE_BINARY, _EQUALITY_CHECKS),
+    qlang.OP_NOT_EQUAL:
+      (_OPTYPE_BINARY, [(flags, compat.partial(_WrapNot, fn))
+                        for (flags, fn) in _EQUALITY_CHECKS]),
+    qlang.OP_GLOB: (_OPTYPE_BINARY, NotImplemented),
+    qlang.OP_REGEXP: (_OPTYPE_BINARY, NotImplemented),
+    qlang.OP_CONTAINS: (_OPTYPE_BINARY, [
+      (None, operator.contains),
+      ]),
+    }
+
+  def __init__(self, fields):
+    """Initializes this class.
+
+    @param fields: Field definitions (return value of L{_PrepareFieldList})
+
+    """
+    self._fields = fields
+    self._hints = None
+    self._op_handler = None
+
+  def __call__(self, hints, filter_):
+    """Converts a query filter into a callable function.
+
+    @type hints: L{_FilterHints} or None
+    @param hints: Callbacks doing analysis on filter
+    @type filter_: list
+    @param filter_: Filter structure
+    @rtype: callable
+    @return: Function receiving context and item as parameters, returning
+             boolean as to whether item matches filter
+
+    """
+    self._op_handler = {
+      self._OPTYPE_LOGIC:
+        (self._HandleLogicOp, getattr(hints, "NoteLogicOp", None)),
+      self._OPTYPE_UNARY:
+        (self._HandleUnaryOp, getattr(hints, "NoteUnaryOp", None)),
+      self._OPTYPE_BINARY:
+        (self._HandleBinaryOp, getattr(hints, "NoteBinaryOp", None)),
+      }
+
+    try:
+      filter_fn = self._Compile(filter_, 0)
+    finally:
+      self._op_handler = None
+
+    return filter_fn
+
+  def _Compile(self, filter_, level):
+    """Inner function for converting filters.
+
+    Calls the correct handler functions for the top-level operator. This
+    function is called recursively (e.g. for logic operators).
+
+    """
+    if not (isinstance(filter_, (list, tuple)) and filter_):
+      raise errors.ParameterError("Invalid filter on level %s" % level)
+
+    # Limit recursion
+    if level >= self._LEVELS_MAX:
+      raise errors.ParameterError("Only up to %s levels are allowed (filter"
+                                  " nested too deep)" % self._LEVELS_MAX)
+
+    # Create copy to be modified
+    operands = filter_[:]
+    op = operands.pop(0)
+
+    try:
+      (kind, op_data) = self._OPS[op]
+    except KeyError:
+      raise errors.ParameterError("Unknown operator '%s'" % op)
+
+    (handler, hints_cb) = self._op_handler[kind]
+
+    return handler(hints_cb, level, op, op_data, operands)
+
+  def _HandleLogicOp(self, hints_fn, level, op, op_fn, operands):
+    """Handles logic operators.
+
+    @type hints_fn: callable
+    @param hints_fn: Callback doing some analysis on the filter
+    @type level: integer
+    @param level: Current depth
+    @type op: string
+    @param op: Operator
+    @type op_fn: callable
+    @param op_fn: Function implementing operator
+    @type operands: list
+    @param operands: List of operands
+
+    """
+    if hints_fn:
+      hints_fn(op)
+
+    return compat.partial(_WrapLogicOp, op_fn,
+                          [self._Compile(op, level + 1) for op in operands])
+
+  def _HandleUnaryOp(self, hints_fn, level, op, op_fn, operands):
+    """Handles unary operators.
+
+    @type hints_fn: callable
+    @param hints_fn: Callback doing some analysis on the filter
+    @type level: integer
+    @param level: Current depth
+    @type op: string
+    @param op: Operator
+    @type op_fn: callable
+    @param op_fn: Function implementing operator
+    @type operands: list
+    @param operands: List of operands
+
+    """
+    if hints_fn:
+      hints_fn(op)
+
+    if len(operands) != 1:
+      raise errors.ParameterError("Unary operator '%s' expects exactly one"
+                                  " operand" % op)
+
+    return compat.partial(_WrapUnaryOp, op_fn,
+                          self._Compile(operands[0], level + 1))
+
+  def _HandleBinaryOp(self, hints_fn, level, op, op_data, operands):
+    """Handles binary operators.
+
+    @type hints_fn: callable
+    @param hints_fn: Callback doing some analysis on the filter
+    @type level: integer
+    @param level: Current depth
+    @type op: string
+    @param op: Operator
+    @param op_data: Functions implementing operators
+    @type operands: list
+    @param operands: List of operands
+
+    """
+    # Unused arguments, pylint: disable-msg=W0613
+    try:
+      (name, value) = operands
+    except (ValueError, TypeError):
+      raise errors.ParameterError("Invalid binary operator, expected exactly"
+                                  " two operands")
+
+    try:
+      (fdef, datakind, field_flags, retrieval_fn) = self._fields[name]
+    except KeyError:
+      raise errors.ParameterError("Unknown field '%s'" % name)
+
+    assert fdef.kind != QFT_UNKNOWN
+
+    # TODO: Type conversions?
+
+    verify_fn = _VERIFY_FN[fdef.kind]
+    if not verify_fn(value):
+      raise errors.ParameterError("Unable to compare field '%s' (type '%s')"
+                                  " with '%s', expected %s" %
+                                  (name, fdef.kind, value.__class__.__name__,
+                                   verify_fn))
+
+    if hints_fn:
+      hints_fn(op, datakind, name, value)
+
+    for (fn_flags, fn) in op_data:
+      if fn_flags is None or fn_flags & field_flags:
+        return compat.partial(_WrapBinaryOp, fn, retrieval_fn, value)
+
+    raise errors.ProgrammerError("Unable to find operator implementation"
+                                 " (op '%s', flags %s)" % (op, field_flags))
+
+
+def _CompileFilter(fields, hints, filter_):
+  """Converts a query filter into a callable function.
+
+  See L{_FilterCompiler} for details.
+
+  @rtype: callable
+
+  """
+  return _FilterCompiler(fields)(hints, filter_)
+
+
 class Query:
-  def __init__(self, fieldlist, selected):
+  def __init__(self, fieldlist, selected, filter_=None, namefield=None):
     """Initializes this class.
 
     The field definition is a dictionary with the field's name as a key and a
@@ -196,17 +519,50 @@ class Query:
     @param selected: List of selected fields
 
     """
+    assert namefield is None or namefield in fieldlist
+
     self._fields = _GetQueryFields(fieldlist, selected)
 
+    self._filter_fn = None
+    self._requested_names = None
+    self._filter_datakinds = frozenset()
+
+    if filter_ is not None:
+      # Collect requested names if wanted
+      if namefield:
+        hints = _FilterHints(namefield)
+      else:
+        hints = None
+
+      # Build filter function
+      self._filter_fn = _CompileFilter(fieldlist, hints, filter_)
+      if hints:
+        self._requested_names = hints.RequestedNames()
+        self._filter_datakinds = hints.ReferencedData()
+
+    if namefield is None:
+      self._name_fn = None
+    else:
+      (_, _, _, self._name_fn) = fieldlist[namefield]
+
+  def RequestedNames(self):
+    """Returns all names referenced in the filter.
+
+    If there is no filter or operators are preventing determining the exact
+    names, C{None} is returned.
+
+    """
+    return self._requested_names
+
   def RequestedData(self):
     """Gets requested kinds of data.
 
     @rtype: frozenset
 
     """
-    return frozenset(datakind
-                     for (_, datakind, _, _) in self._fields
-                     if datakind is not None)
+    return (self._filter_datakinds |
+            frozenset(datakind for (_, datakind, _, _) in self._fields
+                      if datakind is not None))
 
   def GetFields(self):
     """Returns the list of fields for this query.
@@ -225,15 +581,36 @@ class Query:
       support iteration using C{__iter__}
 
     """
-    result = [[_ProcessResult(fn(ctx, item)) for (_, _, _, fn) in self._fields]
-              for item in ctx]
+    result = []
+
+    for idx, item in enumerate(ctx):
+      if not (self._filter_fn is None or self._filter_fn(ctx, item)):
+        continue
 
-    # Verify result
-    if __debug__:
-      for row in result:
+      row = [_ProcessResult(fn(ctx, item)) for (_, _, _, fn) in self._fields]
+
+      # Verify result
+      if __debug__:
         _VerifyResultRow(self._fields, row)
 
-    return result
+      if self._name_fn:
+        (status, name) = _ProcessResult(self._name_fn(ctx, item))
+        assert status == constants.RS_NORMAL
+        # TODO: Are there cases where we wouldn't want to use NiceSort?
+        sortname = utils.NiceSortKey(name)
+      else:
+        sortname = None
+
+      result.append((sortname, idx, row))
+
+    # TODO: Would "heapq" be more efficient than sorting?
+
+    # Sorting in-place instead of using "sorted()"
+    result.sort()
+
+    assert not result or (len(result[0]) == 3 and len(result[-1]) == 3)
+
+    return map(operator.itemgetter(2), result)
 
   def OldStyleQuery(self, ctx):
     """Query with "old" query result format.
diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py
index 816dcc6..df2dc55 100755
--- a/test/ganeti.query_unittest.py
+++ b/test/ganeti.query_unittest.py
@@ -959,5 +959,345 @@ class TestQueryFields(unittest.TestCase):
                          [(fdef2.name, fdef2.title) for fdef2 in fields])
 
 
+class TestQueryFilter(unittest.TestCase):
+  def testRequestedNames(self):
+    innerfilter = [["=", "name", "x%s" % i] for i in range(4)]
+
+    for fielddefs in query.ALL_FIELD_LISTS:
+      assert "name" in fielddefs
+
+      # No name field
+      q = query.Query(fielddefs, ["name"], filter_=["=", "name", "abc"],
+                      namefield=None)
+      self.assertEqual(q.RequestedNames(), None)
+
+      # No filter
+      q = query.Query(fielddefs, ["name"], filter_=None, namefield="name")
+      self.assertEqual(q.RequestedNames(), None)
+
+      # Check empty query
+      q = query.Query(fielddefs, ["name"], filter_=["|"], namefield="name")
+      self.assertEqual(q.RequestedNames(), None)
+
+      # Check order
+      q = query.Query(fielddefs, ["name"], filter_=["|"] + innerfilter,
+                      namefield="name")
+      self.assertEqual(q.RequestedNames(), ["x0", "x1", "x2", "x3"])
+
+      # Check reverse order
+      q = query.Query(fielddefs, ["name"],
+                      filter_=["|"] + list(reversed(innerfilter)),
+                      namefield="name")
+      self.assertEqual(q.RequestedNames(), ["x3", "x2", "x1", "x0"])
+
+      # Duplicates
+      q = query.Query(fielddefs, ["name"],
+                      filter_=["|"] + innerfilter + 
list(reversed(innerfilter)),
+                      namefield="name")
+      self.assertEqual(q.RequestedNames(), ["x0", "x1", "x2", "x3"])
+
+      # Unknown name field
+      self.assertRaises(AssertionError, query.Query, fielddefs, ["name"],
+                        namefield="_unknown_field_")
+
+      # Filter with AND
+      q = query.Query(fielddefs, ["name"],
+                      filter_=["|", ["=", "name", "foo"],
+                                    ["&", ["=", "name", ""]]],
+                      namefield="name")
+      self.assertTrue(q.RequestedNames() is None)
+
+      # Filter with NOT
+      q = query.Query(fielddefs, ["name"],
+                      filter_=["|", ["=", "name", "foo"],
+                                    ["!", ["=", "name", ""]]],
+                      namefield="name")
+      self.assertTrue(q.RequestedNames() is None)
+
+      # Filter with only OR (names must be in correct order)
+      q = query.Query(fielddefs, ["name"],
+                      filter_=["|", ["=", "name", "x17361"],
+                                    ["|", ["=", "name", "x22015"]],
+                                    ["|", ["|", ["=", "name", "x13193"]]],
+                                    ["=", "name", "x15215"]],
+                      namefield="name")
+      self.assertEqual(q.RequestedNames(),
+                       ["x17361", "x22015", "x13193", "x15215"])
+
+  @staticmethod
+  def _GenNestedFilter(op, depth):
+    nested = ["=", "name", "value"]
+    for i in range(depth):
+      nested = [op, nested]
+    return nested
+
+  def testCompileFilter(self):
+    levels_max = query._FilterCompiler._LEVELS_MAX
+
+    checks = [
+      [], ["="], ["=", "foo"], ["unknownop"], ["!"],
+      ["=", "_unknown_field", "value"],
+      self._GenNestedFilter("|", levels_max),
+      self._GenNestedFilter("|", levels_max * 3),
+      self._GenNestedFilter("!", levels_max),
+      ]
+
+    for fielddefs in query.ALL_FIELD_LISTS:
+      for filter_ in checks:
+        self.assertRaises(errors.ParameterError, query._CompileFilter,
+                          fielddefs, None, filter_)
+
+      for op in ["|", "!"]:
+        filter_ = self._GenNestedFilter(op, levels_max - 1)
+        self.assertTrue(callable(query._CompileFilter(fielddefs, None,
+                                                      filter_)))
+
+  def testQueryInputOrder(self):
+    fielddefs = query._PrepareFieldList([
+      (query._MakeField("pnode", "PNode", constants.QFT_TEXT, "Primary"),
+       None, 0, lambda ctx, item: item["pnode"]),
+      (query._MakeField("snode", "SNode", constants.QFT_TEXT, "Secondary"),
+       None, 0, lambda ctx, item: item["snode"]),
+      ], [])
+
+    data = [
+      { "pnode": "node1", "snode": "node44", },
+      { "pnode": "node30", "snode": "node90", },
+      { "pnode": "node25", "snode": "node1", },
+      { "pnode": "node20", "snode": "node1", },
+      ]
+
+    filter_ = ["|", ["=", "pnode", "node1"], ["=", "snode", "node1"]]
+
+    q = query.Query(fielddefs, ["pnode", "snode"], namefield="pnode",
+                    filter_=filter_)
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertFalse(q.RequestedData())
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "node44")],
+       [(constants.RS_NORMAL, "node20"), (constants.RS_NORMAL, "node1")],
+       [(constants.RS_NORMAL, "node25"), (constants.RS_NORMAL, "node1")]])
+
+    # Try again with reversed input data
+    self.assertEqual(q.Query(reversed(data)),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "node44")],
+       [(constants.RS_NORMAL, "node20"), (constants.RS_NORMAL, "node1")],
+       [(constants.RS_NORMAL, "node25"), (constants.RS_NORMAL, "node1")]])
+
+    # No name field, result must be in incoming order
+    q = query.Query(fielddefs, ["pnode", "snode"], namefield=None,
+                    filter_=filter_)
+    self.assertFalse(q.RequestedData())
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "node44")],
+       [(constants.RS_NORMAL, "node25"), (constants.RS_NORMAL, "node1")],
+       [(constants.RS_NORMAL, "node20"), (constants.RS_NORMAL, "node1")]])
+    self.assertEqual(q.OldStyleQuery(data), [
+      ["node1", "node44"],
+      ["node25", "node1"],
+      ["node20", "node1"],
+      ])
+    self.assertEqual(q.Query(reversed(data)),
+      [[(constants.RS_NORMAL, "node20"), (constants.RS_NORMAL, "node1")],
+       [(constants.RS_NORMAL, "node25"), (constants.RS_NORMAL, "node1")],
+       [(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "node44")]])
+    self.assertEqual(q.OldStyleQuery(reversed(data)), [
+      ["node20", "node1"],
+      ["node25", "node1"],
+      ["node1", "node44"],
+      ])
+
+  def testFilter(self):
+    (DK_A, DK_B) = range(1000, 1002)
+
+    fielddefs = query._PrepareFieldList([
+      (query._MakeField("name", "Name", constants.QFT_TEXT, "Name"),
+       DK_A, 0, lambda ctx, item: item["name"]),
+      (query._MakeField("other", "Other", constants.QFT_TEXT, "Other"),
+       DK_B, 0, lambda ctx, item: item["other"]),
+      ], [])
+
+    data = [
+      { "name": "node1", "other": "foo", },
+      { "name": "node2", "other": "bar", },
+      { "name": "node3", "other": "Hello", },
+      ]
+
+    # Empty filter
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["|"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.RequestedData(), set([DK_A, DK_B]))
+    self.assertEqual(q.Query(data), [])
+
+    # Normal filter
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["=", "name", "node1"])
+    self.assertEqual(q.RequestedNames(), ["node1"])
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")]])
+
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=(["|", ["=", "name", "node1"],
+                                   ["=", "name", "node3"]]))
+    self.assertEqual(q.RequestedNames(), ["node1", "node3"])
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")],
+       [(constants.RS_NORMAL, "node3"), (constants.RS_NORMAL, "Hello")]])
+
+    # Complex filter
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=(["|", ["=", "name", "node1"],
+                                   ["|", ["=", "name", "node3"],
+                                         ["=", "name", "node2"]],
+                                   ["=", "name", "node3"]]))
+    self.assertEqual(q.RequestedNames(), ["node1", "node3", "node2"])
+    self.assertEqual(q.RequestedData(), set([DK_A, DK_B]))
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")],
+       [(constants.RS_NORMAL, "node2"), (constants.RS_NORMAL, "bar")],
+       [(constants.RS_NORMAL, "node3"), (constants.RS_NORMAL, "Hello")]])
+
+    # Filter data type mismatch
+    for i in [-1, 0, 1, 123, [], None, True, False]:
+      self.assertRaises(errors.ParameterError, query.Query,
+                        fielddefs, ["name", "other"], namefield="name",
+                        filter_=["=", "name", i])
+
+    # Negative filter
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["!", ["|", ["=", "name", "node1"],
+                                        ["=", "name", "node3"]]])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node2"), (constants.RS_NORMAL, "bar")]])
+
+    # Not equal
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["!=", "name", "node3"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+      [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")],
+       [(constants.RS_NORMAL, "node2"), (constants.RS_NORMAL, "bar")]])
+
+    # Data type
+    q = query.Query(fielddefs, [], namefield="name",
+                    filter_=["|", ["=", "other", "bar"],
+                                  ["=", "name", "foo"]])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.RequestedData(), set([DK_A, DK_B]))
+    self.assertEqual(q.Query(data), [[]])
+
+    # Only one data type
+    q = query.Query(fielddefs, ["other"], namefield="name",
+                    filter_=["=", "other", "bar"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.RequestedData(), set([DK_B]))
+    self.assertEqual(q.Query(data), [[(constants.RS_NORMAL, "bar")]])
+
+    q = query.Query(fielddefs, [], namefield="name",
+                    filter_=["=", "other", "bar"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.RequestedData(), set([DK_B]))
+    self.assertEqual(q.Query(data), [[]])
+
+  def testFilterContains(self):
+    fielddefs = query._PrepareFieldList([
+      (query._MakeField("name", "Name", constants.QFT_TEXT, "Name"),
+       None, 0, lambda ctx, item: item["name"]),
+      (query._MakeField("other", "Other", constants.QFT_OTHER, "Other"),
+       None, 0, lambda ctx, item: item["other"]),
+      ], [])
+
+    data = [
+      { "name": "node2", "other": ["x", "y", "bar"], },
+      { "name": "node3", "other": "Hello", },
+      { "name": "node1", "other": ["a", "b", "foo"], },
+      ]
+
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["=[]", "other", "bar"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node2"),
+       (constants.RS_NORMAL, ["x", "y", "bar"])],
+      ])
+
+    q = query.Query(fielddefs, ["name", "other"], namefield="name",
+                    filter_=["|", ["=[]", "other", "bar"],
+                                  ["=[]", "other", "a"],
+                                  ["=[]", "other", "b"]])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node1"),
+       (constants.RS_NORMAL, ["a", "b", "foo"])],
+      [(constants.RS_NORMAL, "node2"),
+       (constants.RS_NORMAL, ["x", "y", "bar"])],
+      ])
+    self.assertEqual(q.OldStyleQuery(data), [
+      ["node1", ["a", "b", "foo"]],
+      ["node2", ["x", "y", "bar"]],
+      ])
+
+  def testFilterHostname(self):
+    fielddefs = query._PrepareFieldList([
+      (query._MakeField("name", "Name", constants.QFT_TEXT, "Name"),
+       None, query.QFF_HOSTNAME, lambda ctx, item: item["name"]),
+      ], [])
+
+    data = [
+      { "name": "node1.example.com", },
+      { "name": "node2.example.com", },
+      { "name": "node2.example.net", },
+      ]
+
+    q = query.Query(fielddefs, ["name"], namefield="name",
+                    filter_=["=", "name", "node2"])
+    self.assertEqual(q.RequestedNames(), ["node2"])
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node2.example.com")],
+      [(constants.RS_NORMAL, "node2.example.net")],
+      ])
+
+    q = query.Query(fielddefs, ["name"], namefield="name",
+                    filter_=["=", "name", "node1"])
+    self.assertEqual(q.RequestedNames(), ["node1"])
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node1.example.com")],
+      ])
+
+    q = query.Query(fielddefs, ["name"], namefield="name",
+                    filter_=["=", "name", "othername"])
+    self.assertEqual(q.RequestedNames(), ["othername"])
+    self.assertEqual(q.Query(data), [])
+
+    q = query.Query(fielddefs, ["name"], namefield="name",
+                    filter_=["|", ["=", "name", "node1.example.com"],
+                                  ["=", "name", "node2"]])
+    self.assertEqual(q.RequestedNames(), ["node1.example.com", "node2"])
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node1.example.com")],
+      [(constants.RS_NORMAL, "node2.example.com")],
+      [(constants.RS_NORMAL, "node2.example.net")],
+      ])
+    self.assertEqual(q.OldStyleQuery(data), [
+      ["node1.example.com"],
+      ["node2.example.com"],
+      ["node2.example.net"],
+      ])
+
+    q = query.Query(fielddefs, ["name"], namefield="name",
+                    filter_=["!=", "name", "node1"])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data), [
+      [(constants.RS_NORMAL, "node2.example.com")],
+      [(constants.RS_NORMAL, "node2.example.net")],
+      ])
+    self.assertEqual(q.OldStyleQuery(data), [
+      ["node2.example.com"],
+      ["node2.example.net"],
+      ])
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
-- 
1.7.3.5

Reply via email to