Files
2026-07-13 13:22:34 +08:00

193 lines
7.5 KiB
Python

from pathlib import Path
from unittest import mock
import pytest
from sqlalchemy.pool import NullPool
from sqlalchemy.pool.impl import QueuePool
from mlflow.exceptions import MlflowException
from mlflow.store.db import utils
def test_create_sqlalchemy_engine_inject_pool_options(monkeypatch):
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOL_SIZE", "2")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOL_RECYCLE", "3600")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_MAX_OVERFLOW", "4")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_ECHO", "TRUE")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "QueuePool")
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("mydb://host:port/")
mock_create_engine.assert_called_once_with(
"mydb://host:port/",
pool_pre_ping=True,
pool_size=2,
max_overflow=4,
pool_recycle=3600,
echo=True,
poolclass=QueuePool,
)
def test_create_sqlalchemy_engine_null_pool(monkeypatch):
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "NullPool")
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("mydb://host:port/")
mock_create_engine.assert_called_once_with(
"mydb://host:port/",
pool_pre_ping=True,
poolclass=NullPool,
)
def test_create_sqlalchemy_engine_invalid_pool(monkeypatch):
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "SomethingInvalid")
with mock.patch("sqlalchemy.create_engine"):
with pytest.raises(ValueError, match=r"Invalid poolclass parameter.*"):
utils.create_sqlalchemy_engine("mydb://host:port/")
def test_create_sqlalchemy_engine_no_pool_options(monkeypatch):
monkeypatch.delenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", raising=False)
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("mydb://host:port/")
mock_create_engine.assert_called_once_with("mydb://host:port/", pool_pre_ping=True)
def test_alembic_escape_logic():
url = "fakesql://cooluser%40stillusername:apassword@localhost:3306/testingdb"
config = utils._get_alembic_config(url)
assert config.get_main_option("sqlalchemy.url") == url
def test_create_sqlalchemy_engine_with_retry_success():
with (
mock.patch("sqlalchemy.inspect") as mock_sqlalchemy_inspect,
mock.patch(
"mlflow.store.db.utils.create_sqlalchemy_engine", return_value="Engine"
) as mock_create_sqlalchemy_engine,
mock.patch("time.sleep") as mock_sleep,
):
engine = utils.create_sqlalchemy_engine_with_retry("mydb://host:port/")
mock_create_sqlalchemy_engine.assert_called_once_with("mydb://host:port/")
mock_sqlalchemy_inspect.assert_called_once()
mock_sleep.assert_not_called()
assert engine == "Engine"
def test_create_sqlalchemy_engine_with_retry_success_after_third_call():
with (
mock.patch("sqlalchemy.inspect", side_effect=[Exception, Exception, "Inspect"]),
mock.patch(
"mlflow.store.db.utils.create_sqlalchemy_engine", return_value="Engine"
) as mock_create_sqlalchemy_engine,
mock.patch("time.sleep"),
):
engine = utils.create_sqlalchemy_engine_with_retry("mydb://host:port/")
assert mock_create_sqlalchemy_engine.mock_calls == [mock.call("mydb://host:port/")] * 3
assert engine == "Engine"
def test_create_sqlalchemy_engine_with_retry_fail():
with (
mock.patch(
"sqlalchemy.inspect",
side_effect=[Exception("failed")] * utils.MAX_RETRY_COUNT,
),
mock.patch(
"mlflow.store.db.utils.create_sqlalchemy_engine", return_value="Engine"
) as mock_create_sqlalchemy_engine,
mock.patch("time.sleep"),
):
with pytest.raises(Exception, match=r"failed"):
utils.create_sqlalchemy_engine_with_retry("mydb://host:port/")
assert (
mock_create_sqlalchemy_engine.mock_calls
== [mock.call("mydb://host:port/")] * utils.MAX_RETRY_COUNT
)
def test_mysql_ssl_params(monkeypatch):
monkeypatch.setenv("MLFLOW_MYSQL_SSL_CA", "/path/to/ca.pem")
monkeypatch.setenv("MLFLOW_MYSQL_SSL_CERT", "/path/to/cert.pem")
monkeypatch.setenv("MLFLOW_MYSQL_SSL_KEY", "/path/to/key.pem")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "NullPool")
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("mysql+pymysql://user@host:port/db")
# Check that create_engine was called with the right arguments
mock_create_engine.assert_called_once_with(
"mysql+pymysql://user@host:port/db",
pool_pre_ping=True,
connect_args={
"ssl_ca": "/path/to/ca.pem",
"ssl_cert": "/path/to/cert.pem",
"ssl_key": "/path/to/key.pem",
},
poolclass=NullPool,
)
def test_mysql_ssl_params_partial(monkeypatch):
monkeypatch.setenv("MLFLOW_MYSQL_SSL_CA", "/path/to/ca.pem")
monkeypatch.delenv("MLFLOW_MYSQL_SSL_CERT", raising=False)
monkeypatch.delenv("MLFLOW_MYSQL_SSL_KEY", raising=False)
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "NullPool")
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("mysql+pymysql://user@host:port/db")
# Check that create_engine was called with the right arguments
mock_create_engine.assert_called_once_with(
"mysql+pymysql://user@host:port/db",
pool_pre_ping=True,
connect_args={
"ssl_ca": "/path/to/ca.pem",
},
poolclass=NullPool,
)
def test_non_mysql_no_ssl_params(monkeypatch):
monkeypatch.setenv("MLFLOW_MYSQL_SSL_CA", "/path/to/ca.pem")
monkeypatch.setenv("MLFLOW_MYSQL_SSL_CERT", "/path/to/cert.pem")
monkeypatch.setenv("MLFLOW_MYSQL_SSL_KEY", "/path/to/key.pem")
monkeypatch.setenv("MLFLOW_SQLALCHEMYSTORE_POOLCLASS", "NullPool")
with mock.patch("sqlalchemy.create_engine") as mock_create_engine:
utils.create_sqlalchemy_engine("postgresql://user@host:port/db")
# Check that create_engine was called without connect_args
mock_create_engine.assert_called_once_with(
"postgresql://user@host:port/db",
pool_pre_ping=True,
poolclass=NullPool,
)
def test_check_sqlite_version_too_old():
with mock.patch("mlflow.store.db.utils.sqlite3.sqlite_version", "3.30.1"):
with pytest.raises(MlflowException, match=r"MLflow requires SQLite"):
utils._check_sqlite_version("sqlite:///test.db")
def test_make_parent_dirs_if_sqlite(tmp_path: Path) -> None:
nested_path = tmp_path / "a" / "b" / "c" / "test.db"
db_uri = f"sqlite:///{nested_path}"
assert not nested_path.parent.exists()
utils._make_parent_dirs_if_sqlite(db_uri)
assert nested_path.parent.exists()
def test_make_parent_dirs_if_sqlite_skips_memory_db() -> None:
# Should not raise any errors for in-memory databases
utils._make_parent_dirs_if_sqlite("sqlite:///:memory:")
utils._make_parent_dirs_if_sqlite("sqlite://")
def test_make_parent_dirs_if_sqlite_skips_non_sqlite() -> None:
# Should not raise any errors for non-SQLite URIs
utils._make_parent_dirs_if_sqlite("postgresql://localhost/db")
utils._make_parent_dirs_if_sqlite("mysql://localhost/db")