Repository: optuna/optuna
Pull request: Adding check in `cli.py` to handle an empty database
<!-- Thank you for creating a pull request! In general, we merge your pull request after it gets two or more approvals. To proceed to the review process by the maintainers, please make sure that the PR meets the following conditions: (1) it passes all CI checks, and (2) it is neither in the draft nor WIP state. If you wish to discuss the PR in the draft state or need any other help, please mention the Optuna development team in the PR. -->

## Motivation
<!-- Describe your motivation why you will submit this PR. This is useful for reviewers to understand the context of PR. -->
This PR aims to close #5816 

## Description of the changes
<!-- Describe the changes in this PR. -->
- Added a check in `cli.py` in `_dump_table` to handle cases where no rows exist (empty database).
- If `rows` is empty, the column width is determined based only on the header length, so that `max()` isn't called on an empty sequence.
- Also created a conditional block to append row strings (`ret += "\n".join(rows_string) + "\n"`) only if rows exist, to avoid unnecessary empty lines in the output.

Problem statement:
CLI for empty DB raises `ValueError`
### Expected behavior

CLI for empty DB should output empty result, but the current implementation raises `ValueError`.

### Environment

- Optuna version:4.2.0.dev
- Python version:3.13.0
- OS:macOS-15.1-x86_64-i386-64bit-Mach-O
- (Optional) Other libraries and their versions:

### Error messages, stack traces, or logs

```shell
See below.
```


### Steps to reproduce

For empty DB (`tmp.db` does not exist before the command), the `optuna studies` command raises `ValueError`.

```bash
$ optuna --storage sqlite:///tmp.db studies
Traceback (most recent call last):
  File "/Users/naotomizuno/.pyenv/versions/optuna-3.13.0/bin/optuna", line 8, in <module>
    sys.exit(main())
             ~~~~^^
  File "/Users/naotomizuno/optuna/optuna/cli.py", line 991, in main
    return args.handler(args)
           ~~~~~~~~~~~~^^^^^^
  File "/Users/naotomizuno/optuna/optuna/cli.py", line 463, in take_action
    _format_output(
    ~~~~~~~~~~~~~~^
        records, self._study_list_header, parsed_args.format, parsed_args.flatten
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/naotomizuno/optuna/optuna/cli.py", line 258, in _format_output
    return _dump_table(values, header).strip()
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^
  File "/Users/naotomizuno/optuna/optuna/cli.py", line 222, in _dump_table
    max_width = max(len(header[column]), max(row[column].width() for row in rows))
                                         ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: max() iterable argument is empty
```


### Additional context (optional)

_No response_

Hints from issue discussion:

Commit message: removing space

Patch to review:
diff --git a/optuna/cli.py b/optuna/cli.py
index 16fa3a6df1d..db64bcd70a0 100644
--- a/optuna/cli.py
+++ b/optuna/cli.py
@@ -215,7 +215,10 @@ def _dump_table(records: list[dict[str, Any]], header: list[str]) -> str:
         for t in value_types:
             if t == ValueType.STRING:
                 value_type = ValueType.STRING
-        max_width = max(len(header[column]), max(row[column].width() for row in rows))
+        if not rows:
+            max_width = len(header[column])
+        else:
+            max_width = max(len(header[column]), max(row[column].width() for row in rows))
         separator += "-" * (max_width + 2) + "+"
         if value_type == ValueType.NUMERIC:
             header_string += f" {header[column]:>{max_width}} |"
@@ -228,7 +231,8 @@ def _dump_table(records: list[dict[str, Any]], header: list[str]) -> str:
     ret += separator + "\n"
     ret += header_string + "\n"
     ret += separator + "\n"
-    ret += "\n".join(rows_string) + "\n"
+    if rows:
+        ret += "\n".join(rows_string) + "\n"
     ret += separator + "\n"
 
     return ret


Code review comments:
diff_hunk: @@ -228,7 +231,8 @@ def _dump_table(records: list[dict[str, Any]], header: list[str]) -> str:
     ret += separator + "\n"
     ret += header_string + "\n"
     ret += separator + "\n"
-    ret += "\n".join(rows_string) + "\n"
+    if rows:
+        ret += "\n".join(rows_string) + "\n"
path: optuna/cli.py
text: @user1:
```suggestion
    for row_string in rows_string:
        ret += row_string + "\n"
```
It might be more straightforward to simply iterate over rows_string.

diff_hunk: @@ -215,7 +215,10 @@ def _dump_table(records: list[dict[str, Any]], header: list[str]) -> str:
         for t in value_types:
             if t == ValueType.STRING:
                 value_type = ValueType.STRING
-        max_width = max(len(header[column]), max(row[column].width() for row in rows))
+        if not rows:
path: optuna/cli.py
text: @user1:
```suggestion
        if len(rows) == 0:
```
Please use `len(rows) == 0` for checking if the list is empty, rather than relying on `not rows`.