domingo, 13 de julio de 2025

Programando en C# no. 8 (manejo de excepciones)

En el mundo de la programación hay que saber diferenciar entre una excepción y un error.

Una excepción es una falla en el flujo normal de un programa. Si el programa tiene manejo de excepciones, el programa continuará su ejecución o flujo aunque hayan excepciones. Un ejemplo sería que en una parte del prograam se quiera acceder a un índice inexistente de un vector.

Un error es una falla que romperá el flujo del programa. No hay forma de evitarlo. Por ejemplo, que la máquina en la que se está ejecutando un programa se quede sin memoria o que no tenga energía (eléctrica) para funcionar.

Al igual que Java, C# tiene la capacidad de manejar excepciones. Casi de manera idéntica.

Supongamos que queremos acceder a un índice de una cadena "ABC" que no existe. La forma de manejar esa excepción, de tipo ArgumentOutOfRangeException, sería la siguiente:

 try
 {
   string nuevoString = "ABC".Substring(2, 4);
   Console.WriteLine("{0}", nuevoString);
 } catch (ArgumentOutOfRangeException aoex)
  {
    Console.WriteLine("Ha ocurrido una Excepcion: {0}", aoex.Message);
  }

Al compilar no ocurrirá ningún error. La excepción se lanzará a la hora de la ejecución:

Ha ocurrido una Excepcion: Index and length must refer to a location within the string. (Parameter 'length')

Con esto el programa continuará su flujo a pesar de la excepción.

Para operaciones aritméticas haríamos algo como esto (ArithmeticException): 

var x = 1;
var y = 0;
try
{
  int divide = x / y;
}
catch (ArithmeticException ex)
{ 
  Console.WriteLine("Ha ocurrido una Excepcion: {0}", ex.Message);
}

De igual manera, no ocurrirá nada a la hora de la compilación. Ocurrirá a la hora de ejecución:

Ha ocurrido una Excepcion: Attempted to divide by zero.

Ya que se trata de una división entre cero.

Supongamos que queremos acceder a un elemento inexistente de un arreglo (o vector):

int[] arreglo = { 1, 2, 3 };
try
{
   int numero = arreglo[3];
}
catch (IndexOutOfRangeException ex)
{
  Console.WriteLine("Ha ocurrido una Excepcion: {0}", ex.Message);
}

El compilador no adivinará que queremos acceder a un índice que no existe. Sin embargo, el manejo de la excepción, de tipo IndexOutOfRangeException, se encargará de gestionar esa acción.

Ha ocurrido una Excepcion: Index was outside the bounds of the array.

Nota: Existen excepciones marcadas y no marcadas. Las marcadas son las que el compilador obliga a escribir. Las no marcadas no requieren manejo explícito, pero pueden causar la terminación del programa.

Continuaremos con esta serie de entregas sobre C#.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/07/programando-en-c-no-7-uso-de-genericos.html


GCP Pub/Sub: un servicio de mensajería asíncrona y escalable

Según la documentación oficial, Pub/Sub es:

Un servicio de mensajería asíncrona y escalable que separa los servicios que producen mensajes de aquellos que procesan esos mensajes.

Lo que nos permite hacer es:

Permitir que los servicios se comuniquen de forma asíncrona, con latencias de alrededor de 100 milisegundos.

Se usa para:

Las canalizaciones de integración de datos y estadísticas de transmisión a fin de cargar y distribuir datos. Es igual de efectivo que el middleware orientado a la mensajería para la integración de servicios o como una cola con el fin de paralelizar las tareas.

Pub/Sub te permite crear sistemas de productores y consumidores de eventos, llamados publicadores y suscriptores. Los publicadores se comunican con los suscriptores de forma asíncrona mediante la transmisión de eventos, en lugar de llamadas de procedimiento remoto (RPC) síncronas.

Los publicadores envían eventos al servicio de Pub/Sub, sin importar cómo o cuándo se procesarán estos eventos. Luego, Pub/Sub entrega eventos a todos los servicios que reaccionan a ellos. En los sistemas que se comunican a través de RPC, los publicadores deben esperar a que los suscriptores reciban los datos.

Sin embargo, la integración asíncrona en Pub/Sub aumenta la flexibilidad y solidez del sistema general.

En pocas palabras, es una tecnología similar a RabbitMQ, Apache Kafka y/o ActiveMQ por mencionar solo algunas.

Casos habituales

  • Transferir la interacción del usuario y los eventos del servidor. 
  • Distribución de eventos en tiempo real. 
  • Replicar datos entre bases de datos. 
  • Procesamiento y flujos de trabajo paralelos. 
  • Bus de eventos empresariales. 
  • Transmisión de datos desde aplicaciones, servicios o dispositivos de la IoT. 
  • Actualización de cachés distribuidas. 
  • Balanceo de cargas para la confiabilidad.

Un ejemplo sencillo de esta tecnología sería crear una aplicación que:

  • Publique mensajes a un tópico de Pub/Sub y 
  • Reciba mensajes desde ese tópico.

Para ello es necesario tener una cuenta de GCP. Tener el JDK más actual.

Creamos una aplicación de Spring Boot y en nuestro pom.xml agregamos la siguiente dependencia:

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
</dependency>

La configuración del archivo YAML sería algo como esto:

# En application.yml
spring:
  cloud:
    gcp:
      pubsub:
        project-id: tu-id-de-proyecto
        credentials:
          location: classpath:tu-archivo-de-credenciales.json

Crearemos una clase tipo Service que servirá como el "publicador":

@Service
public class PublisherService {
    private final PubSubTemplate pubSubTemplate;

    public PublisherService(PubSubTemplate pubSubTemplate) {
        this.pubSubTemplate = pubSubTemplate;
    }

    public void enviarMensaje(String mensaje) {
        pubSubTemplate.publish("mi-topico", mensaje);
    }
}

Crearemos una clase tipo Component que servirá para escuchar los mensajes:

@Component
public class Subscriber {
    @PubSubSubscriber(subscription = "mi-suscripcion")
    public void recibirMensaje(String mensaje) {
        System.out.println("Mensaje recibido: " + mensaje);
    }
}

Ahora crearemos una clase tipo Controller para invocar el método de envío de mensajes:

@RestController
@RequestMapping("/pubsub")
public class PubSubController {

    private final PublisherService publisherService;

    public PubSubController(PublisherService publisherService) {
        this.publisherService = publisherService;
    }

    @PostMapping("/enviar")
    public ResponseEntity<String> enviar(@RequestBody String mensaje) {
        publisherService.enviarMensaje(mensaje);
        return ResponseEntity.ok("Mensaje publicado: " + mensaje);
    }
}

Usando curl para probar el mensaje:

curl -X POST http://localhost:8080/pubsub/enviar \
     -H "Content-Type: text/plain" \
     -d "Hola desde el controlador!"

También agregando un nuevo método en el controller:

@PostMapping("/enviar-json")
public ResponseEntity<String> enviarJson(@RequestBody Map<String, String> payload) {
    String mensaje = payload.get("mensaje");
    publisherService.enviarMensaje(mensaje);
    return ResponseEntity.ok("Mensaje JSON publicado: " + mensaje);
}

Y usando curl:

curl -X POST http://localhost:8080/pubsub/enviar-json \
     -H "Content-Type: application/json" \
     -d '{"mensaje": "¡Hola JSON!"}'

Continuaremos sobre temas de GCP en próximas entregas.

Enlaces:

https://cloud.google.com/pubsub/docs/overview
https://www.rabbitmq.com/
https://kafka.apache.org/
https://activemq-apache-org.translate.goog/

sábado, 12 de julio de 2025

Programando en C# no. 7 (uso de genéricos)

En el mundo de la programación un genérico nos permite escribir código que trabaja con cualquier tipo de datos sin perder la seguridad de tipos. Es una forma de crear clases, interfaces y métodos que pueden operar sobre varios tipos de datos sin duplicar código. Esto evita castings innecesarios y errores en tiempo de ejecución, ofreciendo reutilización y robustez.

Suponiendo que tenemos esta clase:

public class Caja<T> {
    private T contenido;

    public void Guardar(T elemento) {
        contenido = elemento;
    }

    public T Obtener() {
        return contenido;
    }
}

Lo usaríamos de la siguiente forma:

Caja<int> cajaDeEnteros = new Caja<int>();
cajaDeEnteros.Guardar(42);
Console.WriteLine(cajaDeEnteros.Obtener());  

El tipo T se puede cambiar por cualquier clase o tipo primitivo, lo que lo vuelve extremadamente flexible. Si hacer ninuna conversión o casteo (cast). Veamos otro uso:

var myTypeFloat = new Caja<float>();
myTypeFloat.Guardar(232.0f);
Console.WriteLine("Float: {}", myTypeFloat.Obtener());

var myTypeString = new Caja<string>();
myTypeString.Guardar("232");
Console.WriteLine("String: {}", myTypeString.Obtener());

Usar genéricos nos ahorra tener que hacer conversiones, casteos o incluso crear clases por tipos. ¿Se podrá usar con interfaces? La respuesta es sí.

public interface MyInterface<T> {
        T suma(T num1, T num2);
}

Podríamos crear una clase que lo implemente, además específicamos el tipo a usar, en este caso será un BigInteger.

public class MyInterfaceImpl : MyInterface<BigInteger>
    {
        public BigInteger suma(BigInteger x, BigInteger y)
        {
            return x + y;
        }
    }

Creamos unas variables tipo ``BigInteger`` y se las pasamos al método de suma:

BigInteger x = new BigInteger(23);
var y = new BigInteger(45);
var myInterface = new MyInterfaceImpl();
var resultSuma = myInterface.suma(x, y);
Console.WriteLine("Suma: {}", resultSuma); 

Al final mostramos el resultado de la suma.

Este solo es un ejemplo del uso de clases e interfaces genéricas. Continuaremos con esta serie sobre C# en próximas entregas.

Enlaces:

https://emanuelpeg.blogspot.com/search/label/C%23
Aprender a programar con C# (Libro)

domingo, 29 de junio de 2025

Vibe Coding (la programación vía IA): ¿el futuro de la programación?

Vibe Coding es un nuevo paradigma de programación, una nueva forma de crear código.

Es un enfoque emergente en el desarrollo de software donde la programación se realiza principalmente a través de interacciones en lenguaje natural con una inteligencia artificial, en lugar de escribir código línea por línea. El término fue popularizado por Andrej Karpathy en 2025 y se basa en la idea de que "ver cosas, decir cosas, ejecutar cosas y copiar/pegar cosas" puede ser suficiente para construir software funcional.

¿Cómo funciona?

En lugar de escribir código manualmente, describes lo que quieres lograr —por texto o incluso por voz— y la IA genera el código por ti. Si algo falla, simplemente le explicas el error a la IA y le pides que lo corrija. Es un flujo de trabajo iterativo, conversacional y muy ágil.

En otras palabras:

Una persona, ya sea programador o no (ojo aquí), podrá crear aplicaciones. Esto gracias a la Inteligencia Artificial.

Esto lo vemos en pleno 2025 donde el uso de herramientas como ChatGPT, Grok, Copilot o DeepSeek se ha vuelto más habitual de lo que pensamos.

¿Significará el fin de los programadores? Si y no.

Si, para los programadores que se duermen en sus laureles. Quienes no se preparan constantemente, que no investigan, que se aferran a lo que ya conocen.

No, para los que constantemente crean código. Los que tienen curiosidad por investigar nuevas tecnologías. Los que programan incluso en lenguajes poco comerciales. Los que crean contenido y no son solo consumidores.

Pros y contras del Vibe Coding (o programar vía I.A.)

Pros:

Para quienes saben programar será de mucha ayuda. Automatizará el trabajo que antes erá muy tedioso. Reducirá el tiempo de desarrollo.

Para quienes no saben programar será de ayuda para "minimizar" la complejidad técnica. Esto para ciertas cosas, cabe aclarar.

Contras:

Para quienes saben programar les hará más difícil el mantenimiento de código. Tendrán que rehacer y corregir código inservible arrojado por la I.A., tendrán que revisar lo que es verdaderamente útil y descartar lo que no. La deuda técnica se volverá el pan de cada día.

Para quienes no saben programar les hará creer que son programadores. Creerán que el código arrojado por la I.A. resolverá todo su trabajo, no será así. Tendrán muchos problemas aún mayores. Si desconocen el lenguaje con el que se la I.A. "creó", el código será como entender un idioma indescifrable. Peor cuando salga algún "bug" y no tengan idea de cómo remediarlo, ya que la misma I.A. arrojará resultados nada útiles.

En conclusión: el Vibe Coding es un paradigma para acelerar el desarrollo y "democratizar" la programación, pero requiere criterio técnico para evitar errores, dependencias o problemas de calidad.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/04/firebase-studio-el-visual-studio-code.html
https://codemonkeyjunior.blogspot.com/2025/03/como-ser-un-mejor-programador-en-un.html

sábado, 28 de junio de 2025

Subir un archivo CSV a una tabla en GCP BigQuery

En una entrega pasada vimos como cargar datos desde un CSV a una tabla en BigQuery.

Esta vez crearemos un programa con Java y BigQuery. Para ello necesitamos tener un archivo con datos el cual llamaremos "DATOS.txt". El contenido del archivo será similar a esto:

20250412,34,450.0
20250412,34,432.0
20250412,122,500.0

Tendremos una tabla llamada ``tkdata`` con los siguientes campos:

fchinf DATE 
numregs INT64 
valor STRING 

Requisitos:

  • Tener acceso a GCP BigQuery.
  • Tener JDK 11 o más actual. 
  • Tener Maven (el más actual).

¿Qué hará el programa?

  1. Verificar si existe el archivo a subir ("DATOS.txt"). 
  2. Obtener su contenido y guardarlo en una lista tipo String. 
  3. Crear un objeto tipo StringBuilder a partir de la lista tipo String. 
  4. Enviar el contenido del objeto StringBuilder a un nuevo archivo ("tkdata.csv") que se guardará en el mismo bucket del archivo original. 
  5. Cargar el contenido del nuevo archivo a la tabla ``tkdata``.
  6. Verificar que los datos hayan sido cargados a la tabla.

BigQueryCsvUploader.java

import com.google.cloud.bigquery.*;
import com.google.cloud.storage.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class BigQueryCsvUploader {
    private static final Logger LOG = LoggerFactory.getLogger(BigQueryCsvUploader.class);
    private static final String NAME_FILE_ORIGINAL = "DATOS.csv";
    private static final String TABLE_NAME = "tkdata";
    private static final String DATASET = "mydataset";
    private static final String PROJECT = "myproject";
    private static final String BUCKET = "mybucket";
    private static final String NAME_NEW_FILE = "tkdata.csv";
    private static final long MAX_SIZE_BYTES = 300 * 1024 * 1024; // 300 MB

   
    public static class Tkdata {
        private Date fchinf;
        private long numregs;
        private String valor;

        public Date getFchinf() { return fchinf; }
        public void setFchinf(String fchinf) throws ParseException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            this.fchinf = sdf.parse(fchinf);
        }
        public long getNumregs() { return numregs; }
        public void setNumregs(String numregs) { this.numregs = Long.parseLong(numregs); }
        public String getValor() { return valor; }
        public void setValor(String valor) { this.valor = valor; }
    }

   
    public static boolean existFile(String bucketName, String fileName) {
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        return blob != null && blob.exists();
    }

   
    public static boolean maxSize(String bucketName, 
     String fileName, long maxSizeBytes) {
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        if (blob == null) {
            LOG.error("El archivo {} no existe en el bucket {}", fileName, bucketName);
            return false;
        }
        return blob.getSize() <= maxSizeBytes;
    }

    
    public static List<Tkdata> getListTkdata(String bucketName, String fileName) {
        List<Tkdata> listaTkdata = new ArrayList<>();
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        if (blob == null) {
            LOG.error("Archivo{} no encontrado en el bucket {}", fileName, bucketName);
            return listaTkdata;
        }

        String content = new String(blob.getContent(), StandardCharsets.UTF_8);
        try (BufferedReader reader = new BufferedReader(new StringReader(content))) {
            String line;
            reader.readLine();
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",", -1); 
                if (parts.length == 3) {
                    Tkdata obj = new Tkdata();
                    try {
                        obj.setFchinf(parts[0].trim());
                        obj.setNumregs(parts[1].trim());
                        obj.setValor(parts[2].trim());
                        listaTkdata.add(obj);
                    } catch (ParseException | NumberFormatException e) {
                        LOG.error("Error parseando linea: {}", line, e);
                    }
                } else {
                    LOG.warn("Linea malformada: {}", line);
                }
            }
        } catch (IOException e) {
            LOG.error("Error al leer el contenido", e);
        }
        return listaTkdata;
    }

    
    public static StringBuilder convertToStringBuilder(List<Tkdata> listaTkdata) {
        StringBuilder sb = new StringBuilder();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (Tkdata item : listaTkdata) {
            sb.append(sdf.format(item.getFchinf())).append(",");
            sb.append(item.getNumregs()).append(",");
            sb.append(item.getValor()).append("\n");
        }
        return sb;
    }

   
    public static boolean creaNuevoFileCSV(String bucketName, 
       String fileName, String content) {
        try {
            Storage storage = StorageOptions.getDefaultInstance().getService();
            BlobId blobId = BlobId.of(bucketName, fileName);
            BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("text/csv").build();
            storage.create(blobInfo, content.getBytes(StandardCharsets.UTF_8));
            return true;
        } catch (Exception e) {
            LOG.error("Error al subir archivo {} al bucket {}", fileName, bucketName, e);
            return false;
        }
    }

    
    public static boolean loadCSVToTableBigQuery(String projectId, 
     String datasetId, String bucketName, String sourceFileName, String tableName) {
        try {
            BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService();
            TableId tableId = TableId.of(projectId, datasetId, tableName);

            
            Schema schema = Schema.of(
                Field.of("fchinf", StandardSQLTypeName.DATE),
                Field.of("numregs", StandardSQLTypeName.INT64),
                Field.of("valor", StandardSQLTypeName.STRING)
            );

           
            JobConfiguration jobConfig = LoadJobConfiguration.newBuilder(
                tableId,
                String.format("gs://%s/%s", bucketName, sourceFileName),
                FormatOptions.csv()
            )
                .setSchema(schema)
                .setSkipLeadingRows(0) 
                .setWriteDisposition(JobInfo.WriteDisposition.WRITE_APPEND) 
                .build();

            
            Job job = bigquery.create(JobInfo.of(jobConfig));
            job = job.waitFor();
            if (job.isDone() && job.getStatus().getError() == null) {
                LOG.info("CSV {} successfully loaded into BigQuery table {}.{}", 
                 sourceFileName, datasetId, tableName);
                return true;
            } else {
                LOG.error("Error cargando CSV a BigQuery: {}", job.getStatus().getError());
                return false;
            }
        } catch (Exception e) {
            LOG.error("Error en el proceso de carga", e);
            return false;
        }
    }

  
    public static boolean validateTableData(String projectId, 
      String datasetId, String tableName, List<Tkdata> expectedData) {
        try {
            BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService();
            String query = String.format("SELECT fchinf, numregs, valor FROM %s.%s.%s", 
                        projectId, datasetId, tableName);
            QueryJobConfiguration queryConfig = QueryJobConfiguration.newBuilder(query).build();

           
            TableResult result = bigquery.query(queryConfig);
            long rowCount = result.getTotalRows();

            if (rowCount == 0) {
                LOG.error("No hay datos en la tabla {}.{}", datasetId, tableName);
                return false;
            }

           
            if (expectedData != null && rowCount != expectedData.size()) {
                LOG.warn("Error en conteo de datos {}, found {}", expectedData.size(), rowCount);
                return false;
            }

            
            LOG.info("Sample data from {}.{} (first 5 rows):", datasetId, tableName);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            int maxRowsToLog = 5;
            int rowIndex = 0;
            for (FieldValueList row : result.iterateAll()) {
                if (rowIndex >= maxRowsToLog) break;
                String fchinf = row.get("fchinf").getStringValue(); 
                long numregs = row.get("numregs").getLongValue();
                String valor = row.get("valor").getStringValue();
                LOG.info("Row {}: fchinf={}, numregs={}, valor={}", rowIndex + 1, fchinf, numregs, valor);
                rowIndex++;
            }

            LOG.info("Validacion exitosa: {} rows found in table {}.{}", 
          rowCount, datasetId, tableName);
            return true;
        } catch (Exception e) {
            LOG.error("Error validando datos en la tabla {}.{}", datasetId, tableName, e);
            return false;
        }
    }

    public static void main(String[] args) {
     
        if (!existFile(BUCKET, NAME_FILE_ORIGINAL)) {
            LOG.error("Archivo{} no existe en el buckett {}.", NAME_FILE_ORIGINAL, BUCKET);
            return;
        }

        
        if (!maxSize(BUCKET, NAME_FILE_ORIGINAL, MAX_SIZE_BYTES)) {
            LOG.error("El archivo {} excede el tamaño: 300MB.", NAME_FILE_ORIGINAL);
            return;
        }

        
        List<Tkdata> listaTkdata = getListTkdata(BUCKET, NAME_FILE_ORIGINAL);
        if (listaTkdata.isEmpty()) {
            LOG.error("No hay datos para leer {}. ", NAME_FILE_ORIGINAL);
            return;
        }

        StringBuilder sb = convertToStringBuilder(listaTkdata);

        
        if (creaNuevoFileCSV(BUCKET, NAME_NEW_FILE, sb.toString())) {
            LOG.info("Archivo {} cargado al bucket {}", NAME_NEW_FILE, BUCKET);
            LOG.info("Carga de datos {} a la tabla: {}.{}", NAME_NEW_FILE, DATASET, TABLE_NAME);
            if (loadCSVToTableBigQuery(PROJECT, DATASET, BUCKET, NAME_NEW_FILE, TABLE_NAME)) {
                if (validateTableData(PROJECT, DATASET, TABLE_NAME, listaTkdata)) {
                    LOG.info("Validacion correcta {}.{}", DATASET, TABLE_NAME);
                } else {
                    LOG.error("Validacion fallida {}.{}", DATASET, TABLE_NAME);
                }
            } else {
                LOG.error("Fallo al cargar los datos a la tabla de BigQuery  {}.{}", DATASET, TABLE_NAME);
            }
        } else {
            LOG.error("Fallo al cargar el archivo {} al bucket {}", NAME_NEW_FILE, BUCKET);
        }
    }
}

Es importante hacer notar que los tipos de datos en el archivo deben concordar a los datos de la tabla. En caso contrario, no cargarán.

También es importante tener estas dependencias en el pom.xml

<dependencies>
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-storage</artifactId>
        <version>2.44.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-bigquery</artifactId>
        <version>2.44.0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.13</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.13</version>
    </dependency>
</dependencies>

De preferncia usar Eclipse IDE para generar un .jar o ejecutarlo directamente.

Si la ejecución es correcta podremos ver los datos cargados en la tabla. Continuaremos más sobre BigQuery en próximas entregas.

Enlaces:

https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv

Programando en C# no. 8 (manejo de excepciones)

En el mundo de la programación hay que saber diferenciar entre una excepción y un error. Una excepción es una falla en el flujo normal...

Etiquetas

Archivo del blog