def decode_complex_ops(encoded_querystring, operators=None, negation=True):
    """
    Returns a list of (querystring, negate, op) tuples that represent complex operations.
    This function will raise a `ValidationError`s if:
    - the individual querystrings are not wrapped in parentheses
    - the set operators do not match the provided `operators`
    - there is trailing content after the ending querysting
    Ex::
        # unencoded query: (a=1) & (b=2) | ~(c=3)
        >>> s = '%28a%253D1%29%20%26%20%28b%253D2%29%20%7C%20%7E%28c%253D3%29'
        >>> decode_querystring_ops(s)
        [
            ('a=1', False, QuerySet.__and__),
            ('b=2', False, QuerySet.__or__),
            ('c=3', True, None),
        ]
    """
    complex_op_re = COMPLEX_OP_NEG_RE if negation else COMPLEX_OP_RE
    if operators is None:
        operators = COMPLEX_OPERATORS

    # decode into: (a%3D1) & (b%3D2) | ~(c%3D3)
    decoded_querystring = unquote(encoded_querystring)
    matches = [m for m in complex_op_re.finditer(decoded_querystring)]

    if not matches:
        msg = _("Unable to parse querystring. Decoded: '%(decoded)s'.")
        raise SerializerValidationError(msg % {'decoded': decoded_querystring})

    results, errors = [], []
    for match, has_next in lookahead(matches):
        negate, querystring, op = match.groups()

        negate = negate == '~'
        querystring = unquote(querystring)
        op_func = operators.get(op.strip()) if op else None
        if op_func is None and has_next:
            msg = _("Invalid querystring operator. Matched: '%(op)s'.")
            errors.append(msg % {'op': op})

        results.append(ComplexOp(querystring, negate, op_func))

    trailing_chars = decoded_querystring[matches[-1].end():]
    if trailing_chars:
        msg = _("Ending querystring must not have trailing characters. Matched: '%(chars)s'.")
        errors.append(msg % {'chars': trailing_chars})

    if errors:
        raise SerializerValidationError(errors)

    return results