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:
parent
7937c9603c
commit
f6670ebf22
2 changed files with 113 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue