Files
jRDC-Multi/jRDC_Multi.b4j
Jose Alberto Guerra Ugalde 3b352bb105 - VERSION 5.09.16
- feat: Implementa tolerancia de parámetros configurable y mejora estabilidad general del servidor.
- La tolerancia de parametros permite que si un query requiere 3 parametros y se mandan 4, NO mande un error,
	solo manda a la base de datos los parametros correctos y tira los extras, y guarda una "ADVERTENCIA" en el Log de errores.

- Este commit introduce la funcionalidad de `parameterTolerance` configurable y aborda varias mejoras críticas para la estabilidad y eficiencia del jRDC2-Multi.

- Principales cambios y beneficios:
- **Tolerancia de Parámetros**: Añade la propiedad `parameterTolerance` en `config.properties` para controlar el manejo de parámetros de más. Cuando está habilitada, recorta los parámetros excesivos; si está deshabilitada (modo estricto, por defecto), genera un error, aumentando la robustez de la validación.
- **Inicialización Multi-DB Confiable**: Corrige la lógica de inicialización en `Main.AppStart` para `RDCConnector` de DB3 y DB4, asegurando que cada base de datos tenga su propio *pool* de conexiones correctamente configurado.
- **Optimización de Ejecución SQL**: Elimina llamadas duplicadas a `ExecQuery2` y `ExecNonQuery2` en `DBHandlerB4X.bas`, garantizando que solo los parámetros validados se utilicen y evitando ejecuciones redundantes en la base de datos.
- **Refactorización y Limpieza**: Se eliminó la declaración duplicada de `ActiveRequestsCountByDB` en `Main.bas` y la subrutina `Handle0` obsoleta en `Manager.bas`, mejorando la claridad y mantenibilidad del código.
2025-09-18 22:30:32 -06:00

426 lines
21 KiB
Plaintext

AppType=StandardJava
Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties
File10=stop.bat
File2=config.DB3.properties
File3=config.DB4.properties
File4=config.properties
File5=login.html
File6=reiniciaProcesoBow.bat
File7=reiniciaProcesoPM2.bat
File8=start.bat
File9=start2.bat
FileGroup1=Default Group
FileGroup10=Default Group
FileGroup2=Default Group
FileGroup3=Default Group
FileGroup4=Default Group
FileGroup5=Default Group
FileGroup6=Default Group
FileGroup7=Default Group
FileGroup8=Default Group
FileGroup9=Default Group
Group=Default Group
Library1=byteconverter
Library2=javaobject
Library3=jcore
Library4=jrandomaccessfile
Library5=jserver
Library6=jshell
Library7=json
Library8=jsql
Library9=bcrypt
Module1=Cambios
Module10=Manager
Module11=ParameterValidationUtils
Module12=ping
Module13=RDCConnector
Module14=TestHandler
Module2=ChangePassHandler
Module3=DBHandlerB4X
Module4=DBHandlerJSON
Module5=DoLoginHandler
Module6=faviconHandler
Module7=GlobalParameters
Module8=LoginHandler
Module9=LogoutHandler
NumberOfFiles=10
NumberOfLibraries=9
NumberOfModules=14
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.16
'###########################################################################################################
'###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
'###########################################################################################################
'###################### PUSH #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=github&Args=..\..\
'###########################################################################################################
'###################### PUSH TORTOISE GIT #########################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=TortoiseGitProc&Args=/command:commit&Args=/path:"../"&Args=/closeonend:2
'###########################################################################################################
#End Region
'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0
#AdditionalJar: ojdbc11
' Librería para manejar la base de datos SQLite
#AdditionalJar: sqlite-jdbc-3.7.2
Sub Process_Globals
' --- Variables globales accesibles desde cualquier parte del proyecto ---
' Objeto principal del servidor HTTP de B4J.
Public srvr As Server
' La versión actual de este servidor jRDC modificado.
Public const VERSION As Float = 2.23
' Tipos personalizados (clases) para la serialización y deserialización de datos
' entre el cliente B4X (DBRequestManager) y el servidor jRDC2.
Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL.
Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta.
' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Public listaDeCP As List
' Una lista temporal para almacenar los nombres de archivos de configuración encontrados.
Private cpFiles As List
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.).
' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB.
Public Connectors, commandsMap As Map
' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
Public SQL1 As SQL
' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación de Manager).
Private bc As BCrypt
' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap).
Public MainConnectorsLock As JavaObject
' Timer para ejecutar tareas periódicas, como la limpieza de logs.
Public timerLogs As Timer
' Tipo para encapsular el resultado de la validación de parámetros.
Type ParameterValidationResult ( _
Success As Boolean, _
ErrorMessage As String, _
ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL
)
End Sub
Sub AppStart (Args() As String)
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
bc.Initialize("BC")
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
' Esta base de datos se crea automáticamente si no existe o se migra si es necesario.
InitializeSQLiteDatabase
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos).
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
timerLogs.Enabled = True ' Habilita el timer para que empiece a correr.
Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.")
' <<<< Fin del bloque del Timer >>>>
' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas.
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general.
GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB.
GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB.
GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro.
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos.
srvr.Initialize("") ' Inicializa el objeto servidor HTTP.
Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB).
commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración.
' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap.
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
' DB1 siempre usa el archivo 'config.properties' por defecto.
Try
Dim con1 As RDCConnector ' Declara una variable específica y única para el conector de DB1.
con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1".
Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector.
srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1.
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas.
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null)
' Si DB1 falla, el servidor no puede arrancar correctamente.
ExitApplication
End Try
' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) ===
' El servidor busca archivos de configuración adicionales (ej. config.DB2.properties)
' en el mismo directorio donde se ejecuta el JAR.
cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then
For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then
Try
Dim con2 As RDCConnector
con2.Initialize("DB2")
Connectors.Put("DB2", con2)
listaDeCP.Add("DB2")
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try
End If
' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then
Try
Dim con3 As RDCConnector
con3.Initialize("DB3")
Connectors.Put("DB3", con3)
listaDeCP.Add("DB3")
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try
End If
' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then
Try
Dim con4 As RDCConnector
con4.Initialize("DB4")
Connectors.Put("DB4", con4)
listaDeCP.Add("DB4")
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try
End If
Next
Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
End If
' Log final de las bases de datos que el servidor está gestionando.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ")
Next
If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", "
End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes.
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo,
' lo que es recomendable para la mayoría de los casos para evitar bloqueos.
srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", False) ' Panel de administración del servidor (requiere autenticación).
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON.
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", False) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL.
' 7. Inicia el servidor HTTP.
srvr.Start
Log("===========================================================")
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================")
' 8. Inicia el bucle de mensajes de B4J. Es esencial para que la aplicación
' de servidor continúe ejecutándose y procesando eventos.
StartMessageLoop
End Sub
' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) ---
' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden
' acceder al panel de administración del servidor jRDC y los logs de queries.
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite.
' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación.
If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName)
' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager.
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<<
Log("Creando tabla 'query_logs' con columnas de rendimiento.")
' Esta tabla almacena métricas detalladas de cada query ejecutada.
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
' >>> FIN: Creación de la tabla query_logs <<<
' Crea un usuario por defecto para facilitar el primer acceso al panel de administración.
Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345"
' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad.
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
' Inserta el usuario por defecto en la tabla 'users'.
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$)
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
' >>> INICIO: Creación de la tabla errores con columnas de error/advertencia <<<
Log("Creando tabla 'errores' para registrar eventos.")
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable)
' >>> FIN: Creación de la tabla errores <<<
Else
' Si el archivo de la base de datos ya existe, simplemente se abre.
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.")
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.")
' Primero, verificar si la tabla query_logs existe.
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
Else
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests).
Dim columnExists As Boolean
Dim rs As ResultSet
' --- VERIFICAR Y AÑADIR busy_connections ---
columnExists = False
' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna.
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
columnExists = True
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos!
If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If
' --- VERIFICAR Y AÑADIR handler_active_requests ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") ' Ejecutamos PRAGMA nuevamente para esta columna.
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then
columnExists = True
Exit ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close ' ¡Importante cerrar el ResultSet!
If columnExists = False Then
Log("Añadiendo columna 'handler_active_requests' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If
' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<<
Log("Verificando y migrando tabla 'errores' si es necesario.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
Log("Tabla 'errores' no encontrada, creándola.")
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable)
Else
' Si la tabla ya existe, podrías añadir lógica para ALTER TABLE si se añaden nuevas columnas en el futuro.
' Por ahora, asumimos que la estructura inicial es suficiente.
Log("Tabla 'errores' ya existe.")
End If
' >>> FIN: Lógica de migración para 'errores' <<<
End If
' >>> FIN: Lógica de migración (ALTER TABLE) <<<
End If
End Sub
' Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'.
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
Try
' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler,
' eliminando la necesidad de obtenerlos del conector en este punto.
' Insertamos los datos en la tabla query_logs de SQLite.
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
Catch
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try
End Sub
' Subrutina para registrar errores y advertencias en la tabla 'errores'.
' Type: "ERROR" o "ADVERTENCIA"
' Source: Módulo.Subrutina donde ocurrió el evento (ej. "DBHandlerJSON.Handle")
' Message: El mensaje descriptivo del error/advertencia.
' DBKey: La clave de la base de datos involucrada (ej. "DB1", "DB2"), Null si no aplica.
' CommandName: El nombre del comando SQL (ej. "select_user"), Null si no aplica.
' ClientIp: La dirección IP del cliente que originó la petición, Null si no aplica.
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
Try
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp))
Catch
Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message)
' En este punto, no podemos hacer mucho más que loggear el fallo en la consola,
' para evitar un bucle infinito de errores de logging.
End Try
End Sub
' Subrutina de evento para el Timer 'timerLogs'.
' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs.
Sub TimerLogs_Tick
Try
borraArribaDe15000Logs ' Llama a la función para limpiar los logs.
Catch
Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message
Log(ErrorMsg)
LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) ' <-- Nuevo Log
End Try
End Sub
' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes.
' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'.
Sub borraArribaDe15000Logs 'ignore
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos.
End Sub