Skip to content
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ pip install "wandern[postgresql]"
```bash
uv add wandern --extra postgresql
```
**Using mysql**
##### A note on the DSN format
Since MySQL doesn't support DSN format out of the box, `Wandern` expects a DSN string in the following format:
```env
mysql://user:pass@host:port/database_name?{query_string_parameters}
```
query parameters can include, `autocommit`, `use_ssl`, `charset`, etc.

**PIP**

```bash
pip install "wandern[mysql]"
```

**UV**

```bash
uv add wandern --extra mysql
```

## ⚡️ Quick Start

Expand Down Expand Up @@ -135,7 +154,7 @@ To see all the commands, see [Available commands](#available-commands)

- SQLite
- PostgreSQL
- MySQL (coming soon)
- MySQL
- MSSQL (coming soon)

## 🛠️ Available Commands
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Issues = "https://github.com/s-bose/wandern/issues"
postgresql = [
"psycopg[binary]>=3.2.9",
]
mysql = ["mysql-connector-python>=9.4.0"]
openai = ["pydantic-ai-slim[openai]>=0.7.4"]
google-genai = ["pydantic-ai-slim[google]>=0.7.4"]

Expand All @@ -54,6 +55,8 @@ dev-dependencies = ["pytest>=8.3.3", "pytest-asyncio>=0.24.0"]
dev = ["pre-commit>=4.3.0", "ruff>=0.12.7", "types-networkx>=3.4.2.20250509"]
test = [
"psycopg[binary]>=3.2.9",
"mysql-connector-python>=9.4.0",
"pytest-cov>=6.2.1",
"testcontainers[postgres]>=4.12.0",
"testcontainers[mysql]>=4.12.0",
]
39 changes: 39 additions & 0 deletions tests/databases/mysql/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os

import pytest
from testcontainers.mysql import MySqlContainer

from wandern.models import Config

mysql_container = MySqlContainer("mysql:8.0")


@pytest.fixture(scope="module", autouse=True)
def setup(request: pytest.FixtureRequest):
mysql_container.start()

def remove_container():
mysql_container.stop()

request.addfinalizer(remove_container)

os.environ["MYSQL_USERNAME"] = mysql_container.username
os.environ["MYSQL_PASSWORD"] = mysql_container.password
os.environ["MYSQL_DB"] = mysql_container.dbname
os.environ["MYSQL_PORT"] = str(mysql_container.get_exposed_port(3306))
os.environ["MYSQL_HOST"] = mysql_container.get_container_host_ip()


@pytest.fixture(scope="function")
def config():
dsn = (
f"mysql://{os.environ['MYSQL_USERNAME']}:"
f"{os.environ['MYSQL_PASSWORD']}@"
f"{os.environ['MYSQL_HOST']}:{os.environ['MYSQL_PORT']}"
f"/{os.environ['MYSQL_DB']}"
)

return Config(
dsn=dsn,
migration_dir="migrations",
)
73 changes: 73 additions & 0 deletions tests/databases/mysql/test_mysql_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import mysql.connector
import pytest

from wandern.databases.mysql import MySQLProvider
from wandern.exceptions import ConnectError


def test_connect(config):
"""Test connecting to MySQL."""
provider = MySQLProvider(config)
connection = provider.connect()

assert connection is not None
assert connection.is_connected()

connection.close()


def test_connect_invalid_dsn(config):
"""Test connecting with invalid DSN raises ConnectError."""
config.dsn = "mysql://invalid:pass@nonexistent:3306/testdb"
provider = MySQLProvider(config)

with pytest.raises(ConnectError):
provider.connect()


def test_create_table_migration(config):
"""Test creating migration table."""
provider = MySQLProvider(config)
provider.create_table_migration()

with provider.connect() as connection:
cursor = connection.cursor()
# Check if table exists
cursor.execute(
f"""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = '{config.migration_table}'
"""
)
result = cursor.fetchone()
assert result[0] == 1

# Cleanup
provider.drop_table_migration()


def test_drop_table_migration(config):
"""Test dropping migration table."""
provider = MySQLProvider(config)

# First create the table
provider.create_table_migration()

# Then drop it
provider.drop_table_migration()

with provider.connect() as connection:
cursor = connection.cursor()
# Check if table no longer exists
cursor.execute(
f"""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = '{config.migration_table}'
"""
)
result = cursor.fetchone()
assert result[0] == 0
Loading
Loading