Copy To

Muchas zapatillas cayendose encima de Jorge al abrir el armario.

Tabla de contenidos


Problema

En aplicaciones backend con Vapor, a menudo necesitamos exportar grandes volúmenes de datos desde la base de datos a archivos para diferentes propósitos: backups, análisis offline, integración con sistemas externos, o auditorías de datos.

Usar consultas tradicionales con Fluent para luego serializar los resultados manualmente presenta varios inconvenientes:

  • Alto consumo de memoria: cargar miles de registros en memoria para procesarlos uno a uno.
  • Procesamiento lento: serialización manual a CSV requiere iterar y formatear cada registro.
  • Falta de optimización: no aprovecha las capacidades nativas de exportación del motor de base de datos.
  • Complejidad innecesaria: gestión manual de formatos, escapado de caracteres y manejo de valores nulos.

Para escenarios de exportación masiva, necesitamos una estrategia que aproveche las capacidades nativas de PostgreSQL para generar archivos de forma eficiente.


Solución

Extendemos Database con una función que ejecuta el comando COPY … TO de PostgreSQL, permitiendo exportar datos directamente desde el schema de la tabla a archivos CSV en el sistema de archivos.

extension Database {
    func exportCSV(
        _ model: any Model.Type,
        file: PathEnum)
        async throws {
        let query = SQLQueryString(
            """
            COPY \"\(unsafeRaw: model.space ?? "public")\".
            \"\(unsafeRaw: model.schema)\"
            TO '\(unsafeRaw: file.rawValue)'
            WITH (FORMAT csv, HEADER true, DELIMITER ',',
            QUOTE '"', ESCAPE '"', NULL '')
            """
        )

        try await self.sqlDatabase
            .raw(query).run()
    }
}

Puntos clave:

  • Usa COPY … TO de PostgreSQL, el método más eficiente para exportación masiva.
  • El parámetro model.space soporta schemas personalizados (por defecto “public”).
  • model.schema obtiene automáticamente el nombre de la tabla del modelo Fluent.
  • Configuración CSV estándar: headers incluidos, delimitadores y manejo correcto de valores nulos.
  • Utiliza unsafeRaw para interpolación directa en la query SQL.

Resultado

func exportRegions() async throws {
    try await db.exportCSV(
        LocationRegionModel.self, 
        file: file
    )
}

Beneficios de esta aproximación:

🚀 Performance óptimo: COPY … TO es mucho más rápido que serialización manual.

💾 Eficiencia de recursos: PostgreSQL escribe directamente al archivo sin cargar datos en memoria de la aplicación.

📦 Formato consistente: el motor de base de datos garantiza un CSV válido con escapado correcto.

🔧 Integración nativa: aprovecha capacidades optimizadas del motor PostgreSQL.

📊 Escalabilidad: permite exportar millones de registros sin impacto en el rendimiento de la aplicación.

Esta solución es el complemento perfecto para importCSV, formando un par de funciones que permite movimiento bidireccional de datos entre PostgreSQL y el sistema de archivos de forma eficiente y confiable. Si aún no has visto la parte de importación, explico cómo construirla usando COPY FROM de PostgreSQL en Copy From.

Keep coding, keep running 🏃‍♂️