Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/example/note/sqlite_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""SQLite-backed Note repository using SQLAlchemy Core."""

from nene2.database import DatabaseQueryExecutorInterface

from .entity import Note
from .repository import NoteRepositoryInterface


class SqliteNoteRepository(NoteRepositoryInterface):
"""Persistent Note repository backed by a SQL database via SQLAlchemy Core."""

def __init__(self, executor: DatabaseQueryExecutorInterface) -> None:
self._executor = executor

def find_all(self, limit: int, offset: int) -> list[Note]:
rows = self._executor.fetch_all(
"SELECT id, title, body FROM notes ORDER BY id LIMIT :limit OFFSET :offset",
{"limit": limit, "offset": offset},
)
return [Note(id=row["id"], title=row["title"], body=row["body"]) for row in rows]

def find_by_id(self, note_id: int) -> Note | None:
row = self._executor.fetch_one(
"SELECT id, title, body FROM notes WHERE id = :id",
{"id": note_id},
)
return Note(id=row["id"], title=row["title"], body=row["body"]) if row else None

def save(self, title: str, body: str) -> Note:
new_id = self._executor.write(
"INSERT INTO notes (title, body) VALUES (:title, :body)",
{"title": title, "body": body},
)
return Note(id=new_id, title=title, body=body)

def update(self, note_id: int, title: str, body: str) -> Note | None:
affected = self._executor.write(
"UPDATE notes SET title = :title, body = :body WHERE id = :id",
{"title": title, "body": body, "id": note_id},
)
return Note(id=note_id, title=title, body=body) if affected > 0 else None

def delete(self, note_id: int) -> bool:
affected = self._executor.write(
"DELETE FROM notes WHERE id = :id",
{"id": note_id},
)
return affected > 0

def count(self) -> int:
row = self._executor.fetch_one("SELECT COUNT(*) AS cnt FROM notes")
return int(row["cnt"]) if row else 0
52 changes: 52 additions & 0 deletions src/example/tag/sqlite_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""SQLite-backed Tag repository using SQLAlchemy Core."""

from nene2.database import DatabaseQueryExecutorInterface

from .entity import Tag
from .repository import TagRepositoryInterface


class SqliteTagRepository(TagRepositoryInterface):
"""Persistent Tag repository backed by a SQL database via SQLAlchemy Core."""

def __init__(self, executor: DatabaseQueryExecutorInterface) -> None:
self._executor = executor

def find_all(self, limit: int, offset: int) -> list[Tag]:
rows = self._executor.fetch_all(
"SELECT id, name FROM tags ORDER BY id LIMIT :limit OFFSET :offset",
{"limit": limit, "offset": offset},
)
return [Tag(id=row["id"], name=row["name"]) for row in rows]

def find_by_id(self, tag_id: int) -> Tag | None:
row = self._executor.fetch_one(
"SELECT id, name FROM tags WHERE id = :id",
{"id": tag_id},
)
return Tag(id=row["id"], name=row["name"]) if row else None

def save(self, name: str) -> Tag:
new_id = self._executor.write(
"INSERT INTO tags (name) VALUES (:name)",
{"name": name},
)
return Tag(id=new_id, name=name)

def update(self, tag_id: int, name: str) -> Tag | None:
affected = self._executor.write(
"UPDATE tags SET name = :name WHERE id = :id",
{"name": name, "id": tag_id},
)
return Tag(id=tag_id, name=name) if affected > 0 else None

def delete(self, tag_id: int) -> bool:
affected = self._executor.write(
"DELETE FROM tags WHERE id = :id",
{"id": tag_id},
)
return affected > 0

def count(self) -> int:
row = self._executor.fetch_one("SELECT COUNT(*) AS cnt FROM tags")
return int(row["cnt"]) if row else 0
89 changes: 89 additions & 0 deletions tests/example/note/test_note_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Repository contract tests — run against both InMemory and SQLite implementations."""

import pytest
from sqlalchemy import create_engine

from nene2.database import SqlAlchemyQueryExecutor
from src.example.note.entity import Note
from src.example.note.repository import InMemoryNoteRepository, NoteRepositoryInterface
from src.example.note.sqlite_repository import SqliteNoteRepository


def _create_schema(executor: SqlAlchemyQueryExecutor) -> None:
executor.write(
"CREATE TABLE IF NOT EXISTS notes ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"title TEXT NOT NULL,"
"body TEXT NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")"
)


def _sqlite_repo() -> SqliteNoteRepository:
engine = create_engine("sqlite:///:memory:")
executor = SqlAlchemyQueryExecutor(engine)
_create_schema(executor)
return SqliteNoteRepository(executor)


@pytest.fixture(params=["inmemory", "sqlite"])
def repo(request: pytest.FixtureRequest) -> NoteRepositoryInterface:
if request.param == "inmemory":
return InMemoryNoteRepository()
return _sqlite_repo()


def test_save_and_find_by_id(repo: NoteRepositoryInterface) -> None:
note = repo.save("Hello", "World")
found = repo.find_by_id(note.id)
assert found == note


def test_find_by_id_returns_none_when_missing(repo: NoteRepositoryInterface) -> None:
assert repo.find_by_id(9999) is None


def test_find_all_returns_saved_notes(repo: NoteRepositoryInterface) -> None:
repo.save("A", "a")
repo.save("B", "b")
items = repo.find_all(limit=10, offset=0)
assert len(items) == 2
assert items[0].title == "A"
assert items[1].title == "B"


def test_find_all_respects_limit_and_offset(repo: NoteRepositoryInterface) -> None:
for i in range(5):
repo.save(f"Note {i}", "body")
page = repo.find_all(limit=2, offset=2)
assert len(page) == 2
assert page[0].title == "Note 2"


def test_count_reflects_saved_notes(repo: NoteRepositoryInterface) -> None:
assert repo.count() == 0
repo.save("T", "B")
repo.save("T2", "B2")
assert repo.count() == 2


def test_update_changes_title_and_body(repo: NoteRepositoryInterface) -> None:
note = repo.save("Old", "old body")
updated = repo.update(note.id, "New", "new body")
assert updated == Note(id=note.id, title="New", body="new body")


def test_update_returns_none_for_missing_id(repo: NoteRepositoryInterface) -> None:
assert repo.update(9999, "T", "B") is None


def test_delete_removes_note(repo: NoteRepositoryInterface) -> None:
note = repo.save("T", "B")
assert repo.delete(note.id) is True
assert repo.find_by_id(note.id) is None


def test_delete_returns_false_for_missing_id(repo: NoteRepositoryInterface) -> None:
assert repo.delete(9999) is False
84 changes: 84 additions & 0 deletions tests/example/tag/test_tag_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Repository contract tests — run against both InMemory and SQLite implementations."""

import pytest
from sqlalchemy import create_engine

from nene2.database import SqlAlchemyQueryExecutor
from src.example.tag.entity import Tag
from src.example.tag.repository import InMemoryTagRepository, TagRepositoryInterface
from src.example.tag.sqlite_repository import SqliteTagRepository


def _create_schema(executor: SqlAlchemyQueryExecutor) -> None:
executor.write(
"CREATE TABLE IF NOT EXISTS tags ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL UNIQUE,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")"
)


def _sqlite_repo() -> SqliteTagRepository:
engine = create_engine("sqlite:///:memory:")
executor = SqlAlchemyQueryExecutor(engine)
_create_schema(executor)
return SqliteTagRepository(executor)


@pytest.fixture(params=["inmemory", "sqlite"])
def repo(request: pytest.FixtureRequest) -> TagRepositoryInterface:
if request.param == "inmemory":
return InMemoryTagRepository()
return _sqlite_repo()


def test_save_and_find_by_id(repo: TagRepositoryInterface) -> None:
tag = repo.save("python")
found = repo.find_by_id(tag.id)
assert found == tag


def test_find_by_id_returns_none_when_missing(repo: TagRepositoryInterface) -> None:
assert repo.find_by_id(9999) is None


def test_find_all_returns_saved_tags(repo: TagRepositoryInterface) -> None:
repo.save("python")
repo.save("fastapi")
items = repo.find_all(limit=10, offset=0)
assert len(items) == 2


def test_find_all_respects_limit_and_offset(repo: TagRepositoryInterface) -> None:
for name in ["a", "b", "c", "d", "e"]:
repo.save(name)
page = repo.find_all(limit=2, offset=2)
assert len(page) == 2


def test_count_reflects_saved_tags(repo: TagRepositoryInterface) -> None:
assert repo.count() == 0
repo.save("x")
repo.save("y")
assert repo.count() == 2


def test_update_changes_name(repo: TagRepositoryInterface) -> None:
tag = repo.save("old")
updated = repo.update(tag.id, "new")
assert updated == Tag(id=tag.id, name="new")


def test_update_returns_none_for_missing_id(repo: TagRepositoryInterface) -> None:
assert repo.update(9999, "x") is None


def test_delete_removes_tag(repo: TagRepositoryInterface) -> None:
tag = repo.save("temp")
assert repo.delete(tag.id) is True
assert repo.find_by_id(tag.id) is None


def test_delete_returns_false_for_missing_id(repo: TagRepositoryInterface) -> None:
assert repo.delete(9999) is False
Loading