Models et AST (`model!`)

DSL `model!` & `extend!`

Structure du DSL `model!`

Le parseur attend les blocs dans cet ordre strict (les blocs optionnels peuvent être absents mais pas réordonnés) :

model! {
    NomModele,              // 1. Nom (PascalCase)
    table: "nom_table",     // 2. Nom de la table SQL
    pk: champ => type,      // 3. Clé primaire
    enums: { ... },         // 4. Optionnel — enums locaux
    fields: { ... },        // 5. Champs (syntaxe v1 ou v2)
    relations: { ... },     // 6. Optionnel — relations SeaORM
    meta: { ... },          // 7. Optionnel — contraintes & tri
}

Deux syntaxes de champs

Syntaxe v1 — types SQL explicites (bloc nommé fields:) :

model! {
    Article,
    table: "articles",
    pk: id => i32,
    fields: {
        titre:      String [required, max_len(150)],
        contenu:    text   [required],
        is_active:  bool,
        created_at: datetime [auto_now],
    },
}

Syntaxe v2 — types sémantiques (bloc anonyme { ... }) :

model! {
    Article,
    table: "articles",
    pk: id => i32,
    {
        titre:      text      [required, max_length: 150],
        contenu:    textarea  [required],
        is_active:  bool      [default: true],
        created_at: datetime  [auto_now],
    }
}

En syntaxe v2, le bloc anonyme remplace à la fois fields: et form_fields: — ces deux blocs nommés n'existent pas en v2. En v1, form_fields: est un bloc optionnel parsé après meta:, permettant d'ajouter des annotations sémantiques sur des champs déjà déclarés en SQL.


Clé primaire (`pk`)

pk: nom_champ => type
TypeSQL PostgresSQL MySQLAuto-incrémentCréation
i32SERIALINT AUTO_INCREMENT✅ Ouiséquence DB
i64BIGSERIALBIGINT AUTO_INCREMENT✅ Ouiséquence DB
uuidUUIDVARCHAR(36)❌ NonUuid::new_v4() côté Rust
Pkalias i32 ou i64idem✅ Ouiselon feature big-pk

L'alias Pk résout en i32 par défaut, ou i64 si la feature big-pk est activée :

[dependencies]
runique = { version = "2.1.5", features = ["big-pk"] }

Utilisez big-pk quand vous anticipez plus de ~2 milliards de lignes dans une table, ou pour interopérer avec un schéma existant utilisant des clés primaires BIGINT.

Contraintes lors de l'activation de big-pk :

  • Chaque colonne FK pointant vers une clé primaire Pk doit aussi être déclarée bigint, sinon vous obtenez une erreur de type à la compilation :
derive_form! {
    Commande {
        fields: {
            user_id: bigint [required]   // doit correspondre à users.id qui est Pk (i64)
        }
    }
}
  • Le daemon admin génère parse::<Pk>() par défaut dans admin.rs, le code généré suit donc automatiquement la feature — aucun ajustement manuel nécessaire.

  • Les fichiers de seeds et tout code manuel qui assigne entity.id (un Pk) à un champ FK i32 doivent utiliser .try_into().unwrap() ou changer la colonne FK en bigint.

big-pk doit être décidé avant la première migration. Une fois les migrations appliquées, basculer entre big-pk et le mode par défaut (i32) est un changement cassant : les colonnes en base sont déjà INT ou BIGINT, et changer la feature flag ne modifie que le type Rust — le schéma reste intact. Changer après coup nécessite une migration manuelle pour ALTER chaque colonne PK et FK, avec un risque de troncature des données si des IDs existants dépassent i32::MAX. Choisissez un mode au démarrage du projet et ne le changez pas.


Types de champs — syntaxe v1

Types SQL déclarés directement :

Type DSLType Rust généréColonne SQL
StringStringVARCHAR(255)
textStringTEXT
charStringCHAR
varchar(n)StringVARCHAR(n)
i8i32TINYINT
i16i32SMALLINT
i32i32INTEGER
i64i64BIGINT
u32u32INTEGER UNSIGNED
u64u64BIGINT UNSIGNED
f32f32FLOAT
f64f64DOUBLE
decimalDecimalDECIMAL
decimal(p, s)DecimalDECIMAL(p, s)
boolboolBOOLEAN
dateNaiveDateDATE
timeNaiveTimeTIME
datetimeNaiveDateTimeDATETIME
timestampNaiveDateTimeTIMESTAMP
timestamp_tzNaiveDateTimeTIMESTAMPTZ
uuidUuidUUID
jsonserde_json::ValueJSON
json_binaryserde_json::ValueJSON BINARY
binaryVec<u8>BINARY
binary(n)Vec<u8>BINARY(n)
var_binary(n)Vec<u8>VARBINARY(n)
blobVec<u8>BLOB
inetStringINET
cidrStringCIDR
mac_addressStringMACADDR
intervalStringINTERVAL
enum(NomEnum)NomEnumINTEGER / ENUM / VARCHAR

Types de champs — syntaxe v2 (sémantiques)

Convertis automatiquement en types SQL :

Type sémantiqueSQL généréNotes
textVARCHAR(255) ou VARCHAR(n) si max_length: n
emailVARCHAR(254)Format email validé
passwordVARCHAR(255)Haché automatiquement
richtextTEXTÉditeur HTML
textareaTEXTMulti-ligne
urlVARCHAR(255)Format URL validé
slugVARCHAR(255)
colorVARCHAR(255)Couleur hexadécimale
ipINET
phoneVARCHAR(20) ou VARCHAR(n) si max_length: n<input type="tel">
intINTEGER
bigintBIGINT
floatDOUBLE
decimalDECIMAL
percentDOUBLEStocké comme float
boolBOOLEAN
dateDATE
timeTIME
datetimeDATETIME
uuidUUID
jsonTEXT
imageVARCHAR(255)Stocke le chemin du fichier
documentVARCHAR(255)Stocke le chemin du fichier
fileVARCHAR(255)Stocke le chemin du fichier
choiceVARCHAR / ENUM natifRequiert enum(NomEnum)
radioIdem choiceWidget différent, même SQL
checkboxIdem choiceWidget différent, même SQL

Options de champ — syntaxe v1

Dans un bloc [...], séparées par des virgules :

username: String [required, max_len(150), unique],
OptionDescription
requiredColonne NOT NULL + validation formulaire
nullableColonne NULL — type Rust Option<T>
uniqueContrainte UNIQUE
indexIndex simple (non unique)
default(valeur)Valeur par défaut SQL (true, 0, "draft", etc.)
max_len(n)Longueur max (validation + VARCHAR(n))
min_len(n)Longueur min (validation)
max(n)Valeur max entière (validation)
min(n)Valeur min entière (validation)
max_f(n)Valeur max flottante
min_f(n)Valeur min flottante
auto_nowAssigné à NOW() à chaque INSERT — exclu des formulaires
auto_now_updateAssigné à NOW() à chaque UPDATE — exclu des formulaires
readonlyExclu des formulaires générés
select_as(str)Alias SQL dans les SELECT
label("str")Libellé personnalisé dans les formulaires admin
help("str")Texte d'aide (réservé)
fk(table.col, action)Contrainte clé étrangère (voir Relations)
file(kind)Champ fichier — image, document, any
file(kind, "path")Champ fichier avec dossier d'upload explicite
max_size(n)Taille max upload — n KB, n MB, n GB

Options de champ — syntaxe v2

Utilisent : au lieu de () pour les valeurs :

username: text [required, max_length: 150, unique],
Option v2Équivalent v1Notes
requiredrequired
nullablenullable
uniqueunique
max_length: nmax_len(n)
min_length: nmin_len(n)
min: nmin(n)
max: nmax(n)
min: n.0min_f(n)
max: n.0max_f(n)
default: valeurdefault(valeur)
auto_nowauto_now
auto_now_updateauto_now_update
upload_to: "path"file(kind, "path")
max_size: n MBmax_size(n MB)
rows: nV2 uniquement (textarea)
step: nV2 uniquement (numériques)
fk(table.col, action)fk(table.col, action)
enum(NomEnum)enum(NomEnum)
skipreadonly
no_hashChamps password uniquement

auto_now / auto_now_update : ces champs sont exclus de admin_from_form et d'admin_partial_update. Leur valeur est gérée uniquement par la base. Ils apparaissent dans Model et Column comme Option<T>.


Enums

Les enums se déclarent dans un bloc enums: { ... } distinct des champs, puis sont référencés via enum(NomEnum).

model! {
    Commande,
    table: "commandes",
    pk: id => i32,
    enums: {
        StatutCommande: [
            EnAttente  = ("en_attente",  "En attente"),
            EnCours    = ("en_cours",    "En cours"),
            Livree     = ("livree",      "Livrée"),
            Annulee    = ("annulee",     "Annulée"),
        ],
        Priorite: i32 [Basse = 0, Normale = 1, Haute = 2, Urgente = 9],
    },
    {
        statut:   choice [enum(StatutCommande), required],
        priorite: choice [enum(Priorite), required],
    },
}

Quatre formes de variant

SyntaxeValeur DBLibellé affiché (Display)
Variant"Variant""Variant"
Variant: "Libellé""Variant""Libellé"
Variant = "valeur_db""valeur_db""valeur_db"
Variant = ("valeur_db", "Libellé")"valeur_db""Libellé"

La valeur DB est stockée exactement telle qu'écrite. Aucune transformation automatique.

Types de backing

SyntaxeStockage DB
NomEnum: [A, B]ENUM natif (Postgres) ou VARCHAR (MySQL/SQLite)
NomEnum: i32 [...]INTEGER
NomEnum: i64 [...]BIGINT

Méthodes générées

MéthodeRetourDescription
.to_string()StringLibellé d'affichage
.db_value()&'static str / i32 / i64Valeur exacte en base
::from_str(s) / .parse()Result<Self, ()>Parsing depuis valeur DB, libellé, ou nom variant
::iter()impl Iterator<Item = Self>Itération sur tous les variants
use sea_orm::Iterable;

let s = StatutCommande::EnAttente;
s.db_value()   // → "en_attente"
s.to_string()  // → "En attente"

// Pour un <select>
let options: Vec<(String, String)> = StatutCommande::iter()
    .map(|v| (v.db_value().to_string(), v.to_string()))
    .collect();

// Parser depuis une valeur DB
let statut: Option<StatutCommande> = "en_attente".parse().ok();

Dans les templates Tera, la valeur de comparaison doit correspondre exactement à ce qui est stocké en base (sensible à la casse).


Champs fichier

model! {
    Article,
    table: "articles",
    pk: id => i32,
    {
        image:        image    [upload_to: "media/articles"],
        fichier:      document [upload_to: "docs/"],
        piece_jointe: file     [upload_to: "media/uploads"],
    },
}
TypeExtensions autorisées
imagejpg jpeg png gif webp avif
documentpdf doc docx txt odt
fileaucun filtre

upload_to: est obligatoire pour les trois types. Le chemin est relatif à MEDIA_ROOT.


Relations

relations: {
    belongs_to: Model via champ_fk,
    has_many: Model,
    has_many: Comments as user_comments,   // alias optionnel
    has_one: Profile as user_profile,
    many_to_many: Roles through UserRoles via self_id,
}
TypeContrainte DBDescription
belongs_to❌ code seulRelation N-1 (SeaORM)
has_many❌ code seulRelation 1-N
has_one❌ code seulRelation 1-1
many_to_many❌ code seulRelation N-N via pivot

Contrainte FK réelle : la contrainte SQL FOREIGN KEY et son action (cascade, restrict, set_null, set_default) sont déclarées sur l'option fk(table.col, action) du champ, pas dans le bloc relations:. Le bloc relations: génère uniquement les traits SeaORM pour la navigation objet.

Actions FK disponibles sur l'option fk(...) : cascade · restrict · set_null · set_default


Meta

meta: {
    ordering: [-created_at, titre],
    unique_together: [(slug, lang)],
    indexes: [(lang, sort_order)],
    verbose_name: "Article",
    verbose_name_plural: "Articles",
}
CléSyntaxeEffet
ordering[champ, -champ]Tri par défaut, - = DESC
unique_together[(col1, col2)]Contrainte UNIQUE multi-colonnes
indexes[(col1, col2)]Index simple multi-colonnes
verbose_name"chaîne"Nom singulier dans l'interface admin
verbose_name_plural"chaîne"Nom pluriel dans l'interface admin
abstracttrueModèle abstrait — aucune table générée

`label` et `help`

Par défaut, le libellé est généré depuis le nom snake_case (sort_orderSort order). L'option label(...) le remplace :

fields: {
    titre:        text [required, label("Titre de l'article")],
    sort_order:   i32  [label("Ordre d'affichage")],
    is_published: bool [label("Publié")],
},

label et help sont des options v1 uniquement — non disponibles dans le bloc anonyme v2.

Le libellé s'applique au formulaire admin et aux en-têtes de colonnes dans list_display. Il n'a aucun effet sur la migration.


`extend!{}` — extension des tables framework

Ajoute des colonnes à une table Runique sans la redéfinir. Le CLI makemigrations détecte les blocs extend!{} et génère des instructions ALTER TABLE ADD COLUMN.

extend! {
    table: "eihwaz_users",
    fields: {
        avatar:  image   [upload_to: "avatars/"],
        bio:     textarea,
        website: url     [required],
    }
}

Tables autorisées : eihwaz_users, eihwaz_groupes, eihwaz_droits, eihwaz_sessions, eihwaz_users_groupes, eihwaz_groupes_droits. Tout autre nom provoque une erreur à la compilation.

Les champs déclarés dans extend!{} utilisent les mêmes types et options que la syntaxe v2 de model!.

Limitation : la macro search ne fonctionne pas encore sur les colonnes ajoutées via extend!{}.