feat(#251): SqliteStoreRepository — update/delete/find_by_name/clear_default_except

Étend le repo avec les méthodes manquantes pour le CRUD :
- find_by_name : valider unicité name au create/update
- update : remplace les champs mutables (id et created_at intacts)
- clear_default_except : promotion mono-default lors d'un changement de default
- delete : retourne True/False selon suppression effective

Tests dédiés sur chacune des nouvelles méthodes.
This commit is contained in:
Pier-Jean Malandrino 2026-05-04 12:18:05 +02:00
parent 7937c9603c
commit f6670ebf22
2 changed files with 113 additions and 0 deletions

View file

@ -73,6 +73,12 @@ class SqliteStoreRepository:
row = await cursor.fetchone()
return _row_to_store(row) if row else None
async def find_by_name(self, name: str) -> Store | None:
async with get_connection() as db:
cursor = await db.execute("SELECT * FROM stores WHERE name = ?", (name,))
row = await cursor.fetchone()
return _row_to_store(row) if row else None
async def get_default(self) -> Store | None:
"""Return the seeded `default` store, if any."""
async with get_connection() as db:
@ -81,3 +87,40 @@ class SqliteStoreRepository:
)
row = await cursor.fetchone()
return _row_to_store(row) if row else None
async def update(self, store: Store) -> None:
"""Replace all mutable fields of an existing store. `id` and `created_at`
are not touched."""
async with get_connection() as db:
await db.execute(
"""UPDATE stores
SET name = ?, slug = ?, kind = ?, embedder = ?,
config = ?, is_default = ?
WHERE id = ?""",
(
store.name,
store.slug,
store.kind.value,
store.embedder,
json.dumps(store.config),
1 if store.is_default else 0,
store.id,
),
)
await db.commit()
async def clear_default_except(self, store_id: str) -> None:
"""Reset `is_default = 0` on every store except the given id. Used to
enforce single-default invariant when promoting a new default."""
async with get_connection() as db:
await db.execute(
"UPDATE stores SET is_default = 0 WHERE id != ?",
(store_id,),
)
await db.commit()
async def delete(self, store_id: str) -> bool:
async with get_connection() as db:
cursor = await db.execute("DELETE FROM stores WHERE id = ?", (store_id,))
await db.commit()
return cursor.rowcount > 0

View file

@ -92,6 +92,76 @@ class TestStoreRepo:
all_stores = await store_repo.find_all()
assert all_stores[0].slug == "default" # seeded default comes first
async def test_find_by_name(self, store_repo):
await store_repo.insert(
Store(
id="s-rh",
name="rh-corpus",
slug="rh-corpus",
kind=StoreKind.OPENSEARCH,
embedder="bge-m3",
)
)
found = await store_repo.find_by_name("rh-corpus")
assert found is not None
assert found.slug == "rh-corpus"
assert await store_repo.find_by_name("missing") is None
async def test_update_replaces_mutable_fields(self, store_repo):
await store_repo.insert(
Store(
id="s-rh",
name="rh",
slug="rh",
kind=StoreKind.OPENSEARCH,
embedder="bge-m3",
config={"index_name": "rh"},
)
)
store = await store_repo.find_by_id("s-rh")
assert store is not None
store.embedder = "bge-large"
store.config = {"index_name": "rh-v2"}
store.name = "rh-v2"
await store_repo.update(store)
reloaded = await store_repo.find_by_id("s-rh")
assert reloaded is not None
assert reloaded.embedder == "bge-large"
assert reloaded.config == {"index_name": "rh-v2"}
assert reloaded.name == "rh-v2"
async def test_clear_default_except_promotes_single_winner(self, store_repo):
# Seeded default is the original; insert another candidate as default.
await store_repo.insert(
Store(
id="s-new",
name="new",
slug="new",
kind=StoreKind.OPENSEARCH,
embedder="bge-m3",
is_default=True,
)
)
await store_repo.clear_default_except("s-new")
defaults = [s for s in await store_repo.find_all() if s.is_default]
assert len(defaults) == 1
assert defaults[0].id == "s-new"
async def test_delete_returns_true_when_removed(self, store_repo):
await store_repo.insert(
Store(
id="s-tmp",
name="tmp",
slug="tmp",
kind=StoreKind.OPENSEARCH,
embedder="bge-m3",
)
)
assert await store_repo.delete("s-tmp") is True
assert await store_repo.find_by_id("s-tmp") is None
assert await store_repo.delete("s-tmp") is False
class TestDocumentStoreLinkRepo:
async def _seed_doc(self, document_repo, doc_id: str = "doc-1") -> Document: