Skip to content

Commit c3ae27e

Browse files
committed
tests for sql core
1 parent 697dffe commit c3ae27e

5 files changed

Lines changed: 126 additions & 40 deletions

File tree

.github/workflows/ci-test.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
matrix:
2323
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
2424
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
25-
backend: ["local", "mongodb"]
25+
backend: ["local", "mongodb", "postgres"]
2626
exclude:
2727
# ToDo: take if back when the connection become stable
2828
# or resolve using `InMemoryMongoClient`
@@ -81,6 +81,25 @@ jobs:
8181
- name: Speed eval
8282
run: python tests/speed_eval.py
8383

84+
- name: Start PostgreSQL in docker
85+
if: matrix.backend == 'postgres'
86+
run: |
87+
docker run -d \
88+
-e POSTGRES_USER=testuser \
89+
-e POSTGRES_PASSWORD=testpass \
90+
-e POSTGRES_DB=testdb \
91+
-p 5432:5432 \
92+
--name postgres postgres:15
93+
# wait for PostgreSQL to start
94+
sleep 10
95+
docker ps -a
96+
97+
- name: Unit tests (SQL/Postgres)
98+
if: matrix.backend == 'postgres'
99+
env:
100+
SQLALCHEMY_DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
101+
run: pytest -m sql --cov=cachier --cov-report=term --cov-report=xml:cov.xml
102+
84103
- name: Upload coverage to Codecov
85104
continue-on-error: true
86105
uses: codecov/codecov-action@v5

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,11 @@ Note, however, that ``cachier``'s in-memory core is simple, and has no monitorin
345345
SQLAlchemy (SQL) Core
346346
---------------------
347347

348+
.. note::
349+
The SQL core requires SQLAlchemy to be installed. It is not installed by default with cachier. To use the SQL backend, run::
350+
351+
pip install SQLAlchemy
352+
348353
Cachier now supports a generic SQL backend via SQLAlchemy, allowing you to use SQLite, PostgreSQL, MySQL, and other databases.
349354

350355
**Usage Example (SQLite in-memory):**

src/cachier/cores/sql.py

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,55 @@
55
from datetime import datetime
66
from typing import Any, Callable, Optional, Tuple, Union
77

8-
from sqlalchemy import (
9-
Boolean,
10-
Column,
11-
DateTime,
12-
Index,
13-
LargeBinary,
14-
String,
15-
and_,
16-
create_engine,
17-
delete,
18-
insert,
19-
select,
20-
update,
21-
)
22-
from sqlalchemy.engine import Engine
23-
from sqlalchemy.orm import declarative_base, sessionmaker
8+
try:
9+
from sqlalchemy import (
10+
Boolean,
11+
Column,
12+
DateTime,
13+
Index,
14+
LargeBinary,
15+
String,
16+
and_,
17+
create_engine,
18+
delete,
19+
insert,
20+
select,
21+
update,
22+
)
23+
from sqlalchemy.engine import Engine
24+
from sqlalchemy.orm import declarative_base, sessionmaker
25+
26+
SQLALCHEMY_AVAILABLE = True
27+
except ImportError:
28+
SQLALCHEMY_AVAILABLE = False
29+
import warnings
30+
31+
warnings.warn(
32+
"`SQLAlchemy` was not found. SQL cores will not function.",
33+
ImportWarning,
34+
stacklevel=2,
35+
)
2436

2537
from .._types import HashFunc
2638
from ..config import CacheEntry
2739
from .base import RecalculationNeeded, _BaseCore, _get_func_str
2840

29-
Base = declarative_base()
30-
41+
if SQLALCHEMY_AVAILABLE:
42+
Base = declarative_base()
3143

32-
class CacheTable(Base):
33-
__tablename__ = "cachier_cache"
34-
id = Column(String, primary_key=True)
35-
function_id = Column(String, index=True, nullable=False)
36-
key = Column(String, index=True, nullable=False)
37-
value = Column(LargeBinary, nullable=True)
38-
timestamp = Column(DateTime, nullable=False)
39-
stale = Column(Boolean, default=False)
40-
processing = Column(Boolean, default=False)
41-
completed = Column(Boolean, default=False)
42-
__table_args__ = (Index("ix_func_key", "function_id", "key", unique=True),)
44+
class CacheTable(Base):
45+
__tablename__ = "cachier_cache"
46+
id = Column(String, primary_key=True)
47+
function_id = Column(String, index=True, nullable=False)
48+
key = Column(String, index=True, nullable=False)
49+
value = Column(LargeBinary, nullable=True)
50+
timestamp = Column(DateTime, nullable=False)
51+
stale = Column(Boolean, default=False)
52+
processing = Column(Boolean, default=False)
53+
completed = Column(Boolean, default=False)
54+
__table_args__ = (
55+
Index("ix_func_key", "function_id", "key", unique=True),
56+
)
4357

4458

4559
class _SQLCore(_BaseCore):
@@ -50,9 +64,14 @@ class _SQLCore(_BaseCore):
5064
def __init__(
5165
self,
5266
hash_func: Optional[HashFunc],
53-
sql_engine: Optional[Union[str, Engine, Callable[[], Engine]]],
67+
sql_engine: Optional[Union[str, "Engine", Callable[[], "Engine"]]],
5468
wait_for_calc_timeout: Optional[int] = None,
5569
):
70+
if not SQLALCHEMY_AVAILABLE:
71+
raise ImportError(
72+
"SQLAlchemy is required for the SQL core. "
73+
"Install with `pip install SQLAlchemy`."
74+
)
5675
super().__init__(
5776
hash_func=hash_func, wait_for_calc_timeout=wait_for_calc_timeout
5877
)

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ pandas
1414
collective.checkdocs
1515
pygments
1616
SQLAlchemy
17+
psycopg2-binary

tests/test_sql_core.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
from datetime import timedelta
44
from random import random
55
from time import sleep
6+
import sys
7+
import os
68

79
import pytest
810

911
from cachier import cachier
1012
from cachier.cores.sql import _SQLCore
1113

12-
SQLITE_MEMORY = "sqlite:///:memory:"
14+
SQL_CONN_STR = os.environ.get("SQLALCHEMY_DATABASE_URL", "sqlite:///:memory:")
1315

1416

1517
@pytest.mark.sql
1618
def test_sql_core_basic():
17-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
19+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
1820
def f(x, y):
1921
return random() + x + y
2022

@@ -34,7 +36,7 @@ def f(x, y):
3436

3537
@pytest.mark.sql
3638
def test_sql_core_keywords():
37-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
39+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
3840
def f(x, y):
3941
return random() + x + y
4042

@@ -56,7 +58,7 @@ def f(x, y):
5658
def test_sql_stale_after():
5759
@cachier(
5860
backend="sql",
59-
sql_engine=SQLITE_MEMORY,
61+
sql_engine=SQL_CONN_STR,
6062
stale_after=timedelta(seconds=2),
6163
next_time=False,
6264
)
@@ -74,7 +76,7 @@ def f(x, y):
7476

7577
@pytest.mark.sql
7678
def test_sql_overwrite_and_skip_cache():
77-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
79+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
7880
def f(x):
7981
return random() + x
8082

@@ -92,7 +94,7 @@ def f(x):
9294

9395
@pytest.mark.sql
9496
def test_sql_concurrency():
95-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
97+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
9698
def slow_func(x):
9799
sleep(1)
98100
return random() + x
@@ -119,7 +121,7 @@ def call():
119121

120122
@pytest.mark.sql
121123
def test_sql_clear_being_calculated():
122-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
124+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
123125
def slow_func(x):
124126
sleep(1)
125127
return random() + x
@@ -133,7 +135,7 @@ def slow_func(x):
133135

134136
@pytest.mark.sql
135137
def test_sql_missing_entry():
136-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
138+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
137139
def f(x):
138140
return x
139141

@@ -144,7 +146,7 @@ def f(x):
144146

145147
@pytest.mark.sql
146148
def test_sql_failed_write(monkeypatch):
147-
@cachier(backend="sql", sql_engine=SQLITE_MEMORY)
149+
@cachier(backend="sql", sql_engine=SQL_CONN_STR)
148150
def f(x):
149151
return x
150152

@@ -159,3 +161,43 @@ def fail_set_entry(self, key, func_res):
159161
with pytest.raises(Exception):
160162
f(1)
161163
monkeypatch.setattr(_SQLCore, "set_entry", orig)
164+
165+
166+
@pytest.mark.sql
167+
def test_import_cachier_without_sqlalchemy(monkeypatch):
168+
"""Test that importing cachier works when SQLAlchemy is missing.
169+
170+
This should work unless SQL core is used."""
171+
# Simulate SQLAlchemy not installed
172+
modules_backup = sys.modules.copy()
173+
sys.modules["sqlalchemy"] = None
174+
sys.modules["sqlalchemy.orm"] = None
175+
sys.modules["sqlalchemy.engine"] = None
176+
try:
177+
import importlib # noqa: F401
178+
import cachier # noqa: F401
179+
180+
# Should import fine
181+
finally:
182+
sys.modules.clear()
183+
sys.modules.update(modules_backup)
184+
185+
186+
@pytest.mark.sql
187+
def test_sqlcore_importerror_without_sqlalchemy(monkeypatch):
188+
"""Test that using SQL core without SQLAlchemy raises an ImportError."""
189+
# Simulate SQLAlchemy not installed
190+
modules_backup = sys.modules.copy()
191+
sys.modules["sqlalchemy"] = None
192+
sys.modules["sqlalchemy.orm"] = None
193+
sys.modules["sqlalchemy.engine"] = None
194+
try:
195+
import importlib
196+
197+
sql_mod = importlib.import_module("cachier.cores.sql")
198+
with pytest.raises(ImportError) as excinfo:
199+
sql_mod._SQLCore(hash_func=None, sql_engine="sqlite:///:memory:")
200+
assert "SQLAlchemy is required" in str(excinfo.value)
201+
finally:
202+
sys.modules.clear()
203+
sys.modules.update(modules_backup)

0 commit comments

Comments
 (0)