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.

Juste A. Gnimavo (Thales) & Claude | March 26, 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

Thales & Claude thales

Treize agents, quarante-trois minutes : la première session Workflow de Claude Fable 5, et ce qu'un script d'orchestration déterministe change aux builds multi-agents

Un prompt, treize agents, quarante-trois minutes : la première session de production avec Claude Fable 5 et l'outil Workflow de Claude Code a livré un site web de production complet de sept pages plus un endpoint backend de capture de leads, en un seul commit. Le carnet de bord : le script d'orchestration déterministe, le patron d'injection de contrat entre les phases, l'économie par agent du fan-out parallèle, et le suspense de la limite de session que le journal de reprise a transformé en non-événement.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La porte a détecté sa propre dérive : une journée dans CASP avec Claude Fable 5

Nous avons confié au modèle Claude le plus autonome à ce jour les clés de CASP — le CLI open source qui garde les agents de code IA honnêtes face à git — avec l'autorité de rejeter notre propre roadmap. Il a rejeté cinq choses, trouvé deux vrais bugs dans le validateur en le dogfoodant, les a corrigés sous une porte à deux auditeurs, et a laissé casp check entièrement vert sur son propre dépôt pour la première fois. CASP 0.3.0 en est le résultat.

16 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9
Thales & Claude zerosuite

La transplantation du CASP : comment la discipline des six fichiers est passée de Conductor à un ERP transport anti-fraude, ce que la compétence /next ajoute quand l'opérateur tape juste « next », et pourquoi le coût d'une dérive du CASP grimpe quand le projet, c'est l'argent des autres

La discipline du CASP qui a piloté trente-cinq sessions de Conductor est agnostique au produit. Le carnet de bord de sa transplantation sur KASSIA, un ERP transport anti-fraude pour un exploitant de flotte en Côte d'Ivoire : ce qui a migré, ce qui n'a pas migré (le validateur sur mesure — et ce que son absence coûte), ce que la compétence /next ajoute quand l'opérateur tape un seul mot, et là où le CASP s'arrête — le bug de déploiement qu'il ne pouvait pas voir parce qu'il enregistre l'intention, pas la réalité de l'infrastructure.

23 min Jun 8, 2026
kassiaerp-kassia-transport-logistiquezerosuiteCASP +15