# Buggy code
def narrow_parameter(json: str) -> OptionalNarrowListT:
    data = ujson.loads(json)
    if not isinstance(data, list):
        raise ValueError("argument is not a list")
    if len(data) == 0:
        # The "empty narrow" should be None, and not []
        return None

    def convert_term(elem: Union[Dict[str, Any], List[str]]) -> Dict[str, Any]:
        # We have to support a legacy tuple format.
        if isinstance(elem, list):
            if len(elem) != 2 or any(not isinstance(x, str) for x in elem):
                raise ValueError("element is not a string pair")
            return dict(operator=elem[0], operand=elem[1])

        if isinstance(elem, dict):
            # Make sure to sync this list to frontend also when adding a new operator.
            # that supports user IDs. Relevant code is located in static/js/message_fetch.js
            # in handle_operators_supporting_id_based_api function where you will need to update
            # operators_supporting_id, or operators_supporting_ids array.
            operators_supporting_id = ["sender", "group-pm-with", "stream"]
            operators_supporting_ids = ["pm-with"]

            operator = elem.get("operator", "")
            if operator in operators_supporting_id:
                operand_validator = check_string_or_int
            elif operator in operators_supporting_ids:
                operand_validator = check_string_or_int_list
            else:
                operand_validator = check_string

            validator = check_dict(
                [
                    ("operator", check_string),
                    ("operand", operand_validator),
                ]
            )

            error = validator("elem", elem)
            if error:
                raise JsonableError(error)

            # whitelist the fields we care about for now
            return dict(
                operator=elem["operator"],
                operand=elem["operand"],
                negated=elem.get("negated", False),
            )

        raise ValueError("element is not a dictionary")

    return list(map(convert_term, data))


# Traceback (psycopg2.errors.SystemError)
2020-03-30 18:36:06.228 INFO [zr] 172.17.0.1      GET     500  49ms (db: 34ms/3q) /json/messages [streams,search] (iago@zulip.com via website)
2020-03-30 18:36:06.228 ERR  [django.request] Internal Server Error: /json/messages
2020-03-30 18:36:06.229 ERR  [django.server] "GET /json/messages?anchor=newest&amp;num_before=50&amp;num_after=50&amp;narrow=[{%22negated%22:true,%22operator%22:%22streams%22,%22operand%22:%22public%22},{%22negated%22:false,%22operator%22:%22search%22,%22operand%22:%22%22}]&amp;client_gravatar=true HTTP/1.1" 500 49
2020-03-30 18:36:07.830 ERR  [] Traceback (most recent call last):
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1246, in _execute_context
cursor, statement, parameters, context
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute
cursor.execute(statement, parameters)
File "/srv/zulip/zerver/lib/db.py", line 31, in execute
return wrapper_execute(self, super().execute, query, vars)
File "/srv/zulip/zerver/lib/db.py", line 18, in wrapper_execute
return action(sql, params)
psycopg2.errors.SystemError: pgroonga: query_extract_keywords: failed to parse expression: Syntax error: <||>


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/srv/zulip/zerver/lib/rest.py", line 27, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/srv/zulip/zerver/lib/rest.py", line 166, in rest_dispatch
return target_function(request, **kwargs)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/srv/zulip/zerver/decorator.py", line 677, in _wrapped_view_func
return authenticate_log_and_execute_json(request, view_func, *args, **kwargs)
File "/srv/zulip/zerver/decorator.py", line 656, in authenticate_log_and_execute_json
return limited_view_func(request, user_profile, *args, **kwargs)
File "/srv/zulip/zerver/decorator.py", line 796, in wrapped_func
return func(request, *args, **kwargs)
File "/srv/zulip/zerver/lib/request.py", line 368, in _wrapped_view_func
return view_func(request, *args, **kwargs)
File "/srv/zulip/zerver/views/messages.py", line 903, in get_messages_backend
rows = list(sa_conn.execute(query).fetchall())
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 982, in execute
return meth(self, multiparams, params)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1101, in _execute_clauseelement
distilled_params,
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1250, in _execute_context
e, statement, parameters, cursor, context
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1476, in _handle_dbapi_exception
util.raise_from_cause(sqlalchemy_exception, exc_info)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 398, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 152, in reraise
raise value.with_traceback(tb)
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1246, in _execute_context
cursor, statement, parameters, context
File "/srv/zulip-py3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute
cursor.execute(statement, parameters)
File "/srv/zulip/zerver/lib/db.py", line 31, in execute
return wrapper_execute(self, super().execute, query, vars)
File "/srv/zulip/zerver/lib/db.py", line 18, in wrapper_execute
return action(sql, params)
sqlalchemy.exc.OperationalError: (psycopg2.errors.SystemError) pgroonga: query_extract_keywords: failed to parse expression: Syntax error: <||>

[SQL: SELECT /* get_messages */ anon_1.message_id, anon_1.flags, anon_1.subject, anon_1.rendered_content, anon_1.content_matches, anon_1.topic_matches
FROM (SELECT message_id, flags, subject, rendered_content, pgroonga_match_positions_character(rendered_content, pgroonga_query_extract_keywords(escape_html(%(escape_html_1)s))) AS content_matches, pgroonga_match_positions_character(escape_html(subject), pgroonga_query_extract_keywords(escape_html(%(escape_html_1)s))) AS topic_matches
FROM zerver_usermessage JOIN zerver_message ON zerver_usermessage.message_id = zerver_message.id
WHERE user_profile_id = %(param_1)s AND recipient_id NOT IN (%(recipient_id_1)s, %(recipient_id_2)s, %(recipient_id_3)s, %(recipient_id_4)s, %(recipient_id_5)s, %(recipient_id_6)s, %(recipient_id_7)s, %(recipient_id_8)s, %(recipient_id_9)s, %(recipient_id_10)s, %(recipient_id_11)s, %(recipient_id_12)s, %(recipient_id_13)s, %(recipient_id_14)s) AND (search_pgroonga &amp;@~ escape_html(%(escape_html_1)s)) ORDER BY message_id DESC
LIMIT %(param_2)s) AS anon_1 ORDER BY message_id ASC]
[parameters: {'escape_html_1': '', 'param_1': 10, 'recipient_id_1': 18, 'recipient_id_2': 19, 'recipient_id_3': 20, 'recipient_id_4': 21, 'recipient_id_5': 22, 'recipient_id_6': 26, 'recipient_id_7': 27, 'recipient_id_8': 28, 'recipient_id_9': 29, 'recipient_id_10': 30, 'recipient_id_11': 31, 'recipient_id_12': 32, 'recipient_id_13': 33, 'recipient_id_14': 34, 'param_2': 50}]
(Background on this error at: http://sqlalche.me/e/e3q8)

# Fixed code
def narrow_parameter(json: str) -> OptionalNarrowListT:
    data = ujson.loads(json)
    if not isinstance(data, list):
        raise ValueError("argument is not a list")
    if len(data) == 0:
        # The "empty narrow" should be None, and not []
        return None

    def convert_term(elem: Union[Dict[str, Any], List[str]]) -> Dict[str, Any]:
        # We have to support a legacy tuple format.
        if isinstance(elem, list):
            if len(elem) != 2 or any(not isinstance(x, str) for x in elem):
                raise ValueError("element is not a string pair")
            return dict(operator=elem[0], operand=elem[1])

        if isinstance(elem, dict):
            # Make sure to sync this list to frontend also when adding a new operator.
            # that supports user IDs. Relevant code is located in static/js/message_fetch.js
            # in handle_operators_supporting_id_based_api function where you will need to update
            # operators_supporting_id, or operators_supporting_ids array.
            operators_supporting_id = ["sender", "group-pm-with", "stream"]
            operators_supporting_ids = ["pm-with"]
            operators_non_empty_operand = {"search"}

            operator = elem.get("operator", "")
            if operator in operators_supporting_id:
                operand_validator = check_string_or_int
            elif operator in operators_supporting_ids:
                operand_validator = check_string_or_int_list
            elif operator in operators_non_empty_operand:
                operand_validator = check_required_string
            else:
                operand_validator = check_string

            validator = check_dict(
                [
                    ("operator", check_string),
                    ("operand", operand_validator),
                ]
            )

            error = validator("elem", elem)
            if error:
                raise JsonableError(error)

            # whitelist the fields we care about for now
            return dict(
                operator=elem["operator"],
                operand=elem["operand"],
                negated=elem.get("negated", False),
            )

        raise ValueError("element is not a dictionary")

    return list(map(convert_term, data))