Back to flin
flin

Filtrage et tri temporels

Comment nous avons ajouté le filtrage et le tri aux requêtes d'historique temporel de FLIN -- de la décision de conception d'éviter les lambdas à l'implémentation VM de ListFilterField et ListOrderBy.

Thales & Claude | March 30, 2026 4 min flin
EN/ FR/ ES
flintemporalfilteringorderingqueries

Avoir un historique complet des versions pour chaque entité est puissant, mais la puissance brute sans précision n'est que du bruit. Quand un produit a cinquante changements de prix sur six mois, on ne veut pas les cinquante versions -- on veut les versions où le prix dépassait un seuil, triées par date.

La session 082 a complété la catégorie TEMP-4 (requêtes d'historique) à cent pour cent en implémentant les trois dernières tâches : filtrage par valeurs de champs, combinaison de filtres et tri des résultats.

La question des lambdas

Nous avons considéré la syntaxe à base de closures pour le filtrage. Nous l'avons rejetée pour trois raisons.

Premièrement, FLIN n'avait pas le support des lambdas. Implémenter des closures aurait été un détour de plusieurs sessions.

Deuxièmement, le filtrage par champs couvre quatre-vingt-quinze pour cent des cas d'usage. Quand les développeurs filtrent les historiques de versions, ils filtrent presque toujours par valeur de champ.

Troisièmement, une syntaxe plus simple est meilleure pour le public cible de FLIN. Une méthode qui prend un nom de champ, un opérateur et une valeur est plus immédiatement lisible qu'une expression lambda.

La décision : implémenter .where_field(field, operator, value) et .order_by(field, direction) comme méthodes dédiées.

La conception de l'API

flin// Filtering
history.where_field("price", ">", 15.0)
history.where_field("status", "==", "active")

// Ordering
history.order_by("price", "asc")
history.order_by("created_at", "desc")

// Chaining
history.where_field("price", ">=", 12.0).order_by("price", "desc")

Six opérateurs de comparaison : ==, !=, <, <=, >, >=. Deux directions de tri : "asc" et "desc".

Implémentation : deux nouveaux opcodes

ListFilterField (0xF4)

L'opcode ListFilterField dépile quatre valeurs de la pile : la liste, le nom du champ, la chaîne d'opérateur et la valeur de comparaison. Il itère sur la liste, extrait le champ nommé de chaque élément, applique la comparaison et empile une nouvelle liste contenant uniquement les éléments correspondants.

rustOpCode::ListFilterField => {
    let value_val = self.pop()?;
    let op_val = self.pop()?;
    let field_val = self.pop()?;
    let list_val = self.pop()?;

    // Iterate list, filter by field comparison
    let mut filtered = Vec::new();
    for item in list_items {
        let field_value = extract_field(&item, &field_name);
        let matches = match operator.as_str() {
            "==" => values_equal(&field_value, &value_val),
            "!=" => !values_equal(&field_value, &value_val),
            "<"  => compare_numeric_values(&field_value, &value_val) == Less,
            "<=" => compare_numeric_values(&field_value, &value_val) != Greater,
            ">"  => compare_numeric_values(&field_value, &value_val) == Greater,
            ">=" => compare_numeric_values(&field_value, &value_val) != Less,
            _    => false,
        };
        if matches {
            filtered.push(item);
        }
    }

    self.push(create_list(filtered));
}

ListOrderBy (0xF5)

L'implémentation a dû contourner le borrow checker de Rust. La solution a été de pré-extraire toutes les valeurs de champ avant le tri :

rustOpCode::ListOrderBy => {
    // Pre-extract field values to avoid borrow conflicts
    let mut pairs: Vec<(Value, Value)> = Vec::new();
    for item in &list_items {
        let field_value = extract_field(item, &field_name);
        pairs.push((item.clone(), field_value));
    }

    // Sort by extracted field values
    pairs.sort_by(|(_, val_a), (_, val_b)| {
        let cmp = compare_numeric_values(val_a, val_b);
        if direction == "desc" { cmp.reverse() } else { cmp }
    });
}

Pourquoi c'est important pour les applications

Le filtrage et le tri transforment le modèle temporel de « nous stockons l'historique » en « nous pouvons répondre à des questions sur l'historique ».

Suivi des prix :

flinexpensive_history = product.history
    .where_field("price", ">", 100)
    .order_by("created_at", "asc")

Conformité d'audit :

flinrecent_changes = document.history
    .where_field("created_at", ">", last_month)
    .order_by("created_at", "desc")

Détection d'anomalies :

flinlow_stock_events = inventory.history
    .where_field("quantity", "<", safety_threshold)

Chacun de ces cas nécessiterait une requête SQL personnalisée, une table de reporting dédiée ou un pipeline d'analyse dans une application traditionnelle. Dans FLIN, ce sont des lignes uniques qui se composent naturellement avec le reste de l'API temporelle.

Deux cent cinquante lignes de code d'implémentation. Deux nouveaux opcodes. Deux nouvelles signatures de types. Et les requêtes d'historique de FLIN sont devenues prêtes pour la production.


Ceci est la partie 5 de la série sur le modèle temporel de « How We Built FLIN », documentant le système de filtrage et de tri pour les requêtes d'historique temporel.

Navigation de la série : - [046] Every Entity Remembers Everything: The Temporal Model - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering (vous êtes ici)

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles