Files
stack/tests/zot/test_duck.py
kert 49f004a466
Some checks failed
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / mc (push) Successful in 11s
Deploy / report (push) Successful in 11s
CI / lint (push) Failing after 32s
Deploy / notebooks (push) Has been skipped
Infra CI / notebooks (push) Failing after 13s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Successful in 15s
Infra CI / api (push) Successful in 21s
CI / test (push) Has started running
perf(test): pytest-xdist on pre-commit hook + zotero_db fixture migration
- sem.hooks: add `-n auto` to the targeted pytest invocation. The hook
  already passes --no-cov so the xdist/cov-combining incompatibility
  doesn't apply here. Measured 3.1× speedup on cli/zot/rex subset
  (9:31 → 3:03 on the 32-core runner). Full-suite hook should drop
  proportionally.
- tests/zot/test_duck.py + tests/zot/test_extract.py: migrate
  create_db() callsites to the session-scoped zotero_db fixture
  (already in tests/conftest.py). Saves ~10s per test via shutil.copy2
  off the cached template instead of re-running schema creation.

Refs #388.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:52:07 -04:00

251 lines
7.8 KiB
Python

"""Tests for zot.duck — DuckDB engine for Zotero analytics."""
from __future__ import annotations
from pathlib import Path
import pytest
from zot.db import TYPE_MAP, Db
# We need a populated SQLite DB for DuckDB to attach
HOST_DB = Path("data/zotero/data/zotero.sqlite")
@pytest.fixture()
def sqlite_db(zotero_db) -> str:
"""Populated Zotero SQLite for testing — uses the session-scoped
template fixture (~10s saved per test by skipping create_db)."""
path = zotero_db
with Db(path) as db:
# Create a few items
id1 = db.create_item(TYPE_MAP["statute"])
db.set_fields(
id1, {"nameOfAct": "PFS 2026", "code": "FR", "url": "https://ex.com/pfs"}
)
db.sync_tags(id1, ["module:pfs", "year:2026"])
id2 = db.create_item(TYPE_MAP["journalArticle"])
db.set_fields(
id2,
{
"title": "Skin Substitutes Review",
"DOI": "10.1234/test",
"publicationTitle": "JAMA",
"date": "2025-06-01",
},
)
db.add_creators(id2, [("Jane", "Smith"), ("Bob", "Lee")])
db.sync_tags(id2, ["module:skin-subs", "year:2025"])
id3 = db.create_item(TYPE_MAP["webpage"])
db.set_fields(id3, {"title": "CMS Data", "url": "https://cms.gov/data"})
db.sync_tags(id3, ["module:pfs"])
key = db.ensure_collection("Test Collection")
db.add_to_collection(id1, collection_key=key)
db.add_to_collection(id2, collection_key=key)
db.commit()
return path
class TestAttach:
def test_attach_and_query(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
result = zdb.sql("SELECT count(*) FROM zot.items").fetchone()
assert result[0] == 3
def test_items_flat(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rel = zdb.items()
rows = rel.fetchall()
assert len(rows) == 3
# Check columns exist
cols = [d[0] for d in rel.description]
assert "title" in cols
assert "doi" in cols
assert "item_type" in cols
def test_tags_flat(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.tags().fetchall()
tags = {r[2] for r in rows} # tag column
assert "module:pfs" in tags
assert "module:skin-subs" in tags
def test_creators_flat(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.creators().fetchall()
names = {r[2] for r in rows} # last_name column
assert "Smith" in names
assert "Lee" in names
def test_collections_flat(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.collections().fetchall()
assert len(rows) == 2 # two items in collection
def test_items_by_type(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.items_by_type().fetchall()
types = {r[0]: r[1] for r in rows}
assert types.get("statute") == 1
assert types.get("journalArticle") == 1
assert types.get("webpage") == 1
def test_items_by_tag(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.items_by_tag().fetchall()
tags = {r[0]: r[1] for r in rows}
assert tags.get("module:pfs") == 2
def test_doi_coverage(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.doi_coverage().fetchall()
assert len(rows) > 0
class TestMirror:
def test_mirror_creates_native_tables(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.mirror(sqlite_db) as zdb:
count = zdb.sql("SELECT count(*) FROM items").fetchone()[0]
assert count == 3
def test_mirror_materialize(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.mirror(sqlite_db) as zdb:
zdb.materialize()
rows = zdb.sql("SELECT count(*) FROM flat_items").fetchone()
assert rows[0] == 3
def test_mirror_to_parquet(self, sqlite_db, tmp_path):
from zot.duck import DuckDb
with DuckDb.mirror(sqlite_db) as zdb:
zdb.materialize()
parquet_path = str(tmp_path / "items.parquet")
zdb.to_parquet(parquet_path)
assert Path(parquet_path).exists()
assert Path(parquet_path).stat().st_size > 0
class TestToDataFrame:
def test_to_polars(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
df = zdb.to_df()
assert len(df) == 3
assert "title" in df.columns
assert "doi" in df.columns
class TestFromDb:
"""Line 186: DuckDb.from_db constructs from an open Db instance."""
def test_from_db(self, sqlite_db):
from zot.duck import DuckDb
with Db(sqlite_db) as db:
with DuckDb.from_db(db) as zdb:
count = zdb.sql("SELECT count(*) FROM items").fetchone()[0]
assert count == 3
class TestSqlWithParams:
"""Line 193: sql() with params list."""
def test_sql_with_params(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
result = zdb.sql(
"SELECT count(*) FROM zot.items WHERE itemTypeID = ?",
[TYPE_MAP["statute"]],
).fetchone()
assert result[0] == 1
class TestItemsByYear:
"""Line 257: items_by_year analytics query."""
def test_items_by_year(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.items_by_year().fetchall()
assert len(rows) >= 1
class TestItemsByCollection:
"""Line 267: items_by_collection analytics query."""
def test_items_by_collection(self, sqlite_db):
from zot.duck import DuckDb
with DuckDb.attach(sqlite_db) as zdb:
rows = zdb.items_by_collection().fetchall()
assert len(rows) >= 1
# Should have our "Test Collection"
names = {r[0] for r in rows}
assert "Test Collection" in names
@pytest.mark.skipif(not HOST_DB.exists(), reason="zotero.sqlite not available")
class TestRealDb:
"""Integration tests against a snapshot of the real Zotero database.
Uses the session-scoped ``host_db`` fixture (see conftest.py) which
snapshots the live DB once — opening the live file directly fails
with ``database is locked`` while the Zotero container holds WAL.
"""
def test_attach_real_db(self, host_db):
from zot.duck import DuckDb
with DuckDb.attach(str(host_db)) as zdb:
count = zdb.sql("SELECT count(*) FROM zot.items").fetchone()[0]
assert count > 1000
def test_flat_items_real(self, host_db):
from zot.duck import DuckDb
with DuckDb.attach(str(host_db)) as zdb:
df = zdb.to_df()
assert len(df) > 100
assert "doi" in df.columns
def test_tag_co_occurrence_real(self, host_db):
from zot.duck import DuckDb
with DuckDb.attach(str(host_db)) as zdb:
rows = zdb.tag_co_occurrence(min_count=10).fetchall()
assert len(rows) > 0
def test_pdf_coverage_real(self, host_db):
from zot.duck import DuckDb
with DuckDb.attach(str(host_db)) as zdb:
rows = zdb.pdf_coverage().fetchall()
assert len(rows) > 0