# Buggy code
def do_batch_update(
    cursor: CursorObj,
    table: str,
    cols: List[str],
    vals: List[str],
    batch_size: int = 10000,
    sleep: float = 0.1,
    escape: bool = True,
) -> None:  # nocoverage
    stmt = """
        UPDATE %s
        SET (%s) = (%s)
        WHERE id >= %%s AND id < %%s
    """ % (table, ", ".join(cols), ", ".join(["%s"] * len(cols)))

    cursor.execute("SELECT MIN(id), MAX(id) FROM %s" % (table,))
    (min_id, max_id) = cursor.fetchall()[0]
    if min_id is None:
        return

    print("\n    Range of rows to update: [%s, %s]" % (min_id, max_id))
    while min_id <= max_id:
        lower = min_id
        upper = min_id + batch_size
        print("    Updating range [%s,%s)" % (lower, upper))
        params = list(vals) + [lower, upper]
        if escape:
            cursor.execute(stmt, params=params)
        else:
            cursor.execute(stmt % tuple(params))

        min_id = upper
        time.sleep(sleep)

        # Once we've finished, check if any new rows were inserted to the table
        if min_id > max_id:
            cursor.execute("SELECT MAX(id) FROM %s" % (table,))
            max_id = cursor.fetchall()[0][0]

    print("    Finishing...", end="")


# Traceback (psycopg2.NotSupportedError)
# sudo -u zulip /home/zulip/deployments/current/manage.py migrate pgroonga
Operations to perform:
Apply all migrations: pgroonga
Running migrations:
Applying pgroonga.0001_enable... OK
Applying pgroonga.0002_html_escape_subject...
Range of rows to update: [1, 74065]
Updating range [1,10001)
Traceback (most recent call last):
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 62, in execute
return self.cursor.execute(sql)
File "/home/zulip/deployments/2018-12-13-17-22-20/zerver/lib/db.py", line 33, in execute
return wrapper_execute(self, super().execute, query, vars)
File "/home/zulip/deployments/2018-12-13-17-22-20/zerver/lib/db.py", line 20, in wrapper_execute
return action(sql, params)
psycopg2.NotSupportedError: source for a multiple-column UPDATE item must be a sub-SELECT or ROW() expression
LINE 3:         SET (search_pgroonga) = (escape_html(subject) || ' '...
^


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

Traceback (most recent call last):
File "/home/zulip/deployments/current/manage.py", line 50, in <module>
execute_from_command_line(sys.argv)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/core/management/__init__.py", line 364, in execute_from_command_line
utility.execute()
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/core/management/__init__.py", line 356, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/core/management/base.py", line 283, in run_from_argv
self.execute(*args, **cmd_options)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/core/management/base.py", line 330, in execute
output = self.handle(*args, **options)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 204, in handle
fake_initial=fake_initial,
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/migrations/executor.py", line 115, in migrate
state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/migrations/executor.py", line 145, in _migrate_all_forwards
state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/migrations/executor.py", line 244, in apply_migration
state = migration.apply(state, schema_editor)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/migrations/migration.py", line 129, in apply
operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/migrations/operations/special.py", line 193, in database_forwards
self.code(from_state.apps, schema_editor)
File "/home/zulip/deployments/2018-12-13-17-22-20/pgroonga/migrations/0002_html_escape_subject.py", line 12, in rebuild_pgroonga_index
escape=False, batch_size=10000)
File "/home/zulip/deployments/2018-12-13-17-22-20/zerver/lib/migrate.py", line 61, in do_batch_update
cursor.execute(stmt % tuple(params))
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 64, in execute
return self.cursor.execute(sql, params)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/utils.py", line 94, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/utils/six.py", line 685, in reraise
raise value.with_traceback(tb)
File "/home/zulip/deployments/2018-12-13-17-22-20/zulip-py3-venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 62, in execute
return self.cursor.execute(sql)
File "/home/zulip/deployments/2018-12-13-17-22-20/zerver/lib/db.py", line 33, in execute
return wrapper_execute(self, super().execute, query, vars)
File "/home/zulip/deployments/2018-12-13-17-22-20/zerver/lib/db.py", line 20, in wrapper_execute
return action(sql, params)
django.db.utils.NotSupportedError: source for a multiple-column UPDATE item must be a sub-SELECT or ROW() expression
LINE 3:         SET (search_pgroonga) = (escape_html(subject) || ' '...
^

# Fixed code
def do_batch_update(
    cursor: CursorObj,
    table: str,
    cols: List[str],
    vals: List[str],
    batch_size: int = 10000,
    sleep: float = 0.1,
    escape: bool = True,
) -> None:  # nocoverage
    stmt = """
        UPDATE %s
        SET (%s) = ROW(%s)
        WHERE id >= %%s AND id < %%s
    """ % (table, ", ".join(cols), ", ".join(["%s"] * len(cols)))

    cursor.execute("SELECT MIN(id), MAX(id) FROM %s" % (table,))
    (min_id, max_id) = cursor.fetchall()[0]
    if min_id is None:
        return

    print("\n    Range of rows to update: [%s, %s]" % (min_id, max_id))
    while min_id <= max_id:
        lower = min_id
        upper = min_id + batch_size
        print("    Updating range [%s,%s)" % (lower, upper))
        params = list(vals) + [lower, upper]
        if escape:
            cursor.execute(stmt, params=params)
        else:
            cursor.execute(stmt % tuple(params))

        min_id = upper
        time.sleep(sleep)

        # Once we've finished, check if any new rows were inserted to the table
        if min_id > max_id:
            cursor.execute("SELECT MAX(id) FROM %s" % (table,))
            max_id = cursor.fetchall()[0][0]

    print("    Finishing...", end="")