ADR 0028 – Moteur de vues : langage fermé traduit en SQL
Accepté (2026-10-15, mode autonome)
ADR 0028 – Moteur de vues : langage fermé traduit en SQL
Statut : Accepté (2026-10-15, mode autonome)
Décisions prises en mode autonome (Aurélien, 2026-10-15 : options les plus simples, sûres et réversibles).
Contexte
Le jalon 2 veut des vues déclarées dans config/views/ : quels nœuds, triés comment, avec quelles colonnes. La condition de filtre est écrite par l’utilisateur dans un fichier YAML. Elle ne doit jamais devenir du code exécuté, ni du SQL concaténé.
Décision
Format
name: factures-a-payer
label: Factures à payer
icon: receipt_long # facultatif
query:
aspects: [facture] # le nœud les porte tous
types: [facture-fournisseur] # l'un d'eux
tags: [maison] # tous, sous-tags compris (ADR 0006)
where: "!facture.payee && facture.echeance <= today + 14d"
sort: [facture.echeance asc, title desc] # 5 clés au plus, vides en dernier
limit: 200 # 1 à 1000, défaut 200
display:
mode: table # table (défaut), list, kanban
columns: [title, facture.montant, facture.echeance, link:concerne-projet]
groupBy: tache.statut # enum, type ou booléen ; exigé par kanban
Les colonnes possibles sont :
title,type,tags,createdAt,updatedAt;<aspect>.<champ>(champ de premier niveau) ;link:<type de lien>, qui donne les cibles sortantes.
Le méta-schéma view.schema.json fixe la forme. Le chargeur vérifie ensuite que les aspects, types, champs et liens existent, et il analyse puis type la condition. Une vue fautive est ignorée et signalée, comme les autres définitions. Lors d’un rechargement, sa version précédente est conservée si elle reste valide.
Langage where
La grammaire est fermée (definitions/query/ExprParser) :
||et&&;!;- les comparaisons
== != < <= > >=; - les parenthèses ;
- les littéraux
'texte'ou"texte", les nombres (négatifs compris),true,false,null; todayetnow, avec ou sans décalage± N d|w|m.
Les champs sont ceux des colonnes, sans tags ni liens. Il n’existe ni appel de fonction, ni opérateur arithmétique, ni accès imbriqué.
Garde-fous :
- 2 000 caractères au plus ;
- 32 niveaux d’imbrication au plus ;
- décalage de 4 chiffres au plus.
Les erreurs sont en français, avec leur position.
Le contrôle de type (ExprChecker) vérifie les points suivants :
- la condition est booléenne ;
- chaque comparaison porte sur au moins un champ ;
- le littéral a le type du champ : nombre, booléen, date
AAAA-MM-JJoutodaypour une date, valeur de l’enum ou type chargé, UUID pour une référence ; < <= > >=ne servent que pour le texte, les nombres et les dates ;nullne s’emploie qu’avec==et!=;nowest refusé sur une date (seultodayy est admis).
Le type d’un champ vient de son JSON Schema (Fields).
Traduction SQL (node/ViewSql)
Chaque aspect cité est joint une fois (left join node_aspect aN). Le nom de l’aspect, le nom du champ et les valeurs sont des paramètres liés. Les opérateurs viennent d’une table fixe. today et now sont résolus par le serveur dans le fuseau zone du navigateur, avec l’horloge de l’application.
On applique une logique à deux valeurs, pour que ! se comporte comme on l’attend :
- une comparaison d’ordre avec une valeur absente est fausse (
coalesce(…, false)) ; ==et!=traitent l’absence comme une valeur (is [not] distinct from), donctache.echeance != '2026-10-20'retient les tâches sans échéance ;- un booléen absent vaut faux, donc
!facture.payeeretient les factures sans le champ ; champ == nullest vrai si la valeur est absente : clé manquante,null, ou valeur du mauvais type JSON.- Date-heure comparée à un jour (
today,'AAAA-MM-JJ') : le jour est l’intervalle [minuit, minuit du lendemain[ du fuseau. AinsicreatedAt == todayretient ce qui date d’aujourd’hui etcreatedAt <= '2026-10-15'inclut le 15. - Référence : un UUID littéral est canonisé en minuscules, comme les valeurs stockées.
Les données sont tolérées à la lecture : une valeur du mauvais type JSON vaut absente, et la requête n’échoue jamais.
- Nombre : il est lu seulement si
jsonb_typeof = 'number'(de même'string'pour un texte, un enum, une référence, et'boolean'pour un booléen). - Date : elle est comparée en texte
AAAA-MM-JJ, en collationC, si elle a cette forme. - Date-heure d’aspect : elle passe par
sb_try_timestamptz(migration V9), fonction SQL inlinable sans bloc d’exception : forme ISO 8601 exigée, puispg_input_is_valid(PostgreSQL 16 et plus). Elle rendnullau lieu d’une erreur, et refusenowouinfinity.
Ordre de tri :
- les clés de la vue, valeurs vides en dernier ;
- le titre, sans tenir compte de la casse ;
- l’id.
Un enum se trie dans l’ordre déclaré du schéma (array_position), si bien que priorite desc donne haute, normale, basse.
API
GET /api/definitions/views: définitions, triées par libellé.GET /api/views/{name}/results?zone=&limit=renvoie :- les colonnes (clé, libellé, nature, valeurs d’enum) ;
- les lignes (
valuespar clé de colonne,groupsigroupBy) ; groups, dans l’ordre du schéma ;truncated.
limit est plafonnée par celle de la vue. Les autres conventions :
- un champ référence est rendu par l’id de sa cible, une colonne
link:par la liste des ids de ses cibles hors corbeille, triées par titre ; nodesdécrit ces nœuds par id (NodeReference: titre, type,deleted) ; un id absent denodesest introuvable ;tagsréunit les tags saisis et ceux du corps.
Pas de pagination par curseur pour l’instant : la limite et truncated suffisent aux volumes d’un deuxième cerveau personnel.
Interface
- Le menu « Vue : … ▾ » de l’en-tête liste les vues sous les pages.
- La route
/views/:nameaffiche les résultats en table AG Grid, en lecture seule. - Un clic sur une ligne, ou Entrée sur une cellule, ouvre le nœud.
- L’onglet de page porte le libellé et l’icône de la vue.
- Les modes
listetkanban, le tri, le regroupement et l’édition en ligne viennent avec les tranches suivantes. D’ici là, toute vue s’affiche en table.
Amendement (ADR 0029) : la table n’est plus en lecture seule. Tri par en-tête (paramètre
sort), regroupement et édition en ligne des champs d’aspect.Amendement (ADR 0030) : le mode
lists’affiche en liste compacte, avec une bascule Table/Liste sur la page.Amendement (ADR 0035) : le mode
kanbans’affiche en plateau (colonnes, couloirs, limite WIP, déplacement qui écrit le champ).
Alternatives écartées
- SpEL ou un évaluateur d’expressions : il serait trop puissant, car une expression écrite dans un fichier deviendrait du code. Il serait aussi évalué en Java, après lecture de tous les nœuds.
- JSONPath ou
jsonb_path_query: non typé, sans contrôle au chargement, avec des messages d’erreur peu lisibles. - Cast SQL direct (
::date,::timestamptz) : une seule valeur ancienne non conforme ferait échouer toute la vue.
Conséquences
- Ajouter un opérateur ou une fonction (par exemple
in,containssur un tableau, ou un filtre de sous-arbre) demande de modifier, avec leurs tests :- la grammaire ;
- le contrôle de type ;
- la traduction SQL.
- Le filtre de sous-arbre, paramétré par le nœud courant, arrive avec les vues contextuelles.
Amendement (ADR 0036) : les vues contextuelles apportent
nodeetparent(parent == node: enfants directs) ; le sous-arbre complet (descendants) est écarté tant qu’aucun besoin ne le demande. - Les tags de
query.tagssont canonisés à l’exécution mais pas contrôlés au chargement : un tag refusé par la normalisation ne retient aucun nœud. - Une vue nouvelle qui cite un aspect dont le fichier est devenu invalide (et qui garde sa version précédente) est écartée : elle n’est contrôlée que contre les définitions fraîches.
todaydépend du fuseau de la requête : une même vue peut différer d’un jour selon le navigateur, comme les valeurs par défaut (ADR 0017).