Skip to content

feat(api): filtros y retención del historial (#103) - #105

Merged
g-garciac2022 merged 3 commits into
devfrom
feat/103-historial-filtros-retencion
Aug 14, 2026
Merged

feat(api): filtros y retención del historial (#103)#105
g-garciac2022 merged 3 commits into
devfrom
feat/103-historial-filtros-retencion

Conversation

@g-garciac2022

Copy link
Copy Markdown
Collaborator

#103 · Historial: filtros y retención

Closes #103

La otra mitad de R9. El issue anterior dejaba algo usable —historial paginado y
en orden inverso— y éste añade los dos refinamientos que quedaban, más un
criterio que hubo que reinterpretar porque su premisa había cambiado.

Con esto se completa R9 y con él el hito H2 → release v0.3.0.

Qué incluye

  • backend/config/settings.py: history_max_entries (1000) y
    history_max_days (30), con 0 para desactivar cada uno. Configuración y no
    constantes porque el criterio los propone como ejemplo, no como norma, y
    porque en desarrollo interesa desactivarlos para no perder las pruebas propias.
  • backend/api/history.py: índice en created_at, la poda dentro de la
    transacción del INSERT, y el WHERE dinámico de los filtros.
  • backend/api/app.py y schemas.py: seis parámetros de filtro
    validados en la firma —así salen publicados en OpenAPI— y un campo retention
    en la respuesta.
  • docs/requisitos.md: ajuste de R9.4, justificado abajo.
  • tests/api/test_history.py: 16 tests nuevos (5 de poda, 11 de filtros).
  • spikes/: los cuatro bancos de prueba, con resultados y conclusiones en la
    cabecera — incluidos los dos que salieron mal, con la advertencia escrita.

R9.4 se escribió para un historial que no existe

«Filtrado por nombre de herramienta, intervalo de fechas y estado» se redactó
pensando en el historial de invocaciones, que en #102 se descartó a favor de
guardar análisis. Dos de los tres criterios no encajan tal cual:

«Nombre de herramienta» no aplica a un análisis, que invocó cinco señales y
no tiene una. Se resuelve con dos parámetros: kind separa análisis de
ejecuciones sueltas y tool sólo casa con las segundas. Ambas columnas ya
existían. En la pantalla el desplegable de herramientas aparece únicamente dentro
de la pestaña «Herramientas», así que la restricción no se explica: se ve.

«Estado» tampoco es una sola cosa: un análisis puede tener tres señales bien
y una caída. Se desdobla en verdict —qué concluyó, y es el que interesa en
pantalla— y status —si funcionó la maquinaria, operativo—. Es también el motivo
por el que en #102 status quedó como cadena y no como enum.

Se descartó la lectura literal —guardar qué señales participaron en cada
análisis— porque pide tabla nueva y habilita una consulta de depuración, no de
usuario.

La poda: la formulación barata era la incorrecta

Formulación desde 500 desde 3000 Coste
(a) MIN sobre subconsulta → 700 → 1000 801 µs
(b) OFFSET sobre el índice → 700 → 1000 618 µs
(c) borrar sólo la más vieja → 500 → 3000 14 µs
(d) corte por MAX(id) - N → 700 → 1000 17,7 µs

La (c) es 45 veces más barata que la ganadora y está mal: borra
incondicionalmente, así que mantiene el tamaño de partida en vez de llevarlo al
límite — con 500 filas y techo de 1000 seguía borrando una por escritura. El
primer banco no lo detectó porque arrancaba justo en el límite, así que su
«quedan 1000 filas» salía por construcción.

De ahí el criterio de convergencia desde ambos lados. Desde abajo es
corrección: borrar por debajo del techo destruye lo que la política dice
conservar. Desde arriba es la ruta de actualización, y no es hipotética — el
historial lleva creciendo sin techo desde #102, así que al desplegar esto lo
primero que se encuentra es una tabla por encima del límite. Que (d) reduzca de
golpe, de 3000 a 1000 en una sentencia, es lo que evita escribir una migración.

MAX(id) - N es exacta porque los ids son contiguos, y eso costó dos
afirmaciones falsas antes de comprobarlo: los huecos no vienen de la poda (borra
por la cola) ni de inserciones revertidas (medido: AUTOINCREMENT no quema
el id al revertir). Lo que garantiza es que un id no se reutilice tras borrar.

El índice, y la pregunta que no se le hizo a WAL

1 000 filas 10 000 50 000
Poda por antigüedad sin índice 2 374 µs 11 113 µs 54 595 µs
Poda por antigüedad con índice 0,99 µs 1,02 µs 0,97 µs

Unas 2.400 veces más rápida a 1.000 filas, y constante en vez de lineal. Y no
se paga al escribir
: 14,29 µs sin índice contra 13,91 µs con él, por debajo del
ruido. Esa segunda medición es la que faltó en #102 al evaluar WAL — mirar sólo
lo que una optimización acelera, sin mirar lo que encarece, es cómo se acaba
adoptando algo que sale más lento.

La retención no es sólo higiene de disco

SELECT COUNT(*) cuesta ~1 µs por fila y es lineal, porque SQLite no lo
cachea sino que recorre: 886 µs sobre 1.000 filas, 50 465 µs sobre 50.000. Y
GET /history lo ejecuta en cada lectura desde #102, para devolver el total.
La retención es lo que mantiene barata una lectura que ya estaba escrita.

Dos invariantes frágiles, reforzados sin que hubiera fallo

Ninguno era un bug: eran código correcto por razones que nadie había escrito.

  • El WHERE compuesto construía los filtros de igualdad con
    f"{columna} = ?". Seguro por dónde venía esa variable, no por cómo estaba
    escrita la línea: el día que alguien pase un nombre de campo desde la petición,
    esa misma línea se convierte en una inyección sin dar señal. Ahora el fragmento
    entero va en la tupla.
  • El formato de fechas dependía de que tres sitios se acordaran de usar
    isoformat() en UTC. Comprobado que hoy acierta —incluso mezclando marcas con
    microsegundos y sin ellos, porque . es 46 y + es 43— pero un sufijo Z es
    90 y ordena después de cualquier desfase: mezclarlo rompería las comparaciones
    en silencio. Todo pasa ahora por una única función.

Qué encontró revisar, frente a qué encontró medir

Origen Resultado
Revisión multiagente en la nube (75 ficheros, 7.402 líneas) 0 hallazgos
Revisión externa, dos tandas 8 propuestas → 0 bugs; 2 falsas, 2 insignificantes (0,005 %), 2 invariantes que sí valía reforzar
Medir a mano la convergencia de la poda, el COUNT(*) lineal, el coste del índice, y cuatro afirmaciones propias desmentidas

Se añade ASYNC a ruff, que detecta llamadas bloqueantes dentro de funciones
asíncronas — con la advertencia de que no conoce sqlite3, así que el caso
concreto de este issue se le habría escapado igual.

Notas

  • La poda va en la misma transacción que el INSERT, que es lo que hace que
    se cuele en el fsync ya pagado. El precio: si la poda falla se deshace también
    el INSERT, y como record se traga los errores el síntoma sería «el historial
    dejó de guardar» sin ruido. Se asume, cubierto con tests.
  • ?since=...+00:00 escrito a mano devuelve 422, porque en una cadena de
    consulta + significa espacio. No es un fallo de la API —cualquier cliente que
    codifique sus parámetros funciona— y está avisado en la descripción del
    parámetro, que es donde lo verá quien genere el cliente Angular.
  • La paginación sigue siendo por offset. Es estable a este volumen y la
    retención le quita el problema de rendimiento, pero si la pantalla acaba siendo
    scroll infinito conviene migrar a cursor antes de generar el cliente: cambia
    el contrato.
  • Sin índices por kind, verdict o status. Con techo de 1.000 filas un
    escaneo es ~1 ms; añadirlos sería optimizar sin medir.
  • Los filtros por herramienta no alcanzan a los análisis por diseño, según el
    ajuste de R9.4 de arriba.

…nce benchmarks

- Updated requirements in `docs/requisitos.md` to clarify filtering criteria for execution history in the Backend API.
- Enhanced `ruff.toml` to include detection of blocking calls within async functions and added comments on limitations.
- Introduced new benchmarking scripts in `spikes/bench_poda_convergencia.py` and `spikes/bench_poda_coste_e_indice.py` to evaluate pruning strategies and their costs.
- Created `spikes/bench_poda_limite_fijo_y_count.py` to address design flaws in previous benchmarks and assess the impact of fixed limits on pruning performance.
- Added `spikes/check_fechas_lexicograficas.py` to verify the correctness of date comparisons in SQLite, ensuring lexicographic order aligns with chronological order.
@g-garciac2022
g-garciac2022 merged commit 1d0eb89 into dev Aug 14, 2026
1 check passed
@g-garciac2022
g-garciac2022 deleted the feat/103-historial-filtros-retencion branch August 14, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[api] Historial: filtros y retención (R9.4, R9.5)

1 participant