Repository: mlflow/mlflow

Issue:
[BUG] Renaming a run discards its current status and reverts the run status back to UNFINISHED
### Issues Policy acknowledgement

- [X] I have read and agree to submit bug reports in accordance with the [issues policy](https://www.github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md)

### Willingness to contribute

No. I cannot contribute a bug fix at this time.

### MLflow version

- Client: 2.2.2
- Tracking server: 2.2.2


### System information

N/A

### Describe the problem

When renaming a run, the runs status is set to the default status `UNFINISHED`


Observe a run where the status is `FINISHED` with a duration of 36ms:

<img width="1411" alt="Screenshot 2023-03-31 at 7 12 45 AM" src="https://user-images.githubusercontent.com/111539728/229144707-9dfd4af8-20e4-49d5-9ec5-b5d980845a7f.png">

Click the button to rename the run and change it's name. Notice that it's status has now changed to `UNFINISHED`

<img width="1411" alt="Screenshot 2023-03-31 at 7 14 34 AM" src="https://user-images.githubusercontent.com/111539728/229145133-d1c17e90-2fc5-4443-9e13-7bd39e8ca228.png">


The REST API specifies that status may be supplied to https://www.mlflow.org/docs/latest/rest-api.html#update-run. However, that field is optional (which is what I would expect).

Looking at the code, the problem is that the run status is always defaulted when not supplied, and overwrites existing status information in the database.
 
https://github.com/mlflow/mlflow/blob/master/mlflow/store/tracking/sqlalchemy_store.py#L551-L568

### Tracking information

```
System information: Linux #1 SMP Wed Nov 2 17:44:17 UTC 2022
Python version: 3.9.16
MLflow version: 2.2.2
MLflow module location: /opt/conda/lib/python3.9/site-packages/mlflow/__init__.py
Tracking URI: [http://localhost:80](http://localhost/)
Registry URI: [http://localhost:80](http://localhost/)
MLflow environment variables: 
  MLFLOW_TRACKING_TOKEN: {"projectId":"6392bcff9e3a5133532aeae4","runId":"6423013bacb1650c1e6b106e","projectName":"quick-start","environmentId":"63913cf66d2650476805c785","environmentRevisionId":"6422fdcdacb1650c1e6b1061","hardwareTierId":"medium-k8s","mlflowSourceType":"NOTEBOOK","runNumber":33,"datasetInfo":"6392bd049e3a5133532aeae9-6392bd049e3a5133532aeae8"}
  MLFLOW_TRACKING_URI: [http://localhost:80](http://localhost/)
MLflow dependencies: 
  Flask: 1.1.2
  Jinja2: 3.0.3
  alembic: 1.10.2
  click: 8.0.4
  cloudpickle: 2.2.1
  databricks-cli: 0.17.6
  docker: 6.0.1
  entrypoints: 0.4
  gitpython: 3.1.31
  gunicorn: 20.1.0
  importlib-metadata: 6.1.0
  markdown: 3.4.3
  matplotlib: 3.6.2
  numpy: 1.23.3
  packaging: 23.0
  pandas: 1.5.0
  protobuf: 3.20.3
  pyarrow: 8.0.0
  pytz: 2022.2.1
  pyyaml: 6.0
  querystring-parser: 1.2.4
  requests: 2.28.1
  scikit-learn: 1.1.2
  scipy: 1.9.2
  shap: 0.41.0
  sqlalchemy: 1.4.47
  sqlparse: 0.4.3
```

### Code to reproduce issue

Just curling the REST API is sufficient to trigger this bug. I captured the fetch call in my browser session - here's a minimal repro

```
curl 'https://mlflow/ajax-api/2.0/mlflow/runs/update' \
  -H 'Content-Type: application/json; charset=utf-8' \
  --data-raw '{"run_id":"9742e09dafee42f39b9af2bd2e38bf47","run_name":"spiffy-crab-383_renamed"}'
```

### Stack trace

N/A

### Other info / logs

N/A

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/recipes`: Recipes, Recipe APIs, Recipe configs, Recipe Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [X] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [X] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [ ] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [X] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
Thanks for reporting this ! I will file a PR for it.

Resolving patch:
diff --git a/mlflow/server/handlers.py b/mlflow/server/handlers.py
--- a/mlflow/server/handlers.py
+++ b/mlflow/server/handlers.py
@@ -711,9 +711,10 @@ def _update_run():
         },
     )
     run_id = request_message.run_id or request_message.run_uuid
-    updated_info = _get_tracking_store().update_run_info(
-        run_id, request_message.status, request_message.end_time, request_message.run_name
-    )
+    run_name = request_message.run_name if request_message.HasField("run_name") else None
+    end_time = request_message.end_time if request_message.HasField("end_time") else None
+    status = request_message.status if request_message.HasField("status") else None
+    updated_info = _get_tracking_store().update_run_info(run_id, status, end_time, run_name)
     response_message = UpdateRun.Response(run_info=updated_info.to_proto())
     response = Response(mimetype="application/json")
     response.set_data(message_to_json(response_message))
diff --git a/mlflow/store/tracking/sqlalchemy_store.py b/mlflow/store/tracking/sqlalchemy_store.py
--- a/mlflow/store/tracking/sqlalchemy_store.py
+++ b/mlflow/store/tracking/sqlalchemy_store.py
@@ -552,8 +552,10 @@ def update_run_info(self, run_id, run_status, end_time, run_name):
         with self.ManagedSessionMaker() as session:
             run = self._get_run(run_uuid=run_id, session=session)
             self._check_run_is_active(run)
-            run.status = RunStatus.to_string(run_status)
-            run.end_time = end_time
+            if run_status is not None:
+                run.status = RunStatus.to_string(run_status)
+            if end_time is not None:
+                run.end_time = end_time
             if run_name:
                 run.name = run_name
                 run_name_tag = self._try_get_run_tag(session, run_id, MLFLOW_RUN_NAME)


Test patch:
diff --git a/tests/store/tracking/test_sqlalchemy_store.py b/tests/store/tracking/test_sqlalchemy_store.py
--- a/tests/store/tracking/test_sqlalchemy_store.py
+++ b/tests/store/tracking/test_sqlalchemy_store.py
@@ -1233,6 +1233,13 @@ def test_update_run_info(self):
             assert actual.status == new_status_string
             assert actual.end_time == endtime
 
+        # test updating run name without changing other attributes.
+        origin_run_info = self.store.get_run(run.info.run_id).info
+        updated_info = self.store.update_run_info(run.info.run_id, None, None, "name_abc2")
+        assert updated_info.run_name == "name_abc2"
+        assert updated_info.status == origin_run_info.status
+        assert updated_info.end_time == origin_run_info.end_time
+
     def test_update_run_name(self):
         experiment_id = self._experiment_factory("test_update_run_name")
         configs = self._get_run_configs(experiment_id=experiment_id)
diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py
--- a/tests/tracking/test_rest_tracking.py
+++ b/tests/tracking/test_rest_tracking.py
@@ -1199,3 +1199,14 @@ def test_logging_model_with_local_artifact_uri(mlflow_client):
         assert run.info.artifact_uri.startswith("file://")
         mlflow.sklearn.log_model(LogisticRegression(), "model", registered_model_name="rmn")
         mlflow.pyfunc.load_model("models:/rmn/1")
+
+
+def test_update_run_name_without_changing_status(mlflow_client):
+    experiment_id = mlflow_client.create_experiment("update run name")
+    created_run = mlflow_client.create_run(experiment_id)
+    mlflow_client.set_terminated(created_run.info.run_id, "FINISHED")
+
+    mlflow_client.update_run(created_run.info.run_id, name="name_abc")
+    updated_run_info = mlflow_client.get_run(created_run.info.run_id).info
+    assert updated_run_info.run_name == "name_abc"
+    assert updated_run_info.status == "FINISHED"