Datos relacionales locales, migraciones y consultas tipadas. expo-sqlite es el almacén relacional offline predeterminado para Expo SDK 57: inspecciones, listas de verificación de trabajos, formularios en borrador con claves foráneas y grandes conjuntos de datos cacheados que no caben en blobs JSON de AsyncStorage.
Tarjeta de referencia rápida - lista para copiar y pegar.
npx expo install expo-sqlite
// src/db/migrate.tsimport type { SQLiteDatabase } from "expo-sqlite";const DATABASE_VERSION = 1;export async function migrateDbIfNeeded(db: SQLiteDatabase) { const { user_version: current } = await db.getFirstAsync<{ user_version: number }>( "PRAGMA user_version" ); if (current >= DATABASE_VERSION) return; if (current === 0) { await db.execAsync(` PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; CREATE TABLE inspections ( id TEXT PRIMARY KEY NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', updated_at INTEGER NOT NULL ); `); } await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);}
// app/_layout.tsx (extracto)import { SQLiteProvider } from "expo-sqlite";import { migrateDbIfNeeded } from "@/db/migrate";<SQLiteProvider databaseName="field.db" onInit={migrateDbIfNeeded}> <Stack /></SQLiteProvider>
// src/db/inspections.tsimport type { SQLiteDatabase } from "expo-sqlite";export type Inspection = { id: string; title: string; status: "draft" | "submitted"; updated_at: number;};export async function listInspections(db: SQLiteDatabase): Promise<Inspection[]> { return db.getAllAsync<Inspection>( "SELECT id, title, status, updated_at FROM inspections ORDER BY updated_at DESC" );}export async function upsertInspection(db: SQLiteDatabase, row: Inspection) { await db.runAsync( `INSERT INTO inspections (id, title, status, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET title = excluded.title, status = excluded.status, updated_at = excluded.updated_at`, row.id, row.title, row.status, row.updated_at );}
Cuándo usarlo:
Modelos relacionales con filtros, ordenamiento y uniones (inspecciones - fotos - respuestas)
Aplicaciones de campo offline-first con miles de filas
Reemplazar arrays JSON de AsyncStorage de varios megabytes
Caché local tipada cuando la persistencia de TanStack Query crece más allá de los límites de KV
// app/_layout.tsximport { Stack } from "expo-router";import { SQLiteProvider, useSQLiteContext } from "expo-sqlite";import { migrateDbIfNeeded } from "@/db/migrate";export default function RootLayout() { return ( <SQLiteProvider databaseName="field.db" onInit={migrateDbIfNeeded}> <Stack /> </SQLiteProvider> );}
// src/db/migrate.tsimport type { SQLiteDatabase } from "expo-sqlite";const DATABASE_VERSION = 2;export async function migrateDbIfNeeded(db: SQLiteDatabase) { let { user_version: version } = await db.getFirstAsync<{ user_version: number }>( "PRAGMA user_version" ); if (version >= DATABASE_VERSION) return; if (version === 0) { await db.execAsync(` PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; CREATE TABLE inspections ( id TEXT PRIMARY KEY NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', updated_at INTEGER NOT NULL ); CREATE TABLE answers ( id TEXT PRIMARY KEY NOT NULL, inspection_id TEXT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE, prompt TEXT NOT NULL, value TEXT, updated_at INTEGER NOT NULL ); CREATE INDEX idx_answers_inspection ON answers(inspection_id); `); version = 1; await db.execAsync("PRAGMA user_version = 1"); } if (version === 1) { await db.execAsync(` ALTER TABLE inspections ADD COLUMN synced INTEGER NOT NULL DEFAULT 0; `); version = 2; await db.execAsync("PRAGMA user_version = 2"); }}
// src/db/inspections.ts - consultas tipadas con API de plantilla etiquetadaimport type { SQLiteDatabase } from "expo-sqlite";export type Inspection = { id: string; title: string; status: string; synced: number; updated_at: number;};export async function listUnsynced(db: SQLiteDatabase) { const sql = db.sql; return sql<Inspection>`SELECT * FROM inspections WHERE synced = 0 ORDER BY updated_at ASC`;}export async function markSynced(db: SQLiteDatabase, id: string) { await db.runAsync("UPDATE inspections SET synced = 1 WHERE id = ?", id);}
SQLite almacena un entero monótono user_version en el archivo de base de datos. El patrón recomendado por Expo refleja el control de versiones de esquema de AsyncStorage:
onInit (SQLiteProvider) o primer openDatabaseAsync:1. PRAGMA user_version - actual2. if actual < OBJETIVO: ejecuta SQL para actual - actual+1 PRAGMA user_version = actual+13. repite hasta que actual === OBJETIVO
journal_mode = WAL mejora la lectura/escritura concurrente - configúralo en la creación de v0
foreign_keys = ON debe habilitarse por conexión - inclúyelo en cada bootstrap de BD nuevo
Las migraciones destructivas (drop column) a menudo necesitan reconstrucción de tabla - planifica un banner de inactividad para usuarios de campo
Añade columnas synced, deleted y updated_at localmente. Los trabajadores de fondo consultan WHERE synced = 0 e impulsan la API - reconcilia con Estrategias de sincronización.
Transacciones intercaladas - withTransactionAsync incluye cualquier consulta concurrente hasta que el scope termina. Solución: Usa withExclusiveTransactionAsync para escrituras multi-sentencia.
Olvidar finalizeAsync - Las sentencias preparadas filtradas agotan los identificadores nativos. Solución: Siempre try/finally alrededor de sentencias preparadas.
Ejecutar migraciones en cada pantalla - Condiciones de carrera en onInit paralelo. Solución:SQLiteProvider único en la raíz; migraciones solo en onInit.
Almacenar blobs en SQLite sin necesidad - Las fotos grandes pertenecen al disco (expo-file-system); almacena rutas en SQLite. Solución: Columna BLOB solo para pequeños payloads binarios.
Asumir que Expo Go cubre SQLCipher personalizado - useSQLCipher necesita un dev build y config plugin. Solución: SQLite predeterminado para la mayoría de aplicaciones; cifra solo cuando el cumplimiento lo requiere.
Limitaciones alpha en web - expo-sqlite en web necesita Metro wasm + headers COOP/COEP. Solución: Usa AsyncStorage o caché de servidor en web hasta que SQLite web esté en alcance.
Sin índices en columnas de filtro - WHERE synced = 0 escanea tablas completas a escala. Solución: Indexa claves foráneas y flags de sincronización temprano.
Funciona en Expo Go para SQLite predeterminado. SQLCipher y custom build flags requieren un dev build con el config plugin.
¿Dónde debe vivir el archivo de base de datos?
SQLiteProvider y openDatabaseAsync usan el directorio de base de datos predeterminado de la aplicación automáticamente. En iOS TV, los archivos se ubican en cachés según las directrices de plataforma - no asumas el directorio de documentos en todos los objetivos.
¿Cómo inspecciono la base de datos durante el desarrollo?
Presiona Shift + M en la terminal de Expo CLI - Abre expo-sqlite para navegar tablas, ejecutar SQL y exportar la BD desde tu navegador.
¿Puedo compartir una base de datos con una extensión de iOS?
Sí - configura un App Group en app.config.ts, luego pasa directory desde Paths.appleSharedContainers a SQLiteProvider. Consulta docs de Expo SQLite para títulos.
¿Cómo escribo los resultados de consultas?
Usa genéricos en getAllAsync<Inspection>, db.sql<Inspection> o sentencia preparada executeAsync<Inspection>. Define tipos de fila en src/db/types.ts compartido con mapeadores de API.
¿Debo usar Drizzle ORM?
Drizzle + expo-sqlite es una combinación popular para esquemas tipados y migraciones. SQL crudo está bien para aplicaciones pequeñas - adopta ORM cuando el recuento de tabla excede ~8 o múltiples ingenieros tocan el esquema.
¿Cómo interactúa SQLite con TanStack Query?
Usa SQLite como la fuente de verdad para filas creadas offline; Query almacena snapshots del servidor. Al reconectarse, vacía filas SQLite synced = 0 luego invalidateQueries. Consulta ../state-management/tanstack-query/tanstack-query.md.
¿Qué hay sobre recuperación de corrupción?
En fallo de migración, pone en cuarentena el archivo de BD (renombra con timestamp), crea una base de datos nueva e impulsa una re-sincronización de servidor. Registra user_version y schema hash en telemetría para soporte.