docs: def classify_wherex(scope_, fromx, wherex):
    "helper for wherex_to_rowlist. returns [SingleTableCond,...], [CartesianCond,...]"
    exprs = []
    for exp in fromx:
      if isinstance(exp, sqparse2.JoinX):
        # todo: probably just add exp.on_stmt as a CartesianCond. don't write this until tests are ready.
        # todo: do join-on clauses get special scoping w.r.t. column names? check spec.
        raise NotImplementedError('join')
      elif isinstance(exp, basestring):
        exprs.append(exp)
    def test_and(exp):
      return isinstance(exp, sqparse2.BinX) and exp.op.op == 'and'
    def binx_splitter(exp):
      return [exp.left, exp.right]
    exprs += treepath.flatten_tree(test_and, binx_splitter, wherex) if wherex else [] # wherex is None if not given
    single_conds = []
    cartesian_conds = []
    for exp in exprs:
      if isinstance(exp, basestring):
        # note: bare table names need their own case because they don't work with resolve_column
        single_conds.append(SingleTableCond(exp, exp))
      else:
        tables = zip(*map(scope_.resolve_column, names_from_exp(exp)))[0]
        if len(tables) > 1:
          cartesian_conds.append(CartesianCond(exp))
        else:
          single_conds.append(SingleTableCond(tables[0], exp))
    return single_conds, cartesian_conds