Skip to content

Commit 8ddf196

Browse files
authored
feat: implement support for yugabyte (#84)
1 parent 264d526 commit 8ddf196

8 files changed

Lines changed: 785 additions & 115 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
uses: astral-sh/setup-uv@v7
4141

4242
- name: Intall dependencies
43-
run: uv sync --frozen
43+
run: uv sync --frozen --all-extras
4444

4545
- if: matrix.python-version == '3.12'
4646
name: Run tests with coverage tracking
@@ -81,7 +81,7 @@ jobs:
8181
uses: astral-sh/setup-uv@v7
8282

8383
- name: Intall dependencies
84-
run: uv sync --frozen
84+
run: uv sync --frozen --all-extras
8585

8686
- name: Run tests with coverage tracking
8787
run: uv run pytest -k elasticsearch

docs/supported-databases/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This section provides detailed information on the supported databases, including
1414
spanner
1515
bigquery
1616
cockroachdb
17+
yugabyte
1718
gizmosql
1819
redis
1920
valkey
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Yugabyte
2+
========
3+
4+
Integration with `Yugabyte DB <https://www.yugabyte.com/>`_
5+
6+
Installation
7+
------------
8+
9+
.. code-block:: bash
10+
11+
pip install pytest-databases[yugabyte]
12+
13+
Usage Example
14+
-------------
15+
16+
.. code-block:: python
17+
18+
import pytest
19+
import psycopg
20+
from pytest_databases.docker.yugabyte import YugabyteService
21+
22+
pytest_plugins = ["pytest_databases.docker.yugabyte"]
23+
24+
@pytest.fixture(scope="session")
25+
def yugabyte_uri(yugabyte_service: YugabyteService) -> str:
26+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
27+
return f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}"
28+
29+
def test_yugabyte_service(yugabyte_uri: str) -> None:
30+
with psycopg.connect(yugabyte_uri) as conn:
31+
db_open = conn.execute("SELECT 1").fetchone()
32+
assert db_open is not None and db_open[0] == 1
33+
34+
def test_yugabyte_connection(yugabyte_connection: psycopg.Connection) -> None:
35+
yugabyte_connection.execute("CREATE TABLE if not exists simple_table as SELECT 1")
36+
result = yugabyte_connection.execute("select * from simple_table").fetchone()
37+
assert result is not None and result[0] == 1
38+
39+
Available Fixtures
40+
------------------
41+
42+
* ``yugabyte_image``: The Docker image to use for Yugabyte DB.
43+
* ``yugabyte_service``: A fixture that provides a Yugabyte DB service.
44+
* ``yugabyte_connection``: A fixture that provides a Yugabyte DB connection.
45+
* ``yugabyte_driver_opts``: A fixture that provides driver options for Yugabyte DB.
46+
47+
Service API
48+
-----------
49+
50+
.. automodule:: pytest_databases.docker.yugabyte
51+
:members:
52+
:undoc-members:
53+
:show-inheritance:

pyproject.toml

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,34 +74,18 @@ postgres = ["psycopg>=3"]
7474
redis = ["redis"]
7575
spanner = ["google-cloud-spanner"]
7676
valkey = ["valkey"]
77+
yugabyte = ["psycopg2-yugabytedb"]
7778

7879
[dependency-groups]
80+
build = ["bump-my-version"]
7981
dev = [
80-
# tests
81-
"bump-my-version",
82-
"pytest-databases[azure-storage,bigquery,cockroachdb,dragonfly,elasticsearch7,elasticsearch8,gizmosql,keydb,mssql,mysql,mariadb,oracle,postgres,redis,spanner,minio,valkey]",
83-
"coverage[toml]>=6.2",
84-
"pytest",
85-
"pytest-cov",
86-
"pytest-cdist>=0.2",
87-
"pytest-mock",
88-
"pytest-click",
89-
"pytest-xdist",
90-
"slotscheck",
91-
"psycopg-binary", # This fixes tests failing on M series CPUs.
92-
# lint
93-
"mypy",
94-
"ruff",
95-
"pyright",
96-
"pre-commit",
97-
"types-click",
98-
"types-six",
99-
"types-decorator",
100-
"types-pyyaml",
101-
"types-docutils",
102-
"types-redis",
103-
"types-pymysql",
104-
# docs
82+
{ include-group = "extra" },
83+
{ include-group = "lint" },
84+
{ include-group = "doc" },
85+
{ include-group = "test" },
86+
{ include-group = "build" },
87+
]
88+
doc = [
10589
"auto-pytabs[sphinx]>=0.5.0",
10690
"shibuya",
10791
"sphinx>=7.0.0; python_version <= \"3.9\"",
@@ -118,6 +102,31 @@ dev = [
118102
"sphinx-autodoc-typehints",
119103
"sphinx-rtd-theme",
120104
]
105+
extra = ["psycopg2-binary", "psycopg[binary,pool]", "psycopg2-yugabytedb-binary"]
106+
lint = [
107+
"mypy",
108+
"ruff",
109+
"pyright",
110+
"pre-commit",
111+
"types-click",
112+
"types-six",
113+
"types-decorator",
114+
"types-pyyaml",
115+
"types-docutils",
116+
"types-redis",
117+
"types-pymysql",
118+
"slotscheck",
119+
]
120+
test = [
121+
"coverage[toml]>=6.2",
122+
"pytest",
123+
"pytest-cov",
124+
"pytest-cdist>=0.2",
125+
"pytest-mock",
126+
"pytest-click",
127+
"pytest-xdist",
128+
"pytest-sugar",
129+
]
121130

122131
##################
123132
# External Tools #
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
import psycopg
7+
import pytest
8+
9+
from pytest_databases._service import DockerService, ServiceContainer
10+
from pytest_databases.helpers import get_xdist_worker_num
11+
12+
if TYPE_CHECKING:
13+
from collections.abc import Generator
14+
15+
from pytest_databases.types import XdistIsolationLevel
16+
17+
18+
@pytest.fixture(scope="session")
19+
def xdist_yugabyte_isolation_level() -> XdistIsolationLevel:
20+
return "database"
21+
22+
23+
@dataclass
24+
class YugabyteService(ServiceContainer):
25+
database: str
26+
driver_opts: dict[str, str]
27+
28+
29+
@pytest.fixture(scope="session")
30+
def yugabyte_driver_opts() -> dict[str, str]:
31+
return {"sslmode": "disable"}
32+
33+
34+
@pytest.fixture(scope="session")
35+
def yugabyte_image() -> str:
36+
return "software.yugabyte.com/yugabytedb/yugabyte:latest"
37+
38+
39+
@pytest.fixture(scope="session")
40+
def yugabyte_service(
41+
docker_service: DockerService,
42+
xdist_yugabyte_isolation_level: XdistIsolationLevel,
43+
yugabyte_driver_opts: dict[str, str],
44+
yugabyte_image: str,
45+
) -> Generator[YugabyteService, None, None]:
46+
def yugabyte_responsive(_service: ServiceContainer) -> bool:
47+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_driver_opts.items()) if yugabyte_driver_opts else ""
48+
try:
49+
conn = psycopg.connect(f"postgresql://yugabyte:yugabyte@{_service.host}:{_service.port}/yugabyte?{opts}")
50+
except Exception: # noqa: BLE001
51+
return False
52+
53+
try:
54+
db_open = conn.execute("SELECT 1").fetchone()
55+
return bool(db_open is not None and db_open[0] == 1)
56+
finally:
57+
conn.close()
58+
59+
container_name = "yugabyte"
60+
db_name = "pytest_databases"
61+
worker_num = get_xdist_worker_num()
62+
if worker_num is not None:
63+
suffix = f"_{worker_num}"
64+
if xdist_yugabyte_isolation_level == "server":
65+
container_name += suffix
66+
else:
67+
db_name += suffix
68+
69+
with docker_service.run(
70+
image=yugabyte_image,
71+
container_port=5433, # YugabyteDB YSQL port (not CockroachDB's 26257)
72+
check=yugabyte_responsive,
73+
name=container_name,
74+
command="bin/yugabyted start --background=false",
75+
exec_after_start=f"sh -c 'bin/ysqlsh -h $(hostname) -U yugabyte -d yugabyte -c \"CREATE DATABASE {db_name};\"'",
76+
transient=xdist_yugabyte_isolation_level == "server",
77+
timeout=60, # YugabyteDB needs longer startup time
78+
) as service:
79+
yield YugabyteService(
80+
host=service.host,
81+
port=service.port,
82+
database=db_name,
83+
driver_opts=yugabyte_driver_opts,
84+
)
85+
86+
87+
@pytest.fixture(scope="session")
88+
def yugabyte_connection(
89+
yugabyte_service: YugabyteService,
90+
yugabyte_driver_opts: dict[str, str],
91+
) -> Generator[psycopg.Connection, None, None]:
92+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_driver_opts.items()) if yugabyte_driver_opts else ""
93+
with psycopg.connect(
94+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}"
95+
) as conn:
96+
yield conn

tests/test_elasticsearch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test(elasticsearch_7_service) -> None:
3535

3636
def test_elasticsearch_8(pytester: pytest.Pytester) -> None:
3737
pytester.makepyfile("""
38-
from elasticsearch7 import Elasticsearch
38+
from elasticsearch8 import Elasticsearch
3939
4040
pytest_plugins = ["pytest_databases.docker.elastic_search"]
4141

tests/test_yugabyte.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
import pytest
7+
8+
9+
def test_service_fixture(pytester: pytest.Pytester) -> None:
10+
pytester.makepyfile("""
11+
import pytest
12+
import psycopg
13+
from pytest_databases.docker.postgres import _make_connection_string # noqa: PLC2701
14+
15+
pytest_plugins = ["pytest_databases.docker.yugabyte"]
16+
17+
def test(yugabyte_service) -> None:
18+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
19+
with psycopg.connect(
20+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}"
21+
) as conn:
22+
db_open = conn.execute("SELECT 1").fetchone()
23+
assert db_open is not None and db_open[0] == 1
24+
""")
25+
26+
result = pytester.runpytest_subprocess("-p", "pytest_databases")
27+
result.assert_outcomes(passed=1)
28+
29+
30+
def test_startup_connection_fixture(pytester: pytest.Pytester) -> None:
31+
pytester.makepyfile("""
32+
import pytest
33+
import psycopg
34+
from pytest_databases.docker.postgres import _make_connection_string # noqa: PLC2701
35+
36+
37+
pytest_plugins = ["pytest_databases.docker.yugabyte"]
38+
39+
def test(yugabyte_connection) -> None:
40+
yugabyte_connection.execute("CREATE TABLE if not exists simple_table as SELECT 1")
41+
result = yugabyte_connection.execute("select * from simple_table").fetchone()
42+
assert result is not None and result[0] == 1
43+
""")
44+
45+
result = pytester.runpytest_subprocess("-p", "pytest_databases")
46+
result.assert_outcomes(passed=1)
47+
48+
49+
def test_xdist_isolate_database(pytester: pytest.Pytester) -> None:
50+
pytester.makepyfile("""
51+
import pytest
52+
import psycopg
53+
from pytest_databases.docker.postgres import _make_connection_string
54+
55+
pytest_plugins = ["pytest_databases.docker.yugabyte"]
56+
57+
def test_one(yugabyte_service) -> None:
58+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
59+
with psycopg.connect(
60+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}"
61+
) as conn:
62+
conn.execute("CREATE TABLE foo AS SELECT 1")
63+
64+
def test_two(yugabyte_service) -> None:
65+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
66+
with psycopg.connect(
67+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}"
68+
) as conn:
69+
conn.execute("CREATE TABLE foo AS SELECT 1")
70+
""")
71+
72+
result = pytester.runpytest_subprocess("-n", "2")
73+
result.assert_outcomes(passed=2)
74+
75+
76+
def test_xdist_isolate_server(pytester: pytest.Pytester) -> None:
77+
pytester.makepyfile("""
78+
import pytest
79+
import psycopg
80+
from pytest_databases.docker.postgres import _make_connection_string
81+
82+
pytest_plugins = ["pytest_databases.docker.yugabyte"]
83+
84+
@pytest.fixture(scope="session")
85+
def xdist_yugabyte_isolation_level():
86+
return "server"
87+
88+
def test_one(yugabyte_service) -> None:
89+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
90+
with psycopg.connect(
91+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}",
92+
autocommit=True,
93+
) as conn:
94+
conn.execute("CREATE DATABASE foo")
95+
96+
def test_two(yugabyte_service) -> None:
97+
opts = "&".join(f"{k}={v}" for k, v in yugabyte_service.driver_opts.items())
98+
with psycopg.connect(
99+
f"postgresql://yugabyte:yugabyte@{yugabyte_service.host}:{yugabyte_service.port}/{yugabyte_service.database}?{opts}",
100+
autocommit=True,
101+
) as conn:
102+
conn.execute("CREATE DATABASE foo")
103+
""")
104+
105+
result = pytester.runpytest_subprocess("-n", "2")
106+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)