Skip to content

Commit cc2fe77

Browse files
committed
Tests: Add software tests for CrateDB/pandas
1 parent a98534c commit cc2fe77

File tree

5 files changed

+218
-3
lines changed

5 files changed

+218
-3
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
cratedb:
3333
image: crate/crate:nightly
3434
ports:
35+
- 4200:4200
3536
- 5432:5432
3637

3738
env:

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ release = [
7777
"twine<5",
7878
]
7979
test = [
80+
"crate[sqlalchemy]",
81+
"pandas<2.1",
8082
"pytest<8",
8183
"pytest-asyncio<1",
8284
"pytest-cov<5",
@@ -122,7 +124,7 @@ show_missing = true
122124

123125
[tool.ruff]
124126
line-length = 120
125-
127+
extend-ignore = ["PD901"]
126128
select = [
127129
# Bandit
128130
"S",
@@ -167,8 +169,8 @@ check = [
167169
]
168170

169171
format = [
170-
{ cmd = "black ." },
171-
{ cmd = "isort ." },
172+
{ cmd = "black sqlalchemy_postgresql_relaxed/ tests/" },
173+
{ cmd = "isort sqlalchemy_postgresql_relaxed/ tests/" },
172174
# Configure Ruff not to auto-fix (remove!) unused variables (F841) and `print` statements (T201).
173175
{ cmd = "ruff --fix --ignore=ERA --ignore=F401 --ignore=F841 --ignore=T20 ." },
174176
{ cmd = "pyproject-fmt pyproject.toml" },

tests/conftest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
4+
def pytest_addoption(parser):
5+
"""
6+
Register custom options to support invocation by cr8.
7+
Use cr8 to invoke a pytest test suite, see `run.sh`.
8+
9+
Example::
10+
11+
pytest -vvv --http-host 127.0.0.1 --http-port 4200 --psql-host 127.0.0.1 --psql-port 5432
12+
13+
https://github.com/mfussenegger/cr8
14+
15+
TODO: Refactor to `cratedb-toolkit` or `pytest-cratedb` in the long run.
16+
"""
17+
parser.addoption("--http-url", action="store", default="localhost:4200")
18+
parser.addoption("--http-host", action="store", default="localhost")
19+
parser.addoption("--http-port", action="store", default="4200")
20+
parser.addoption("--psql-host", action="store", default="localhost")
21+
parser.addoption("--psql-port", action="store", default="5432")
22+
23+
24+
@pytest.fixture
25+
def cratedb_http_url(pytestconfig):
26+
return pytestconfig.getoption("--http-url")
27+
28+
29+
@pytest.fixture
30+
def cratedb_http_host(pytestconfig):
31+
return pytestconfig.getoption("--http-host")
32+
33+
34+
@pytest.fixture
35+
def cratedb_http_port(pytestconfig):
36+
return pytestconfig.getoption("--http-port")
37+
38+
39+
@pytest.fixture
40+
def cratedb_psql_host(pytestconfig):
41+
return pytestconfig.getoption("--psql-host")
42+
43+
44+
@pytest.fixture
45+
def cratedb_psql_port(pytestconfig):
46+
return pytestconfig.getoption("--psql-port")

tests/test_cratedb_pandas_read.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pandas as pd
2+
import pytest
3+
import sqlalchemy as sa
4+
from pandas._testing import assert_frame_equal
5+
from sqlalchemy.ext.asyncio import create_async_engine
6+
7+
REFERENCE_FRAME = pd.DataFrame.from_records([{"mountain": "Mont Blanc", "height": 4808}])
8+
SQL_SELECT_STATEMENT = "SELECT mountain, height FROM sys.summits ORDER BY height DESC LIMIT 1;"
9+
10+
11+
def test_crate_read_sql(cratedb_http_host, cratedb_http_port):
12+
engine = sa.create_engine(
13+
url=f"crate://{cratedb_http_host}:{cratedb_http_port}",
14+
echo=True,
15+
)
16+
conn = engine.connect()
17+
df = pd.read_sql(sql=sa.text(SQL_SELECT_STATEMENT), con=conn)
18+
assert_frame_equal(df, REFERENCE_FRAME)
19+
20+
21+
def test_psycopg_read_sql(cratedb_psql_host, cratedb_psql_port):
22+
engine = sa.create_engine(
23+
url=f"postgresql+psycopg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
24+
isolation_level="AUTOCOMMIT",
25+
use_native_hstore=False,
26+
echo=True,
27+
)
28+
conn = engine.connect()
29+
df = pd.read_sql(sql=sa.text(SQL_SELECT_STATEMENT), con=conn)
30+
assert_frame_equal(df, REFERENCE_FRAME)
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_psycopg_async_read_sql(cratedb_psql_host, cratedb_psql_port):
35+
engine = create_async_engine(
36+
url=f"postgresql+psycopg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
37+
isolation_level="AUTOCOMMIT",
38+
use_native_hstore=False,
39+
echo=True,
40+
)
41+
42+
async with engine.begin() as conn:
43+
df = await conn.run_sync(read_sql_sync, sa.text(SQL_SELECT_STATEMENT))
44+
assert_frame_equal(df, REFERENCE_FRAME)
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_asyncpg_read_sql(cratedb_psql_host, cratedb_psql_port):
49+
engine = create_async_engine(
50+
url=f"postgresql+asyncpg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
51+
isolation_level="AUTOCOMMIT",
52+
echo=True,
53+
)
54+
55+
async with engine.begin() as conn:
56+
df = await conn.run_sync(read_sql_sync, sa.text(SQL_SELECT_STATEMENT))
57+
assert_frame_equal(df, REFERENCE_FRAME)
58+
59+
60+
def read_sql_sync(conn, stmt):
61+
"""
62+
Making pd.read_sql connection the first argument to make it compatible
63+
with conn.run_sync(), see https://stackoverflow.com/a/70861276.
64+
"""
65+
return pd.read_sql(stmt, conn)

tests/test_cratedb_pandas_write.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import pandas as pd
2+
import pytest
3+
import sqlalchemy as sa
4+
from pandas._testing import assert_frame_equal
5+
from sqlalchemy.ext.asyncio import create_async_engine
6+
7+
INPUT_FRAME = pd.DataFrame.from_records([{"id": 1, "name": "foo", "value": 42.42}])
8+
SQL_SELECT_STATEMENT = "SELECT * FROM doc.foo;"
9+
SQL_REFRESH_STATEMENT = "REFRESH TABLE doc.foo;"
10+
11+
12+
def test_crate_to_sql(cratedb_http_host, cratedb_http_port):
13+
# Connect to database.
14+
engine = sa.create_engine(
15+
url=f"crate://{cratedb_http_host}:{cratedb_http_port}",
16+
echo=True,
17+
)
18+
con = engine.connect()
19+
20+
# Insert data using pandas.
21+
df = INPUT_FRAME
22+
retval = df.to_sql(name="foo", con=con, if_exists="replace", index=False)
23+
assert retval == -1
24+
25+
# Synchronize table content.
26+
con.execute(sa.text(SQL_REFRESH_STATEMENT))
27+
28+
# Read back and verify data using pandas.
29+
df = pd.read_sql(sql=sa.text(SQL_SELECT_STATEMENT), con=con)
30+
assert_frame_equal(df, INPUT_FRAME)
31+
32+
33+
@pytest.mark.skip(reason="Needs COLLATE and pg_table_is_visible")
34+
def test_psycopg_to_sql(cratedb_psql_host, cratedb_psql_port):
35+
# Connect to database.
36+
engine = sa.create_engine(
37+
url=f"postgresql+psycopg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
38+
isolation_level="AUTOCOMMIT",
39+
use_native_hstore=False,
40+
echo=True,
41+
)
42+
conn = engine.connect()
43+
44+
# Insert data using pandas.
45+
df = INPUT_FRAME
46+
retval = df.to_sql(name="foo", con=conn, if_exists="replace", index=False)
47+
assert retval == -1
48+
49+
# Synchronize table content.
50+
conn.execute(sa.text(SQL_REFRESH_STATEMENT))
51+
52+
# Read back and verify data using pandas.
53+
df = pd.read_sql(sql=sa.text(SQL_SELECT_STATEMENT), con=conn)
54+
assert_frame_equal(df, INPUT_FRAME)
55+
56+
57+
@pytest.mark.skip(reason="Needs COLLATE and pg_table_is_visible")
58+
@pytest.mark.asyncio
59+
async def test_psycopg_async_to_sql(cratedb_psql_host, cratedb_psql_port):
60+
# Connect to database.
61+
engine = create_async_engine(
62+
url=f"postgresql+psycopg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
63+
isolation_level="AUTOCOMMIT",
64+
use_native_hstore=False,
65+
echo=True,
66+
)
67+
68+
# Insert data using pandas.
69+
async with engine.begin() as conn:
70+
df = INPUT_FRAME
71+
retval = await conn.run_sync(to_sql_sync, df=df, name="foo", if_exists="replace", index=False)
72+
assert retval == -1
73+
74+
# TODO: Read back dataframe and compare with original.
75+
76+
77+
@pytest.mark.skip(reason="Needs COLLATE and pg_table_is_visible")
78+
@pytest.mark.asyncio
79+
async def test_asyncpg_to_sql(cratedb_psql_host, cratedb_psql_port):
80+
# Connect to database.
81+
engine = create_async_engine(
82+
url=f"postgresql+asyncpg_relaxed://crate@{cratedb_psql_host}:{cratedb_psql_port}",
83+
isolation_level="AUTOCOMMIT",
84+
echo=True,
85+
)
86+
87+
# Insert data using pandas.
88+
async with engine.begin() as conn:
89+
df = INPUT_FRAME
90+
retval = await conn.run_sync(to_sql_sync, df=df, name="foo", if_exists="replace", index=False)
91+
assert retval == -1
92+
93+
# TODO: Read back dataframe and compare with original.
94+
95+
96+
def to_sql_sync(conn, df, name, **kwargs):
97+
"""
98+
Making df.to_sql connection the first argument to make it compatible
99+
with conn.run_sync(), see https://stackoverflow.com/a/70861276.
100+
"""
101+
return df.to_sql(name=name, con=conn, **kwargs)

0 commit comments

Comments
 (0)