104 / Redes

Sin driver de por medio

La mayoría de los lenguajes llegan a Postgres a través de libpq, una biblioteca en C que el runtime tiene que enlazar y el despliegue tiene que llevar consigo. std/postgres la evita: el protocolo wire —incluida SCRAM-SHA-256, el método de autenticación que exige cualquier Postgres moderno— está implementado directamente en Nyx. Nada que instalar además del servidor. Esta receta conecta, aplica una migración, inserta una fila con parámetros y la vuelve a leer.

104-postgres.nxFuente →
// PostgreSQL: conectar, migrar, insertar con parámetros y leer
//
// std/postgres habla el protocolo v3 en Nyx puro: no necesita libpq ni ninguna
// biblioteca instalada. La autenticación es SCRAM-SHA-256, que es la que exige
// cualquier PostgreSQL moderno.
//
// Para correrlo hace falta un servidor. Con uno local:
//   sudo -u postgres psql -c "CREATE USER app WITH PASSWORD 'secreto';"
//   sudo -u postgres psql -c "CREATE DATABASE midb OWNER app;"

import "std/postgres"

fn main() -> int {
    let conninfo: String = "host=127.0.0.1 port=5432 dbname=midb user=app password=secreto"

    match try_pg_connect(conninfo) {
        Result.Err(e) => {
            print("no se pudo conectar: " + e.msg)
            return 1
        }
        Result.Ok(conn) => {
            // Migraciones: el estado vive en el servidor, así que dos procesos
            // ven la misma versión. Aplicar dos veces la misma es un no-op.
            pg_migrate_init(conn)
            pg_migrate(conn, 1, "crear clientes",
                       "CREATE TABLE clientes (id serial PRIMARY KEY, nombre text, email text)")

            // Los parámetros van POR VALOR: un nombre con comillas es solo un
            // nombre, nunca SQL.
            match try_pg_exec_params(conn,
                    "INSERT INTO clientes (nombre, email) VALUES ($1, $2)",
                    ["O'Brien", "obrien@ejemplo.com"]) {
                Result.Ok(n) => { print("insertadas: " + int_to_string(n)) }
                Result.Err(e) => { print("insert fallo: " + e.msg) }
            }

            match try_pg_query(conn, "SELECT id, nombre, email FROM clientes ORDER BY id") {
                Result.Ok(filas) => {
                    var i: int = 0
                    while i < filas.length() {
                        let f: Array = filas[i]
                        // Todo llega como texto; un NULL se pregunta con
                        // pg_is_null, NUNCA comparando con "".
                        var email: String = "(sin email)"
                        if not pg_is_null(f[2]) { email = f[2] }
                        print(f[0] + " · " + f[1] + " · " + email)
                        i = i + 1
                    }
                }
                Result.Err(e) => { print("select fallo: " + e.msg) }
            }

            let _c = try_pg_close(conn)
            return 0
        }
    }
}
Salida ilustrativastdout
insertadas: 1
1 · O'Brien · obrien@ejemplo.com

Cómo funciona

pg_migrate es idempotente, y la razón está en dónde guarda su estado: no en un archivo local, sino en una tabla nyx_migrations del propio servidor, escrita por pg_migrate_init. pg_migrate_version lee de esa tabla la versión más alta ya aplicada, y pg_migrate no hace nada cuando la versión que recibe no es mayor. Dos procesos —o el mismo despliegue corrido dos veces— ven la misma tabla y coinciden en dónde está el esquema, porque la versión vive donde vive el esquema.

El insert pasa por try_pg_exec_params en vez de armar un string de SQL. "O'Brien" viaja en el mensaje Bind del protocolo como un valor, no como un fragmento que se concatena y se parsea —así que la comilla es solo un carácter dentro de un nombre, nunca una comilla que cierra el literal antes de tiempo. Parametrizar no es un paso extra de seguridad agregado sobre el camino fácil; acá es la única forma que ofrece try_pg_exec_params.

Todo lo que vuelve de try_pg_query llega como texto. El NULL no: viene marcado en el wire. En el mensaje DataRow, cada columna va precedida por su longitud en un entero de 32 bits con signo, y una longitud de -1 significa NULL — distinta de 0, que es una cadena vacía de verdad. Lo que no puede sostener esa distinción es la fila del lado de Nyx: es un Array de String, y un String no tiene forma de decir «ausente». Por eso el módulo traduce ese -1 a un centinela propio —un String de un solo byte 0x00— en vez de dejarlo caer a "". Comparar una celda contra "" trataría un string vacío y un NULL como la misma cosa, y no lo son. pg_is_null pregunta por ese centinela en vez de adivinar por el texto, que es por qué email solo cae a "(sin email)" cuando la columna de verdad no tiene valor.