Saltar al contenido
Development

La base de datos vacía que parecía perfectamente sana

Por Victor Da Luz
sqliterusttauridev-loggreenhouse

Greenhouse, el gestor de proyectos creativos, guarda todo su estado en un solo archivo SQLite: state.db. Cada proyecto, cada toque, cada timestamp vive ahí. Esta semana se le agregó un sistema de respaldos y una pantalla de recuperación ante corrupción, y en el camino apareció un error en el propio arreglo que habría vuelto la función de “recuperación” activamente peligrosa.

La situación inicial: sin plan para una base de datos corrupta

La app ya manejaba un settings.json corrupto (retrocediendo al onboarding) y una carpeta de vault que desaparece (un disco desconectado, por ejemplo). Pero nada cubría el caso de que state.db mismo se dañara: un corte de energía a mitad de una escritura, o un cliente de sincronización en la nube atrapando el archivo a mitad de transferencia. En el peor de los casos, SQLite simplemente abre el archivo dañado como si fuera una base de datos nueva y vacía, y la app muestra tan campante un panel vacío. Desde el lado del usuario, eso se lee como “se perdió todo mi historial”, sin ninguna explicación y sin nada ofrecido para arreglarlo.

El plan era simple: respaldar state.db una vez al día con VACUUM INTO (un comando propio de SQLite que escribe una instantánea consistente, más seguro que una copia de archivo directa), correr PRAGMA quick_check al activar el vault, y dirigir una verificación fallida a una nueva pantalla de recuperación que ofrece restaurar desde el respaldo.

El error que casi publiqué

Construí todo el pipeline, escribí pruebas unitarias para cada pieza, vi pasar 186 pruebas, y estaba listo para darlo por terminado. Después corrí una revisión de segunda opinión antes de cerrar el tema, más por costumbre que por sospecha.

La pregunta que volvió: ¿qué pasa cuando state.db se trunca a exactamente cero bytes?

No había probado ese caso específico. Así que lo rastreé a mano. SQLite trata un archivo vacío como una base de datos nueva y válida, sin corrupción alguna desde el punto de vista de SQLite. Mi código lo abriría, escribiría un esquema nuevo (el mismo paso de migración que corre en cada primer lanzamiento legítimo), y después correría quick_check contra ese esquema recién creado. Que vuelve limpio. Cada verificación que había construido diría “este vault está bien”, mientras silenciosamente le mostraba al usuario un historial de proyectos borrado como si fuera una instalación nueva.

Y empeora: mi código después respaldaría diligentemente esa base de datos vacía como la instantánea del día. En una semana, la lógica de poda que conserva solo los últimos 7 respaldos habría borrado todos los respaldos buenos que sí tenían los datos reales del usuario.

El arreglo: cruzar información con algo fuera de la propia base de datos

Una base de datos vacía no puede indicar, por sí sola, si se supone que debe estar vacía. La única forma de saberlo es verificar algo externo. Mi arreglo: comparar el conteo de elementos de la base de datos activa contra el conteo del respaldo más reciente. Si el respaldo tiene proyectos reales pero la base de datos activa no tiene ninguno, eso no es un vault nuevo, son datos que desaparecieron. Dirigir a la pantalla de vault dañado en vez de al panel principal.

Escribí una prueba que reproduce el escenario exacto (sembrar un vault, respaldarlo, truncar el archivo activo a cero bytes) y confirmé que el arreglo lo detecta. Después, para asegurarme de que la prueba no fuera solo decorativa, borré el arreglo temporalmente y volví a correr la prueba. Falló, tal como debía. Después volví a poner el arreglo. Ese hábito, romper el arreglo a propósito y ver si la prueba lo detecta, es la única forma que conozco de confiar en que una prueba de regresión realmente está probando algo.

Una limitación de las herramientas que se convirtió en una lección más grande

Greenhouse tiene una configuración de WebDriver para manejar la app real ya compilada, genuinamente útil para demostrar que un arreglo funciona fuera de los mocks. Fui a usarla acá y me topé con una pared: arranca saltándose el onboarding con una variable de entorno solo para depuración, y ese atajo se salta toda la ruta de activación que acababa de construir, verificación de corrupción incluida. No había forma de llegar a la nueva pantalla de recuperación a través de ella.

La solución alternativa tentadora era apuntar una instancia real de la app al archivo de configuración real guardado en mi máquina, en vez del vault de prueba descartable. Llegué a escribir ese script antes de que mis propias herramientas de seguridad me detuvieran, correctamente. Ese camino habría significado una instancia real de la app apuntando a mi vault real, mientras un script justo al lado estaba ocupado truncando y corrompiendo archivos de base de datos para probar la recuperación. Un error en mi propia lógica de restaurar el archivo original, o un script que fallara en el momento equivocado, y yo habría sido quien necesitara la pantalla de recuperación que estaba construyendo.

Así que no busqué un rodeo. En vez de eso corrí una verificación de regresión simple (confirmé que el flujo normal de captura seguía funcionando después de los cambios) y me apoyé más en las pruebas unitarias, que sí ejercitan las funciones reales de producción directamente, no un mock que las reemplaza. Después dejé anotado exactamente por qué la automatización no puede llegar a esta función, para que la próxima persona (o el próximo yo) no pierda una tarde redescubriéndolo.

Lo que se publicó

Respaldos diarios con VACUUM INTO, nombrados por fecha para que un segundo lanzamiento el mismo día no haga nada automáticamente, conservando los últimos 7. Una verificación estructural de salud al activar el vault, más la verificación cruzada para el caso de base de datos vacía. Una nueva pantalla de recuperación que ofrece restaurar desde el respaldo, reflejando la pantalla existente de “el vault desapareció” de la app. Y el archivo corrupto nunca se borra, ni siquiera durante una restauración, se renombra y se guarda aparte, siguiendo la filosofía general de la app de que “nada se borra nunca”.

El instinto de confiar en “todo en verde” y seguir adelante es fuerte, sobre todo después de una sesión larga de implementación. El valor de una segunda mirada, aunque sea breve, antes de dar algo por terminado, sigue dando frutos.

Lecturas relacionadas

Development

Building Greenhouse: la bóveda en disco

La estructura de carpetas que sobrevive a la app: zonas versus etapas, una configuración que se niega a sobrescribirse, un archivo que es una impresión de la base de datos, importación por adopción en el lugar, y el parámetro que no hacía nada.

Leer
Development

El estado que nada llegó a escribir (bitácora del motor de Greenhouse)

Un estado que ningún código llegó a fijar, un parser que adivinaba, temporizadores en desacuerdo sobre qué significa 'después', y la cola larga de una revisión de motor: ordenamientos, restricciones, decisiones a medio escribir, y un archivo de configuración que podía dejar la app sin arrancar.

Leer