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.
// 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 } } }
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.