Repository: optuna/optuna

Issue:
`inf` handling in Optuna
<!-- Please write a clear and concise description of what the bug is. -->

## Expected behavior

<!-- Please write a clear and concise description of what you expected to happen. -->
Regardless of the type of storage used, the trial accepts `inf` for its value when an objective function returns `inf`, when `inf` is passed in `trial.report`, or when `inf` is passed in `study.tell`. Note that it is the responsibility of the sampler or pruner to decide how to use the `inf` values stored in the storage. For example, `TPESampler`, the default sampler in Optuna, is implemented to be able to use `inf`.

We need to fix the followings.
- [x] If `inf` is returned by objective function, the study accepts it regardless of the type of storage used.
- [x] If `inf` is passed in `trial.report`, the study accepts it regardless of the type of storage used.
- [x] If `inf` is passed in `study.tell`, the study accepts it regardless of the type of storage used.

## Environment

<!--
You can get this information by typing the following:
```
python -c 'import optuna; print(optuna.__version__)'
python -c 'import platform; print(platform.python_version())'
python -c 'import platform; print(platform.platform())'
```
-->

- Optuna version: a70cbff8dbc454f137b61622633404bd4e4657fc
- Python version: 3.9.9
- OS: macOS Catalina 10.15.7
- (Optional) Other libraries and their versions:
psycopg2:    2.9.2
PyMySQL:     1.0.2

## Error messages, stack traces, or logs

If we run the script `inf-objective.py`, we will get the following output. The script is provided in the section of `Reproducible examples`. I think we need to modify this behavior as explained in `Expected behavior`. Concretely, we should fix the behavior with MySQL to accept the `inf`.
<details>
<summary>output</summary>

```
(venv) mamu@HideakinoMacBook-puro test % python inf-objective.py 
[I 2021-12-24 13:58:54,771] A new study created in memory with name: no-name-f7f28497-2f1c-4739-b9c8-edca726274fc
[I 2021-12-24 13:58:54,772] Trial 0 finished with value: inf and parameters: {}. Best is trial 0 with value: inf.
In-memory : Trial state is TrialState.COMPLETE
[I 2021-12-24 13:58:54,843] A new study created in RDB with name: no-name-168f23c0-0ac5-4785-a6d6-bdfd31dda5ab
[I 2021-12-24 13:58:54,909] Trial 0 finished with value: inf and parameters: {}. Best is trial 0 with value: inf.
SQLite    : Trial state is TrialState.COMPLETE
[I 2021-12-24 13:58:55,052] A new study created in RDB with name: no-name-22068297-6685-4435-bf88-124621f797c1
Record does not exist.
MySQL     : Trial state is TrialState.RUNNING
[I 2021-12-24 13:58:55,501] A new study created in RDB with name: no-name-39229033-d3b5-43fa-93f6-1e9a313593f6
[I 2021-12-24 13:58:55,700] Trial 0 finished with value: inf and parameters: {}. Best is trial 0 with value: inf.
PostgreSQL: Trial state is TrialState.COMPLETE
```
</details>

On the other hand, if we run the script `inf-report.py`, we will get the following output. The script is provided in the section of `Reproducible examples`. I think we need to modify this behavior as explained in `Expected behavior`. Concretely, we should fix the behavior with MySQL to accept the `inf`.
<details>
<summary>output</summary>

```
(venv) mamu@HideakinoMacBook-puro test % python inf-report.py 
[I 2021-12-24 14:01:01,970] A new study created in memory with name: no-name-44b8d7da-1b6f-4e38-80f4-33d826b4ff74
[I 2021-12-24 14:01:01,971] Trial 0 finished with value: 1.0 and parameters: {}. Best is trial 0 with value: 1.0.
In-memory : Trial state is TrialState.COMPLETE
[I 2021-12-24 14:01:02,040] A new study created in RDB with name: no-name-8e47d68f-83e3-4016-b18c-b7ae4930986b
[I 2021-12-24 14:01:02,564] Trial 0 finished with value: 1.0 and parameters: {}. Best is trial 0 with value: 1.0.
SQLite    : Trial state is TrialState.COMPLETE
[I 2021-12-24 14:01:02,712] A new study created in RDB with name: no-name-dede90c7-9907-47d5-9a78-873233f61f36
[W 2021-12-24 14:01:02,830] Trial 0 failed because of the following error: StorageInternalError('An exception is raised during the commit. This typically happens due to invalid data in the commit, e.g. exceeding max length. ')
Traceback (most recent call last):
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1802, in _execute_context
    self.dialect.do_execute(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 732, in do_execute
    cursor.execute(statement, parameters)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 146, in execute
    query = self.mogrify(query, args)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 125, in mogrify
    query = query % self._escape_args(args, conn)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 109, in _escape_args
    return {key: conn.literal(val) for (key, val) in args.items()}
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 109, in <dictcomp>
    return {key: conn.literal(val) for (key, val) in args.items()}
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/connections.py", line 517, in literal
    return self.escape(obj, self.encoders)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/connections.py", line 510, in escape
    return converters.escape_item(obj, self.charset, mapping=mapping)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/converters.py", line 25, in escape_item
    val = encoder(val, mapping)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/converters.py", line 60, in escape_float
    raise ProgrammingError("%s can not be used with MySQL" % s)
pymysql.err.ProgrammingError: inf can not be used with MySQL

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/storages/_rdb/storage.py", line 54, in _create_scoped_session
    session.commit()
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 1431, in commit
    self._transaction.commit(_to_root=self.future)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 829, in commit
    self._prepare_impl()
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 808, in _prepare_impl
    self.session.flush()
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 3363, in flush
    self._flush(objects)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 3503, in _flush
    transaction.rollback(_capture_exception=True)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.py", line 70, in __exit__
    compat.raise_(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 3463, in _flush
    flush_context.execute()
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/unitofwork.py", line 456, in execute
    rec.execute(self)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/unitofwork.py", line 630, in execute
    util.preloaded.orm_persistence.save_obj(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/persistence.py", line 244, in save_obj
    _emit_insert_statements(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/orm/persistence.py", line 1221, in _emit_insert_statements
    result = connection._execute_20(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1614, in _execute_20
    return meth(self, args_10style, kwargs_10style, execution_options)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/sql/elements.py", line 325, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1481, in _execute_clauseelement
    ret = self._execute_context(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1845, in _execute_context
    self._handle_dbapi_exception(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 2026, in _handle_dbapi_exception
    util.raise_(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1802, in _execute_context
    self.dialect.do_execute(
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 732, in do_execute
    cursor.execute(statement, parameters)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 146, in execute
    query = self.mogrify(query, args)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 125, in mogrify
    query = query % self._escape_args(args, conn)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 109, in _escape_args
    return {key: conn.literal(val) for (key, val) in args.items()}
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/cursors.py", line 109, in <dictcomp>
    return {key: conn.literal(val) for (key, val) in args.items()}
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/connections.py", line 517, in literal
    return self.escape(obj, self.encoders)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/connections.py", line 510, in escape
    return converters.escape_item(obj, self.charset, mapping=mapping)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/converters.py", line 25, in escape_item
    val = encoder(val, mapping)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/pymysql/converters.py", line 60, in escape_float
    raise ProgrammingError("%s can not be used with MySQL" % s)
sqlalchemy.exc.ProgrammingError: (pymysql.err.ProgrammingError) inf can not be used with MySQL
[SQL: INSERT INTO trial_intermediate_values (trial_id, step, intermediate_value) VALUES (%(trial_id)s, %(step)s, %(intermediate_value)s)]
[parameters: {'trial_id': 10, 'step': 1, 'intermediate_value': inf}]
(Background on this error at: https://sqlalche.me/e/14/f405)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/study/_optimize.py", line 213, in _run_trial
    value_or_values = func(trial)
  File "/Users/mamu/Dev/pfn/code_snippets_for_optuna/test/inf-report.py", line 5, in objective
    trial.report(float(np.inf), 1)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/trial/_trial.py", line 506, in report
    self.storage.set_trial_intermediate_value(self._trial_id, step, value)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/storages/_cached_storage.py", line 293, in set_trial_intermediate_value
    self._backend.set_trial_intermediate_value(trial_id, step, intermediate_value)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/storages/_rdb/storage.py", line 754, in set_trial_intermediate_value
    self._set_trial_intermediate_value_without_commit(
  File "/Users/mamu/.pyenv/versions/3.9.9/lib/python3.9/contextlib.py", line 126, in __exit__
    next(self.gen)
  File "/Users/mamu/Dev/pfn/venv/lib/python3.9/site-packages/optuna/storages/_rdb/storage.py", line 71, in _create_scoped_session
    raise optuna.exceptions.StorageInternalError(message) from e
optuna.exceptions.StorageInternalError: An exception is raised during the commit. This typically happens due to invalid data in the commit, e.g. exceeding max length. 
An exception is raised during the commit. This typically happens due to invalid data in the commit, e.g. exceeding max length. 
MySQL     : Trial state is TrialState.FAIL
[I 2021-12-24 14:01:03,078] A new study created in RDB with name: no-name-da490862-bbd5-4e04-8b64-be71deddeb52
[I 2021-12-24 14:01:03,238] Trial 0 finished with value: 1.0 and parameters: {}. Best is trial 0 with value: 1.0.
PostgreSQL: Trial state is TrialState.COMPLETE
```
</details>

In addition, if we run the script `inf-tell.py`, we will get the following output. The script is provided in the section of `Reproducible examples`. I think we need to modify this behavior as explained in `Expected behavior`. Concretely, we should fix the behavior with MySQL to accept the `inf`.
<details>
<summary>output</summary>

```
[I 2021-12-24 15:26:11,042] A new study created in memory with name: no-name-7cc77d37-ffee-45a6-8488-8b66aeda30e8
In-memory : Trial state is TrialState.COMPLETE
[I 2021-12-24 15:26:11,105] A new study created in RDB with name: no-name-5200946f-3b52-4043-b477-74c84d1fa160
SQLite    : Trial state is TrialState.COMPLETE
[I 2021-12-24 15:26:11,287] A new study created in RDB with name: no-name-5821e24f-163e-4e3b-9702-6adb73fe2311
An exception is raised during the commit. This typically happens due to invalid data in the commit, e.g. exceeding max length. 
MySQL     : Trial state is TrialState.RUNNING
[I 2021-12-24 15:26:11,613] A new study created in RDB with name: no-name-218980c1-e3ff-48b8-b349-243b210034f7
PostgreSQL: Trial state is TrialState.COMPLETE
```
</details>

## Steps to reproduce

1. Install docker
2. Setup MySQL DB
```
docker run --name mysql -e MYSQL_ROOT_PASSWORD=test -p 3306:3306 -p 33060:33060 -d mysql:8.0.22
docker run --network host -it --rm mysql:8.0.22  mysql -h 127.0.0.1 -uroot -ptest -e "create database mysql;"
```
3. Setup PostgreSQL DB
```
docker run -it --rm --name postgres-test -e POSTGRES_PASSWORD=test -p 15432:5432 -d postgres
```
4. Run `inf-objectibe.py`, `inf-report.py`, and `inf-tell.py`.

## Reproducible examples (optional)
- `inf-objective.py`
```python
import optuna
import numpy as np

def objective(trial):
    return float(np.inf)

# In-memory storage
study = optuna.create_study()
study.optimize(objective, n_trials=1)
print(f"In-memory : Trial state is {study.trials[-1].state}")

# RDB storage (SQLite)
study = optuna.create_study(storage='sqlite:///../sqlite.db')
study.optimize(objective, n_trials=1)
print(f"SQLite    : Trial state is {study.trials[-1].state}")

# RDB storage (MySQL)
study = optuna.create_study(storage="mysql+pymysql://root:test@localhost/mysql")
try:
    study.optimize(objective, n_trials=1)
except Exception as e:
    print(e)
    print(f"MySQL     : Trial state is {study.trials[-1].state}")

# RDB storage (PostgreSQL)
study = optuna.create_study(storage="postgresql+psycopg2://postgres:test@localhost:15432/postgres")
study.optimize(objective, n_trials=1)
print(f"PostgreSQL: Trial state is {study.trials[-1].state}")
```

- `inf-report.py`
```python
import optuna
import numpy as np

def objective(trial):
    trial.report(float(np.inf), 1)
    return 1

# In-memory storage
study = optuna.create_study()
study.optimize(objective, n_trials=1)
print(f"In-memory : Trial state is {study.trials[-1].state}")

# RDB storage (SQLite)
study = optuna.create_study(storage='sqlite:///../sqlite.db')
study.optimize(objective, n_trials=1)
print(f"SQLite    : Trial state is {study.trials[-1].state}")

# RDB storage (MySQL)
study = optuna.create_study(storage="mysql+pymysql://root:test@localhost/mysql")
try:
    study.optimize(objective, n_trials=1)
except Exception as e:
    print(e)
    print(f"MySQL     : Trial state is {study.trials[-1].state}")

# RDB storage (PostgreSQL)
study = optuna.create_study(storage="postgresql+psycopg2://postgres:test@localhost:15432/postgres")
study.optimize(objective, n_trials=1)
print(f"PostgreSQL: Trial state is {study.trials[-1].state}")
``` 

- `inf-tel.py`
```python
import optuna
import numpy as np


# In-memory storage
study = optuna.create_study()
trial = study.ask()
study.tell(trial, float(np.inf))
print(f"In-memory : Trial state is {study.trials[-1].state}")

# RDB storage (SQLite)
study = optuna.create_study(storage='sqlite:///../sqlite.db')
trial = study.ask()
study.tell(trial, float(np.inf))
print(f"SQLite    : Trial state is {study.trials[-1].state}")

# RDB storage (MySQL)
study = optuna.create_study(storage="mysql+pymysql://root:test@localhost/mysql")
trial = study.ask()
try:
    study.tell(trial, float(np.inf))
except Exception as e:
    print(e)
    print(f"MySQL     : Trial state is {study.trials[-1].state}")

# RDB storage (PostgreSQL)
study = optuna.create_study(storage="postgresql+psycopg2://postgres:test@localhost:15432/postgres")
trial = study.ask()
study.tell(trial, float(np.inf))
print(f"PostgreSQL: Trial state is {study.trials[-1].state}")
```

## Additional context (optional)

<!-- Please add any other context or screenshots about the problem here. -->



Discussion:
I've looked into it and it seems like MySQL [does not support](https://dev.mysql.com/doc/refman/5.7/en/type-conversion.html) storing `+inf` or `-inf`:
>these properties imply that F -> D -> F conversions are lossless unless F is -inf, +inf, or NaN. The latter values are not supported because the SQL standard defines them as invalid values for FLOAT or DOUBLE.

My solution would be to store `+inf` as `sys.float_info.max` and `-inf` as `sys.float_info.min` with some thin translation layer (e.g. in `_check_and_convert_to_values`). This should provide expected behavior in all cases with only minor changes. Let me know what you think, I'm happy to implement it. 
@xadrianzetx Thank you for your interest in this task. I think your solution will work well enough for many use cases. Could you implement and submit a PR?
Great! I will submit a PR soon.

Resolving patch:
diff --git a/optuna/storages/_rdb/storage.py b/optuna/storages/_rdb/storage.py
--- a/optuna/storages/_rdb/storage.py
+++ b/optuna/storages/_rdb/storage.py
@@ -20,6 +20,7 @@
 import alembic.config
 import alembic.migration
 import alembic.script
+import numpy as np
 from sqlalchemy import func
 from sqlalchemy import orm
 from sqlalchemy.engine import create_engine
@@ -41,6 +42,10 @@
 from optuna.trial import TrialState
 
 
+_RDB_MAX_FLOAT = np.finfo(np.float32).max
+_RDB_MIN_FLOAT = np.finfo(np.float32).min
+
+
 _logger = optuna.logging.get_logger(__name__)
 
 
@@ -471,14 +476,17 @@ def get_all_study_summaries(self) -> List[StudySummary]:
                     best_trial_frozen = FrozenTrial(
                         best_trial.number,
                         TrialState.COMPLETE,
-                        value.value,
+                        self._lift_numerical_limit(value.value),
                         best_trial.datetime_start,
                         best_trial.datetime_complete,
                         param_dict,
                         param_distributions,
                         {i.key: json.loads(i.value_json) for i in user_attrs},
                         {i.key: json.loads(i.value_json) for i in system_attrs},
-                        {value.step: value.intermediate_value for value in intermediate},
+                        {
+                            value.step: self._lift_numerical_limit(value.intermediate_value)
+                            for value in intermediate
+                        },
                         best_trial.trial_id,
                     )
                 user_attrs = _user_attrs.get(study.study_id, [])
@@ -731,6 +739,25 @@ def get_trial_param(self, trial_id: int, param_name: str) -> float:
 
         return param_value
 
+    @staticmethod
+    def _ensure_numerical_limit(value: float) -> float:
+
+        # Max and min trial values that can be stored are limited by
+        # dialect. Most limiting one is MySQL which in current data
+        # model will store floats as single precision (32 bit).
+        # There is no support for +inf and -inf in this dialect.
+        return float(min(_RDB_MAX_FLOAT, max(_RDB_MIN_FLOAT, value)))
+
+    @staticmethod
+    def _lift_numerical_limit(value: float) -> float:
+
+        # Floats can't be compared for equality because they are
+        # approximate and not stored as exact values.
+        # https://dev.mysql.com/doc/refman/8.0/en/problems-with-float.html
+        if np.isclose(value, _RDB_MIN_FLOAT) or np.isclose(value, _RDB_MAX_FLOAT):
+            return float(np.sign(value) * float("inf"))
+        return value
+
     def set_trial_values(self, trial_id: int, values: Sequence[float]) -> None:
 
         with _create_scoped_session(self.scoped_session) as session:
@@ -745,6 +772,7 @@ def _set_trial_value_without_commit(
 
         trial = models.TrialModel.find_or_raise_by_id(trial_id, session)
         self.check_trial_is_updatable(trial_id, trial.state)
+        value = self._ensure_numerical_limit(value)
 
         trial_value = models.TrialValueModel.find_by_trial_and_objective(trial, objective, session)
         if trial_value is None:
@@ -770,6 +798,7 @@ def _set_trial_intermediate_value_without_commit(
 
         trial = models.TrialModel.find_or_raise_by_id(trial_id, session)
         self.check_trial_is_updatable(trial_id, trial.state)
+        intermediate_value = self._ensure_numerical_limit(intermediate_value)
 
         trial_intermediate_value = models.TrialIntermediateValueModel.find_by_trial_and_step(
             trial, step, session
@@ -932,14 +961,13 @@ def _get_trials(
 
         return trials
 
-    @staticmethod
-    def _build_frozen_trial_from_trial_model(trial: models.TrialModel) -> FrozenTrial:
+    def _build_frozen_trial_from_trial_model(self, trial: models.TrialModel) -> FrozenTrial:
 
         values: Optional[List[float]]
         if trial.values:
             values = [0 for _ in trial.values]
             for value_model in trial.values:
-                values[value_model.objective] = value_model.value
+                values[value_model.objective] = self._lift_numerical_limit(value_model.value)
         else:
             values = None
 
@@ -964,7 +992,10 @@ def _build_frozen_trial_from_trial_model(trial: models.TrialModel) -> FrozenTria
             system_attrs={
                 attr.key: json.loads(attr.value_json) for attr in trial.system_attributes
             },
-            intermediate_values={v.step: v.intermediate_value for v in trial.intermediate_values},
+            intermediate_values={
+                v.step: self._lift_numerical_limit(v.intermediate_value)
+                for v in trial.intermediate_values
+            },
             trial_id=trial.trial_id,
         )
 


Test patch:
diff --git a/tests/storages_tests/rdb_tests/test_with_server.py b/tests/storages_tests/rdb_tests/test_with_server.py
--- a/tests/storages_tests/rdb_tests/test_with_server.py
+++ b/tests/storages_tests/rdb_tests/test_with_server.py
@@ -102,6 +102,28 @@ def test_loaded_trials(storage_url: str) -> None:
     _check_trials(loaded_study.trials)
 
 
+@pytest.mark.parametrize(
+    "input,expected",
+    [
+        (float("inf"), float("inf")),
+        (-float("inf"), -float("inf")),
+        (np.finfo(np.float32).max, float("inf")),
+        (np.finfo(np.float32).min, -float("inf")),
+        (np.finfo(np.float64).max, float("inf")),
+        (np.finfo(np.float64).min, -float("inf")),
+    ],
+)
+def test_store_infinite_values(input: float, expected: float, storage_url: str) -> None:
+
+    storage = optuna.storages.RDBStorage(url=storage_url)
+    study_id = storage.create_new_study()
+    trial_id = storage.create_new_trial(study_id)
+    storage.set_trial_intermediate_value(trial_id, 1, input)
+    storage.set_trial_values(trial_id, (input,))
+    assert storage.get_trial(trial_id).value == expected
+    assert storage.get_trial(trial_id).intermediate_values[1] == expected
+
+
 def test_multiprocess(storage_url: str) -> None:
     n_workers = 8
     study_name = _STUDY_NAME