Skip to content

Set precision of sqlalchemy.Float in RDBStorage table definition#3327

Merged
contramundum53 merged 7 commits intooptuna:masterfrom
toshihikoyanase:set-precision-of-float-in-rdb
May 13, 2022
Merged

Set precision of sqlalchemy.Float in RDBStorage table definition#3327
contramundum53 merged 7 commits intooptuna:masterfrom
toshihikoyanase:set-precision-of-float-in-rdb

Conversation

@toshihikoyanase
Copy link
Copy Markdown
Member

@toshihikoyanase toshihikoyanase commented Feb 22, 2022

Motivation

Related to #3227.
I found pandas set the precision option of sqlalchemy.Float to 53 in .https://github.com/pandas-dev/pandas/blob/af8ad6d8b0db8ea98dab98ae77a48092ba3832f8/pandas/io/sql.py#L1149-L1186. This change will change the column type of the trial value from float to double when I confirmed the table definition in MySQL 5.8.

I guess the value 53 comes from IEEE 754, but I'm not sure.

Current master

CREATE TABLE `trial_values` (
  `trial_value_id` int(11) NOT NULL AUTO_INCREMENT,
  `trial_id` int(11) NOT NULL,
  `objective` int(11) NOT NULL,
  `value` float NOT NULL,
  PRIMARY KEY (`trial_value_id`),
  UNIQUE KEY `trial_id` (`trial_id`,`objective`),
  CONSTRAINT `trial_values_ibfk_1` FOREIGN KEY (`trial_id`) REFERENCES `trials` (`trial_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

This PR

CREATE TABLE `trial_values` (
  `trial_value_id` int(11) NOT NULL AUTO_INCREMENT,
  `trial_id` int(11) NOT NULL,
  `objective` int(11) NOT NULL,
  `value` double NOT NULL,
  PRIMARY KEY (`trial_value_id`),
  UNIQUE KEY `trial_id` (`trial_id`,`objective`),
  CONSTRAINT `trial_values_ibfk_1` FOREIGN KEY (`trial_id`) REFERENCES `trials` (`trial_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;

Description of the changes

  • Set precision=53 to sqlalchemy.Float
    • intermediate_value of trial_intermediate_values table
    • param_value of trial_params table
    • value of trial_values table
  • Change nullable of intermediate_value of trial_intermediate_values table

Data types of the other RDB

SQLite3

REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.
https://www.sqlite.org/datatype3.html

PostgreSQL

PostgreSQL also supports the SQL-standard notations float and float(p) for specifying inexact numeric types. Here, p specifies the minimum acceptable precision in binary digits. PostgreSQL accepts float(1) to float(24) as selecting the real type, while float(25) to float(53) select double precision. Values of p outside the allowed range draw an error. float with no precision specified is taken to mean double precision.
https://www.postgresql.org/docs/current/datatype-numeric.html

TODO

  • Add migration script
  • Check the behavior of the other database (i.e., SQLite3 and PostgreSQL.)
  • Resolve conflict

@github-actions github-actions bot added the optuna.storages Related to the `optuna.storages` submodule. This is automatically labeled by github-actions. label Feb 22, 2022
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 8, 2022

This pull request has not seen any recent activity.

@github-actions github-actions bot added the stale Exempt from stale bot labeling. label Mar 8, 2022
@toshihikoyanase
Copy link
Copy Markdown
Member Author

I'm still working on this PR.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 10, 2022

Codecov Report

Merging #3327 (f29ef89) into master (c245717) will decrease coverage by 0.01%.
The diff coverage is 76.92%.

@@            Coverage Diff             @@
##           master    #3327      +/-   ##
==========================================
- Coverage   91.48%   91.47%   -0.02%     
==========================================
  Files         158      159       +1     
  Lines       12241    12270      +29     
==========================================
+ Hits        11199    11224      +25     
- Misses       1042     1046       +4     
Impacted Files Coverage Δ
optuna/storages/_rdb/alembic/versions/v3.0.0.b.py 71.42% <71.42%> (ø)
optuna/storages/_rdb/alembic/env.py 75.00% <100.00%> (ø)
optuna/storages/_rdb/models.py 98.79% <100.00%> (+<0.01%) ⬆️
optuna/study/study.py 96.19% <0.00%> (ø)
optuna/trial/_trial.py 100.00% <0.00%> (ø)
optuna/samplers/_qmc.py 95.09% <0.00%> (ø)
optuna/trial/_frozen.py 97.91% <0.00%> (ø)
optuna/samplers/_grid.py 100.00% <0.00%> (ø)
optuna/samplers/_cmaes.py 98.97% <0.00%> (ø)
optuna/importance/_base.py 96.36% <0.00%> (ø)
... and 25 more

📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more

@github-actions github-actions bot removed the stale Exempt from stale bot labeling. label Mar 10, 2022
@toshihikoyanase toshihikoyanase added the enhancement Change that does not break compatibility and not affect public interfaces, but improves performance. label Mar 11, 2022
@himkt himkt self-assigned this Mar 14, 2022
@github-actions
Copy link
Copy Markdown
Contributor

This pull request has not seen any recent activity.

@github-actions github-actions bot added the stale Exempt from stale bot labeling. label Mar 28, 2022
@himkt
Copy link
Copy Markdown
Member

himkt commented Apr 12, 2022

[memo]

[at]c-bata suggested to use filter warning in a migration script, which avoids to show deprecation messages to users.
(It had to do in my PR as well. 🙇)

@github-actions github-actions bot removed the stale Exempt from stale bot labeling. label Apr 12, 2022
@toshihikoyanase
Copy link
Copy Markdown
Member Author

#3348 requires migration script and it will be conflict with this PR. I'll restart this PR after #3348 is merged or closed.

@toshihikoyanase toshihikoyanase force-pushed the set-precision-of-float-in-rdb branch from bc74f1f to 8ea3644 Compare April 27, 2022 07:22
from sqlalchemy import pool

import optuna
import optuna.storages._rdb.models
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was required to avoid the following import error.

$ alembic revision --autogenerate  --rev-id v3.0.0.b
Traceback (most recent call last):
  File "/Users/yanase/pfn/code/optuna/.venv/bin/alembic", line 8, in <module>
    sys.exit(main())
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/config.py", line 588, in main
    CommandLine(prog=prog).main(argv=argv)
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/config.py", line 582, in main
    self.run_cmd(cfg, options)
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/config.py", line 559, in run_cmd
    fn(
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/command.py", line 227, in revision
    script_directory.run_env()
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/script/base.py", line 563, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/util/pyfiles.py", line 92, in load_python_file
    module = load_module_py(module_id, path)
  File "/Users/yanase/pfn/code/optuna/.venv/lib/python3.9/site-packages/alembic/util/pyfiles.py", line 108, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "alembic/env.py", line 25, in <module>
    target_metadata = optuna.storages._rdb.models.BaseModel.metadata
AttributeError: module 'optuna.storages._rdb' has no attribute 'models'

@toshihikoyanase
Copy link
Copy Markdown
Member Author

  • Resolved the conflict
  • Changed nullable of intermediate values column from False to True (c.f., Accept nan in trial.report #3348)
  • Update import line of alembic/env.py

@himkt himkt removed their assignment Apr 27, 2022
@toshihikoyanase toshihikoyanase marked this pull request as ready for review April 28, 2022 07:43
@toshihikoyanase
Copy link
Copy Markdown
Member Author

I confirmed the change by using @himkt 's optuna-2e. The corresponding branch is this one.

To dump DB data, please use the following commands:

# SQLite3
echo ".dump" | sqlite3 data/sample.db

# MySQL
docker compose run --rm mysql bash
mysqldump -u root -h mysql -B optuna -p

# PostgreSQL
docker compose run --rm postgresql bash
pg_dump -U root -h postgresql optuna > pg.sql

@toshihikoyanase
Copy link
Copy Markdown
Member Author

@c-bata @contramundum53 Could you review this PR, please?

@xadrianzetx
Copy link
Copy Markdown
Collaborator

xadrianzetx commented May 7, 2022

I guess this means we should relax following limits to actually store double precision values.

_RDB_MAX_FLOAT = np.finfo(np.float32).max
_RDB_MIN_FLOAT = np.finfo(np.float32).min

Ref #3238 (comment)

@c-bata
Copy link
Copy Markdown
Member

c-bata commented May 8, 2022

@xadrianzetx Yes, you are correct 👍 Actually I'm planning to fix the problem you pointed.
https://github.com/optuna/optuna/compare/master...c-bata:rdb-inf-handling?expand=1

@xadrianzetx
Copy link
Copy Markdown
Collaborator

Yeah, that's what I had in mind. My first solution was a bit too hacky.

Copy link
Copy Markdown
Member

@c-bata c-bata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your pull request. LGTM with nits!

It might be a bit controversial that this PR also contains a schema migration for #3348. I think it's acceptable since those schema changes are executed in one query like the following.

SQL queries of forward migration
2022-05-10T05:12:41.383794Z	   26 Query	ALTER TABLE trial_intermediate_values MODIFY intermediate_value FLOAT(53) NULL
2022-05-10T05:12:41.456748Z	   26 Query	ALTER TABLE trial_params MODIFY param_value FLOAT(53) NULL
2022-05-10T05:12:41.532337Z	   26 Query	ALTER TABLE trial_values MODIFY value FLOAT(53) NOT NULL
2022-05-10T05:12:41.605596Z	   26 Query	UPDATE alembic_version SET version_num='v3.0.0.b' WHERE alembic_version.version_num = 'v3.0.0.a'
2022-05-10T05:12:41.608029Z	   26 Query	COMMIT
SQL queries of backward migration
2022-05-10T05:11:38.376333Z	   25 Query	ALTER TABLE trial_intermediate_values MODIFY intermediate_value FLOAT NOT NULL
2022-05-10T05:11:38.466837Z	   25 Query	ALTER TABLE trial_params MODIFY param_value FLOAT NULL
2022-05-10T05:11:38.539438Z	   25 Query	ALTER TABLE trial_values MODIFY value FLOAT NOT NULL
2022-05-10T05:11:38.613731Z	   25 Query	UPDATE alembic_version SET version_num='v3.0.0.a' WHERE alembic_version.version_num = 'v3.0.0.b'
2022-05-10T05:11:38.616350Z	   25 Query	COMMIT

Here is a note about how table schemas are changed on SQLite3, MySQL, and PostgreSQL.

SQLite3

In SQLite3, the only data type that stores floating point numbers is REAL. REAL is a double precision floating point number, so the precision does not change before or after this PR.

Before running migration
sqlite> .schema trial_intermediate_values
CREATE TABLE trial_intermediate_values (
	trial_intermediate_value_id INTEGER NOT NULL,
	trial_id INTEGER NOT NULL,
	step INTEGER NOT NULL,
	intermediate_value FLOAT NOT NULL,
	PRIMARY KEY (trial_intermediate_value_id),
	UNIQUE (trial_id, step),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id)
);
sqlite> .schema trial_values
CREATE TABLE trial_values (
	trial_value_id INTEGER NOT NULL,
	trial_id INTEGER NOT NULL,
	objective INTEGER NOT NULL,
	value FLOAT NOT NULL,
	PRIMARY KEY (trial_value_id),
	UNIQUE (trial_id, objective),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id)
);
sqlite> .schema trial_params
CREATE TABLE trial_params (
	param_id INTEGER NOT NULL,
	trial_id INTEGER,
	param_name VARCHAR(512),
	param_value FLOAT,
	distribution_json TEXT,
	PRIMARY KEY (param_id),
	UNIQUE (trial_id, param_name),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id)
);
After running migration
sqlite> .schema trial_intermediate_values
CREATE TABLE IF NOT EXISTS "trial_intermediate_values" (
	trial_intermediate_value_id INTEGER NOT NULL,
	trial_id INTEGER NOT NULL,
	step INTEGER NOT NULL,
	intermediate_value FLOAT,
	PRIMARY KEY (trial_intermediate_value_id),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id),
	UNIQUE (trial_id, step)
);
sqlite> .schema trial_values
CREATE TABLE IF NOT EXISTS "trial_values" (
	trial_value_id INTEGER NOT NULL,
	trial_id INTEGER NOT NULL,
	objective INTEGER NOT NULL,
	value FLOAT NOT NULL,
	PRIMARY KEY (trial_value_id),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id),
	UNIQUE (trial_id, objective)
);
sqlite> .schema trial_params
CREATE TABLE IF NOT EXISTS "trial_params" (
	param_id INTEGER NOT NULL,
	trial_id INTEGER,
	param_name VARCHAR(512),
	param_value FLOAT,
	distribution_json TEXT,
	PRIMARY KEY (param_id),
	FOREIGN KEY(trial_id) REFERENCES trials (trial_id),
	UNIQUE (trial_id, param_name)
);

PostgreSQL

In PostgreSQL, real is a 4 bytes floating point data type and double precision is a 8 bytes floating point data type. SQLAlchemy's Float field actually uses double precision data type by default. so the precision does not change before or after this PR.

Before running migration
optuna=# \d trial_params
                                            Table "public.trial_params"
      Column       |          Type          | Collation | Nullable |                    Default
-------------------+------------------------+-----------+----------+------------------------------------------------
 param_id          | integer                |           | not null | nextval('trial_params_param_id_seq'::regclass)
 trial_id          | integer                |           |          |
 param_name        | character varying(512) |           |          |
 param_value       | double precision       |           |          |
 distribution_json | text                   |           |          |
Indexes:
    "trial_params_pkey" PRIMARY KEY, btree (param_id)
    "trial_params_trial_id_param_name_key" UNIQUE CONSTRAINT, btree (trial_id, param_name)
Foreign-key constraints:
    "trial_params_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id)

optuna=# \d trial_values
                                           Table "public.trial_values"
     Column     |       Type       | Collation | Nullable |                       Default
----------------+------------------+-----------+----------+------------------------------------------------------
 trial_value_id | integer          |           | not null | nextval('trial_values_trial_value_id_seq'::regclass)
 trial_id       | integer          |           | not null |
 objective      | integer          |           | not null |
 value          | double precision |           | not null |
Indexes:
    "trial_values_pkey" PRIMARY KEY, btree (trial_value_id)
    "trial_values_trial_id_objective_key" UNIQUE CONSTRAINT, btree (trial_id, objective)
Foreign-key constraints:
    "trial_values_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id)

optuna=# \d trial_intermediate_values
                                                        Table "public.trial_intermediate_values"
           Column            |       Type       | Collation | Nullable |                                    Default
-----------------------------+------------------+-----------+----------+--------------------------------------------------------------------------------
 trial_intermediate_value_id | integer          |           | not null | nextval('trial_intermediate_values_trial_intermediate_value_id_seq'::regclass)
 trial_id                    | integer          |           | not null |
 step                        | integer          |           | not null |
 intermediate_value          | double precision |           | not null |
Indexes:
    "trial_intermediate_values_pkey" PRIMARY KEY, btree (trial_intermediate_value_id)
    "trial_intermediate_values_trial_id_step_key" UNIQUE CONSTRAINT, btree (trial_id, step)
Foreign-key constraints:
    "trial_intermediate_values_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id
After running migration
optuna=# \d trial_params
                                            Table "public.trial_params"
      Column       |          Type          | Collation | Nullable |                    Default
-------------------+------------------------+-----------+----------+------------------------------------------------
 param_id          | integer                |           | not null | nextval('trial_params_param_id_seq'::regclass)
 trial_id          | integer                |           |          |
 param_name        | character varying(512) |           |          |
 param_value       | double precision       |           |          |
 distribution_json | text                   |           |          |
Indexes:
    "trial_params_pkey" PRIMARY KEY, btree (param_id)
    "trial_params_trial_id_param_name_key" UNIQUE CONSTRAINT, btree (trial_id, param_name)
Foreign-key constraints:
    "trial_params_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id)

optuna=# \d trial_intermediate_values
                                                        Table "public.trial_intermediate_values"
           Column            |       Type       | Collation | Nullable |                                    Default
-----------------------------+------------------+-----------+----------+--------------------------------------------------------------------------------
 trial_intermediate_value_id | integer          |           | not null | nextval('trial_intermediate_values_trial_intermediate_value_id_seq'::regclass)
 trial_id                    | integer          |           | not null |
 step                        | integer          |           | not null |
 intermediate_value          | double precision |           |          |
Indexes:
    "trial_intermediate_values_pkey" PRIMARY KEY, btree (trial_intermediate_value_id)
    "trial_intermediate_values_trial_id_step_key" UNIQUE CONSTRAINT, btree (trial_id, step)
Foreign-key constraints:
    "trial_intermediate_values_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id)

optuna=# \d trial_values
                                           Table "public.trial_values"
     Column     |       Type       | Collation | Nullable |                       Default
----------------+------------------+-----------+----------+------------------------------------------------------
 trial_value_id | integer          |           | not null | nextval('trial_values_trial_value_id_seq'::regclass)
 trial_id       | integer          |           | not null |
 objective      | integer          |           | not null |
 value          | double precision |           | not null |
Indexes:
    "trial_values_pkey" PRIMARY KEY, btree (trial_value_id)
    "trial_values_trial_id_objective_key" UNIQUE CONSTRAINT, btree (trial_id, objective)
Foreign-key constraints:
    "trial_values_trial_id_fkey" FOREIGN KEY (trial_id) REFERENCES trials(trial_id)

MySQL

In MySQL, we can specify precision of floating point number like FLOAT(p). According to a MySQL documentation, a precision from 24 to 53 results in an 8-byte double-precision DOUBLE column. 53 seems to be reasonable precision number.

I confirmed that the precision of each columns are changed from FLOAT to DOUBLE. Please see the following schemas for details.

Before running migration
mysql> desc trial_intermediate_values;
+-----------------------------+-------+------+-----+---------+----------------+
| Field                       | Type  | Null | Key | Default | Extra          |
+-----------------------------+-------+------+-----+---------+----------------+
| trial_intermediate_value_id | int   | NO   | PRI | NULL    | auto_increment |
| trial_id                    | int   | NO   | MUL | NULL    |                |
| step                        | int   | NO   |     | NULL    |                |
| intermediate_value          | float | NO   |     | NULL    |                |
+-----------------------------+-------+------+-----+---------+----------------+
4 rows in set (0.04 sec)

mysql> desc trial_values;
+----------------+-------+------+-----+---------+----------------+
| Field          | Type  | Null | Key | Default | Extra          |
+----------------+-------+------+-----+---------+----------------+
| trial_value_id | int   | NO   | PRI | NULL    | auto_increment |
| trial_id       | int   | NO   | MUL | NULL    |                |
| objective      | int   | NO   |     | NULL    |                |
| value          | float | NO   |     | NULL    |                |
+----------------+-------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> desc trial_params;
+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| param_id          | int          | NO   | PRI | NULL    | auto_increment |
| trial_id          | int          | YES  | MUL | NULL    |                |
| param_name        | varchar(512) | YES  |     | NULL    |                |
| param_value       | float        | YES  |     | NULL    |                |
| distribution_json | text         | YES  |     | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+
5 rows in set (0.03 sec)
After running migration
mysql> desc trial_intermediate_values;
+-----------------------------+--------+------+-----+---------+----------------+
| Field                       | Type   | Null | Key | Default | Extra          |
+-----------------------------+--------+------+-----+---------+----------------+
| trial_intermediate_value_id | int    | NO   | PRI | NULL    | auto_increment |
| trial_id                    | int    | NO   | MUL | NULL    |                |
| step                        | int    | NO   |     | NULL    |                |
| intermediate_value          | double | YES  |     | NULL    |                |
+-----------------------------+--------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

mysql> desc trial_values;
+----------------+--------+------+-----+---------+----------------+
| Field          | Type   | Null | Key | Default | Extra          |
+----------------+--------+------+-----+---------+----------------+
| trial_value_id | int    | NO   | PRI | NULL    | auto_increment |
| trial_id       | int    | NO   | MUL | NULL    |                |
| objective      | int    | NO   |     | NULL    |                |
| value          | double | NO   |     | NULL    |                |
+----------------+--------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> desc trial_params;
+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| param_id          | int          | NO   | PRI | NULL    | auto_increment |
| trial_id          | int          | YES  | MUL | NULL    |                |
| param_name        | varchar(512) | YES  |     | NULL    |                |
| param_value       | double       | YES  |     | NULL    |                |
| distribution_json | text         | YES  |     | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)

Co-authored-by: Masashi Shibata <c-bata@users.noreply.github.com>
Comment on lines +28 to +33
with op.batch_alter_table("trial_params") as batch_op:
batch_op.alter_column(
"param_value",
type_=Float(precision=FLOAT_PRECISION),
existing_nullable=True,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need param_value to be nullable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry but I couldn't understand your question. I didn't change the nullable setting of param_value, and I set existing_nullable=True.
Do you mean we need to replace existing_nullable=True with nullable=True?

@contramundum53
Copy link
Copy Markdown
Member

contramundum53 commented May 11, 2022

Otherwise this looks good to me. I couldn't test this change on mssql, but for other databases I confirmed that the migration works as intended.

@contramundum53 contramundum53 merged commit c23e17c into optuna:master May 13, 2022
@knshnb knshnb added this to the v3.0.0-b1 milestone Jun 3, 2022
@toshihikoyanase toshihikoyanase deleted the set-precision-of-float-in-rdb branch February 17, 2025 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Change that does not break compatibility and not affect public interfaces, but improves performance. optuna.storages Related to the `optuna.storages` submodule. This is automatically labeled by github-actions.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants