feat(ttp): E.3.3 repository — insert_tags + listing rollups (dual backend)

Dialect-split: portable rollup queries on TTPMixin; bulk insert with
ON CONFLICT DO NOTHING / INSERT IGNORE in the per-dialect repos.
Confidence-floor (< 0.3) drop applied at mixin layer before the
dialect hook. BaseRepository now declares the six TTP methods abstract.

Tests in tests/web/db/test_ttp_repo.py flipped from pytest.fail stubs
to real dual-backend behavioral tests; tests/ttp/test_confidence.py
drop-below-floor xfail removed.
This commit is contained in:
2026-05-01 08:04:46 -04:00
parent 226b3adfa2
commit fee697694d
7 changed files with 452 additions and 98 deletions

View File

@@ -15,10 +15,11 @@ from __future__ import annotations
from typing import Any, List, Optional
from sqlalchemy import func, select, text, literal_column
from sqlalchemy.dialects.mysql import insert as mysql_insert
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from decnet.web.db.models import Log
from decnet.web.db.models import Log, TTPTag
from decnet.web.db.mysql.database import get_async_engine
from decnet.web.db.sqlmodel_repo import SQLModelRepository
@@ -151,6 +152,26 @@ class MySQLRepository(SQLModelRepository):
# TEXT-stored JSON, same behavior we rely on in SQLite.
return text(f"JSON_UNQUOTE(JSON_EXTRACT(fields, '$.{key}')) = :val")
async def _insert_tags_or_ignore(self, rows: list[TTPTag]) -> int:
"""Bulk-insert with MySQL's ``INSERT IGNORE`` on the ``uuid`` PK.
``rowcount`` returns the number of NEW rows; duplicates are
silently ignored (matching the SQLite ``ON CONFLICT DO NOTHING``
contract).
"""
if not rows:
return 0
payload = [r.model_dump() for r in rows]
stmt = (
mysql_insert(TTPTag.__table__) # type: ignore[attr-defined]
.values(payload)
.prefix_with("IGNORE")
)
async with self._session() as session:
result = await session.execute(stmt)
await session.commit()
return int(result.rowcount or 0)
async def get_log_histogram(
self,
search: Optional[str] = None,