104 / Networking

No driver in between

Most languages reach Postgres through libpq, a C library the runtime has to link against and the deployment has to ship. std/postgres skips it: the wire protocol — including SCRAM-SHA-256, the authentication method any modern Postgres server requires — is implemented directly in Nyx. Nothing to install beyond the server itself. This recipe connects, applies a migration, inserts a row with parameters, and reads it back.

104-postgres.nxSource →
// 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
        }
    }
}
Illustrative outputstdout
insertadas: 1
1 · O'Brien · obrien@ejemplo.com

How it works

pg_migrate is idempotent, and the reason is where it keeps its state: not a local file, but a nyx_migrations table on the server itself, written by pg_migrate_init. pg_migrate_version reads the highest version already applied from that table, and pg_migrate is a no-op when the version it's given is not greater. Two processes — or the same deploy run twice — see the same table and agree on where the schema stands, because the version lives where the schema does.

The insert goes through try_pg_exec_params rather than building a SQL string. "O'Brien" travels in the protocol's Bind message as a value, not as a fragment that gets concatenated and parsed — so the apostrophe is just a character in a name, never a quote that closes the literal early. Binding parameters isn't an extra safety step bolted on top of the easy way; here it's the only way try_pg_exec_params offers.

Everything that comes back from try_pg_query arrives as text. NULL does not: the wire marks it. In the DataRow message every column is preceded by its length as a signed 32-bit integer, and a length of -1 means NULL — distinct from 0, which is a genuinely empty string. What cannot carry that distinction is the row on the Nyx side: it is an Array of String, and a String has no way to say “absent”. So the module translates that -1 into a sentinel of its own — a one-byte 0x00 String — instead of letting it collapse into "". Comparing a cell against "" would treat an empty string and a NULL as the same thing, which they are not. pg_is_null asks for that sentinel instead of guessing from the text, which is why email only falls back to "(sin email)" when the column genuinely has no value.