SofiaDB desde el lenguaje Sofía#
Consejo
En corto: una app de Sofía que declara permiso datos recibe una base SQL privada que ninguna otra app puede abrir. Se usa con datos.ejecutar, datos.consultar, datos.entero, datos.texto y datos.copiar; los valores van siempre entre { } y viajan aparte como parámetros, así que la inyección SQL no compila. Un bloque transaccion { } guarda todo junto o nada.
La base vive en la carpeta privada de la app (~/.sofia/datos/<id>/.sofiadb/). archivos.* no la ve. Cuando una sentencia o un transaccion { } termina, lo guardado sobrevive a un corte de luz. El detalle del motor está en SofiaDB; el SQL que acepta, en la referencia de SQL; y un recorrido con ejemplos, en el tutorial.
Un ejemplo completo#
app "Citas" id "citas" version "1.0"
permiso consola, datos
tipo Cita { id: entero, profesional: texto, inicio: entero, monto: texto }
fn reservar(prof: texto, inicio: entero) -> texto {
var r = "RESERVADA"
intentar {
transaccion {
let choca = datos.entero($"SELECT count(*) FROM cita WHERE profesional = {prof} AND inicio = {inicio}")
si choca > 0 { fallar("HORA_OCUPADA") }
datos.ejecutar($"INSERT INTO cita (id, profesional, inicio, monto) VALUES ({inicio}, {prof}, {inicio}, {"25.50"})")
}
} si falla e {
r = e
}
devolver r
}
fn principal() {
datos.ejecutar("CREATE TABLE IF NOT EXISTS cita (id INTEGER PRIMARY KEY, profesional TEXT, inicio TIMESTAMPTZ, monto NUMERIC(10,2))")
consola.escribir_linea(reservar("ana", 1790586000000))
para c en datos.consultar(Cita, "SELECT id, profesional, inicio, monto FROM cita ORDER BY inicio") {
consola.escribir_linea($"{c.profesional}: {c.monto} €")
}
}
Funciones#
| Función | Devuelve | Notas |
|---|---|---|
datos.ejecutar(sql) | entero | CREATE TABLE, INSERT, UPDATE, DELETE… Devuelve las filas afectadas |
datos.consultar(Registro, sql) | lista<Registro> | Una fila por registro: cada campo se rellena con la columna del mismo nombre (usa AS para renombrar). Los campos solo pueden ser entero, texto o logico |
datos.entero(sql) · datos.texto(sql) | entero · texto | La primera columna de la primera fila (0 o "" si no hay filas): para count(*), sum(…), un nombre suelto |
datos.copiar(nombre) | entero | Copia en caliente de la base al archivo nombre de la carpeta privada (que no debe existir), sin detener a las demás peticiones: exactamente lo confirmado al empezar, entera y comprobada, o nada. Devuelve sus bytes. Cuenta para el disco; no se permite dentro de transaccion { } |
transaccion { … } | Todo lo del bloque se confirma junto al salir; si algo dentro llama a fallar (también una función llamada desde el bloque o un error de la base), se deshace todo y el fallo sigue hacia fuera con el mismo mensaje |
Consultas parametrizadas obligatorias#
Importante
La inyección SQL es imposible por construcción. El SQL tiene que ser un texto escrito en el programa; los valores van entre llaves, $"… WHERE id = {id}", y la plataforma los pasa aparte, como parámetros ($1, $2…), nunca pegados en el SQL. Un nombre como x'); DROP TABLE cita; -- se guarda tal cual. Armar el SQL con +, pasarlo en una variable o poner comillas alrededor de {valor} no compila.
Entre llaves va un entero, un texto o un logico, o cualquier expresión que dé uno de ellos. Un real se pasa como texto con texto.de_real(x).
Permiso#
permiso consola, datos
Sin permiso datos, las funciones datos.* y transaccion { } no compilan.
Tipos#
| Columna | En el lenguaje | Nota |
|---|---|---|
INTEGER (o ENTERO) | entero | Entero de 64 bits |
TEXT (o TEXTO) | texto | |
BOOLEAN (o LOGICO) | logico | |
BYTEA (o BYTES) | texto | En base64 |
NUMERIC(p,s) (o DECIMAL) | texto | Decimal exacto, para dinero: "25.50", sin coma flotante |
TIMESTAMPTZ (o INSTANTE) | entero | Milisegundos desde 1970, como reloj.ahora() |
Del lenguaje a la base: un entero, texto o logico entre llaves llega con su tipo; un texto se convierte al tipo de la columna como un literal SQL ({"25.50"} a NUMERIC, {"2026-09-28T09:00:00Z"} a TIMESTAMPTZ). Un tipo que no encaja es un fallo recuperable que dice qué columna es.
Reglas#
- SQL: un subconjunto del SQL estándar, en inglés. Todo lo que acepta, sentencia por sentencia, está en la referencia de SQL.
- Sin
NULL, como el lenguaje: toda columna tiene valor (NULLeIS NULLson errores). Las columnas que se añaden conALTER TABLEnecesitanDEFAULT.sumde ninguna fila da0;minymaxde ninguna fila son un fallo. UnLEFT JOINsin pareja y una subconsulta escalar sin filas dan el vacío del tipo, ydatos.consultarrechaza dos columnas con el mismo nombre (usaAS). - Errores recuperables: una clave primaria repetida, un tipo que no encaja o un SQL mal escrito son
fallarrecuperables conintentar { … } si falla e { … }, con el mensaje del motor ene. Los de restricciones empiezan por un código estable:UNICO_VIOLADO:<indice>,FORANEA_VIOLADA:<restriccion>,COMPROBAR_VIOLADO:<restriccion>yCONFLICTO_SERIALIZACION. - Transacciones: sin
transaccion, cadadatos.*es su propia transacción. Dentro detransaccion { }no se puededevolverni salir conromperocontinuar(se guarda el valor en una variable y se devuelve después), ni abrir otratransaccion. Si una sentencia falla dentro del bloque y la app lo recoge con unintentarinterior, la transacción ya no admite más sentencias: se deshace al terminar el bloque. - Repetición automática: si otra transacción confirmó antes un cambio en algo que el bloque leyó, el bloque se repite solo (hasta 5 veces); si sigue chocando, falla con
CONFLICTO_SERIALIZACION. Por eso dentro del bloque solo vadatosy cálculo: llamar ared,archivos,consolau otro permiso no compila. - Varias peticiones a la vez (apps web): comparten la base y no se esperan; cada
transaccion { }trabaja sobre su propia foto. Así «dos reservan la misma hora» deja una sola reserva.
Límites#
| Qué | Límite |
|---|---|
| Tamaño de la base | Hasta el disco de la app (256 MiB por defecto) |
| Una fila | 16 MiB |
| Clave primaria | 1 KiB |
| Columnas por tabla | 1000 |
Valores {…} por sentencia | 4096 |
| Consulta que junta o agrupa | Hasta 64 MiB de memoria: agrega filtros, un índice o LIMIT |
| Espera a que otra petición termine su transacción | 10 s |
| Una transacción larga en una app web | Pasados 30 s falla |
Para copias de seguridad, cifrado y acceso remoto, la línea de órdenes está en sofia datos.