-
Notifications
You must be signed in to change notification settings - Fork 297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Run seed scripts in DbContainer #542
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -40,6 +40,25 @@ class MySqlContainer(DbContainer): | |||||
... with engine.begin() as connection: | ||||||
... result = connection.execute(sqlalchemy.text("select version()")) | ||||||
... version, = result.fetchone() | ||||||
|
||||||
The optional :code:`seed` param enables arbitrary SQL files to be loaded. This | ||||||
is perfect for schema and sample data. The format is a tuple, made up of the | ||||||
path to local data, and list of script files to load. Each script will be loaded | ||||||
by exec in the container, using the MySQL root user, before yielding the | ||||||
container. Any errors loading the scripts, will cause a ValueError. | ||||||
|
||||||
.. doctest:: | ||||||
|
||||||
>>> import sqlalchemy | ||||||
>>> from testcontainers.mysql import MySqlContainer | ||||||
>>> seed_data = ("../../tests/", ["schema.sql", "data.sql"]) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tradeoff: the docstring is ugly due to weird relative path to real sql files, but if I write what I mean:
Suggested change
Well then the doctests fail, which won't work either! |
||||||
>>> with MySqlContainer(seed=seed_data) as mysql: | ||||||
... engine = sqlalchemy.create_engine(mysql.get_connection_url()) | ||||||
... with engine.begin() as connection: | ||||||
... query = "select * from stuff" # Can now rely on schema/data | ||||||
... result = connection.execute(sqlalchemy.text(query)) | ||||||
... first_stuff, = result.fetchone() | ||||||
|
||||||
""" | ||||||
|
||||||
def __init__( | ||||||
|
@@ -50,6 +69,7 @@ def __init__( | |||||
password: Optional[str] = None, | ||||||
dbname: Optional[str] = None, | ||||||
port: int = 3306, | ||||||
seed: Optional[tuple[str, list[str]]] = None, | ||||||
**kwargs, | ||||||
) -> None: | ||||||
raise_for_deprecated_parameter(kwargs, "MYSQL_USER", "username") | ||||||
|
@@ -65,6 +85,7 @@ def __init__( | |||||
self.password = password or environ.get("MYSQL_PASSWORD", "test") | ||||||
self.dbname = dbname or environ.get("MYSQL_DATABASE", "test") | ||||||
|
||||||
self._setup_seed(seed) | ||||||
if self.username == "root": | ||||||
self.root_password = self.password | ||||||
|
||||||
|
@@ -86,3 +107,15 @@ def get_connection_url(self) -> str: | |||||
return super()._create_connection_url( | ||||||
dialect="mysql+pymysql", username=self.username, password=self.password, dbname=self.dbname, port=self.port | ||||||
) | ||||||
|
||||||
def _seed(self) -> None: | ||||||
"""Apply the seed scripts given""" | ||||||
if not self.seed_scripts: # Defined in DbContainer._setup_seed(s) | ||||||
return | ||||||
container = self.get_wrapped_container() | ||||||
for script in self.seed_scripts: | ||||||
mysql_query = f"source {self.seed_mount}/{script}" | ||||||
schema_cmd = ["mysql", f"-p{self.root_password}", self.dbname, "-e", mysql_query] | ||||||
exit_code, _out = container.exec_run(schema_cmd) | ||||||
if exit_code != 0: | ||||||
raise ValueError(f"Error seeding the database with {script=}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
-- Sample SQL schema, no data | ||
CREATE TABLE `stuff` ( | ||
`id` mediumint NOT NULL AUTO_INCREMENT, | ||
`name` VARCHAR(63) NOT NULL, | ||
PRIMARY KEY (`id`) | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
-- Sample data, to be loaded after the schema | ||
INSERT INTO stuff (name) | ||
VALUES ("foo"), ("bar"), ("qux"), ("frob"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,26 @@ def test_docker_run_legacy_mysql(): | |
assert row[0].startswith("5.7.44") | ||
|
||
|
||
@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") | ||
def test_docker_run_mysql_8_seed(): | ||
seeds = ("modules/mysql/tests/", ["schema.sql", "seeds.sql"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These paths are hardcoded, assuming Leaving it up to maintainers to agree if that's good enough, of if a more obscure |
||
config = MySqlContainer("mysql:8", seed=seeds) | ||
with config as mysql: | ||
engine = sqlalchemy.create_engine(mysql.get_connection_url()) | ||
with engine.begin() as connection: | ||
result = connection.execute(sqlalchemy.text("select * from stuff")) | ||
assert len(list(result)) == 4, "Should have gotten all the stuff" | ||
|
||
|
||
@pytest.mark.skipif(is_arm(), reason="mysql container not available for ARM") | ||
def test_docker_run_mysql_8_seed_missing(): | ||
seeds = ("modules/mysql/tests/", ["schema.sql", "nosuchfile.sql"]) | ||
config = MySqlContainer("mysql:8", seed=seeds) | ||
with pytest.raises(ValueError, match="Error seeding the database with script"): | ||
with config as mysql: | ||
pass | ||
|
||
|
||
@pytest.mark.parametrize("version", ["11.3.2", "10.11.7"]) | ||
def test_docker_run_mariadb(version: str): | ||
with MySqlContainer(f"mariadb:{version}") as mariadb: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just realized we of course shouldn't penalize DbContainer variants for not defining this method, should do something like
if not self.seeds: return
and only otherwise raise this error