SQLite Frontend
Descripción
Librería: estructura.sqlite.frontend.js
Compatibilidad: ES6+ / WebAssembly / Web Workers / OPFS
Dependencias internas obligatorias: estructura.sqlite.frontend.wasm.worker.js (vinculado a "./_dependencies/_sqlite/index.mjs" del directorio "_sqlite" self hosted de SQLite WASM v3.53.0-build1 [Official]). Al vincular la librería principal (estructura.sqlite.frontend.js) mediante CDN, no es necesario importar estas dependencias, únicamente es necesario vía self hosted.
<!-- CDN: Estructura / SQLite Frontend -->
<script src="https://estructura.okzgn.com/extensions/sqlite.frontend/estructura.sqlite.frontend.js"></script>
Documentación
1. Arquitectura y Modelo de Ejecución Asíncrona
La librería expone la instancia global _sqlite_frontend. Todas las operaciones se delegan de forma transparente a un Web Worker WebAssembly (estructura.sqlite.frontend.wasm.worker.js).
Modelo Dual (Promesas y Callbacks):
Cada método de ejecución admite una función de retorno (callback) como primer argumento opcional. Si se omite el callback, la llamada retorna síncronamente una Promise compatible con async/await.
// 1. Uso mediante Promesas (async/await)
try {
var resultado = await _sqlite_frontend('SELECT * FROM usuarios').sql();
console.log('Datos:', resultado.sql.result);
} catch (error) {
console.error('Error SQL:', error.message);
}
// 2. Uso mediante Callback tradicional
_sqlite_frontend('SELECT * FROM usuarios').sql(function(respuesta) {
if (respuesta.error) {
console.error('Error:', respuesta.error);
return;
}
console.log('Resultado:', respuesta.sql.result);
});
2. Detección y Control de Errores
El sistema captura y aísla los errores tanto a nivel de inicialización del motor como durante la ejecución de consultas SQL o del Query Builder:
- En modo Promesas (async/await): Si ocurre un error general en la base de datos o en la consulta SQL, la promesa se rechaza automáticamente. Sin embargo, al utilizar el Query Builder (
.get(),.set(), etc.), si ocurre un error en una columna específica dentro de una consulta múltiple, la promesa se resolverá exitosamente y el mensaje de error se alojará enrespuesta[nombre_columna].error. - En modo Callbacks: La función de retorno siempre se ejecuta pasando el objeto de respuesta. Si ocurrió una falla general, el campo
respuesta.errorcontendrá el mensaje del error. - Errores granulares en Query Builder: Cuando se consultan o actualizan múltiples columnas en una sola llamada, un error en una columna no destruye la respuesta completa; la falla se aísla en la propiedad
respuesta[nombre_columna].error.
Ejemplo de captura granular en Callbacks:
_sqlite_frontend({ 'columna_inexistente': true }).get(function(res) {
// 1. Error a nivel de respuesta global
if (res.error) {
console.error('Error general:', res.error);
return;
}
// 2. Error granular a nivel de columna
if (res.get.columna_inexistente.error) {
console.warn('Error en la columna:', res.get.columna_inexistente.error);
return;
}
console.log('Resultados:', res.get.columna_inexistente.result);
});
3. Métodos para Cadenas (String)
Permiten ejecutar sentencias SQL directas, administrar bases de datos, tablas o exportar archivos.
.sql(callback?, timeout?):
Ejecuta una consulta SQL cruda (raw SQL) en la base de datos activa.
_sqlite_frontend('PRAGMA database_list;').sql(function(res) {
console.log('Bases de datos activas:', res);
});
_sqlite_frontend("SELECT name FROM sqlite_master WHERE type='table'").sql(function(res) {
console.log('Tablas en la BD:', res);
});
.db(callback?, timeout?):
Configura y selecciona la base de datos de trabajo activa. Si no se especifica un nombre de base de datos, el motor utilizará automáticamente la base de datos por defecto _default_db. Una vez ejecutado, todas las consultas posteriores se dirigirán a esta base de datos.
// Seleccionar 'mi_base_datos.db' como el contexto de trabajo activo
_sqlite_frontend('mi_base_datos.db').db();
.table(callback?, timeout?):
Configura y selecciona la tabla de trabajo activa sobre la cual operarán los métodos estructurados (.column(), .row(), .get(), .set(), .del()). Si no se especifica un nombre, operará por defecto sobre _default_table. Si la tabla no existe en la base de datos activa, se crea automáticamente incluyendo la clave primaria autogenerada _auto_id.
// Seleccionar 'usuarios' como la tabla activa para las operaciones siguientes
_sqlite_frontend('usuarios').table();
.save(callback?, timeout?):
Exporta y descarga el estado binario de la base de datos activa como una instancia Blob de tipo application/x-sqlite3.
_sqlite_frontend('respaldo.sqlite').save(function(res) {
var blob = URL.createObjectURL(res.save.data);
var a = document.createElement('a');
a.href = blob;
a.download = res.save.name;
a.click();
URL.revokeObjectURL(blob);
});
.close(callback?, timeout?):
Cierra la conexión con la base de datos especificada y libera los recursos asignados en la memoria del Web Worker.
// 1. Cerrar mediante async/await
try {
var res = await _sqlite_frontend('mi_base_datos.db').close();
console.log('Base de datos cerrada:', res.close.name);
}
catch (error) {
console.error('Error al cerrar BD:', error.message);
}
// 2. Cerrar mediante Callback tradicional
_sqlite_frontend('mi_base_datos.db').close(function(res) {
if (res.error) {
console.error('Error:', res.error);
return;
}
console.log('Base de datos cerrada correctamente:', res.close.name);
});
4. Métodos para Objetos (Object)
Proporcionan una capa fluida de abstracción (Query Builder) para manipular estructuras y datos sin escribir SQL manual.
.column(callback?, timeout?):
Define o añade una nueva columna a la tabla activa.
_sqlite_frontend({ name: 'fecha', type: 'INTEGER' }).column(function(res) {
console.log('Columna creada/verificada:', res);
});
.row(callback?, timeout?):
Inserta un nuevo registro (fila) en la tabla activa asignándole un identificador numérico autoincrementable (_auto_id).
_sqlite_frontend({ fecha: Date.now() }).row(function(res) {
console.log('ID asignado al registro:', res.row.id);
});
.get(callback?, timeout?):
Consulta registros de la tabla activa. Soporta selección de columnas, atajos por ID numérico, cláusulas where avanzadas (con operadores como LIKE, IN, >=, IS NULL), límites de resultados (limit) y agregaciones (count).
// 1. Obtener todas las columnas y registros
_sqlite_frontend({ '*': true }).get(function(res) {
console.log('Registros:', res.get['*'].result);
});
// 2. Consulta filtrada por _auto_id
_sqlite_frontend({
'fecha': {
where: { _auto_id: 1 }
}
}).get(function(res) {
console.log('Fecha obtenida:', res.get.fecha.result);
});
// 3. Atajo directo por ID numérico (_auto_id = 1)
_sqlite_frontend({ '*': 1 }).get(function(res) {
console.log('Registro #1:', res.get['*'].result);
});
// 4. Consulta avanzada con operadores relacionales y límite
_sqlite_frontend({
'fecha': {
where: {
fecha: { '>=': 1000000 },
estado: { 'IN': ['activo', 'pendiente'] }
},
limit: 10
}
}).get(function(res) {
console.log('Resultados filtrados:', res.get.fecha.result);
});
// 5. Lanza un reject con Error('SQLite request timeout.') si supera los 2000ms
try {
// Para poder especificar el límite de tiempo ('timeout' en ms) como segundo parámetro, el primer parámetro debe pasarse como 'null' o 'undefined'.
var datos = await _sqlite_frontend({ '*': true }).get(null, 2000);
console.log(datos.get['*'].result);
} catch (err) {
console.error('Consulta cancelada por tiempo:', err.message);
}
Operadores soportados en cláusulas WHERE:
- Comparación:
=,==,!=,<>,>,<,<=,>= - Búsqueda y Rangos:
LIKE,NOT LIKE,GLOB,NOT GLOB,BETWEEN,NOT BETWEEN - Conjuntos y Nulos:
IN,NOT IN,IS,IS NOT,IS DISTINCT FROM,IS NOT DISTINCT FROM - Lógicos y Bits:
AND,OR,NOT,EXISTS,NOT EXISTS,&,|,<<,>>,~,||
Nota de compatibilidad: Las cláusulas count y limit son de uso exclusivo para operaciones de lectura mediante .get(). Si se incluyen en operaciones de modificación (.set()) o eliminación (.del()), el motor devolverá un mensaje de error especificando que la cláusula no está soportada.
Nota sobre IDs numéricos: Al realizar búsquedas por ID numérico directo (ejemplo: { '*': 1 }), la consulta opera sobre la columna primaria autogenerada _auto_id. Se aceptan valores numéricos enteros comprendidos entre 1 y Number.MAX_SAFE_INTEGER.
.set(callback?, timeout?):
Actualiza registros en la tabla activa según las condiciones provistas. El parámetro query acepta un objeto con cláusula where o un número directo referente al _auto_id. Devuelve el número de filas modificadas en el campo result de cada columna procesada.
// 1. Actualización mediante objeto de consulta 'where'
_sqlite_frontend({
'fecha': {
set: Date.now(),
query: { where: { _auto_id: 1 } }
}
}).set(function(res) {
console.log('Filas modificadas:', res.set.fecha.result);
});
// 2. Actualización directa indicando el _auto_id objetivo
_sqlite_frontend({
'fecha': { set: 9999, query: 1 }
}).set(function(res) {
console.log('Filas modificadas:', res.set.fecha.result);
});
Advertencia de Actualización Global: Si se define una columna únicamente con el valor a modificar sin especificar la propiedad query (por ejemplo: { 'estado': 'activo' }), la consulta se ejecutará como una actualización global sin cláusula WHERE, aplicando el nuevo valor a todas las filas de la tabla activa.
Nota sobre IDs numéricos: Al realizar actualizaciones por ID numérico directo (ejemplo: { '*': 1 }), la consulta opera sobre la columna primaria autogenerada _auto_id. Se aceptan valores numéricos enteros comprendidos entre 1 y Number.MAX_SAFE_INTEGER.
.del(callback?, timeout?):
Elimina registros que coincidan con la condición dada o con un valor de identificador numérico implícito (_auto_id).
// 1. Eliminar registro especificando el _auto_id directo (ej. registro #1)
_sqlite_frontend({ 'fecha': 1 }).del(function(res) {
console.log('Registros eliminados:', res);
console.log('Filas eliminadas:', res.del.fecha.result);
});
// 2. Eliminar registros mediante condición 'where'
_sqlite_frontend({
'*': { where: { fecha: 0 } }
}).del(function(res) {
console.log('Registros eliminados:', res);
console.log('Filas eliminadas:', res.del['*'].result);
});
Advertencia de Borrado Masivo: Al pasar una evaluación general con un valor verdadero (por ejemplo: { '*': true }) sin especificar una condición where o un ID directo, la consulta ejecutará un borrado global en la tabla activa, eliminando la totalidad de los registros.
Nota sobre IDs numéricos: Al realizar eliminaciones por ID numérico directo (ejemplo: { '*': 1 }), la consulta opera sobre la columna primaria autogenerada _auto_id. Se aceptan valores numéricos enteros comprendidos entre 1 y Number.MAX_SAFE_INTEGER.
.open(callback?, timeout?):
Importa y carga una base de datos existente a partir de datos binarios (ArrayBuffer, Uint8Array o TypedArray). La base de datos cargada pasa a ser la base de datos de trabajo activa y, si el entorno lo soporta, se almacena de forma persistente en OPFS.
// Ejemplo con un input de tipo archivo: <input type="file" id="archivo_db">
var file = document.getElementById('archivo_db').files[0];
var buffer = await file.arrayBuffer();
// 1. Cargar mediante async/await
try {
var res = await _sqlite_frontend({
name: file.name,
data: buffer
}).open();
console.log('Base de datos cargada:', res.open.name);
}
catch (error) {
console.error('Error al abrir la BD:', error.message);
}
// 2. Cargar mediante Callback tradicional
_sqlite_frontend({
name: 'mi_respaldo.sqlite',
data: buffer
}).open(function(res) {
if (res.error) {
console.error('Error al abrir:', res.error);
return;
}
console.log('Base de datos lista para operar:', res.open.name);
});
5. Flujo Completo de Ejemplo
Integración de definición de esquema, inserción, consulta, actualización y exportación:
document.addEventListener('DOMContentLoaded', async function () {
// 1. Crear columna en el esquema
await _sqlite_frontend({ name: 'fecha', type: 'INTEGER' }).column();
// 2. Insertar nuevo registro
var insercion = await _sqlite_frontend({ fecha: Date.now() }).row();
var nuevoId = insercion.row.id;
console.log('Fila agregada con _auto_id:', nuevoId);
// 3. Consultar todos los registros
var datos = await _sqlite_frontend({ '*': true }).get();
console.log('Resultado de lectura:', datos.get['*'].result);
// 4. Actualizar el registro recién insertado
await _sqlite_frontend({
'fecha': { set: 9999, query: { where: { _auto_id: nuevoId } } }
}).set();
// 5. Eliminar el registro por su _auto_id
await _sqlite_frontend({ 'fecha': nuevoId }).del();
console.log('Registro eliminado con éxito.');
});
6. Manejo de Respuestas Tardías o Huérfanas (unhandledResponse)
En operaciones asíncronas donde se especifica un límite de tiempo de espera (timeout), es posible que una consulta en la base de datos tarde más del tiempo concedido. En estos casos, la promesa en el hilo principal se rechaza y se descarta de la cola interna de seguimiento.
Sin embargo, dado que el Web Worker continúa procesando la consulta en segundo plano hasta su culminación, el resultado final terminará llegando al hilo principal. Para evitar que estas respuestas queden en el olvido o se descarten en silencio, la librería permite definir un manejador global denominado unhandledResponse.
Caso de uso: Permite registrar, auditar o recuperar los resultados de operaciones cuyos límites de tiempo locales expiraron en la interfaz, pero cuya ejecución fue exitosa en el motor SQLite.
Ejemplo de configuración:
// Definir el recolector global para respuestas fuera de tiempo
_sqlite_frontend.unhandledResponse = function (respuesta) {
console.warn('Respuesta recibida tras expiración de tiempo (ID ' + respuesta.id + '):', respuesta);
// Ejemplo: Si la consulta que expiró era una actualización, podemos verificar su resultado
if (respuesta.set) {
console.log('La actualización tardía se completó con éxito en segundo plano.');
}
};
// Ejemplo: Ejecutar una consulta con un timeout muy estricto de 1ms
_sqlite_frontend({ '*': true }).get(null, 1).catch(function(error) {
console.error('El cliente expiró por timeout:', error.message);
// Si el Worker responde un momento después, 'unhandledResponse' capturará el resultado.
});
7. Características Técnicas
Ejecución No Bloqueante (Web Worker): Desplaza todo el procesamiento de base de datos, consultas e I/O a un hilo secundario mediante Web Workers, sin interferir en la interfaz principal del navegador.
Persistencia Inteligente (OPFS con Fallback): Detecta e inicializa automáticamente el sistema de archivos privado del origen (Origin Private File System / OpfsDb) para almacenamiento de alto rendimiento en disco. Si el navegador no soporta OPFS, conmuta de forma transparente a SQLite en memoria (DB).
Cola de Pre-inicialización (Prestart Queue): Captura y encola automáticamente cualquier transacción o consulta emitida antes de que el motor WebAssembly haya completado su carga inicial, despachándolas en orden estricto una vez listo el entorno.
Sanitización y Parametrización Integrada: Limpia y valida dinámicamente nombres de bases de datos (admite puntos para extensiones de archivo, conserva únicamente [a-zA-Z0-9_.]), tablas y columnas (solo se admiten caracteres alfanuméricos y guiones bajos, conserva únicamente [a-zA-Z0-9_]), eliminando automáticamente espacios y caracteres especiales no permitidos. Además, aplica sentencias preparadas parametrizadas (bind) en las operaciones de escritura y lectura para proteger contra inyecciones SQL.
8. Requisitos del Servidor y Cabeceras HTTP (OPFS / WASM)
Para habilitar el acceso persistente al disco mediante OPFS (Origin Private File System) y permitir la compilación del módulo SQLite WebAssembly, el servidor web debe servir la aplicación con un entorno de aislamiento de origen (Cross-Origin Isolation) y con permisos para eval de WebAssembly en la política de seguridad (CSP).
Cabeceras requeridas:
Cross-Origin-Opener-Policy: same-origin(Habilita el aislamiento de origen COOP).Cross-Origin-Embedder-Policy: require-corp(Permite recursos de origen cruzado protegidos COEP).Cross-Origin-Resource-Policy: same-origin(Protección CORP).Content-Security-Policy: ... 'wasm-unsafe-eval'(Permite la compilación ejecutable de la base de datos WASM).
Ejemplo de Middleware en Node.js (Express / Custom Server):
module.exports = function customMiddleware (req, res, next) {
// Cabeceras de aislamiento de origen para OPFS
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
next();
};