D'une machine nue à un programme Keal qui parle à une base PostgreSQL qu'il a créée lui-même. Chaque commande est donnée, avec ce qu'elle fait et ce que l'écran doit montrer avant de continuer. Comptez une demi-heure, surtout à attendre des compilateurs.
Il faut un terminal et une connexion internet. Les commandes sont celles de Linux (Debian, Ubuntu) et macOS ; des notes Windows sont données là où ça diffère — le compilateur, le SQL et les migrations y marchent sous Git Bash, les fonctions stockées et le client pas encore.
Quand une commande commence par sudo, la machine demandera votre mot de passe : c'est pour installer des paquets système, rien d'autre.
Le compilateur de Keal se construit avec l'outillage de Rust, et c'est la seule raison d'en avoir besoin. rustup l'installe dans votre répertoire personnel, sans root :
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh . "$HOME/.cargo/env" cargo --version
cargo 1.8x.x (…)
Sous Windows, téléchargez rustup-init.exe sur rustup.rs et choisissez l'outillage GNU (x86_64-pc-windows-gnu) avec MinGW : c'est celui avec lequel KealSql a été testé.
Keal est le langage dans lequel KealSql est écrit et vers lequel il compile. Clonez-le, construisez-le, et mettez le binaire sur votre chemin. La construction prend une ou deux minutes.
git clone https://github.com/geneacta/keal cd keal cargo build --release export PATH="$PWD/target/release:$PATH" keal version cd ..
keal 1.2.0
La ligne export PATH ne vaut que pour ce terminal ; ajoutez-la au fichier de démarrage de votre shell (~/.bashrc, ~/.zshrc) pour la garder. cargo install --path . dans keal/ est l'alternative permanente.
KealSql veut Keal 1.3.0 ou plus récent — la version dont le chargeur lit un import de .kealsql.
La base elle-même, ses outils clients, et — pour les fonctions stockées et le client — ses en-têtes de développement.
# Debian, Ubuntu sudo apt install postgresql postgresql-server-dev-all libpq-dev # macOS avec Homebrew brew install postgresql@17 brew services start postgresql@17 psql --version pg_config --includedir
psql (PostgreSQL) 17.x /usr/include/postgresql
Sous Windows, l'installeur EDB donne psql, createdb et pg_config ; ajoutez son répertoire bin au chemin.
PostgreSQL a ses propres utilisateurs. Sous Linux le serveur est installé avec un seul, postgres, que seul l'utilisateur système du même nom peut devenir ; le plus simple est de créer un utilisateur PostgreSQL à votre nom de login, pour que psql se connecte sans rien demander :
sudo -u postgres createuser --superuser "$USER" psql -d postgres -c 'select current_user'
current_user -------------- renard (1 row)
Sous macOS avec Homebrew, le serveur connaît déjà votre utilisateur ; sautez la première ligne. Sous Windows, l'installeur vous a demandé le mot de passe de postgres : posez PGUSER=postgres et PGPASSWORD=… dans l'environnement, ou écrivez-les dans %APPDATA%\postgresql\pgpass.conf.
Si ça ne marche pas : peer authentication failed veut dire que l'utilisateur PostgreSQL n'existe pas encore — la première ligne a échoué, lisez son message. connection refused veut dire que le serveur ne tourne pas : sudo systemctl start postgresql sous Linux.
Clonez-le, récupérez le morceau de Keal qu'il importe (son lexer, épinglé sur un commit), et construisez le compilateur en un binaire nommé kealsql. Le chargeur de Keal cherchera ce binaire par son nom quand un programme importera un fichier .kealsql.
git clone https://github.com/geneacta/kealsql cd kealsql keal fetch keal build src/main.keal -o kealsql export PATH="$PWD:$PATH" kealsql tests/cases/blog.kealsql | head -5 cd ..
CREATE TABLE "user" (
id serial PRIMARY KEY,
name text UNIQUE NOT NULL,
email text UNIQUE NOT NULL,
bio textTant que le binaire n'est pas sur le chemin, keal src/main.keal fichier.kealsql fait la même chose sur la VM de Keal, et export KEALSQL=/chemin/vers/kealsql dit au chargeur où il est.
Facultatif mais rassurant : tests/run.sh lance toute la suite. Avec PostgreSQL installé, elle démarre un serveur privé dans un répertoire temporaire et y exécute chaque cas ; les dernières lignes doivent être des ok et des skip, jamais un FAIL.
Faites un répertoire pour le projet et écrivez le schéma et ses requêtes. C'est le blog que la suite utilise ; lisez les notes dessous.
mkdir monblog cd monblog # puis créez blog.kealsql avec le contenu ci-dessous
enum Status { Draft, Published }
table User {
id: Id
name: Slug
unique email: String
bio: String?
}
table Post {
id: Id
cascade author: RefId<User>
editor: RefId<User>?
title: String
status: Status
created: Timestamp
// A method on the row, for the programs that read it.
func headline(): String { "${this.title} [${this.status}]" }
}
func byAuthor(name: String): List<(Int, String)> {
from(Post as p)
.where(p.author.name == name)
.where(status == Published)
.orderBy(created.desc)
.select(p.id, p.title)
}
func editorOf(post: Int): String? {
from(Post).where(id == post).select(editor?.name).first()
}
func drafts(): Int {
from(Post).where(status == Draft).count()
}
func publish(post: Int): Post {
update(Post).where(id == post).set(status = Published)
}
proc forget(user: RefId<User>) {
delete(Post).where(author == user)
}enum Status devient un type enum PostgreSQL. table User { … } devient une table : id: Id est une clé primaire serial, name: Slug un texte unique — chaque table a un primary et au plus un slug. String? est une colonne qui peut être nulle ; String ne peut pas.
cascade author: RefId<User> est une clé étrangère vers le primary de User, supprimée avec l'utilisateur ; editor: RefId<User>? en est une optionnelle, mise à null quand l'utilisateur disparaît.
Une func est une requête avec un nom, des paramètres et un résultat déclaré. Elle se lit de from à select, dans l'ordre où la base l'évalue. p.author.name suit la référence — une jointure que le compilateur écrit. editor?.name en suit une optionnelle — une jointure gauche, et le résultat est String?. Un proc est une modification qui ne répond rien.
Le compilateur transforme le fichier en SQL : les tables en instructions CREATE, puis une instruction préparée par requête. Regardez le fichier écrit ; c'est du SQL ordinaire que vous auriez pu taper.
kealsql blog.kealsql > blog.sql grep -c PREPARE blog.sql grep -A5 'func editorOf' blog.sql
5 -- func editorOf(post: Int): String? PREPARE editor_of(integer) AS SELECT editor.name FROM post LEFT JOIN "user" AS editor ON post.editor = editor.id WHERE post.id = $1
Si ça ne marche pas : une ligne error blog.kealsql:L:C … nomme l'endroit et dit quoi écrire. La plus fréquente sur un premier fichier : une colonne nommée dans une requête que la table n'a pas.
createdb fait une base vide nommée blog ; y charger le fichier compilé fait les tables. Puis regardez-les.
createdb blog psql -d blog -f blog.sql psql -d blog -c '\dt'
List of tables Schema | Name | Type | Owner --------+------+-------+-------- public | post | table | you public | user | table | you (2 rows)
Les instructions CREATE sont permanentes. Les PREPARE ne le sont pas : elles vivent le temps de la session psql qui les a exécutées. C'est pourquoi l'étape suivante les recharge, avec --queries, dans la session qui s'en sert — et pourquoi un programme (étape 12) prépare les siennes.
Une session psql : charger les requêtes — --queries imprime les PREPARE sans les CREATE, qui échoueraient sur des tables qui existent — insérer deux utilisateurs et trois billets, et appeler les requêtes par leur nom. EXECUTE est la façon d'appeler une instruction préparée ; les noms sont ceux du fichier, en snake_case.
kealsql --queries blog.kealsql > queries.sql
psql -d blog
blog=> \i queries.sql
blog=> INSERT INTO "user" (name, email) VALUES ('ada', 'ada@x'), ('bob', 'bob@x');
blog=> INSERT INTO post (author, editor, title, status, created) VALUES
(1, 2, 'Hello', 'Published', now()),
(1, NULL, 'Draft one', 'Draft', now()),
(2, NULL, 'Bob post', 'Published', now());
blog=> EXECUTE by_author('ada');
blog=> EXECUTE editor_of(2);
blog=> EXECUTE drafts;
blog=> \q id | title
----+-------
1 | Hello
(1 row)
name
------
(1 row)
count
-------
1
(1 row)editor_of(2) répond une ligne qui ne tient rien : le billet 2 existe et n'a pas d'éditeur. C'est la jointure gauche que le ?. du fichier demandait. by_author('ada') répond un billet, pas deux : le fichier dit status == Published, et le second billet d'Ada est un brouillon.
Maintenant les mêmes requêtes depuis un programme. Un programme Keal importe le fichier .kealsql lui-même ; le chargeur de Keal lance kealsql pour écrire un module à côté, .kealsql/blog.client.keal, où chaque requête est une méthode sur une connexion. Écrivez app.keal à côté de blog.kealsql :
import "./blog.kealsql"
// La base et ses tables, créées si elles manquent ; une connexion dessus.
val db = createBlog("", "blog")
println("drafts: ${db.drafts()}")
for (p in db.byAuthor("ada")) {
println("#${p.id} ${p.title}")
}
val editor = db.editorOf(1)
println("editor of #1: ${editor ?: "none"}")
db.close()createBlog("", "blog") : le premier argument est une chaîne de connexion libpq — vide, elle prend les mêmes défauts que psql (votre utilisateur, le serveur local) ; le second est le nom de la base, créée si elle n'existe pas. Ici elle existe, avec des lignes, donc l'appel ne fait que se connecter.
db.byAuthor("ada") répond une liste de records avec id et title ; db.editorOf(1) répond un String?, d'où le ?: "none".
Le programme se lie à libpq, la bibliothèque cliente de PostgreSQL ; les deux drapeaux disent où sont son en-tête et sa bibliothèque.
keal build app.keal -I$(pg_config --includedir) -lpq -o app ./app ls .kealsql
drafts: 1 #1 Hello editor of #1: bob blog.client.keal
Le répertoire .kealsql/ tient le module généré. Commitez-le avec le projet : un checkout se construit alors sans kealsql installé, et Keal le régénère dès que blog.kealsql est plus récent.
Si ça ne marche pas : cannot generate … `kealsql` is not installed — le binaire de l'étape 6 n'est pas sur le chemin ; export KEALSQL=/chemin/vers/kealsql. libpq-fe.h: No such file — installez libpq-dev (Linux) et vérifiez pg_config --includedir. connection refused ou authentication failed — étape 5.
Renommez la requête drafts en draftCount dans blog.kealsql et reconstruisez le programme, sans y toucher :
sed -i 's/func drafts()/func draftCount()/' blog.kealsql keal build app.keal -I$(pg_config --includedir) -lpq -o app
error: `BlogDb` has no method `drafts` --> app.keal:6:23
Le chargeur a vu que le fichier était plus récent, a régénéré le module, et le programme a cessé de compiler à la ligne qui utilisait l'ancien nom. C'est tout le propos : une requête que le fichier n'a plus est une erreur dans le programme, pas un échec à l'exécution. Remettez drafts, ou corrigez le programme.
Ajoutez une colonne au fichier — bio: String? y est déjà ; ajoutez joined: Date? dessous dans User — et demandez ce qui manque à la base vivante :
kealsql --migrate blog.kealsql --db blog
ALTER TABLE "user" ADD COLUMN joined date;
Rien n'a été appliqué : la migration est imprimée pour que vous la lisiez. Appliquez-la en une transaction et redemandez — la réponse doit être nothing to do :
kealsql --migrate blog.kealsql --db blog > migration.sql psql -1 -d blog -f migration.sql kealsql --migrate blog.kealsql --db blog
-- nothing to do: the database matches the declaration
Une colonne supprimée, un type rétréci ou un nouveau NOT NULL seraient imprimés en commentaires, retenus jusqu'à ce que vous passiez --destructive — le compilateur nomme ce qu'ils coûteraient. Un renommage n'est jamais deviné : écrivez renamed(ancienNom) nouveauNom: Type dans le fichier, et la migration renomme.
Une stored func est du Keal que PostgreSQL exécute comme fonction native. Ajoutez-en une à blog.kealsql et servez-vous-en dans une requête :
stored pure func shout(s: String): String { s.toUpper() + "!" }
func shouted(): List<String> {
from(Post).orderBy(id).select(shout(title))
}Puis générez la source de la bibliothèque, construisez-la contre les en-têtes serveur, et chargez les fonctions avec le chemin de la bibliothèque — --queries, parce que les tables existent :
kealsql --plkeal build/ blog.kealsql
sh build/build.sh
kealsql --lib "$PWD/build/blog.so" --queries blog.kealsql | psql -d blog
psql -d blog -c "SELECT shout('hello')"shout -------- HELLO! (1 row)
Si ça ne marche pas : postgres.h: No such file — les en-têtes serveur manquent : postgresql-server-dev-all sous Debian et Ubuntu. Sous Windows cette étape n'est pas encore disponible.
Premiers pas est le même parcours en cinq minutes ; les docs tiennent le design, la grammaire avec ce que chaque construction compile, et le README. examples/shop.kealsql est un vrai fichier plus gros à lire ensuite.