clickhouse-rust-view-column-order

Par divinevideo · divine-mobile

Déboguer les erreurs de désérialisation ClickHouse en Rust causées par un décalage dans l'ORDER des colonnes lors de l'utilisation de `SELECT alias.*` sur une VIEW qui ajoute des colonnes calculées. À utiliser quand : (1) Certaines variantes de requête (ex. `sort=trending`) renvoient une erreur 500 mais d'autres (`sort=recent`) réussissent, (2) Les requêtes utilisent `SELECT view_alias.*, extra_cols FROM view JOIN ...`, (3) La struct Rust `Row` possède le bon nombre et les bons types de champs, mais la désérialisation échoue quand même (erreur de type incorrect, pas « données insuffisantes »), (4) La VIEW a récemment été étendue avec une colonne calculée (ex. `trending_score`).

npx skills add https://github.com/divinevideo/divine-mobile --skill clickhouse-rust-view-column-order

ClickHouse Rust : Ordre des colonnes calculées dans les vues

Problème

Lors de l'utilisation de SELECT alias.* , extra_col FROM my_view alias LEFT JOIN ..., ClickHouse développe alias.* pour inclure TOUTES les colonnes définies dans la vue — y compris les colonnes calculées ajoutées par la vue elle-même (p. ex. SELECT *, expr AS trending_score FROM base). Ces colonnes calculées apparaissent À L'INTÉRIEUR du développement alias.*, AVANT les colonnes ajoutées après dans la requête externe.

Le crate Rust clickhouse (#[derive(Row)]) désérialise positionnellement — le champ N dans la struct Rust reçoit la colonne N du résultat de la requête. Si l'ordre des champs de la struct ne correspond pas à l'ordre réel des colonnes SQL, les mauvais octets se retrouvent dans les mauvais champs, causant des erreurs de type à la désérialisation même si le nombre de colonnes est correct.

Symptômes

  • HTTP 500 sur une variante de tri (p. ex. sort=trending, sort=popular) mais pas sur d'autres (p. ex. sort=recent) qui interrogent une vue ou table différente
  • Erreur du type : "cannot decode Float64 from String" ou autre erreur de type
  • Le nombre de colonnes est correct (pas d'erreur « not enough data »)
  • Le bug apparaît après l'ajout d'une colonne calculée à une vue existante

Explication de la cause racine

Supposons que trending_videos soit définie comme :

CREATE VIEW trending_videos AS
SELECT
    *,                                          -- toutes les colonnes de base
    (views * 0.5 + likes * 2.0) AS trending_score  -- colonne calculée AJOUTÉE ICI
FROM video_stats;

Une requête externe effectue ensuite :

SELECT
    tv.*,                   -- se développe en : base_cols..., trending_score
    text_track_ref,         -- les colonnes de sous-titre viennent APRÈS
    text_track_content
FROM trending_videos tv
LEFT JOIN subtitle_subquery USING (id);

L'ordre réel des colonnes du résultat SQL est :

[...base_cols, trending_score, text_track_ref, text_track_content]

Mais si la struct Rust a été écrite avec les champs de sous-titre avant trending_score :

pub struct TrendingVideo {
    // ...champs de base...
    pub text_track_ref: String,      // position N   <- FAUX : reçoit les octets de trending_score
    pub text_track_content: String,  // position N+1 <- FAUX : reçoit les octets de text_track_ref
    pub trending_score: f64,         // position N+2 <- FAUX : reçoit les octets de text_track_content
}

ClickHouse envoie un Float64 alors que Rust attend une String → erreur de désérialisation → 500.

Pourquoi seules certaines requêtes échouent

La variante sort=recent interroge directement video_stats avec vs.* — cette vue n'a pas de colonnes calculées supplémentaires, donc les colonnes de sous-titre arrivent à la même position relative que la struct s'y attend. Seule la vue trending_videos ajoute trending_score à l'intérieur de vs.*, décalant tout ce qui suit.

Étapes de débogage

  1. Identifiez quelles variantes de requête échouent ou réussissent. Les défaillantes utilisent probablement une vue différente ou ont une source SELECT * différente.

  2. *Développez le `SELECT alias.` défaillant** — exécutez la requête de vue interne directement :

    curl -u 'user:pass' 'https://clickhouse-host:8443' \
      --data-binary "SELECT * FROM trending_videos LIMIT 0 FORMAT TabSeparated"

    Ou utilisez DESCRIBE TABLE trending_videos pour voir l'ordre des colonnes déclarées.

  3. Listez l'ordre réel des colonnes de la requête externe complète :

    curl ... --data-binary \
      "SELECT tv.*, '' as text_track_ref, '' as text_track_content
       FROM trending_videos tv LIMIT 0 FORMAT TabSeparatedWithNames"
  4. Comparez avec l'ordre des champs de la struct Rust ligne par ligne — ils doivent correspondre exactement.

  5. Trouvez le champ mal placé — recherchez les colonnes calculées ajoutées à la vue qui apparaissent à l'intérieur de alias.* mais après des colonnes qui apparaissent dans le SELECT externe.

Solution

Réorganisez les champs de la struct Rust pour correspondre à l'ordre réel des colonnes SQL — les colonnes VUE calculées doivent apparaître avant toute colonne ajoutée dans le SELECT externe :

pub struct TrendingVideo {
    // ...champs de base dans le même ordre que la table de base...

    // trending_score provient de tv.* (calculée par la vue), donc elle apparaît
    // AVANT les colonnes de sous-titre que nous ajoutons dans la requête externe
    pub trending_score: f64,

    // Les colonnes de sous-titre sont ajoutées après tv.* dans le SELECT externe
    pub text_track_ref: String,
    pub text_track_content: String,
}

NE modifiez PAS la requête SQL ou la vue — alignez simplement l'ordre des champs de la struct.

Règle clé

Quand une VUE ClickHouse ajoute une colonne calculée via SELECT *, expr AS col FROM base, cette colonne devient partie de la liste des colonnes de la vue. Tout SELECT alias.* dans une requête externe l'émettra dans l'ordre déclaré de la vue — AVANT toute colonne supplémentaire ajoutée après alias.* dans le SELECT externe. La struct Rust #[derive(Row)] doit refléter cet ordre exact.

Prévention

  • Quand vous ajoutez une colonne calculée à une vue ClickHouse, vérifiez immédiatement TOUTES les structs Rust qui interrogent cette vue avec SELECT alias.* et mettez à jour l'ordre des champs.
  • Ajoutez un test d'intégration qui interroge le point de terminaison affecté ; la CI détectera les écarts futurs d'ordre avant le staging.
  • Envisagez d'utiliser SELECT col1, col2, ..., trending_score, text_track_ref, text_track_content (liste explicite de colonnes au lieu de *) dans la requête externe pour rendre l'ordre explicite et immunisé contre les changements de schéma de vue.

Compétences connexes

  • clickhouse-rust-type-mismatches — couvre « not enough data » (erreur du NOMBRE de colonnes), encodage FixedString, problèmes de nullabilité Option vs String. Cette compétence couvre les erreurs d'ordre des colonnes (le nombre de colonnes est correct, les types correspondent, mais l'ordre positionnel est faux).

Références

Skills similaires