wandres.dev
D1 · SQLite en el edge

D1 en local: desarrollo y datos de prueba

El desarrollo local con D1: una base SQLite real en tu disco dentro de .wrangler/state, ejecutar SQL con wrangler d1 execute --local, y sembrar datos de prueba desde un archivo seed. Por qué la base local y la remota son universos separados y cómo aprovechar esa separación para iterar sin miedo a romper producción.

⏱ 14 min

Nada frena más el aprendizaje de una base de datos que tener miedo de tocarla. Con D1 no lo tienes, porque en desarrollo no trabajas contra tu base real: wrangler dev respalda tu binding con un SQLite de verdad que vive en tu disco, tuyo y solo tuyo, que puedes llenar, vaciar y romper cuantas veces quieras. Esta última lección del nivel cierra el círculo: crear, enlazar, consultar y agrupar ya lo sabes; ahora toca dominar el entorno donde practicas todo eso a golpe de milisegundo, con datos de prueba propios y sin rozar producción.

🎯 Al terminar esta lección sabrás
  • Localizar la base D1 local y entender que es un SQLite real en tu disco.
  • Ejecutar SQL contra ella con wrangler d1 execute --local.
  • Sembrar datos de prueba desde un archivo seed.sql.
  • Distinguir la base local de la remota y cruzar entre ellas a propósito.

La base local: un SQLite de verdad

Cuando levantas wrangler dev, la plataforma no simula tu base D1 con un mock aproximado: monta un archivo SQLite auténtico dentro de .wrangler/state, y tu env.DB habla con él exactamente con la misma API que usará en producción. Es el mismo motor, el mismo SQL, los mismos métodos —prepare, bind, first, all, run, batch—, solo que los datos viven en tu máquina.

Dentro de .wrangler/state los datos de D1 se guardan como ficheros SQLite bajo un subdirectorio propio; son archivos normales que, si quieres, puedes abrir con cualquier herramienta de SQLite para inspeccionarlos por tu cuenta. No hay nada mágico ni propietario en ese estado: es SQLite en tu disco. Y como persiste entre sesiones, los datos que escribes hoy siguen ahí mañana cuando vuelvas a arrancar wrangler dev. La carpeta suele ir ignorada por git, porque es tu entorno de trabajo personal.

# arranca el runtime local con tu base D1 respaldada en disco
wrangler dev

# empezar de cero: borra el estado local y vuelve a aplicar esquema y datos
rm -rf .wrangler/state
ℹ️
La misma base, dos localizaciones

Tu base D1 tiene, en la práctica, dos encarnaciones: la local en .wrangler/state y la remota en tu cuenta. Comparten el esquema que tú les apliques y la API con la que las consultas, pero no comparten ni una fila de datos. Esa dualidad es una ventaja enorme —experimentas en local sin consecuencias— siempre que la tengas presente. La mayoría de las confusiones de principiante con D1 nacen de olvidarla: buscar en local unos datos que solo escribiste en remoto, o al revés.

Ejecutar SQL con wrangler d1 execute

No necesitas levantar el Worker para hablar con la base: wrangler d1 execute ejecuta SQL directamente contra ella desde la terminal, ideal para inspeccionar, corregir o poblar datos. El flag decisivo, otra vez, es --local frente a --remote: define contra qué encarnación de la base actúas.

# una consulta suelta contra la base local
wrangler d1 execute mi-app --local --command "SELECT count(*) AS total FROM usuarios"

# ejecutar un archivo entero de SQL contra la base local
wrangler d1 execute mi-app --local --file=./schema.sql

# la misma consulta, pero contra la base remota real
wrangler d1 execute mi-app --remote --command "SELECT count(*) AS total FROM usuarios"

Con --command lanzas una sentencia rápida y Wrangler te imprime el resultado como una tabla en la consola, comodísimo para comprobar de un vistazo qué hay dentro. Con --file ejecutas un archivo completo, que es como aplicas el esquema o cualquier lote de SQL. Si omites el flag, versiones recientes de Wrangler te preguntan para evitar que toques producción por accidente: no des por sentado a qué base apuntas, dilo siempre.

Y si necesitas la salida para otra herramienta o para un script, --json la devuelve como JSON estructurado en lugar de como tabla, ideal para canalizarla o comprobarla de forma automática.

# resultados en JSON, listos para canalizar a otro comando
wrangler d1 execute mi-app --local --command "SELECT * FROM usuarios" --json
💡
Comparte el estado local con --persist-to

Por defecto el estado vive en .wrangler/state, pero puedes redirigirlo con --persist-to a un directorio propio, y tanto wrangler dev como wrangler d1 execute lo respetarán si apuntan al mismo sitio. Es útil para mantener un conjunto de datos de prueba en una carpeta aparte o para que varios comandos compartan exactamente la misma base local. Lo importante es que dev y execute miren al mismo estado: si no, escribirás en una base y leerás de otra, y no entenderás por qué tus datos no aparecen.

Sembrar datos de prueba

Una base vacía sirve de poco para desarrollar: quieres filas realistas contra las que probar tus consultas. La forma limpia es un archivo seed.sql con las inserciones de prueba, versionado junto al código, que aplicas contra la base local cuando la necesites poblada.

INSERT OR IGNORE INTO usuarios (email) VALUES
  ('ada@ejemplo.dev'),
  ('linus@ejemplo.dev'),
  ('grace@ejemplo.dev');
# aplica el esquema y luego los datos de prueba a la base local
wrangler d1 execute mi-app --local --file=./schema.sql
wrangler d1 execute mi-app --local --file=./seed.sql

Un detalle que ahorra dolores: escribe la siembra de forma idempotente, con INSERT OR IGNORE o limpiando antes con un DELETE, para que reaplicarla sobre una base que quizá ya tiene datos no falle por colisiones de clave única. Así repoblar produce siempre el mismo estado conocido, lo ejecutes una vez o diez. Y separar schema.sql de seed.sql es deliberado: el esquema define la estructura y viaja también a producción; los datos de siembra son solo para desarrollo y nunca deben acabar en la base real.

{
  "scripts": {
    "db:reset": "rm -rf .wrangler/state && wrangler d1 execute mi-app --local --file=./schema.sql && wrangler d1 execute mi-app --local --file=./seed.sql"
  }
}

Ese mismo guion es la base de un entorno reproducible en integración continua: antes de correr las pruebas, el pipeline crea la base local, le aplica el esquema y la siembra, y arranca de un estado idéntico en cada ejecución. La base efímera de las pruebas y la base real de producción no se rozan jamás, y esa es justo la garantía que quieres.

flowchart LR
SC[schema sql] --> EX[wrangler d1 execute local]
SEED[seed sql] --> EX
EX --> ST[SQLite en wrangler state]
ST --> DEV[wrangler dev usa la base local]
style ST fill:#89b4fa,color:#11111b
style DEV fill:#a6e3a1,color:#11111b
💡
Un solo comando para repoblar de cero

Reúne el reinicio en ese script de tu package.json: borrar .wrangler/state, aplicar schema.sql y aplicar seed.sql, todo en orden con un npm run db:reset. Así, cuando tus experimentos dejen la base local hecha un lío —y lo harán, porque para eso es local— vuelves a un estado limpio y conocido en un solo paso. Un entorno de datos reproducible con un comando es lo que convierte la iteración en algo cómodo en lugar de frágil.

Local y remoto: dos universos que cruzas a propósito

La base local no es un simulacro: es corrección con red de seguridad

El desarrollo local de D1 encierra la misma filosofía que recorre toda la plataforma, y vale la pena leerla despacio porque cambia cómo trabajas. La base local no es una imitación que se parece a D1 pero se comporta distinto en los casos difíciles: es SQLite, el mismísimo motor que ejecuta tus consultas en producción, corriendo en tu disco. Eso significa que el SQL que valides en local es SQL que funcionará en remoto, que las restricciones que violes en tu máquina son las que se violarían en la nube, que el comportamiento de una transacción de batch es idéntico a los dos lados. Se cierra así la grieta más traicionera del desarrollo de bases de datos —la del motor que finge ser otro— y con ella toda una categoría de bugs que solo aparecían en producción. Pero la fidelidad del motor no borra la separación de los datos, y ahí está el matiz que hay que interiorizar: local y remoto comparten el comportamiento, no el contenido. Esa separación no es un defecto que haya que sortear, sino la propiedad que te da libertad: puedes borrar todas las tablas, sembrar datos absurdos, provocar fallos a propósito y repoblar de cero cincuenta veces al día sin que un solo usuario real lo note, porque tu campo de pruebas y tu producción son mundos distintos que solo se tocan cuando tú, deliberadamente, cruzas con --remote. La disciplina del ingeniero del edge está justo en ese cruce consciente: iterar rápido y sin miedo en local porque nada de lo que rompas importa, y tratar cada --remote como lo que es —tocar la base real— con el respeto que merece. Cuando interiorizas que la fidelidad vive en el motor y la seguridad vive en la separación, dejas de temer a tu base de datos y empiezas a usarla como lo que debe ser: un instrumento con el que experimentar, no una pieza de cristal que hay que proteger de ti mismo.

⚔️ Haz tuyo el entorno local
  1. Arranca wrangler dev, escribe una fila desde un handler y confírmala reiniciando el servidor y volviéndola a leer: comprueba que persistió.
  2. Con wrangler d1 execute --local --command, cuenta las filas de una tabla sin levantar el Worker.
  3. Escribe un seed.sql idempotente con varias filas y aplícalo a la base local; verifica los datos con una consulta rápida.
  4. Añade un script db:reset que borre .wrangler/state y reaplique esquema y siembra; ejecútalo y confirma que vuelves a un estado limpio.
  5. Explica con tus palabras por qué la base local es fiel en comportamiento pero está separada en datos, y qué disciplina exige eso al usar --remote.