Dados relacionais locais, migrações e consultas tipadas. expo-sqlite é o armazenamento relacional offline padrão para o Expo SDK 57 - inspeções, checklists de trabalho, rascunhos de formulários com chaves estrangeiras e grandes conjuntos de dados cacheados que não cabem em blobs JSON do AsyncStorage.
Cartão de receita de referência rápida - pronto para copiar e colar.
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 (trecho)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 );}
Quando usar isto:
Modelos relacionais com filtros, ordenações e junções (inspeções → fotos → respostas)
Aplicativos de campo offline-first com milhares de linhas
Substituindo arrays JSON do AsyncStorage de vários megabytes
Cache local tipado quando a persistência do TanStack Query excede os limites 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 com API de template tagimport 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);}
O SQLite armazena um inteiro monotônico user_version no arquivo do banco de dados. O padrão recomendado pelo Expo espelha o versionamento de esquema do AsyncStorage:
onInit (SQLiteProvider) ou a primeira openDatabaseAsync:1. PRAGMA user_version → current2. if current < TARGET: executa SQL para current → current+1 PRAGMA user_version = current+13. repete até current === TARGET
journal_mode = WAL melhora a concorrência de leitura/escrita - definido na criação v0
foreign_keys = ON deve ser habilitado por conexão - inclua em cada inicialização de DB nova
Migrações destrutivas (excluir coluna) geralmente precisam de reconstrução da tabela - planeje um banner de inatividade para usuários de campo
Adicione colunas synced, deleted e updated_at localmente. Workers em segundo plano consultam WHERE synced = 0 e enviam para a API - reconcilie com Estratégias de Sincronização.
Transações intercaladas - withTransactionAsync inclui qualquer consulta concorrente até que o escopo termine. Correção: Use withExclusiveTransactionAsync para escritas de várias instruções.
Esquecer finalizeAsync - Instruções preparadas vazadas esgotam os handles nativos. Correção: Sempre use try/finally em torno de instruções preparadas.
Executar migrações em todas as telas - Condições de corrida em onInit paralelos. Correção: Um único SQLiteProvider na raiz; migrações apenas em onInit.
Armazenar blobs no SQLite sem necessidade - Fotos grandes pertencem ao disco (expo-file-system); armazene caminhos no SQLite. Correção: Coluna BLOB apenas para pequenos payloads binários.
Assumir que o Expo Go cobre SQLCipher personalizado - useSQLCipher precisa de um build de desenvolvimento e plugin de configuração. Correção: SQLite padrão para a maioria dos aplicativos; criptografe apenas quando a conformidade exigir.
Limitações alfa da Web - expo-sqlite na web requer Metro wasm + cabeçalhos COOP/COEP. Correção: Use AsyncStorage ou cache do servidor na web até que o SQLite da web esteja em escopo.
Sem índices em colunas de filtro - WHERE synced = 0 escaneia tabelas inteiras em escala. Correção: Indexe chaves estrangeiras e flags de sincronização antecipadamente.
Funciona no Expo Go para SQLite padrão. SQLCipher e flags de build personalizadas exigem um build de desenvolvimento com o plugin de configuração.
Onde o arquivo do banco de dados deve ficar?
SQLiteProvider e openDatabaseAsync usam o diretório de banco de dados padrão do aplicativo automaticamente. Em iOS TV, os arquivos ficam em caches de acordo com as diretrizes da plataforma - não assuma o diretório de documentos em todos os alvos.
Como inspeciono o banco de dados durante o desenvolvimento?
Pressione Shift + M no terminal do Expo CLI → Abrir expo-sqlite para navegar pelas tabelas, executar SQL e exportar o DB do seu navegador.
Posso compartilhar um banco de dados com uma extensão iOS?
Sim - configure um App Group em app.config.ts, então passe directory de Paths.appleSharedContainers para SQLiteProvider. Veja a documentação do Expo SQLite para entitlements.
Como tipar os resultados das consultas?
Use genéricos em getAllAsync<Inspection>, db.sql<Inspection>, ou executeAsync<Inspection> da instrução preparada. Defina tipos de linha em src/db/types.ts compartilhados com mappers de API.
Devo usar Drizzle ORM?
Drizzle + expo-sqlite é uma combinação popular para esquemas tipados e migrações. SQL bruto é bom para aplicativos pequenos - adote ORM quando o número de tabelas exceder ~8 ou vários engenheiros mexerem no esquema.
Como o SQLite interage com o TanStack Query?
Use o SQLite como a fonte da verdade para linhas criadas offline; o Query armazena em cache snapshots do servidor. Ao reconectar, descarregue as linhas synced = 0 do SQLite e então invalidateQueries. Veja ../state-management/tanstack-query/tanstack-query.md.
E a recuperação de corrupção?
Em caso de falha na migração, isole o arquivo do DB (renomeie com timestamp), crie um novo banco de dados e acione uma ressincronização do servidor. Registre user_version e hash do esquema na telemetria para suporte.