diff --git a/Documentation/ref/ovsdb-server.7.rst b/Documentation/ref/ovsdb-server.7.rst
index 2ed392feed1f..e3a8ccc61399 100644
--- a/Documentation/ref/ovsdb-server.7.rst
+++ b/Documentation/ref/ovsdb-server.7.rst
@@ -414,6 +414,41 @@ The reply is always the same::
     "error": null
     "id": same "id" as request
 
+4.1.17 Schema Conversion
+------------------------
+
+Open vSwitch 2.9 adds a new JSON-RPC request to convert an online database from
+one schema to another.  The request contains the following members::
+
+    "method": "convert"
+    "params": [<database-schema>]
+    "id": <nonnull-json-value>
+
+Upon receipt, the server converts the database named in <database-schema> to
+that schema.  The conversion is atomic, consistent, isolated, and durable.  The
+data in the database must be valid when interpreted under <database-schema>,
+with only one exception: data for tables and columns that do not exist in the
+new schema are ignored.  Columns that exist in <database-schema> but not in the
+database are set to their default values.  All of the new schema's constraints
+apply in full.
+
+If the conversion is successful, the server notifies clients that use the
+``set_db_change_aware`` RPC introduced in Open vSwitch 2.9 and cancels their
+outstanding transactions and monitors.  The server disconnects other clients,
+enabling them to notice the change when they reconnect.  The server sends the
+following reply::
+
+    "result": {}
+    "error": null
+    "id": same "id" as request
+
+If the conversion fails, then the server sends an error reply in the following
+form::
+
+    "result": null
+    "error": [<error>]
+    "id": same "id" as request
+
 5.1 Notation
 ------------
 
diff --git a/Documentation/ref/ovsdb.7.rst b/Documentation/ref/ovsdb.7.rst
index 25a6e5fc36ed..6adef73826e8 100644
--- a/Documentation/ref/ovsdb.7.rst
+++ b/Documentation/ref/ovsdb.7.rst
@@ -367,10 +367,17 @@ active-backup database, first stop the database server or servers, then use
 ``ovsdb-tool convert`` to convert it to the new schema, and then restart the
 database server.
 
+OVSDB also supports online database schema conversion.
+To convert a database online, use ``ovsdb-client convert``.
+The conversion is atomic, consistent, isolated, and durable.  ``ovsdb-server``
+disconnects any clients connected when the conversion takes place (except
+clients that use the ``set_db_change_aware`` Open vSwitch extension RPC).  Upon
+reconnection, clients will discover that the schema has changed.
+
 Schema versions and checksums (see Schemas_ above) can give hints about whether
 a database needs to be converted to a new schema.  If there is any question,
-though, the ``needs-conversion`` command on ``ovsdb-tool`` can provide a
-definitive answer.
+though, the ``needs-conversion`` command on ``ovsdb-tool`` and ``ovsdb-client``
+can provide a definitive answer.
 
 Working with Database History
 -----------------------------
diff --git a/NEWS b/NEWS
index b697e4968072..dfc2fb7728a4 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,8 @@ Post-v2.8.0
      * New high-level documentation in ovsdb(7).
      * New file format documentation for developers in ovsdb(5).
      * Protocol documentation moved from ovsdb-server(1) to ovsdb-server(7).
+     * ovsdb-server now supports online schema conversion via
+       "ovsdb-client convert".
      * ovsdb-server now always hosts a built-in database named _Server.  See
        ovsdb-server(5) for more details.
      * ovsdb-client: New "get-schema-cksum" and "query" commands.
diff --git a/lib/ovsdb-data.c b/lib/ovsdb-data.c
index 87d8effd1d67..69122dc10432 100644
--- a/lib/ovsdb-data.c
+++ b/lib/ovsdb-data.c
@@ -1684,6 +1684,19 @@ ovsdb_datum_from_smap(struct ovsdb_datum *datum, const struct smap *smap)
     ovsdb_datum_sort_unique(datum, OVSDB_TYPE_STRING, OVSDB_TYPE_STRING);
 }
 
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_datum_convert(struct ovsdb_datum *dst,
+                    const struct ovsdb_type *dst_type,
+                    const struct ovsdb_datum *src,
+                    const struct ovsdb_type *src_type)
+{
+    struct json *json = ovsdb_datum_to_json(src, src_type);
+    struct ovsdb_error *error = ovsdb_datum_from_json(dst, dst_type, json,
+                                                      NULL);
+    json_destroy(json);
+    return error;
+}
+
 static uint32_t
 hash_atoms(enum ovsdb_atomic_type type, const union ovsdb_atom *atoms,
            unsigned int n, uint32_t basis)
diff --git a/lib/ovsdb-data.h b/lib/ovsdb-data.h
index c842fe28fc1a..c5a80ee39fd6 100644
--- a/lib/ovsdb-data.h
+++ b/lib/ovsdb-data.h
@@ -192,6 +192,12 @@ void ovsdb_datum_to_bare(const struct ovsdb_datum *,
 
 void ovsdb_datum_from_smap(struct ovsdb_datum *, const struct smap *);
 
+struct ovsdb_error *ovsdb_datum_convert(struct ovsdb_datum *dst,
+                                        const struct ovsdb_type *dst_type,
+                                        const struct ovsdb_datum *src,
+                                        const struct ovsdb_type *src_type)
+    OVS_WARN_UNUSED_RESULT;
+
 /* Comparison. */
 uint32_t ovsdb_datum_hash(const struct ovsdb_datum *,
                           const struct ovsdb_type *, uint32_t basis);
diff --git a/ovsdb/file.c b/ovsdb/file.c
index 4aafb3be8ab4..dadb988d3088 100644
--- a/ovsdb/file.c
+++ b/ovsdb/file.c
@@ -566,22 +566,31 @@ ovsdb_file_txn_annotate(struct json *json, const char *comment)
     return json;
 }
 
-struct ovsdb_error *
-ovsdb_file_commit(struct ovsdb_file *file,
-                  const struct ovsdb_txn *txn, bool durable)
+/* Returns 'txn' transformed into the JSON format that is used in OVSDB files.
+ * (But the caller must use ovsdb_file_txn_annotate() to add the _comment the
+ * _date members.)  If 'txn' doesn't actually change anything, returns NULL */
+static struct json *
+ovsdb_file_txn_to_json(const struct ovsdb_txn *txn)
 {
     struct ovsdb_file_txn ftxn;
-    struct ovsdb_error *error;
 
     ovsdb_file_txn_init(&ftxn);
     ovsdb_txn_for_each_change(txn, ovsdb_file_change_cb, &ftxn);
-    if (!ftxn.json) {
+    return ftxn.json;
+}
+
+struct ovsdb_error *
+ovsdb_file_commit(struct ovsdb_file *file,
+                  const struct ovsdb_txn *txn, bool durable)
+{
+    struct json *txn_json = ovsdb_file_txn_to_json(txn);
+    if (!txn_json) {
         /* Nothing to commit. */
         return NULL;
     }
 
-    error = ovsdb_file_txn_commit(ftxn.json, ovsdb_txn_get_comment(txn),
-                                  durable, file->log);
+    struct ovsdb_error *error = ovsdb_file_txn_commit(
+        txn_json, ovsdb_txn_get_comment(txn), durable, file->log);
     if (error) {
         return error;
     }
@@ -844,3 +853,115 @@ ovsdb_file_txn_commit(struct json *json, const char *comment,
 
     return NULL;
 }
+
+static struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_convert_table(struct ovsdb_txn *txn,
+                    const struct ovsdb_table *src_table,
+                    struct ovsdb_table *dst_table)
+{
+    const struct ovsdb_row *src_row;
+    HMAP_FOR_EACH (src_row, hmap_node, &src_table->rows) {
+        struct ovsdb_row *dst_row = ovsdb_row_create(dst_table);
+        *ovsdb_row_get_uuid_rw(dst_row) = *ovsdb_row_get_uuid(src_row);
+
+        struct shash_node *node;
+        SHASH_FOR_EACH (node, &src_table->schema->columns) {
+            const struct ovsdb_column *src_column = node->data;
+            if (src_column->index == OVSDB_COL_UUID ||
+                src_column->index == OVSDB_COL_VERSION) {
+                continue;
+            }
+
+            const struct ovsdb_column *dst_column
+                = shash_find_data(&dst_table->schema->columns,
+                                  src_column->name);
+            if (!dst_column) {
+                continue;
+            }
+
+            struct ovsdb_error *error = ovsdb_datum_convert(
+                &dst_row->fields[dst_column->index], &dst_column->type,
+                &src_row->fields[src_column->index], &src_column->type);
+            if (error) {
+                ovsdb_row_destroy(dst_row);
+                return error;
+            }
+        }
+
+        ovsdb_txn_row_insert(txn, dst_row);
+    }
+    return NULL;
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_file_convert(const struct ovsdb_file *file,
+                   const struct ovsdb_schema *new_schema)
+{
+    struct ovsdb *new_db = ovsdb_create(ovsdb_schema_clone(new_schema));
+    struct ovsdb_txn *txn = ovsdb_txn_create(new_db);
+    struct ovsdb_error *error = NULL;
+
+    struct shash_node *node;
+    SHASH_FOR_EACH (node, &file->db->tables) {
+        const char *table_name = node->name;
+        const struct ovsdb_table *src_table = node->data;
+        struct ovsdb_table *dst_table = shash_find_data(&new_db->tables,
+                                                        table_name);
+        if (!dst_table) {
+            continue;
+        }
+
+        error = ovsdb_convert_table(txn, src_table, dst_table);
+        if (error) {
+            goto error;
+        }
+    }
+
+    error = ovsdb_txn_start_commit(txn);
+    if (error) {
+        goto error;
+    }
+
+    struct ovsdb_log *new;
+    error = ovsdb_log_replace_start(file->log, &new);
+    if (error) {
+        goto error;
+    }
+
+    /* Write schema. */
+    struct json *schema_json = ovsdb_schema_to_json(new_schema);
+    error = ovsdb_log_write(new, schema_json);
+    json_destroy(schema_json);
+    if (error) {
+        goto error;
+    }
+
+    /* Write data. */
+    struct json *txn_json = ovsdb_file_txn_to_json(txn);
+    if (txn_json) {
+        error = ovsdb_log_write(new, txn_json);
+        json_destroy(txn_json);
+        if (error) {
+            goto error;
+        }
+    }
+
+    error = ovsdb_log_replace_commit(file->log, new);
+    if (error) {
+        goto error;
+    }
+
+    error = ovsdb_txn_finish_commit(txn, true);
+    ovs_assert(!error);         /* Can't happen. */
+
+    ovsdb_replace(file->db, new_db);
+
+    return NULL;
+
+error:
+    ovsdb_destroy(new_db);
+    if (txn) {
+        ovsdb_txn_abort(txn);
+    }
+    return error;
+}
diff --git a/ovsdb/file.h b/ovsdb/file.h
index a9ef0585b261..bc9b32cf6c33 100644
--- a/ovsdb/file.h
+++ b/ovsdb/file.h
@@ -51,4 +51,8 @@ void ovsdb_file_destroy(struct ovsdb_file *);
 
 struct json *ovsdb_file_txn_annotate(struct json *, const char *comment);
 
+struct ovsdb_error *ovsdb_file_convert(const struct ovsdb_file *,
+                                       const struct ovsdb_schema *)
+    OVS_WARN_UNUSED_RESULT;
+
 #endif /* ovsdb/file.h */
diff --git a/ovsdb/jsonrpc-server.c b/ovsdb/jsonrpc-server.c
index d51a56854517..df268cd4eedc 100644
--- a/ovsdb/jsonrpc-server.c
+++ b/ovsdb/jsonrpc-server.c
@@ -82,7 +82,7 @@ static void ovsdb_jsonrpc_session_send(struct ovsdb_jsonrpc_session *,
 /* Triggers. */
 static void ovsdb_jsonrpc_trigger_create(struct ovsdb_jsonrpc_session *,
                                          struct ovsdb *,
-                                         struct json *id, struct json *params);
+                                         struct jsonrpc_msg *request);
 static struct ovsdb_jsonrpc_trigger *ovsdb_jsonrpc_trigger_find(
     struct ovsdb_jsonrpc_session *, const struct json *id, size_t hash);
 static void ovsdb_jsonrpc_trigger_complete(struct ovsdb_jsonrpc_trigger *);
@@ -936,17 +936,6 @@ ovsdb_jsonrpc_session_unlock(struct ovsdb_jsonrpc_session *s,
 }
 
 static struct jsonrpc_msg *
-execute_transaction(struct ovsdb_jsonrpc_session *s, struct ovsdb *db,
-                    struct jsonrpc_msg *request)
-{
-    ovsdb_jsonrpc_trigger_create(s, db, request->id, request->params);
-    request->id = NULL;
-    request->params = NULL;
-    jsonrpc_msg_destroy(request);
-    return NULL;
-}
-
-static struct jsonrpc_msg *
 ovsdb_jsonrpc_session_set_db_change_aware(struct ovsdb_jsonrpc_session *s,
                                           const struct jsonrpc_msg *request)
 {
@@ -967,10 +956,11 @@ ovsdb_jsonrpc_session_got_request(struct ovsdb_jsonrpc_session *s,
 {
     struct jsonrpc_msg *reply;
 
-    if (!strcmp(request->method, "transact")) {
+    if (!strcmp(request->method, "transact") ||
+        !strcmp(request->method, "convert")) {
         struct ovsdb *db = ovsdb_jsonrpc_lookup_db(s, request, &reply);
         if (!reply) {
-            reply = execute_transaction(s, db, request);
+            ovsdb_jsonrpc_trigger_create(s, db, request);
         }
     } else if (!strcmp(request->method, "monitor") ||
                (monitor_cond_enable__ && !strcmp(request->method,
@@ -1082,37 +1072,35 @@ struct ovsdb_jsonrpc_trigger {
 
 static void
 ovsdb_jsonrpc_trigger_create(struct ovsdb_jsonrpc_session *s, struct ovsdb *db,
-                             struct json *id, struct json *params)
+                             struct jsonrpc_msg *request)
 {
-    struct ovsdb_jsonrpc_trigger *t;
-    size_t hash;
-
     /* Check for duplicate ID. */
-    hash = json_hash(id, 0);
-    t = ovsdb_jsonrpc_trigger_find(s, id, hash);
+    size_t hash = json_hash(request->id, 0);
+    struct ovsdb_jsonrpc_trigger *t
+        = ovsdb_jsonrpc_trigger_find(s, request->id, hash);
     if (t) {
-        struct jsonrpc_msg *msg;
-
-        msg = jsonrpc_create_error(json_string_create("duplicate request ID"),
-                                   id);
-        ovsdb_jsonrpc_session_send(s, msg);
-        json_destroy(id);
-        json_destroy(params);
+        ovsdb_jsonrpc_session_send(
+            s, syntax_error_reply(request, "duplicate request ID"));
+        jsonrpc_msg_destroy(request);
         return;
     }
 
     /* Insert into trigger table. */
     t = xmalloc(sizeof *t);
-    ovsdb_trigger_init(&s->up, db, &t->trigger, params, time_msec(),
-                       s->read_only, s->remote->role,
-                       jsonrpc_session_get_id(s->js));
-    t->id = id;
+    bool disconnect_all = ovsdb_trigger_init(
+        &s->up, db, &t->trigger, request, time_msec(), s->read_only,
+        s->remote->role, jsonrpc_session_get_id(s->js));
+    t->id = json_clone(request->id);
     hmap_insert(&s->triggers, &t->hmap_node, hash);
 
     /* Complete early if possible. */
     if (ovsdb_trigger_is_complete(&t->trigger)) {
         ovsdb_jsonrpc_trigger_complete(t);
     }
+
+    if (disconnect_all) {
+        ovsdb_jsonrpc_server_reconnect(s->remote->server, false);
+    }
 }
 
 static struct ovsdb_jsonrpc_trigger *
@@ -1139,12 +1127,9 @@ ovsdb_jsonrpc_trigger_complete(struct ovsdb_jsonrpc_trigger *t)
 
     if (jsonrpc_session_is_connected(s->js)) {
         struct jsonrpc_msg *reply;
-        struct json *result;
 
-        result = ovsdb_trigger_steal_result(&t->trigger);
-        if (result) {
-            reply = jsonrpc_create_reply(result, t->id);
-        } else {
+        reply = ovsdb_trigger_steal_reply(&t->trigger);
+        if (!reply) {
             reply = jsonrpc_create_error(json_string_create("canceled"),
                                          t->id);
         }
@@ -1159,7 +1144,7 @@ ovsdb_jsonrpc_trigger_complete(struct ovsdb_jsonrpc_trigger *t)
 
 static void
 ovsdb_jsonrpc_trigger_remove__(struct ovsdb_jsonrpc_session *s,
-                                   struct ovsdb *db)
+                               struct ovsdb *db)
 {
     struct ovsdb_jsonrpc_trigger *t, *next;
     HMAP_FOR_EACH_SAFE (t, next, hmap_node, &s->triggers) {
@@ -1189,11 +1174,9 @@ ovsdb_jsonrpc_trigger_complete_all(struct ovsdb_jsonrpc_session *s)
 static void
 ovsdb_jsonrpc_trigger_complete_done(struct ovsdb_jsonrpc_session *s)
 {
-    while (!ovs_list_is_empty(&s->up.completions)) {
-        struct ovsdb_jsonrpc_trigger *t
-            = CONTAINER_OF(s->up.completions.next,
-                           struct ovsdb_jsonrpc_trigger, trigger.node);
-        ovsdb_jsonrpc_trigger_complete(t);
+    struct ovsdb_jsonrpc_trigger *trigger, *next;
+    LIST_FOR_EACH_SAFE (trigger, next, trigger.node, &s->up.completions) {
+        ovsdb_jsonrpc_trigger_complete(trigger);
     }
 }
 
@@ -1441,7 +1424,7 @@ ovsdb_jsonrpc_monitor_create(struct ovsdb_jsonrpc_session *s, struct ovsdb *db,
 
 error:
     if (m) {
-        ovsdb_jsonrpc_monitor_destroy(m);
+        ovsdb_jsonrpc_monitor_destroy(m, false);
     }
 
     return jsonrpc_create_error(ovsdb_error_to_json_free(error), request_id);
@@ -1598,32 +1581,12 @@ ovsdb_jsonrpc_monitor_cancel(struct ovsdb_jsonrpc_session *s,
             return jsonrpc_create_error(json_string_create("unknown monitor"),
                                         request_id);
         } else {
-            ovsdb_jsonrpc_monitor_destroy(m);
+            ovsdb_jsonrpc_monitor_destroy(m, false);
             return jsonrpc_create_reply(json_object_create(), request_id);
         }
     }
 }
 
-static void
-ovsdb_jsonrpc_monitor_remove__(struct ovsdb_jsonrpc_session *s,
-                               struct ovsdb *db)
-{
-    struct ovsdb_jsonrpc_monitor *m, *next;
-
-    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
-        if (!db || m->db == db) {
-            if (db && jsonrpc_session_is_connected(s->js)
-                && s->db_change_aware) {
-                struct jsonrpc_msg *notify = jsonrpc_create_notify(
-                    "monitor_canceled",
-                    json_array_create_1(json_clone(m->monitor_id)));
-                ovsdb_jsonrpc_session_send(s, notify);
-            }
-            ovsdb_jsonrpc_monitor_destroy(m);
-        }
-    }
-}
-
 /* Database 'db' is about to be removed from the database server.  To prepare,
  * this function removes all references from monitors in 's' to 'db'. */
 static void
@@ -1631,14 +1594,24 @@ ovsdb_jsonrpc_monitor_preremove_db(struct ovsdb_jsonrpc_session *s,
                                    struct ovsdb *db)
 {
     ovs_assert(db);
-    ovsdb_jsonrpc_monitor_remove__(s, db);
+
+    struct ovsdb_jsonrpc_monitor *m, *next;
+    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
+        if (m->db == db) {
+            ovsdb_jsonrpc_monitor_destroy(m, true);
+        }
+    }
 }
 
 /* Cancels all monitors in 's'. */
 static void
 ovsdb_jsonrpc_monitor_remove_all(struct ovsdb_jsonrpc_session *s)
 {
-    ovsdb_jsonrpc_monitor_remove__(s, NULL);
+    struct ovsdb_jsonrpc_monitor *m, *next;
+
+    HMAP_FOR_EACH_SAFE (m, next, node, &s->monitors) {
+        ovsdb_jsonrpc_monitor_destroy(m, false);
+    }
 }
 
 static struct json *
@@ -1669,8 +1642,19 @@ ovsdb_jsonrpc_monitor_needs_flush(struct ovsdb_jsonrpc_session *s)
 }
 
 void
-ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *m)
-{
+ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *m,
+                              bool notify_cancellation)
+{
+    if (notify_cancellation) {
+        struct ovsdb_jsonrpc_session *s = m->session;
+        if (jsonrpc_session_is_connected(s->js) && s->db_change_aware) {
+            struct jsonrpc_msg *notify = jsonrpc_create_notify(
+                "monitor_canceled",
+                json_array_create_1(json_clone(m->monitor_id)));
+            ovsdb_jsonrpc_session_send(s, notify);
+        }
+    }
+
     json_destroy(m->monitor_id);
     hmap_remove(&m->session->monitors, &m->node);
     ovsdb_monitor_remove_jsonrpc_monitor(m->dbmon, m, m->unflushed);
diff --git a/ovsdb/jsonrpc-server.h b/ovsdb/jsonrpc-server.h
index 50a8b879c5a9..0fc16f21b2d9 100644
--- a/ovsdb/jsonrpc-server.h
+++ b/ovsdb/jsonrpc-server.h
@@ -80,7 +80,8 @@ const struct uuid *ovsdb_jsonrpc_server_get_uuid(
     const struct ovsdb_jsonrpc_server *);
 
 struct ovsdb_jsonrpc_monitor;
-void ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *);
+void ovsdb_jsonrpc_monitor_destroy(struct ovsdb_jsonrpc_monitor *,
+                                   bool notify_cancellation);
 void ovsdb_jsonrpc_disable_monitor_cond(void);
 
 #endif /* ovsdb/jsonrpc-server.h */
diff --git a/ovsdb/monitor.c b/ovsdb/monitor.c
index 3e58c3fbd274..97706932614c 100644
--- a/ovsdb/monitor.c
+++ b/ovsdb/monitor.c
@@ -1613,7 +1613,7 @@ ovsdb_monitors_remove(struct ovsdb *db)
          * end monitor will also destroy the corresponding 'ovsdb_monitor'.
          * ovsdb monitor will also be destroied.  */
         LIST_FOR_EACH_SAFE (jm, next_jm, node, &m->jsonrpc_monitors) {
-            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor);
+            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor, false);
         }
     }
 }
@@ -1630,3 +1630,20 @@ ovsdb_monitor_get_memory_usage(struct simap *usage)
         simap_increase(usage, "json-caches", hmap_count(&dbmon->json_cache));
     }
 }
+
+void
+ovsdb_monitor_prereplace_db(struct ovsdb *db)
+{
+    struct ovsdb_monitor *m, *next_m;
+
+    LIST_FOR_EACH_SAFE (m, next_m, list_node, &db->monitors) {
+        struct jsonrpc_monitor_node *jm, *next_jm;
+
+        /* Delete all front end monitors. Removing the last front
+         * end monitor will also destroy the corresponding 'ovsdb_monitor'.
+         * ovsdb monitor will also be destroied.  */
+        LIST_FOR_EACH_SAFE (jm, next_jm, node, &m->jsonrpc_monitors) {
+            ovsdb_jsonrpc_monitor_destroy(jm->jsonrpc_monitor, true);
+        }
+    }
+}
diff --git a/ovsdb/monitor.h b/ovsdb/monitor.h
index 99d43c45dff9..eb3ff270c9f3 100644
--- a/ovsdb/monitor.h
+++ b/ovsdb/monitor.h
@@ -49,6 +49,8 @@ struct ovsdb_monitor *ovsdb_monitor_create(struct ovsdb *db,
 void ovsdb_monitors_remove(struct ovsdb *);
 void ovsdb_monitors_commit(struct ovsdb *, const struct ovsdb_txn *);
 
+void ovsdb_monitor_prereplace_db(struct ovsdb *);
+
 struct ovsdb_monitor *ovsdb_monitor_add(struct ovsdb_monitor *dbmon);
 
 void ovsdb_monitor_add_jsonrpc_monitor(struct ovsdb_monitor *dbmon,
diff --git a/ovsdb/ovsdb-client.1.in b/ovsdb/ovsdb-client.1.in
index 5dbd49f25263..56d4797e933c 100644
--- a/ovsdb/ovsdb-client.1.in
+++ b/ovsdb/ovsdb-client.1.in
@@ -22,6 +22,9 @@ ovsdb\-client \- command-line interface to \fBovsdb-server\fR(1)
 .br
 \fBovsdb\-client\fR [\fIoptions\fR] \fBlist\-columns\fR [\fIserver\fR] [\fIdatabase\fR] [\fItable\fR]
 .IP "Database Version Management Commands:"
+\fBovsdb\-client \fR[\fIoptions\fR] \fBconvert \fR[\fIserver\fR] \fIschema\fR
+.br
+\fBovsdb\-client \fR[\fIoptions\fR] \fBneeds\-conversion \fR[\fIserver\fR] \fIschema\fR
 .br
 \fBovsdb\-client\fR [\fIoptions\fR] \fBget\-schema\-version\fR [\fIserver\fR] [\fIdatabase\fR]
 .IP "Data Management Commands:"
@@ -117,7 +120,43 @@ listed; otherwise, the tables include columns in all tables.
 These commands work with different versions of OVSDB schemas and
 databases.
 .
-.IP "\fBget\-schema\-version\fR [\fIserver\fR] [\fIdatabase\fR]"
+.IP "\fBconvert \fR[\fIserver\fR] \fIschema\fR"
+Reads an OVSDB schema in JSON format, as specified in the OVSDB
+specification, from \fIschema\fR, then connects to \fIserver\fR and
+requests the server to convert the database whose name is specified in
+\fIschema\fR to the schema also specified in \fIschema\fR.
+.IP
+The conversion is atomic, consistent, isolated, and durable.
+Following the schema change, the server notifies clients that use the
+\fBset_db_change_aware\fR RPC introduced in Open vSwitch 2.9 and
+cancels their outstanding transactions and monitors.  The server
+disconnects other clients, enabling them to notice the change when
+they reconnect.
+.IP
+This command can do simple ``upgrades'' and ``downgrades'' on a
+database's schema.  The data in the database must be valid when
+interpreted under \fIschema\fR, with only one exception: data for
+tables and columns that do not exist in \fIschema\fR are ignored.
+Columns that exist in \fIschema\fR but not in the database are set to
+their default values.  All of \fIschema\fR's constraints apply in
+full.
+.IP
+Some uses of this command can cause unrecoverable data loss.  For
+example, converting a database from a schema that has a given column
+or table to one that does not will delete all data in that column or
+table.  Back up critical databases before converting them.
+.IP
+This command works with clustered and standalone databases.
+Standalone databases may also be converted (offline) with
+\fBovsdb\-tool\fR's \fBconvert\fR command.
+.
+.IP "\fBneeds\-conversion \fR[\fIserver\fR] \fIschema\fR"
+Reads the schema from \fIschema\fR, then connects to \fIserver\fR and
+requests the schema from the database whose name is specified in
+\fIschema\fR.  If the two schemas are the same, prints \fBno\fR on
+stdout; if they differ, prints \fByes\fR.
+.
+.IP "\fBget\-schema\-version \fR[\fIserver\fR] [\fIdatabase\fR]"
 Connects to \fIserver\fR, retrieves the schema for \fIdatabase\fR, and
 prints its version number on stdout.
 If \fIdatabase\fR was created before schema versioning was introduced,
diff --git a/ovsdb/ovsdb-client.c b/ovsdb/ovsdb-client.c
index 600c5070db78..a7cab600c98b 100644
--- a/ovsdb/ovsdb-client.c
+++ b/ovsdb/ovsdb-client.c
@@ -77,9 +77,14 @@ static bool timestamp;
 /* --db-change-aware, --no-db-change-aware: Enable db_change_aware feature for
  * "monitor" command?
  *
- * (This option is undocumented because it is expected to be useful only for
- * testing that the db_change_aware feature actually works.) */
-static int db_change_aware;
+ * -1 (default): Use db_change_aware if available.
+ * 0: Disable db_change_aware.
+ * 1: Require db_change_aware.
+ *
+ * (This option is undocumented because anything other than the default is
+ * expected to be useful only for testing that the db_change_aware feature
+ * actually works.) */
+static int db_change_aware = -1;
 
 /* --force: Ignore schema differences for "restore" command? */
 static bool force;
@@ -303,6 +308,8 @@ usage(void)
            "    DATABASE on SERVER.\n"
            "    COLUMNs may include !initial, !insert, !delete, !modify\n"
            "    to avoid seeing the specified kinds of changes.\n"
+           "\n  convert [SERVER] SCHEMA\n"
+           "    convert database on SERVER named in SCHEMA to SCHEMA.\n"
            "\n  monitor [SERVER] [DATABASE] ALL\n"
            "    monitor all changes to all columns in all tables\n"
            "    in DATBASE on SERVER.\n"
@@ -557,11 +564,40 @@ do_list_columns(struct jsonrpc *rpc, const char *database,
     table_destroy(&t);
 }
 
+static void
+send_db_change_aware(struct jsonrpc *rpc)
+{
+    if (db_change_aware != 0) {
+        struct jsonrpc_msg *request = jsonrpc_create_request(
+            "set_db_change_aware",
+            json_array_create_1(json_boolean_create(true)),
+            NULL);
+        struct jsonrpc_msg *reply;
+        int error = jsonrpc_transact_block(rpc, request, &reply);
+        if (error) {
+            ovs_fatal(error, "%s: error setting db_change_aware",
+                      jsonrpc_get_name(rpc));
+        }
+        if (reply->type == JSONRPC_ERROR && db_change_aware == 1) {
+            ovs_fatal(0, "%s: set_db_change_aware failed (%s)",
+                      jsonrpc_get_name(rpc), json_to_string(reply->error, 0));
+        }
+        jsonrpc_msg_destroy(reply);
+    }
+}
+
 static struct json *
 do_transact__(struct jsonrpc *rpc, struct json *transaction)
 {
     struct jsonrpc_msg *request, *reply;
 
+    if (db_change_aware == 1) {
+        send_db_change_aware(rpc);
+    }
+    daemon_save_fd(STDOUT_FILENO);
+    daemon_save_fd(STDERR_FILENO);
+    daemonize();
+
     request = jsonrpc_create_request("transact", transaction, NULL);
     check_txn(jsonrpc_transact_block(rpc, request, &reply), &reply);
     struct json *result = json_clone(reply->result);
@@ -1040,6 +1076,7 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
     ovs_assert(version < OVSDB_MONITOR_VERSION_MAX);
 
     daemon_save_fd(STDOUT_FILENO);
+    daemon_save_fd(STDERR_FILENO);
     daemonize_start(false);
     if (get_detach()) {
         int error;
@@ -1097,22 +1134,7 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
         free(nodes);
     }
 
-    if (db_change_aware) {
-        struct jsonrpc_msg *request = jsonrpc_create_request(
-            "set_db_change_aware",
-            json_array_create_1(json_boolean_create(true)),
-            NULL);
-        struct jsonrpc_msg *reply;
-        int error = jsonrpc_transact_block(rpc, request, &reply);
-        if (error) {
-            ovs_fatal(error, "%s: error setting db_change_aware", server);
-        }
-        if (reply->type == JSONRPC_ERROR) {
-            ovs_fatal(0, "%s: set_db_change_aware failed (%s)",
-                      server, json_to_string(reply->error, 0));
-        }
-        jsonrpc_msg_destroy(reply);
-    }
+    send_db_change_aware(rpc);
 
     monitor = json_array_create_3(json_string_create(database),
                                   json_null_create(), monitor_requests);
@@ -1174,6 +1196,10 @@ do_monitor__(struct jsonrpc *rpc, const char *database,
                     monitor2_print(params->u.array.elems[1], mts, n_mts);
                     fflush(stdout);
                 }
+            } else if (msg->type == JSONRPC_NOTIFY
+                       && !strcmp(msg->method, "monitor_canceled")) {
+                ovs_fatal(0, "%s: %s database was removed",
+                          server, database);
             }
             jsonrpc_msg_destroy(msg);
         }
@@ -1229,6 +1255,35 @@ do_monitor_cond(struct jsonrpc *rpc, const char *database,
     ovsdb_schema_destroy(schema);
 }
 
+static void
+do_convert(struct jsonrpc *rpc, const char *database OVS_UNUSED,
+           int argc OVS_UNUSED, char *argv[])
+{
+    struct ovsdb_schema *new_schema;
+    check_ovsdb_error(ovsdb_schema_from_file(argv[0], &new_schema));
+
+    struct jsonrpc_msg *request, *reply;
+    request = jsonrpc_create_request(
+        "convert",
+        json_array_create_2(json_string_create(new_schema->name),
+                            ovsdb_schema_to_json(new_schema)), NULL);
+    check_txn(jsonrpc_transact_block(rpc, request, &reply), &reply);
+    jsonrpc_msg_destroy(reply);
+}
+
+static void
+do_needs_conversion(struct jsonrpc *rpc, const char *database OVS_UNUSED,
+                    int argc OVS_UNUSED, char *argv[])
+{
+    struct ovsdb_schema *schema1;
+    check_ovsdb_error(ovsdb_schema_from_file(argv[0], &schema1));
+
+    struct ovsdb_schema *schema2 = fetch_schema(rpc, schema1->name);
+    puts(ovsdb_schema_equal(schema1, schema2) ? "no" : "yes");
+    ovsdb_schema_destroy(schema1);
+    ovsdb_schema_destroy(schema2);
+}
+
 struct dump_table_aux {
     struct ovsdb_datum **data;
     const struct ovsdb_column **columns;
@@ -1910,6 +1965,8 @@ static const struct ovsdb_client_command all_commands[] = {
     { "query",              NEED_RPC,      1, 1,       do_query },
     { "monitor",            NEED_DATABASE, 1, INT_MAX, do_monitor },
     { "monitor-cond",       NEED_DATABASE, 2, 3,       do_monitor_cond },
+    { "convert",            NEED_RPC,      1, 1,       do_convert },
+    { "needs-conversion",   NEED_RPC,      1, 1,       do_needs_conversion },
     { "dump",               NEED_DATABASE, 0, INT_MAX, do_dump },
     { "backup",             NEED_DATABASE, 0, 0,       do_backup },
     { "restore",            NEED_DATABASE, 0, 0,       do_restore },
diff --git a/ovsdb/ovsdb-server.c b/ovsdb/ovsdb-server.c
index 1e36b27958f8..f7bf1e270120 100644
--- a/ovsdb/ovsdb-server.c
+++ b/ovsdb/ovsdb-server.c
@@ -111,10 +111,11 @@ static char *open_db(struct server_config *config, const char *filename);
 static void add_server_db(struct server_config *);
 static void close_db(struct db *db);
 
-static void parse_options(int *argc, char **argvp[],
-                          struct sset *remotes, char **unixctl_pathp,
-                          char **run_command, char **sync_from,
-                          char **sync_exclude, bool *is_backup);
+static void parse_options(int argc, char *argvp[],
+                          struct sset *db_filenames, struct sset *remotes,
+                          char **unixctl_pathp, char **run_command,
+                          char **sync_from, char **sync_exclude,
+                          bool *is_backup);
 OVS_NO_RETURN static void usage(void);
 
 static char *reconfigure_remotes(struct ovsdb_jsonrpc_server *,
@@ -202,7 +203,9 @@ main_loop(struct ovsdb_jsonrpc_server *jsonrpc, struct shash *all_dbs,
 
         SHASH_FOR_EACH(node, all_dbs) {
             struct db *db = node->data;
-            ovsdb_trigger_run(db->db, time_msec());
+            if (ovsdb_trigger_run(db->db, time_msec())) {
+                ovsdb_jsonrpc_server_reconnect(jsonrpc, false);
+            }
         }
         if (run_process) {
             process_run();
@@ -265,7 +268,6 @@ main(int argc, char *argv[])
     struct shash all_dbs;
     struct shash_node *node, *next;
     char *error;
-    int i;
 
     ovs_cmdl_proctitle_init(argc, argv);
     set_program_name(argv[0]);
@@ -274,8 +276,8 @@ main(int argc, char *argv[])
     process_init();
 
     bool active = false;
-    parse_options(&argc, &argv, &remotes, &unixctl_path, &run_command,
-                  &sync_from, &sync_exclude, &active);
+    parse_options(argc, argv, &db_filenames, &remotes, &unixctl_path,
+                  &run_command, &sync_from, &sync_exclude, &active);
     is_backup = sync_from && !active;
 
     daemon_become_new_user(false);
@@ -290,17 +292,6 @@ main(int argc, char *argv[])
         ovs_fatal(errno, "failed to create temporary file");
     }
 
-    sset_init(&db_filenames);
-    if (argc > 0) {
-        for (i = 0; i < argc; i++) {
-            sset_add(&db_filenames, argv[i]);
-         }
-    } else {
-        char *default_db = xasprintf("%s/conf.db", ovs_dbdir());
-        sset_add(&db_filenames, default_db);
-        free(default_db);
-    }
-
     server_config.remotes = &remotes;
     server_config.config_tmpfile = config_tmpfile;
 
@@ -1477,8 +1468,9 @@ ovsdb_server_get_sync_status(struct unixctl_conn *conn, int argc OVS_UNUSED,
 }
 
 static void
-parse_options(int *argcp, char **argvp[],
-              struct sset *remotes, char **unixctl_pathp, char **run_command,
+parse_options(int argc, char *argv[],
+              struct sset *db_filenames, struct sset *remotes,
+              char **unixctl_pathp, char **run_command,
               char **sync_from, char **sync_exclude, bool *active)
 {
     enum {
@@ -1490,10 +1482,12 @@ parse_options(int *argcp, char **argvp[],
         OPT_SYNC_FROM,
         OPT_SYNC_EXCLUDE,
         OPT_ACTIVE,
+        OPT_NO_DBS,
         VLOG_OPTION_ENUMS,
         DAEMON_OPTION_ENUMS,
         SSL_OPTION_ENUMS,
     };
+
     static const struct option long_options[] = {
         {"remote",      required_argument, NULL, OPT_REMOTE},
         {"unixctl",     required_argument, NULL, OPT_UNIXCTL},
@@ -1510,14 +1504,15 @@ parse_options(int *argcp, char **argvp[],
         {"sync-from",   required_argument, NULL, OPT_SYNC_FROM},
         {"sync-exclude-tables", required_argument, NULL, OPT_SYNC_EXCLUDE},
         {"active", no_argument, NULL, OPT_ACTIVE},
+        {"no-dbs", no_argument, NULL, OPT_NO_DBS},
         {NULL, 0, NULL, 0},
     };
     char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
-    int argc = *argcp;
-    char **argv = *argvp;
+    bool add_default_db = true;
 
     *sync_from = NULL;
     *sync_exclude = NULL;
+    sset_init(db_filenames);
     sset_init(remotes);
     for (;;) {
         int c;
@@ -1596,6 +1591,10 @@ parse_options(int *argcp, char **argvp[],
             *active = true;
             break;
 
+        case OPT_NO_DBS:
+            add_default_db = false;
+            break;
+
         case '?':
             exit(EXIT_FAILURE);
 
@@ -1605,8 +1604,15 @@ parse_options(int *argcp, char **argvp[],
     }
     free(short_options);
 
-    *argcp -= optind;
-    *argvp += optind;
+    argc -= optind;
+    argv += optind;
+    if (argc > 0) {
+        for (int i = 0; i < argc; i++) {
+            sset_add(db_filenames, argv[i]);
+        }
+    } else if (add_default_db) {
+        sset_add_and_free(db_filenames, xasprintf("%s/conf.db", ovs_dbdir()));
+    }
 }
 
 static void
diff --git a/ovsdb/ovsdb.c b/ovsdb/ovsdb.c
index 19755e673861..89f530bcccfb 100644
--- a/ovsdb/ovsdb.c
+++ b/ovsdb/ovsdb.c
@@ -27,6 +27,7 @@
 #include "simap.h"
 #include "table.h"
 #include "transaction.h"
+#include "trigger.h"
 
 struct ovsdb_schema *
 ovsdb_schema_create(const char *name, const char *version, const char *cksum)
@@ -162,7 +163,7 @@ root_set_size(const struct ovsdb_schema *schema)
 }
 
 struct ovsdb_error *
-ovsdb_schema_from_json(struct json *json, struct ovsdb_schema **schemap)
+ovsdb_schema_from_json(const struct json *json, struct ovsdb_schema **schemap)
 {
     struct ovsdb_schema *schema;
     const struct json *name, *tables, *version_json, *cksum;
@@ -361,6 +362,29 @@ ovsdb_create(struct ovsdb_schema *schema)
 }
 
 void
+ovsdb_replace(struct ovsdb *dst, struct ovsdb *src)
+{
+    /* Cancel monitors. */
+    ovsdb_monitor_prereplace_db(dst);
+
+    /* Cancel triggers. */
+    struct ovsdb_trigger *trigger, *next;
+    LIST_FOR_EACH_SAFE (trigger, next, node, &dst->triggers) {
+        ovsdb_trigger_prereplace_db(trigger);
+    }
+
+    struct ovsdb_schema *tmp_schema = dst->schema;
+    dst->schema = src->schema;
+    src->schema = tmp_schema;
+
+    shash_swap(&dst->tables, &src->tables);
+
+    dst->rbac_role = ovsdb_get_table(dst, "RBAC_Role");
+
+    ovsdb_destroy(src);
+}
+
+void
 ovsdb_destroy(struct ovsdb *db)
 {
     if (db) {
diff --git a/ovsdb/ovsdb.h b/ovsdb/ovsdb.h
index 9d915f0f15ae..c3e8f2091e35 100644
--- a/ovsdb/ovsdb.h
+++ b/ovsdb/ovsdb.h
@@ -45,7 +45,7 @@ void ovsdb_schema_destroy(struct ovsdb_schema *);
 struct ovsdb_error *ovsdb_schema_from_file(const char *file_name,
                                            struct ovsdb_schema **)
     OVS_WARN_UNUSED_RESULT;
-struct ovsdb_error *ovsdb_schema_from_json(struct json *,
+struct ovsdb_error *ovsdb_schema_from_json(const struct json *,
                                            struct ovsdb_schema **)
     OVS_WARN_UNUSED_RESULT;
 struct json *ovsdb_schema_to_json(const struct ovsdb_schema *);
@@ -68,6 +68,7 @@ struct ovsdb {
 };
 
 struct ovsdb *ovsdb_create(struct ovsdb_schema *);
+void ovsdb_replace(struct ovsdb *dst, struct ovsdb *src);
 void ovsdb_destroy(struct ovsdb *);
 
 void ovsdb_get_memory_usage(const struct ovsdb *, struct simap *usage);
diff --git a/ovsdb/transaction.c b/ovsdb/transaction.c
index f1502ffb398c..893ea1152c5a 100644
--- a/ovsdb/transaction.c
+++ b/ovsdb/transaction.c
@@ -806,8 +806,14 @@ update_version(struct ovsdb_txn *txn OVS_UNUSED, struct ovsdb_txn_row *txn_row)
     return NULL;
 }
 
-static struct ovsdb_error *
-ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
+static bool
+ovsdb_txn_is_empty(const struct ovsdb_txn *txn)
+{
+    return ovs_list_is_empty(&txn->txn_tables);
+}
+
+struct ovsdb_error * OVS_WARN_UNUSED_RESULT
+ovsdb_txn_start_commit(struct ovsdb_txn *txn)
 {
     struct ovsdb_error *error;
 
@@ -818,29 +824,25 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
         ovsdb_txn_abort(txn);
         return OVSDB_WRAP_BUG("can't happen", error);
     }
-    if (ovs_list_is_empty(&txn->txn_tables)) {
-        ovsdb_txn_abort(txn);
+    if (ovsdb_txn_is_empty(txn)) {
         return NULL;
     }
 
     /* Update reference counts and check referential integrity. */
     error = update_ref_counts(txn);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
     /* Delete unreferenced, non-root rows. */
     error = for_each_txn_row(txn, collect_garbage);
     if (error) {
-        ovsdb_txn_abort(txn);
         return OVSDB_WRAP_BUG("can't happen", error);
     }
 
     /* Check maximum rows table constraints. */
     error = check_max_rows(txn);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
@@ -848,14 +850,12 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
      * integrity. */
     error = for_each_txn_row(txn, assess_weak_refs);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
     /* Verify that the indexes will still be unique post-transaction. */
     error = for_each_txn_row(txn, check_index_uniqueness);
     if (error) {
-        ovsdb_txn_abort(txn);
         return error;
     }
 
@@ -865,9 +865,16 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
         return OVSDB_WRAP_BUG("can't happen", error);
     }
 
+    return NULL;
+}
+
+struct ovsdb_error *
+ovsdb_txn_finish_commit(struct ovsdb_txn *txn, bool durable)
+{
     /* Send the commit to each replica. */
     if (txn->db->file) {
-        error = ovsdb_file_commit(txn->db->file, txn, durable);
+        struct ovsdb_error *error = ovsdb_file_commit(txn->db->file, txn,
+                                                      durable);
         if (error) {
             ovsdb_txn_abort(txn);
             return error;
@@ -887,10 +894,12 @@ ovsdb_txn_commit_(struct ovsdb_txn *txn, bool durable)
 struct ovsdb_error *
 ovsdb_txn_commit(struct ovsdb_txn *txn, bool durable)
 {
-   struct ovsdb_error *err;
-
-   PERF(__func__, err = ovsdb_txn_commit_(txn, durable));
-   return err;
+    struct ovsdb_error *error = ovsdb_txn_start_commit(txn);
+    if (error || ovsdb_txn_is_empty(txn)) {
+        ovsdb_txn_abort(txn);
+        return error;
+    }
+    return ovsdb_txn_finish_commit(txn, durable);
 }
 
 void
diff --git a/ovsdb/transaction.h b/ovsdb/transaction.h
index 1ecd15a56a8d..f9b886411bf4 100644
--- a/ovsdb/transaction.h
+++ b/ovsdb/transaction.h
@@ -26,6 +26,11 @@ struct uuid;
 
 struct ovsdb_txn *ovsdb_txn_create(struct ovsdb *);
 void ovsdb_txn_abort(struct ovsdb_txn *);
+
+struct ovsdb_error *ovsdb_txn_start_commit(struct ovsdb_txn *)
+    OVS_WARN_UNUSED_RESULT;
+struct ovsdb_error *ovsdb_txn_finish_commit(struct ovsdb_txn *, bool durable)
+    OVS_WARN_UNUSED_RESULT;
 struct ovsdb_error *ovsdb_txn_commit(struct ovsdb_txn *, bool durable)
     OVS_WARN_UNUSED_RESULT;
 
diff --git a/ovsdb/trigger.c b/ovsdb/trigger.c
index 165cd6ebbdd1..346db7b5fb28 100644
--- a/ovsdb/trigger.c
+++ b/ovsdb/trigger.c
@@ -19,42 +19,48 @@
 
 #include <limits.h>
 
+#include "file.h"
+#include "log.h"
 #include "openvswitch/json.h"
 #include "jsonrpc.h"
 #include "ovsdb.h"
+#include "ovsdb-error.h"
 #include "openvswitch/poll-loop.h"
 #include "server.h"
 #include "util.h"
 
+
 static bool ovsdb_trigger_try(struct ovsdb_trigger *, long long int now);
-static void ovsdb_trigger_complete(struct ovsdb_trigger *);
+static void trigger_error(struct ovsdb_trigger *, struct ovsdb_error *);
+static void trigger_success(struct ovsdb_trigger *, struct json *result);
 
-void
+bool
 ovsdb_trigger_init(struct ovsdb_session *session, struct ovsdb *db,
                    struct ovsdb_trigger *trigger,
-                   struct json *request, long long int now,
-                   bool read_only, const char *role,
-                   const char *id)
+                   struct jsonrpc_msg *request, long long int now,
+                   bool read_only, const char *role, const char *id)
 {
+    ovs_assert(!strcmp(request->method, "transact") ||
+               !strcmp(request->method, "convert"));
     trigger->session = session;
     trigger->db = db;
     ovs_list_push_back(&trigger->db->triggers, &trigger->node);
     trigger->request = request;
-    trigger->result = NULL;
+    trigger->reply = NULL;
     trigger->created = now;
     trigger->timeout_msec = LLONG_MAX;
     trigger->read_only = read_only;
     trigger->role = nullable_xstrdup(role);
     trigger->id = nullable_xstrdup(id);
-    ovsdb_trigger_try(trigger, now);
+    return ovsdb_trigger_try(trigger, now);
 }
 
 void
 ovsdb_trigger_destroy(struct ovsdb_trigger *trigger)
 {
     ovs_list_remove(&trigger->node);
-    json_destroy(trigger->request);
-    json_destroy(trigger->result);
+    jsonrpc_msg_destroy(trigger->request);
+    jsonrpc_msg_destroy(trigger->reply);
     free(trigger->role);
     free(trigger->id);
 }
@@ -62,30 +68,53 @@ ovsdb_trigger_destroy(struct ovsdb_trigger *trigger)
 bool
 ovsdb_trigger_is_complete(const struct ovsdb_trigger *trigger)
 {
-    return trigger->result != NULL;
+    return trigger->reply != NULL;
 }
 
-struct json *
-ovsdb_trigger_steal_result(struct ovsdb_trigger *trigger)
+struct jsonrpc_msg *
+ovsdb_trigger_steal_reply(struct ovsdb_trigger *trigger)
 {
-    struct json *result = trigger->result;
-    trigger->result = NULL;
-    return result;
+    struct jsonrpc_msg *reply = trigger->reply;
+    trigger->reply = NULL;
+    return reply;
 }
 
 void
+ovsdb_trigger_prereplace_db(struct ovsdb_trigger *trigger)
+{
+    if (!strcmp(trigger->request->method, "transact")) {
+        trigger_error(trigger, ovsdb_error("canceled", NULL));
+    } else if (!strcmp(trigger->request->method, "convert")) {
+        /* We don't cancel "convert" requests when a database is being replaced
+         * for two reasons.  First, we expect the administrator to do some kind
+         * of sensible synchronization on conversion requests, that is, it only
+         * really makes sense for the admin to do a single conversion at a time
+         * at a scheduled point.  Second, if we did then every "convert"
+         * request would end up getting canceled since "convert" itself causes
+         * the database to be replaced. */
+    } else {
+        OVS_NOT_REACHED();
+    }
+}
+
+bool
 ovsdb_trigger_run(struct ovsdb *db, long long int now)
 {
     struct ovsdb_trigger *t, *next;
-    bool run_triggers;
 
-    run_triggers = db->run_triggers;
+    bool run_triggers = db->run_triggers;
     db->run_triggers = false;
+
+    bool disconnect_all = false;
+
     LIST_FOR_EACH_SAFE (t, next, node, &db->triggers) {
         if (run_triggers || now - t->created >= t->timeout_msec) {
-            ovsdb_trigger_try(t, now);
+            if (ovsdb_trigger_try(t, now)) {
+                disconnect_all = true;
+            }
         }
     }
+    return disconnect_all;
 }
 
 void
@@ -118,22 +147,81 @@ ovsdb_trigger_wait(struct ovsdb *db, long long int now)
 static bool
 ovsdb_trigger_try(struct ovsdb_trigger *t, long long int now)
 {
-    t->result = ovsdb_execute(t->db, t->session,
-                              t->request, t->read_only,
-                              t->role, t->id,
-                              now - t->created, &t->timeout_msec);
-    if (t->result) {
-        ovsdb_trigger_complete(t);
+    if (!strcmp(t->request->method, "transact")) {
+        struct json *result = ovsdb_execute(t->db, t->session,
+                                            t->request->params, t->read_only,
+                                            t->role, t->id, now - t->created,
+                                            &t->timeout_msec);
+        if (result) {
+            trigger_success(t, result);
+        }
+        return false;
+    } else if (!strcmp(t->request->method, "convert")) {
+        /* Permission check. */
+        if (t->role && *t->role) {
+            trigger_error(t, ovsdb_perm_error(
+                              "RBAC rules for client \"%s\" role \"%s\" "
+                              "prohibit \"convert\" of database %s "
+                              "(only the root role may convert databases)",
+                              t->id, t->role, t->db->schema->name));
+            return false;
+        }
+
+        /* Validate parameters. */
+        const struct json *params = t->request->params;
+        if (params->type != JSON_ARRAY || params->u.array.n != 2) {
+            trigger_error(t, ovsdb_syntax_error(params, NULL,
+                                                "array expected"));
+            return false;
+        }
+
+        /* Parse new schema and make a converted copy. */
+        const struct json *new_schema_json = params->u.array.elems[1];
+        struct ovsdb_schema *new_schema;
+        struct ovsdb_error *error = ovsdb_schema_from_json(new_schema_json,
+                                                           &new_schema);
+        if (!error && strcmp(new_schema->name, t->db->schema->name)) {
+            error = ovsdb_error(
+                "invalid parameters",
+                "new schema name (%s) does not match database name (%s)",
+                new_schema->name, t->db->schema->name);
+        }
+        if (!error) {
+            error = ovsdb_file_convert(t->db->file, new_schema);
+        }
+        ovsdb_schema_destroy(new_schema);
+        if (error) {
+            trigger_error(t, error);
+            return false;
+        }
+
+        trigger_success(t, json_object_create());
         return true;
     } else {
-        return false;
+        OVS_NOT_REACHED();
     }
 }
 
 static void
-ovsdb_trigger_complete(struct ovsdb_trigger *t)
+ovsdb_trigger_complete(struct ovsdb_trigger *t, struct jsonrpc_msg *reply)
 {
-    ovs_assert(t->result != NULL);
+    ovs_assert(reply && !t->reply);
+    t->reply = reply;
     ovs_list_remove(&t->node);
     ovs_list_push_back(&t->session->completions, &t->node);
 }
+
+static void
+trigger_error(struct ovsdb_trigger *t, struct ovsdb_error *error)
+{
+    struct jsonrpc_msg *reply = jsonrpc_create_error(
+        ovsdb_error_to_json_free(error), t->request->id);
+    ovsdb_trigger_complete(t, reply);
+}
+
+static void
+trigger_success(struct ovsdb_trigger *t, struct json *result)
+{
+    struct jsonrpc_msg *reply = jsonrpc_create_reply(result, t->request->id);
+    ovsdb_trigger_complete(t, reply);
+}
diff --git a/ovsdb/trigger.h b/ovsdb/trigger.h
index 90246a4a42bd..d9df97f31222 100644
--- a/ovsdb/trigger.h
+++ b/ovsdb/trigger.h
@@ -25,8 +25,8 @@ struct ovsdb_trigger {
     struct ovsdb *db;           /* Database on which trigger acts. */
     struct ovs_list node;       /* !result: in db->triggers;
                                  * result: in session->completions. */
-    struct json *request;       /* Database request. */
-    struct json *result;        /* Result (null if none yet). */
+    struct jsonrpc_msg *request; /* Database request. */
+    struct jsonrpc_msg *reply;   /* Result (null if none yet).. */
     long long int created;      /* Time created. */
     long long int timeout_msec; /* Max wait duration. */
     bool read_only;             /* Database is in read only mode. */
@@ -34,17 +34,18 @@ struct ovsdb_trigger {
     char *id;                   /* ID, for role-based access controls. */
 };
 
-void ovsdb_trigger_init(struct ovsdb_session *, struct ovsdb *,
+bool ovsdb_trigger_init(struct ovsdb_session *, struct ovsdb *,
                         struct ovsdb_trigger *,
-                        struct json *request, long long int now,
-                        bool read_only, const char *role,
-                        const char *id);
+                        struct jsonrpc_msg *request, long long int now,
+                        bool read_only, const char *role, const char *id);
 void ovsdb_trigger_destroy(struct ovsdb_trigger *);
 
 bool ovsdb_trigger_is_complete(const struct ovsdb_trigger *);
-struct json *ovsdb_trigger_steal_result(struct ovsdb_trigger *);
+struct jsonrpc_msg *ovsdb_trigger_steal_reply(struct ovsdb_trigger *);
 
-void ovsdb_trigger_run(struct ovsdb *, long long int now);
+void ovsdb_trigger_prereplace_db(struct ovsdb_trigger *);
+
+bool ovsdb_trigger_run(struct ovsdb *, long long int now);
 void ovsdb_trigger_wait(struct ovsdb *, long long int now);
 
 #endif /* ovsdb/trigger.h */
diff --git a/tests/ovsdb-monitor.at b/tests/ovsdb-monitor.at
index 2434f43cb761..917a5cc09ace 100644
--- a/tests/ovsdb-monitor.at
+++ b/tests/ovsdb-monitor.at
@@ -29,11 +29,11 @@ m4_define([OVSDB_CHECK_MONITOR],
    on_exit 'kill `cat ovsdb-server.pid`'
    AT_CAPTURE_FILE([ovsdb-client-log])
    if test "$IS_WIN32" = "yes"; then
-     AT_CHECK([ovsdb-client -vjsonrpc --pidfile --log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 $5 $8 > output 2>/dev/null &],
+     AT_CHECK([ovsdb-client -vjsonrpc --detach --pidfile --log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 $5 $8 > output],
               [0], [ignore], [ignore])
      sleep 1
    else
-     AT_CHECK([ovsdb-client -vjsonrpc --detach --no-chdir --pidfile --log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 $5 $8 > output],
+     AT_CHECK([ovsdb-client -vjsonrpc --detach --no-chdir --pidfile --log-file="`pwd`"/ovsdb-client-log -d json monitor --format=csv unix:socket $4 $5 $8 > output 2>/dev/null],
             [0], [ignore], [ignore])
    fi
    on_exit 'kill `cat ovsdb-client.pid`'
diff --git a/tests/ovsdb-server.at b/tests/ovsdb-server.at
index 2e3d8ad14636..54ff04ef3146 100644
--- a/tests/ovsdb-server.at
+++ b/tests/ovsdb-server.at
@@ -790,6 +790,269 @@ _uuid                                name  number
 OVSDB_SERVER_SHUTDOWN
 AT_CLEANUP
 
+AT_SETUP([schema conversion online])
+AT_KEYWORDS([ovsdb server convert needs-conversion])
+on_exit 'kill `cat *.pid`'
+ordinal_schema > schema
+AT_DATA([new-schema],
+  [[{"name": "ordinals",
+     "tables": {
+       "ordinals": {
+         "columns": {
+           "number": {"type": "integer"}}}}}
+]])
+dnl Make sure that "ovsdb-tool create" works with a dangling symlink for
+dnl the database and the lockfile, creating the target of each symlink rather
+dnl than replacing the symlinks with regular files.
+mkdir dir
+if test "$IS_WIN32" = "no"; then
+    ln -s dir/db db
+    ln -s dir/.db.~lock~ .db.~lock~
+    AT_SKIP_IF([test ! -h db || test ! -h .db.~lock~])
+fi
+AT_CHECK([ovsdb-tool create db schema])
+dnl Put some data in the database.
+AT_CHECK(
+  [[for pair in 'zero 0' 'one 1' 'two 2' 'three 3' 'four 4' 'five 5'; do
+      set -- $pair
+      ovsdb-tool transact db '
+        ["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "'$1'", "number": '$2'}},
+         {"op": "comment",
+          "comment": "add row for '"$pair"'"}]'
+    done | uuidfilt]], [0],
+[[[{"uuid":["uuid","<0>"]},{}]
+[{"uuid":["uuid","<1>"]},{}]
+[{"uuid":["uuid","<2>"]},{}]
+[{"uuid":["uuid","<3>"]},{}]
+[{"uuid":["uuid","<4>"]},{}]
+[{"uuid":["uuid","<5>"]},{}]
+]], [ignore])
+
+dnl Start the database server.
+AT_CHECK([ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock db], [0])
+AT_CAPTURE_FILE([ovsdb-server.log])
+
+dnl Try "needs-conversion".
+AT_CHECK([ovsdb-client needs-conversion schema], [0], [no
+])
+AT_CHECK([ovsdb-client needs-conversion new-schema], [0], [yes
+])
+
+dnl Start two monitors on the 'ordinals' db, one that is database
+dnl change aware and one that is not.
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=monitor-ordinals-aware.pid --log-file=monitor-ordinals-aware.log --db-change-aware --no-headings monitor ordinals ordinals number name > monitor-ordinals-aware.stdout 2> monitor-ordinals-aware.stderr])
+AT_CAPTURE_FILE([monitor-ordinals-aware.stdout])
+AT_CAPTURE_FILE([monitor-ordinals-aware.log])
+AT_CAPTURE_FILE([monitor-ordinals-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=monitor-ordinals-unaware.pid --log-file=monitor-ordinals-unaware.log --no-db-change-aware --no-headings monitor ordinals ordinals number name > monitor-ordinals-unaware.stdout 2> monitor-ordinals-unaware.stderr])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.stdout])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.log])
+AT_CAPTURE_FILE([monitor-ordinals-unaware.stderr])
+
+dnl Start two monitors on the '_Server' db, one that is database
+dnl change aware and one that is not.
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=monitor-server-aware.pid --log-file=monitor-server-aware.log --db-change-aware --no-headings monitor _Server Database name > monitor-server-aware.stdout 2> monitor-server-aware.stderr])
+AT_CAPTURE_FILE([monitor-server-aware.stdout])
+AT_CAPTURE_FILE([monitor-server-aware.log])
+AT_CAPTURE_FILE([monitor-server-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=monitor-server-unaware.pid --log-file=monitor-server-unaware.log --no-db-change-aware --no-headings monitor _Server Database name > monitor-server-unaware.stdout 2> monitor-server-unaware.stderr])
+AT_CAPTURE_FILE([monitor-server-unaware.stdout])
+AT_CAPTURE_FILE([monitor-server-unaware.log])
+AT_CAPTURE_FILE([monitor-server-unaware.stderr])
+
+dnl Start two long-running transactions (triggers) on the 'ordinals' db,
+dnl one that is database change aware and one that is not.
+ordinals_txn='[["ordinals",
+		{"op": "wait",
+		 "table": "ordinals",
+		 "where": [["name", "==", "seven"]],
+		 "columns": ["name", "number"],
+		 "rows": [],
+		 "until": "!="}]]'
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=trigger-ordinals-aware.pid --log-file=trigger-ordinals-aware.log --db-change-aware transact "$ordinals_txn"  > trigger-ordinals-aware.stdout 2> trigger-ordinals-aware.stderr])
+AT_CAPTURE_FILE([trigger-ordinals-aware.stdout])
+AT_CAPTURE_FILE([trigger-ordinals-aware.log])
+AT_CAPTURE_FILE([trigger-ordinals-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=trigger-ordinals-unaware.pid --log-file=trigger-ordinals-unaware.log --no-db-change-aware transact  "$ordinals_txn" > trigger-ordinals-unaware.stdout 2> trigger-ordinals-unaware.stderr])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.stdout])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.log])
+AT_CAPTURE_FILE([trigger-ordinals-unaware.stderr])
+
+dnl Start two long-running transactions (triggers) on the _Server db,
+dnl one that is database change aware and one that is not.
+server_txn='[["_Server",
+	      {"op": "wait",
+	       "table": "Database",
+	       "where": [["name", "==", "xyzzy"]],
+	       "columns": ["name"],
+	       "rows": [],
+	       "until": "!="}]]'
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=trigger-server-aware.pid --log-file=trigger-server-aware.log --db-change-aware transact "$server_txn"  > trigger-server-aware.stdout 2> trigger-server-aware.stderr])
+AT_CAPTURE_FILE([trigger-server-aware.stdout])
+AT_CAPTURE_FILE([trigger-server-aware.log])
+AT_CAPTURE_FILE([trigger-server-aware.stderr])
+
+AT_CHECK([ovsdb-client -vfile -vvlog:off --detach --pidfile=trigger-server-unaware.pid --log-file=trigger-server-unaware.log --no-db-change-aware transact  "$server_txn" > trigger-server-unaware.stdout 2> trigger-server-unaware.stderr])
+AT_CAPTURE_FILE([trigger-server-unaware.stdout])
+AT_CAPTURE_FILE([trigger-server-unaware.log])
+AT_CAPTURE_FILE([trigger-server-unaware.stderr])
+
+dnl Dump out and check the actual database contents.
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals], [0], [stdout])
+AT_CHECK([uuidfilt stdout], [0], [dnl
+ordinals table
+_uuid                                name  number
+------------------------------------ ----- ------
+<0> five  5
+<1> four  4
+<2> one   1
+<3> three 3
+<4> two   2
+<5> zero  0
+])
+
+dnl Convert the database.
+AT_CHECK([ovsdb-client convert new-schema])
+
+dnl Try "needs-conversion".
+AT_CHECK([ovsdb-client needs-conversion schema], [0], [yes
+])
+AT_CHECK([ovsdb-client needs-conversion new-schema], [0], [no
+])
+
+dnl Verify that the "ordinals" monitors behaved as they should have.
+dnl Both should have exited, for different reasons.
+dnl The db-aware _Server monitor should still be running, but not the unaware
+dnl one.
+for x in unaware aware; do
+    OVS_WAIT_WHILE([test -e monitor-ordinals-$x.pid])
+    AT_CHECK([sort -k 3 monitor-ordinals-$x.stdout | uuidfilt], [0],
+[<0> initial 0 zero
+<1> initial 1 one
+<2> initial 2 two
+<3> initial 3 three
+<4> initial 4 four
+<5> initial 5 five
+])
+done
+AT_CHECK([sed 's/.*: //' monitor-ordinals-unaware.stderr], [0], [receive failed (End of file)
+])
+AT_CHECK([sed 's/.*: //' monitor-ordinals-aware.stderr], [0], [ordinals database was removed
+])
+
+dnl Verify that the _Server monitors behaved as they should have.
+dnl The db-aware monitor should still be running, but not the unaware one.
+for x in aware unaware; do
+    AT_CHECK([sort -k 3 monitor-server-$x.stdout | uuidfilt], [0],
+[<0> initial _Server
+<1> initial ordinals
+])
+done
+OVS_WAIT_WHILE([test -e monitor-server-unaware.pid])
+AT_CHECK([sed 's/.*: //' monitor-ordinals-unaware.stderr], [0], [receive failed (End of file)
+])
+AT_CHECK([test -e monitor-server-aware.pid])
+
+dnl Verify that the "ordinals" triggers behaved as they should have:
+dnl Both should have exited, for different reasons.
+for x in unaware aware; do
+    OVS_WAIT_WHILE([test -e trigger-ordinals-$x.pid])
+    AT_CHECK([cat trigger-ordinals-$x.stdout])
+done
+AT_CHECK([cat trigger-ordinals-unaware.stderr], [0], [ovsdb-client: transaction failed (End of file)
+])
+AT_CHECK([cat trigger-ordinals-aware.stderr], [0], [ovsdb-client: transaction returned error: {"error":"canceled"}
+])
+
+dnl Verify that the _Server triggers behaved as they should have:
+dnl The db-aware trigger should still be waiting, but not the unaware one.
+for x in aware unaware; do
+    AT_CHECK([cat trigger-server-$x.stdout])
+done
+OVS_WAIT_WHILE([test -e trigger-server-unaware.pid])
+AT_CHECK([sed 's/.*: //' trigger-ordinals-unaware.stderr], [0], [transaction failed (End of file)
+])
+AT_CHECK([test -e trigger-server-aware.pid])
+
+dnl We can't fully re-check the contents of the database log, because the
+dnl order of the records is not predictable, but there should only be 4 lines
+dnl in it now.
+AT_CAPTURE_FILE([db])
+AT_CHECK([test `wc -l < db` -eq 4])
+dnl And check that the dumped data is the same except for the removed column:
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | uuidfilt], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+])
+dnl Now check that the converted database is still online and can be modified,
+dnl then check that the database log has one more record and that the data
+dnl is as expected.
+AT_CHECK(
+  [[ovsdb-client transact '
+     ["ordinals",
+      {"op": "insert",
+       "table": "ordinals",
+       "row": {"number": 6}},
+      {"op": "comment",
+       "comment": "add row for 6"}]' | uuidfilt]], [0],
+  [[[{"uuid":["uuid","<0>"]},{}]
+]])
+AT_CHECK([test `wc -l < db` -eq 6])
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | uuidfilt], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+<6> 6
+])
+dnl Now kill and restart the database server to ensure that the data is
+dnl correct on disk as well as in memory.
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CHECK([[ovsdb-server -vfile -vvlog:off --detach --no-chdir --pidfile --log-file --remote=punix:db.sock db]],
+  [0])
+AT_CHECK([ovsdb-client dump unix:db.sock ordinals | uuidfilt], [0], [dnl
+ordinals table
+_uuid                                number
+------------------------------------ ------
+<0> 0
+<1> 1
+<2> 2
+<3> 3
+<4> 4
+<5> 5
+<6> 6
+])
+
+dnl Make sure that "db" is still a symlink to dir/db instead of getting
+dnl replaced by a regular file, ditto for .db.~lock~.
+if test "$IS_WIN32" = "no"; then
+    AT_CHECK([test -h db])
+    AT_CHECK([test -h .db.~lock~])
+    AT_CHECK([test -f dir/db])
+    AT_CHECK([test -f dir/.db.~lock~])
+fi
+
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+AT_CLEANUP
+
 AT_SETUP([ovsdb-server combines updates on backlogged connections])
 on_exit 'kill `cat *.pid`'
 
diff --git a/tests/test-ovsdb.c b/tests/test-ovsdb.c
index c0c5a4df51af..8502ad73ff69 100644
--- a/tests/test-ovsdb.c
+++ b/tests/test-ovsdb.c
@@ -1522,14 +1522,14 @@ struct test_trigger {
 static void
 do_trigger_dump(struct test_trigger *t, long long int now, const char *title)
 {
-    struct json *result;
+    struct jsonrpc_msg *reply;
     char *s;
 
-    result = ovsdb_trigger_steal_result(&t->trigger);
-    s = json_to_string(result, JSSF_SORT);
+    reply = ovsdb_trigger_steal_reply(&t->trigger);
+    s = json_to_string(reply->result, JSSF_SORT);
     printf("t=%lld: trigger %d (%s): %s\n", now, t->number, title, s);
     free(s);
-    json_destroy(result);
+    jsonrpc_msg_destroy(reply);
     ovsdb_trigger_destroy(&t->trigger);
     free(t);
 }
@@ -1569,8 +1569,10 @@ do_trigger(struct ovs_cmdl_context *ctx)
             json_destroy(params);
         } else {
             struct test_trigger *t = xmalloc(sizeof *t);
-            ovsdb_trigger_init(&session, db, &t->trigger, params, now, false,
-                               NULL, NULL);
+            ovsdb_trigger_init(&session, db, &t->trigger,
+                               jsonrpc_create_request("transact", params,
+                                                      NULL),
+                               now, false, NULL, NULL);
             t->number = number++;
             if (ovsdb_trigger_is_complete(&t->trigger)) {
                 do_trigger_dump(t, now, "immediate");
