Copy From

Jorge metiendo en un armario un montón de zapatillas a la vez.

Tabla de contenidos


Problema

En aplicaciones backend con Vapor, cuando necesitamos insertar grandes volúmenes de datos en la base de datos (migraciones iniciales, importación de catálogos, carga masiva desde APIs externas), usar .save() en un bucle genera múltiples transacciones individuales.

Esto resulta en:

  • Alta latencia: cada insert abre/cierra conexión y transaction overhead.
  • Pobre throughput: no aprovecha las capacidades de inserción masiva del motor de base de datos.
  • Riesgo de timeout: operaciones lentas que pueden fallar en entornos con límites de tiempo.

Para escenarios de importación masiva (miles o millones de registros), necesitamos una estrategia de bulk insert que aproveche las capacidades nativas de PostgreSQL.


Solución

Extendemos Database con una función que ejecuta el comando COPY de PostgreSQL, permitiendo importar archivos CSV directamente desde el sistema de archivos al schema de la tabla.

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

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

Puntos clave:

  • Usa COPY de PostgreSQL, el método más rápido para bulk insert desde archivos.
  • 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, delimitadores y manejo de valores nulos.
  • Utiliza unsafeRaw para interpolación directa en la query SQL.

Resultado

private func importRegions() async throws {
    try await db.importCSV(
        RegionModel.self,
        file: .LocationFile("regions", .csv)
    )
}

Beneficios de esta aproximación:

🚀 Performance extremo: COPY es hasta 10-100x más rápido que inserts individuales.

📦 Transacción atómica: toda la importación ocurre en una sola operación, garantizando consistencia.

💾 Eficiencia de recursos: minimiza el uso de memoria y conexiones de base de datos.

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

📊 Escalabilidad: permite importar millones de registros sin degradación significativa.


Notas

Esta implementación utiliza COPY … FROM con archivos del sistema. Actualmente existe una issue abierta en Vapor para implementar soporte nativo de COPY … FROM STDIN, que permitiría realizar bulk inserts directamente desde memoria sin necesidad de archivos intermedios.

Estoy vigilando esta issue de manera activa para integrar esta funcionalidad cuando esté disponible, lo que proporcionará una API aún más flexible y eficiente para operaciones de importación masiva.

Si también necesitas la operación inversa — exportar datos de PostgreSQL a archivos CSV — lo cubrí en Copy To.

Keep coding, keep running 🏃‍♂️